ObjectPartsPanel.pm 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. # Configuration of mesh modifiers and their parameters.
  2. # This panel is inserted into ObjectSettingsDialog.
  3. package Slic3r::GUI::Plater::ObjectPartsPanel;
  4. use strict;
  5. use warnings;
  6. use utf8;
  7. use File::Basename qw(basename);
  8. use Wx qw(:misc :sizer :treectrl :button wxTAB_TRAVERSAL wxSUNKEN_BORDER wxBITMAP_TYPE_PNG wxID_CANCEL
  9. wxTheApp);
  10. use List::Util qw(max);
  11. use Wx::Event qw(EVT_BUTTON EVT_TREE_ITEM_COLLAPSING EVT_TREE_SEL_CHANGED EVT_TREE_ITEM_RIGHT_CLICK);
  12. use Slic3r::Geometry qw(X Y Z MIN MAX scale unscale deg2rad rad2deg);
  13. use base 'Wx::Panel';
  14. use constant ICON_OBJECT => 0;
  15. use constant ICON_SOLIDMESH => 1;
  16. use constant ICON_MODIFIERMESH => 2;
  17. sub new {
  18. my $class = shift;
  19. my ($parent, %params) = @_;
  20. my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
  21. my $object = $self->{model_object} = $params{model_object};
  22. # create TreeCtrl
  23. my $tree = $self->{tree} = Wx::TreeCtrl->new($self, -1, wxDefaultPosition, [300, 100],
  24. wxTR_NO_BUTTONS | wxSUNKEN_BORDER | wxTR_HAS_VARIABLE_ROW_HEIGHT
  25. | wxTR_SINGLE | wxTR_NO_BUTTONS);
  26. {
  27. $self->{tree_icons} = Wx::ImageList->new(16, 16, 1);
  28. $tree->AssignImageList($self->{tree_icons});
  29. $self->{tree_icons}->Add(Wx::Bitmap->new($Slic3r::var->("brick.png"), wxBITMAP_TYPE_PNG)); # ICON_OBJECT
  30. $self->{tree_icons}->Add(Wx::Bitmap->new($Slic3r::var->("package.png"), wxBITMAP_TYPE_PNG)); # ICON_SOLIDMESH
  31. $self->{tree_icons}->Add(Wx::Bitmap->new($Slic3r::var->("plugin.png"), wxBITMAP_TYPE_PNG)); # ICON_MODIFIERMESH
  32. my $rootId = $tree->AddRoot("Object", ICON_OBJECT);
  33. $tree->SetPlData($rootId, { type => 'object' });
  34. }
  35. # buttons
  36. $self->{btn_load_part} = Wx::Button->new($self, -1, "Load part…", wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
  37. $self->{btn_load_modifier} = Wx::Button->new($self, -1, "Load modifier…", wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
  38. $self->{btn_load_lambda_modifier} = Wx::Button->new($self, -1, "Create modifier…", wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
  39. $self->{btn_delete} = Wx::Button->new($self, -1, "Delete part", wxDefaultPosition, wxDefaultSize, wxBU_LEFT);
  40. if ($Slic3r::GUI::have_button_icons) {
  41. $self->{btn_load_part}->SetBitmap(Wx::Bitmap->new($Slic3r::var->("brick_add.png"), wxBITMAP_TYPE_PNG));
  42. $self->{btn_load_modifier}->SetBitmap(Wx::Bitmap->new($Slic3r::var->("brick_add.png"), wxBITMAP_TYPE_PNG));
  43. $self->{btn_load_lambda_modifier}->SetBitmap(Wx::Bitmap->new($Slic3r::var->("brick_add.png"), wxBITMAP_TYPE_PNG));
  44. $self->{btn_delete}->SetBitmap(Wx::Bitmap->new($Slic3r::var->("brick_delete.png"), wxBITMAP_TYPE_PNG));
  45. }
  46. # buttons sizer
  47. my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  48. $buttons_sizer->Add($self->{btn_load_part}, 0);
  49. $buttons_sizer->Add($self->{btn_load_modifier}, 0);
  50. $buttons_sizer->Add($self->{btn_load_lambda_modifier}, 0);
  51. $buttons_sizer->Add($self->{btn_delete}, 0);
  52. $self->{btn_load_part}->SetFont($Slic3r::GUI::small_font);
  53. $self->{btn_load_modifier}->SetFont($Slic3r::GUI::small_font);
  54. $self->{btn_load_lambda_modifier}->SetFont($Slic3r::GUI::small_font);
  55. $self->{btn_delete}->SetFont($Slic3r::GUI::small_font);
  56. # part settings panel
  57. $self->{settings_panel} = Slic3r::GUI::Plater::OverrideSettingsPanel->new($self, on_change => sub { $self->{part_settings_changed} = 1; });
  58. my $settings_sizer = Wx::StaticBoxSizer->new($self->{staticbox} = Wx::StaticBox->new($self, -1, "Part Settings"), wxVERTICAL);
  59. $settings_sizer->Add($self->{settings_panel}, 1, wxEXPAND | wxALL, 0);
  60. my $optgroup_movers;
  61. # initialize the movement target before it's used.
  62. # on Windows this causes a segfault due to calling distance_to()
  63. # on the object.
  64. $self->{move_target} = Slic3r::Pointf3->new;
  65. $optgroup_movers = $self->{optgroup_movers} = Slic3r::GUI::OptionsGroup->new(
  66. parent => $self,
  67. title => 'Move',
  68. on_change => sub {
  69. my ($opt_id) = @_;
  70. # There seems to be an issue with wxWidgets 3.0.2/3.0.3, where the slider
  71. # genates tens of events for a single value change.
  72. # Only trigger the recalculation if the value changes
  73. # or a live preview was activated and the mesh cut is not valid yet.
  74. my $new = Slic3r::Pointf3->new(map $optgroup_movers->get_value($_), qw(x y z));
  75. if ($self->{move_target}->distance_to($new) > 0) {
  76. $self->{move_target} = $new;
  77. wxTheApp->CallAfter(sub {
  78. $self->_update;
  79. });
  80. }
  81. },
  82. label_width => 20,
  83. );
  84. $optgroup_movers->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  85. opt_id => 'x',
  86. type => 'slider',
  87. label => 'X',
  88. default => 0,
  89. full_width => 1,
  90. ));
  91. $optgroup_movers->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  92. opt_id => 'y',
  93. type => 'slider',
  94. label => 'Y',
  95. default => 0,
  96. full_width => 1,
  97. ));
  98. $optgroup_movers->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  99. opt_id => 'z',
  100. type => 'slider',
  101. label => 'Z',
  102. default => 0,
  103. full_width => 1,
  104. ));
  105. # left pane with tree
  106. my $left_sizer = $self->{left_sizer} = Wx::BoxSizer->new(wxVERTICAL);
  107. $left_sizer->Add($tree, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
  108. $left_sizer->Add($buttons_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
  109. $left_sizer->Add($settings_sizer, 1, wxEXPAND | wxALL, 0);
  110. $left_sizer->Add($optgroup_movers->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  111. # right pane with preview canvas
  112. my $canvas;
  113. if ($Slic3r::GUI::have_OpenGL) {
  114. $canvas = $self->{canvas} = Slic3r::GUI::3DScene->new($self);
  115. $canvas->enable_picking(1);
  116. $canvas->select_by('volume');
  117. $canvas->on_select(sub {
  118. my ($volume_idx) = @_;
  119. # convert scene volume to model object volume
  120. $self->reload_tree($canvas->volume_idx($volume_idx));
  121. });
  122. $canvas->load_object($self->{model_object}, undef, [0]);
  123. $canvas->set_auto_bed_shape;
  124. $canvas->SetSize([500,700]);
  125. $canvas->zoom_to_volumes;
  126. }
  127. $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL);
  128. $self->{sizer}->Add($left_sizer, 0, wxEXPAND | wxALL, 0);
  129. $self->{sizer}->Add($canvas, 1, wxEXPAND | wxALL, 0) if $canvas;
  130. $self->SetSizer($self->{sizer});
  131. $self->{sizer}->SetSizeHints($self);
  132. # attach events
  133. EVT_TREE_ITEM_COLLAPSING($self, $tree, sub {
  134. my ($self, $event) = @_;
  135. $event->Veto;
  136. });
  137. EVT_TREE_SEL_CHANGED($self, $tree, sub {
  138. my ($self, $event) = @_;
  139. return if $self->{disable_tree_sel_changed_event};
  140. $self->selection_changed;
  141. });
  142. EVT_TREE_ITEM_RIGHT_CLICK($self, $tree, sub {
  143. my ($self, $event) = @_;
  144. my $item = $event->GetItem;
  145. my $frame = $self->GetFrame;
  146. my $menu = Wx::Menu->new;
  147. {
  148. my $scaleMenu = Wx::Menu->new;
  149. wxTheApp->append_menu_item($scaleMenu, "Uniformly… ", 'Scale the selected object along the XYZ axes',
  150. sub { $self->changescale(undef, 0) });
  151. wxTheApp->append_menu_item($scaleMenu, "Along X axis…", 'Scale the selected object along the X axis',
  152. sub { $self->changescale(X, 0) }, undef, 'bullet_red.png');
  153. wxTheApp->append_menu_item($scaleMenu, "Along Y axis…", 'Scale the selected object along the Y axis',
  154. sub { $self->changescale(Y, 0) }, undef, 'bullet_green.png');
  155. wxTheApp->append_menu_item($scaleMenu, "Along Z axis…", 'Scale the selected object along the Z axis',
  156. sub { $self->changescale(Z, 0) }, undef, 'bullet_blue.png');
  157. wxTheApp->append_submenu($menu, "Scale", 'Scale the selected object by a given factor',
  158. $scaleMenu, undef, 'arrow_out.png');
  159. }
  160. {
  161. my $scaleToSizeMenu = Wx::Menu->new;
  162. wxTheApp->append_menu_item($scaleToSizeMenu, "Uniformly… ", 'Scale the selected object along the XYZ axes',
  163. sub { $self->changescale(undef, 1) });
  164. wxTheApp->append_menu_item($scaleToSizeMenu, "Along X axis…", 'Scale the selected object along the X axis',
  165. sub { $self->changescale(X, 1) }, undef, 'bullet_red.png');
  166. wxTheApp->append_menu_item($scaleToSizeMenu, "Along Y axis…", 'Scale the selected object along the Y axis',
  167. sub { $self->changescale(Y, 1) }, undef, 'bullet_green.png');
  168. wxTheApp->append_menu_item($scaleToSizeMenu, "Along Z axis…", 'Scale the selected object along the Z axis',
  169. sub { $self->changescale(Z, 1) }, undef, 'bullet_blue.png');
  170. wxTheApp->append_submenu($menu, "Scale to size", 'Scale the selected object to match a given size',
  171. $scaleToSizeMenu, undef, 'arrow_out.png');
  172. }
  173. {
  174. my $rotateMenu = Wx::Menu->new;
  175. wxTheApp->append_menu_item($rotateMenu, "Around X axis…", 'Rotate the selected object by an arbitrary angle around X axis',
  176. sub { $self->rotate(undef, X) }, undef, 'bullet_red.png');
  177. wxTheApp->append_menu_item($rotateMenu, "Around Y axis…", 'Rotate the selected object by an arbitrary angle around Y axis',
  178. sub { $self->rotate(undef, Y) }, undef, 'bullet_green.png');
  179. wxTheApp->append_menu_item($rotateMenu, "Around Z axis…", 'Rotate the selected object by an arbitrary angle around Z axis',
  180. sub { $self->rotate(undef, Z) }, undef, 'bullet_blue.png');
  181. wxTheApp->append_submenu($menu, "Rotate", 'Rotate the selected object by an arbitrary angle',
  182. $rotateMenu, undef, 'arrow_rotate_anticlockwise.png');
  183. }
  184. $frame->PopupMenu($menu, $event->GetPoint);
  185. });
  186. EVT_BUTTON($self, $self->{btn_load_part}, sub { $self->on_btn_load(0) });
  187. EVT_BUTTON($self, $self->{btn_load_modifier}, sub { $self->on_btn_load(1) });
  188. EVT_BUTTON($self, $self->{btn_load_lambda_modifier}, sub { $self->on_btn_lambda(1) });
  189. EVT_BUTTON($self, $self->{btn_delete}, \&on_btn_delete);
  190. $self->reload_tree;
  191. return $self;
  192. }
  193. sub reload_tree {
  194. my ($self, $selected_volume_idx) = @_;
  195. $selected_volume_idx //= -1;
  196. my $object = $self->{model_object};
  197. my $tree = $self->{tree};
  198. my $rootId = $tree->GetRootItem;
  199. # despite wxWidgets states that DeleteChildren "will not generate any events unlike Delete() method",
  200. # the MSW implementation of DeleteChildren actually calls Delete() for each item, so
  201. # EVT_TREE_SEL_CHANGED is being called, with bad effects (the event handler is called; this
  202. # subroutine is never continued; an invisible EndModal is called on the dialog causing Plater
  203. # to continue its logic and rescheduling the background process etc. GH #2774)
  204. $self->{disable_tree_sel_changed_event} = 1;
  205. $tree->DeleteChildren($rootId);
  206. $self->{disable_tree_sel_changed_event} = 0;
  207. my $selectedId = $rootId;
  208. foreach my $volume_id (0..$#{$object->volumes}) {
  209. my $volume = $object->volumes->[$volume_id];
  210. my $icon = $volume->modifier ? ICON_MODIFIERMESH : ICON_SOLIDMESH;
  211. my $itemId = $tree->AppendItem($rootId, $volume->name || $volume_id, $icon);
  212. if ($volume_id == $selected_volume_idx) {
  213. $selectedId = $itemId;
  214. }
  215. $tree->SetPlData($itemId, {
  216. type => 'volume',
  217. volume_id => $volume_id,
  218. });
  219. }
  220. $tree->ExpandAll;
  221. Slic3r::GUI->CallAfter(sub {
  222. $self->{tree}->SelectItem($selectedId);
  223. # SelectItem() should trigger EVT_TREE_SEL_CHANGED as per wxWidgets docs,
  224. # but in fact it doesn't if the given item is already selected (this happens
  225. # on first load)
  226. $self->selection_changed;
  227. });
  228. }
  229. sub get_selection {
  230. my ($self) = @_;
  231. my $nodeId = $self->{tree}->GetSelection;
  232. if ($nodeId->IsOk) {
  233. return $self->{tree}->GetPlData($nodeId);
  234. }
  235. return undef;
  236. }
  237. sub selection_changed {
  238. my ($self) = @_;
  239. # deselect all meshes
  240. if ($self->{canvas}) {
  241. $_->selected(0) for @{$self->{canvas}->volumes};
  242. }
  243. # disable things as if nothing is selected
  244. $self->{btn_delete}->Disable;
  245. $self->{settings_panel}->disable;
  246. $self->{settings_panel}->set_config(undef);
  247. # reset move sliders
  248. $self->{optgroup_movers}->set_value("x", 0);
  249. $self->{optgroup_movers}->set_value("y", 0);
  250. $self->{optgroup_movers}->set_value("z", 0);
  251. $self->{move_target} = Slic3r::Pointf3->new;
  252. if (my $itemData = $self->get_selection) {
  253. my ($config, @opt_keys);
  254. if ($itemData->{type} eq 'volume') {
  255. # select volume in 3D preview
  256. if ($self->{canvas}) {
  257. $self->{canvas}->volumes->[ $itemData->{volume_id} ]{selected} = 1;
  258. }
  259. $self->{btn_delete}->Enable;
  260. # attach volume config to settings panel
  261. my $volume = $self->{model_object}->volumes->[ $itemData->{volume_id} ];
  262. my $movers = $self->{optgroup_movers};
  263. my $obj_bb = $self->{model_object}->raw_bounding_box;
  264. my $vol_bb = $volume->mesh->bounding_box;
  265. my $vol_size = $vol_bb->size;
  266. $movers->get_field('x')->set_range($obj_bb->x_min - $vol_size->x, $obj_bb->x_max);
  267. $movers->get_field('y')->set_range($obj_bb->y_min - $vol_size->y, $obj_bb->y_max); #,,
  268. $movers->get_field('z')->set_range($obj_bb->z_min - $vol_size->z, $obj_bb->z_max);
  269. $movers->get_field('x')->set_value($vol_bb->x_min);
  270. $movers->get_field('y')->set_value($vol_bb->y_min);
  271. $movers->get_field('z')->set_value($vol_bb->z_min);
  272. $self->{left_sizer}->Show($movers->sizer);
  273. $config = $volume->config;
  274. $self->{staticbox}->SetLabel('Part Settings');
  275. # get default values
  276. @opt_keys = @{Slic3r::Config::PrintRegion->new->get_keys};
  277. } elsif ($itemData->{type} eq 'object') {
  278. # select nothing in 3D preview
  279. # attach object config to settings panel
  280. $self->{left_sizer}->Hide($self->{optgroup_movers}->sizer);
  281. $self->{staticbox}->SetLabel('Object Settings');
  282. @opt_keys = (map @{$_->get_keys}, Slic3r::Config::PrintObject->new, Slic3r::Config::PrintRegion->new);
  283. $config = $self->{model_object}->config;
  284. }
  285. # get default values
  286. my $default_config = Slic3r::Config->new_from_defaults(@opt_keys);
  287. # append default extruder
  288. push @opt_keys, 'extruder';
  289. $default_config->set('extruder', 0);
  290. $config->set_ifndef('extruder', 0);
  291. $self->{settings_panel}->set_default_config($default_config);
  292. $self->{settings_panel}->set_config($config);
  293. $self->{settings_panel}->set_opt_keys(\@opt_keys);
  294. $self->{settings_panel}->set_fixed_options([qw(extruder)]);
  295. $self->{settings_panel}->enable;
  296. }
  297. $self->{canvas}->Render if $self->{canvas};
  298. }
  299. sub on_btn_load {
  300. my ($self, $is_modifier) = @_;
  301. my @input_files = wxTheApp->open_model($self);
  302. foreach my $input_file (@input_files) {
  303. my $model = eval { Slic3r::Model->read_from_file($input_file) };
  304. if ($@) {
  305. Slic3r::GUI::show_error($self, $@);
  306. next;
  307. }
  308. foreach my $object (@{$model->objects}) {
  309. foreach my $volume (@{$object->volumes}) {
  310. my $new_volume = $self->{model_object}->add_volume($volume);
  311. $new_volume->set_modifier($is_modifier);
  312. $new_volume->set_name(basename($input_file));
  313. # apply the same translation we applied to the object
  314. $new_volume->mesh->translate(@{$self->{model_object}->origin_translation});
  315. # set a default extruder value, since user can't add it manually
  316. $new_volume->config->set_ifndef('extruder', 0);
  317. $self->{parts_changed} = 1;
  318. }
  319. }
  320. }
  321. $self->_parts_changed;
  322. }
  323. sub on_btn_lambda {
  324. my ($self, $is_modifier) = @_;
  325. my $dlg = Slic3r::GUI::Plater::LambdaObjectDialog->new($self);
  326. if ($dlg->ShowModal() == wxID_CANCEL) {
  327. return;
  328. }
  329. my $params = $dlg->ObjectParameter;
  330. my $type = "".$params->{"type"};
  331. my $name = "lambda-".$params->{"type"};
  332. my $mesh;
  333. if ($type eq "box") {
  334. $mesh = Slic3r::TriangleMesh::make_cube(@{$params->{"dim"}});
  335. } elsif ($type eq "cylinder") {
  336. $mesh = Slic3r::TriangleMesh::make_cylinder($params->{"cyl_r"}, $params->{"cyl_h"});
  337. } elsif ($type eq "sphere") {
  338. $mesh = Slic3r::TriangleMesh::make_sphere($params->{"sph_rho"});
  339. } elsif ($type eq "slab") {
  340. my $size = $self->{model_object}->bounding_box->size;
  341. $mesh = Slic3r::TriangleMesh::make_cube(
  342. $size->x*1.5,
  343. $size->y*1.5, #**
  344. $params->{"slab_h"},
  345. );
  346. # box sets the base coordinate at 0,0, move to center of plate
  347. $mesh->translate(
  348. -$size->x*1.5/2.0,
  349. -$size->y*1.5/2.0, #**
  350. 0,
  351. );
  352. } else {
  353. return;
  354. }
  355. my $center = $self->{model_object}->bounding_box->center;
  356. if (!$Slic3r::GUI::Settings->{_}{autocenter}) {
  357. #TODO what we really want to do here is just align the
  358. # center of the modifier to the center of the part.
  359. $mesh->translate($center->x, $center->y, 0);
  360. }
  361. $mesh->repair;
  362. my $new_volume = $self->{model_object}->add_volume(mesh => $mesh);
  363. $new_volume->set_modifier($is_modifier);
  364. $new_volume->set_name($name);
  365. # set a default extruder value, since user can't add it manually
  366. $new_volume->config->set_ifndef('extruder', 0);
  367. $self->_parts_changed($self->{model_object}->volumes_count-1);
  368. }
  369. sub on_btn_delete {
  370. my ($self) = @_;
  371. my $itemData = $self->get_selection;
  372. if ($itemData && $itemData->{type} eq 'volume') {
  373. my $volume = $self->{model_object}->volumes->[$itemData->{volume_id}];
  374. # if user is deleting the last solid part, throw error
  375. if (!$volume->modifier && scalar(grep !$_->modifier, @{$self->{model_object}->volumes}) == 1) {
  376. Slic3r::GUI::show_error($self, "You can't delete the last solid part from this object.");
  377. return;
  378. }
  379. $self->{model_object}->delete_volume($itemData->{volume_id});
  380. $self->{parts_changed} = 1;
  381. }
  382. $self->_parts_changed;
  383. }
  384. sub _parts_changed {
  385. my ($self, $selected_volume_idx) = @_;
  386. $self->{parts_changed} = 1;
  387. $self->reload_tree($selected_volume_idx);
  388. if ($self->{canvas}) {
  389. $self->{canvas}->reset_objects;
  390. $self->{canvas}->load_object($self->{model_object});
  391. $self->{canvas}->zoom_to_volumes;
  392. $self->{canvas}->Render;
  393. }
  394. }
  395. sub CanClose {
  396. my $self = shift;
  397. return 1; # skip validation for now
  398. # validate options before allowing user to dismiss the dialog
  399. # the validate method only works on full configs so we have
  400. # to merge our settings with the default ones
  401. my $config = Slic3r::Config->merge($self->GetParent->GetParent->GetParent->GetParent->GetParent->config, $self->model_object->config);
  402. eval {
  403. $config->validate;
  404. };
  405. return 0 if Slic3r::GUI::catch_error($self);
  406. return 1;
  407. }
  408. sub PartsChanged {
  409. my ($self) = @_;
  410. return $self->{parts_changed};
  411. }
  412. sub PartSettingsChanged {
  413. my ($self) = @_;
  414. return $self->{part_settings_changed};
  415. }
  416. sub _update {
  417. my ($self) = @_;
  418. my $itemData = $self->get_selection;
  419. if ($itemData && $itemData->{type} eq 'volume') {
  420. my $volume = $self->{model_object}->volumes->[$itemData->{volume_id}];
  421. $volume->mesh->translate(@{ $volume->mesh->bounding_box->min_point->vector_to($self->{move_target}) });
  422. }
  423. $self->{parts_changed} = 1;
  424. my @objects = ();
  425. push @objects, $self->{model_object};
  426. $self->{canvas}->reset_objects;
  427. $self->{canvas}->load_object($_, undef, [0]) for @objects;
  428. $self->{canvas}->Render;
  429. }
  430. sub changescale {
  431. my ($self, $axis, $tosize) = @_;
  432. my $itemData = $self->get_selection;
  433. if ($itemData && $itemData->{type} eq 'volume') {
  434. my $volume = $self->{model_object}->volumes->[$itemData->{volume_id}];
  435. my $object_size = $volume->bounding_box->size;
  436. if (defined $axis) {
  437. my $axis_name = $axis == X ? 'X' : $axis == Y ? 'Y' : 'Z';
  438. my $scale;
  439. if (defined $tosize) {
  440. my $cursize = $object_size->[$axis];
  441. # Wx::GetNumberFromUser() does not support decimal numbers
  442. my $newsize = Wx::GetTextFromUser(
  443. sprintf("Enter the new size for the selected mesh:"),
  444. "Scale along $axis_name",
  445. $cursize, $self);
  446. return if !$newsize || $newsize !~ /^\d*(?:\.\d*)?$/ || $newsize < 0;
  447. $scale = $newsize / $cursize * 100;
  448. } else {
  449. # Wx::GetNumberFromUser() does not support decimal numbers
  450. $scale = Wx::GetTextFromUser("Enter the scale % for the selected object:",
  451. "Scale along $axis_name", 100, $self);
  452. $scale =~ s/%$//;
  453. return if !$scale || $scale !~ /^\d*(?:\.\d*)?$/ || $scale < 0;
  454. }
  455. my $versor = [1,1,1];
  456. $versor->[$axis] = $scale/100;
  457. $volume->mesh->scale_xyz(Slic3r::Pointf3->new(@$versor));
  458. } else {
  459. my $scale;
  460. if ($tosize) {
  461. my $cursize = max(@$object_size);
  462. # Wx::GetNumberFromUser() does not support decimal numbers
  463. my $newsize = Wx::GetTextFromUser("Enter the new max size for the selected object:",
  464. "Scale", $cursize, $self);
  465. return if !$newsize || $newsize !~ /^\d*(?:\.\d*)?$/ || $newsize < 0;
  466. $scale = $newsize / $cursize;
  467. } else {
  468. # max scale factor should be above 2540 to allow importing files exported in inches
  469. # Wx::GetNumberFromUser() does not support decimal numbers
  470. $scale = Wx::GetTextFromUser("Enter the scale % for the selected object:", 'Scale',
  471. 100, $self);
  472. return if !$scale || $scale !~ /^\d*(?:\.\d*)?$/ || $scale < 0;
  473. }
  474. return if !$scale || $scale < 0;
  475. $volume->mesh->scale($scale);
  476. }
  477. $self->_parts_changed;
  478. }
  479. }
  480. sub rotate {
  481. my $self = shift;
  482. my ($angle, $axis) = @_;
  483. # angle is in degrees
  484. my $itemData = $self->get_selection;
  485. if ($itemData && $itemData->{type} eq 'volume') {
  486. my $volume = $self->{model_object}->volumes->[$itemData->{volume_id}];
  487. if (!defined $angle) {
  488. my $axis_name = $axis == X ? 'X' : $axis == Y ? 'Y' : 'Z';
  489. my $default = $axis == Z ? 0 : 0;
  490. # Wx::GetNumberFromUser() does not support decimal numbers
  491. $angle = Wx::GetTextFromUser("Enter the rotation angle:", "Rotate around $axis_name axis",
  492. $default, $self);
  493. return if !$angle || $angle !~ /^-?\d*(?:\.\d*)?$/ || $angle == -1;
  494. }
  495. if ($axis == X) { $volume->mesh->rotate_x(deg2rad($angle)); }
  496. if ($axis == Y) { $volume->mesh->rotate_y(deg2rad($angle)); }
  497. if ($axis == Z) { $volume->mesh->rotate_z(deg2rad($angle)); }
  498. $self->_parts_changed;
  499. }
  500. }
  501. sub GetFrame {
  502. my ($self) = @_;
  503. return &Wx::GetTopLevelParent($self);
  504. }
  505. 1;