ENH: auto arrange wipe tower when changing bed

Allow wipe tower to be arranged with highest priority.

jira: STUDIO-10225
Change-Id: Ia88374666906afd226bf8580d28fe788dad5f4c0
This commit is contained in:
Arthur 2025-02-13 16:03:54 +08:00 committed by lane.wei
parent e684f8fcc6
commit 2dfb9a15f4
7 changed files with 182 additions and 52 deletions

View File

@ -559,10 +559,12 @@ public:
template<class Range = ConstItemRange<typename Base::DefaultIter>>
PackResult trypack(Item& item,
const Range& remaining = Range()) {
auto result = _trypack(item, remaining);
if (item.is_wipe_tower) {
PackResult result1 = _trypack_with_original_pos(item);
if (result1.score() >= 0 && result1.score() < LARGE_COST_TO_REJECT) return result1;
}
// Experimental
// if(!result) repack(item, result);
auto result = _trypack(item, remaining);
return result;
}
@ -1003,6 +1005,101 @@ private:
item.rotation(final_rot);
}
#ifdef SVGTOOLS_HPP
if (config_.save_svg) {
svg::SVGWriter<RawShape> svgwriter;
Box binbb2(binbb.width() * 2, binbb.height() * 2, binbb.center()); // expand bbox to allow object be drawed outside
svgwriter.setSize(binbb2);
svgwriter.conf_.x0 = binbb.width();
svgwriter.conf_.y0 = -binbb.height() / 2; // origin is top left corner
svgwriter.add_comment("bed");
svgwriter.writeShape(box2RawShape(binbb), "none", "black");
svgwriter.add_comment("nfps");
for (int i = 0; i < nfps.size(); i++) svgwriter.writeShape(nfps[i], "none", "blue");
for (int i = 0; i < items_.size(); i++) {
svgwriter.add_comment(items_[i].get().name);
svgwriter.writeItem(items_[i], "none", "black");
}
svgwriter.add_comment("merged_pile_");
for (int i = 0; i < merged_pile_.size(); i++) svgwriter.writeShape(merged_pile_[i], "none", "yellow");
svgwriter.add_comment("current item");
svgwriter.writeItem(item, "red", "red", 2);
std::stringstream ss;
ss.setf(std::ios::fixed | std::ios::showpoint);
ss.precision(1);
ss << "t=" << round(item.translation().x() / 1e6) << ","
<< round(item.translation().y() / 1e6)
//<< "-rot=" << round(item.rotation().toDegrees())
<< "-sco=" << round(global_score);
svgwriter.draw_text(20, 20, ss.str(), "blue", 20);
ss.str("");
ss << "items.size=" << items_.size() << "-merged_pile.size=" << merged_pile_.size();
svgwriter.draw_text(20, 40, ss.str(), "blue", 20);
svgwriter.save(boost::filesystem::path("C:/Users/arthur.tang/AppData/Roaming/BambuStudioInternal/SVG") /
("nfpplacer_" + std::to_string(plate_id) + "_" + ss.str() + "_" + item.name + ".svg"));
}
#endif
if(can_pack) {
ret = PackResult(item);
ret.score_ = global_score;
//merged_pile_ = nfp::merge(merged_pile_, item.transformedShape());
} else {
ret = PackResult(best_overfit);
}
return ret;
}
PackResult _trypack_with_original_pos(Item &item)
{
PackResult ret;
bool can_pack = false;
double best_overfit = std::numeric_limits<double>::max();
double global_score = std::numeric_limits<double>::max();
auto initial_tr = item.translation();
auto initial_rot = item.rotation();
Vertex final_tr = initial_tr;
Radians final_rot = initial_rot;
Shapes nfps;
auto binbb = sl::boundingBox(bin_);
for (auto &it : items_) { config_.progressFunc("existing object: " + it.get().name); }
// item won't overlap with virtual objects if it's inside or touches NFP
// @return 1 if current item overlaps with virtual objects, 0 otherwise
auto overlapWithVirtObject = [&]() -> double {
if (items_.empty()) return 0;
nfps = calcnfp(item, binbb, Lvl<MaxNfpLevel::value>());
auto v = item.referenceVertex();
for (const RawShape &nfp : nfps) {
if (sl::isInside(v, nfp) || sl::touches(v, nfp)) { return 0; }
}
return 1;
};
{
for (auto rot : item.allowed_rotations) {
item.translation(initial_tr);
item.rotation(initial_rot + rot);
if (0==overlapWithVirtObject()) {
can_pack = true;
final_tr = initial_tr;
final_rot = initial_rot + rot;
global_score = 0.3;
break;
}
}
item.translation(final_tr);
item.rotation(final_rot);
}
#ifdef SVGTOOLS_HPP
if (config_.save_svg) {
svg::SVGWriter<RawShape> svgwriter;

View File

@ -118,6 +118,11 @@ public:
int j = 0;
while(!was_packed && !cancelled()) {
for(; j < placers.size() && !was_packed && !cancelled(); j++) {
if (it->get().is_wipe_tower && it->get().binId() != placers[j].plateID()) {
if (this->unfitindicator_)
this->unfitindicator_(it->get().name + " cant be placed in plate_id=" + std::to_string(j) + "/" + std::to_string(placers.size()) + ", continue to next plate");
continue;
}
result = placers[j].pack(*it, rem(it, store_));
score = result.score();
score_all_plates = score;
@ -184,11 +189,11 @@ public:
if(!was_packed){
if (this->unfitindicator_ && !placers.empty())
this->unfitindicator_(it->get().name + " not fit! height=" +std::to_string(it->get().height)
+ " ,plate_id=" + std::to_string(j-1)
+ " ,plate_id=" + std::to_string(j)
+ ", score=" + std::to_string(score)
+ ", best_bed_id=" + std::to_string(best_bed_id)
+ ", score_all_plates=" + std::to_string(score_all_plates)
+", overfit=" + std::to_string(result.overfit()));
+", item.bed_id=" + std::to_string(it->get().binId()));
placers.emplace_back(bin);
placers.back().plateID(placers.size() - 1);

View File

@ -1033,6 +1033,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent);
//BBS: add arrange and orient event
wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE_PARTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE_OUTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);

View File

@ -162,6 +162,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent);
//BBS: add arrange and orient event
wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE_PARTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE_OUTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);

