1 #include "BedShapeDialog.hpp"
2 #include "GUI_App.hpp"
3 #include "OptionsGroup.hpp"
4 
5 #include <wx/wx.h>
6 #include <wx/numformatter.h>
7 #include <wx/sizer.h>
8 #include <wx/statbox.h>
9 #include <wx/tooltip.h>
10 
11 #include "libslic3r/BoundingBox.hpp"
12 #include "libslic3r/Model.hpp"
13 #include "libslic3r/Polygon.hpp"
14 
15 #include "boost/nowide/iostream.hpp"
16 #include <boost/algorithm/string/predicate.hpp>
17 #include <boost/filesystem.hpp>
18 
19 #include <algorithm>
20 
21 namespace Slic3r {
22 namespace GUI {
23 
BedShape(const ConfigOptionPoints & points)24 BedShape::BedShape(const ConfigOptionPoints& points)
25 {
26     auto polygon = Polygon::new_scale(points.values);
27 
28     // is this a rectangle ?
29     if (points.size() == 4) {
30         auto lines = polygon.lines();
31         if (lines[0].parallel_to(lines[2]) && lines[1].parallel_to(lines[3])) {
32             // okay, it's a rectangle
33             // find origin
34             coordf_t x_min, x_max, y_min, y_max;
35             x_max = x_min = points.values[0](0);
36             y_max = y_min = points.values[0](1);
37             for (auto pt : points.values)
38             {
39                 x_min = std::min(x_min, pt(0));
40                 x_max = std::max(x_max, pt(0));
41                 y_min = std::min(y_min, pt(1));
42                 y_max = std::max(y_max, pt(1));
43             }
44 
45             m_type          = Type::Rectangular;
46             m_rectSize      = Vec2d(x_max - x_min, y_max - y_min);
47             m_rectOrigin    = Vec2d(-x_min, -y_min);
48 
49             return;
50         }
51     }
52 
53     // is this a circle ?
54     {
55         // Analyze the array of points.Do they reside on a circle ?
56         auto center = polygon.bounding_box().center();
57         std::vector<double> vertex_distances;
58         double avg_dist = 0;
59         for (auto pt : polygon.points)
60         {
61             double distance = (pt - center).cast<double>().norm();
62             vertex_distances.push_back(distance);
63             avg_dist += distance;
64         }
65 
66         avg_dist /= vertex_distances.size();
67         bool defined_value = true;
68         for (auto el : vertex_distances)
69         {
70             if (abs(el - avg_dist) > 10 * SCALED_EPSILON)
71                 defined_value = false;
72             break;
73         }
74         if (defined_value) {
75             // all vertices are equidistant to center
76             m_type      = Type::Circular;
77             m_diameter  = unscale<double>(avg_dist * 2);
78 
79             return;
80         }
81     }
82 
83     if (points.size() < 3)
84         return;
85 
86     // This is a custom bed shape, use the polygon provided.
87     m_type = Type::Custom;
88 }
89 
get_option_label(BedShape::Parameter param)90 static std::string get_option_label(BedShape::Parameter param)
91 {
92     switch (param) {
93     case BedShape::Parameter::RectSize  : return L("Size");
94     case BedShape::Parameter::RectOrigin: return L("Origin");
95     case BedShape::Parameter::Diameter  : return L("Diameter");
96     default:                              return "";
97     }
98 }
99 
append_option_line(ConfigOptionsGroupShp optgroup,Parameter param)100 void BedShape::append_option_line(ConfigOptionsGroupShp optgroup, Parameter param)
101 {
102     ConfigOptionDef def;
103 
104     if (param == Parameter::RectSize) {
105         def.type = coPoints;
106         def.set_default_value(new ConfigOptionPoints{ Vec2d(200, 200) });
107         def.min = 0;
108         def.max = 1200;
109         def.label = get_option_label(param);
110         def.tooltip = L("Size in X and Y of the rectangular plate.");
111 
112         Option option(def, "rect_size");
113         optgroup->append_single_option_line(option);
114     }
115     else if (param == Parameter::RectOrigin) {
116         def.type = coPoints;
117         def.set_default_value(new ConfigOptionPoints{ Vec2d(0, 0) });
118         def.min = -600;
119         def.max = 600;
120         def.label = get_option_label(param);
121         def.tooltip = L("Distance of the 0,0 G-code coordinate from the front left corner of the rectangle.");
122 
123         Option option(def, "rect_origin");
124         optgroup->append_single_option_line(option);
125     }
126     else if (param == Parameter::Diameter) {
127         def.type = coFloat;
128         def.set_default_value(new ConfigOptionFloat(200));
129         def.sidetext = L("mm");
130         def.label = get_option_label(param);
131         def.tooltip = L("Diameter of the print bed. It is assumed that origin (0,0) is located in the center.");
132 
133         Option option(def, "diameter");
134         optgroup->append_single_option_line(option);
135     }
136 }
137 
get_name(Type type)138 wxString BedShape::get_name(Type type)
139 {
140     switch (type) {
141         case Type::Rectangular  : return _L("Rectangular");
142         case Type::Circular     : return _L("Circular");
143         case Type::Custom       : return _L("Custom");
144         case Type::Invalid      :
145         default                 : return _L("Invalid");
146     }
147 }
148 
get_type()149 size_t BedShape::get_type()
150 {
151     return static_cast<size_t>(m_type == Type::Invalid ? Type::Rectangular : m_type);
152 }
153 
get_full_name_with_params()154 wxString BedShape::get_full_name_with_params()
155 {
156     wxString out = _L("Shape") + ": " + get_name(m_type);
157 
158     if (m_type == Type::Rectangular) {
159         out += "\n" + _(get_option_label(Parameter::RectSize))  + ": [" + ConfigOptionPoint(m_rectSize).serialize()     + "]";
160         out += "\n" + _(get_option_label(Parameter::RectOrigin))+ ": [" + ConfigOptionPoint(m_rectOrigin).serialize()   + "]";
161     }
162     else if (m_type == Type::Circular)
163         out += "\n" + _L(get_option_label(Parameter::Diameter)) + ": [" + double_to_string(m_diameter)                  + "]";
164 
165     return out;
166 }
167 
apply_optgroup_values(ConfigOptionsGroupShp optgroup)168 void BedShape::apply_optgroup_values(ConfigOptionsGroupShp optgroup)
169 {
170     if (m_type == Type::Rectangular || m_type == Type::Invalid) {
171         optgroup->set_value("rect_size"     , new ConfigOptionPoints{ m_rectSize    });
172         optgroup->set_value("rect_origin"   , new ConfigOptionPoints{ m_rectOrigin  });
173     }
174     else if (m_type == Type::Circular)
175         optgroup->set_value("diameter", double_to_string(m_diameter));
176 }
177 
build_dialog(const ConfigOptionPoints & default_pt,const ConfigOptionString & custom_texture,const ConfigOptionString & custom_model)178 void BedShapeDialog::build_dialog(const ConfigOptionPoints& default_pt, const ConfigOptionString& custom_texture, const ConfigOptionString& custom_model)
179 {
180     SetFont(wxGetApp().normal_font());
181 	m_panel = new BedShapePanel(this);
182     m_panel->build_panel(default_pt, custom_texture, custom_model);
183 
184 	auto main_sizer = new wxBoxSizer(wxVERTICAL);
185 	main_sizer->Add(m_panel, 1, wxEXPAND);
186 	main_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, 10);
187 
188 	SetSizer(main_sizer);
189 	SetMinSize(GetSize());
190 	main_sizer->SetSizeHints(this);
191 
192     this->Bind(wxEVT_CLOSE_WINDOW, ([this](wxCloseEvent& evt) {
193         EndModal(wxID_CANCEL);
194     }));
195 }
196 
on_dpi_changed(const wxRect & suggested_rect)197 void BedShapeDialog::on_dpi_changed(const wxRect &suggested_rect)
198 {
199     const int& em = em_unit();
200     m_panel->m_shape_options_book->SetMinSize(wxSize(25 * em, -1));
201 
202     for (auto og : m_panel->m_optgroups)
203         og->msw_rescale();
204 
205     const wxSize& size = wxSize(50 * em, -1);
206 
207     SetMinSize(size);
208     SetSize(size);
209 
210     Refresh();
211 }
212 
213 const std::string BedShapePanel::NONE = "None";
214 const std::string BedShapePanel::EMPTY_STRING = "";
215 
build_panel(const ConfigOptionPoints & default_pt,const ConfigOptionString & custom_texture,const ConfigOptionString & custom_model)216 void BedShapePanel::build_panel(const ConfigOptionPoints& default_pt, const ConfigOptionString& custom_texture, const ConfigOptionString& custom_model)
217 {
218     m_shape = default_pt.values;
219     m_custom_texture = custom_texture.value.empty() ? NONE : custom_texture.value;
220     m_custom_model = custom_model.value.empty() ? NONE : custom_model.value;
221 
222     auto sbsizer = new wxStaticBoxSizer(wxVERTICAL, this, _(L("Shape")));
223     sbsizer->GetStaticBox()->SetFont(wxGetApp().bold_font());
224 
225 	// shape options
226     m_shape_options_book = new wxChoicebook(this, wxID_ANY, wxDefaultPosition, wxSize(25*wxGetApp().em_unit(), -1), wxCHB_TOP);
227     sbsizer->Add(m_shape_options_book);
228 
229     auto optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Rectangular));
230     BedShape::append_option_line(optgroup, BedShape::Parameter::RectSize);
231     BedShape::append_option_line(optgroup, BedShape::Parameter::RectOrigin);
232     activate_options_page(optgroup);
233 
234     optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Circular));
235     BedShape::append_option_line(optgroup, BedShape::Parameter::Diameter);
236     activate_options_page(optgroup);
237 
238     optgroup = init_shape_options_page(BedShape::get_name(BedShape::Type::Custom));
239 
240 	Line line{ "", "" };
241 	line.full_width = 1;
242 	line.widget = [this](wxWindow* parent) {
243         wxButton* shape_btn = new wxButton(parent, wxID_ANY, _L("Load shape from STL..."));
244         wxSizer* shape_sizer = new wxBoxSizer(wxHORIZONTAL);
245         shape_sizer->Add(shape_btn, 1, wxEXPAND);
246 
247         wxSizer* sizer = new wxBoxSizer(wxVERTICAL);
248         sizer->Add(shape_sizer, 1, wxEXPAND);
249 
250         shape_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
251 			load_stl();
252 		});
253 
254 		return sizer;
255 	};
256 	optgroup->append_line(line);
257     activate_options_page(optgroup);
258 
259     wxPanel* texture_panel = init_texture_panel();
260     wxPanel* model_panel = init_model_panel();
261 
262     Bind(wxEVT_CHOICEBOOK_PAGE_CHANGED, ([this](wxCommandEvent& e)
263     {
264 		update_shape();
265 	}));
266 
267 	// right pane with preview canvas
268 	m_canvas = new Bed_2D(this);
269     m_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent& e) { m_canvas->repaint(m_shape); });
270     m_canvas->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { m_canvas->Refresh(); });
271 
272     wxSizer* left_sizer = new wxBoxSizer(wxVERTICAL);
273     left_sizer->Add(sbsizer, 0, wxEXPAND);
274     left_sizer->Add(texture_panel, 1, wxEXPAND);
275     left_sizer->Add(model_panel, 1, wxEXPAND);
276 
277     wxSizer* top_sizer = new wxBoxSizer(wxHORIZONTAL);
278     top_sizer->Add(left_sizer, 0, wxEXPAND | wxLEFT | wxTOP | wxBOTTOM, 10);
279     top_sizer->Add(m_canvas, 1, wxEXPAND | wxALL, 10);
280 
281 	SetSizerAndFit(top_sizer);
282 
283 	set_shape(default_pt);
284 	update_preview();
285 }
286 
287 // Called from the constructor.
288 // Create a panel for a rectangular / circular / custom bed shape.
init_shape_options_page(const wxString & title)289 ConfigOptionsGroupShp BedShapePanel::init_shape_options_page(const wxString& title)
290 {
291     wxPanel* panel = new wxPanel(m_shape_options_book);
292     ConfigOptionsGroupShp optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Settings")));
293 
294     optgroup->label_width = 10;
295     optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
296         update_shape();
297     };
298 
299     m_optgroups.push_back(optgroup);
300 //    panel->SetSizerAndFit(optgroup->sizer);
301     m_shape_options_book->AddPage(panel, title);
302 
303     return optgroup;
304 }
305 
activate_options_page(ConfigOptionsGroupShp options_group)306 void BedShapePanel::activate_options_page(ConfigOptionsGroupShp options_group)
307 {
308     options_group->activate();
309     options_group->parent()->SetSizerAndFit(options_group->sizer);
310 }
311 
init_texture_panel()312 wxPanel* BedShapePanel::init_texture_panel()
313 {
314     wxPanel* panel = new wxPanel(this);
315     ConfigOptionsGroupShp optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Texture")));
316 
317     optgroup->label_width = 10;
318     optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
319         update_shape();
320     };
321 
322     Line line{ "", "" };
323     line.full_width = 1;
324     line.widget = [this](wxWindow* parent) {
325         wxButton* load_btn = new wxButton(parent, wxID_ANY, _(L("Load...")));
326         wxSizer* load_sizer = new wxBoxSizer(wxHORIZONTAL);
327         load_sizer->Add(load_btn, 1, wxEXPAND);
328 
329         wxStaticText* filename_lbl = new wxStaticText(parent, wxID_ANY, _(NONE));
330         wxSizer* filename_sizer = new wxBoxSizer(wxHORIZONTAL);
331         filename_sizer->Add(filename_lbl, 1, wxEXPAND);
332 
333         wxButton* remove_btn = new wxButton(parent, wxID_ANY, _(L("Remove")));
334         wxSizer* remove_sizer = new wxBoxSizer(wxHORIZONTAL);
335         remove_sizer->Add(remove_btn, 1, wxEXPAND);
336 
337         wxSizer* sizer = new wxBoxSizer(wxVERTICAL);
338         sizer->Add(filename_sizer, 1, wxEXPAND);
339         sizer->Add(load_sizer, 1, wxEXPAND);
340         sizer->Add(remove_sizer, 1, wxEXPAND);
341 
342         load_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e)
343             {
344                 load_texture();
345             }));
346 
347         remove_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e)
348             {
349                 m_custom_texture = NONE;
350                 update_shape();
351             }));
352 
353         filename_lbl->Bind(wxEVT_UPDATE_UI, ([this](wxUpdateUIEvent& e)
354             {
355                 e.SetText(_(boost::filesystem::path(m_custom_texture).filename().string()));
356                 wxStaticText* lbl = dynamic_cast<wxStaticText*>(e.GetEventObject());
357                 if (lbl != nullptr)
358                 {
359                     bool exists = (m_custom_texture == NONE) || boost::filesystem::exists(m_custom_texture);
360                     lbl->SetForegroundColour(exists ? wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT) : wxColor(*wxRED));
361 
362                     wxString tooltip_text = "";
363                     if (m_custom_texture != NONE)
364                     {
365                         if (!exists)
366                             tooltip_text += _(L("Not found:")) + " ";
367 
368                         tooltip_text += _(m_custom_texture);
369                     }
370 
371                     wxToolTip* tooltip = lbl->GetToolTip();
372                     if ((tooltip == nullptr) || (tooltip->GetTip() != tooltip_text))
373                         lbl->SetToolTip(tooltip_text);
374                 }
375             }));
376 
377         remove_btn->Bind(wxEVT_UPDATE_UI, ([this](wxUpdateUIEvent& e)
378             {
379                 e.Enable(m_custom_texture != NONE);
380             }));
381 
382         return sizer;
383     };
384     optgroup->append_line(line);
385     optgroup->activate();
386 
387     panel->SetSizerAndFit(optgroup->sizer);
388 
389     return panel;
390 }
391 
init_model_panel()392 wxPanel* BedShapePanel::init_model_panel()
393 {
394     wxPanel* panel = new wxPanel(this);
395     ConfigOptionsGroupShp optgroup = std::make_shared<ConfigOptionsGroup>(panel, _(L("Model")));
396 
397     optgroup->label_width = 10;
398     optgroup->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
399         update_shape();
400     };
401 
402     Line line{ "", "" };
403     line.full_width = 1;
404     line.widget = [this](wxWindow* parent) {
405         wxButton* load_btn = new wxButton(parent, wxID_ANY, _(L("Load...")));
406         wxSizer* load_sizer = new wxBoxSizer(wxHORIZONTAL);
407         load_sizer->Add(load_btn, 1, wxEXPAND);
408 
409         wxStaticText* filename_lbl = new wxStaticText(parent, wxID_ANY, _(NONE));
410         wxSizer* filename_sizer = new wxBoxSizer(wxHORIZONTAL);
411         filename_sizer->Add(filename_lbl, 1, wxEXPAND);
412 
413         wxButton* remove_btn = new wxButton(parent, wxID_ANY, _(L("Remove")));
414         wxSizer* remove_sizer = new wxBoxSizer(wxHORIZONTAL);
415         remove_sizer->Add(remove_btn, 1, wxEXPAND);
416 
417         wxSizer* sizer = new wxBoxSizer(wxVERTICAL);
418         sizer->Add(filename_sizer, 1, wxEXPAND);
419         sizer->Add(load_sizer, 1, wxEXPAND);
420         sizer->Add(remove_sizer, 1, wxEXPAND);
421 
422         load_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e)
423             {
424                 load_model();
425             }));
426 
427         remove_btn->Bind(wxEVT_BUTTON, ([this](wxCommandEvent& e)
428             {
429                 m_custom_model = NONE;
430                 update_shape();
431             }));
432 
433         filename_lbl->Bind(wxEVT_UPDATE_UI, ([this](wxUpdateUIEvent& e)
434             {
435                 e.SetText(_(boost::filesystem::path(m_custom_model).filename().string()));
436                 wxStaticText* lbl = dynamic_cast<wxStaticText*>(e.GetEventObject());
437                 if (lbl != nullptr)
438                 {
439                     bool exists = (m_custom_model == NONE) || boost::filesystem::exists(m_custom_model);
440                     lbl->SetForegroundColour(exists ? wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT) : wxColor(*wxRED));
441 
442                     wxString tooltip_text = "";
443                     if (m_custom_model != NONE)
444                     {
445                         if (!exists)
446                             tooltip_text += _(L("Not found:")) + " ";
447 
448                         tooltip_text += _(m_custom_model);
449                     }
450 
451                     wxToolTip* tooltip = lbl->GetToolTip();
452                     if ((tooltip == nullptr) || (tooltip->GetTip() != tooltip_text))
453                         lbl->SetToolTip(tooltip_text);
454                 }
455             }));
456 
457         remove_btn->Bind(wxEVT_UPDATE_UI, ([this](wxUpdateUIEvent& e)
458             {
459                 e.Enable(m_custom_model != NONE);
460             }));
461 
462         return sizer;
463     };
464     optgroup->append_line(line);
465     optgroup->activate();
466 
467     panel->SetSizerAndFit(optgroup->sizer);
468 
469     return panel;
470 }
471 
472 // Called from the constructor.
473 // Set the initial bed shape from a list of points.
474 // Deduce the bed shape type(rect, circle, custom)
475 // This routine shall be smart enough if the user messes up
476 // with the list of points in the ini file directly.
set_shape(const ConfigOptionPoints & points)477 void BedShapePanel::set_shape(const ConfigOptionPoints& points)
478 {
479     BedShape shape(points);
480 
481     m_shape_options_book->SetSelection(shape.get_type());
482     shape.apply_optgroup_values(m_optgroups[shape.get_type()]);
483 
484     // Copy the polygon to the canvas, make a copy of the array, if custom shape is selected
485     if (shape.is_custom())
486         m_loaded_shape = points.values;
487 
488     update_shape();
489 
490     return;
491 }
492 
update_preview()493 void BedShapePanel::update_preview()
494 {
495 	if (m_canvas) m_canvas->Refresh();
496 	Refresh();
497 }
498 
499 // Update the bed shape from the dialog fields.
update_shape()500 void BedShapePanel::update_shape()
501 {
502 	auto page_idx = m_shape_options_book->GetSelection();
503     auto opt_group = m_optgroups[page_idx];
504 
505     BedShape::Type page_type = static_cast<BedShape::Type>(page_idx);
506 
507 	if (page_type == BedShape::Type::Rectangular) {
508 		Vec2d rect_size(Vec2d::Zero());
509 		Vec2d rect_origin(Vec2d::Zero());
510 
511 		try { rect_size = boost::any_cast<Vec2d>(opt_group->get_value("rect_size")); }
512         catch (const std::exception& /* e */) { return; }
513 
514         try { rect_origin = boost::any_cast<Vec2d>(opt_group->get_value("rect_origin")); }
515 		catch (const std::exception & /* e */)  { return; }
516 
517 		auto x = rect_size(0);
518 		auto y = rect_size(1);
519 		// empty strings or '-' or other things
520 		if (x == 0 || y == 0)	return;
521 		double x0 = 0.0;
522 		double y0 = 0.0;
523 		double x1 = x;
524 		double y1 = y;
525 
526 		auto dx = rect_origin(0);
527 		auto dy = rect_origin(1);
528 
529 		x0 -= dx;
530 		x1 -= dx;
531 		y0 -= dy;
532 		y1 -= dy;
533         m_shape = { Vec2d(x0, y0),
534                     Vec2d(x1, y0),
535                     Vec2d(x1, y1),
536                     Vec2d(x0, y1) };
537     }
538     else if (page_type == BedShape::Type::Circular) {
539 		double diameter;
540 		try { diameter = boost::any_cast<double>(opt_group->get_value("diameter")); }
541 		catch (const std::exception & /* e */) { return; }
542 
543  		if (diameter == 0.0) return ;
544 		auto r = diameter / 2;
545 		auto twopi = 2 * PI;
546         auto edges = 72;
547         std::vector<Vec2d> points;
548         for (int i = 1; i <= edges; ++i) {
549             auto angle = i * twopi / edges;
550 			points.push_back(Vec2d(r*cos(angle), r*sin(angle)));
551 		}
552         m_shape = points;
553     }
554     else if (page_type == BedShape::Type::Custom)
555         m_shape = m_loaded_shape;
556 
557     update_preview();
558 }
559 
560 // Loads an stl file, projects it to the XY plane and calculates a polygon.
load_stl()561 void BedShapePanel::load_stl()
562 {
563     wxFileDialog dialog(this, _(L("Choose an STL file to import bed shape from:")), "", "", file_wildcards(FT_STL), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
564     if (dialog.ShowModal() != wxID_OK)
565         return;
566 
567     std::string file_name = dialog.GetPath().ToUTF8().data();
568     if (!boost::algorithm::iends_with(file_name, ".stl"))
569     {
570         show_error(this, _(L("Invalid file format.")));
571         return;
572     }
573 
574     wxBusyCursor wait;
575 
576 	Model model;
577 	try {
578         model = Model::read_from_file(file_name);
579 	}
580 	catch (std::exception &) {
581         show_error(this, _(L("Error! Invalid model")));
582         return;
583     }
584 
585 	auto mesh = model.mesh();
586 	auto expolygons = mesh.horizontal_projection();
587 
588 	if (expolygons.size() == 0) {
589 		show_error(this, _(L("The selected file contains no geometry.")));
590 		return;
591 	}
592 	if (expolygons.size() > 1) {
593 		show_error(this, _(L("The selected file contains several disjoint areas. This is not supported.")));
594 		return;
595 	}
596 
597 	auto polygon = expolygons[0].contour;
598 	std::vector<Vec2d> points;
599 	for (auto pt : polygon.points)
600 		points.push_back(unscale(pt));
601 
602     m_loaded_shape = points;
603     update_shape();
604 }
605 
load_texture()606 void BedShapePanel::load_texture()
607 {
608     wxFileDialog dialog(this, _(L("Choose a file to import bed texture from (PNG/SVG):")), "", "",
609         file_wildcards(FT_TEX), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
610 
611     if (dialog.ShowModal() != wxID_OK)
612         return;
613 
614     m_custom_texture = NONE;
615 
616     std::string file_name = dialog.GetPath().ToUTF8().data();
617     if (!boost::algorithm::iends_with(file_name, ".png") && !boost::algorithm::iends_with(file_name, ".svg"))
618     {
619         show_error(this, _(L("Invalid file format.")));
620         return;
621     }
622 
623     wxBusyCursor wait;
624 
625     m_custom_texture = file_name;
626     update_shape();
627 }
628 
load_model()629 void BedShapePanel::load_model()
630 {
631     wxFileDialog dialog(this, _(L("Choose an STL file to import bed model from:")), "", "",
632         file_wildcards(FT_STL), wxFD_OPEN | wxFD_FILE_MUST_EXIST);
633 
634     if (dialog.ShowModal() != wxID_OK)
635         return;
636 
637     m_custom_model = NONE;
638 
639     std::string file_name = dialog.GetPath().ToUTF8().data();
640     if (!boost::algorithm::iends_with(file_name, ".stl"))
641     {
642         show_error(this, _(L("Invalid file format.")));
643         return;
644     }
645 
646     wxBusyCursor wait;
647 
648     m_custom_model = file_name;
649     update_shape();
650 }
651 
652 } // GUI
653 } // Slic3r
654