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 <noreply@anthropic.com>
This commit is contained in:
Stephen Boddy
2026-07-16 15:29:44 +01:00
parent 183e4c47f7
commit f3a7a35acf
6 changed files with 135 additions and 30 deletions
@@ -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;
@@ -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
@@ -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",
@@ -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()
+18
View File
@@ -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:
+45
View File
@@ -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):