diff --git a/src/bonsai/bonsai/bim/data/assets/default.css b/src/bonsai/bonsai/bim/data/assets/default.css index 68307f4c3f..5e4670af4d 100644 --- a/src/bonsai/bonsai/bim/data/assets/default.css +++ b/src/bonsai/bonsai/bim/data/assets/default.css @@ -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 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; } diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index b1c4c69c3b..9a8faf4d58 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -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( diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index c022bfdd1c..49d0fccbfb 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -371,6 +371,24 @@ namespace ifcopenshell { static constexpr double defaultvalue = -1.; }; + struct SvgRidgeAngleMinDegrees : public SettingBase { + 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 { + 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 { + 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 { 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 + std::tuple > {}; } diff --git a/src/serializers/SvgSerializer.cpp b/src/serializers/SvgSerializer.cpp index 16659587ac..24bc2c561a 100644 --- a/src/serializers/SvgSerializer.cpp +++ b/src/serializers/SvgSerializer.cpp @@ -102,10 +102,13 @@ const double PI2 = M_PI * 2.; bool SvgSerializer::ready() { + svg_ridge_angle_min_deg_ = geometry_settings().get().get(); + svg_valley_angle_min_deg_ = geometry_settings().get().get(); + svg_emit_flush_edges_ = geometry_settings().get().get(); return true; } -void SvgSerializer::write(path_object& p, const TopoDS_Shape& comp_or_wire, boost::optional> dash_array) { +void SvgSerializer::write(path_object& p, const TopoDS_Shape& comp_or_wire, boost::optional> dash_array, boost::optional 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& faces, + const gp_Dir& projection_direction, + double ridge_angle_min_deg, + double valley_angle_min_deg + ) { + std::vector faces_vec; + for (NCollection_List::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_heights_storage; const std::vector* section_heights_used = §ion_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 classified_edge_buckets; + { + NCollection_IndexedDataMap, 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, 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 per class (see plan notes on why that clobbers classes in Python). + std::map 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 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 element (each classified edge carries its own + // class), not on an ancestor , 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" diff --git a/src/serializers/SvgSerializer.h b/src/serializers/SvgSerializer.h index 7e564c589b..223bc5e441 100644 --- a/src/serializers/SvgSerializer.h +++ b/src/serializers/SvgSerializer.h @@ -56,6 +56,7 @@ #include #include #include +#include typedef std::pair drawing_key; @@ -212,9 +213,16 @@ namespace { private: const HLRAlgo_Projector& projector_; const std::list>* 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>* classified_shapes_ = nullptr; public: - typedef std::list> result_type; + typedef std::list> 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>* classified_shapes) { + classified_shapes_ = classified_shapes; + } + result_type operator()(boost::blank&) const { throw std::runtime_error(""); } + template + 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& algo) { algo->Projector(projector_); algo->Update(); algo->Hide(); HLRBRep_HLRToShape hlr_shapes(algo); - if (product_shapes_) { - std::list> 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& algo) { @@ -248,15 +269,7 @@ namespace { algo->Update(); HLRBRep_PolyHLRToShape hlr_shapes; hlr_shapes.Update(algo); - if (product_shapes_) { - std::list> 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 large_ortho_faces_; std::list> items_; + // SVG edge classification (issue #3668): see add_classified_edges(). + std::list> 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> build() { + std::list> 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 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> dash_array=boost::none); + void write(path_object& p, const TopoDS_Shape& wire, boost::optional> dash_array=boost::none, boost::optional 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);