Merge pull request #8608 from sboddy/feature-svg-edge-classification-3668-4

Classify projection edges in SVG elevations (#3668)
This commit is contained in:
sboddy
2026-07-18 20:32:37 +01:00
committed by GitHub
11 changed files with 651 additions and 74 deletions
+22 -4
View File
@@ -24,10 +24,28 @@ 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; }
.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; }
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
/* 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.crease { stroke: black; stroke-width: 0.25; stroke-opacity: 0.85; }
path.sharp { stroke: black; stroke-width: 0.18; stroke-opacity: 0.7; }
path.flush { stroke: black; stroke-width: 0.1; stroke-opacity: 0.4; }
/* Debug CSS for troubleshooting edge classification */
/*
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
path.boundary { stroke: orange; stroke-width: 0.3; stroke-opacity: 0.9; }
path.crease { stroke: green; stroke-width: 0.25; stroke-opacity: 0.85; }
path.sharp { stroke: red; stroke-width: 0.18; stroke-opacity: 0.7; }
path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
*/
.surface {fill: white; stroke-width: 0.1;}
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
.PredefinedType-LINEWORK.dashed { stroke-dasharray: 3, 2; }
.PredefinedType-LINEWORK.fine { stroke-width: 0.18; stroke: #777777; }
@@ -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;
@@ -1316,6 +1316,18 @@ 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. Settings are
# per-drawing, stored in EPset_Drawing and read into self.cprops by import_camera_props.
try:
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
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
self.serialiser = ifcopenshell.geom.serializers.svg(
@@ -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):
+37 -1
View File
@@ -371,6 +371,42 @@ namespace ifcopenshell {
static constexpr double defaultvalue = -1.;
};
struct SvgRidgeAngleMinDegrees : public SettingBase<SvgRidgeAngleMinDegrees, double> {
static constexpr const char* const name = "svg-ridge-angle-min-degrees";
static constexpr const char* const description = "SVG edge classification (issue #3668): minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as 'sharp' rather than 'flush'.";
static constexpr double defaultvalue = 45.;
};
struct SvgValleyAngleMinDegrees : public SettingBase<SvgValleyAngleMinDegrees, double> {
static constexpr const char* const name = "svg-valley-angle-min-degrees";
static constexpr const char* const description = "SVG edge classification (issue #3668): minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as 'crease' rather than 'flush'.";
static constexpr double defaultvalue = 12.;
};
struct SvgEmitFlushEdges : public SettingBase<SvgEmitFlushEdges, bool> {
static constexpr const char* const name = "svg-emit-flush-edges";
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'flush' projection edges (dihedral deviation below both ridge/valley thresholds). Defaults to false, i.e. flush edges are omitted from the output.";
static constexpr bool defaultvalue = false;
};
struct SvgUseEdgeClassification : public SettingBase<SvgUseEdgeClassification, bool> {
static constexpr const char* const name = "svg-use-edge-classification";
static constexpr const char* const description = "SVG edge classification (issue #3668): enable the 5-class boundary/outline/sharp/crease/flush scheme. When false (the default), falls back to the original unclassified linework.";
static constexpr bool defaultvalue = false;
};
struct SvgRenderCreaseEdges : public SettingBase<SvgRenderCreaseEdges, bool> {
static constexpr const char* const name = "svg-render-crease-edges";
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'crease' (concave) projection edges. Only relevant when svg-use-edge-classification is enabled.";
static constexpr bool defaultvalue = true;
};
struct SvgRenderSharpEdges : public SettingBase<SvgRenderSharpEdges, bool> {
static constexpr const char* const name = "svg-render-sharp-edges";
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'sharp' (convex) projection edges. Only relevant when svg-use-edge-classification is enabled.";
static constexpr bool defaultvalue = true;
};
struct KeepBoundingBoxes : public SettingBase<KeepBoundingBoxes, bool> {
static constexpr const char* const name = "keep-bounding-boxes";
static constexpr const char* const description =
@@ -653,7 +689,7 @@ namespace ifcopenshell {
};
class Settings : public SettingsContainer<
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, SvgRidgeAngleMinDegrees, SvgValleyAngleMinDegrees, SvgEmitFlushEdges, SvgUseEdgeClassification, SvgRenderCreaseEdges, SvgRenderSharpEdges, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
>
{};
}
@@ -109,6 +109,12 @@ SETTING = Literal[
"reorient-shells",
"site-local-placement",
"surface-colour",
"svg-emit-flush-edges",
"svg-render-crease-edges",
"svg-render-sharp-edges",
"svg-ridge-angle-min-degrees",
"svg-use-edge-classification",
"svg-valley-angle-min-degrees",
"triangulation-type",
"unify-shapes",
"use-material-names",
+374 -45
View File
@@ -102,10 +102,16 @@
const double PI2 = M_PI * 2.;
bool SvgSerializer::ready() {
svg_ridge_angle_min_deg_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgRidgeAngleMinDegrees>().get();
svg_valley_angle_min_deg_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgValleyAngleMinDegrees>().get();
svg_emit_flush_edges_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgEmitFlushEdges>().get();
svg_use_edge_classification_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgUseEdgeClassification>().get();
svg_render_crease_edges_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgRenderCreaseEdges>().get();
svg_render_sharp_edges_ = geometry_settings().get<ifcopenshell::geometry::settings::SvgRenderSharpEdges>().get();
return true;
}
void SvgSerializer::write(path_object& p, const TopoDS_Shape& comp_or_wire, boost::optional<std::vector<double>> dash_array) {
void SvgSerializer::write(path_object& p, const TopoDS_Shape& comp_or_wire, boost::optional<std::vector<double>> dash_array, boost::optional<std::string> css_class) {
/* ShapeFix_Wire fix;
Handle(ShapeExtend_WireData) data = new ShapeExtend_WireData;
for (TopExp_Explorer edges(result, TopAbs_EDGE); edges.More(); edges.Next()) {
@@ -351,6 +357,12 @@ void SvgSerializer::write(path_object& p, const TopoDS_Shape& comp_or_wire, boos
if (!path.empty()) {
path.add("\"");
if (css_class) {
path.add(" class=\"");
path.add(*css_class);
path.add("\"");
}
if (dash_array) {
path.add(" stroke-dasharray=\"");
bool first = true;
@@ -622,7 +634,7 @@ void SvgSerializer::write(const IfcGeom::BRepElement* brep_obj) {
} else if (elevation_ref_guid_) {
is_elevation = *elevation_ref_guid_ == brep_obj->guid();
}
BRepBuilderAPI_Transform make_transform_global(compound_local, trsf, true);
make_transform_global.Build();
// (When determinant < 0, copy is implied and the input is not mutated.)
@@ -795,6 +807,182 @@ namespace {
}
}
namespace {
// SVG edge classification (issue #3668). See edge-classification.md at the repo root for
// the authoritative definition of the five classes and their evaluation order.
enum class edge_style_class { boundary, outline, sharp, crease, flush };
const char* edge_style_class_name(edge_style_class c) {
switch (c) {
case edge_style_class::boundary: return "boundary";
case edge_style_class::outline: return "outline";
case edge_style_class::sharp: return "sharp";
case edge_style_class::crease: return "crease";
default: return "flush";
}
}
// Outward face normal, accounting for face orientation. Only planar faces are supported;
// returns false otherwise (caller should conservatively treat the edge as an outline).
bool face_normal_from_planar_face(const TopoDS_Face& f, gp_Dir& out) {
auto s = BRep_Tool::Surface(f);
if (s->DynamicType() != STANDARD_TYPE(Geom_Plane)) {
return false;
}
auto p = Handle(Geom_Plane)::DownCast(s);
gp_Dir d = p->Axis().Direction();
if (f.Orientation() == TopAbs_REVERSED) {
d.Reverse();
}
out = d;
return true;
}
double clamp_dot(double v) {
if (v < -1.0) return -1.0;
if (v > 1.0) return 1.0;
return v;
}
edge_style_class classify_edge_from_faces(
const TopoDS_Edge& edge,
const NCollection_List<TopoDS_Shape>& faces,
const gp_Dir& projection_direction,
double ridge_angle_min_deg,
double valley_angle_min_deg
) {
std::vector<TopoDS_Face> faces_vec;
for (NCollection_List<TopoDS_Shape>::Iterator it(faces); it.More(); it.Next()) {
const TopoDS_Shape& s = it.Value();
if (s.ShapeType() == TopAbs_FACE) {
faces_vec.push_back(TopoDS::Face(s));
}
}
// Boundary: naked edge, or non-manifold (3+ faces) -- the latter is explicitly out of
// scope for the 5-class scheme (a geometry-health/QA concern), so fall back to the
// same conservative bucket rather than force-fitting it into outline/sharp/crease.
if (faces_vec.size() != 2) {
return edge_style_class::boundary;
}
const TopoDS_Face& f0 = faces_vec[0];
const TopoDS_Face& f1 = faces_vec[1];
gp_Dir n0, n1;
if (!face_normal_from_planar_face(f0, n0) || !face_normal_from_planar_face(f1, n1)) {
// Conservative fallback for non-planar-face edges.
return edge_style_class::outline;
}
// Note the negation: `projection_direction` (as constructed by the caller from the
// drawing plane's axis) points from the scene *towards the camera*, not into the scene.
// A face that's actually front-facing (visible, facing the viewer) has an outward normal
// pointing the same general way as that -- i.e. a *positive* dot product -- so negate
// here to get the more intuitive "front-facing is negative" convention used below.
// Confirmed against this feature's own real-world test scene: the SOUTH ELEVATION
// camera's placement matrix transforms local +Z (what the un-negated projection_direction
// is built from) to world (0, 1, 0), while the camera's actual Blender-convention view
// direction (local -Z) transforms to world (0, -1, 0) -- i.e. exactly opposite.
const double d0 = -projection_direction.Dot(n0);
const double d1 = -projection_direction.Dot(n1);
// Front/back/edge-on classification of each face relative to the view direction, using
// a tolerance band around zero rather than a bare sign comparison. A face at or near
// edge-on to the camera (|d| within the band) is common for regular/symmetric
// tessellations viewed from "nice" angles (icospheres, N-gon cylinder/cone
// approximations) and must count as outline on both its edges, not just the one that
// happens to pair it with a clearly front-facing neighbour.
constexpr double kOutlineDotEps = 1.e-5;
const bool front0 = d0 < -kOutlineDotEps;
const bool back0 = d0 > kOutlineDotEps;
const bool front1 = d1 < -kOutlineDotEps;
const bool back1 = d1 > kOutlineDotEps;
// Outline: silhouette, either a genuine front/back flip, or either face is at/near
// edge-on to the view direction (also covers both faces edge-on at once).
if (!(front0 && front1) && !(back0 && back1)) {
return edge_style_class::outline;
}
// Signed deviation from flat (0 degrees between outward normals = perfectly flat, i.e.
// coplanar faces have identical outward normals). Positive = convex (ridge/sharp),
// negative = concave (valley/crease).
//
// Sign via a position-based (not orientation-based) test: find a vertex of f1 that
// isn't one of the shared edge's own endpoints, and check which side of f0's plane it
// falls on. If it's behind f0's plane (opposite side from f0's outward normal), f1
// curves back towards the solid's interior relative to f0 -- a convex fold, like a box
// corner. This avoids relying on TopoDS_Edge/wire orientation semantics (which proved
// unreliable in practice: an earlier attempt using edge.Orientation() combined with
// cross(n0, n1) gave a self-consistent-looking but wrong sign on real BRep topology --
// verified against known-convex geometry, e.g. every edge of a convex icosphere, where
// that approach misclassified a majority of edges as concave).
double deviation_deg = std::acos(clamp_dot(n0.Dot(n1))) * 180.0 / M_PI;
TopoDS_Vertex ev0, ev1;
TopExp::Vertices(edge, ev0, ev1);
const gp_Pnt edge_p0 = BRep_Tool::Pnt(ev0);
const gp_Pnt edge_p1 = BRep_Tool::Pnt(ev1);
for (TopExp_Explorer vexp(f1, TopAbs_VERTEX); vexp.More(); vexp.Next()) {
const gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(vexp.Current()));
if (p.Distance(edge_p0) > Precision::Confusion() && p.Distance(edge_p1) > Precision::Confusion()) {
const bool convex = gp_Vec(edge_p0, p).Dot(gp_Vec(n0.XYZ())) < 0.0;
if (!convex) {
deviation_deg = -deviation_deg;
}
break;
}
}
// View-relative flip for folds seen from behind through an opening (e.g. the "Rotated
// Box w/Boundary" test object -- a box with one face removed; the 3 interior lines
// visible through the opening read as the *inside* of an ordinary convex box corner,
// which should look like a crease, not a sharp ridge). Two earlier unconditional
// versions of this flip (triggered on plain back0&&back1, with no further gate) were
// tried and reverted -- see edge-classification.md follow-up notes -- because they
// corrupted otherwise-correct classification broadly, manifesting as spurious `crease`
// edges on a fully-convex icosphere test case that has no opening at all.
//
// That corruption wasn't a fundamental inability to distinguish "genuinely seen through
// a hole" from "ordinary far side of closed geometry": bucket membership here is purely
// a post-hoc query key into an already-completed, correct HLR visibility computation,
// so reclassifying an edge can never make a genuinely hidden edge appear or vice versa.
// The real cause is a threshold-crossing artifact: near the silhouette, facet-normal
// noise on regular/symmetric tessellations (icospheres, N-gon cylinder/cone
// approximations) makes some genuinely near-edge-on facets test as "back" under the
// flat-normal-based back0/back1 test even though they're still visible. An unconditional
// negate then took their small, correctly-`flush` solid-relative deviation and re-tested
// it against the *other* threshold -- `ridge_angle_min_deg` (45 degrees by default) and
// `valley_angle_min_deg` (12 degrees by default) are deliberately asymmetric, so a gentle
// ~20 degree convex facet transition that safely sits under the ridge threshold crosses
// well over the much smaller valley threshold once flipped, becoming a spurious `crease`.
//
// Fix: gate the flip so it can only reinterpret a fold that would already be visible
// (sharp or crease) under its own pre-flip threshold -- i.e. only folds sharp/deep
// enough to draw from the front get reinterpreted as the opposite class from behind.
// Gentle tessellation-noise deviations that are correctly `flush` either way never cross
// the asymmetric threshold gap, because they never reach the flip at all. Verified
// against the full test scene: every object's classification is byte-for-byte unchanged
// except "Rotated Box w/Boundary", whose 3 interior lines now correctly read `crease`
// (previously all 4 non-boundary edges read `sharp`).
if (back0 && back1) {
const bool would_show_unflipped =
(deviation_deg >= 0.0) ? (deviation_deg >= ridge_angle_min_deg) : (-deviation_deg >= valley_angle_min_deg);
if (would_show_unflipped) {
deviation_deg = -deviation_deg;
}
}
if (deviation_deg >= 0.0) {
return (deviation_deg >= ridge_angle_min_deg) ? edge_style_class::sharp : edge_style_class::flush;
} else {
return (-deviation_deg >= valley_angle_min_deg) ? edge_style_class::crease : edge_style_class::flush;
}
}
}
void SvgSerializer::write(const geometry_data& data) {
std::vector<section_data> section_heights_storage;
const std::vector<section_data>* section_heights_used = &section_heights_storage;
@@ -1208,6 +1396,92 @@ void SvgSerializer::write(const geometry_data& data) {
}
}
// SVG edge classification (issue #3668): classify *compound_to_hlr's edges (real
// face topology, pre-HLR) into per-class edge-only sub-compounds. The full shape
// is still registered via add()/it->second.add() below, unchanged, for correct
// occlusion; these buckets only affect which class each edge's visible portion is
// later extracted as (see hlr_calc::extract() in SvgSerializer.h).
//
// Gated behind svg_use_edge_classification_ (default false): the whole block must
// be skipped, not just individually suppressed per-edge, so that when disabled
// classified_edge_buckets stays empty for *every* product in the document, not
// just this one. hlr_calc::extract() only takes the classified-buckets branch
// when its shared classified_shapes_ list is non-empty; if even one product added
// classified buckets while others didn't, those others would silently fall back
// to unclassified linework while this one used classification, an inconsistent
// mix. Leaving classified_edge_buckets empty here means add_classified_edges() is
// never called for this product either, so every product uniformly falls through
// to the pre-existing product_shapes_ fallback -- the original, pre-classification
// linework.
std::map<std::string, TopoDS_Compound> classified_edge_buckets;
if (svg_use_edge_classification_) {
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> edge_face_map;
TopExp::MapShapesAndAncestors(*compound_to_hlr, TopAbs_EDGE, TopAbs_FACE, edge_face_map);
gp_Dir view_dir;
try {
view_dir = gp_Dir(projection_direction);
} catch (const Standard_Failure&) {
view_dir = gp::DZ();
}
BRep_Builder BBcls;
for (int i = 1; i <= edge_face_map.Extent(); ++i) {
const TopoDS_Edge& cls_edge = TopoDS::Edge(edge_face_map.FindKey(i));
edge_style_class cls = edge_style_class::outline;
try {
cls = classify_edge_from_faces(cls_edge, edge_face_map.FindFromIndex(i), view_dir, svg_ridge_angle_min_deg_, svg_valley_angle_min_deg_);
} catch (const Standard_Failure& e) {
logger_.Warning("SER", 30, std::string("SVG edge classification OCC exception: ") + e.GetMessageString());
} catch (const std::exception& e) {
logger_.Warning("SER", 31, std::string("SVG edge classification exception: ") + e.what());
}
if (cls == edge_style_class::flush && !svg_emit_flush_edges_) {
continue;
}
if (cls == edge_style_class::crease && !svg_render_crease_edges_) {
continue;
}
if (cls == edge_style_class::sharp && !svg_render_sharp_edges_) {
continue;
}
std::string name = edge_style_class_name(cls);
auto bucket_it = classified_edge_buckets.find(name);
if (bucket_it == classified_edge_buckets.end()) {
TopoDS_Compound c;
BBcls.MakeCompound(c);
bucket_it = classified_edge_buckets.emplace(name, c).first;
}
BBcls.Add(bucket_it->second, cls_edge);
}
// Non-planar faces (e.g. a real analytic cylindrical wall from a
// circular-profile column/pile, swept via BRepPrimAPI_MakePrism rather than
// faceted) have a silhouette that HLR synthesizes on the fly -- it is not a
// pre-existing topological edge, so the edge-only loop above can never bucket
// it. OutLineVCompound(S) correlates a curved face's silhouette by the
// identity of the originating *face*, not any edge, so add the non-planar
// face itself into the outline bucket alongside whatever edges it already
// contributed (top/bottom/seam), giving HLR's per-face OutLine reconstruction
// something to match against.
for (TopExp_Explorer fexp(*compound_to_hlr, TopAbs_FACE); fexp.More(); fexp.Next()) {
const TopoDS_Face& f = TopoDS::Face(fexp.Current());
if (BRep_Tool::Surface(f)->DynamicType() != STANDARD_TYPE(Geom_Plane)) {
std::string name = edge_style_class_name(edge_style_class::outline);
auto bucket_it = classified_edge_buckets.find(name);
if (bucket_it == classified_edge_buckets.end()) {
TopoDS_Compound c;
BBcls.MakeCompound(c);
bucket_it = classified_edge_buckets.emplace(name, c).first;
}
BBcls.Add(bucket_it->second, f);
}
}
}
if (is_floor_plan_) {
if (storey) {
auto it = storey_hlr.find(storey);
@@ -1215,11 +1489,17 @@ void SvgSerializer::write(const geometry_data& data) {
it = storey_hlr.insert({ storey, hlr_t(logger_, use_prefiltering_, use_hlr_poly_, segment_projection_, projection_plane) }).first;
}
it->second.add(*compound_to_hlr, data.product);
for (auto& kv : classified_edge_buckets) {
it->second.add_classified_edges(data.product, kv.first, kv.second);
}
} else {
logger_.Warning("SER", 28, "Unable to invoke HLR due to absence of storey containment", data.product);
}
} else if (hlr) {
hlr->add(*compound_to_hlr, data.product);
for (auto& kv : classified_edge_buckets) {
hlr->add_classified_edges(data.product, kv.first, kv.second);
}
}
}
}
@@ -1767,49 +2047,63 @@ std::array<std::array<double, 3>, 3> SvgSerializer::resize() {
}
void SvgSerializer::draw_hlr(const gp_Pln& pln, const drawing_key& drawing_name) {
auto hlr_items = (drawing_name.first ? this->storey_hlr.find(drawing_name.first)->second : *hlr).build();
hlr_t& hlr_source = drawing_name.first ? this->storey_hlr.find(drawing_name.first)->second : *hlr;
auto hlr_items = hlr_source.build();
for (auto& p : hlr_items) {
const TopoDS_Shape& hlr_compound_unmirrored = p.second;
// SVG edge classification (issue #3668): each item's class is already known -- it was
// determined pre-HLR from real face topology (see the classified_edge_buckets block in
// write(const geometry_data&)) and threaded through via hlr_calc::extract(). No post-hoc
// lookup against HLR's own (face-less) output is needed. Multiple items can share the same
// product (one per non-empty class bucket); keep a single path_object/group per product so
// per-path classes survive Bonsai's merge_linework_and_add_metadata untouched, rather than
// creating a <g> per class (see plan notes on why that clobbers classes in Python).
std::map<const IfcUtil::IfcBaseEntity*, path_object*> group_by_product;
if (!hlr_compound_unmirrored.IsNull()) {
// Compound 3D curves for mirroring to work
ShapeFix_Edge sfe;
TopExp_Explorer exp(hlr_compound_unmirrored, TopAbs_EDGE);
for (; exp.More(); exp.Next()) {
sfe.FixAddCurve3d(TopoDS::Edge(exp.Current()));
for (auto& item : hlr_items) {
const IfcUtil::IfcBaseEntity* product = std::get<0>(item);
const std::string& cls = std::get<1>(item);
const TopoDS_Shape& hlr_compound_unmirrored = std::get<2>(item);
if (hlr_compound_unmirrored.IsNull()) {
continue;
}
// Compound 3D curves for mirroring to work
ShapeFix_Edge sfe;
TopExp_Explorer exp(hlr_compound_unmirrored, TopAbs_EDGE);
for (; exp.More(); exp.Next()) {
sfe.FixAddCurve3d(TopoDS::Edge(exp.Current()));
}
// Mirror to match SVG coord system.
// @todo this is very wasteful. We better do the Y-mirror in the SVG writing and
// not on the TopoDS_Shape input.
TopoDS_Shape hlr_compound;
if (drawing_name.first == nullptr) {
gp_Trsf trsf_mirror;
if (!mirror_y_) {
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
}
// Mirror to match SVG coord system.
// @todo this is very wasteful. We better do the Y-mirror in the SVG writing and
// not on the TopoDS_Shape input.
TopoDS_Shape hlr_compound;
if (drawing_name.first == nullptr) {
gp_Trsf trsf_mirror;
if (!mirror_y_) {
trsf_mirror.SetMirror(gp_Ax2(gp::Origin(), gp::DY()));
}
if (mirror_x_) {
gp_Trsf mirror_x;
mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX()));
trsf_mirror.PreMultiply(mirror_x);
}
BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true);
make_transform_mirror.Build();
hlr_compound = make_transform_mirror.Shape();
} else {
// In case of building storey-based floor plan the mirroring has already
// been taken into account before projection.
hlr_compound = hlr_compound_unmirrored;
if (mirror_x_) {
gp_Trsf mirror_x;
mirror_x.SetMirror(gp_Ax2(gp::Origin(), gp::DX()));
trsf_mirror.PreMultiply(mirror_x);
}
BRepBuilderAPI_Transform make_transform_mirror(hlr_compound_unmirrored, trsf_mirror, true);
make_transform_mirror.Build();
hlr_compound = make_transform_mirror.Shape();
} else {
// In case of building storey-based floor plan the mirroring has already
// been taken into account before projection.
hlr_compound = hlr_compound_unmirrored;
}
exp.Init(hlr_compound, TopAbs_EDGE);
BRep_Builder B;
path_object* po;
path_object*& po = group_by_product[product];
if (!po) {
std::string name;
if (p.first) {
name = nameElement(p.first);
if (product) {
name = nameElement(product);
boost::replace_all(name, "class=\"", "class=\"projection ");
} else {
name = "class=\"projection\"";
@@ -1819,13 +2113,19 @@ void SvgSerializer::draw_hlr(const gp_Pln& pln, const drawing_key& drawing_name)
} else {
po = &start_path(pln, drawing_name.second, name);
}
for (; exp.More(); exp.Next()) {
TopoDS_Wire w;
B.MakeWire(w);
B.Add(w, exp.Current());
write(*po, w);
}
}
boost::optional<std::string> css_class;
if (!cls.empty()) {
css_class = cls;
}
BRep_Builder B;
for (TopExp_Explorer exp_mirrored(hlr_compound, TopAbs_EDGE); exp_mirrored.More(); exp_mirrored.Next()) {
TopoDS_Wire w;
B.MakeWire(w);
B.Add(w, exp_mirrored.Current());
write(*po, w, boost::none, css_class);
}
}
}
@@ -2236,6 +2536,35 @@ void SvgSerializer::doWriteHeader() {
" fill: none;\n"
" stroke-opacity: 0.6;\n"
" }\n"
// SVG edge classification (issue #3668) -- see edge-classification.md. These
// select directly on the <path> element (each classified edge carries its own
// class), not on an ancestor <g>, so they win over the inherited .projection
// path rule above regardless of specificity.
" path.outline {\n"
" stroke: #000000;\n"
" stroke-width: 0.35px;\n"
" stroke-opacity: 1;\n"
" }\n"
" path.boundary {\n"
" stroke: #000000;\n"
" stroke-width: 0.3px;\n"
" stroke-opacity: 0.9;\n"
" }\n"
" path.sharp {\n"
" stroke: #000000;\n"
" stroke-width: 0.25px;\n"
" stroke-opacity: 0.85;\n"
" }\n"
" path.crease {\n"
" stroke: #000000;\n"
" stroke-width: 0.18px;\n"
" stroke-opacity: 0.7;\n"
" }\n"
" path.flush {\n"
" stroke: #000000;\n"
" stroke-width: 0.1px;\n"
" stroke-opacity: 0.4;\n"
" }\n"
" .IfcDoor path,\n"
" .Symbol path {\n"
" fill: none;\n"
+73 -23
View File
@@ -56,6 +56,7 @@
#include <string>
#include <limits>
#include <array>
#include <tuple>
typedef std::pair<const IfcUtil::IfcBaseEntity*, std::string> drawing_key;
@@ -212,9 +213,16 @@ namespace {
private:
const HLRAlgo_Projector& projector_;
const std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>>* product_shapes_ = nullptr;
// SVG edge classification (issue #3668): per-(product, class) edge-only sub-shapes,
// classified pre-HLR on the original (real-face) topology. Queried via
// VCompound(S)/OutLineVCompound(S), which correlate by the identity of the *original*
// edges added to the algorithm -- not by the reconstructed output -- so this works even
// though HLR's own output compounds carry no face topology at all. Empty class string
// means "unclassified" (used for the two fallback cases below).
const std::list<std::tuple<const IfcUtil::IfcBaseEntity*, std::string, TopoDS_Shape>>* classified_shapes_ = nullptr;
public:
typedef std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>> result_type;
typedef std::list<std::tuple<const IfcUtil::IfcBaseEntity*, std::string, TopoDS_Shape>> result_type;
hlr_calc(const HLRAlgo_Projector& projector) : projector_(projector)
{}
@@ -223,24 +231,37 @@ namespace {
product_shapes_ = product_shapes;
}
void set_classified_shapes(const std::list<std::tuple<const IfcUtil::IfcBaseEntity*, std::string, TopoDS_Shape>>* classified_shapes) {
classified_shapes_ = classified_shapes;
}
result_type operator()(boost::blank&) const {
throw std::runtime_error("");
}
template <typename HlrToShapeT>
result_type extract(HlrToShapeT& hlr_shapes) {
result_type r;
if (classified_shapes_ && !classified_shapes_->empty()) {
for (auto& t : *classified_shapes_) {
r.push_back({ std::get<0>(t), std::get<1>(t), occt_join(hlr_shapes.OutLineVCompound(std::get<2>(t)), hlr_shapes.VCompound(std::get<2>(t))) });
}
} else if (product_shapes_) {
for (auto& p : *product_shapes_) {
r.push_back({ p.first, std::string(), occt_join(hlr_shapes.OutLineVCompound(p.second), hlr_shapes.VCompound(p.second)) });
}
} else {
r.push_back({ nullptr, std::string(), occt_join(hlr_shapes.OutLineVCompound(), hlr_shapes.VCompound()) });
}
return r;
}
result_type operator()(opencascade::handle<HLRBRep_Algo>& algo) {
algo->Projector(projector_);
algo->Update();
algo->Hide();
HLRBRep_HLRToShape hlr_shapes(algo);
if (product_shapes_) {
std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>> r;
for (auto& p : *product_shapes_) {
r.push_back({ p.first, occt_join(hlr_shapes.OutLineVCompound(p.second), hlr_shapes.VCompound(p.second)) });
}
return r;
} else {
return { {nullptr, occt_join(hlr_shapes.OutLineVCompound(), hlr_shapes.VCompound())}};
}
return extract(hlr_shapes);
}
result_type operator()(opencascade::handle<HLRBRep_PolyAlgo>& algo) {
@@ -248,15 +269,7 @@ namespace {
algo->Update();
HLRBRep_PolyHLRToShape hlr_shapes;
hlr_shapes.Update(algo);
if (product_shapes_) {
std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>> r;
for (auto& p : *product_shapes_) {
r.push_back({ p.first, occt_join(hlr_shapes.OutLineVCompound(p.second), hlr_shapes.VCompound(p.second)) });
}
return r;
} else {
return { {nullptr, occt_join(hlr_shapes.OutLineVCompound(), hlr_shapes.VCompound()) } };
}
return extract(hlr_shapes);
}
};
@@ -367,6 +380,8 @@ namespace {
std::multimap<double, face_info> large_ortho_faces_;
std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>> items_;
// SVG edge classification (issue #3668): see add_classified_edges().
std::list<std::tuple<const IfcUtil::IfcBaseEntity*, std::string, TopoDS_Shape>> classified_items_;
Logger& logger_;
@@ -391,6 +406,16 @@ namespace {
projector_ = HLRAlgo_Projector(trsf, false, 1.);
}
// SVG edge classification (issue #3668): register an edge-only sub-shape of `product`'s
// original (pre-HLR, real-face) geometry under a given class name (e.g. "outline",
// "sharp"). The full shape must still be added via add() as usual for correct occlusion;
// this only affects which *class* each edge's visible portion is later extracted as, via
// HLRBRep_HLRToShape::VCompound(S)/OutLineVCompound(S) in hlr_calc, which correlate by the
// identity of the original edges within S.
void add_classified_edges(const IfcUtil::IfcBaseEntity* product, const std::string& cls, const TopoDS_Shape& edges) {
classified_items_.push_back({ product, cls, edges });
}
bool is_obscured_(TopoDS_Shape* sit) {
const TopoDS_Shape& s = *sit;
@@ -510,7 +535,7 @@ namespace {
}
}
std::list<std::pair<const IfcUtil::IfcBaseEntity*, TopoDS_Shape>> build() {
std::list<std::tuple<const IfcUtil::IfcBaseEntity*, std::string, TopoDS_Shape>> build() {
size_t n_included = 0;
for (auto it = items_.begin(); it != items_.end(); ++it) {
if (!use_prefiltering_ || !is_obscured_(&it->second)) {
@@ -522,11 +547,12 @@ namespace {
if (use_prefiltering_) {
logger_.Notice("SER", 35, "Included " + std::to_string(n_included) + " elements out of " + std::to_string(items_.size()) + " after prefiltering");
}
hlr_calc vis(projector_);
if (segment_projection_) {
vis.set_product_shape(&items_);
}
vis.set_classified_shapes(&classified_items_);
return boost::apply_visitor(vis, engine_);
}
};
@@ -570,6 +596,14 @@ protected:
int profile_threshold_;
// SVG edge classification (issue #3668): see classify_edge_from_faces() in SvgSerializer.cpp.
double svg_ridge_angle_min_deg_;
double svg_valley_angle_min_deg_;
bool svg_emit_flush_edges_;
bool svg_use_edge_classification_;
bool svg_render_crease_edges_;
bool svg_render_sharp_edges_;
IfcParse::IfcFile* file;
const IfcUtil::IfcBaseEntity* storey_;
std::multimap<drawing_key, path_object, storey_sorter> paths;
@@ -623,6 +657,12 @@ public:
, mirror_x_(false)
, unify_inputs_(false)
, profile_threshold_(-1)
, svg_ridge_angle_min_deg_(45.)
, svg_valley_angle_min_deg_(12.)
, svg_emit_flush_edges_(false)
, svg_use_edge_classification_(false)
, svg_render_crease_edges_(true)
, svg_render_sharp_edges_(true)
, file(0)
, storey_(0)
, xcoords_begin(0)
@@ -631,7 +671,17 @@ public:
, hlr(nullptr)
, namespace_prefix_("data-")
, subtraction_settings_(ON_SLABS_AT_FLOORPLANS)
{}
{
// ready() only reads geometry_settings() (already valid at this point, since the base
// WriteOnlyGeometrySerializer initializer above has run) and has no other side effects,
// so it's safe to call here. This is needed because ready() is otherwise only invoked
// explicitly by IfcConvert.cpp's CLI driver -- callers that construct this serializer
// directly via the Python bindings (e.g. Bonsai's drawing generation, which never calls
// a ready()-equivalent because it isn't exposed via SWIG) would otherwise silently keep
// every settings::Svg* member at its hardcoded constructor default forever, regardless
// of what ifcopenshell.geom.settings().set(...) was actually configured to.
ready();
}
void addXCoordinate(const boost::shared_ptr<util::string_buffer::float_item>& fi) { xcoords.push_back(fi); }
void addYCoordinate(const boost::shared_ptr<util::string_buffer::float_item>& fi) { ycoords.push_back(fi); }
void addSizeComponent(const boost::shared_ptr<util::string_buffer::float_item>& fi) { radii.push_back(fi); }
@@ -641,7 +691,7 @@ public:
bool ready();
void write(const IfcGeom::TriangulationElement* /*o*/) {}
void write(const IfcGeom::BRepElement* o);
void write(path_object& p, const TopoDS_Shape& wire, boost::optional<std::vector<double>> dash_array=boost::none);
void write(path_object& p, const TopoDS_Shape& wire, boost::optional<std::vector<double>> dash_array=boost::none, boost::optional<std::string> css_class=boost::none);
void write(const geometry_data& data);
path_object& start_path(const gp_Pln& p, const IfcUtil::IfcBaseEntity* storey, const std::string& id);
path_object& start_path(const gp_Pln& p, const std::string& drawing_name, const std::string& id);