Classify projection edges in SVG elevations

Adds boundary/outline/sharp/crease/flush classification of HLR
projection edges in SvgSerializer, so CSS can style silhouettes,
ridges, and valleys differently instead of drawing every edge
identically (fixes the "ugly faceted sphere" problem from #3668).

Classification happens pre-HLR on the original solid's real face
topology (three prior attempts tried to classify HLR's own output,
which carries no face topology at all and can't be correlated back
by edge identity). Each class's visible portion is then extracted via
HLRBRep_HLRToShape::VCompound(S)/OutLineVCompound(S), the same
per-shape filtering mechanism already used for per-product
segmentation, applied per class instead. Classes are tagged directly
on individual <path> elements so Bonsai's merge_linework_and_add_metadata
group-level class rewrite in operator.py never touches them.

New settings: svg-ridge-angle-min-degrees, svg-valley-angle-min-degrees,
svg-emit-flush-edges (ConversionSettings.h), wired through Bonsai's
CreateDrawing operator and exposed via its redo panel.

Refs #3668.

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-15 06:28:02 +01:00
parent 71a598e63a
commit f0970b90b0
5 changed files with 379 additions and 68 deletions
@@ -24,6 +24,14 @@ a text, a tspan { fill: blue !important; text-decoration: underline;}
a:hover { cursor: pointer; }
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
/* SVG edge classification (issue #3668): see edge-classification.md. These select directly on
the <path> element (each classified projection edge carries its own class), so they win over
the inherited .projection rule above regardless of specificity. */
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
path.boundary { stroke: black; stroke-width: 0.3; stroke-opacity: 0.9; }
path.sharp { stroke: black; stroke-width: 0.25; stroke-opacity: 0.85; }
path.crease { stroke: black; stroke-width: 0.18; stroke-opacity: 0.7; }
path.flush { stroke: black; stroke-width: 0.1; stroke-opacity: 0.4; }
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
@@ -261,11 +261,36 @@ 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]
@@ -1309,6 +1334,14 @@ 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.
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)
except Exception:
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
pass
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser = ifcopenshell.geom.serializers.svg(