#include "profile_helper.h" #include "infra_sweep_helper.h" #include "function_item_evaluator.h" #include using namespace ifcopenshell::geometry; namespace { // std::lerp when upgrading to C++ 20 template T lerp(const T& a, const T& b, double t) { return a + t * (b - a); } } 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()); auto loft = taxonomy::make(); // @todo intialize as default loft->axis = nullptr; // @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); double start = std::max(0., cross_sections.front().dist_along); double end = std::min(fn->length(), cross_sections.back().dist_along); if (end - start < 1.e-9) { Logger::Warning("Empty sweep domain with start at " + std::to_string(cross_sections.front().dist_along) + " end at " + std::to_string(cross_sections.back().dist_along) + " and curve domain length " + std::to_string(fn->length()), inst); return nullptr; } auto curve_length = end - start; auto param_type = settings_.get().get(); auto param = settings_.get().get(); size_t num_steps = 0; if (param_type == ifcopenshell::geometry::settings::FunctionStepMethod::MAXSTEPSIZE) { // parameter is max step size num_steps = (size_t)std::ceil(curve_length / param); } else { // 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); } 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 + delta_step * i; while (dist_along > *(profile_index + 1)) { profile_index++; if (profile_index == longitudes.end()) { // @todo handle this? } } 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; const auto& rotation_a = cross_sections[std::distance(longitudes.begin(), profile_index)].rotation; taxonomy::geom_item::ptr interpolated = nullptr; // Only interpolate if: // - there is a profile ahead of us, and // - we're not exactly at the location of the current profile or whether there is an offset involved bool should_interpolate = (profile_index + 1 < longitudes.end()) && (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0. || rotation_a); boost::optional interpolated_rotation; if (should_interpolate) { taxonomy::geom_item::ptr profile_b; Eigen::Vector3d offset_b; boost::optional rotation_b; if ((profile_index + 1 < longitudes.end())) { profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry; offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset; rotation_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].rotation; } else { profile_b = profile_a; offset_b = offset_a; rotation_b = rotation_a; } // Only interpolate if the profiles are different or either of the offsets is non-zero bool should_interpolate2 = (profile_a->instance != profile_b->instance) || (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0. || rotation_b); if (should_interpolate2) { std::vector loops_a, loops_b; if (profile_a->kind() == taxonomy::FACE) { interpolated = taxonomy::make(); auto profile_a_f = std::static_pointer_cast(profile_a); auto profile_b_f = std::static_pointer_cast(profile_b); if (profile_a_f->children.size() != profile_b_f->children.size()) { Logger::Warning("Mismatching number of face boundaries: " + std::to_string(profile_a_f->children.size()) + " vs " + std::to_string(profile_b_f->children.size()), inst ); return nullptr; } loops_a = profile_a_f->children; loops_b = profile_b_f->children; } else { loops_a = { std::static_pointer_cast(profile_a) }; loops_b = { std::static_pointer_cast(profile_b) }; interpolated = taxonomy::make(); } // @todo should_interpolate should also be informed based by different face matrices. if (profile_a->matrix || profile_b->matrix) { interpolated->matrix = taxonomy::make(); Eigen::Matrix4d m4a = Eigen::Matrix4d::Identity(); Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); if (profile_a->matrix) { m4a = profile_a->matrix->ccomponents(); } if (profile_b->matrix) { m4b = profile_b->matrix->ccomponents(); } interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along); } auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along); if (rotation_a.has_value() && rotation_b.has_value() ) { // @todo we don't support an overridden rotation on only one of the placements // in which case we would need to lerp with the rotation component below in m4b. interpolated_rotation = lerp(*rotation_a, *rotation_b, relative_dist_along); } 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->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 auto& p1 = boost::get(w1->children.back()->end); auto& p2 = boost::get(w2->children.back()->end); auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); points.push_back(taxonomy::make(p3)); } else if (w1->closed.get_value_or(true) && w2->closed.get_value_or(true)) { // close polygon by referencing first point // @todo add a closed=true|false to polygon_from_points()? points.push_back(points.front()); } } */ auto interpolated_loop = polygon_from_points(points); if (interpolated->kind() == taxonomy::FACE) { 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; } } } } auto m4 = evaluator.evaluate(dist_along); /* { std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl; }*/ Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); if (interpolated_rotation) { // direction vectors on the linear placement overwrite the placement otherwise inferred from the tangent m4b.col(0).head<3>() = interpolated_rotation->col(1); m4b.col(1).head<3>() = interpolated_rotation->col(2); m4b.col(2).head<3>() = interpolated_rotation->col(0); } else { m4b.col(0).head<3>() = m4.col(1).head<3>().normalized(); m4b.col(1).head<3>() = m4.col(2).head<3>().normalized(); m4b.col(2).head<3>() = m4.col(0).head<3>().normalized(); } m4b.col(3).head<3>() = m4.col(3).head<3>(); if (interpolated) { loft->children.push_back(interpolated); } else { if (profile_a->kind() == taxonomy::FACE) { loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_()))); } else { loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_()))); } if (profile_a->matrix) { loft->children.back()->matrix = taxonomy::matrix4::ptr(profile_a->matrix->clone_()); } } if (!loft->children.back()->matrix) { // @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism. loft->children.back()->matrix = taxonomy::make(); } auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval(); loft->children.back()->matrix->components() = m; } } return loft; }