View File

@ -30,9 +30,10 @@ public:
: GLCanvas3D::WipeTowerInfo(std::move(wti))
{}
void apply_arrange_result(const Vec2d& tr, double rotation, int item_id)
void apply_arrange_result(const Vec2d& tr, double rotation, int item_id, int bed_id)
{
m_pos = unscaled(tr); m_rotation = rotation;
m_plate_idx = bed_id;
apply_wipe_tower();
}
@ -71,7 +72,13 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower)
{
ArrangePolygon ap = tower->get_arrange_polygon();
ap.bed_idx = 0;
ap.setter = NULL; // do not move wipe tower
//ap.setter = NULL; // do not move wipe tower
ap.setter = [tower](const ArrangePolygon &p) {
if (p.is_arranged()) {
Vec2d t = p.translation.cast<double>();
tower->apply_arrange_result(t, p.rotation, p.itemid, p.bed_idx);
}
};
return ap;
}
@ -301,7 +308,7 @@ arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<i
// 2打开了支撑且支撑体与接触面使用的是不同材料
// 3允许不同材料落在相同盘且所有选定对象中使用了多种热床温度相同的材料
// 所有对象都是单色的但不同对象的材料不同例如对象A使用红色PLA对象B使用白色PLA
void ArrangeJob::prepare_wipe_tower()
void ArrangeJob::prepare_wipe_tower(bool select)
{
bool need_wipe_tower = false;
@ -368,6 +375,10 @@ void ArrangeJob::prepare_wipe_tower()
// wipe tower is already there
wipe_tower_ap = get_wipetower_arrange_poly(&wti);
wipe_tower_ap.bed_idx = bedid_unlocked;
wipe_tower_ap.name = "WipeTower" + std::to_string(bedid_unlocked);
if (select)
m_selected.emplace_back(wipe_tower_ap);
else
m_unselected.emplace_back(wipe_tower_ap);
}
else if (need_wipe_tower) {
@ -433,7 +444,7 @@ void ArrangeJob::prepare_partplate() {
//skip this object due to be not in current plate, treated as locked
ap.itemid = m_locked.size();
m_locked.emplace_back(std::move(ap));
//BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, name %2%") % oidx % mo->name;
//ARRANGE_LOG(debug) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, name %2%") % oidx % mo->name;
}
}
}
@ -450,9 +461,9 @@ void ArrangeJob::prepare_partplate() {
void ArrangeJob::prepare_outside_plate() {
clear_input();
std::set<std::pair<int, int>> all_inside_objects;
std::set<std::pair<int, int>> all_outside_objects;
typedef std::tuple<int, int, int> obj_inst_plate_t;
std::map<std::pair<int,int>,int> all_inside_objects;
std::map<std::pair<int, int>, int> all_outside_objects;
Model &model = m_plater->model();
PartPlateList &plate_list = m_plater->get_partplate_list();
@ -468,9 +479,8 @@ void ArrangeJob::prepare_outside_plate() {
continue;
}
all_inside_objects.insert(plate_objects.begin(), plate_objects.end());
if (!plate_outside_objects.empty())
all_outside_objects.insert(plate_outside_objects.begin(), plate_outside_objects.end());
for (auto &obj_inst : plate_objects) { all_inside_objects[obj_inst] = plate_idx; }
for (auto &obj_inst : plate_outside_objects) { all_outside_objects[obj_inst] = plate_idx; }
if (plate->is_locked()) {
ARRANGE_LOG(info) << __FUNCTION__ << format(": skip locked plate %d!", plate_idx);
@ -478,24 +488,22 @@ void ArrangeJob::prepare_outside_plate() {
}
// if there are objects inside the plate, lock the plate and don't put new objects in it
if (plate_objects.size() > plate_outside_objects.size()) {
plate->lock(true);
m_uncompatible_plates.push_back(plate_idx);
ARRANGE_LOG(info) << __FUNCTION__ << format(": lock plate %d because there are objects inside!", plate_idx);
}
//if (plate_objects.size() > plate_outside_objects.size()) {
// plate->lock(true);
// m_uncompatible_plates.push_back(plate_idx);
// ARRANGE_LOG(info) << __FUNCTION__ << format(": lock plate %d because there are objects inside!", plate_idx);
//}
}
for (int obj_idx = 0; obj_idx < model.objects.size(); obj_idx++) {
ModelObject *object = model.objects[obj_idx];
for (size_t inst_idx = 0; inst_idx < object->instances.size(); ++inst_idx) {
ModelInstance * instance = object->instances[inst_idx];
std::set<std::pair<int, int>>::iterator iter1, iter2;
iter1 = all_inside_objects.find(std::pair(obj_idx, inst_idx));
iter2 = all_outside_objects.find(std::pair(obj_idx, inst_idx));
auto iter1 = all_inside_objects.find(std::pair(obj_idx, inst_idx));
auto iter2 = all_outside_objects.find(std::pair(obj_idx, inst_idx));
bool outside_plate = false;
if (iter1 == all_inside_objects.end()) {
//skip
continue;
}
if (iter2 != all_outside_objects.end()) {
@ -505,13 +513,15 @@ void ArrangeJob::prepare_outside_plate() {
ArrangePolygons &cont = instance->printable ? (outside_plate ? m_selected : m_locked) : m_unprintable;
ap.itemid = cont.size();
if (!outside_plate) {
plate_list.preprocess_arrange_polygon(obj_idx, inst_idx, ap, true);
plate_list.preprocess_arrange_polygon(obj_idx, inst_idx, ap, false);
ap.bed_idx = iter1->second;
ap.locked_plate = iter1->second;
}
cont.emplace_back(std::move(ap));
}
}
prepare_wipe_tower();
prepare_wipe_tower(true);
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, current_plate_index + 1);
@ -544,6 +554,7 @@ void ArrangeJob::prepare()
prepare_outside_plate();
}
ARRANGE_LOG(info) << "prepare state: " << state << ", items selected : " << m_selected.size();
#if SAVE_ARRANGE_POLY
if (1)
@ -586,6 +597,12 @@ void ArrangeJob::prepare()
#endif
check_unprintable();
if (!m_selected.empty()) {
m_plater->get_notification_manager()->push_notification(NotificationType::ArrangeOngoing, NotificationManager::NotificationLevel::RegularNotificationLevel,
_u8L("Arranging..."));
m_plater->get_notification_manager()->bbl_close_plateinfo_notification();
}
}
void ArrangeJob::check_unprintable()
@ -640,7 +657,7 @@ void ArrangeJob::process()
partplate_list.preprocess_exclude_areas(params.excluded_regions, 1, scale_(1));
BOOST_LOG_TRIVIAL(debug) << "arrange bedpts:" << bedpts[0].transpose() << ", " << bedpts[1].transpose() << ", " << bedpts[2].transpose() << ", " << bedpts[3].transpose();
ARRANGE_LOG(debug) << "bedpts:" << bedpts[0].transpose() << ", " << bedpts[1].transpose() << ", " << bedpts[2].transpose() << ", " << bedpts[3].transpose();
params.stopcondition = [this]() { return was_canceled(); };
@ -648,35 +665,36 @@ void ArrangeJob::process()
update_status(num_finished, _L("Arranging") + " "+ wxString::FromUTF8(str));
};
ArrangePolygons unselected_and_locked = m_unselected;
append(unselected_and_locked, m_locked);
{
BOOST_LOG_TRIVIAL(warning)<< "Arrange full params: "<< params.to_json();
BOOST_LOG_TRIVIAL(info) << boost::format("arrange: items selected before arranging: %1%") % m_selected.size();
ARRANGE_LOG(warning)<< "full params: "<< params.to_json();
ARRANGE_LOG(info) << boost::format("items selected before arranging: %1%") % m_selected.size();
for (auto selected : m_selected) {
BOOST_LOG_TRIVIAL(debug) << selected.name << ", extruder: " << VectorFormatter(selected.extrude_ids)
ARRANGE_LOG(debug) << selected.name << ", extruder: " << VectorFormatter(selected.extrude_ids)
<< ", filament types: " << VectorFormatter(selected.filament_types) << ", bed: " << selected.bed_idx
<< ", filemant_type:" << selected.filament_temp_type << ", trans: " << unscale<double>(selected.translation(X)) << ","
<< unscale<double>(selected.translation(Y)) << ", rotation: " << selected.rotation;
}
BOOST_LOG_TRIVIAL(debug) << "arrange: items unselected before arrange: " << m_unselected.size();
for (auto item : m_unselected)
BOOST_LOG_TRIVIAL(debug) << item.name << ", bed: " << item.bed_idx << ", trans: " << item.translation.transpose()
<<", bbox:"<<get_extents(item.poly).min.transpose()<<","<<get_extents(item.poly).max.transpose();
ARRANGE_LOG(debug) << "items unselected before arrange: " << unselected_and_locked.size();
for (auto item : unselected_and_locked)
ARRANGE_LOG(debug) << item.name << ", bed: " << item.bed_idx << ", trans: " << unscale<double>(item.translation(X)) << "," << unscale<double>(item.translation(Y));
}
arrangement::arrange(m_selected, m_unselected, bedpts, params);
arrangement::arrange(m_selected, unselected_and_locked, bedpts, params);
// sort by item id
std::sort(m_selected.begin(), m_selected.end(), [](auto a, auto b) {return a.itemid < b.itemid; });
{
BOOST_LOG_TRIVIAL(info) << boost::format("arrange: items selected after arranging: %1%") % m_selected.size();
ARRANGE_LOG(info) << boost::format("items selected after arranging: %1%") % m_selected.size();
for (auto selected : m_selected)
BOOST_LOG_TRIVIAL(debug) << selected.name << ", extruder: " << VectorFormatter(selected.extrude_ids) << ", bed: " << selected.bed_idx
ARRANGE_LOG(debug) << selected.name << ", extruder: " << VectorFormatter(selected.extrude_ids) << ", bed: " << selected.bed_idx
<< ", bed_temp: " << selected.first_bed_temp << ", print_temp: " << selected.print_temp
<< ", trans: " << unscale<double>(selected.translation(X)) << "," << unscale<double>(selected.translation(Y))
<< ", rotation: " << selected.rotation;
BOOST_LOG_TRIVIAL(debug) << "arrange: items unselected after arrange: "<< m_unselected.size();
for (auto item : m_unselected)
BOOST_LOG_TRIVIAL(debug) << item.name << ", bed: " << item.bed_idx << ", trans: " << item.translation.transpose() << ", rotation: " << item.rotation;
ARRANGE_LOG(debug) << "items unselected after arrange: " << unselected_and_locked.size();
for (auto item : unselected_and_locked)
ARRANGE_LOG(debug) << item.name << ", bed: " << item.bed_idx << ", trans: " << unscale<double>(item.translation(X)) << "," << unscale<double>(item.translation(Y));
}
// put unpackable items to m_unprintable so they goes outside
@ -732,7 +750,7 @@ void ArrangeJob::finalize()
beds = std::max(ap.bed_idx, beds);
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": arrange selected %4%: bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
ARRANGE_LOG(debug) << __FUNCTION__ << boost::format(": selected %4%: bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
}
//BBS: adjust the bed_index, create new plates, get the max bed_index
@ -745,7 +763,7 @@ void ArrangeJob::finalize()
plate_list.postprocess_bed_index_for_unselected(ap);
beds = std::max(ap.bed_idx, beds);
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange unselected %4%: bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
ARRANGE_LOG(debug) << __FUNCTION__ << boost::format(": unselected %4%: bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
}
for (ArrangePolygon& ap : m_locked) {
@ -782,7 +800,7 @@ void ArrangeJob::finalize()
plate_list.postprocess_arrange_polygon(ap, true);
ap.apply();
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
ARRANGE_LOG(debug) << __FUNCTION__ << boost::format(": m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale<double>(ap.translation(X)) % unscale<double>(ap.translation(Y)) % ap.name;
}
m_plater->update();

View File

@ -42,9 +42,7 @@ class ArrangeJob : public PlaterJob
// prepare the items which are selected and not on the current partplate
void prepare_outside_plate();
void prepare_wipe_tower();
ArrangePolygon prepare_arrange_polygon(void* instance);
void prepare_wipe_tower(bool select = false);
protected:
@ -68,6 +66,8 @@ public:
}
void finalize() override;
static ArrangePolygon prepare_arrange_polygon(void *instance);
};
std::optional<arrangement::ArrangePolygon> get_wipe_tower_arrangepoly(const Plater &);

