diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index 2d04b9e025..c7e6562af5 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -18,7 +18,13 @@ ********************************************************************************/ #define _USE_MATH_DEFINES +#include +#include #include +#include +#include +#include +#include #include "mapping.h" @@ -26,6 +32,9 @@ #include "../../ifcparse/file.h" #include "../../ifcparse/si_prefix.h" +#include +#include + using namespace ifcopenshell; using namespace ifcopenshell::geometry; using namespace IfcGeom; @@ -222,16 +231,22 @@ std::vector mapping::find_openings(const express::Base& inst) { void mapping::get_representations(std::vector& tasks, std::vector& filters) { std::vector representations; + const bool has_context_ids = settings_.get().has(); + const bool uses_priorities = !has_context_ids && settings_.get().has(); - if (!settings_.get().has()) { - addRepresentationsFromDefaultContexts(representations); - } else { + if (has_context_ids) { addRepresentationsFromContextIds(representations); + } else if (uses_priorities) { + addRepresentationsFromPriorities(representations); + } else { + addRepresentationsFromDefaultContexts(representations); } std::vector ok_mapped_representations; int task_index = 0; + + std::set products_seen; for (auto representation : representations) { IfcSchema::IfcRepresentationMap rmap; @@ -279,6 +294,12 @@ void mapping::get_representations(std::vector& tasks, // reuse_ok is taken into account in products_represented_by(), but not when // the same IfcRepresentation is directly assigned to multiple products. for (auto& p : ifcproducts) { + if (uses_priorities) { + if (products_seen.find(p) != products_seen.end()) { + continue; + } + products_seen.insert(p); + } geometry_conversion_task task; task.index = task_index++; task.representation = representation; @@ -286,6 +307,18 @@ void mapping::get_representations(std::vector& tasks, tasks.emplace_back(task); } } else { + if (uses_priorities) { + ifcproducts.erase(std::remove_if(ifcproducts.begin(), ifcproducts.end(), [&](const IfcSchema::IfcProduct& p) { + return products_seen.find(p) != products_seen.end(); + }), + ifcproducts.end()); + for (auto& p : ifcproducts) { + products_seen.insert(p); + } + if (ifcproducts.empty()) { + continue; + } + } geometry_conversion_task task; task.index = task_index++; task.representation = representation; @@ -1300,39 +1333,225 @@ void mapping::addRepresentationsFromDefaultContexts(std::vector(c)); + }), normalized.end()); + return normalized; +} + +const std::unordered_map& context_filter_attribute_names() { + static const auto names = [] { + std::unordered_map result; + const auto& attributes = IfcSchema::get_schema() + .declaration_by_name("IfcGeometricRepresentationSubContext") + ->as_entity() + ->all_attributes(); + for (const auto* attribute : attributes) { + result.emplace(normalized_context_filter_key(attribute->name()), attribute->name()); + } + return result; + }(); + return names; +} + +std::string canonical_context_filter_attribute(std::string attribute) { + boost::trim(attribute); + const auto& names = context_filter_attribute_names(); + const auto it = names.find(normalized_context_filter_key(attribute)); + return it == names.end() ? attribute : it->second; +} + +template +struct has_std_to_string : std::false_type {}; + +template +struct has_std_to_string()))>> : std::true_type {}; + +struct compiled_context_filter { + std::optional type; + std::vector> args; + + bool matches(const std::optional& actual, const std::string& expected) const; + bool matches(const IfcSchema::IfcGeometricRepresentationContext& context) const; + bool matches( + const IfcSchema::IfcGeometricRepresentationContext& context, + const std::string& key, + const std::string& value) const; + + template + bool matches(const T& actual, const std::string& expected) const; +}; + +template +bool compiled_context_filter::matches(const T& actual, const std::string& expected) const { + using value_t = std::decay_t; + if constexpr (std::is_same_v) { + return boost::iequals(actual, expected); + } else if constexpr (std::is_same_v) { + return boost::iequals(actual.value(), expected); + } else if constexpr (has_std_to_string::value) { + return std::to_string(actual) == expected; + } else { + return false; + } +} + +bool compiled_context_filter::matches(const std::optional& actual, const std::string& expected) const { + return actual && matches(*actual, expected); +} + +bool compiled_context_filter::matches( + const IfcSchema::IfcGeometricRepresentationContext& context, + const std::string& key, + const std::string& value) const +{ + try { + attribute_value val = context.get(key); + return val.apply_visitor([&](const auto& v) { + return matches(v, value); + }); + } catch (const ifcopenshell::exception&) { + return false; + } +} + +bool compiled_context_filter::matches(const IfcSchema::IfcGeometricRepresentationContext& context) const { + if (type.has_value() && !matches(context.ContextIdentifier(), *type)) { + return false; + } + for (auto& [key, value] : args) { + if (!matches(context, key, value)) { + return false; + } + } + return true; +} + +std::optional parse_context_filter(const std::string& statement) { + namespace x3 = boost::spirit::x3; + + std::string type; + std::vector> args; + + const auto text = x3::lexeme[+(x3::char_ - '[' - ']' - '=' - ',')]; + std::string arg_key; + const auto key = text[([&](auto& ctx) { arg_key = x3::_attr(ctx); })]; + const auto value = text[([&](auto& ctx) { args.emplace_back(arg_key, x3::_attr(ctx)); })]; + const auto assignment = key >> '=' >> value; + const auto assignments = assignment % ','; + const auto wildcard = x3::lit('*')[([&](auto&) { type.clear(); })]; + const auto typed = text[([&](auto& ctx) { type = x3::_attr(ctx); })]; + const auto query = + (wildcard | typed) >> + -('[' >> assignments >> ']'); + + auto first = statement.begin(); + auto last = statement.end(); + if (!x3::phrase_parse(first, last, query, x3::space) || first != last) { + return std::nullopt; + } + + boost::trim(type); + compiled_context_filter filter; + if (!type.empty()) { + filter.type = type; + } + for (auto& arg : args) { + boost::trim(arg.first); + boost::trim(arg.second); + filter.args.emplace_back(canonical_context_filter_attribute(arg.first), arg.second); + } + return filter; +} + +} // namespace + +void mapping::addRepresentationsFromPriorities(std::vector& representations) { + std::vector filtered_contexts; + std::vector filters; + + for (auto& p : settings_.get().get()) { + // Parse statement below into a compiled_context_filter: + // - 'body' -> {'body', {}} + // - 'tesselation[targetscale=10]' -> {'tesselation', {'targetscale', '10'}} + // - '*' -> {{},{}} + if (auto filter = parse_context_filter(p)) { + filters.push_back(*filter); + } else { + logger_.warning("GEO", 325, "Ignoring invalid context filter '" + p + "'"); + } + } + + auto contexts = file_->instances_by_type(); + for (const auto& filter : filters) { + for (auto& context : contexts) { + if (filter.matches(context)) { + // Filtered contexts is in order of the priorities, so that the first context in the list is the highest priority. + filtered_contexts.push_back(context); + } + } + } + + for (auto& context : filtered_contexts) { + auto reps_in_context = context.RepresentationsInContext(); + representations.insert(representations.end(), reps_in_context.begin(), reps_in_context.end()); + } +} + void mapping::ensureRepresentationContextCache_() { const auto has_context_ids = settings_.get().has(); + const auto has_context_priorities = !has_context_ids && settings_.get().has(); const auto dimensionality = settings_.get().get(); const auto context_ids = has_context_ids ? settings_.get().get() : std::set{}; + const auto context_priorities = + has_context_priorities ? settings_.get().get() : std::vector{}; std::lock_guard guard(representation_context_cache_guard_); if (representation_context_cache_valid_ && representation_context_cache_has_context_ids_ == has_context_ids && + representation_context_cache_has_context_priorities_ == has_context_priorities && representation_context_cache_dimensionality_ == dimensionality && - representation_context_cache_ids_ == context_ids) { + representation_context_cache_ids_ == context_ids && + representation_context_cache_priorities_ == context_priorities) { return; } std::vector representations; - if (!has_context_ids) { - addRepresentationsFromDefaultContexts(representations); - } else { + if (has_context_ids) { addRepresentationsFromContextIds(representations); + } else if (has_context_priorities) { + addRepresentationsFromPriorities(representations); + } else { + addRepresentationsFromDefaultContexts(representations); } std::unordered_set representation_ids; representation_ids.reserve(representations.size()); + std::vector representation_priority_ids; + representation_priority_ids.reserve(representations.size()); for (auto& representation : representations) { if (representation) { - representation_ids.insert((uint32_t)representation.id()); + const auto representation_id = (uint32_t)representation.id(); + if (representation_ids.insert(representation_id).second) { + representation_priority_ids.push_back(representation_id); + } } } representation_context_cache_ = std::move(representation_ids); + representation_context_priority_cache_ = std::move(representation_priority_ids); representation_context_cache_ids_ = std::move(context_ids); + representation_context_cache_priorities_ = std::move(context_priorities); representation_context_cache_dimensionality_ = dimensionality; representation_context_cache_has_context_ids_ = has_context_ids; + representation_context_cache_has_context_priorities_ = has_context_priorities; representation_context_cache_valid_ = true; } @@ -1349,9 +1568,20 @@ express::Base mapping::representation_of(const express::Base& product) { { std::lock_guard guard(representation_context_cache_guard_); - for (auto& r : of_product) { - if (representation_context_cache_.find((uint32_t)r.id()) != representation_context_cache_.end()) { - intersection.push_back(r); + if (representation_context_cache_has_context_priorities_) { + for (auto representation_id : representation_context_priority_cache_) { + auto it = std::find_if(of_product.begin(), of_product.end(), [&](const auto& r) { + return (uint32_t)r.id() == representation_id; + }); + if (it != of_product.end()) { + intersection.push_back(*it); + } + } + } else { + for (auto& r : of_product) { + if (representation_context_cache_.find((uint32_t)r.id()) != representation_context_cache_.end()) { + intersection.push_back(r); + } } } } diff --git a/src/ifcgeom/mapping/mapping.h b/src/ifcgeom/mapping/mapping.h index 3f11646a4d..9f4dfc652c 100644 --- a/src/ifcgeom/mapping/mapping.h +++ b/src/ifcgeom/mapping/mapping.h @@ -33,6 +33,7 @@ namespace geometry { void initialize_units_(); void addRepresentationsFromContextIds(std::vector&); + void addRepresentationsFromPriorities(std::vector&); void addRepresentationsFromDefaultContexts(std::vector&); void ensureRepresentationContextCache_(); @@ -41,9 +42,12 @@ namespace geometry { std::set failed_on_purpose_; std::set not_reusable_maps_; std::unordered_set representation_context_cache_; + std::vector representation_context_priority_cache_; std::set representation_context_cache_ids_; + std::vector representation_context_cache_priorities_; settings::OutputDimensionalityTypes representation_context_cache_dimensionality_ = settings::SURFACES_AND_SOLIDS; bool representation_context_cache_has_context_ids_ = false; + bool representation_context_cache_has_context_priorities_ = false; bool representation_context_cache_valid_ = false; std::mutex representation_context_cache_guard_; diff --git a/src/ifcgeom/tests/test_ifcopenshell_geometry.cpp b/src/ifcgeom/tests/test_ifcopenshell_geometry.cpp index 2bee8548e2..fbfd23309f 100644 --- a/src/ifcgeom/tests/test_ifcopenshell_geometry.cpp +++ b/src/ifcgeom/tests/test_ifcopenshell_geometry.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -68,6 +69,86 @@ std::size_t count_geo403_for_wall(hierarchy_helper& file, const IfcSc return log.count("GEO403"); } +IfcSchema::IfcShapeRepresentation add_box_representation( + hierarchy_helper& file, + IfcSchema::IfcRepresentationContext context, + const std::string& identifier, + double size) +{ + auto representation = file.create(); + representation.setContextOfItems(context); + representation.setRepresentationIdentifier(identifier); + representation.setRepresentationType("SweptSolid"); + representation.setItems(std::vector{}); + file.addBox(representation, size, size, size); + return representation; +} + +IfcSchema::IfcRepresentation select_representation( + hierarchy_helper& file, + const IfcSchema::IfcWallStandardCase& wall, + const std::vector& context_priorities) +{ + ifcopenshell::geometry::Settings settings; + settings.set("context-priorities", std::vector(context_priorities)); + + logger log; + log.output_format(logger::FMT_INMEMORY); + ifcopenshell::geometry::Converter converter( + ifcopenshell::geometry::kernels::construct(&file, "passthrough", settings), &file, settings, log); + + auto selected = converter.mapping()->representation_of(wall).as(); + REQUIRE(selected); + return selected; +} + +IfcSchema::IfcWallStandardCase add_wall_with_representations( + hierarchy_helper& file, + const std::vector& representations) +{ + auto wall = file.create(); + wall.setGlobalId(ifcopenshell::global_id()); + file.addBuildingProduct(wall); + + auto shape = file.create(); + shape.setRepresentations(representations); + wall.setRepresentation(shape); + + return wall; +} + +std::vector representation_tasks( + hierarchy_helper& file, + const std::vector& context_priorities) +{ + ifcopenshell::geometry::Settings settings; + settings.set("context-priorities", std::vector(context_priorities)); + + logger log; + log.output_format(logger::FMT_INMEMORY); + ifcopenshell::geometry::Converter converter( + ifcopenshell::geometry::kernels::construct(&file, "passthrough", settings), &file, settings, log); + + std::vector tasks; + std::vector filters; + converter.mapping()->get_representations(tasks, filters); + return tasks; +} + +const ifcopenshell::geometry::geometry_conversion_task* task_for_product( + const std::vector& tasks, + const express::Base& product) +{ + for (const auto& task : tasks) { + if (std::any_of(task.products.begin(), task.products.end(), [&](const express::Base& task_product) { + return task_product.id() == product.id(); + })) { + return &task; + } + } + return nullptr; +} + } // namespace TEST_CASE("IfcGeom C++ fixture creates walls below and above the void limit", "[ifcgeom][voids]") { @@ -79,3 +160,80 @@ TEST_CASE("IfcGeom C++ fixture creates walls below and above the void limit", "[ const auto above_limit_wall = create_wall_with_voids(above_limit_file, MAX_VOIDS + 1); REQUIRE(count_geo403_for_wall(above_limit_file, above_limit_wall) == 1); } + +TEST_CASE("IfcGeom context priorities select representations by subcontext filter", "[ifcgeom][context]") { + hierarchy_helper file; + + auto body_context = file.getRepresentationSubContext("Body", "Model"); + auto tesselation_context = file.getRepresentationSubContext("Tesselation", "Model"); + + auto wall = file.create(); + wall.setGlobalId(ifcopenshell::global_id()); + file.addBuildingProduct(wall); + + const auto body_representation = add_box_representation(file, body_context, "Body", 1000.0); + const auto tesselation_representation = add_box_representation(file, tesselation_context, "Tesselation", 500.0); + REQUIRE(body_context.RepresentationsInContext().size() == 1); + REQUIRE(tesselation_context.RepresentationsInContext().size() == 1); + + auto shape = file.create(); + shape.setRepresentations( + std::vector{tesselation_representation, body_representation}); + wall.setRepresentation(shape); + + REQUIRE(select_representation(file, wall, {"body"}).id() == body_representation.id()); + REQUIRE(select_representation(file, wall, {"tesselation"}).id() == tesselation_representation.id()); + REQUIRE( + select_representation(file, wall, {"tesselation[context-identifier=Tesselation]"}).id() == + tesselation_representation.id()); + REQUIRE( + select_representation(file, wall, {"tesselation[targetview=MODEL_VIEW]"}).id() == + tesselation_representation.id()); + REQUIRE( + select_representation(file, wall, {"tesselation[target_view=MODEL_VIEW]"}).id() == + tesselation_representation.id()); + REQUIRE( + select_representation(file, wall, {"body", "tesselation[targetview=MODEL_VIEW]"}).id() == + body_representation.id()); + REQUIRE( + select_representation(file, wall, {"tesselation[targetview=MODEL_VIEW]", "body"}).id() == + tesselation_representation.id()); +} + +TEST_CASE("IfcGeom context priorities create tasks from highest priority representation", "[ifcgeom][context]") { + hierarchy_helper file; + + auto body_context = file.getRepresentationSubContext("Body", "Model"); + auto tesselation_context = file.getRepresentationSubContext("Tesselation", "Model"); + + const auto body_representation = add_box_representation(file, body_context, "Body", 1000.0); + const auto tesselation_representation = add_box_representation(file, tesselation_context, "Tesselation", 500.0); + const auto body_only_representation = add_box_representation(file, body_context, "Body", 750.0); + + const auto wall_with_both = add_wall_with_representations( + file, + std::vector{body_representation, tesselation_representation}); + const auto wall_with_body_only = add_wall_with_representations( + file, + std::vector{body_only_representation}); + + auto tesselation_first_tasks = representation_tasks(file, {"tesselation[target_view=MODEL_VIEW]", "body"}); + REQUIRE(tesselation_first_tasks.size() == 2); + REQUIRE(tesselation_first_tasks[0].representation.id() == tesselation_representation.id()); + REQUIRE(tesselation_first_tasks[0].products.size() == 1); + REQUIRE(tesselation_first_tasks[0].products.front().id() == wall_with_both.id()); + REQUIRE(tesselation_first_tasks[1].representation.id() == body_only_representation.id()); + REQUIRE(tesselation_first_tasks[1].products.size() == 1); + REQUIRE(tesselation_first_tasks[1].products.front().id() == wall_with_body_only.id()); + + auto body_first_tasks = representation_tasks(file, {"body", "tesselation[targetview=MODEL_VIEW]"}); + REQUIRE(body_first_tasks.size() == 2); + + const auto* wall_with_both_task = task_for_product(body_first_tasks, wall_with_both); + REQUIRE(wall_with_both_task != nullptr); + REQUIRE(wall_with_both_task->representation.id() == body_representation.id()); + + const auto* wall_with_body_only_task = task_for_product(body_first_tasks, wall_with_body_only); + REQUIRE(wall_with_body_only_task != nullptr); + REQUIRE(wall_with_body_only_task->representation.id() == body_only_representation.id()); +}