Adds support for IfcOpenCrossProfileDef and branching with IfcSectionedSurface

This commit is contained in:
Richard Brice
2026-01-14 13:28:07 -08:00
parent 46a6356f93
commit df7318973d
5 changed files with 462 additions and 63 deletions
+224 -20
View File
@@ -14,6 +14,27 @@ namespace {
}
}
namespace {
template <typename T, typename Cmp = std::less<T>>
bool has_intersection(const std::set<T, Cmp>& A,
const std::set<T, Cmp>& 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_section>& 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<double> 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<double>::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<taxonomy::point3::ptr> points;
for (auto tmp__ : boost::combine(w1->children, w2->children)) {
boost::tie(e1, e2) = tmp__;
auto& p1 = boost::get<taxonomy::point3::ptr>(e1->start);
auto& p2 = boost::get<taxonomy::point3::ptr>(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<taxonomy::point3>(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<std::string> 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<std::string> 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<std::string, taxonomy::point3::ptr> tag_to_point_on_w1, tag_to_point_on_w2;
auto loop_to_points = [](const taxonomy::loop::ptr& loop, const boost::optional<std::vector<std::string>>& input_tags) -> std::pair<std::vector<taxonomy::point3::ptr>, std::vector<std::set<std::string>>> {
std::vector<taxonomy::point3::ptr> points;
std::vector<std::set<std::string>> tags;
std::vector<std::string>::const_iterator tag_it;
if (!loop->closed.get_value_or(false)) {
points = {boost::get<taxonomy::point3::ptr>(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<taxonomy::point3::ptr>(e->start);
const auto& p2_ = boost::get<taxonomy::point3::ptr>(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<std::set<std::string>>& tag_sets) -> std::set<std::string> {
return std::accumulate(
tag_sets.begin(), tag_sets.end(), std::set<std::string>{},
[](std::set<std::string> acc,
const std::set<std::string>& m) {
acc.insert(m.begin(), m.end());
return acc;
});
};
auto join_tags = [](const std::set<std::string>& 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<taxonomy::point3::ptr> points;
std::vector<std::string> common_tags_vec;
if (w1->tags) {
std::set<std::string> 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<std::string> 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<taxonomy::point3>(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<taxonomy::point3>(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<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
interpolated_loop->external = w1->external;
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
} else {
if (w1->tags) {
std::static_pointer_cast<taxonomy::loop>(interpolated)->tags = common_tags_vec;
}
std::static_pointer_cast<taxonomy::loop>(interpolated)->closed = w1->closed;
std::static_pointer_cast<taxonomy::loop>(interpolated)->children = interpolated_loop->children;
}
}
+227 -29
View File
@@ -27,12 +27,34 @@
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepOffsetAPI_ThruSections.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRepBuilderAPI_Transform.hxx>
using namespace ifcopenshell::geometry;
using namespace ifcopenshell::geometry::kernels;
using namespace IfcGeom;
using namespace IfcGeom::util;
// @todo duplicated
namespace {
template <typename T, typename Cmp = std::less<T>>
bool has_intersection(const std::set<T, Cmp>& A,
const std::set<T, Cmp>& 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<TopoDS_Shape> shps(loft->children.size());
std::vector<std::vector<std::set<std::string>>> 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<taxonomy::face>((*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<std::vector<std::string>>& input_tags) -> std::pair<std::vector<taxonomy::point3::ptr>, std::vector<std::set<std::string>>> {
std::vector<taxonomy::point3::ptr> points;
std::vector<std::set<std::string>> tags;
std::vector<std::string>::const_iterator tag_it;
if (!loop->closed.get_value_or(false)) {
points = {boost::get<taxonomy::point3::ptr>(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<taxonomy::point3::ptr>(e->start);
const auto& p2 = boost::get<taxonomy::point3::ptr>(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<taxonomy::loop>(*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<taxonomy::loop>((*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<taxonomy::item::ptr, 2> fa = { *it, *jt };
std::array<TopoDS_Shape, 2> shps;
std::array<std::vector<TopoDS_Shape>::const_iterator, 2> fa = { it, jt };
std::vector<std::array<TopoDS_Wire, 2>> ws;
ws.emplace_back();
for (int i = 0; i < 2; ++i) {
if (fa[i]->kind() == taxonomy::FACE) {
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) {
return false;
}
}
if (fa[i]->kind() == taxonomy::LOOP) {
TopoDS_Wire w;
if (!convert(std::static_pointer_cast<taxonomy::loop>(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<std::vector<gp_Pnt>, 2> profile_points;
std::array<std::vector<std::vector<std::set<std::string>>>::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]);
@@ -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<taxonomy::point3>(x, y, z, tag));
points.push_back(taxonomy::make<taxonomy::point3>(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;
}
+6 -6
View File
@@ -822,11 +822,11 @@ namespace {
boost::optional<function_item::ptr> ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) {
boost::optional<function_item::ptr> fi_;
boost::optional<function_item::ptr> function_item_;
auto loop_ = dcast<loop>(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<function_item::ptr> ifcopenshell::geometry::taxonomy::loop_to_fu
return boost::none;
}
}
fi_ = make<piecewise_function>(0.0,spans);
loop_->fi = fi_;
function_item_ = make<piecewise_function>(0.0, spans);
loop_->function_item = function_item_;
}
}
return fi_;
return function_item_;
}
+2 -1
View File
@@ -928,7 +928,8 @@ typedef item const* ptr;
DECLARE_PTR(loop)
boost::optional<bool> external, closed;
boost::optional<taxonomy::function_item::ptr> fi;
boost::optional<taxonomy::function_item::ptr> function_item;
boost::optional<std::vector<std::string>> tags;
bool is_polyhedron() const {
for (auto& e : children) {