From f3a7a35acfdb14118f069d63cc25de2cf932cf4c Mon Sep 17 00:00:00 2001 From: Stephen Boddy Date: Thu, 16 Jul 2026 15:29:44 +0100 Subject: [PATCH] Expose SVG edge classification settings in drawing UI Add UseEdgeClassification, RenderCreases, ValleyAngleMinDegrees, RenderSharp, RidgeAngleMinDegrees, and RenderFlush to EPset_Drawing, following the existing HasUnderlay/DPI/PerspectiveShiftX pattern. The master toggle defaults off, preserving current linework output; the three dependent controls only show in the panel once it's on. Removes the previous dormant, transient operator-redo properties for the ridge/valley thresholds and flush-edge toggle, which were never persisted per-drawing or exposed in any panel, replacing them with the persistent camera properties read in setup_serialiser(). Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Sonnet 5 --- .../bonsai/bim/data/pset/EPset_Drawing.ifc | 8 +++- .../bonsai/bim/module/drawing/operator.py | 37 ++++----------- src/bonsai/bonsai/bim/module/drawing/prop.py | 44 ++++++++++++++++++ src/bonsai/bonsai/bim/module/drawing/ui.py | 13 ++++++ src/bonsai/bonsai/tool/drawing.py | 18 ++++++++ src/bonsai/test/tool/test_drawing.py | 45 +++++++++++++++++++ 6 files changed, 135 insertions(+), 30 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc index 114b766ff8..4d8bc1ad47 100644 --- a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc +++ b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc @@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D FILE_SCHEMA(('IFC4')); ENDSEC; DATA; -#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2)); +#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31,#32,#33,#34,#35,#36)); #2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); @@ -35,5 +35,11 @@ DATA; #28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); #30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#31=IFCSIMPLEPROPERTYTEMPLATE('1cFVJnqT13m8ItkMHaI1tp',$,'UseEdgeClassification','Enable the boundary/outline/sharp/crease/flush SVG edge classification scheme (issue #3668). When false, drawings use the original unclassified linework.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#32=IFCSIMPLEPROPERTYTEMPLATE('2kB$mxBgnBUvhjh0Ti0c4P',$,'RenderCreases','Whether to render ''crease'' (concave) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#33=IFCSIMPLEPROPERTYTEMPLATE('3MSIJNW$T8r9Hl12kk0BY$',$,'ValleyAngleMinDegrees','Minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as ''crease'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#34=IFCSIMPLEPROPERTYTEMPLATE('2epSGfC4bFM9gb1X7zBIp4',$,'RenderSharp','Whether to render ''sharp'' (convex) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#35=IFCSIMPLEPROPERTYTEMPLATE('3TZwsEjkr5WRDKcgrYzSIA',$,'RidgeAngleMinDegrees','Minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as ''sharp'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#36=IFCSIMPLEPROPERTYTEMPLATE('2Jua$lO754vgZOkBoHM2gA',$,'RenderFlush','Whether to render ''flush'' edges (dihedral deviation below both ridge/valley thresholds). Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); ENDSEC; END-ISO-10303-21; diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 9a8faf4d58..b6017f0704 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -261,36 +261,11 @@ class CreateDrawing(bpy.types.Operator): description="Could save some time if you're sure IFC and current Blender session are already in sync", default=True, ) - svg_ridge_angle_min_deg: bpy.props.FloatProperty( - name="Ridge Angle Minimum", - description="Minimum convex dihedral deviation from flat, in degrees, for a projection " - "edge to be classified as 'sharp' rather than 'flush'. See edge-classification.md", - default=45.0, - min=0.0, - max=180.0, - ) - svg_valley_angle_min_deg: bpy.props.FloatProperty( - name="Valley Angle Minimum", - description="Minimum concave dihedral deviation from flat, in degrees, for a projection " - "edge to be classified as 'crease' rather than 'flush'. See edge-classification.md", - default=12.0, - min=0.0, - max=180.0, - ) - svg_emit_flush_edges: bpy.props.BoolProperty( - name="Emit Flush Edges", - description="Include projection edges whose dihedral deviation is below both the ridge " - "and valley thresholds (class 'flush'). Omitted by default", - default=False, - ) if TYPE_CHECKING: print_all: bool open_viewer: bool sync: bool - svg_ridge_angle_min_deg: float - svg_valley_angle_min_deg: float - svg_emit_flush_edges: bool drawing_name: str is_manifold_cache: dict[str, bool] @@ -1334,11 +1309,15 @@ class CreateDrawing(bpy.types.Operator): self.svg_settings = ifcopenshell.geom.settings() self.svg_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) self.svg_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE) - # SVG edge classification (issue #3668). See edge-classification.md. + # SVG edge classification (issue #3668). See edge-classification.md. Settings are + # per-drawing, stored in EPset_Drawing and read into self.cprops by import_camera_props. try: - self.svg_settings.set("svg-ridge-angle-min-degrees", self.svg_ridge_angle_min_deg) - self.svg_settings.set("svg-valley-angle-min-degrees", self.svg_valley_angle_min_deg) - self.svg_settings.set("svg-emit-flush-edges", self.svg_emit_flush_edges) + self.svg_settings.set("svg-use-edge-classification", self.cprops.use_edge_classification) + self.svg_settings.set("svg-render-crease-edges", self.cprops.render_creases) + self.svg_settings.set("svg-valley-angle-min-degrees", self.cprops.valley_angle_min_degrees) + self.svg_settings.set("svg-render-sharp-edges", self.cprops.render_sharp) + self.svg_settings.set("svg-ridge-angle-min-degrees", self.cprops.ridge_angle_min_degrees) + self.svg_settings.set("svg-emit-flush-edges", self.cprops.render_flush) except Exception: # Backwards compatibility with older ifcopenshell builds that don't expose these keys. pass diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index f75de7fd34..f22d7128d8 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -536,6 +536,50 @@ class BIMCameraProperties(PropertyGroup): default=True, update=get_update_layer_callback("has_annotation", "HasAnnotation"), ) + use_edge_classification: BoolProperty( + name="Use Edge Classification", + description="Classify projection edges into boundary/outline/sharp/crease/flush " + "instead of drawing all linework identically. See edge-classification.md", + default=False, + update=get_update_layer_callback("use_edge_classification", "UseEdgeClassification"), + ) + render_creases: BoolProperty( + name="Render Creases", + description="Render 'crease' (concave) projection edges", + default=True, + update=get_update_layer_callback("render_creases", "RenderCreases"), + ) + valley_angle_min_degrees: FloatProperty( + name="Valley Angle Minimum", + description="Minimum concave dihedral deviation from flat, in degrees, for a projection " + "edge to be classified as 'crease' rather than 'flush'", + default=12.0, + min=0.0, + max=180.0, + update=get_update_layer_callback("valley_angle_min_degrees", "ValleyAngleMinDegrees"), + ) + render_sharp: BoolProperty( + name="Render Sharp", + description="Render 'sharp' (convex) projection edges", + default=True, + update=get_update_layer_callback("render_sharp", "RenderSharp"), + ) + ridge_angle_min_degrees: FloatProperty( + name="Ridge Angle Minimum", + description="Minimum convex dihedral deviation from flat, in degrees, for a projection " + "edge to be classified as 'sharp' rather than 'flush'", + default=45.0, + min=0.0, + max=180.0, + update=get_update_layer_callback("ridge_angle_min_degrees", "RidgeAngleMinDegrees"), + ) + render_flush: BoolProperty( + name="Render Flush", + description="Render 'flush' projection edges (dihedral deviation below both ridge/valley " + "thresholds). Omitted by default", + default=False, + update=get_update_layer_callback("render_flush", "RenderFlush"), + ) target_view: EnumProperty( name="Target View", default="PLAN_VIEW", diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index e0df93a4a6..640ebf91a3 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -113,6 +113,19 @@ class BIM_PT_camera(Panel): row.prop(props, "fill_mode") row = self.layout.row() row.prop(props, "cut_mode") + + row = self.layout.row() + row.prop(props, "use_edge_classification") + if props.use_edge_classification: + row = self.layout.row() + row.prop(props, "render_creases") + row.prop(props, "valley_angle_min_degrees") + row = self.layout.row() + row.prop(props, "render_sharp") + row.prop(props, "ridge_angle_min_degrees") + row = self.layout.row() + row.prop(props, "render_flush") + row = self.layout.row() row.prop(props, "width") row = self.layout.row() diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 7493b02d33..8b6ca68b0e 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1072,6 +1072,12 @@ class Drawing(bonsai.core.tool.Drawing): camera_props.has_annotation = True camera_props.target_view = "PLAN_VIEW" camera_props.is_nts = False + camera_props.use_edge_classification = False + camera_props.render_creases = True + camera_props.valley_angle_min_degrees = 12.0 + camera_props.render_sharp = True + camera_props.ridge_angle_min_degrees = 45.0 + camera_props.render_flush = False camera.shift_x = 0.0 camera.shift_y = 0.0 @@ -1101,6 +1107,18 @@ class Drawing(bonsai.core.tool.Drawing): camera_props.has_annotation = bool(pset["HasAnnotation"]) if "IsNTS" in pset: camera_props.is_nts = bool(pset["IsNTS"]) + if "UseEdgeClassification" in pset: + camera_props.use_edge_classification = bool(pset["UseEdgeClassification"]) + if "RenderCreases" in pset: + camera_props.render_creases = bool(pset["RenderCreases"]) + if "ValleyAngleMinDegrees" in pset: + camera_props.valley_angle_min_degrees = float(pset["ValleyAngleMinDegrees"]) + if "RenderSharp" in pset: + camera_props.render_sharp = bool(pset["RenderSharp"]) + if "RidgeAngleMinDegrees" in pset: + camera_props.ridge_angle_min_degrees = float(pset["RidgeAngleMinDegrees"]) + if "RenderFlush" in pset: + camera_props.render_flush = bool(pset["RenderFlush"]) if "DPI" in pset: camera_props.dpi = int(pset["DPI"]) if "LineworkMode" in pset: diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 9d69fe51f6..a14b4d9d79 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -112,6 +112,51 @@ class TestImportCameraProps(NewFile): assert camera.shift_x == 0.0 assert camera.shift_y == 0.0 + def test_defaults_edge_classification_props_when_pset_is_absent(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + camera = bpy.data.cameras.new("Camera") + + subject.import_camera_props(drawing, camera) + + props = subject.get_camera_props(camera) + assert props.use_edge_classification is False + assert props.render_creases is True + assert props.valley_angle_min_degrees == pytest.approx(12.0) + assert props.render_sharp is True + assert props.ridge_angle_min_degrees == pytest.approx(45.0) + assert props.render_flush is False + + def test_imports_edge_classification_props_from_drawing_pset(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + drawing = ifc.createIfcAnnotation(ObjectType="DRAWING") + pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing") + ifcopenshell.api.pset.edit_pset( + ifc, + pset=pset, + properties={ + "UseEdgeClassification": True, + "RenderCreases": False, + "ValleyAngleMinDegrees": 8.0, + "RenderSharp": False, + "RidgeAngleMinDegrees": 30.0, + "RenderFlush": True, + }, + ) + camera = bpy.data.cameras.new("Camera") + + subject.import_camera_props(drawing, camera) + + props = subject.get_camera_props(camera) + assert props.use_edge_classification is True + assert props.render_creases is False + assert props.valley_angle_min_degrees == pytest.approx(8.0) + assert props.render_sharp is False + assert props.ridge_angle_min_degrees == pytest.approx(30.0) + assert props.render_flush is True + class TestSyncPerspectiveCameraShifts(NewFile): def test_round_trips_perspective_camera_shifts_through_drawing_pset(self):