View File

@ -4375,7 +4375,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_PARTPLATE, [this](SimpleEvent& evt) {
//BBS arrage from EVT set default state.
this->q->set_prepare_state(Job::PREPARE_STATE_MENU);
this->q->arrange(); });
this->q->arrange();
});
view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_OUTPLATE, [this](SimpleEvent &evt) {
if (this->q->last_arrange_job_is_finished()) {
this->q->set_prepare_state(Job::PREPARE_STATE_OUTSIDE_BED);
this->q->arrange();
}
});
view3D_canvas->Bind(EVT_GLCANVAS_ORIENT, [this](SimpleEvent& evt) {
//BBS oriant from EVT set default state.
this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT);
@ -9437,6 +9444,7 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio
bool cur_plate_is_smaller = cur_plate_size.x() + 1.0 < old_plate_size.x() || cur_plate_size.y() + 1.0 < old_plate_size.y();
BOOST_LOG_TRIVIAL(info) << format("change bed pos from (%.0f,%.0f) to (%.0f,%.0f)", old_plate_pos.x(), old_plate_pos.y(), cur_plate_pos.x(), cur_plate_pos.y());
bool plate_not_empty = std::any_of(plate_object.begin(), plate_object.end(), [](const std::vector<int> &obj_idxs) { return !obj_idxs.empty(); });
if (old_plate_pos.x() != cur_plate_pos.x() || old_plate_pos.y() != cur_plate_pos.y() || cur_plate_is_smaller) {
for (int i = 0; i < plate_object.size(); ++i) {
view3D->select_object_from_idx(plate_object[i]);
@ -9445,7 +9453,7 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio
}
BOOST_LOG_TRIVIAL(info) << format("change bed size from (%.0f,%.0f) to (%.0f,%.0f)", old_plate_size.x(), old_plate_size.y(), cur_plate_size.x(), cur_plate_size.y());
if (cur_plate_is_smaller && std::any_of(plate_object.begin(), plate_object.end(), [](const std::vector<int>& obj_idxs) { return !obj_idxs.empty(); })) {
if (cur_plate_is_smaller && plate_not_empty) {
take_snapshot("Arrange after bed size changes");
//collect all the objects on the current plates
std::set<ModelObject*> new_all_plate_object;
@ -9465,8 +9473,6 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio
obj_out_set.emplace(std::pair<int, int>{i, 0});
}
}
q->set_prepare_state(Job::PREPARE_STATE_OUTSIDE_BED);
q->arrange();
}
#if 0
const BoundingBoxf3 &cur_platelist_bbox = cur_plate_list.get_bounding_box();
@ -9504,6 +9510,8 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio
#endif
view3D->deselect_all();
}
wxQueueEvent(view3D->get_wxglcanvas(), new SimpleEvent(EVT_GLCANVAS_ARRANGE_OUTPLATE));
}
void Plater::orient()