From df7318973d5f5d03a6dd597b4705bab9d1989cc9 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:28:07 -0800 Subject: [PATCH] Adds support for IfcOpenCrossProfileDef and branching with IfcSectionedSurface --- src/ifcgeom/infra_sweep_helper.cpp | 244 +++++++++++++++-- src/ifcgeom/kernels/opencascade/loft.cpp | 256 ++++++++++++++++-- .../mapping/IfcOpenCrossProfileDef.cpp | 10 +- src/ifcgeom/taxonomy.cpp | 12 +- src/ifcgeom/taxonomy.h | 3 +- 5 files changed, 462 insertions(+), 63 deletions(-) diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp index b9c9e598d9..58f6d3108a 100644 --- a/src/ifcgeom/infra_sweep_helper.cpp +++ b/src/ifcgeom/infra_sweep_helper.cpp @@ -14,6 +14,27 @@ namespace { } } +namespace { +template > +bool has_intersection(const std::set& A, + const std::set& B) { + auto itA = A.begin(); + auto itB = B.begin(); + + while (itA != A.end() && itB != B.end()) { + if (Cmp()(*itA, *itB)) { + ++itA; + } else if (Cmp()(*itB, *itA)) { + ++itB; + } else { + return true; + } + } + return false; +} + +} + taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::function_item::ptr& fn, std::vector& cross_sections) { std::sort(cross_sections.begin(), cross_sections.end()); @@ -25,7 +46,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, // @todo currently only the case is handled where directrix returns a function_item // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a function_item function if (fn) { - function_item_evaluator evaluator(settings_,fn); + function_item_evaluator evaluator(settings_, fn); double start = std::max(0., cross_sections.front().dist_along); double end = std::min(fn->length(), cross_sections.back().dist_along); @@ -45,6 +66,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, // parameter is minimum number of steps num_steps = (size_t)std::ceil(param); } + auto delta_step = curve_length / num_steps; std::vector longitudes; for (auto& x : cross_sections) { longitudes.push_back(x.dist_along); @@ -52,7 +74,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, longitudes.push_back(std::numeric_limits::infinity()); auto profile_index = longitudes.begin(); for (size_t i = 0; i <= num_steps; ++i) { - auto dist_along = start + curve_length / num_steps * i; + auto dist_along = start + delta_step * i; while (dist_along > *(profile_index + 1)) { profile_index++; if (profile_index == longitudes.end()) { @@ -60,6 +82,8 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, } } + const bool is_last_placement_of_this_profile = profile_index + 1 >= longitudes.end() ? false : ((start + delta_step * (i+1)) > *(profile_index + 1)); + auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index + 1) - *profile_index); const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry; const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset; @@ -143,28 +167,203 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, } else if (rotation_a != rotation_b) { Logger::Error("Direction vectors on cross section placements only supported when used consistently"); } + taxonomy::loop::ptr w1, w2; taxonomy::edge::ptr e1, e2; + taxonomy::point3::ptr p1, p2; + for (auto tmp_ : boost::combine(loops_a, loops_b)) { boost::tie(w1, w2) = tmp_; - if (w1->children.size() != w2->children.size()) { - Logger::Warning("Mismatching number of edges: " + - std::to_string(w1->children.size()) + " vs " + - std::to_string(w2->children.size()), - inst - ); - return nullptr; - } - std::vector points; - for (auto tmp__ : boost::combine(w1->children, w2->children)) { - boost::tie(e1, e2) = tmp__; - auto& p1 = boost::get(e1->start); - auto& p2 = boost::get(e2->start); - auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); - // auto p4 = (interpolated_rotation * p3).eval(); - points.push_back(taxonomy::make(p3)); + if (w1->closed != w2->closed) { + Logger::Warning("Mismatching closed property on loops", inst); + return nullptr; + } + + if (w1->tags.is_initialized() != w2->tags.is_initialized()) { + Logger::Warning("Mismatching availability tags on loops", inst); + return nullptr; + } + + if (w1->tags) { + // check uniqueness + std::set tags_seen; + for (const auto& t : *w1->tags) { + if (tags_seen.find(t) != tags_seen.end()) { + Logger::Warning("Duplicate tag '" + t + "' on loft profile", inst); + return nullptr; + } + tags_seen.insert(t); + } } + + if (w2->tags) { + // check uniqueness + std::set tags_seen; + for (const auto& t : *w2->tags) { + if (tags_seen.find(t) != tags_seen.end()) { + Logger::Warning("Duplicate tag '" + t + "' on loft profile", inst); + return nullptr; + } + tags_seen.insert(t); + } + } + + std::map tag_to_point_on_w1, tag_to_point_on_w2; + + auto loop_to_points = [](const taxonomy::loop::ptr& loop, const boost::optional>& input_tags) -> std::pair, std::vector>> { + std::vector points; + std::vector> tags; + std::vector::const_iterator tag_it; + + if (!loop->closed.get_value_or(false)) { + points = {boost::get(loop->children[0]->start)}; + if (input_tags) { + tags = {{input_tags->front()}}; + tag_it = ++input_tags->begin(); + } + } + for (auto& e : loop->children) { + const auto& p1_ = boost::get(e->start); + const auto& p2_ = boost::get(e->end); + if (input_tags && p1_->ccomponents() == p2_->ccomponents()) { + tags.back().insert(*tag_it); + ++tag_it; + } else { + points.push_back(p2_); + if (input_tags) { + tags.emplace_back(); + tags.back().insert(*tag_it); + ++tag_it; + } + } + } + if (!input_tags) { + if (loop->closed.get_value_or(false)) { + // close polygon by referencing first point + points.push_back(points.front()); + } + } + return {points, tags}; + }; + + auto combine_tags = [](const std::vector>& tag_sets) -> std::set { + return std::accumulate( + tag_sets.begin(), tag_sets.end(), std::set{}, + [](std::set acc, + const std::set& m) { + acc.insert(m.begin(), m.end()); + return acc; + }); + }; + + auto join_tags = [](const std::set& tag_set) -> std::string { + std::string result; + for (auto it = tag_set.begin(); it != tag_set.end(); ++it) { + if (it != tag_set.begin()) { + result += ", "; + } + result += *it; + } + return result; + }; + + auto [w1_points, w1_tags] = loop_to_points(w1, w1->tags); + auto [w2_points, w2_tags] = loop_to_points(w2, w2->tags); + + if (w1->tags && w2->tags) { + { + auto it = w1_points.begin(); + auto jt = w1_tags.begin(); + while (it != w1_points.end() && jt != w1_tags.end()) { + for (auto& t : *jt) { + tag_to_point_on_w1[t] = *it; + } + ++it; + ++jt; + } + } + + { + auto it = w2_points.begin(); + auto jt = w2_tags.begin(); + while (it != w2_points.end() && jt != w2_tags.end()) { + for (auto& t : *jt) { + tag_to_point_on_w2[t] = *it; + } + ++it; + ++jt; + } + } + + auto w1_tags_combined = combine_tags(w1_tags); + auto w2_tags_combined = combine_tags(w2_tags); + + // For every point (which can have multiple tags in case of 0-width edges) there needs to be a corresponding point on the other profile + + for (auto& p1_tags : w1_tags) { + if (!has_intersection(p1_tags, w2_tags_combined)) { + Logger::Warning("No matching tags found on loft profiles: " + join_tags(p1_tags) + " not in " + join_tags(w2_tags_combined), inst); + return nullptr; + } + } + + for (auto& p2_tags : w2_tags) { + if (!has_intersection(p2_tags, w1_tags_combined)) { + Logger::Warning("No matching tags found on loft profiles: " + join_tags(p2_tags) + " not in " + join_tags(w1_tags_combined), inst); + return nullptr; + } + } + } else { + if (w1->children.size() != w2->children.size()) { + Logger::Warning("Mismatching number of edges: " + + std::to_string(w1->children.size()) + " vs " + + std::to_string(w2->children.size()), + inst); + return nullptr; + } + } + + std::vector points; + + std::vector common_tags_vec; + if (w1->tags) { + std::set common_tags; + for (const auto& t : *w1->tags) { + if (tag_to_point_on_w2.find(t) == tag_to_point_on_w2.end()) { + continue; + } + + const auto& p1_ = tag_to_point_on_w1[t]; + const auto& p2_ = tag_to_point_on_w2[t]; + + auto p3 = (lerp(p1_->ccomponents(), p2_->ccomponents(), relative_dist_along) + interpolated_offset).eval(); + + std::set tags_for_this_point_on_subsequent_profile = {t}; + + if (is_last_placement_of_this_profile) { + for (auto& ts : w2_tags) { + if (ts.find(t) != ts.end()) { + tags_for_this_point_on_subsequent_profile = ts; + } + } + } + + for (auto& x : tags_for_this_point_on_subsequent_profile) { + points.push_back(taxonomy::make(p3)); + common_tags_vec.push_back(x); + } + } + } else { + for (auto tmp__ : boost::combine(w1_points, w2_points)) { + boost::tie(p1, p2) = tmp__; + auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); + points.push_back(taxonomy::make(p3)); + } + } + + /* + // This is handled in the loop_to_points() function above if (!points.empty()) { if (!w1->closed.get_value_or(true) && !w2->closed.get_value_or(true)) { // open polygon, add last point @@ -178,12 +377,17 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, points.push_back(points.front()); } } + */ auto interpolated_loop = polygon_from_points(points); - interpolated_loop->external = w1->external; if (interpolated->kind() == taxonomy::FACE) { - std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop); + interpolated_loop->external = w1->external; + std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop); } else { + if (w1->tags) { + std::static_pointer_cast(interpolated)->tags = common_tags_vec; + } + std::static_pointer_cast(interpolated)->closed = w1->closed; std::static_pointer_cast(interpolated)->children = interpolated_loop->children; } } diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index 188150b614..60997d24ad 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -27,12 +27,34 @@ #include #include #include +#include using namespace ifcopenshell::geometry; using namespace ifcopenshell::geometry::kernels; using namespace IfcGeom; using namespace IfcGeom::util; +// @todo duplicated +namespace { +template > +bool has_intersection(const std::set& A, + const std::set& B) { + auto itA = A.begin(); + auto itB = B.begin(); + + while (itA != A.end() && itB != B.end()) { + if (Cmp()(*itA, *itB)) { + ++itA; + } else if (Cmp()(*itB, *itA)) { + ++itB; + } else { + return true; + } + } + return false; +} +} + bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& result) { if (loft->children.size() < 2) { return false; @@ -110,37 +132,125 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re BRep_Builder BB; BB.MakeCompound(comp); - // @todo this approach is - // potentially incorrect as there is no guarantee that the wires for - // subsequently placed profiles are traversed from an equivalent start vertex. + std::vector shps(loft->children.size()); + std::vector>> all_tags; - for (auto it = loft->children.begin(); it < loft->children.end() - 1; ++it) { + + std::ostringstream oss; + loft->children[0]->print(oss); + loft->children[1]->print(oss); + auto s = oss.str(); + std::wcout << s.c_str() << std::endl; + + // First convert all taxonomy items to TopoDS_Wire/Face + for (auto it = loft->children.begin(); it < loft->children.end(); ++it) { + auto i = std::distance(loft->children.begin(), it); + if ((*it)->kind() == taxonomy::FACE) { + if (!convert(std::static_pointer_cast((*it)), shps[i])) { + return false; + } + } + if ((*it)->kind() == taxonomy::LOOP) { + + // @todo duplicated with infra_sweep_helper + // I think make_loft() where should just return a shell instead, because + // this faceted lofting does not depend on any functionality in the geometry library + // and the branching with tags needs to be solved twice otherwise + auto loop_to_points = [](const taxonomy::loop::ptr& loop, const boost::optional>& input_tags) -> std::pair, std::vector>> { + std::vector points; + std::vector> tags; + std::vector::const_iterator tag_it; + + if (!loop->closed.get_value_or(false)) { + points = {boost::get(loop->children[0]->start)}; + if (input_tags) { + tags = {{input_tags->front()}}; + tag_it = ++input_tags->begin(); + } + } + for (auto& e : loop->children) { + const auto& p1 = boost::get(e->start); + const auto& p2 = boost::get(e->end); + if (input_tags && p1->ccomponents() == p2->ccomponents()) { + tags.back().insert(*tag_it); + ++tag_it; + } else { + points.push_back(p2); + if (input_tags) { + tags.emplace_back(); + tags.back().insert(*tag_it); + ++tag_it; + } + } + } + if (!input_tags) { + if (loop->closed.get_value_or(false)) { + // close polygon by referencing first point + points.push_back(points.front()); + } + } + return {points, tags}; + }; + + auto lp = std::static_pointer_cast(*it); + TopoDS_Wire w; + + if (lp->tags) { + auto [points, tags] = loop_to_points(lp, lp->tags); + BRepBuilderAPI_MakePolygon mp; + for (auto& p : points) { + const auto& xyz = p->ccomponents(); + mp.Add(gp_Pnt(xyz(0), xyz(1), xyz(2))); + } + w = mp.Wire(); + + if (lp->matrix && !lp->matrix->is_identity()) { + const auto& m = lp->matrix->ccomponents(); + gp_Trsf tr; + tr.SetValues( + m(0, 0), m(0, 1), m(0, 2), m(0, 3), m(1, 0), m(1, 1), m(1, 2), m(1, 3), m(2, 0), m(2, 1), m(2, 2), m(2, 3)); + w = TopoDS::Wire(BRepBuilderAPI_Transform(w, tr).Shape()); + } + + all_tags.push_back(tags); + } else { + if (!convert(std::static_pointer_cast((*it)), w)) { + return false; + } + } + + shps[i] = w; + } + if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) { + return false; + } + } + + /* + // With --dimensionality CURVES_SURFACES_AND_SOLIDS this will give the interpolated profiles as line geometry + { + for (auto& f : shps) { + BB.Add(comp, f); + } + } + result = comp; + return true; + */ + + // @todo this approach is + // potentially incorrect as there is no guarantee that the wires for + // subsequently placed profiles are traversed from an equivalent start vertex. + for (auto it = shps.begin(); it < shps.end() - 1; ++it) { + auto ii = std::distance(shps.begin(), it); auto jt = it + 1; - std::array fa = { *it, *jt }; - std::array shps; + std::array::const_iterator, 2> fa = { it, jt }; std::vector> ws; ws.emplace_back(); for (int i = 0; i < 2; ++i) { - if (fa[i]->kind() == taxonomy::FACE) { - if (!convert(std::static_pointer_cast(fa[i]), shps[i])) { - return false; - } - } - if (fa[i]->kind() == taxonomy::LOOP) { - TopoDS_Wire w; - if (!convert(std::static_pointer_cast(fa[i]), w)) { - return false; - } - shps[i] = w; - } - if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) { - return false; - } - - if (shps[i].ShapeType() == TopAbs_FACE) { - ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i])); + if (fa[i]->ShapeType() == TopAbs_FACE) { + ws[0][i] = BRepTools::OuterWire(TopoDS::Face(*fa[i])); size_t j = 1; - for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) { + for (TopExp_Explorer exp(*fa[i], TopAbs_WIRE); exp.More(); exp.Next()) { if (exp.Current() != ws[0][i]) { while (ws.size() <= j) { ws.emplace_back(); @@ -149,22 +259,110 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re } } } else { - ws[0][i] = TopoDS::Wire(shps[i]); + ws[0][i] = TopoDS::Wire(*fa[i]); } } - if (shps[0].ShapeType() == TopAbs_FACE) { + if (it->ShapeType() == TopAbs_FACE) { // When processing a sectioned *surface* there are no // begin and end caps that need to be added. - if (it == loft->children.begin()) { + if (it == shps.begin()) { // faces.Append(shps[0]); BB.Add(comp, shps[0]); } - if (jt == loft->children.end() - 1) { + if (jt == shps.end() - 1) { // faces.Append(shps[1]); BB.Add(comp, shps[1]); } } + if (!all_tags.empty()) { + // only open profiles have tags for now, so there is only one wire, no inner wires + const auto& wp = ws[0]; + std::array, 2> profile_points; + std::array>>::const_iterator, 2> tag_pairs = { + all_tags.begin() + std::distance(shps.begin(), it), + all_tags.begin() + std::distance(shps.begin(), jt)}; + + for (size_t i = 0; i < 2; ++i) { + TopTools_IndexedDataMapOfShapeListOfShape ancestors; + const auto& wire = wp[i]; + auto& result = profile_points[i]; + + TopExp::MapShapesAndAncestors( + wire, + TopAbs_VERTEX, + TopAbs_EDGE, + ancestors); + + TopoDS_Vertex v0, vn, previous; + TopExp::Vertices(wire, v0, vn); + + TopoDS_Vertex curr = v0; + result.push_back(BRep_Tool::Pnt(curr)); + + while (true) { + if (curr.IsSame(vn)) { + break; + } + + const TopTools_ListOfShape& incidentEdges = ancestors.FindFromKey(curr); + + for (TopTools_ListIteratorOfListOfShape it(incidentEdges); it.More(); it.Next()) { + const TopoDS_Edge& e = TopoDS::Edge(it.Value()); + + TopoDS_Vertex ev0, ev1; + TopExp::Vertices(e, ev0, ev1); + + TopoDS_Vertex other_on_edge = curr.IsSame(ev0) ? ev1 : ev0; + if (other_on_edge.IsSame(previous)) { + continue; + } else { + previous = curr; + curr = other_on_edge; + result.push_back(BRep_Tool::Pnt(curr)); + break; + } + } + } + } + + auto a = profile_points[0].begin(); + auto b = profile_points[1].begin(); + auto c = tag_pairs[0]->begin(); + auto d = tag_pairs[1]->begin(); + + if (!has_intersection(*c, *d)) { + throw std::runtime_error("Starting vertices do not have corresponding tags"); + } + + auto emit_triangle = [&](const gp_Pnt& p1, const gp_Pnt& p2, const gp_Pnt& p3) { + BB.Add(comp, BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(p1, p2, p3, true).Wire()).Face()); + }; + + while (c != (tag_pairs[0]->end() - 1) && d != (tag_pairs[0]->end() - 1)) { + if (c != (tag_pairs[0]->end() - 1) && has_intersection(*(c + 1), *d)) { + emit_triangle(*a, *(a + 1), *b); + ++a; + ++c; + } else if (d != (tag_pairs[1]->end() - 1) && has_intersection(*c, *(d + 1))) { + emit_triangle(*a, *(b + 1), *b); + ++b; + ++d; + } else if (c != (tag_pairs[0]->end() - 1) && d != (tag_pairs[1]->end() - 1) && has_intersection(*(c + 1), *(d + 1))) { + emit_triangle(*a, *(a + 1), *b); + emit_triangle(*(a + 1), *(b + 1), *b); + ++a; + ++b; + ++c; + ++d; + } else { + throw std::runtime_error("Unable to construct surface"); + } + } + + continue; + } + for (auto& wp : ws) { BRepTools_WireExplorer a(wp[0]); BRepTools_WireExplorer b(wp[1]); diff --git a/src/ifcgeom/mapping/IfcOpenCrossProfileDef.cpp b/src/ifcgeom/mapping/IfcOpenCrossProfileDef.cpp index 334be1324a..7f3c4f43c1 100644 --- a/src/ifcgeom/mapping/IfcOpenCrossProfileDef.cpp +++ b/src/ifcgeom/mapping/IfcOpenCrossProfileDef.cpp @@ -50,7 +50,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOpenCrossProfileDef* inst) { if (tags.has_value() && !tags.get().empty()) { tag = tags.get()[0]; } - // start->tag = tag; auto widths = inst->Widths(); auto angles = inst->Slopes(); // these are actually angles, but the attribute is called Slopes @@ -79,16 +78,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOpenCrossProfileDef* inst) { tag = tags.get()[i+1]; } - // points.push_back(taxonomy::make(x, y, z, tag)); points.push_back(taxonomy::make(x, y, z)); } auto mapped = polygon_from_points(points); - if (mapped->kind() == taxonomy::LOOP) { - auto r = taxonomy::loop::ptr((taxonomy::loop*)mapped->clone_()); - r->closed = false; - return r; - } + mapped->closed = false; + mapped->tags = tags; + return mapped; } diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index e03c1010a3..7d509db8cb 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -822,11 +822,11 @@ namespace { boost::optional ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) { - boost::optional fi_; + boost::optional function_item_; auto loop_ = dcast(item); if (loop_) { - if (loop_->fi.is_initialized()) { - fi_ = loop_->fi; + if (loop_->function_item.is_initialized()) { + function_item_ = loop_->function_item; } else { // piecewise_function is a specialization of function_item - callers don't need to know this detail piecewise_function::spans_t spans; @@ -880,9 +880,9 @@ boost::optional ifcopenshell::geometry::taxonomy::loop_to_fu return boost::none; } } - fi_ = make(0.0,spans); - loop_->fi = fi_; + function_item_ = make(0.0, spans); + loop_->function_item = function_item_; } } - return fi_; + return function_item_; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index d3ec5c5e17..ab8a222aee 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -928,7 +928,8 @@ typedef item const* ptr; DECLARE_PTR(loop) boost::optional external, closed; - boost::optional fi; + boost::optional function_item; + boost::optional> tags; bool is_polyhedron() const { for (auto& e : children) {