diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index 20132250cd..9cdeea85e9 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -441,6 +441,27 @@ namespace ifcopenshell { static constexpr const char* const description = "Experimental as not all topology hash functions fully implemented"; static constexpr bool defaultvalue = false; }; + + struct DeferProcessingFirstElement : public SettingBase { + static constexpr const char* const name = "defer-processing-first-element"; + static constexpr const char* const description = "Don't process first element in Iterator::initialize call()"; + static constexpr bool defaultvalue = false; + }; + + struct MaxOffset : public SettingBase { + static constexpr const char* const name = "max-offset"; + static constexpr const char* const description = "Maximum translation offset to be observed after which median offset in model gets removed and logged. Requires --no-parallel-mapping."; + }; + + struct MaxOffsetDeviation : public SettingBase { + static constexpr const char* const name = "max-offset-deviation"; + static constexpr const char* const description = "To retain field of view, completely remove elements outside of the median offset. Requires --no-parallel-mapping."; + }; + + struct ApplyOffset : public SettingBase> { + static constexpr const char* const name = "apply-offset"; + static constexpr const char* const description = "Slight variation of --model-offset where large offsets are applied by negating existing large offsets to retain maximum precision. Requires --no-parallel-mapping."; + }; } namespace impl { @@ -616,7 +637,7 @@ namespace ifcopenshell { }; class IFC_GEOM_API Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/Iterator.cpp b/src/ifcgeom/Iterator.cpp index 0e82e2a678..7cb86aa49c 100644 --- a/src/ifcgeom/Iterator.cpp +++ b/src/ifcgeom/Iterator.cpp @@ -53,7 +53,7 @@ bool IfcGeom::Iterator::initialize() { if (settings_.get().get() && settings_.get().get()) { std::unordered_map< ifcopenshell::geometry::taxonomy::item::ptr, - std::vector>> folded; + std::vector>> folded; for (auto& r : tasks_) { auto i = r.item; @@ -94,6 +94,10 @@ bool IfcGeom::Iterator::initialize() { } } + if (settings_.get().get()) { + remove_offset_(); + } + size_t num_products = 0; for (auto& r : tasks_) { num_products += !settings_.get().get() ? r.products_2->size() : r.products.size(); @@ -136,7 +140,7 @@ bool IfcGeom::Iterator::initialize() { if (tasks_.size() == 0) { Logger::Warning("No representations encountered, aborting"); initialization_outcome_.reset(false); - } else { + } else if (!settings_.get().get()) { task_iterator_ = tasks_.begin(); @@ -153,6 +157,8 @@ bool IfcGeom::Iterator::initialize() { } else { initialization_outcome_ = create(); } + } else { + initialization_outcome_.reset(true); } return *initialization_outcome_; @@ -505,6 +511,10 @@ IfcGeom::Element* IfcGeom::Iterator::get() throw std::runtime_error("Iterator not initialized"); } + if (settings_.get().get() && !task_result_ptr_initialized) { + throw std::runtime_error("No elements processed"); + } + auto ret = *task_result_iterator_; // If we want to organize the element considering their hierarchy @@ -625,6 +635,178 @@ const IfcUtil::IfcBaseClass* IfcGeom::Iterator::create() { return product; } +ifcopenshell::geometry::taxonomy::direction3::ptr IfcGeom::Iterator::remove_offset_() { + + using namespace ifcopenshell::geometry::taxonomy; + using namespace ifcopenshell::geometry::settings; + + if (!settings_.get().has()) { + return nullptr; + } + + if (!settings_.get().get()) { + throw std::runtime_error("remove_offset() can only be called with defer-processing-first-element and no-parallel-mapping settings"); + } + + auto collect_offset = [&](const item::ptr& itm, const std::vector>& pr) -> std::pair { + std::function(const item::ptr&, Eigen::Matrix4d)> traverse; + traverse = [&](const item::ptr& node, Eigen::Matrix4d m4) -> std::pair { + if (auto shl = std::dynamic_pointer_cast(node)) { + auto p = shl->centroid(); + Eigen::Vector4d v; + v << p->components()(0), p->components()(1), p->components()(2), 1.0; + Eigen::Vector3d translation_part = (m4 * v).head<3>(); + double translation_amnt = translation_part.norm(); + if (translation_amnt > settings_.get().get()) { + return { translation_amnt, translation_part }; + } else { + return { 0.0, Eigen::Vector3d::Zero() }; + } + } else { + if (auto gi = std::dynamic_pointer_cast(node)) { + if (gi->matrix) { + m4 = m4 * gi->matrix->ccomponents(); + } + } + Eigen::Vector3d translation_part = m4.block<3, 1>(0, 3); + double translation_amnt = translation_part.norm(); + if (translation_amnt > settings_.get().get()) { + return { translation_amnt, translation_part }; + } else if (auto col = std::dynamic_pointer_cast(node)) { + std::vector> child_transforms; + for (const auto& child : col->children) { + child_transforms.push_back(traverse(child, m4)); + } + if (!child_transforms.empty()) { + return *std::max_element(child_transforms.begin(), child_transforms.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + } + } + return { 0.0, Eigen::Vector3d::Zero() }; + } + }; + + Eigen::Matrix4d m4 = Eigen::Matrix4d::Identity(); + if (pr.size() == 1 && pr[0].second) { + m4 = pr[0].second->ccomponents(); + } + return traverse(itm, m4); + }; + + Eigen::Vector3d vec; + + if (settings_.get().has()) { + auto vs = settings_.get().get(); + if (vs.size() != 3) { + throw std::runtime_error("ApplyOffset setting must be a vector of size 3"); + } + vec = Eigen::Vector3d(vs[0], vs[1], vs[2]); + } else { + // Collect all norms and vectors + std::vector norms; + std::vector vectors; + for (const auto& task : tasks_) { + auto result = collect_offset(task.item, task.products); + norms.push_back(result.first); + vectors.push_back(result.second); + } + + // Find the median norm index + std::vector sorted_norms = norms; + std::nth_element(sorted_norms.begin(), sorted_norms.begin() + sorted_norms.size() / 2, sorted_norms.end()); + double median = sorted_norms[sorted_norms.size() / 2]; + auto median_it = std::find(norms.begin(), norms.end(), median); + size_t median_index = std::distance(norms.begin(), median_it); + + if (median_index >= vectors.size()) { + return nullptr; + } + + vec = -vectors[median_index]; + } + + Eigen::Matrix4d translation_matrix = Eigen::Matrix4d::Identity(); + translation_matrix.block<3, 1>(0, 3) = vec; + + auto remove_offset = [&](const item::ptr& itm, const std::vector>& pr) -> bool { + std::function traverse; + traverse = [&](const item::ptr& node, Eigen::Matrix4d m4) -> bool { + if (auto shl = std::dynamic_pointer_cast(node)) { + auto p = shl->centroid(); + Eigen::Vector4d v; + v << p->components()(0), p->components()(1), p->components()(2), 1.0; + Eigen::Vector3d translation_part = (m4 * v).head<3>(); + double translation_amnt = translation_part.norm(); + if (translation_amnt > settings_.get().get()) { + shl->matrix = make(translation_matrix); + } + return true; + } else { + auto m4b = m4; + if (auto gi = std::dynamic_pointer_cast(node)) { + if (gi->matrix) { + m4b = m4 * gi->matrix->ccomponents(); + } + Eigen::Vector3d translation_part = m4b.block<3, 1>(0, 3); + double translation_amnt = translation_part.norm(); + if (translation_amnt > settings_.get().get()) { + auto inverted_rot_scale3 = m4.block<3, 3>(0, 0).inverse(); + Eigen::Matrix4d inverted_rot_scale = Eigen::Matrix4d::Identity(); + inverted_rot_scale.block<3, 3>(0, 0) = inverted_rot_scale3; + if (!gi->matrix) { + gi->matrix = make(); + } + gi->matrix->components() = (inverted_rot_scale * translation_matrix) * gi->matrix->ccomponents(); + return true; + } + } + bool b = true; + if (auto col = std::dynamic_pointer_cast(node)) { + for (const auto& child : col->children) { + if (!traverse(child, m4b)) { + b = false; + } + } + } + return b; + } + }; + + Eigen::Matrix4d m4 = Eigen::Matrix4d::Identity(); + if (pr.size() == 1 && pr[0].second) { + m4 = pr[0].second->ccomponents(); + } + return traverse(itm, m4); + }; + + size_t num_offset_applied = 0; + for (auto& task : tasks_) { + bool all_applied = true; + for (auto& p : task.products) { + auto bb = p.second->components().block<3, 1>(0, 3); + double translation_amnt = bb.norm(); + if (translation_amnt > settings_.get().get()) { + // block has an underlying mutable ref to the matrix + bb -= vec; + } else { + all_applied = false; + } + } + if (all_applied) { + num_offset_applied += 1; + continue; + } + if (remove_offset(task.item, task.products)) { + num_offset_applied += 1; + } + } + + Logger::Notice("Removed large offsets within " + std::to_string(num_offset_applied) + " products"); + Logger::Notice("Offset applied (" + std::to_string(vec(0)) + "," + std::to_string(vec(1)) + "," + std::to_string(vec(2)) + ")"); + + return make(vec); +} + IfcGeom::Iterator::~Iterator() { if (num_threads_ != 1) { terminating_ = true; diff --git a/src/ifcgeom/Iterator.h b/src/ifcgeom/Iterator.h index d43803feed..1f14024c1c 100644 --- a/src/ifcgeom/Iterator.h +++ b/src/ifcgeom/Iterator.h @@ -91,7 +91,7 @@ namespace IfcGeom { // For NoParallelMapping==true ifcopenshell::geometry::taxonomy::ptr item; - std::vector> products; + std::vector> products; // For NoParallelMapping==false IfcUtil::IfcBaseEntity* representation; @@ -209,7 +209,7 @@ namespace IfcGeom { void log_timepoints() const; - /// @todo public/private sections all over the place: move all public to the beginning of the class + ifcopenshell::geometry::taxonomy::direction3::ptr remove_offset_(); public: Iterator(const std::string& geometry_library, const ifcopenshell::geometry::Settings& settings, IfcParse::IfcFile* file, const std::vector& filters, int num_threads) : settings_(settings) @@ -262,6 +262,31 @@ namespace IfcGeom { void set_cache(GeometrySerializer* cache) { cache_ = cache; } + std::vector get_task_items() const { + std::vector items; + items.reserve(tasks_.size()); + for (const auto& task : tasks_) { + items.push_back(task.item); + } + return items; + } + + aggregate_of_aggregate_of_instance::ptr get_task_products() const { + aggregate_of_aggregate_of_instance::ptr products = aggregate_of_aggregate_of_instance::ptr(new aggregate_of_aggregate_of_instance); + for (const auto& task : tasks_) { + if (task.products_2) { + products->push(task.products_2); + } else { + for (auto& product : task.products) { + aggregate_of_instance::ptr p(new aggregate_of_instance); + p->push(product.first); + products->push(p); + } + } + } + return products; + } + const std::string& unit_name() const { return converter_->mapping()->get_length_unit_name(); } double unit_magnitude() const { return converter_->mapping()->get_length_unit(); } // Check if error occurred during iterator initialization or iteration over elements. diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index 7138d9fa89..849b9d5790 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -952,6 +952,21 @@ typedef item const* ptr; auto v = std::make_tuple(static_cast(LOOP), hash_elements(), external ? *external ? 2 : 1 : 0, closed ? *closed ? 2 : 1 : 0); return boost::hash{}(v); } + + // nb only takes into account explicit points + taxonomy::point3::ptr centroid() const { + Eigen::Vector3d c(0, 0, 0); + for (auto& e : children) { + if (e->start.which() == 1) { + c += boost::get(e->start)->ccomponents(); + } + if (e->end.which() == 1) { + c += boost::get(e->end)->ccomponents(); + } + } + c /= static_cast(children.size()); + return make(c); + } }; struct face : public collection_base { @@ -992,6 +1007,25 @@ typedef item const* ptr; auto v = std::make_tuple(static_cast(SHELL), hash_elements(), closed ? *closed ? 2 : 1 : 0); return boost::hash{}(v); } + + // nb only takes into account explicit points + taxonomy::point3::ptr centroid() const { + Eigen::Vector3d c(0, 0, 0); + for (auto& f : children) { + for (auto& l : f->children) { + for (auto& e : l->children) { + if (e->start.which() == 1) { + c += boost::get(e->start)->ccomponents(); + } + if (e->end.which() == 1) { + c += boost::get(e->end)->ccomponents(); + } + } + } + } + c /= static_cast(children.size()); + return make(c); + } }; struct solid : public collection_base { diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 927597996d..620cc026e9 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -299,8 +299,11 @@ class iterator(ifcopenshell_wrapper.Iterator): ): self.settings = settings if isinstance(file_or_filename, file): + self.file = file file_or_filename = file_or_filename.wrapped_data else: + # @todo? + self.file = None # Makes sure people are able to use python's platform agnostic paths file_or_filename = os.path.abspath(file_or_filename) @@ -346,6 +349,9 @@ class iterator(ifcopenshell_wrapper.Iterator): if not self.next(): break + def get_task_products(self): + return entity_instance.wrap_value(ifcopenshell_wrapper.Iterator.get_task_products(self), self.file) + ClashType = Literal["protrusion", "pierce", "collision", "clearance"] CLASH_TYPE_ITEMS = ("protrusion", "pierce", "collision", "clearance") @@ -453,9 +459,7 @@ def create_shape( geometry_library: GEOMETRY_LIBRARY = "opencascade", ) -> Union[ShapeType, ShapeElementType, ifcopenshell_wrapper.Transformation, utils.shape_tuple, TopoDS.TopoDS_Shape]: """ - Return a geometric representation from STEP-based IFCREPRESENTATIONSHAPE - or - Return an OpenCASCADE BRep if 'use-python-opencascade' is True + Returns a geometric interpretation of the IFC entity instance Note that in Python, you must store a reference to the element returned by this function to prevent garbage collection when you access its children. See #1124. @@ -504,6 +508,20 @@ def create_shape( ) +def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item: + """ + Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer. + In many cases this is somewhat equivalent to the raw IFC data (but schema-agnostic in C++), but + in other cases such as IfcParameterizedProfileDef the returned item is the equivalent + of an explicit composite curve. + + >>> point = ifc_file.by_type('IfcCartesianPoint')[0] + >>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components + (0.0, 0.0, 0.0) + """ + return ifcopenshell_wrapper.map_shape(settings, inst.wrapped_data) + + @overload def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ... @overload diff --git a/src/ifcwrap/utils/typemaps_out.i b/src/ifcwrap/utils/typemaps_out.i index 1c9761e4fe..10e4ab253c 100644 --- a/src/ifcwrap/utils/typemaps_out.i +++ b/src/ifcwrap/utils/typemaps_out.i @@ -6,6 +6,16 @@ } } +%typemap(out) aggregate_of_aggregate_of_instance::ptr { + const unsigned size = $1 ? $1->size() : 0; + $result = PyTuple_New(size); + for (unsigned i = 0; i < size; ++i) { + const auto& r_i = *(result->begin() + i); + PyTuple_SetItem($result, i, pythonize_vector(r_i)); + } +} + + %typemap(out) IfcUtil::ArgumentType { $result = SWIG_Python_str_FromChar(IfcUtil::ArgumentTypeToString($1)); } @@ -122,6 +132,14 @@ CREATE_VECTOR_TYPEMAP_OUT(IfcGeom::ConversionResultShape *) } }; +%typemap(out) std::vector { + const auto& v = (std::vector) $1; + $result = PyTuple_New(v.size()); + for (int i = 0; i < v.size(); ++i) { + PyTuple_SetItem($result, i, item_to_pyobject(v[i])); + } +}; + %typemap(out) const item_name::ptr& { $result = item_to_pyobject(*$1); };