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(
+19 -1
View File
@@ -371,6 +371,24 @@ 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 KeepBoundingBoxes : public SettingBase<KeepBoundingBoxes, bool> {
static constexpr const char* const name = "keep-bounding-boxes";
static constexpr const char* const description =
@@ -653,7 +671,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, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
>
{};
}
+263 -45
View File
@@ -102,10 +102,13 @@
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();
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 +354,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 +631,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 +804,115 @@ 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;
}
const double d0 = projection_direction.Dot(n0);
const double d1 = projection_direction.Dot(n1);
// Outline: silhouette, either against the background or self-occluding -- one face
// turns toward the viewer while the other turns away.
if ((d0 < 0.0) != (d1 < 0.0)) {
return edge_style_class::outline;
}
// Signed deviation from flat (180 degrees between outward normals = perfectly flat).
// Positive = convex (ridge/sharp), negative = concave (valley/crease). The sign comes
// from the rotation of n0 onto n1 about the edge tangent.
const double angle_between_normals_deg = std::acos(clamp_dot(n0.Dot(n1))) * 180.0 / M_PI;
double deviation_deg = 180.0 - angle_between_normals_deg;
double u0, u1;
Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, u0, u1);
if (!curve.IsNull()) {
gp_Pnt p_mid;
gp_Vec tangent;
curve->D1((u0 + u1) / 2.0, p_mid, tangent);
if (tangent.SquareMagnitude() > 1.e-10) {
tangent.Normalize();
if (edge.Orientation() == TopAbs_REVERSED) {
tangent.Reverse();
}
const gp_Vec cross = gp_Vec(n0.XYZ()).Crossed(gp_Vec(n1.XYZ()));
if (cross.Dot(tangent) < 0.0) {
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 +1326,51 @@ 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).
std::map<std::string, TopoDS_Compound> classified_edge_buckets;
{
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;
}
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);
}
}
if (is_floor_plan_) {
if (storey) {
auto it = storey_hlr.find(storey);
@@ -1215,11 +1378,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 +1936,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 +2002,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 +2425,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"
+56 -22
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,11 @@ 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_;
IfcParse::IfcFile* file;
const IfcUtil::IfcBaseEntity* storey_;
std::multimap<drawing_key, path_object, storey_sorter> paths;
@@ -623,6 +654,9 @@ 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)
, file(0)
, storey_(0)
, xcoords_begin(0)
@@ -641,7 +675,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);