diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index c5bbccb24..e600f343c 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -382,7 +382,7 @@ int CLI::run(int argc, char **argv) } else if (opt_key == "align_xy") { const Vec2d &p = m_config.option("align_xy")->value; for (auto &model : m_models) { - BoundingBoxf3 bb = model.bounding_box(); + BoundingBoxf3 bb = model.bounding_box_exact(); // this affects volumes: model.translate(-(bb.min.x() - p.x()), -(bb.min.y() - p.y()), -bb.min.z()); } @@ -423,7 +423,7 @@ int CLI::run(int argc, char **argv) } else if (opt_key == "cut" || opt_key == "cut_x" || opt_key == "cut_y") { std::vector new_models; for (auto &model : m_models) { - model.translate(0, 0, -model.bounding_box().min.z()); // align to z = 0 + model.translate(0, 0, -model.bounding_box_exact().min.z()); // align to z = 0 size_t num_objects = model.objects.size(); for (size_t i = 0; i < num_objects; ++ i) { diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index ac4af4828..4b2454848 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -918,4 +918,30 @@ double rotation_diff_z(const Transform3d &trafo_from, const Transform3d &trafo_t return atan2(vx.y(), vx.x()); } +bool trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(const Transform3d &t1, const Transform3d &t2) +{ + if (std::abs(t1.translation().z() - t2.translation().z()) > EPSILON) + // One of the object is higher than the other above the build plate (or below the build plate). + return false; + Matrix3d m1 = t1.matrix().block<3, 3>(0, 0); + Matrix3d m2 = t2.matrix().block<3, 3>(0, 0); + Matrix3d m = m2.inverse() * m1; + Vec3d z = m.block<3, 1>(0, 2); + if (std::abs(z.x()) > EPSILON || std::abs(z.y()) > EPSILON || std::abs(z.z() - 1.) > EPSILON) + // Z direction or length changed. + return false; + // Z still points in the same direction and it has the same length. + Vec3d x = m.block<3, 1>(0, 0); + Vec3d y = m.block<3, 1>(0, 1); + if (std::abs(x.z()) > EPSILON || std::abs(y.z()) > EPSILON) + return false; + double lx2 = x.squaredNorm(); + double ly2 = y.squaredNorm(); + if (lx2 - 1. > EPSILON * EPSILON || ly2 - 1. > EPSILON * EPSILON) + return false; + // Verify whether the vectors x, y are still perpendicular. + double d = x.dot(y); + return std::abs(d * d) < EPSILON * lx2 * ly2; +} + }} // namespace Slic3r::Geometry diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 1f287f6a7..ba7e7a4b2 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -584,6 +584,13 @@ inline bool is_rotation_ninety_degrees(const Vec3d &rotation) return is_rotation_ninety_degrees(rotation.x()) && is_rotation_ninety_degrees(rotation.y()) && is_rotation_ninety_degrees(rotation.z()); } +// Returns true if one transformation may be converted into another transformation by +// rotation around Z and by mirroring in X / Y only. Two objects sharing such transformation +// may share support structures and they share Z height. +bool trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(const Transform3d &t1, const Transform3d &t2); +inline bool trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(const Transformation &t1, const Transformation &t2) + { return trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(t1.get_matrix(), t2.get_matrix()); } + template std::pair dir_to_spheric(const Vec<3, Tin> &n, Tout norm = 1.) { diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 01b4c5f6f..dec734ca1 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -323,14 +323,30 @@ bool Model::add_default_instances() } // this returns the bounding box of the *transformed* instances -BoundingBoxf3 Model::bounding_box() const +BoundingBoxf3 Model::bounding_box_approx() const { BoundingBoxf3 bb; for (ModelObject *o : this->objects) - bb.merge(o->bounding_box()); + bb.merge(o->bounding_box_approx()); return bb; } +BoundingBoxf3 Model::bounding_box_exact() const +{ + BoundingBoxf3 bb; + for (ModelObject *o : this->objects) + bb.merge(o->bounding_box_exact()); + return bb; +} + +double Model::max_z() const +{ + double z = 0; + for (ModelObject *o : this->objects) + z = std::max(z, o->max_z()); + return z; +} + unsigned int Model::update_print_volume_state(const BuildVolume &build_volume) { unsigned int num_printable = 0; @@ -377,7 +393,7 @@ void Model::duplicate_objects_grid(size_t x, size_t y, coordf_t dist) ModelObject* object = this->objects.front(); object->clear_instances(); - Vec3d ext_size = object->bounding_box().size() + dist * Vec3d::Ones(); + Vec3d ext_size = object->bounding_box_exact().size() + dist * Vec3d::Ones(); for (size_t x_copy = 1; x_copy <= x; ++x_copy) { for (size_t y_copy = 1; y_copy <= y; ++y_copy) { @@ -548,13 +564,13 @@ void Model::adjust_min_z() if (objects.empty()) return; - if (bounding_box().min(2) < 0.0) + if (this->bounding_box_exact().min.z() < 0.0) { for (ModelObject* obj : objects) { if (obj != nullptr) { - coordf_t obj_min_z = obj->bounding_box().min(2); + coordf_t obj_min_z = obj->min_z(); if (obj_min_z < 0.0) obj->translate_instances(Vec3d(0.0, 0.0, -obj_min_z)); } @@ -627,12 +643,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs) this->printable = rhs.printable; this->origin_translation = rhs.origin_translation; this->cut_id.copy(rhs.cut_id); - m_bounding_box = rhs.m_bounding_box; - m_bounding_box_valid = rhs.m_bounding_box_valid; - m_raw_bounding_box = rhs.m_raw_bounding_box; - m_raw_bounding_box_valid = rhs.m_raw_bounding_box_valid; - m_raw_mesh_bounding_box = rhs.m_raw_mesh_bounding_box; - m_raw_mesh_bounding_box_valid = rhs.m_raw_mesh_bounding_box_valid; + this->copy_transformation_caches(rhs); this->clear_volumes(); this->volumes.reserve(rhs.volumes.size()); @@ -668,12 +679,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs) this->layer_height_profile = std::move(rhs.layer_height_profile); this->printable = std::move(rhs.printable); this->origin_translation = std::move(rhs.origin_translation); - m_bounding_box = std::move(rhs.m_bounding_box); - m_bounding_box_valid = std::move(rhs.m_bounding_box_valid); - m_raw_bounding_box = rhs.m_raw_bounding_box; - m_raw_bounding_box_valid = rhs.m_raw_bounding_box_valid; - m_raw_mesh_bounding_box = rhs.m_raw_mesh_bounding_box; - m_raw_mesh_bounding_box_valid = rhs.m_raw_mesh_bounding_box_valid; + this->copy_transformation_caches(rhs); this->clear_volumes(); this->volumes = std::move(rhs.volumes); @@ -864,16 +870,73 @@ void ModelObject::clear_instances() // Returns the bounding box of the transformed instances. // This bounding box is approximate and not snug. -const BoundingBoxf3& ModelObject::bounding_box() const +const BoundingBoxf3& ModelObject::bounding_box_approx() const { - if (! m_bounding_box_valid) { - m_bounding_box_valid = true; + if (! m_bounding_box_approx_valid) { + m_bounding_box_approx_valid = true; BoundingBoxf3 raw_bbox = this->raw_mesh_bounding_box(); - m_bounding_box.reset(); + m_bounding_box_approx.reset(); for (const ModelInstance *i : this->instances) - m_bounding_box.merge(i->transform_bounding_box(raw_bbox)); + m_bounding_box_approx.merge(i->transform_bounding_box(raw_bbox)); + } + return m_bounding_box_approx; +} + +// Returns the bounding box of the transformed instances. +// This bounding box is approximate and not snug. +const BoundingBoxf3& ModelObject::bounding_box_exact() const +{ + if (! m_bounding_box_exact_valid) { + m_bounding_box_exact_valid = true; + m_min_max_z_valid = true; + BoundingBoxf3 raw_bbox = this->raw_mesh_bounding_box(); + m_bounding_box_exact.reset(); + for (size_t i = 0; i < this->instances.size(); ++ i) + m_bounding_box_exact.merge(this->instance_bounding_box(i)); + } + return m_bounding_box_exact; +} + +double ModelObject::min_z() const +{ + const_cast(this)->update_min_max_z(); + return m_bounding_box_exact.min.z(); +} + +double ModelObject::max_z() const +{ + const_cast(this)->update_min_max_z(); + return m_bounding_box_exact.max.z(); +} + +void ModelObject::update_min_max_z() +{ + assert(! this->instances.empty()); + if (! m_min_max_z_valid && ! this->instances.empty()) { + m_min_max_z_valid = true; + const Transform3d mat_instance = this->instances.front()->get_transformation().get_matrix(); + double global_min_z = std::numeric_limits::max(); + double global_max_z = - std::numeric_limits::max(); + for (const ModelVolume *v : this->volumes) + if (v->is_model_part()) { + const Transform3d m = mat_instance * v->get_matrix(); + const Vec3d row_z = m.linear().row(2).cast(); + const double shift_z = m.translation().z(); + double this_min_z = std::numeric_limits::max(); + double this_max_z = - std::numeric_limits::max(); + for (const Vec3f &p : v->mesh().its.vertices) { + double z = row_z.dot(p.cast()); + this_min_z = std::min(this_min_z, z); + this_max_z = std::max(this_max_z, z); + } + this_min_z += shift_z; + this_max_z += shift_z; + global_min_z = std::min(global_min_z, this_min_z); + global_max_z = std::max(global_max_z, this_max_z); + } + m_bounding_box_exact.min.z() = global_min_z; + m_bounding_box_exact.max.z() = global_max_z; } - return m_bounding_box; } // A mesh containing all transformed instances of this object. @@ -1031,19 +1094,19 @@ void ModelObject::ensure_on_bed(bool allow_negative_z) if (allow_negative_z) { if (parts_count() == 1) { - const double min_z = get_min_z(); - const double max_z = get_max_z(); + const double min_z = this->min_z(); + const double max_z = this->max_z(); if (min_z >= SINKING_Z_THRESHOLD || max_z < 0.0) z_offset = -min_z; } else { - const double max_z = get_max_z(); + const double max_z = this->max_z(); if (max_z < SINKING_MIN_Z_THRESHOLD) z_offset = SINKING_MIN_Z_THRESHOLD - max_z; } } else - z_offset = -get_min_z(); + z_offset = -this->min_z(); if (z_offset != 0.0) translate_instances(z_offset * Vec3d::UnitZ()); @@ -1070,8 +1133,10 @@ void ModelObject::translate(double x, double y, double z) v->translate(x, y, z); } - if (m_bounding_box_valid) - m_bounding_box.translate(x, y, z); + if (m_bounding_box_approx_valid) + m_bounding_box_approx.translate(x, y, z); + if (m_bounding_box_exact_valid) + m_bounding_box_exact.translate(x, y, z); } void ModelObject::scale(const Vec3d &versor) @@ -1866,32 +1931,6 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) this->invalidate_bounding_box(); } -double ModelObject::get_min_z() const -{ - if (instances.empty()) - return 0.0; - else { - double min_z = DBL_MAX; - for (size_t i = 0; i < instances.size(); ++i) { - min_z = std::min(min_z, get_instance_min_z(i)); - } - return min_z; - } -} - -double ModelObject::get_max_z() const -{ - if (instances.empty()) - return 0.0; - else { - double max_z = -DBL_MAX; - for (size_t i = 0; i < instances.size(); ++i) { - max_z = std::max(max_z, get_instance_max_z(i)); - } - return max_z; - } -} - double ModelObject::get_instance_min_z(size_t instance_idx) const { double min_z = DBL_MAX; @@ -2268,7 +2307,7 @@ void ModelVolume::scale(const Vec3d& scaling_factors) void ModelObject::scale_to_fit(const Vec3d &size) { - Vec3d orig_size = this->bounding_box().size(); + Vec3d orig_size = this->bounding_box_exact().size(); double factor = std::min( size.x() / orig_size.x(), std::min( @@ -2373,37 +2412,6 @@ void ModelInstance::transform_mesh(TriangleMesh* mesh, bool dont_translate) cons #endif // ENABLE_WORLD_COORDINATE } -BoundingBoxf3 ModelInstance::transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate) const -{ - // Rotate around mesh origin. - TriangleMesh copy(mesh); -#if ENABLE_WORLD_COORDINATE - copy.transform(get_transformation().get_rotation_matrix()); -#else - copy.transform(get_matrix(true, false, true, true)); -#endif // ENABLE_WORLD_COORDINATE - BoundingBoxf3 bbox = copy.bounding_box(); - - if (!empty(bbox)) { - // Scale the bounding box along the three axes. - for (unsigned int i = 0; i < 3; ++i) - { - if (std::abs(get_scaling_factor((Axis)i)-1.0) > EPSILON) - { - bbox.min(i) *= get_scaling_factor((Axis)i); - bbox.max(i) *= get_scaling_factor((Axis)i); - } - } - - // Translate the bounding box. - if (! dont_translate) { - bbox.min += get_offset(); - bbox.max += get_offset(); - } - } - return bbox; -} - BoundingBoxf3 ModelInstance::transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate) const { #if ENABLE_WORLD_COORDINATE diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 2e0ec3e16..08fa79481 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -168,7 +168,7 @@ private: friend class cereal::access; friend class UndoRedo::StackImpl; // Create an object for deserialization, don't allocate IDs for ModelMaterial and its config. - ModelMaterial() : ObjectBase(-1), config(-1), m_model(nullptr) { assert(this->id().invalid()); assert(this->config.id().invalid()); } + ModelMaterial() : ObjectBase(-1), config(-1) { assert(this->id().invalid()); assert(this->config.id().invalid()); } template void serialize(Archive &ar) { assert(this->id().invalid()); assert(this->config.id().invalid()); Internal::StaticSerializationWrapper config_wrapper(config); @@ -343,7 +343,7 @@ public: // The pairs of are packed into a 1D array. LayerHeightProfile layer_height_profile; // Whether or not this object is printable - bool printable; + bool printable { true }; // This vector holds position of selected support points for SLA. The data are // saved in mesh coordinates to allow using them for several instances. @@ -397,11 +397,22 @@ public: void delete_last_instance(); void clear_instances(); - // Returns the bounding box of the transformed instances. - // This bounding box is approximate and not snug. - // This bounding box is being cached. - const BoundingBoxf3& bounding_box() const; - void invalidate_bounding_box() { m_bounding_box_valid = false; m_raw_bounding_box_valid = false; m_raw_mesh_bounding_box_valid = false; } + // Returns the bounding box of the transformed instances. This bounding box is approximate and not snug, it is being cached. + const BoundingBoxf3& bounding_box_approx() const; + // Returns an exact bounding box of the transformed instances. The result it is being cached. + const BoundingBoxf3& bounding_box_exact() const; + // Return minimum / maximum of a printable object transformed into the world coordinate system. + // All instances share the same min / max Z. + double min_z() const; + double max_z() const; + + void invalidate_bounding_box() { + m_bounding_box_approx_valid = false; + m_bounding_box_exact_valid = false; + m_min_max_z_valid = false; + m_raw_bounding_box_valid = false; + m_raw_mesh_bounding_box_valid = false; + } // A mesh containing all transformed instances of this object. TriangleMesh mesh() const; @@ -477,8 +488,6 @@ public: // Rotation and mirroring is being baked in. In case the instance scaling was non-uniform, it is baked in as well. void bake_xy_rotation_into_meshes(size_t instance_idx); - double get_min_z() const; - double get_max_z() const; double get_instance_min_z(size_t instance_idx) const; double get_instance_max_z(size_t instance_idx) const; @@ -500,14 +509,13 @@ public: private: friend class Model; // This constructor assigns new ID to this ModelObject and its config. - explicit ModelObject(Model* model) : m_model(model), printable(true), origin_translation(Vec3d::Zero()), - m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + explicit ModelObject(Model* model) : m_model(model), origin_translation(Vec3d::Zero()) { assert(this->id().valid()); assert(this->config.id().valid()); assert(this->layer_height_profile.id().valid()); } - explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), m_model(nullptr), printable(true), origin_translation(Vec3d::Zero()), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) + explicit ModelObject(int) : ObjectBase(-1), config(-1), layer_height_profile(-1), origin_translation(Vec3d::Zero()) { assert(this->id().invalid()); assert(this->config.id().invalid()); @@ -585,15 +593,31 @@ private: OBJECTBASE_DERIVED_COPY_MOVE_CLONE(ModelObject) // Parent object, owning this ModelObject. Set to nullptr here, so the macros above will have it initialized. - Model *m_model = nullptr; + Model *m_model { nullptr }; // Bounding box, cached. - mutable BoundingBoxf3 m_bounding_box; - mutable bool m_bounding_box_valid; + mutable BoundingBoxf3 m_bounding_box_approx; + mutable bool m_bounding_box_approx_valid { false }; + mutable BoundingBoxf3 m_bounding_box_exact; + mutable bool m_bounding_box_exact_valid { false }; + mutable bool m_min_max_z_valid { false }; mutable BoundingBoxf3 m_raw_bounding_box; - mutable bool m_raw_bounding_box_valid; + mutable bool m_raw_bounding_box_valid { false }; mutable BoundingBoxf3 m_raw_mesh_bounding_box; - mutable bool m_raw_mesh_bounding_box_valid; + mutable bool m_raw_mesh_bounding_box_valid { false }; + + // Only use this method if now the source and dest ModelObjects are equal, for example they were synchronized by Print::apply(). + void copy_transformation_caches(const ModelObject &src) { + m_bounding_box_approx = src.m_bounding_box_approx; + m_bounding_box_approx_valid = src.m_bounding_box_approx_valid; + m_bounding_box_exact = src.m_bounding_box_exact; + m_bounding_box_exact_valid = src.m_bounding_box_exact_valid; + m_min_max_z_valid = src.m_min_max_z_valid; + m_raw_bounding_box = src.m_raw_bounding_box; + m_raw_bounding_box_valid = src.m_raw_bounding_box_valid; + m_raw_mesh_bounding_box = src.m_raw_mesh_bounding_box; + m_raw_mesh_bounding_box_valid = src.m_raw_mesh_bounding_box_valid; + } // Called by Print::apply() to set the model pointer after making a copy. friend class Print; @@ -605,8 +629,7 @@ private: friend class UndoRedo::StackImpl; // Used for deserialization -> Don't allocate any IDs for the ModelObject or its config. ModelObject() : - ObjectBase(-1), config(-1), layer_height_profile(-1), - m_model(nullptr), m_bounding_box_valid(false), m_raw_bounding_box_valid(false), m_raw_mesh_bounding_box_valid(false) { + ObjectBase(-1), config(-1), layer_height_profile(-1) { assert(this->id().invalid()); assert(this->config.id().invalid()); assert(this->layer_height_profile.id().invalid()); @@ -617,12 +640,17 @@ private: Internal::StaticSerializationWrapper layer_heigth_profile_wrapper(layer_height_profile); ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper, sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, - m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid, + m_bounding_box_approx, m_bounding_box_approx_valid, + m_bounding_box_exact, m_bounding_box_exact_valid, m_min_max_z_valid, + m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid, cut_connectors, cut_id); } // Called by Print::validate() from the UI thread. unsigned int update_instances_print_volume_state(const BuildVolume &build_volume); + + // Called by min_z(), max_z() + void update_min_max_z(); }; enum class EnforcerBlockerType : int8_t { @@ -1106,7 +1134,7 @@ public: // flag showing the position of this instance with respect to the print volume (set by Print::validate() using ModelObject::check_instances_print_volume_state()) ModelInstanceEPrintVolumeState print_volume_state; // Whether or not this instance is printable - bool printable; + bool printable { true }; ModelObject* get_object() const { return this->object; } @@ -1156,9 +1184,7 @@ public: // To be called on an external mesh void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const; - // Calculate a bounding box of a transformed mesh. To be called on an external mesh. - BoundingBoxf3 transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate = false) const; - // Transform an external bounding box. + // Transform an external bounding box, thus the resulting bounding box is no more snug. BoundingBoxf3 transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate = false) const; // Transform an external vector. Vec3d transform_vector(const Vec3d& v, bool dont_translate = false) const; @@ -1201,7 +1227,7 @@ private: ModelObject* object; // Constructor, which assigns a new unique ID. - explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), printable(true), object(object) { assert(this->id().valid()); } + explicit ModelInstance(ModelObject* object) : print_volume_state(ModelInstancePVS_Inside), object(object) { assert(this->id().valid()); } // Constructor, which assigns a new unique ID. explicit ModelInstance(ModelObject *object, const ModelInstance &other) : m_transformation(other.m_transformation), print_volume_state(ModelInstancePVS_Inside), printable(other.printable), object(object) { assert(this->id().valid() && this->id() != other.id()); } @@ -1316,8 +1342,12 @@ public: void delete_material(t_model_material_id material_id); void clear_materials(); bool add_default_instances(); - // Returns approximate axis aligned bounding box of this model - BoundingBoxf3 bounding_box() const; + // Returns approximate axis aligned bounding box of this model. + BoundingBoxf3 bounding_box_approx() const; + // Returns exact axis aligned bounding box of this model. + BoundingBoxf3 bounding_box_exact() const; + // Return maximum height of all printable objects. + double max_z() const; // Set the print_volume_state of PrintObject::instances, // return total number of printable objects. unsigned int update_print_volume_state(const BuildVolume &build_volume); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e137f0157..ae5f18fcb 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1250,7 +1250,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ l->get_transformation().get_matrix().isApprox(r->get_transformation().get_matrix()); })) { // If some of the instances changed, the bounding box of the updated ModelObject is likely no more valid. // This is safe as the ModelObject's bounding box is only accessed from this function, which is called from the main thread only. - model_object.invalidate_bounding_box(); // Synchronize the content of instances. auto new_instance = model_object_new.instances.begin(); for (auto old_instance = model_object.instances.begin(); old_instance != model_object.instances.end(); ++ old_instance, ++ new_instance) { @@ -1259,6 +1258,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ (*old_instance)->printable = (*new_instance)->printable; } } + // Source / dest object share the same bounding boxes, just copy them. + model_object.copy_transformation_caches(model_object_new); } } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c6dbd7e4c..19056b682 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -91,7 +91,7 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor Vec3d bbox_center = bbox.center(); // We may need to rotate the bbox / bbox_center from the original instance to the current instance. double z_diff = Geometry::rotation_diff_z(model_object->instances.front()->get_matrix(), instances.front().model_instance->get_matrix()); - if (std::abs(z_diff) > EPSILON) { + if (std::abs(z_diff) > EPSILON) { auto z_rot = Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()); bbox = bbox.transformed(Transform3d(z_rot)); bbox_center = (z_rot * bbox_center).eval(); @@ -101,6 +101,7 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor m_center_offset = Point::new_scale(bbox_center.x(), bbox_center.y()); // Size of the transformed mesh. This bounding may not be snug in XY plane, but it is snug in Z. m_size = (bbox.size() * (1. / SCALING_FACTOR)).cast(); + m_size.z() = model_object->max_z(); this->set_instances(std::move(instances)); } @@ -2251,7 +2252,7 @@ void PrintObject::update_slicing_parameters() { if (!m_slicing_params.valid) m_slicing_params = SlicingParameters::create_from_config( - this->print()->config(), m_config, this->model_object()->bounding_box().max.z(), this->object_extruders()); + this->print()->config(), m_config, this->model_object()->max_z(), this->object_extruders()); } SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig& full_config, const ModelObject& model_object, float object_max_z) diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index 10769bff8..981519ca6 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -335,6 +335,7 @@ SupportParameters::SupportParameters(const PrintObject &object) this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height)); this->support_material_flow = Slic3r::support_material_flow(&object, float(slicing_params.layer_height)); this->support_material_interface_flow = Slic3r::support_material_interface_flow(&object, float(slicing_params.layer_height)); + this->raft_interface_flow = support_material_interface_flow; // Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um. this->support_layer_height_min = scaled(0.01); @@ -377,13 +378,14 @@ SupportParameters::SupportParameters(const PrintObject &object) this->base_angle = Geometry::deg2rad(float(object_config.support_material_angle.value)); this->interface_angle = Geometry::deg2rad(float(object_config.support_material_angle.value + 90.)); - this->interface_spacing = object_config.support_material_interface_spacing.value + this->support_material_interface_flow.spacing(); - this->interface_density = std::min(1., this->support_material_interface_flow.spacing() / this->interface_spacing); - this->support_spacing = object_config.support_material_spacing.value + this->support_material_flow.spacing(); - this->support_density = std::min(1., this->support_material_flow.spacing() / this->support_spacing); + double interface_spacing = object_config.support_material_interface_spacing.value + this->support_material_interface_flow.spacing(); + this->interface_density = std::min(1., this->support_material_interface_flow.spacing() / interface_spacing); + double raft_interface_spacing = object_config.support_material_interface_spacing.value + this->raft_interface_flow.spacing(); + this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing); + double support_spacing = object_config.support_material_spacing.value + this->support_material_flow.spacing(); + this->support_density = std::min(1., this->support_material_flow.spacing() / support_spacing); if (object_config.support_material_interface_layers.value == 0) { // No interface layers allowed, print everything with the base support pattern. - this->interface_spacing = this->support_spacing; this->interface_density = this->support_density; } @@ -393,6 +395,7 @@ SupportParameters::SupportParameters(const PrintObject &object) support_pattern == smpHoneycomb ? ipHoneycomb : this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase; this->interface_fill_pattern = (this->interface_density > 0.95 ? ipRectilinear : ipSupportBase); + this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase; this->contact_fill_pattern = (object_config.support_material_interface_pattern == smipAuto && slicing_params.soluble_interface) || object_config.support_material_interface_pattern == smipConcentric ? @@ -4240,11 +4243,11 @@ void generate_support_toolpaths( for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) { assert(support_layer_id < raft_layers.size()); - SupportLayer &support_layer = *support_layers[support_layer_id]; + SupportLayer &support_layer = *support_layers[support_layer_id]; assert(support_layer.support_fills.entities.empty()); SupportGeneratorLayer &raft_layer = *raft_layers[support_layer_id]; - std::unique_ptr filler_interface = std::unique_ptr(Fill::new_from_type(support_params.interface_fill_pattern)); + std::unique_ptr filler_interface = std::unique_ptr(Fill::new_from_type(support_params.raft_interface_fill_pattern)); std::unique_ptr filler_support = std::unique_ptr(Fill::new_from_type(support_params.base_fill_pattern)); filler_interface->set_bounding_box(bbox_object); filler_support->set_bounding_box(bbox_object); @@ -4299,8 +4302,8 @@ void generate_support_toolpaths( // value that guarantees that all layers are correctly aligned. filler->spacing = support_params.support_material_flow.spacing(); assert(! raft_layer.bridging); - flow = Flow(float(support_params.support_material_interface_flow.width()), float(raft_layer.height), support_params.support_material_flow.nozzle_diameter()); - density = float(support_params.interface_density); + flow = Flow(float(support_params.raft_interface_flow.width()), float(raft_layer.height), support_params.raft_interface_flow.nozzle_diameter()); + density = float(support_params.raft_interface_density); } else continue; filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); @@ -4343,7 +4346,7 @@ void generate_support_toolpaths( tbb::parallel_for(tbb::blocked_range(n_raft_layers, support_layers.size()), [&config, &support_params, &support_layers, &bottom_contacts, &top_contacts, &intermediate_layers, &interface_layers, &base_interface_layers, &layer_caches, &loop_interface_processor, - &bbox_object, &angles, link_max_length_factor] + &bbox_object, &angles, n_raft_layers, link_max_length_factor] (const tbb::blocked_range& range) { // Indices of the 1st layer in their respective container at the support layer height. size_t idx_layer_bottom_contact = size_t(-1); @@ -4357,6 +4360,11 @@ void generate_support_toolpaths( auto filler_first_layer_ptr = std::unique_ptr(range.begin() == 0 && support_params.contact_fill_pattern != fill_type_first_layer ? Fill::new_from_type(fill_type_first_layer) : nullptr); // Pointer to the 1st layer interface filler. auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get(); + // Filler for the 1st layer interface, if different from filler_interface. + auto filler_raft_contact_ptr = std::unique_ptr(range.begin() == n_raft_layers && config.support_material_interface_layers.value == 0 ? + Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr); + // Pointer to the 1st layer interface filler. + auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get(); // Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer). auto filler_base_interface = std::unique_ptr(base_interface_layers.empty() ? nullptr : Fill::new_from_type(support_params.interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase)); @@ -4364,6 +4372,8 @@ void generate_support_toolpaths( filler_interface->set_bounding_box(bbox_object); if (filler_first_layer_ptr) filler_first_layer_ptr->set_bounding_box(bbox_object); + if (filler_raft_contact_ptr) + filler_raft_contact_ptr->set_bounding_box(bbox_object); if (filler_base_interface) filler_base_interface->set_bounding_box(bbox_object); filler_support->set_bounding_box(bbox_object); @@ -4402,10 +4412,12 @@ void generate_support_toolpaths( if (idx_layer_intermediate < intermediate_layers.size() && intermediate_layers[idx_layer_intermediate]->print_z < support_layer.print_z + EPSILON) base_layer.layer = intermediate_layers[idx_layer_intermediate]; + bool raft_layer = support_layer_id == n_raft_layers; if (config.support_material_interface_layers == 0) { // If no top interface layers were requested, we treat the contact layer exactly as a generic base layer. - if (support_params.can_merge_support_regions) { - if (base_layer.could_merge(top_contact_layer)) + // Don't merge the raft contact layer though. + if (support_params.can_merge_support_regions && ! raft_layer) { + if (base_layer.could_merge(top_contact_layer)) base_layer.merge(std::move(top_contact_layer)); else if (base_layer.empty()) base_layer = std::move(top_contact_layer); @@ -4415,7 +4427,7 @@ void generate_support_toolpaths( // If no loops are allowed, we treat the contact layer exactly as a generic interface layer. // Merge interface_layer into top_contact_layer, as the top_contact_layer is not synchronized and therefore it will be used // to trim other layers. - if (top_contact_layer.could_merge(interface_layer)) + if (top_contact_layer.could_merge(interface_layer) && ! raft_layer) top_contact_layer.merge(std::move(interface_layer)); } if ((config.support_material_interface_layers == 0 || config.support_material_bottom_interface_layers == 0) && support_params.can_merge_support_regions) { @@ -4423,7 +4435,7 @@ void generate_support_toolpaths( base_layer.merge(std::move(bottom_contact_layer)); else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging) base_layer = std::move(bottom_contact_layer); - } else if (bottom_contact_layer.could_merge(top_contact_layer)) + } else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) top_contact_layer.merge(std::move(bottom_contact_layer)); else if (bottom_contact_layer.could_merge(interface_layer)) bottom_contact_layer.merge(std::move(interface_layer)); @@ -4441,35 +4453,44 @@ void generate_support_toolpaths( #endif // Top and bottom contacts, interface layers. - for (size_t i = 0; i < 3; ++ i) { - SupportGeneratorLayerExtruded &layer_ex = (i == 0) ? top_contact_layer : (i == 1 ? bottom_contact_layer : interface_layer); - if (layer_ex.empty() || layer_ex.polygons_to_extrude().empty()) - continue; - bool interface_as_base = config.support_material_interface_layers.value == 0 || - (config.support_material_bottom_interface_layers == 0 && &layer_ex == &bottom_contact_layer); - //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore - // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) - auto interface_flow = layer_ex.layer->bridging ? - Flow::bridging_flow(layer_ex.layer->height, support_params.support_material_bottom_interface_flow.nozzle_diameter()) : - (interface_as_base ? &support_params.support_material_flow : &support_params.support_material_interface_flow)->with_height(float(layer_ex.layer->height)); - filler_interface->angle = interface_as_base ? - // If zero interface layers are configured, use the same angle as for the base layers. - angles[support_layer_id % angles.size()] : - // Use interface angle for the interface layers. - support_params.interface_angle + interface_angle_delta; - double density = interface_as_base ? support_params.support_density : support_params.interface_density; - filler_interface->spacing = interface_as_base ? support_params.support_material_flow.spacing() : support_params.support_material_interface_flow.spacing(); - filler_interface->link_max_length = coord_t(scale_(filler_interface->spacing * link_max_length_factor / density)); - fill_expolygons_generate_paths( - // Destination - layer_ex.extrusions, - // Regions to fill - union_safety_offset_ex(layer_ex.polygons_to_extrude()), - // Filler and its parameters - filler_interface.get(), float(density), - // Extrusion parameters - ExtrusionRole::SupportMaterialInterface, interface_flow); - } + enum class InterfaceLayerType { TopContact, BottomContact, RaftContact, Interface, InterfaceAsBase }; + auto extrude_interface = [&](SupportGeneratorLayerExtruded &layer_ex, InterfaceLayerType interface_layer_type) { + if (! layer_ex.empty() && ! layer_ex.polygons_to_extrude().empty()) { + bool interface_as_base = interface_layer_type == InterfaceLayerType::InterfaceAsBase; + bool raft_contact = interface_layer_type == InterfaceLayerType::RaftContact; + //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore + // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) + auto *filler = raft_contact ? filler_raft_contact : filler_interface.get(); + auto interface_flow = layer_ex.layer->bridging ? + Flow::bridging_flow(layer_ex.layer->height, support_params.support_material_bottom_interface_flow.nozzle_diameter()) : + (raft_contact ? &support_params.raft_interface_flow : + interface_as_base ? &support_params.support_material_flow : &support_params.support_material_interface_flow) + ->with_height(float(layer_ex.layer->height)); + filler->angle = interface_as_base ? + // If zero interface layers are configured, use the same angle as for the base layers. + angles[support_layer_id % angles.size()] : + // Use interface angle for the interface layers. + support_params.interface_angle + interface_angle_delta; + double density = raft_contact ? support_params.raft_interface_density : interface_as_base ? support_params.support_density : support_params.interface_density; + filler->spacing = raft_contact ? support_params.raft_interface_flow.spacing() : + interface_as_base ? support_params.support_material_flow.spacing() : support_params.support_material_interface_flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); + fill_expolygons_generate_paths( + // Destination + layer_ex.extrusions, + // Regions to fill + union_safety_offset_ex(layer_ex.polygons_to_extrude()), + // Filler and its parameters + filler, float(density), + // Extrusion parameters + ExtrusionRole::SupportMaterialInterface, interface_flow); + } + }; + const bool top_interfaces = config.support_material_interface_layers.value != 0; + const bool bottom_interfaces = top_interfaces && config.support_material_bottom_interface_layers != 0; + extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase); + extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase); + extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase); // Base interface layers under soluble interfaces if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) { diff --git a/src/libslic3r/SupportMaterial.hpp b/src/libslic3r/SupportMaterial.hpp index ecfb5a761..eda70517f 100644 --- a/src/libslic3r/SupportMaterial.hpp +++ b/src/libslic3r/SupportMaterial.hpp @@ -122,10 +122,17 @@ using SupportGeneratorLayersPtr = std::vector; struct SupportParameters { SupportParameters(const PrintObject &object); + // Flow at the 1st print layer. Flow first_layer_flow; + // Flow at the support base (neither top, nor bottom interface). + // Also flow at the raft base with the exception of raft interface and contact layers. Flow support_material_flow; + // Flow at the top interface and contact layers. Flow support_material_interface_flow; + // Flow at the bottom interfaces and contacts. Flow support_material_bottom_interface_flow; + // Flow at raft inteface & contact layers. + Flow raft_interface_flow; // Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder? bool can_merge_support_regions; @@ -136,14 +143,23 @@ struct SupportParameters { float base_angle; float interface_angle; - coordf_t interface_spacing; + + // Density of the top / bottom interface and contact layers. coordf_t interface_density; - coordf_t support_spacing; + // Density of the raft interface and contact layers. + coordf_t raft_interface_density; + // Density of the base support layers. coordf_t support_density; + // Pattern of the sparse infill including sparse raft layers. InfillPattern base_fill_pattern; + // Pattern of the top / bottom interface and contact layers. InfillPattern interface_fill_pattern; + // Pattern of the raft interface and contact layers. + InfillPattern raft_interface_fill_pattern; + // Pattern of the contact layers. InfillPattern contact_fill_pattern; + // Shall the sparse (base) layers be printed with a single perimeter line (sheath) for robustness? bool with_sheath; }; diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 38761405e..05a42083d 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -1440,7 +1440,7 @@ static void generate_initial_areas( top_contacts[i] = nullptr; move_bounds[i].clear(); } - if (raft_contact_layer_idx != std::numeric_limits::max() && print_object.config().raft_expansion.value > 0) { + if (raft_contact_layer_idx != std::numeric_limits::max() && print_object.config().raft_expansion.value > 0) { // If any tips at first_tree_layer now are completely inside the expanded raft layer, remove them as well before they are propagated to the ground. Polygons &raft_polygons = top_contacts[raft_contact_layer_idx]->polygons; EdgeGrid::Grid grid(get_extents(raft_polygons).inflated(SCALED_EPSILON)); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 24f692d11..74f7bf50e 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2246,7 +2246,7 @@ void GCodeViewer::load_shells(const Print& print) if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF) { // adds wipe tower's volume - const double max_z = print.objects()[0]->model_object()->get_model()->bounding_box().max(2); + const double max_z = print.objects()[0]->model_object()->get_model()->max_z(); const PrintConfig& config = print.config(); const size_t extruders_count = config.nozzle_diameter.size(); if (extruders_count > 1 && config.wipe_tower && !config.complete_objects) { diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 427c0e99f..d459c87de 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -140,7 +140,7 @@ void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id) // Maximum height of an object changes when the object gets rotated or scaled. // Changing maximum height of an object will invalidate the layer heigth editing profile. // m_model_object->bounding_box() is cached, therefore it is cheap even if this method is called frequently. - const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast(model_object_new->bounding_box().max.z()); + const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast(model_object_new->max_z()); if (m_model_object != model_object_new || this->last_object_id != object_id || m_object_max_z != new_max_z || (model_object_new != nullptr && m_model_object->id() != model_object_new->id())) { m_layer_height_profile.clear(); @@ -1977,7 +1977,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re if (extruders_count > 1 && wt && !co) { // Height of a print (Show at least a slab) - const double height = std::max(m_model->bounding_box().max.z(), 10.0); + const double height = std::max(m_model->max_z(), 10.0); const float x = dynamic_cast(m_config->option("wipe_tower_x"))->value; const float y = dynamic_cast(m_config->option("wipe_tower_y"))->value; @@ -7100,12 +7100,10 @@ const ModelVolume *get_model_volume(const GLVolume &v, const Model &model) { const ModelVolume * ret = nullptr; - if (model.objects.size() < v.object_idx()) { - if (v.object_idx() < model.objects.size()) { - const ModelObject *obj = model.objects[v.object_idx()]; - if (v.volume_idx() < obj->volumes.size()) { - ret = obj->volumes[v.volume_idx()]; - } + if (v.object_idx() < model.objects.size()) { + const ModelObject *obj = model.objects[v.object_idx()]; + if (v.volume_idx() < obj->volumes.size()) { + ret = obj->volumes[v.volume_idx()]; } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index dc33b56fb..a522f5533 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -1338,7 +1338,8 @@ void GLGizmoCut3D::update_bb() on_unregister_raycasters_for_picking(); clear_selection(); - if (CommonGizmosDataObjects::SelectionInfo* selection = m_c->selection_info()) + if (CommonGizmosDataObjects::SelectionInfo* selection = m_c->selection_info(); + selection && selection->model_object()) m_selected.resize(selection->model_object()->cut_connectors.size(), false); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 73617531d..6b9df9217 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -3520,7 +3520,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const ModelObject* old_model_object = model.objects[object_idx]; ModelVolume* old_volume = old_model_object->volumes[volume_idx]; - bool sinking = old_model_object->bounding_box().min.z() < SINKING_Z_THRESHOLD; + bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; ModelObject* new_model_object = new_model.objects.front(); old_model_object->add_volume(*new_model_object->volumes.front()); @@ -3835,7 +3835,7 @@ void Plater::priv::reload_from_disk() ModelObject* old_model_object = model.objects[obj_idx]; ModelVolume* old_volume = old_model_object->volumes[vol_idx]; - bool sinking = old_model_object->bounding_box().min.z() < SINKING_Z_THRESHOLD; + bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); @@ -4816,7 +4816,7 @@ bool Plater::priv::layers_height_allowed() const return false; int obj_idx = get_selected_object_idx(); - return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->bounding_box().max.z() > SINKING_Z_THRESHOLD && + return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && config->opt_bool("variable_layer_height") && view3D->is_layers_editing_allowed(); } @@ -7435,7 +7435,7 @@ void Plater::changed_objects(const std::vector& object_idxs) for (size_t obj_idx : object_idxs) { if (obj_idx < p->model.objects.size()) { - if (p->model.objects[obj_idx]->bounding_box().min.z() >= SINKING_Z_THRESHOLD) + if (p->model.objects[obj_idx]->min_z() >= SINKING_Z_THRESHOLD) // re - align to Z = 0 p->model.objects[obj_idx]->ensure_on_bed(); }