Plater.pm 130 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382
  1. # The "Plater" tab. It contains the "3D", "2D", "Preview" and "Layers" subtabs.
  2. package Slic3r::GUI::Plater::UndoOperation;
  3. use strict;
  4. use warnings;
  5. sub new{
  6. my $class = shift;
  7. my $self = {
  8. type => shift,
  9. object_identifier => shift,
  10. attributes => shift,
  11. };
  12. bless ($self, $class);
  13. return $self;
  14. }
  15. package Slic3r::GUI::Plater;
  16. use strict;
  17. use warnings;
  18. use utf8;
  19. use File::Basename qw(basename dirname);
  20. use List::Util qw(sum first max none any);
  21. use Slic3r::Geometry qw(X Y Z MIN MAX scale unscale deg2rad rad2deg);
  22. use Math::Trig qw(acos);
  23. use LWP::UserAgent;
  24. use threads::shared qw(shared_clone);
  25. use Wx qw(:button :cursor :dialog :filedialog :keycode :icon :font :id :misc
  26. :panel :sizer :toolbar :window wxTheApp :notebook :combobox);
  27. use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL
  28. EVT_CHOICE EVT_COMBOBOX EVT_TIMER EVT_NOTEBOOK_PAGE_CHANGED EVT_LEFT_UP EVT_CLOSE);
  29. use base qw(Wx::Panel Class::Accessor);
  30. __PACKAGE__->mk_accessors(qw(presets));
  31. use constant TB_ADD => &Wx::NewId;
  32. use constant TB_REMOVE => &Wx::NewId;
  33. use constant TB_RESET => &Wx::NewId;
  34. use constant TB_ARRANGE => &Wx::NewId;
  35. use constant TB_EXPORT_GCODE => &Wx::NewId;
  36. use constant TB_EXPORT_STL => &Wx::NewId;
  37. use constant TB_MORE => &Wx::NewId;
  38. use constant TB_FEWER => &Wx::NewId;
  39. use constant TB_45CW => &Wx::NewId;
  40. use constant TB_45CCW => &Wx::NewId;
  41. use constant TB_ROTFACE => &Wx::NewId;
  42. use constant TB_SCALE => &Wx::NewId;
  43. use constant TB_SPLIT => &Wx::NewId;
  44. use constant TB_CUT => &Wx::NewId;
  45. use constant TB_LAYERS => &Wx::NewId;
  46. use constant TB_SETTINGS => &Wx::NewId;
  47. # package variables to avoid passing lexicals to threads
  48. our $THUMBNAIL_DONE_EVENT : shared = Wx::NewEventType;
  49. our $PROGRESS_BAR_EVENT : shared = Wx::NewEventType;
  50. our $ERROR_EVENT : shared = Wx::NewEventType;
  51. our $EXPORT_COMPLETED_EVENT : shared = Wx::NewEventType;
  52. our $PROCESS_COMPLETED_EVENT : shared = Wx::NewEventType;
  53. use constant FILAMENT_CHOOSERS_SPACING => 0;
  54. use constant PROCESS_DELAY => 0.5 * 1000; # milliseconds
  55. sub new {
  56. my $class = shift;
  57. my ($parent) = @_;
  58. my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
  59. $self->{config} = Slic3r::Config->new_from_defaults(qw(
  60. bed_shape complete_objects extruder_clearance_radius skirts skirt_distance brim_width
  61. serial_port serial_speed host_type print_host octoprint_apikey shortcuts filament_colour
  62. ));
  63. $self->{model} = Slic3r::Model->new;
  64. $self->{print} = Slic3r::Print->new;
  65. $self->{processed} = 0;
  66. # List of Perl objects Slic3r::GUI::Plater::Object, representing a 2D preview of the platter.
  67. $self->{objects} = [];
  68. # Objects identifier used for undo/redo operations. It's a one time id assigned to each newly created object.
  69. $self->{object_identifier} = 0;
  70. # Stack of undo operations.
  71. $self->{undo_stack} = [];
  72. # Stack of redo operations.
  73. $self->{redo_stack} = [];
  74. $self->{print}->set_status_cb(sub {
  75. my ($percent, $message) = @_;
  76. if ($Slic3r::have_threads) {
  77. Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROGRESS_BAR_EVENT, shared_clone([$percent, $message])));
  78. } else {
  79. $self->on_progress_event($percent, $message);
  80. }
  81. });
  82. # Initialize preview notebook
  83. $self->{preview_notebook} = Wx::Notebook->new($self, -1, wxDefaultPosition, [335,335], wxNB_BOTTOM);
  84. # Initialize handlers for canvases
  85. my $on_select_object = sub {
  86. my ($obj_idx) = @_;
  87. $self->select_object($obj_idx);
  88. };
  89. my $on_double_click = sub {
  90. $self->object_settings_dialog if $self->selected_object;
  91. };
  92. my $on_right_click = sub {
  93. my ($canvas, $click_pos) = @_;
  94. my ($obj_idx, $object) = $self->selected_object;
  95. return if !defined $obj_idx;
  96. my $menu = $self->object_menu;
  97. $canvas->PopupMenu($menu, $click_pos);
  98. $menu->Destroy;
  99. };
  100. my $on_instances_moved = sub {
  101. $self->on_model_change;
  102. };
  103. # Initialize 3D plater
  104. if ($Slic3r::GUI::have_OpenGL) {
  105. $self->{canvas3D} = Slic3r::GUI::Plater::3D->new($self->{preview_notebook}, $self->{objects}, $self->{model}, $self->{config});
  106. $self->{preview_notebook}->AddPage($self->{canvas3D}, '3D');
  107. $self->{canvas3D}->set_on_select_object($on_select_object);
  108. $self->{canvas3D}->set_on_double_click($on_double_click);
  109. $self->{canvas3D}->set_on_right_click(sub { $on_right_click->($self->{canvas3D}, @_); });
  110. $self->{canvas3D}->set_on_instances_moved($on_instances_moved);
  111. $self->{canvas3D}->on_viewport_changed(sub {
  112. $self->{preview3D}->canvas->set_viewport_from_scene($self->{canvas3D});
  113. });
  114. }
  115. # Initialize 2D preview canvas
  116. $self->{canvas} = Slic3r::GUI::Plater::2D->new($self->{preview_notebook}, wxDefaultSize, $self->{objects}, $self->{model}, $self->{config});
  117. $self->{preview_notebook}->AddPage($self->{canvas}, '2D');
  118. $self->{canvas}->on_select_object($on_select_object);
  119. $self->{canvas}->on_double_click($on_double_click);
  120. $self->{canvas}->on_right_click(sub { $on_right_click->($self->{canvas}, @_); });
  121. $self->{canvas}->on_instances_moved($on_instances_moved);
  122. # Initialize 3D toolpaths preview
  123. $self->{preview3D_page_idx} = -1;
  124. if ($Slic3r::GUI::have_OpenGL) {
  125. $self->{preview3D} = Slic3r::GUI::Plater::3DPreview->new($self->{preview_notebook}, $self->{print});
  126. $self->{preview3D}->canvas->on_viewport_changed(sub {
  127. $self->{canvas3D}->set_viewport_from_scene($self->{preview3D}->canvas);
  128. });
  129. $self->{preview_notebook}->AddPage($self->{preview3D}, 'Preview');
  130. $self->{preview3D_page_idx} = $self->{preview_notebook}->GetPageCount-1;
  131. }
  132. # Initialize toolpaths preview
  133. $self->{toolpaths2D_page_idx} = -1;
  134. if ($Slic3r::GUI::have_OpenGL) {
  135. $self->{toolpaths2D} = Slic3r::GUI::Plater::2DToolpaths->new($self->{preview_notebook}, $self->{print});
  136. $self->{preview_notebook}->AddPage($self->{toolpaths2D}, 'Layers');
  137. $self->{toolpaths2D_page_idx} = $self->{preview_notebook}->GetPageCount-1;
  138. }
  139. EVT_NOTEBOOK_PAGE_CHANGED($self, $self->{preview_notebook}, sub {
  140. wxTheApp->CallAfter(sub {
  141. my $sel = $self->{preview_notebook}->GetSelection;
  142. if ($sel == $self->{preview3D_page_idx} || $sel == $self->{toolpaths2D_page_idx}) {
  143. if (!$Slic3r::GUI::Settings->{_}{background_processing} && !$self->{processed}) {
  144. $self->statusbar->SetCancelCallback(sub {
  145. $self->stop_background_process;
  146. $self->statusbar->SetStatusText("Slicing cancelled");
  147. $self->{preview_notebook}->SetSelection(0);
  148. });
  149. $self->start_background_process;
  150. } else {
  151. $self->{preview3D}->load_print
  152. if $sel == $self->{preview3D_page_idx};
  153. }
  154. }
  155. });
  156. });
  157. # toolbar for object manipulation
  158. if (!&Wx::wxMSW) {
  159. Wx::ToolTip::Enable(1);
  160. $self->{htoolbar} = Wx::ToolBar->new($self, -1, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL | wxTB_TEXT | wxBORDER_SIMPLE | wxTAB_TRAVERSAL);
  161. $self->{htoolbar}->AddTool(TB_ADD, "Add…", Wx::Bitmap->new($Slic3r::var->("brick_add.png"), wxBITMAP_TYPE_PNG), '');
  162. $self->{htoolbar}->AddTool(TB_REMOVE, "Delete", Wx::Bitmap->new($Slic3r::var->("brick_delete.png"), wxBITMAP_TYPE_PNG), '');
  163. $self->{htoolbar}->AddTool(TB_RESET, "Delete All", Wx::Bitmap->new($Slic3r::var->("cross.png"), wxBITMAP_TYPE_PNG), '');
  164. $self->{htoolbar}->AddTool(TB_ARRANGE, "Arrange", Wx::Bitmap->new($Slic3r::var->("bricks.png"), wxBITMAP_TYPE_PNG), '');
  165. $self->{htoolbar}->AddSeparator;
  166. $self->{htoolbar}->AddTool(TB_MORE, "More", Wx::Bitmap->new($Slic3r::var->("add.png"), wxBITMAP_TYPE_PNG), '');
  167. $self->{htoolbar}->AddTool(TB_FEWER, "Fewer", Wx::Bitmap->new($Slic3r::var->("delete.png"), wxBITMAP_TYPE_PNG), '');
  168. $self->{htoolbar}->AddSeparator;
  169. $self->{htoolbar}->AddTool(TB_45CCW, "45° ccw", Wx::Bitmap->new($Slic3r::var->("arrow_rotate_anticlockwise.png"), wxBITMAP_TYPE_PNG), '');
  170. $self->{htoolbar}->AddTool(TB_45CW, "45° cw", Wx::Bitmap->new($Slic3r::var->("arrow_rotate_clockwise.png"), wxBITMAP_TYPE_PNG), '');
  171. $self->{htoolbar}->AddTool(TB_ROTFACE, "Rotate face", Wx::Bitmap->new($Slic3r::var->("rotate_face.png"), wxBITMAP_TYPE_PNG), '');
  172. $self->{htoolbar}->AddTool(TB_SCALE, "Scale…", Wx::Bitmap->new($Slic3r::var->("arrow_out.png"), wxBITMAP_TYPE_PNG), '');
  173. $self->{htoolbar}->AddTool(TB_SPLIT, "Split", Wx::Bitmap->new($Slic3r::var->("shape_ungroup.png"), wxBITMAP_TYPE_PNG), '');
  174. $self->{htoolbar}->AddTool(TB_CUT, "Cut…", Wx::Bitmap->new($Slic3r::var->("package.png"), wxBITMAP_TYPE_PNG), '');
  175. $self->{htoolbar}->AddSeparator;
  176. $self->{htoolbar}->AddTool(TB_SETTINGS, "Settings…", Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG), '');
  177. $self->{htoolbar}->AddTool(TB_LAYERS, "Layer heights…", Wx::Bitmap->new($Slic3r::var->("variable_layer_height.png"), wxBITMAP_TYPE_PNG), '');
  178. } else {
  179. my %tbar_buttons = (
  180. add => "Add…",
  181. remove => "Delete",
  182. reset => "Delete All",
  183. arrange => "Arrange",
  184. increase => "",
  185. decrease => "",
  186. rotate45ccw => "",
  187. rotate45cw => "",
  188. rotateFace => "",
  189. changescale => "Scale…",
  190. split => "Split",
  191. cut => "Cut…",
  192. layers => "Layer heights…",
  193. settings => "Settings…",
  194. );
  195. $self->{btoolbar} = Wx::BoxSizer->new(wxHORIZONTAL);
  196. for (qw(add remove reset arrange increase decrease rotate45ccw rotate45cw rotateFace changescale split cut layers settings)) {
  197. $self->{"btn_$_"} = Wx::Button->new($self, -1, $tbar_buttons{$_}, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
  198. $self->{btoolbar}->Add($self->{"btn_$_"});
  199. }
  200. }
  201. # right pane buttons
  202. $self->{btn_export_gcode} = Wx::Button->new($self, -1, "Export G-code…", wxDefaultPosition, [-1, 30], wxBU_LEFT);
  203. $self->{btn_print} = Wx::Button->new($self, -1, "Print…", wxDefaultPosition, [-1, 30], wxBU_LEFT);
  204. $self->{btn_send_gcode} = Wx::Button->new($self, -1, "Send to printer", wxDefaultPosition, [-1, 30], wxBU_LEFT);
  205. $self->{btn_export_stl} = Wx::Button->new($self, -1, "Export STL…", wxDefaultPosition, [-1, 30], wxBU_LEFT);
  206. #$self->{btn_export_gcode}->SetFont($Slic3r::GUI::small_font);
  207. #$self->{btn_export_stl}->SetFont($Slic3r::GUI::small_font);
  208. $self->{btn_print}->Hide;
  209. $self->{btn_send_gcode}->Hide;
  210. if ($Slic3r::GUI::have_button_icons) {
  211. my %icons = qw(
  212. add brick_add.png
  213. remove brick_delete.png
  214. reset cross.png
  215. arrange bricks.png
  216. export_gcode cog_go.png
  217. print arrow_up.png
  218. send_gcode arrow_up.png
  219. export_stl brick_go.png
  220. increase add.png
  221. decrease delete.png
  222. rotate45cw arrow_rotate_clockwise.png
  223. rotate45ccw arrow_rotate_anticlockwise.png
  224. rotateFace rotate_face.png
  225. changescale arrow_out.png
  226. split shape_ungroup.png
  227. cut package.png
  228. layers variable_layer_height.png
  229. settings cog.png
  230. );
  231. for (grep $self->{"btn_$_"}, keys %icons) {
  232. $self->{"btn_$_"}->SetBitmap(Wx::Bitmap->new($Slic3r::var->($icons{$_}), wxBITMAP_TYPE_PNG));
  233. }
  234. }
  235. $self->selection_changed(0);
  236. $self->object_list_changed;
  237. EVT_BUTTON($self, $self->{btn_export_gcode}, sub {
  238. $self->export_gcode;
  239. });
  240. EVT_BUTTON($self, $self->{btn_print}, sub {
  241. $self->{print_file} = $self->export_gcode(Wx::StandardPaths::Get->GetTempDir());
  242. });
  243. EVT_LEFT_UP($self->{btn_send_gcode}, sub {
  244. my (undef, $e) = @_;
  245. my $alt = $e->ShiftDown;
  246. wxTheApp->CallAfter(sub {
  247. $self->prepare_send($alt);
  248. });
  249. });
  250. EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl);
  251. if ($self->{htoolbar}) {
  252. EVT_TOOL($self, TB_ADD, sub { $self->add; });
  253. EVT_TOOL($self, TB_REMOVE, sub { $self->remove() }); # explicitly pass no argument to remove
  254. EVT_TOOL($self, TB_RESET, sub { $self->reset; });
  255. EVT_TOOL($self, TB_ARRANGE, sub { $self->arrange; });
  256. EVT_TOOL($self, TB_MORE, sub { $self->increase; });
  257. EVT_TOOL($self, TB_FEWER, sub { $self->decrease; });
  258. EVT_TOOL($self, TB_45CW, sub { $_[0]->rotate(-45) });
  259. EVT_TOOL($self, TB_45CCW, sub { $_[0]->rotate(45) });
  260. EVT_TOOL($self, TB_ROTFACE, sub { $_[0]->rotate_face });
  261. EVT_TOOL($self, TB_SCALE, sub { $self->changescale(undef); });
  262. EVT_TOOL($self, TB_SPLIT, sub { $self->split_object; });
  263. EVT_TOOL($self, TB_CUT, sub { $_[0]->object_cut_dialog });
  264. EVT_TOOL($self, TB_LAYERS, sub { $_[0]->object_layers_dialog });
  265. EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog });
  266. } else {
  267. EVT_BUTTON($self, $self->{btn_add}, sub { $self->add; });
  268. EVT_BUTTON($self, $self->{btn_remove}, sub { $self->remove() }); # explicitly pass no argument to remove
  269. EVT_BUTTON($self, $self->{btn_reset}, sub { $self->reset; });
  270. EVT_BUTTON($self, $self->{btn_arrange}, sub { $self->arrange; });
  271. EVT_BUTTON($self, $self->{btn_increase}, sub { $self->increase; });
  272. EVT_BUTTON($self, $self->{btn_decrease}, sub { $self->decrease; });
  273. EVT_BUTTON($self, $self->{btn_rotate45cw}, sub { $_[0]->rotate(-45) });
  274. EVT_BUTTON($self, $self->{btn_rotate45ccw}, sub { $_[0]->rotate(45) });
  275. EVT_BUTTON($self, $self->{btn_rotateFace}, sub { $_[0]->rotate_face });
  276. EVT_BUTTON($self, $self->{btn_changescale}, sub { $self->changescale(undef); });
  277. EVT_BUTTON($self, $self->{btn_split}, sub { $self->split_object; });
  278. EVT_BUTTON($self, $self->{btn_cut}, sub { $_[0]->object_cut_dialog });
  279. EVT_BUTTON($self, $self->{btn_layers}, sub { $_[0]->object_layers_dialog });
  280. EVT_BUTTON($self, $self->{btn_settings}, sub { $_[0]->object_settings_dialog });
  281. }
  282. $_->SetDropTarget(Slic3r::GUI::Plater::DropTarget->new($self))
  283. for grep defined($_),
  284. $self, $self->{canvas}, $self->{canvas3D}, $self->{preview3D};
  285. EVT_COMMAND($self, -1, $THUMBNAIL_DONE_EVENT, sub {
  286. my ($self, $event) = @_;
  287. my ($obj_idx) = @{$event->GetData};
  288. return if !$self->{objects}[$obj_idx]; # object was deleted before thumbnail generation completed
  289. $self->on_thumbnail_made($obj_idx);
  290. });
  291. EVT_COMMAND($self, -1, $PROGRESS_BAR_EVENT, sub {
  292. my ($self, $event) = @_;
  293. my ($percent, $message) = @{$event->GetData};
  294. $self->on_progress_event($percent, $message);
  295. });
  296. EVT_COMMAND($self, -1, $ERROR_EVENT, sub {
  297. my ($self, $event) = @_;
  298. Slic3r::GUI::show_error($self, @{$event->GetData});
  299. });
  300. EVT_COMMAND($self, -1, $EXPORT_COMPLETED_EVENT, sub {
  301. my ($self, $event) = @_;
  302. $self->on_export_completed($event->GetData);
  303. });
  304. EVT_COMMAND($self, -1, $PROCESS_COMPLETED_EVENT, sub {
  305. my ($self, $event) = @_;
  306. $self->on_process_completed($event->GetData);
  307. });
  308. if ($Slic3r::have_threads) {
  309. my $timer_id = Wx::NewId();
  310. $self->{apply_config_timer} = Wx::Timer->new($self, $timer_id);
  311. EVT_TIMER($self, $timer_id, sub {
  312. my ($self, $event) = @_;
  313. $self->async_apply_config;
  314. });
  315. }
  316. $self->{canvas}->update_bed_size;
  317. if ($self->{canvas3D}) {
  318. $self->{canvas3D}->update_bed_size;
  319. $self->{canvas3D}->zoom_to_bed;
  320. }
  321. if ($self->{preview3D}) {
  322. $self->{preview3D}->set_bed_shape($self->{config}->bed_shape);
  323. }
  324. {
  325. my $presets = $self->{presets_sizer} = Wx::FlexGridSizer->new(3, 3, 1, 2);
  326. $presets->AddGrowableCol(1, 1);
  327. $presets->SetFlexibleDirection(wxHORIZONTAL);
  328. my %group_labels = (
  329. print => 'Print settings',
  330. filament => 'Filament',
  331. printer => 'Printer',
  332. );
  333. $self->{preset_choosers} = {};
  334. $self->{preset_choosers_names} = {}; # wxChoice* => []
  335. for my $group (qw(print filament printer)) {
  336. # label
  337. my $text = Wx::StaticText->new($self, -1, "$group_labels{$group}:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT);
  338. $text->SetFont($Slic3r::GUI::small_font);
  339. # dropdown control
  340. my $choice = Wx::BitmapComboBox->new($self, -1, "", wxDefaultPosition, wxDefaultSize, [], wxCB_READONLY);
  341. $self->{preset_choosers}{$group} = [$choice];
  342. # setup the listener
  343. EVT_COMBOBOX($choice, $choice, sub {
  344. my ($choice) = @_;
  345. wxTheApp->CallAfter(sub {
  346. $self->_on_change_combobox($group, $choice);
  347. });
  348. });
  349. # settings button
  350. my $settings_btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG),
  351. wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
  352. EVT_BUTTON($self, $settings_btn, sub {
  353. $self->show_preset_editor($group, 0);
  354. });
  355. $presets->Add($text, 0, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
  356. $presets->Add($choice, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxBOTTOM, 0);
  357. $presets->Add($settings_btn, 0, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxLEFT, 3);
  358. }
  359. {
  360. my $o = $self->{settings_override_panel} = Slic3r::GUI::Plater::OverrideSettingsPanel->new($self,
  361. on_change => sub {
  362. my ($opt_key) = @_;
  363. my ($preset) = $self->selected_presets('print');
  364. $preset->load_config;
  365. # If this option is not in the override panel it means it was manually deleted,
  366. # so let's restore the profile value.
  367. if (!$self->{settings_override_config}->has($opt_key)) {
  368. $preset->_dirty_config->set($opt_key, $preset->_config->get($opt_key));
  369. } else {
  370. # Apply the overrides to the current Print preset, potentially making it dirty
  371. $preset->_dirty_config->apply($self->{settings_override_config});
  372. # If this is a configured shortcut (and not just a dirty option),
  373. # save it now.
  374. if (any { $_ eq $opt_key } @{$preset->dirty_config->shortcuts}) {
  375. $preset->save([$opt_key]);
  376. }
  377. }
  378. $self->load_presets;
  379. $self->config_changed;
  380. # Reload the open tab if any
  381. if (my $print_tab = $self->GetFrame->{preset_editor_tabs}{print}) {
  382. $print_tab->load_presets;
  383. $print_tab->reload_preset;
  384. }
  385. });
  386. $o->can_add(0);
  387. $o->can_delete(1);
  388. $o->set_opt_keys([ Slic3r::GUI::PresetEditor::Print->options ]);
  389. $self->{settings_override_config} = Slic3r::Config->new;
  390. $o->set_default_config($self->{settings_override_config});
  391. $o->set_config($self->{settings_override_config});
  392. }
  393. my $object_info_sizer;
  394. {
  395. my $box = Wx::StaticBox->new($self, -1, "Info");
  396. $object_info_sizer = Wx::StaticBoxSizer->new($box, wxVERTICAL);
  397. $object_info_sizer->SetMinSize([350,-1]);
  398. {
  399. my $sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  400. $object_info_sizer->Add($sizer, 0, wxEXPAND | wxBOTTOM, 5);
  401. my $text = Wx::StaticText->new($self, -1, "Object:", wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
  402. $text->SetFont($Slic3r::GUI::small_font);
  403. $sizer->Add($text, 0, wxALIGN_CENTER_VERTICAL);
  404. # We supply a bogus width to wxChoice (sizer will override it and stretch
  405. # the control anyway), because if we leave the default (-1) it will stretch
  406. # too much according to the contents, and this is bad with long file names.
  407. $self->{object_info_choice} = Wx::Choice->new($self, -1, wxDefaultPosition, [100,-1], []);
  408. $self->{object_info_choice}->SetFont($Slic3r::GUI::small_font);
  409. $sizer->Add($self->{object_info_choice}, 1, wxALIGN_CENTER_VERTICAL);
  410. EVT_CHOICE($self, $self->{object_info_choice}, sub {
  411. $self->select_object($self->{object_info_choice}->GetSelection);
  412. $self->refresh_canvases;
  413. });
  414. }
  415. my $grid_sizer = Wx::FlexGridSizer->new(3, 4, 5, 5);
  416. $grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
  417. $grid_sizer->AddGrowableCol(1, 1);
  418. $grid_sizer->AddGrowableCol(3, 1);
  419. $object_info_sizer->Add($grid_sizer, 0, wxEXPAND);
  420. my @info = (
  421. copies => "Copies",
  422. size => "Size",
  423. volume => "Volume",
  424. facets => "Facets",
  425. materials => "Materials",
  426. manifold => "Manifold",
  427. );
  428. while (my $field = shift @info) {
  429. my $label = shift @info;
  430. my $text = Wx::StaticText->new($self, -1, "$label:", wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
  431. $text->SetFont($Slic3r::GUI::small_font);
  432. $grid_sizer->Add($text, 0);
  433. $self->{"object_info_$field"} = Wx::StaticText->new($self, -1, "", wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
  434. $self->{"object_info_$field"}->SetFont($Slic3r::GUI::small_font);
  435. if ($field eq 'manifold') {
  436. $self->{object_info_manifold_warning_icon} = Wx::StaticBitmap->new($self, -1, Wx::Bitmap->new($Slic3r::var->("error.png"), wxBITMAP_TYPE_PNG));
  437. $self->{object_info_manifold_warning_icon}->Hide;
  438. my $h_sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  439. $h_sizer->Add($self->{object_info_manifold_warning_icon}, 0);
  440. $h_sizer->Add($self->{"object_info_$field"}, 0);
  441. $grid_sizer->Add($h_sizer, 0, wxEXPAND);
  442. } else {
  443. $grid_sizer->Add($self->{"object_info_$field"}, 0);
  444. }
  445. }
  446. }
  447. my $print_info_sizer;
  448. {
  449. my $box = Wx::StaticBox->new($self, -1, "Print Summary");
  450. $print_info_sizer = Wx::StaticBoxSizer->new($box, wxVERTICAL);
  451. $print_info_sizer->SetMinSize([350,-1]);
  452. my $grid_sizer = Wx::FlexGridSizer->new(2, 2, 5, 5);
  453. $grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
  454. $grid_sizer->AddGrowableCol(1, 1);
  455. $grid_sizer->AddGrowableCol(3, 1);
  456. $print_info_sizer->Add($grid_sizer, 0, wxEXPAND);
  457. my @info = (
  458. fil => "Used Filament",
  459. cost => "Cost",
  460. );
  461. while (my $field = shift @info) {
  462. my $label = shift @info;
  463. my $text = Wx::StaticText->new($self, -1, "$label:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT);
  464. $text->SetFont($Slic3r::GUI::small_font);
  465. $grid_sizer->Add($text, 0);
  466. $self->{"print_info_$field"} = Wx::StaticText->new($self, -1, "", wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
  467. $self->{"print_info_$field"}->SetFont($Slic3r::GUI::small_font);
  468. $grid_sizer->Add($self->{"print_info_$field"}, 0);
  469. }
  470. $self->{sliced_info_box} = $print_info_sizer;
  471. }
  472. my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  473. $buttons_sizer->AddStretchSpacer(1);
  474. $buttons_sizer->Add($self->{btn_export_stl}, 0, wxALIGN_RIGHT, 0);
  475. $buttons_sizer->Add($self->{btn_print}, 0, wxALIGN_RIGHT, 0);
  476. $buttons_sizer->Add($self->{btn_send_gcode}, 0, wxALIGN_RIGHT, 0);
  477. $buttons_sizer->Add($self->{btn_export_gcode}, 0, wxALIGN_RIGHT, 0);
  478. $self->{right_sizer} = my $right_sizer = Wx::BoxSizer->new(wxVERTICAL);
  479. $right_sizer->Add($presets, 0, wxEXPAND | wxTOP, 10) if defined $presets;
  480. $right_sizer->Add($buttons_sizer, 0, wxEXPAND | wxBOTTOM, 5);
  481. $right_sizer->Add($self->{settings_override_panel}, 1, wxEXPAND, 5);
  482. $right_sizer->Add($object_info_sizer, 0, wxEXPAND, 0);
  483. $right_sizer->Add($print_info_sizer, 0, wxEXPAND, 0);
  484. $right_sizer->Hide($print_info_sizer);
  485. my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL);
  486. $hsizer->Add($self->{preview_notebook}, 1, wxEXPAND | wxTOP, 1);
  487. $hsizer->Add($right_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, 3);
  488. my $sizer = Wx::BoxSizer->new(wxVERTICAL);
  489. $sizer->Add($self->{htoolbar}, 0, wxEXPAND, 0) if $self->{htoolbar};
  490. $sizer->Add($self->{btoolbar}, 0, wxEXPAND, 0) if $self->{btoolbar};
  491. $sizer->Add($hsizer, 1, wxEXPAND, 0);
  492. $sizer->SetSizeHints($self);
  493. $self->SetSizer($sizer);
  494. }
  495. $self->load_presets;
  496. $self->_on_select_preset($_) for qw(printer filament print);
  497. return $self;
  498. }
  499. sub prompt_unsaved_changes {
  500. my ($self) = @_;
  501. foreach my $group (qw(printer filament print)) {
  502. foreach my $choice (@{$self->{preset_choosers}{$group}}) {
  503. my $pp = $self->{preset_choosers_names}{$choice};
  504. for my $i (0..$#$pp) {
  505. my $preset = first { $_->name eq $pp->[$i] } @{wxTheApp->presets->{$group}};
  506. if (!$preset->prompt_unsaved_changes($self)) {
  507. # Restore the previous one
  508. $choice->SetSelection($i);
  509. return 0;
  510. }
  511. }
  512. }
  513. }
  514. return 1;
  515. }
  516. sub _on_change_combobox {
  517. my ($self, $group, $choice) = @_;
  518. if (0) {
  519. # This code is disabled because wxPerl doesn't provide GetCurrentSelection
  520. my $current_name = $self->{preset_choosers_names}{$choice}[$choice->GetCurrentSelection];
  521. my $current = first { $_->name eq $current_name } @{wxTheApp->presets->{$group}};
  522. if (!$current->prompt_unsaved_changes($self)) {
  523. # Restore the previous one
  524. $choice->SetSelection($choice->GetCurrentSelection);
  525. return;
  526. }
  527. } else {
  528. return 0 if !$self->prompt_unsaved_changes;
  529. }
  530. wxTheApp->CallAfter(sub {
  531. # Close the preset editor tab if any
  532. if (exists $self->GetFrame->{preset_editor_tabs}{$group}) {
  533. my $tabpanel = $self->GetFrame->{tabpanel};
  534. $tabpanel->DeletePage($tabpanel->GetPageIndex($self->GetFrame->{preset_editor_tabs}{$group}));
  535. delete $self->GetFrame->{preset_editor_tabs}{$group};
  536. $tabpanel->SetSelection(0); # without this, a newly created tab will not be selected by wx
  537. }
  538. $self->_on_select_preset($group);
  539. # This will remove the "(modified)" mark from any dirty preset handled here.
  540. $self->load_presets;
  541. });
  542. }
  543. sub _on_select_preset {
  544. my ($self, $group) = @_;
  545. my @presets = $self->selected_presets($group);
  546. my $s_presets = $Slic3r::GUI::Settings->{presets};
  547. my $changed = !$s_presets->{$group} || $s_presets->{$group} ne $presets[0]->name;
  548. $s_presets->{$group} = $presets[0]->name;
  549. $s_presets->{"${group}_${_}"} = $presets[$_]->name for 1..$#presets;
  550. wxTheApp->save_settings;
  551. # Ignore overrides in the plater, we only care about the preset configs.
  552. my $config = $self->config(1);
  553. $self->on_extruders_change(scalar @{$config->get('nozzle_diameter')});
  554. if ($group eq 'print') {
  555. my $o_config = $self->{settings_override_config};
  556. my $o_panel = $self->{settings_override_panel};
  557. my $shortcuts = $config->get('shortcuts');
  558. # Re-populate the override panel with the configured shortcuts
  559. # and the dirty options.
  560. $o_config->clear;
  561. foreach my $opt_key (@$shortcuts, $presets[0]->dirty_options) {
  562. # Don't add shortcut for shortcuts!
  563. next if $opt_key eq 'shortcuts';
  564. $o_config->set($opt_key, $config->get($opt_key));
  565. }
  566. $o_panel->set_default_config($config);
  567. $o_panel->set_fixed_options(\@$shortcuts);
  568. $o_panel->update_optgroup;
  569. } elsif ($group eq 'printer') {
  570. # reload print and filament settings to honor their compatible_printer options
  571. $self->load_presets;
  572. }
  573. $self->config_changed;
  574. }
  575. sub load_config {
  576. my ($self, $config) = @_;
  577. # This method is called with the CLI options.
  578. # We add them to the visible overrides.
  579. $self->{settings_override_config}->apply($config);
  580. $self->{settings_override_panel}->update_optgroup;
  581. $self->config_changed;
  582. }
  583. sub GetFrame {
  584. my ($self) = @_;
  585. return &Wx::GetTopLevelParent($self);
  586. }
  587. sub load_presets {
  588. my ($self) = @_;
  589. my $selected_printer_name;
  590. foreach my $group (qw(printer filament print)) {
  591. my @presets = @{wxTheApp->presets->{$group}};
  592. # Skip presets not compatible with the selected printer, if they
  593. # have other compatible printers configured (and at least one of them exists).
  594. if ($group eq 'filament' || $group eq 'print') {
  595. my %printer_names = map { $_->name => 1 } @{ wxTheApp->presets->{printer} };
  596. for (my $i = 0; $i <= $#presets; ++$i) {
  597. my $config = $presets[$i]->dirty_config;
  598. next if !$config->has('compatible_printers');
  599. my @compat = @{$config->compatible_printers};
  600. if (@compat
  601. && (none { $_ eq $selected_printer_name } @compat)
  602. && (any { $printer_names{$_} } @compat)) {
  603. splice @presets, $i, 1;
  604. --$i;
  605. }
  606. }
  607. }
  608. # Only show the default presets if we have no other presets.
  609. if (@presets > 1) {
  610. @presets = grep { !$_->default } @presets;
  611. }
  612. # get the wxChoice objects for this group
  613. my @choosers = @{ $self->{preset_choosers}{$group} };
  614. # find the currently selected one(s) according to the saved file
  615. my @sel = ();
  616. if (my $current = $Slic3r::GUI::Settings->{presets}{$group}) {
  617. push @sel, grep defined, first { $presets[$_]->name eq $current } 0..$#presets;
  618. }
  619. for my $i (1..(@choosers-1)) {
  620. if (my $current = $Slic3r::GUI::Settings->{presets}{"${group}_$i"}) {
  621. push @sel, grep defined, first { $presets[$_]->name eq $current } 0..$#presets;
  622. }
  623. }
  624. @sel = (0) if !@sel;
  625. # populate the wxChoice objects
  626. my @preset_names = ();
  627. foreach my $choice (@choosers) {
  628. $choice->Clear;
  629. $self->{preset_choosers_names}{$choice} = [];
  630. foreach my $preset (@presets) {
  631. # load/generate the proper icon
  632. my $bitmap;
  633. if ($group eq 'filament') {
  634. my $config = $preset->dirty_config;
  635. if ($preset->default || !$config->has('filament_colour')) {
  636. $bitmap = Wx::Bitmap->new($Slic3r::var->("spool.png"), wxBITMAP_TYPE_PNG);
  637. } else {
  638. my $rgb_hex = $config->filament_colour->[0];
  639. $rgb_hex =~ s/^#//;
  640. my @rgb = unpack 'C*', pack 'H*', $rgb_hex;
  641. my $image = Wx::Image->new(16,16);
  642. $image->SetRGB(Wx::Rect->new(0,0,16,16), @rgb);
  643. $bitmap = Wx::Bitmap->new($image);
  644. }
  645. } elsif ($group eq 'print') {
  646. $bitmap = Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG);
  647. } elsif ($group eq 'printer') {
  648. $bitmap = Wx::Bitmap->new($Slic3r::var->("printer_empty.png"), wxBITMAP_TYPE_PNG);
  649. }
  650. $choice->AppendString($preset->dropdown_name, $bitmap);
  651. push @{$self->{preset_choosers_names}{$choice}}, $preset->name;
  652. }
  653. my $selected = shift @sel;
  654. if (defined $selected && $selected <= $#presets) {
  655. # call SetSelection() only after SetString() otherwise the new string
  656. # won't be picked up as the visible string
  657. $choice->SetSelection($selected);
  658. my $preset_name = $self->{preset_choosers_names}{$choice}[$selected];
  659. push @preset_names, $preset_name;
  660. # TODO: populate other filament preset placeholders
  661. $selected_printer_name = $preset_name if $group eq 'printer';
  662. }
  663. }
  664. $self->{print}->placeholder_parser->set_multiple("${group}_preset", [ @preset_names ]);
  665. }
  666. }
  667. sub select_preset_by_name {
  668. my ($self, $name, $group, $n) = @_;
  669. # $n is optional
  670. my $presets = wxTheApp->presets->{$group};
  671. my $choosers = $self->{preset_choosers}{$group};
  672. my $names = $self->{preset_choosers_names}{$choosers->[0]};
  673. my $i = first { $names->[$_] eq $name } 0..$#$names;
  674. return if !defined $i;
  675. if (defined $n && $n <= $#$choosers) {
  676. $choosers->[$n]->SetSelection($i);
  677. } else {
  678. $_->SetSelection($i) for @$choosers;
  679. }
  680. $self->_on_select_preset($group);
  681. }
  682. sub selected_presets {
  683. my ($self, $group) = @_;
  684. my %presets = ();
  685. foreach my $group (qw(printer filament print)) {
  686. $presets{$group} = [];
  687. foreach my $choice (@{$self->{preset_choosers}{$group}}) {
  688. my $sel = $choice->GetSelection;
  689. $sel = 0 if $sel == -1;
  690. push @{ $presets{$group} },
  691. grep { $_->name eq $self->{preset_choosers_names}{$choice}[$sel] }
  692. @{wxTheApp->presets->{$group}};
  693. }
  694. }
  695. return $group ? @{$presets{$group}} : %presets;
  696. }
  697. sub show_preset_editor {
  698. my ($self, $group, $i) = @_;
  699. wxTheApp->CallAfter(sub {
  700. my @presets = $self->selected_presets($group);
  701. my $preset_editor;
  702. my $dlg;
  703. my $mainframe = $self->GetFrame;
  704. my $tabpanel = $mainframe->{tabpanel};
  705. if (exists $mainframe->{preset_editor_tabs}{$group}) {
  706. # we already have an open editor
  707. $tabpanel->SetSelection($tabpanel->GetPageIndex($mainframe->{preset_editor_tabs}{$group}));
  708. return;
  709. } elsif ($Slic3r::GUI::Settings->{_}{tabbed_preset_editors}) {
  710. my $class = "Slic3r::GUI::PresetEditor::" . ucfirst($group);
  711. $mainframe->{preset_editor_tabs}{$group} = $preset_editor = $class->new($self->GetFrame);
  712. $tabpanel->AddPage($preset_editor, ucfirst($group) . " Settings", 1);
  713. } else {
  714. my $class = "Slic3r::GUI::PresetEditorDialog::" . ucfirst($group);
  715. $dlg = $class->new($self);
  716. $preset_editor = $dlg->preset_editor;
  717. }
  718. $preset_editor->select_preset_by_name($presets[$i // 0]->name);
  719. $preset_editor->on_value_change(sub {
  720. # Re-load the presets in order to toggle the (modified) suffix
  721. $self->load_presets;
  722. # Update shortcuts
  723. $self->_on_select_preset($group);
  724. # Use the new config wherever we actually use its contents
  725. $self->config_changed;
  726. });
  727. my $cb = sub {
  728. my ($group, $preset) = @_;
  729. # Re-load the presets as they might have changed.
  730. $self->load_presets;
  731. # Select the preset in plater too
  732. $self->select_preset_by_name($preset->name, $group, $i, 1);
  733. };
  734. $preset_editor->on_select_preset($cb);
  735. $preset_editor->on_save_preset($cb);
  736. if ($dlg) {
  737. $dlg->Show;
  738. }
  739. });
  740. }
  741. # Returns the current config by merging the selected presets and the overrides.
  742. sub config {
  743. my ($self, $ignore_overrides) = @_;
  744. # use a DynamicConfig because FullPrintConfig is not enough
  745. my $config = Slic3r::Config->new_from_defaults;
  746. # get defaults also for the values tracked by the Plater's config
  747. # (for example 'shortcuts')
  748. $config->apply(Slic3r::Config->new_from_defaults(@{$self->{config}->get_keys}));
  749. my %classes = map { $_ => "Slic3r::GUI::PresetEditor::".ucfirst($_) }
  750. qw(print filament printer);
  751. my %presets = $self->selected_presets;
  752. $config->apply($_->dirty_config) for @{ $presets{printer} };
  753. if (@{ $presets{filament} }) {
  754. my $filament_config = $presets{filament}[0]->dirty_config;
  755. for my $i (1..$#{ $presets{filament} }) {
  756. my $preset = $presets{filament}[$i];
  757. my $config = $preset->dirty_config;
  758. foreach my $opt_key (@{$config->get_keys}) {
  759. if ($filament_config->has($opt_key)) {
  760. my $value = $filament_config->get($opt_key);
  761. next unless ref $value eq 'ARRAY';
  762. $value->[$i] = $config->get($opt_key)->[0];
  763. $filament_config->set($opt_key, $value);
  764. }
  765. }
  766. }
  767. $config->apply($filament_config);
  768. }
  769. $config->apply($_->dirty_config) for @{ $presets{print} };
  770. $config->apply($self->{settings_override_config})
  771. unless $ignore_overrides;
  772. return $config;
  773. }
  774. sub get_object_index {
  775. my $self = shift;
  776. my ($object_indentifier) = @_;
  777. return undef if !defined $object_indentifier;
  778. for (my $i = 0; $i <= $#{$self->{objects}}; $i++){
  779. if ($self->{objects}->[$i]->identifier eq $object_indentifier) {
  780. return $i;
  781. }
  782. }
  783. return undef;
  784. }
  785. sub add_undo_operation {
  786. my $self = shift;
  787. my @parameters = @_;
  788. my $type = $parameters[0];
  789. my $object_identifier = $parameters[1];
  790. my @attributes = @parameters[2..$#parameters]; # operation values.
  791. my $new_undo_operation = new Slic3r::GUI::Plater::UndoOperation($type, $object_identifier, \@attributes);
  792. push @{$self->{undo_stack}}, $new_undo_operation;
  793. $self->{redo_stack} = [];
  794. $self->limit_undo_operations(8); # Current limit of undo/redo operations.
  795. $self->GetFrame->on_undo_redo_stacks_changed;
  796. return $new_undo_operation;
  797. }
  798. sub limit_undo_operations {
  799. my ($self, $limit)= @_;
  800. return if !defined $limit;
  801. # Delete undo operations succeeded by 4 operations or more to save memory.
  802. while ($#{$self->{undo_stack}} + 1 > $limit) {
  803. print "Removing an old operation.\n";
  804. splice @{$self->{undo_stack}}, 0, 1;
  805. }
  806. }
  807. sub undo {
  808. my $self = shift;
  809. my $operation = pop @{$self->{undo_stack}};
  810. return if !defined $operation;
  811. push @{$self->{redo_stack}}, $operation;
  812. my $type = $operation->{type};
  813. if ($type eq "ROTATE") {
  814. my $object_id = $operation->{object_identifier};
  815. my $obj_idx = $self->get_object_index($object_id);
  816. $self->select_object($obj_idx);
  817. my $angle = $operation->{attributes}->[0];
  818. my $axis = $operation->{attributes}->[1];
  819. $self->rotate(-1 * $angle, $axis, 'true'); # Apply inverse transformation.
  820. } elsif ($type eq "INCREASE") {
  821. my $object_id = $operation->{object_identifier};
  822. my $obj_idx = $self->get_object_index($object_id);
  823. $self->select_object($obj_idx);
  824. my $copies = $operation->{attributes}->[0];
  825. $self->decrease($copies, 'true');
  826. } elsif ($type eq "DECREASE") {
  827. my $object_id = $operation->{object_identifier};
  828. my $obj_idx = $self->get_object_index($object_id);
  829. $self->select_object($obj_idx);
  830. my $copies = $operation->{attributes}->[0];
  831. $self->increase($copies, 'true');
  832. } elsif ($type eq "MIRROR") {
  833. my $object_id = $operation->{object_identifier};
  834. my $obj_idx = $self->get_object_index($object_id);
  835. $self->select_object($obj_idx);
  836. my $axis = $operation->{attributes}->[0];
  837. $self->mirror($axis, 'true');
  838. } elsif ($type eq "REMOVE") {
  839. my $_model = $operation->{attributes}->[0];
  840. $self->load_model_objects(@{$_model->objects});
  841. $self->{object_identifier}--; # Decrement the identifier as we will change the object identifier with the saved one.
  842. $self->{objects}->[-1]->identifier($operation->{object_identifier});
  843. } elsif ($type eq "CUT" || $type eq "SPLIT") {
  844. # Delete the produced objects.
  845. my $obj_identifiers_start = $operation->{attributes}->[2];
  846. for (my $i_object = 0; $i_object < $#{$operation->{attributes}->[1]->objects} + 1; $i_object++) {
  847. $self->remove($self->get_object_index($obj_identifiers_start++), 'true');
  848. }
  849. # Add the original object.
  850. $self->load_model_objects(@{$operation->{attributes}->[0]->objects});
  851. $self->{object_identifier}--;
  852. $self->{objects}->[-1]->identifier($operation->{object_identifier}); # Add the original assigned identifier.
  853. } elsif ($type eq "CHANGE_SCALE") {
  854. my $object_id = $operation->{object_identifier};
  855. my $obj_idx = $self->get_object_index($object_id);
  856. $self->select_object($obj_idx);
  857. my $axis = $operation->{attributes}->[0];
  858. my $tosize = $operation->{attributes}->[1];
  859. my $saved_scale = $operation->{attributes}->[3];
  860. $self->changescale($axis, $tosize, $saved_scale, 'true');
  861. } elsif ($type eq "RESET") {
  862. # Revert changes to the plater object identifier. It's modified when adding new objects only not when undo/redo is executed.
  863. my $current_objects_identifier = $self->{object_identifier};
  864. my $_model = $operation->{attributes}->[0];
  865. $self->load_model_objects(@{$_model->objects});
  866. $self->{object_identifier} = $current_objects_identifier;
  867. # don't forget the identifiers.
  868. my $objects_count = $#{$operation->{attributes}->[0]->objects} + 1;
  869. foreach my $identifier (@{$operation->{attributes}->[1]})
  870. {
  871. $self->{objects}->[-$objects_count]->identifier($identifier);
  872. $objects_count--;
  873. }
  874. } elsif ($type eq "ADD") {
  875. my $objects_count = $#{$operation->{attributes}->[0]->objects} + 1;
  876. my $identifier_start = $operation->{attributes}->[1];
  877. for (my $identifier = $identifier_start; $identifier < $objects_count + $identifier_start; $identifier++) {
  878. my $obj_idx = $self->get_object_index($identifier);
  879. $self->remove($obj_idx, 'true');
  880. }
  881. } elsif ($type eq "GROUP"){
  882. my @ops = @{$operation->{attributes}};
  883. push @{$self->{undo_stack}}, @ops;
  884. foreach my $op (@ops) {
  885. $self->undo;
  886. pop @{$self->{redo_stack}};
  887. }
  888. }
  889. }
  890. sub redo {
  891. my $self = shift;
  892. my $operation = pop @{$self->{redo_stack}};
  893. return if !defined $operation;
  894. push @{$self->{undo_stack}}, $operation;
  895. my $type = $operation->{type};
  896. if ($type eq "ROTATE") {
  897. my $object_id = $operation->{object_identifier};
  898. my $obj_idx = $self->get_object_index($object_id);
  899. $self->select_object($obj_idx);
  900. my $angle = $operation->{attributes}->[0];
  901. my $axis = $operation->{attributes}->[1];
  902. $self->rotate($angle, $axis, 'true');
  903. } elsif ($type eq "INCREASE") {
  904. my $object_id = $operation->{object_identifier};
  905. my $obj_idx = $self->get_object_index($object_id);
  906. $self->select_object($obj_idx);
  907. my $copies = $operation->{attributes}->[0];
  908. $self->increase($copies, 'true');
  909. } elsif ($type eq "DECREASE") {
  910. my $object_id = $operation->{object_identifier};
  911. my $obj_idx = $self->get_object_index($object_id);
  912. $self->select_object($obj_idx);
  913. my $copies = $operation->{attributes}->[0];
  914. $self->decrease($copies, 'true');
  915. } elsif ($type eq "MIRROR") {
  916. my $object_id = $operation->{object_identifier};
  917. my $obj_idx = $self->get_object_index($object_id);
  918. $self->select_object($obj_idx);
  919. my $axis = $operation->{attributes}->[0];
  920. $self->mirror($axis, 'true');
  921. } elsif ($type eq "REMOVE") {
  922. my $object_id = $operation->{object_identifier};
  923. my $obj_idx = $self->get_object_index($object_id);
  924. $self->select_object($obj_idx);
  925. $self->remove(undef, 'true');
  926. } elsif ($type eq "CUT" || $type eq "SPLIT") {
  927. # Delete the org objects.
  928. $self->remove($self->get_object_index($operation->{object_identifier}), 'true');
  929. # Add the new objects and revert changes to the plater object identifier.
  930. my $current_objects_identifier = $self->{object_identifier};
  931. $self->load_model_objects(@{$operation->{attributes}->[1]->objects});
  932. $self->{object_identifier} = $current_objects_identifier;
  933. # Add their identifiers.
  934. my $obj_identifiers_start = $operation->{attributes}->[2];
  935. my $obj_count = $#{$operation->{attributes}->[1]->objects} + 1;
  936. for (my $i_object = 0; $i_object <= $#{$operation->{attributes}->[1]->objects}; $i_object++){
  937. $self->{objects}->[-$obj_count]->identifier($obj_identifiers_start++);
  938. $obj_count--;
  939. }
  940. } elsif ($type eq "CHANGE_SCALE") {
  941. my $object_id = $operation->{object_identifier};
  942. my $obj_idx = $self->get_object_index($object_id);
  943. $self->select_object($obj_idx);
  944. my $axis = $operation->{attributes}->[0];
  945. my $tosize = $operation->{attributes}->[1];
  946. my $old_scale = $operation->{attributes}->[2];
  947. $self->changescale($axis, $tosize, $old_scale, 'true');
  948. } elsif ($type eq "RESET") {
  949. $self->reset('true');
  950. } elsif ($type eq "ADD") {
  951. # Revert changes to the plater object identifier. It's modified when adding new objects only not when undo/redo is executed.
  952. my $current_objects_identifier = $self->{object_identifier};
  953. $self->load_model_objects(@{$operation->{attributes}->[0]->objects});
  954. $self->{object_identifier} = $current_objects_identifier;
  955. my $objects_count = $#{$operation->{attributes}->[0]->objects} + 1;
  956. my $start_identifier = $operation->{attributes}->[1];
  957. foreach my $object (@{$operation->{attributes}->[0]->objects})
  958. {
  959. $self->{objects}->[-$objects_count]->identifier($start_identifier++);
  960. $objects_count--;
  961. }
  962. } elsif ($type eq "GROUP"){
  963. my @ops = @{$operation->{attributes}};
  964. foreach my $op (@ops) {
  965. push @{$self->{redo_stack}}, $op;
  966. $self->redo;
  967. pop @{$self->{undo_stack}};
  968. }
  969. }
  970. }
  971. sub add {
  972. my $self = shift;
  973. # Save the current object identifier to track added objects.
  974. my $start_object_id = $self->{object_identifier};
  975. my @input_files = wxTheApp->open_model($self);
  976. $self->load_file($_) for @input_files;
  977. # Check if no objects are added.
  978. if ($start_object_id == $self->{object_identifier}) {
  979. return;
  980. }
  981. # Save the added objects.
  982. my $new_model = $self->{model}->new;
  983. # Get newly added objects count.
  984. my $new_objects_count = $self->{object_identifier} - $start_object_id;
  985. for (my $i_object = $start_object_id; $i_object < $new_objects_count + $start_object_id; $i_object++){
  986. my $object_index = $self->get_object_index($i_object);
  987. $new_model->add_object($self->{model}->get_object($object_index));
  988. }
  989. $self->add_undo_operation("ADD", undef, $new_model, $start_object_id);
  990. }
  991. sub add_tin {
  992. my $self = shift;
  993. my @input_files = wxTheApp->open_model($self);
  994. return if !@input_files;
  995. my $offset = Wx::GetNumberFromUser("", "Enter the minimum thickness in mm (i.e. the offset from the lowest point):", "2.5D TIN",
  996. 5, 0, 1000000, $self);
  997. return if $offset < 0;
  998. foreach my $input_file (@input_files) {
  999. my $model = eval { Slic3r::Model->read_from_file($input_file) };
  1000. Slic3r::GUI::show_error($self, $@) if $@;
  1001. next if !$model;
  1002. if ($model->looks_like_multipart_object) {
  1003. Slic3r::GUI::show_error($self, "Multi-part models cannot be opened as 2.5D TIN files. Please load a single continuous mesh.");
  1004. next;
  1005. }
  1006. my $model_object = $model->get_object(0);
  1007. eval {
  1008. $model_object->get_volume(0)->extrude_tin($offset);
  1009. };
  1010. Slic3r::GUI::show_error($self, $@) if $@;
  1011. $self->load_model_objects($model_object);
  1012. }
  1013. }
  1014. sub load_file {
  1015. my $self = shift;
  1016. my ($input_file, $obj_idx_to_load) = @_;
  1017. $Slic3r::GUI::Settings->{recent}{skein_directory} = dirname($input_file);
  1018. wxTheApp->save_settings;
  1019. my $process_dialog = Wx::ProgressDialog->new('Loading…', "Processing input file…", 100, $self, 0);
  1020. $process_dialog->Pulse;
  1021. local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self);
  1022. my $model = eval { Slic3r::Model->read_from_file($input_file) };
  1023. Slic3r::GUI::show_error($self, $@) if $@;
  1024. my @obj_idx = ();
  1025. if (defined $model) {
  1026. if ($model->looks_like_multipart_object) {
  1027. my $dialog = Wx::MessageDialog->new($self,
  1028. "This file contains several objects positioned at multiple heights. "
  1029. . "Instead of considering them as multiple objects, should I consider\n"
  1030. . "this file as a single object having multiple parts?\n",
  1031. 'Multi-part object detected', wxICON_WARNING | wxYES | wxNO);
  1032. if ($dialog->ShowModal() == wxID_YES) {
  1033. $model->convert_multipart_object;
  1034. }
  1035. }
  1036. for my $obj_idx (0..($model->objects_count-1)) {
  1037. my $object = $model->objects->[$obj_idx];
  1038. $object->set_input_file($input_file);
  1039. for my $vol_idx (0..($object->volumes_count-1)) {
  1040. my $volume = $object->get_volume($vol_idx);
  1041. $volume->set_input_file($input_file);
  1042. $volume->set_input_file_obj_idx($obj_idx);
  1043. $volume->set_input_file_obj_idx($vol_idx);
  1044. }
  1045. }
  1046. my $i = 0;
  1047. if (defined $obj_idx_to_load) {
  1048. return () if $obj_idx_to_load >= $model->objects_count;
  1049. @obj_idx = $self->load_model_objects($model->get_object($obj_idx_to_load));
  1050. $i = $obj_idx_to_load;
  1051. } else {
  1052. @obj_idx = $self->load_model_objects(@{$model->objects});
  1053. }
  1054. foreach my $obj_idx (@obj_idx) {
  1055. $self->{objects}[$obj_idx]->input_file($input_file);
  1056. $self->{objects}[$obj_idx]->input_file_obj_idx($i++);
  1057. }
  1058. $self->statusbar->SetStatusText("Loaded " . basename($input_file));
  1059. if($self->{scaled_down}) {
  1060. $self->statusbar->SetStatusText('Your object appears to be too large, so it was automatically scaled down to fit your print bed.');
  1061. }
  1062. if($self->{outside_bounds}) {
  1063. $self->statusbar->SetStatusText('Some of your object(s) appear to be outside the print bed. Use the arrange button to correct this.');
  1064. }
  1065. }
  1066. $process_dialog->Destroy;
  1067. # Empty the redo stack
  1068. $self->{redo_stack} = [];
  1069. return @obj_idx;
  1070. }
  1071. sub load_model_objects {
  1072. my ($self, @model_objects) = @_;
  1073. # Always restart background process when adding new objects.
  1074. # This prevents lack of processing in some circumstances when background process is
  1075. # running but adding a new object does not invalidate anything.
  1076. $self->stop_background_process;
  1077. my $bed_centerf = $self->bed_centerf;
  1078. my $bed_shape = Slic3r::Polygon->new_scale(@{$self->{config}->bed_shape});
  1079. my $bed_size = $bed_shape->bounding_box->size;
  1080. my $need_arrange = 0;
  1081. my @obj_idx = ();
  1082. foreach my $model_object (@model_objects) {
  1083. my $o = $self->{model}->add_object($model_object);
  1084. $o->repair;
  1085. push @{ $self->{objects} }, Slic3r::GUI::Plater::Object->new(
  1086. name => $model_object->name || basename($model_object->input_file), identifier =>
  1087. $self->{object_identifier}++
  1088. );
  1089. push @obj_idx, $#{ $self->{objects} };
  1090. if ($model_object->instances_count == 0) {
  1091. if ($Slic3r::GUI::Settings->{_}{autocenter}) {
  1092. # if object has no defined position(s) we need to rearrange everything after loading
  1093. $need_arrange = 1;
  1094. # add a default instance and center object around origin
  1095. $o->center_around_origin; # also aligns object to Z = 0
  1096. $o->add_instance(offset => $bed_centerf);
  1097. } else {
  1098. # if user turned autocentering off, automatic arranging would disappoint them
  1099. $need_arrange = 0;
  1100. if ($Slic3r::GUI::Settings->{_}{autoalignz}) {
  1101. $o->align_to_ground; # aligns object to Z = 0
  1102. }
  1103. $o->add_instance();
  1104. }
  1105. } else {
  1106. if ($Slic3r::GUI::Settings->{_}{autoalignz}) {
  1107. # if object has defined positions we still need to ensure it's aligned to Z = 0
  1108. $o->align_to_ground;
  1109. }
  1110. }
  1111. {
  1112. # if the object is too large (more than 5 times the bed), scale it down
  1113. my $size = $o->bounding_box->size;
  1114. my $ratio = max(@$size[X,Y]) / unscale(max(@$bed_size[X,Y]));
  1115. if ($ratio > 5) {
  1116. $_->set_scaling_factor(1/$ratio) for @{$o->instances};
  1117. $self->{scaled_down} = 1;
  1118. }
  1119. }
  1120. {
  1121. # if after scaling the object does not fit on the bed provide a warning
  1122. my $bed_bounds = Slic3r::Geometry::BoundingBoxf->new_from_points($self->{config}->bed_shape);
  1123. my $o_bounds = $o->bounding_box;
  1124. my $min = Slic3r::Pointf->new($o_bounds->x_min, $o_bounds->y_min);
  1125. my $max = Slic3r::Pointf->new($o_bounds->x_max, $o_bounds->y_max);
  1126. if (!$bed_bounds->contains_point($min) || !$bed_bounds->contains_point($max))
  1127. {
  1128. $self->{outside_bounds} = 1;
  1129. }
  1130. }
  1131. $self->{print}->auto_assign_extruders($o);
  1132. $self->{print}->add_model_object($o);
  1133. }
  1134. $self->make_thumbnail($_) for @obj_idx;
  1135. $self->arrange if $need_arrange;
  1136. $self->on_model_change;
  1137. # zoom to objects
  1138. $self->{canvas3D}->zoom_to_volumes
  1139. if $self->{canvas3D};
  1140. $self->object_list_changed;
  1141. return @obj_idx;
  1142. }
  1143. sub bed_centerf {
  1144. my ($self) = @_;
  1145. my $bed_shape = Slic3r::Polygon->new_scale(@{$self->{config}->bed_shape});
  1146. my $bed_center = $bed_shape->bounding_box->center;
  1147. return Slic3r::Pointf->new(unscale($bed_center->x), unscale($bed_center->y)); #)
  1148. }
  1149. sub remove {
  1150. my $self = shift;
  1151. my ($obj_idx, $dont_push) = @_;
  1152. $self->stop_background_process;
  1153. # Prevent toolpaths preview from rendering while we modify the Print object
  1154. $self->{toolpaths2D}->enabled(0) if $self->{toolpaths2D};
  1155. $self->{preview3D}->enabled(0) if $self->{preview3D};
  1156. # if no object index is supplied, remove the selected one
  1157. if (!defined $obj_idx) {
  1158. ($obj_idx, undef) = $self->selected_object;
  1159. }
  1160. # Save the object identifier and copy the object for undo/redo operations.
  1161. my $object_id = $self->{objects}->[$obj_idx]->identifier;
  1162. my $new_model = Slic3r::Model->new; # store this before calling get_object()
  1163. $new_model->add_object($self->{model}->get_object($obj_idx));
  1164. splice @{$self->{objects}}, $obj_idx, 1;
  1165. $self->{model}->delete_object($obj_idx);
  1166. $self->{print}->delete_object($obj_idx);
  1167. $self->object_list_changed;
  1168. $self->select_object(undef);
  1169. $self->on_model_change;
  1170. if (!defined $dont_push) {
  1171. $self->add_undo_operation("REMOVE", $object_id, $new_model);
  1172. }
  1173. }
  1174. sub reset {
  1175. my ($self, $dont_push) = @_;
  1176. $self->stop_background_process;
  1177. # Prevent toolpaths preview from rendering while we modify the Print object
  1178. $self->{toolpaths2D}->enabled(0) if $self->{toolpaths2D};
  1179. $self->{preview3D}->enabled(0) if $self->{preview3D};
  1180. # Save the current model.
  1181. my $current_model = $self->{model}->clone;
  1182. if (!defined $dont_push) {
  1183. # Get the identifiers of the curent model objects.
  1184. my $objects_identifiers = [];
  1185. for (my $i = 0; $i <= $#{$self->{objects}}; $i++){
  1186. push @{$objects_identifiers}, $self->{objects}->[$i]->identifier;
  1187. }
  1188. $self->add_undo_operation("RESET", undef, $current_model, $objects_identifiers);
  1189. }
  1190. @{$self->{objects}} = ();
  1191. $self->{model}->clear_objects;
  1192. $self->{print}->clear_objects;
  1193. $self->object_list_changed;
  1194. $self->select_object(undef);
  1195. $self->on_model_change;
  1196. }
  1197. sub increase {
  1198. my ($self, $copies, $dont_push) = @_;
  1199. $copies //= 1;
  1200. my ($obj_idx, $object) = $self->selected_object;
  1201. my $model_object = $self->{model}->objects->[$obj_idx];
  1202. my $instance = $model_object->instances->[-1];
  1203. for my $i (1..$copies) {
  1204. $instance = $model_object->add_instance(
  1205. offset => Slic3r::Pointf->new(map 10+$_, @{$instance->offset}),
  1206. z_translation => $instance->z_translation,
  1207. scaling_factor => $instance->scaling_factor,
  1208. scaling_vector => $instance->scaling_vector,
  1209. rotation => $instance->rotation,
  1210. x_rotation => $instance->x_rotation,
  1211. y_rotation => $instance->y_rotation,
  1212. );
  1213. $self->{print}->objects->[$obj_idx]->add_copy($instance->offset);
  1214. }
  1215. if (!defined $dont_push) {
  1216. $self->add_undo_operation("INCREASE", $object->identifier , $copies);
  1217. }
  1218. # only autoarrange if user has autocentering enabled
  1219. $self->stop_background_process;
  1220. if ($Slic3r::GUI::Settings->{_}{autocenter}) {
  1221. $self->arrange;
  1222. } else {
  1223. $self->on_model_change;
  1224. }
  1225. }
  1226. sub decrease {
  1227. my ($self, $copies, $dont_push) = @_;
  1228. $copies //= 1;
  1229. $self->stop_background_process;
  1230. my ($obj_idx, $object) = $self->selected_object;
  1231. my $model_object = $self->{model}->objects->[$obj_idx];
  1232. if ($model_object->instances_count > $copies) {
  1233. for my $i (1..$copies) {
  1234. $model_object->delete_last_instance;
  1235. $self->{print}->objects->[$obj_idx]->delete_last_copy;
  1236. }
  1237. if (!defined $dont_push) {
  1238. $self->add_undo_operation("DECREASE", $object->identifier, $copies);
  1239. }
  1240. } else {
  1241. $self->remove;
  1242. }
  1243. $self->on_model_change;
  1244. }
  1245. sub set_number_of_copies {
  1246. my ($self) = @_;
  1247. $self->pause_background_process;
  1248. # get current number of copies
  1249. my ($obj_idx, $object) = $self->selected_object;
  1250. my $model_object = $self->{model}->objects->[$obj_idx];
  1251. # prompt user
  1252. my $copies = Wx::GetNumberFromUser("", "Enter the number of copies of the selected object:", "Copies", $model_object->instances_count, 0, 1000, $self);
  1253. return if $copies == -1;
  1254. my $diff = $copies - $model_object->instances_count;
  1255. if ($diff == 0) {
  1256. # no variation
  1257. $self->resume_background_process;
  1258. } elsif ($diff > 0) {
  1259. $self->increase($diff);
  1260. } elsif ($diff < 0) {
  1261. $self->decrease(-$diff);
  1262. }
  1263. }
  1264. sub center_selected_object_on_bed {
  1265. my ($self) = @_;
  1266. my ($obj_idx, $object) = $self->selected_object;
  1267. return if !defined $obj_idx;
  1268. my $model_object = $self->{model}->objects->[$obj_idx];
  1269. my $bb = $model_object->bounding_box;
  1270. my $size = $bb->size;
  1271. my $vector = Slic3r::Pointf->new(
  1272. $self->bed_centerf->x - $bb->x_min - $size->x/2,
  1273. $self->bed_centerf->y - $bb->y_min - $size->y/2, #//
  1274. );
  1275. $_->offset->translate(@$vector) for @{$model_object->instances};
  1276. $self->on_model_change;
  1277. }
  1278. sub rotate_face {
  1279. my $self = shift;
  1280. my ($obj_idx, $object) = $self->selected_object;
  1281. return if !defined $obj_idx;
  1282. # Get the selected normal
  1283. if (!$Slic3r::GUI::have_OpenGL) {
  1284. Slic3r::GUI::show_error($self, "Please install the OpenGL modules to use this feature (see build instructions).");
  1285. return;
  1286. }
  1287. my $dlg = Slic3r::GUI::Plater::ObjectRotateFaceDialog->new($self,
  1288. object => $self->{objects}[$obj_idx],
  1289. model_object => $self->{model}->objects->[$obj_idx],
  1290. );
  1291. return unless $dlg->ShowModal == wxID_OK;
  1292. my $normal = $dlg->SelectedNormal;
  1293. return if !defined $normal;
  1294. my $axis = $dlg->SelectedAxis;
  1295. return if !defined $axis;
  1296. # Actual math to rotate
  1297. my $angleToXZ = atan2($normal->y(),$normal->x());
  1298. my $angleToZ = acos(-$normal->z());
  1299. $self->rotate(-rad2deg($angleToXZ),Z);
  1300. $self->rotate(rad2deg($angleToZ),Y);
  1301. if($axis == Z){
  1302. $self->add_undo_operation("GROUP", $object->identifier, splice(@{$self->{undo_stack}},-2));
  1303. } else {
  1304. if($axis == X){
  1305. $self->rotate(90,Y);
  1306. } else {
  1307. $self->rotate(90,X);
  1308. }
  1309. $self->add_undo_operation("GROUP", $object->identifier, splice(@{$self->{undo_stack}},-3));
  1310. }
  1311. }
  1312. sub rotate {
  1313. my $self = shift;
  1314. my ($angle, $axis, $dont_push) = @_;
  1315. # angle is in degrees
  1316. $axis //= Z;
  1317. my ($obj_idx, $object) = $self->selected_object;
  1318. return if !defined $obj_idx;
  1319. my $model_object = $self->{model}->objects->[$obj_idx];
  1320. my $model_instance = $model_object->instances->[0];
  1321. # we need thumbnail to be computed before allowing rotation
  1322. return if !$object->thumbnail;
  1323. if (!defined $angle) {
  1324. my $axis_name = $axis == X ? 'X' : $axis == Y ? 'Y' : 'Z';
  1325. my $default = $axis == Z ? rad2deg($model_instance->rotation) : 0;
  1326. # Wx::GetNumberFromUser() does not support decimal numbers
  1327. $angle = Wx::GetTextFromUser("Enter the rotation angle:", "Rotate around $axis_name axis",
  1328. $default, $self);
  1329. return if !$angle || $angle !~ /^-?\d*(?:\.\d*)?$/ || $angle == -1;
  1330. }
  1331. $self->stop_background_process;
  1332. if ($axis == Z) {
  1333. my $new_angle = deg2rad($angle);
  1334. $_->set_rotation($_->rotation + $new_angle) for @{ $model_object->instances };
  1335. $object->transform_thumbnail($self->{model}, $obj_idx);
  1336. } else {
  1337. # rotation around X and Y needs to be performed on mesh
  1338. # so we first apply any Z rotation
  1339. $model_object->transform_by_instance($model_instance, 1);
  1340. $model_object->rotate(deg2rad($angle), $axis);
  1341. # realign object to Z = 0
  1342. $model_object->center_around_origin;
  1343. $self->make_thumbnail($obj_idx);
  1344. }
  1345. $model_object->update_bounding_box;
  1346. # update print and start background processing
  1347. $self->{print}->add_model_object($model_object, $obj_idx);
  1348. if (!defined $dont_push) {
  1349. $self->add_undo_operation("ROTATE", $object->identifier, $angle, $axis);
  1350. }
  1351. $self->selection_changed; # refresh info (size etc.)
  1352. $self->on_model_change;
  1353. }
  1354. sub mirror {
  1355. my ($self, $axis, $dont_push) = @_;
  1356. my ($obj_idx, $object) = $self->selected_object;
  1357. return if !defined $obj_idx;
  1358. my $model_object = $self->{model}->objects->[$obj_idx];
  1359. my $model_instance = $model_object->instances->[0];
  1360. # apply Z rotation before mirroring
  1361. $model_object->transform_by_instance($model_instance, 1);
  1362. $model_object->mirror($axis);
  1363. $model_object->update_bounding_box;
  1364. # realign object to Z = 0
  1365. $model_object->center_around_origin;
  1366. $self->make_thumbnail($obj_idx);
  1367. # update print and start background processing
  1368. $self->stop_background_process;
  1369. $self->{print}->add_model_object($model_object, $obj_idx);
  1370. if (!defined $dont_push) {
  1371. $self->add_undo_operation("MIRROR", $object->identifier, $axis);
  1372. }
  1373. $self->selection_changed; # refresh info (size etc.)
  1374. $self->on_model_change;
  1375. }
  1376. sub changescale {
  1377. my ($self, $axis, $tosize, $saved_scale, $dont_push) = @_;
  1378. my ($obj_idx, $object) = $self->selected_object;
  1379. return if !defined $obj_idx;
  1380. my $model_object = $self->{model}->objects->[$obj_idx];
  1381. my $model_instance = $model_object->instances->[0];
  1382. # we need thumbnail to be computed before allowing scaling
  1383. return if !$object->thumbnail;
  1384. my $object_size = $model_object->bounding_box->size;
  1385. my $bed_size = Slic3r::Polygon->new_scale(@{$self->{config}->bed_shape})->bounding_box->size;
  1386. my $old_scale;
  1387. my $scale;
  1388. if (defined $axis) {
  1389. my $axis_name = $axis == X ? 'X' : $axis == Y ? 'Y' : 'Z';
  1390. if (!defined $saved_scale) {
  1391. if ($tosize) {
  1392. my $cursize = $object_size->[$axis];
  1393. # Wx::GetNumberFromUser() does not support decimal numbers
  1394. my $newsize = Wx::GetTextFromUser(
  1395. sprintf("Enter the new size for the selected object (print bed: %smm):", $bed_size->[$axis]),
  1396. "Scale along $axis_name",
  1397. $cursize, $self);
  1398. return if !$newsize || $newsize !~ /^\d*(?:\.\d*)?$/ || $newsize < 0;
  1399. $scale = $newsize / $cursize * 100;
  1400. $old_scale = $cursize / $newsize * 100;
  1401. } else {
  1402. # Wx::GetNumberFromUser() does not support decimal numbers
  1403. $scale = Wx::GetTextFromUser("Enter the scale % for the selected object:",
  1404. "Scale along $axis_name", 100, $self);
  1405. $scale =~ s/%$//;
  1406. return if !$scale || $scale !~ /^\d*(?:\.\d*)?$/ || $scale < 0;
  1407. $old_scale = 100 * 100 / $scale;
  1408. }
  1409. }
  1410. else {
  1411. $scale = $saved_scale;
  1412. }
  1413. # apply Z rotation before scaling
  1414. $model_object->transform_by_instance($model_instance, 1);
  1415. my $versor = [1,1,1];
  1416. $versor->[$axis] = $scale/100;
  1417. $model_object->scale_xyz(Slic3r::Pointf3->new(@$versor));
  1418. # object was already aligned to Z = 0, so no need to realign it
  1419. $self->make_thumbnail($obj_idx);
  1420. } else {
  1421. if (!defined $saved_scale) {
  1422. if ($tosize) {
  1423. my $cursize = max(@$object_size);
  1424. # Wx::GetNumberFromUser() does not support decimal numbers
  1425. my $newsize = Wx::GetTextFromUser("Enter the new max size for the selected object:",
  1426. "Scale", $cursize, $self);
  1427. return if !$newsize || $newsize !~ /^\d*(?:\.\d*)?$/ || $newsize < 0;
  1428. $scale = $model_instance->scaling_factor * $newsize / $cursize * 100;
  1429. $old_scale = $model_instance->scaling_factor * 100;
  1430. } else {
  1431. # max scale factor should be above 2540 to allow importing files exported in inches
  1432. # Wx::GetNumberFromUser() does not support decimal numbers
  1433. $scale = Wx::GetTextFromUser("Enter the scale % for the selected object:", 'Scale',
  1434. $model_instance->scaling_factor * 100, $self);
  1435. return if !$scale || $scale !~ /^\d*(?:\.\d*)?$/ || $scale < 0;
  1436. $old_scale = $model_instance->scaling_factor * 100;
  1437. }
  1438. return if !$scale || $scale < 0;
  1439. } else {
  1440. $scale = $saved_scale;
  1441. }
  1442. $scale /= 100; # turn percent into factor
  1443. my $variation = $scale / $model_instance->scaling_factor;
  1444. foreach my $range (@{ $model_object->layer_height_ranges }) {
  1445. $range->[0] *= $variation;
  1446. $range->[1] *= $variation;
  1447. }
  1448. $_->set_scaling_factor($scale) for @{ $model_object->instances };
  1449. $object->transform_thumbnail($self->{model}, $obj_idx);
  1450. $scale *= 100;
  1451. }
  1452. # Add the new undo operation.
  1453. if (!defined $dont_push) {
  1454. $self->add_undo_operation("CHANGE_SCALE", $object->identifier, $axis, $tosize, $scale, $old_scale);
  1455. }
  1456. $model_object->update_bounding_box;
  1457. # update print and start background processing
  1458. $self->stop_background_process;
  1459. $self->{print}->add_model_object($model_object, $obj_idx);
  1460. $self->selection_changed(1); # refresh info (size, volume etc.)
  1461. $self->on_model_change;
  1462. }
  1463. sub arrange {
  1464. my $self = shift;
  1465. $self->pause_background_process;
  1466. my $bb = Slic3r::Geometry::BoundingBoxf->new_from_points($self->{config}->bed_shape);
  1467. my $success = $self->{model}->arrange_objects($self->config->min_object_distance, $bb);
  1468. # ignore arrange failures on purpose: user has visual feedback and we don't need to warn him
  1469. # when parts don't fit in print bed
  1470. $self->statusbar->SetStatusText('Objects were arranged.');
  1471. $self->on_model_change(1);
  1472. }
  1473. sub split_object {
  1474. my ($self, $dont_push) = @_;
  1475. my ($obj_idx, $current_object) = $self->selected_object;
  1476. # we clone model object because split_object() adds the split volumes
  1477. # into the same model object, thus causing duplicates when we call load_model_objects()
  1478. my $new_model = $self->{model}->clone; # store this before calling get_object()
  1479. my $current_model_object = $new_model->get_object($obj_idx);
  1480. if ($current_model_object->volumes_count > 1) {
  1481. Slic3r::GUI::warning_catcher($self)->("The selected object can't be split because it contains more than one volume/material.");
  1482. return;
  1483. }
  1484. $self->pause_background_process;
  1485. # Save the curent model object for undo/redo operataions.
  1486. my $org_object_model = Slic3r::Model->new;
  1487. $org_object_model->add_object($current_model_object);
  1488. # Save the org object identifier.
  1489. my $object_id = $self->{objects}->[$obj_idx]->identifier;
  1490. my @model_objects = @{$current_model_object->split_object};
  1491. if (@model_objects == 1) {
  1492. $self->resume_background_process;
  1493. Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains only one part.");
  1494. $self->resume_background_process;
  1495. return;
  1496. }
  1497. foreach my $object (@model_objects) {
  1498. $object->instances->[$_]->offset->translate($_ * 10, $_ * 10)
  1499. for 1..$#{ $object->instances };
  1500. # we need to center this single object around origin
  1501. $object->center_around_origin;
  1502. }
  1503. # remove the original object before spawning the object_loaded event, otherwise
  1504. # we'll pass the wrong $obj_idx to it (which won't be recognized after the
  1505. # thumbnail thread returns)
  1506. $self->remove($obj_idx, 'true'); # Don't push to the undo stack it's considered a split opeation not a remove one.
  1507. $current_object = $obj_idx = undef;
  1508. # Save the object identifiers used in undo/redo operations.
  1509. my $new_objects_id_start = $self->{object_identifier};
  1510. print "The new object identifier start for split is " .$new_objects_id_start . "\n";
  1511. # load all model objects at once, otherwise the plate would be rearranged after each one
  1512. # causing original positions not to be kept
  1513. $self->load_model_objects(@model_objects);
  1514. # Create two models to save the current object and the resulted objects.
  1515. my $new_objects_model = Slic3r::Model->new;
  1516. foreach my $new_object (@model_objects) {
  1517. $new_objects_model->add_object($new_object);
  1518. }
  1519. $self->add_undo_operation("SPLIT", $object_id, $org_object_model, $new_objects_model, $new_objects_id_start);
  1520. }
  1521. sub toggle_print_stats {
  1522. my ($self, $show) = @_;
  1523. return if !$self->GetFrame->is_loaded;
  1524. if ($show) {
  1525. $self->{right_sizer}->Show($self->{sliced_info_box});
  1526. } else {
  1527. $self->{right_sizer}->Hide($self->{sliced_info_box});
  1528. }
  1529. $self->{right_sizer}->Layout;
  1530. }
  1531. sub config_changed {
  1532. my $self = shift;
  1533. my $config = $self->config;
  1534. if ($Slic3r::GUI::autosave) {
  1535. $config->save($Slic3r::GUI::autosave);
  1536. }
  1537. # Apply changes to the plater-specific config options.
  1538. foreach my $opt_key (@{$self->{config}->diff($config)}) {
  1539. # Ignore overrides. No need to set them in our config; we'll use them directly below.
  1540. next if $opt_key eq 'overrides';
  1541. $self->{config}->set($opt_key, $config->get($opt_key));
  1542. if ($opt_key eq 'bed_shape') {
  1543. $self->{canvas}->update_bed_size;
  1544. $self->{canvas3D}->update_bed_size if $self->{canvas3D};
  1545. $self->{preview3D}->set_bed_shape($self->{config}->bed_shape)
  1546. if $self->{preview3D};
  1547. $self->on_model_change;
  1548. } elsif ($opt_key eq 'serial_port') {
  1549. if ($config->get('serial_port')) {
  1550. $self->{btn_print}->Show;
  1551. } else {
  1552. $self->{btn_print}->Hide;
  1553. }
  1554. $self->Layout;
  1555. } elsif ($opt_key eq 'print_host') {
  1556. if ($config->get('print_host')) {
  1557. $self->{btn_send_gcode}->Show;
  1558. } else {
  1559. $self->{btn_send_gcode}->Hide;
  1560. }
  1561. $self->Layout;
  1562. }
  1563. }
  1564. return if !$self->GetFrame->is_loaded;
  1565. $self->toggle_print_stats(0);
  1566. if ($Slic3r::GUI::Settings->{_}{background_processing}) {
  1567. # (re)start timer
  1568. $self->schedule_background_process;
  1569. } else {
  1570. $self->async_apply_config;
  1571. }
  1572. }
  1573. sub schedule_background_process {
  1574. my ($self) = @_;
  1575. warn 'schedule_background_process() is not supposed to be called when background processing is disabled'
  1576. if !$Slic3r::GUI::Settings->{_}{background_processing};
  1577. $self->{processed} = 0;
  1578. if (defined $self->{apply_config_timer}) {
  1579. $self->{apply_config_timer}->Start(PROCESS_DELAY, 1); # 1 = one shot
  1580. }
  1581. }
  1582. # Executed asynchronously by a timer every PROCESS_DELAY (0.5 second).
  1583. # The timer is started by schedule_background_process(),
  1584. sub async_apply_config {
  1585. my ($self) = @_;
  1586. # pause process thread before applying new config
  1587. # since we don't want to touch data that is being used by the threads
  1588. $self->pause_background_process;
  1589. # apply new config
  1590. my $invalidated = $self->{print}->apply_config($self->config);
  1591. # reset preview canvases (invalidated contents will be hidden)
  1592. $self->{toolpaths2D}->reload_print if $self->{toolpaths2D};
  1593. $self->{preview3D}->reload_print if $self->{preview3D};
  1594. $self->{AdaptiveLayersDialog}->reload_preview if $self->{AdaptiveLayersDialog};
  1595. if (!$Slic3r::GUI::Settings->{_}{background_processing}) {
  1596. $self->hide_preview if $invalidated;
  1597. return;
  1598. }
  1599. if ($invalidated) {
  1600. # kill current thread if any
  1601. $self->stop_background_process;
  1602. # remove the sliced statistics box because something changed.
  1603. $self->toggle_print_stats(0);
  1604. } else {
  1605. $self->resume_background_process;
  1606. }
  1607. # schedule a new process thread in case it wasn't running
  1608. $self->start_background_process;
  1609. }
  1610. sub start_background_process {
  1611. my ($self) = @_;
  1612. return if !$Slic3r::have_threads;
  1613. return if $self->{process_thread};
  1614. if (!@{$self->{objects}}) {
  1615. $self->on_process_completed;
  1616. return;
  1617. }
  1618. # It looks like declaring a local $SIG{__WARN__} prevents the ugly
  1619. # "Attempt to free unreferenced scalar" warning...
  1620. local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self);
  1621. # don't start process thread if config is not valid
  1622. eval {
  1623. # this will throw errors if config is not valid
  1624. $self->config->validate;
  1625. $self->{print}->validate;
  1626. };
  1627. if ($@) {
  1628. $self->statusbar->SetStatusText($@);
  1629. return;
  1630. }
  1631. if ($Slic3r::GUI::Settings->{_}{threads}) {
  1632. $self->{print}->config->set('threads', $Slic3r::GUI::Settings->{_}{threads});
  1633. }
  1634. # start thread
  1635. @_ = ();
  1636. $self->{process_thread} = Slic3r::spawn_thread(sub {
  1637. eval {
  1638. $self->{print}->process;
  1639. };
  1640. if ($@) {
  1641. Slic3r::debugf "Background process error: $@\n";
  1642. Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, $@));
  1643. } else {
  1644. Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, undef));
  1645. }
  1646. Slic3r::thread_cleanup();
  1647. });
  1648. Slic3r::debugf "Background processing started.\n";
  1649. }
  1650. sub stop_background_process {
  1651. my ($self) = @_;
  1652. $self->{apply_config_timer}->Stop if defined $self->{apply_config_timer};
  1653. $self->statusbar->SetCancelCallback(undef);
  1654. $self->statusbar->StopBusy;
  1655. $self->statusbar->SetStatusText("");
  1656. $self->{toolpaths2D}->reload_print if $self->{toolpaths2D};
  1657. $self->{preview3D}->reload_print if $self->{preview3D};
  1658. $self->{AdaptiveLayersDialog}->reload_preview if $self->{AdaptiveLayersDialog};
  1659. if ($self->{process_thread}) {
  1660. Slic3r::debugf "Killing background process.\n";
  1661. Slic3r::kill_all_threads();
  1662. $self->{process_thread} = undef;
  1663. } else {
  1664. Slic3r::debugf "No background process running.\n";
  1665. }
  1666. # if there's an export process, kill that one as well
  1667. if ($self->{export_thread}) {
  1668. Slic3r::debugf "Killing background export process.\n";
  1669. Slic3r::kill_all_threads();
  1670. $self->{export_thread} = undef;
  1671. }
  1672. }
  1673. sub pause_background_process {
  1674. my ($self) = @_;
  1675. if ($self->{process_thread} || $self->{export_thread}) {
  1676. Slic3r::pause_all_threads();
  1677. return 1;
  1678. } elsif (defined $self->{apply_config_timer} && $self->{apply_config_timer}->IsRunning) {
  1679. $self->{apply_config_timer}->Stop;
  1680. return 0; # we didn't actually pause any running thread; need to reschedule
  1681. }
  1682. return 0;
  1683. }
  1684. sub resume_background_process {
  1685. my ($self) = @_;
  1686. if ($self->{process_thread} || $self->{export_thread}) {
  1687. Slic3r::resume_all_threads();
  1688. }
  1689. }
  1690. sub export_gcode {
  1691. my ($self, $output_file) = @_;
  1692. return if !@{$self->{objects}};
  1693. if ($self->{export_gcode_output_file}) {
  1694. Wx::MessageDialog->new($self, "Another export job is currently running.", 'Error', wxOK | wxICON_ERROR)->ShowModal;
  1695. return;
  1696. }
  1697. # if process is not running, validate config
  1698. # (we assume that if it is running, config is valid)
  1699. eval {
  1700. # this will throw errors if config is not valid
  1701. $self->config->validate;
  1702. $self->{print}->validate;
  1703. };
  1704. Slic3r::GUI::catch_error($self) and return;
  1705. # apply config and validate print
  1706. my $config = $self->config;
  1707. eval {
  1708. # this will throw errors if config is not valid
  1709. $config->validate;
  1710. $self->{print}->apply_config($config);
  1711. $self->{print}->validate;
  1712. };
  1713. if (!$Slic3r::have_threads) {
  1714. Slic3r::GUI::catch_error($self) and return;
  1715. }
  1716. # select output file
  1717. if ($output_file) {
  1718. $self->{export_gcode_output_file} = $self->{print}->output_filepath($output_file);
  1719. } else {
  1720. my $default_output_file = $self->{print}->output_filepath($main::opt{output} // '');
  1721. my $dlg = Wx::FileDialog->new($self, 'Save G-code file as:', wxTheApp->output_path(dirname($default_output_file)),
  1722. basename($default_output_file), &Slic3r::GUI::FILE_WILDCARDS->{gcode}, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
  1723. if ($dlg->ShowModal != wxID_OK) {
  1724. $dlg->Destroy;
  1725. return;
  1726. }
  1727. my $path = Slic3r::decode_path($dlg->GetPath);
  1728. $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($path);
  1729. wxTheApp->save_settings;
  1730. $self->{export_gcode_output_file} = $path;
  1731. $dlg->Destroy;
  1732. }
  1733. $self->statusbar->StartBusy;
  1734. if ($Slic3r::have_threads) {
  1735. $self->statusbar->SetCancelCallback(sub {
  1736. $self->stop_background_process;
  1737. $self->statusbar->SetStatusText("Export cancelled");
  1738. $self->{export_gcode_output_file} = undef;
  1739. $self->{send_gcode_file} = undef;
  1740. # this updates buttons status
  1741. $self->object_list_changed;
  1742. });
  1743. # start background process, whose completion event handler
  1744. # will detect $self->{export_gcode_output_file} and proceed with export
  1745. $self->start_background_process;
  1746. } else {
  1747. eval {
  1748. $self->{print}->process;
  1749. $self->{print}->export_gcode(output_file => $self->{export_gcode_output_file});
  1750. };
  1751. my $result = !Slic3r::GUI::catch_error($self);
  1752. $self->on_export_completed($result);
  1753. }
  1754. # this updates buttons status
  1755. $self->object_list_changed;
  1756. $self->toggle_print_stats(1);
  1757. return $self->{export_gcode_output_file};
  1758. }
  1759. # This gets called only if we have threads.
  1760. sub on_process_completed {
  1761. my ($self, $error) = @_;
  1762. $self->statusbar->SetCancelCallback(undef);
  1763. $self->statusbar->StopBusy;
  1764. $self->statusbar->SetStatusText($error // "");
  1765. Slic3r::debugf "Background processing completed.\n";
  1766. $self->{process_thread}->detach if $self->{process_thread};
  1767. $self->{process_thread} = undef;
  1768. $self->{processed} = 1;
  1769. # if we're supposed to perform an explicit export let's display the error in a dialog
  1770. if ($error && $self->{export_gcode_output_file}) {
  1771. $self->{export_gcode_output_file} = undef;
  1772. Slic3r::GUI::show_error($self, $error);
  1773. }
  1774. return if $error;
  1775. $self->{toolpaths2D}->reload_print if $self->{toolpaths2D};
  1776. $self->{preview3D}->reload_print if $self->{preview3D};
  1777. $self->{AdaptiveLayersDialog}->reload_preview if $self->{AdaptiveLayersDialog};
  1778. # if we have an export filename, start a new thread for exporting G-code
  1779. if ($self->{export_gcode_output_file}) {
  1780. @_ = ();
  1781. # workaround for "Attempt to free un referenced scalar..."
  1782. our $_thread_self = $self;
  1783. $self->{export_thread} = Slic3r::spawn_thread(sub {
  1784. eval {
  1785. $_thread_self->{print}->export_gcode(output_file => $_thread_self->{export_gcode_output_file});
  1786. };
  1787. if ($@) {
  1788. Wx::PostEvent($_thread_self, Wx::PlThreadEvent->new(-1, $ERROR_EVENT, shared_clone([ $@ ])));
  1789. Wx::PostEvent($_thread_self, Wx::PlThreadEvent->new(-1, $EXPORT_COMPLETED_EVENT, 0));
  1790. } else {
  1791. Wx::PostEvent($_thread_self, Wx::PlThreadEvent->new(-1, $EXPORT_COMPLETED_EVENT, 1));
  1792. }
  1793. Slic3r::thread_cleanup();
  1794. });
  1795. Slic3r::debugf "Background G-code export started.\n";
  1796. }
  1797. }
  1798. # This gets called also if we have no threads.
  1799. sub on_progress_event {
  1800. my ($self, $percent, $message) = @_;
  1801. $self->statusbar->SetProgress($percent);
  1802. $self->statusbar->SetStatusText("$message…");
  1803. }
  1804. # This gets called also if we don't have threads.
  1805. sub on_export_completed {
  1806. my ($self, $result) = @_;
  1807. $self->statusbar->SetCancelCallback(undef);
  1808. $self->statusbar->StopBusy;
  1809. $self->statusbar->SetStatusText("");
  1810. Slic3r::debugf "Background export process completed.\n";
  1811. $self->{export_thread}->detach if $self->{export_thread};
  1812. $self->{export_thread} = undef;
  1813. my $message;
  1814. my $send_gcode = 0;
  1815. my $do_print = 0;
  1816. if ($result) {
  1817. if ($self->{print_file}) {
  1818. $message = "File added to print queue";
  1819. $do_print = 1;
  1820. } elsif ($self->{send_gcode_file}) {
  1821. $message = "Sending G-code file to the " . $self->{config}->host_type . " server...";
  1822. $send_gcode = 1;
  1823. } else {
  1824. $message = "G-code file exported to " . $self->{export_gcode_output_file};
  1825. }
  1826. } else {
  1827. $message = "Export failed";
  1828. }
  1829. $self->{export_gcode_output_file} = undef;
  1830. $self->statusbar->SetStatusText($message);
  1831. wxTheApp->notify($message);
  1832. $self->do_print if $do_print;
  1833. $self->send_gcode if $send_gcode;
  1834. $self->{print_file} = undef;
  1835. $self->{send_gcode_file} = undef;
  1836. {
  1837. my $fil = sprintf(
  1838. '%.2fcm (%.2fcm³%s)',
  1839. $self->{print}->total_used_filament / 10,
  1840. $self->{print}->total_extruded_volume / 1000,
  1841. $self->{print}->total_weight
  1842. ? sprintf(', %.2fg', $self->{print}->total_weight)
  1843. : '',
  1844. );
  1845. my $cost = $self->{print}->total_cost
  1846. ? sprintf("%.2f" , $self->{print}->total_cost)
  1847. : 'n.a.';
  1848. $self->{print_info_fil}->SetLabel($fil);
  1849. $self->{print_info_cost}->SetLabel($cost);
  1850. }
  1851. # this updates buttons status
  1852. $self->object_list_changed;
  1853. }
  1854. sub do_print {
  1855. my ($self) = @_;
  1856. my $controller = $self->GetFrame->{controller} or return;
  1857. my %current_presets = $self->selected_presets;
  1858. my $printer_panel = $controller->add_printer($current_presets{printer}->[0], $self->config);
  1859. my $filament_stats = $self->{print}->filament_stats;
  1860. $filament_stats = { map { $current_presets{filament}[$_]->name => $filament_stats->{$_} } keys %$filament_stats };
  1861. $printer_panel->load_print_job($self->{print_file}, $filament_stats);
  1862. $self->GetFrame->select_tab(1);
  1863. }
  1864. sub prepare_send {
  1865. my ($self, $skip_dialog) = @_;
  1866. return if !$self->{btn_send_gcode}->IsEnabled;
  1867. my $filename = basename($self->{print}->output_filepath($main::opt{output} // ''));
  1868. if (!$skip_dialog) {
  1869. # When the alt key is pressed, bypass the dialog.
  1870. my $dlg = Slic3r::GUI::Plater::OctoPrintSpoolDialog->new($self, $filename);
  1871. return unless $dlg->ShowModal == wxID_OK;
  1872. $filename = $dlg->{filename};
  1873. }
  1874. if (!$Slic3r::GUI::Settings->{octoprint}{overwrite}) {
  1875. my $progress = Wx::ProgressDialog->new('Querying OctoPrint…',
  1876. "Checking whether file already exists…", 100, $self, 0);
  1877. $progress->Pulse;
  1878. my $ua = LWP::UserAgent->new;
  1879. $ua->timeout(5);
  1880. my $res;
  1881. if ($self->{config}->print_host) {
  1882. if($self->{config}->host_type eq 'octoprint'){
  1883. $res = $ua->get(
  1884. "http://" . $self->{config}->print_host . "/api/files/local",
  1885. 'X-Api-Key' => $self->{config}->octoprint_apikey,
  1886. );
  1887. }else {
  1888. $res = $ua->get(
  1889. "http://" . $self->{config}->print_host . "/rr_files",
  1890. );
  1891. }
  1892. }
  1893. $progress->Destroy;
  1894. if ($res->is_success) {
  1895. my $searchterm = ($self->{config}->host_type eq 'octoprint') ? '/"name":\s*"\Q$filename\E"/' : '"'.$filename.'"';
  1896. if ($res->decoded_content =~ $searchterm) {
  1897. my $dialog = Wx::MessageDialog->new($self,
  1898. "It looks like a file with the same name already exists in the server. "
  1899. . "Shall I overwrite it?",
  1900. $self->{config}->host_type, wxICON_WARNING | wxYES | wxNO);
  1901. if ($dialog->ShowModal() == wxID_NO) {
  1902. return;
  1903. }
  1904. }
  1905. } else {
  1906. my $message = "Error while connecting to the " . $self->{config}->host_type . " server: " . $res->status_line;
  1907. Slic3r::GUI::show_error($self, $message);
  1908. return;
  1909. }
  1910. }
  1911. $self->{send_gcode_file_print} = $Slic3r::GUI::Settings->{octoprint}{start};
  1912. $self->{send_gcode_file} = $self->export_gcode(Wx::StandardPaths::Get->GetTempDir() . "/" . $filename);
  1913. }
  1914. sub send_gcode {
  1915. my ($self) = @_;
  1916. $self->statusbar->StartBusy;
  1917. my $ua = LWP::UserAgent->new;
  1918. $ua->timeout(180);
  1919. my $path = Slic3r::encode_path($self->{send_gcode_file});
  1920. my $filename = basename($self->{print}->output_filepath($main::opt{output} // ''));
  1921. my $res;
  1922. if($self->{config}->print_host){
  1923. if($self->{config}->host_type eq 'octoprint'){
  1924. $res = $ua->post(
  1925. "http://" . $self->{config}->print_host . "/api/files/local",
  1926. Content_Type => 'form-data',
  1927. 'X-Api-Key' => $self->{config}->octoprint_apikey,
  1928. Content => [
  1929. # OctoPrint doesn't like Windows paths so we use basename()
  1930. # Also, since we need to read from filesystem we process it through encode_path()
  1931. file => [ $path, basename($path) ],
  1932. print => $self->{send_gcode_file_print} ? 1 : 0,
  1933. ],
  1934. );
  1935. }else{
  1936. # slurp the file we would send into a string - should be someplace to reference this but could not find it?
  1937. local $/=undef;
  1938. open (my $gch,$path);
  1939. my $gcode=<$gch>;
  1940. close($gch);
  1941. # get the time string
  1942. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  1943. my $t = sprintf("%4d-%02d-%02dT%02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec);
  1944. my $req = HTTP::Request->new(POST => "http://" . $self->{config}->print_host . "/rr_upload?name=0:/gcodes/" . basename($path) . "&time=$t",);
  1945. $req->content( $gcode );
  1946. $res = $ua->request($req);
  1947. if ($res->is_success) {
  1948. if ($self->{send_gcode_file_print}) {
  1949. $res = $ua->get(
  1950. "http://" . $self->{config}->print_host . "/rr_gcode?gcode=M32%20" . basename($path),
  1951. );
  1952. }
  1953. }
  1954. }
  1955. }
  1956. $self->statusbar->StopBusy;
  1957. if ($res->is_success) {
  1958. $self->statusbar->SetStatusText("G-code file successfully uploaded to the " . $self->{config}->host_type . " server");
  1959. } else {
  1960. my $message = "Error while uploading to the " . $self->{config}->host_type . " server: " . $res->status_line;
  1961. Slic3r::GUI::show_error($self, $message);
  1962. $self->statusbar->SetStatusText($message);
  1963. }
  1964. }
  1965. sub export_stl {
  1966. my $self = shift;
  1967. return if !@{$self->{objects}};
  1968. my $output_file = $self->_get_export_file('STL') or return;
  1969. $self->{model}->write_stl($output_file, 1);
  1970. $self->statusbar->SetStatusText("STL file exported to $output_file");
  1971. }
  1972. sub reload_from_disk {
  1973. my ($self) = @_;
  1974. my ($obj_idx, $object) = $self->selected_object;
  1975. return if !defined $obj_idx;
  1976. if (!$object->input_file) {
  1977. Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be reloaded because it isn't referenced to its input file any more. This is the case after performing operations like cut or split.");
  1978. return;
  1979. }
  1980. if (!-e $object->input_file) {
  1981. Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be reloaded because the file doesn't exist anymore on the disk.");
  1982. return;
  1983. }
  1984. # Only reload the selected object and not all objects from the input file.
  1985. my @new_obj_idx = $self->load_file($object->input_file, $object->input_file_obj_idx);
  1986. if (!@new_obj_idx) {
  1987. Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be reloaded because the new file doesn't contain the object.");
  1988. return;
  1989. }
  1990. my $org_obj = $self->{model}->objects->[$obj_idx];
  1991. # check if the object is dependant of more than one file
  1992. my $org_obj_has_modifiers=0;
  1993. for my $i (0..($org_obj->volumes_count-1)) {
  1994. if ($org_obj->input_file ne $org_obj->get_volume($i)->input_file) {
  1995. $org_obj_has_modifiers=1;
  1996. last;
  1997. }
  1998. }
  1999. my $reload_behavior = $Slic3r::GUI::Settings->{_}{reload_behavior};
  2000. # ask the user how to proceed, if option is selected in preferences
  2001. if ($org_obj_has_modifiers && !$Slic3r::GUI::Settings->{_}{reload_hide_dialog}) {
  2002. my $dlg = Slic3r::GUI::ReloadDialog->new(undef,$reload_behavior);
  2003. my $res = $dlg->ShowModal;
  2004. if ($res==wxID_CANCEL) {
  2005. $self->remove($_) for @new_obj_idx;
  2006. $dlg->Destroy;
  2007. return;
  2008. }
  2009. $reload_behavior = $dlg->GetSelection;
  2010. my $save = 0;
  2011. if ($reload_behavior != $Slic3r::GUI::Settings->{_}{reload_behavior}) {
  2012. $Slic3r::GUI::Settings->{_}{reload_behavior} = $reload_behavior;
  2013. $save = 1;
  2014. }
  2015. if ($dlg->GetHideOnNext) {
  2016. $Slic3r::GUI::Settings->{_}{reload_hide_dialog} = 1;
  2017. $save = 1;
  2018. }
  2019. Slic3r::GUI->save_settings if $save;
  2020. $dlg->Destroy;
  2021. }
  2022. my $volume_unmatched=0;
  2023. foreach my $new_obj_idx (@new_obj_idx) {
  2024. my $new_obj = $self->{model}->objects->[$new_obj_idx];
  2025. $new_obj->clear_instances;
  2026. $new_obj->add_instance($_) for @{$org_obj->instances};
  2027. $new_obj->config->apply($org_obj->config);
  2028. my $new_vol_idx = 0;
  2029. my $org_vol_idx = 0;
  2030. my $new_vol_count=$new_obj->volumes_count;
  2031. my $org_vol_count=$org_obj->volumes_count;
  2032. while ($new_vol_idx<=$new_vol_count-1) {
  2033. if (($org_vol_idx<=$org_vol_count-1) && ($org_obj->get_volume($org_vol_idx)->input_file eq $new_obj->input_file)) {
  2034. # apply config from the matching volumes
  2035. $new_obj->get_volume($new_vol_idx++)->config->apply($org_obj->get_volume($org_vol_idx++)->config);
  2036. } else {
  2037. # reload has more volumes than original (first file), apply config from the first volume
  2038. $new_obj->get_volume($new_vol_idx++)->config->apply($org_obj->get_volume(0)->config);
  2039. $volume_unmatched=1;
  2040. }
  2041. }
  2042. $org_vol_idx=$org_vol_count if $reload_behavior==2; # Reload behavior: discard
  2043. while (($org_vol_idx<=$org_vol_count-1) && ($org_obj->get_volume($org_vol_idx)->input_file eq $new_obj->input_file)) {
  2044. # original has more volumes (first file), skip those
  2045. $org_vol_idx++;
  2046. $volume_unmatched=1;
  2047. }
  2048. while ($org_vol_idx<=$org_vol_count-1) {
  2049. if ($reload_behavior==1) { # Reload behavior: copy
  2050. my $new_volume = $new_obj->add_volume($org_obj->get_volume($org_vol_idx));
  2051. $new_volume->mesh->translate(@{$org_obj->origin_translation->negative});
  2052. $new_volume->mesh->translate(@{$new_obj->origin_translation});
  2053. if ($new_volume->name =~ m/link to path\z/) {
  2054. my $new_name = $new_volume->name;
  2055. $new_name =~ s/ - no link to path$/ - copied/;
  2056. $new_volume->set_name($new_name);
  2057. }elsif(!($new_volume->name =~ m/copied\z/)) {
  2058. $new_volume->set_name($new_volume->name . " - copied");
  2059. }
  2060. }else{ # Reload behavior: Reload all, also fallback solution if ini was manually edited to a wrong value
  2061. if ($org_obj->get_volume($org_vol_idx)->input_file) {
  2062. my $model = eval { Slic3r::Model->read_from_file($org_obj->get_volume($org_vol_idx)->input_file) };
  2063. if ($@) {
  2064. $org_obj->get_volume($org_vol_idx)->set_input_file("");
  2065. }elsif ($org_obj->get_volume($org_vol_idx)->input_file_obj_idx > ($model->objects_count-1)) {
  2066. # Object Index for that part / modifier not found in current version of the file
  2067. $org_obj->get_volume($org_vol_idx)->set_input_file("");
  2068. }else{
  2069. my $prt_mod_obj = $model->objects->[$org_obj->get_volume($org_vol_idx)->input_file_obj_idx];
  2070. if ($org_obj->get_volume($org_vol_idx)->input_file_vol_idx > ($prt_mod_obj->volumes_count-1)) {
  2071. # Volume Index for that part / modifier not found in current version of the file
  2072. $org_obj->get_volume($org_vol_idx)->set_input_file("");
  2073. }else{
  2074. # all checks passed, load new mesh and copy metadata
  2075. my $new_volume = $new_obj->add_volume($prt_mod_obj->get_volume($org_obj->get_volume($org_vol_idx)->input_file_vol_idx));
  2076. $new_volume->set_input_file($org_obj->get_volume($org_vol_idx)->input_file);
  2077. $new_volume->set_input_file_obj_idx($org_obj->get_volume($org_vol_idx)->input_file_obj_idx);
  2078. $new_volume->set_input_file_vol_idx($org_obj->get_volume($org_vol_idx)->input_file_vol_idx);
  2079. $new_volume->config->apply($org_obj->get_volume($org_vol_idx)->config);
  2080. $new_volume->set_modifier($org_obj->get_volume($org_vol_idx)->modifier);
  2081. $new_volume->mesh->translate(@{$new_obj->origin_translation});
  2082. }
  2083. }
  2084. }
  2085. if (!$org_obj->get_volume($org_vol_idx)->input_file) {
  2086. my $new_volume = $new_obj->add_volume($org_obj->get_volume($org_vol_idx)); # error -> copy old mesh
  2087. $new_volume->mesh->translate(@{$org_obj->origin_translation->negative});
  2088. $new_volume->mesh->translate(@{$new_obj->origin_translation});
  2089. if ($new_volume->name =~ m/copied\z/) {
  2090. my $new_name = $new_volume->name;
  2091. $new_name =~ s/ - copied$/ - no link to path/;
  2092. $new_volume->set_name($new_name);
  2093. }elsif(!($new_volume->name =~ m/link to path\z/)) {
  2094. $new_volume->set_name($new_volume->name . " - no link to path");
  2095. }
  2096. $volume_unmatched=1;
  2097. }
  2098. }
  2099. $org_vol_idx++;
  2100. }
  2101. }
  2102. $self->remove($obj_idx);
  2103. # TODO: refresh object list which contains wrong count and scale
  2104. # Trigger thumbnail generation again, because the remove() method altered
  2105. # object indexes before background thumbnail generation called its completion
  2106. # event, so the on_thumbnail_made callback is called with the wrong $obj_idx.
  2107. # When porting to C++ we'll probably have cleaner ways to do this.
  2108. $self->make_thumbnail($_-1) for @new_obj_idx;
  2109. # update print
  2110. $self->stop_background_process;
  2111. $self->{print}->reload_object($_-1) for @new_obj_idx;
  2112. $self->on_model_change;
  2113. # Empty the redo stack
  2114. $self->{redo_stack} = [];
  2115. if ($volume_unmatched) {
  2116. Slic3r::GUI::warning_catcher($self)->("At least 1 volume couldn't be matched between the original object and the reloaded one.");
  2117. }
  2118. }
  2119. sub export_object_stl {
  2120. my $self = shift;
  2121. my ($obj_idx, $object) = $self->selected_object;
  2122. return if !defined $obj_idx;
  2123. my $model_object = $self->{model}->objects->[$obj_idx];
  2124. my $output_file = $self->_get_export_file('STL') or return;
  2125. $model_object->mesh->write_binary($output_file);
  2126. $self->statusbar->SetStatusText("STL file exported to $output_file");
  2127. }
  2128. # Export function for a single AMF output
  2129. sub export_object_amf {
  2130. my $self = shift;
  2131. my ($obj_idx, $object) = $self->selected_object;
  2132. return if !defined $obj_idx;
  2133. my $local_model = Slic3r::Model->new;
  2134. my $model_object = $self->{model}->objects->[$obj_idx];
  2135. # copy model_object -> local_model
  2136. $local_model->add_object($model_object);
  2137. my $output_file = $self->_get_export_file('AMF') or return;
  2138. $local_model->write_amf($output_file);
  2139. $self->statusbar->SetStatusText("AMF file exported to $output_file");
  2140. }
  2141. # Export function for a single 3MF output
  2142. sub export_object_tmf {
  2143. my $self = shift;
  2144. my ($obj_idx, $object) = $self->selected_object;
  2145. return if !defined $obj_idx;
  2146. my $local_model = Slic3r::Model->new;
  2147. my $model_object = $self->{model}->objects->[$obj_idx];
  2148. # copy model_object -> local_model
  2149. $local_model->add_object($model_object);
  2150. my $output_file = $self->_get_export_file('TMF') or return;
  2151. $local_model->write_tmf($output_file);
  2152. $self->statusbar->SetStatusText("3MF file exported to $output_file");
  2153. }
  2154. sub export_amf {
  2155. my $self = shift;
  2156. return if !@{$self->{objects}};
  2157. my $output_file = $self->_get_export_file('AMF') or return;
  2158. $self->{model}->write_amf($output_file);
  2159. $self->statusbar->SetStatusText("AMF file exported to $output_file");
  2160. }
  2161. sub export_tmf {
  2162. my $self = shift;
  2163. return if !@{$self->{objects}};
  2164. my $output_file = $self->_get_export_file('TMF') or return;
  2165. $self->{model}->write_tmf($output_file);
  2166. $self->statusbar->SetStatusText("3MF file exported to $output_file");
  2167. }
  2168. sub _get_export_file {
  2169. my $self = shift;
  2170. my ($format) = @_;
  2171. my $suffix = $format eq 'STL' ? '.stl' : ( $format eq 'AMF' ? '.amf' : '.3mf');
  2172. my $output_file = $main::opt{output};
  2173. {
  2174. $output_file = $self->{print}->output_filepath($output_file // '');
  2175. $output_file =~ s/\.gcode$/$suffix/i;
  2176. my $dlg;
  2177. $dlg = Wx::FileDialog->new($self, "Save $format file as:", dirname($output_file),
  2178. basename($output_file), &Slic3r::GUI::STL_MODEL_WILDCARD, wxFD_SAVE | wxFD_OVERWRITE_PROMPT)
  2179. if $format eq 'STL';
  2180. $dlg = Wx::FileDialog->new($self, "Save $format file as:", dirname($output_file),
  2181. basename($output_file), &Slic3r::GUI::AMF_MODEL_WILDCARD, wxFD_SAVE | wxFD_OVERWRITE_PROMPT)
  2182. if $format eq 'AMF';
  2183. $dlg = Wx::FileDialog->new($self, "Save $format file as:", dirname($output_file),
  2184. basename($output_file), &Slic3r::GUI::TMF_MODEL_WILDCARD, wxFD_SAVE | wxFD_OVERWRITE_PROMPT)
  2185. if $format eq 'TMF';
  2186. if ($dlg->ShowModal != wxID_OK) {
  2187. $dlg->Destroy;
  2188. return undef;
  2189. }
  2190. $output_file = Slic3r::decode_path($dlg->GetPath);
  2191. $dlg->Destroy;
  2192. }
  2193. return $output_file;
  2194. }
  2195. sub make_thumbnail {
  2196. my $self = shift;
  2197. my ($obj_idx) = @_;
  2198. my $plater_object = $self->{objects}[$obj_idx];
  2199. return if($plater_object->remaking_thumbnail);
  2200. $plater_object->remaking_thumbnail(1);
  2201. $plater_object->thumbnail(Slic3r::ExPolygon::Collection->new);
  2202. my $cb = sub {
  2203. $plater_object->make_thumbnail($self->{model}, $obj_idx);
  2204. if ($Slic3r::have_threads) {
  2205. Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx ])));
  2206. Slic3r::thread_cleanup();
  2207. threads->exit;
  2208. } else {
  2209. $self->on_thumbnail_made($obj_idx);
  2210. }
  2211. };
  2212. @_ = ();
  2213. $Slic3r::have_threads
  2214. ? threads->create(sub { $cb->(); Slic3r::thread_cleanup(); })->detach
  2215. : $cb->();
  2216. }
  2217. sub on_thumbnail_made {
  2218. my $self = shift;
  2219. my ($obj_idx) = @_;
  2220. $self->{objects}[$obj_idx]->remaking_thumbnail(0);
  2221. $self->{objects}[$obj_idx]->transform_thumbnail($self->{model}, $obj_idx);
  2222. $self->refresh_canvases;
  2223. }
  2224. # this method gets called whenever print center is changed or the objects' bounding box changes
  2225. # (i.e. when an object is added/removed/moved/rotated/scaled)
  2226. sub on_model_change {
  2227. my ($self, $force_autocenter) = @_;
  2228. # reload the select submenu (if already initialized)
  2229. if (my $menu = $self->GetFrame->{plater_select_menu}) {
  2230. $menu->DeleteItem($_) for $menu->GetMenuItems;
  2231. for my $i (0..$#{$self->{objects}}) {
  2232. my $name = $self->{objects}->[$i]->name;
  2233. my $count = $self->{model}->get_object($i)->instances_count;
  2234. if ($count > 1) {
  2235. $name .= " (${count}x)";
  2236. }
  2237. my $item = wxTheApp->append_menu_item($menu, $name, 'Select object', sub {
  2238. $self->select_object($i);
  2239. $self->refresh_canvases;
  2240. }, undef, undef, wxITEM_CHECK);
  2241. $item->Check(1) if $self->{objects}->[$i]->selected;
  2242. }
  2243. }
  2244. # reload the objects info choice
  2245. if (my $choice = $self->{object_info_choice}) {
  2246. $choice->Clear;
  2247. for my $i (0..$#{$self->{objects}}) {
  2248. my $name = $self->{objects}->[$i]->name;
  2249. my $count = $self->{model}->get_object($i)->instances_count;
  2250. if ($count > 1) {
  2251. $name .= " (${count}x)";
  2252. }
  2253. $choice->Append($name);
  2254. }
  2255. my ($obj_idx, $object) = $self->selected_object;
  2256. $choice->SetSelection($obj_idx // -1);
  2257. }
  2258. my $running = $self->pause_background_process;
  2259. if ($Slic3r::GUI::Settings->{_}{autocenter} || $force_autocenter) {
  2260. $self->{model}->center_instances_around_point($self->bed_centerf);
  2261. }
  2262. $self->refresh_canvases;
  2263. my $invalidated = $self->{print}->reload_model_instances();
  2264. if ($Slic3r::GUI::Settings->{_}{background_processing}) {
  2265. if ($invalidated || !$running) {
  2266. # The mere fact that no steps were invalidated when reloading model instances
  2267. # doesn't mean that all steps were done: for example, validation might have
  2268. # failed upon previous instance move, so we have no running thread and no steps
  2269. # are invalidated on this move, thus we need to schedule a new run.
  2270. $self->schedule_background_process;
  2271. $self->toggle_print_stats(0);
  2272. } else {
  2273. $self->resume_background_process;
  2274. }
  2275. } else {
  2276. $self->hide_preview;
  2277. }
  2278. }
  2279. sub hide_preview {
  2280. my ($self) = @_;
  2281. my $sel = $self->{preview_notebook}->GetSelection;
  2282. if ($sel == $self->{preview3D_page_idx} || $sel == $self->{toolpaths2D_page_idx}) {
  2283. $self->{preview_notebook}->SetSelection(0);
  2284. }
  2285. $self->{processed} = 0;
  2286. }
  2287. sub on_extruders_change {
  2288. my ($self, $num_extruders) = @_;
  2289. my $choices = $self->{preset_choosers}{filament};
  2290. while (@$choices < $num_extruders) {
  2291. # copy strings from first choice
  2292. my @presets = $choices->[0]->GetStrings;
  2293. # initialize new choice
  2294. my $choice = Wx::BitmapComboBox->new($self, -1, "", wxDefaultPosition, wxDefaultSize, [@presets], wxCB_READONLY);
  2295. push @$choices, $choice;
  2296. # copy icons from first choice
  2297. $choice->SetItemBitmap($_, $choices->[0]->GetItemBitmap($_)) for 0..$#presets;
  2298. # settings button
  2299. my $settings_btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG),
  2300. wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
  2301. # insert new row into sizer
  2302. $self->{presets_sizer}->Insert(6 + ($#$choices-1)*3, 0, 0);
  2303. $self->{presets_sizer}->Insert(7 + ($#$choices-1)*3, $choice, 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING);
  2304. $self->{presets_sizer}->Insert(8 + ($#$choices-1)*3, $settings_btn, 0, wxEXPAND | wxLEFT, 4);
  2305. # setup the listeners
  2306. EVT_COMBOBOX($choice, $choice, sub {
  2307. my ($choice) = @_;
  2308. wxTheApp->CallAfter(sub {
  2309. $self->_on_change_combobox('filament', $choice);
  2310. });
  2311. });
  2312. EVT_BUTTON($self, $settings_btn, sub {
  2313. $self->show_preset_editor('filament', $#$choices);
  2314. });
  2315. # initialize selection
  2316. my $i = first { $choice->GetString($_) eq ($Slic3r::GUI::Settings->{presets}{"filament_" . $#$choices} || '') } 0 .. $#presets;
  2317. $choice->SetSelection($i || 0);
  2318. }
  2319. # remove unused choices if any
  2320. while (@$choices > $num_extruders) {
  2321. my $i = 6 + ($#$choices-1)*3;
  2322. $self->{presets_sizer}->Remove($i); # label
  2323. $self->{presets_sizer}->Remove($i); # wxChoice
  2324. my $settings_btn = $self->{presets_sizer}->GetItem($i)->GetWindow;
  2325. $self->{presets_sizer}->Remove($i); # settings btn
  2326. $settings_btn->Destroy;
  2327. $choices->[-1]->Destroy;
  2328. pop @$choices;
  2329. }
  2330. $self->Layout;
  2331. }
  2332. sub object_cut_dialog {
  2333. my $self = shift;
  2334. my ($obj_idx) = @_;
  2335. if (!defined $obj_idx) {
  2336. ($obj_idx, undef) = $self->selected_object;
  2337. }
  2338. if (!$Slic3r::GUI::have_OpenGL) {
  2339. Slic3r::GUI::show_error($self, "Please install the OpenGL modules to use this feature (see build instructions).");
  2340. return;
  2341. }
  2342. my $dlg = Slic3r::GUI::Plater::ObjectCutDialog->new($self,
  2343. object => $self->{objects}[$obj_idx],
  2344. model_object => $self->{model}->objects->[$obj_idx],
  2345. );
  2346. return unless $dlg->ShowModal == wxID_OK;
  2347. if (my @new_objects = $dlg->NewModelObjects) {
  2348. my $process_dialog = Wx::ProgressDialog->new('Loading…', "Loading new objects…", 100, $self, 0);
  2349. $process_dialog->Pulse;
  2350. # Create two models to save the current object and the resulted objects.
  2351. my $new_objects_model = Slic3r::Model->new;
  2352. foreach my $new_object (@new_objects) {
  2353. $new_objects_model->add_object($new_object);
  2354. }
  2355. my $org_object_model = Slic3r::Model->new;
  2356. $org_object_model->add_object($self->{model}->get_object($obj_idx));
  2357. # Save the object identifiers used in undo/redo operations.
  2358. my $object_id = $self->{objects}->[$obj_idx]->identifier;
  2359. my $new_objects_id_start = $self->{object_identifier};
  2360. $self->add_undo_operation("CUT", $object_id, $org_object_model, $new_objects_model, $new_objects_id_start);
  2361. $self->remove($obj_idx, 'true');
  2362. $self->load_model_objects(grep defined($_), @new_objects);
  2363. $self->arrange if @new_objects <= 2; # don't arrange for grid cuts
  2364. $process_dialog->Destroy;
  2365. }
  2366. }
  2367. sub object_layers_dialog {
  2368. my $self = shift;
  2369. my ($obj_idx) = @_;
  2370. $self->object_settings_dialog($obj_idx, adaptive_layers => 1);
  2371. }
  2372. sub object_settings_dialog {
  2373. my $self = shift;
  2374. my ($obj_idx, %params) = @_;
  2375. if (!defined $obj_idx) {
  2376. ($obj_idx, undef) = $self->selected_object;
  2377. }
  2378. my $model_object = $self->{model}->objects->[$obj_idx];
  2379. # validate config before opening the settings dialog because
  2380. # that dialog can't be closed if validation fails, but user
  2381. # can't fix any error which is outside that dialog
  2382. return unless $self->validate_config;
  2383. my $dlg = Slic3r::GUI::Plater::ObjectSettingsDialog->new($self,
  2384. object => $self->{objects}[$obj_idx],
  2385. model_object => $model_object,
  2386. obj_idx => $obj_idx,
  2387. );
  2388. # store pointer to the adaptive layer tab to push preview updates
  2389. $self->{AdaptiveLayersDialog} = $dlg->{adaptive_layers};
  2390. # and jump directly to the tab if called by "promo-button"
  2391. $dlg->{tabpanel}->SetSelection(1) if $params{adaptive_layers};
  2392. $self->pause_background_process;
  2393. $dlg->ShowModal;
  2394. $self->{AdaptiveLayersDialog} = undef;
  2395. # update thumbnail since parts may have changed
  2396. if ($dlg->PartsChanged) {
  2397. # recenter and re-align to Z = 0
  2398. $model_object->center_around_origin;
  2399. $self->make_thumbnail($obj_idx);
  2400. }
  2401. # update print
  2402. if ($dlg->PartsChanged || $dlg->PartSettingsChanged) {
  2403. $self->stop_background_process;
  2404. $self->{print}->reload_object($obj_idx);
  2405. $self->on_model_change;
  2406. } else {
  2407. $self->resume_background_process;
  2408. }
  2409. }
  2410. sub object_list_changed {
  2411. my $self = shift;
  2412. my $have_objects = @{$self->{objects}} ? 1 : 0;
  2413. my $method = $have_objects ? 'Enable' : 'Disable';
  2414. $self->{"btn_$_"}->$method
  2415. for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl print send_gcode);
  2416. if ($self->{export_gcode_output_file} || $self->{send_gcode_file}) {
  2417. $self->{btn_export_gcode}->Disable;
  2418. $self->{btn_print}->Disable;
  2419. $self->{btn_send_gcode}->Disable;
  2420. }
  2421. if ($self->{htoolbar}) {
  2422. $self->{htoolbar}->EnableTool($_, $have_objects)
  2423. for (TB_RESET, TB_ARRANGE);
  2424. }
  2425. # prepagate the event to the frame (a custom Wx event would be cleaner)
  2426. $self->GetFrame->on_plater_object_list_changed($have_objects);
  2427. }
  2428. sub selection_changed {
  2429. my $self = shift;
  2430. my ($obj_idx, $object) = $self->selected_object;
  2431. my $have_sel = defined $obj_idx;
  2432. # Remove selection in 2d Plater.
  2433. $self->{canvas}->{selected_instance} = undef;
  2434. if (my $menu = $self->GetFrame->{plater_select_menu}) {
  2435. $_->Check(0) for $menu->GetMenuItems;
  2436. if ($have_sel) {
  2437. $menu->FindItemByPosition($obj_idx)->Check(1);
  2438. }
  2439. }
  2440. my $method = $have_sel ? 'Enable' : 'Disable';
  2441. $self->{"btn_$_"}->$method
  2442. for grep $self->{"btn_$_"}, qw(remove increase decrease rotate45cw rotate45ccw rotateFace changescale split cut layers settings);
  2443. if ($self->{htoolbar}) {
  2444. $self->{htoolbar}->EnableTool($_, $have_sel)
  2445. for (TB_REMOVE, TB_MORE, TB_FEWER, TB_45CW, TB_45CCW, TB_ROTFACE, TB_SCALE, TB_SPLIT, TB_CUT, TB_LAYERS, TB_SETTINGS);
  2446. }
  2447. if ($self->{object_info_size}) { # have we already loaded the info pane?
  2448. if ($have_sel) {
  2449. my $model_object = $self->{model}->objects->[$obj_idx];
  2450. $self->{object_info_choice}->SetSelection($obj_idx);
  2451. $self->{object_info_copies}->SetLabel($model_object->instances_count);
  2452. my $model_instance = $model_object->instances->[0];
  2453. {
  2454. my $size_string = sprintf "%.2f x %.2f x %.2f", @{$model_object->instance_bounding_box(0)->size};
  2455. if ($model_instance->scaling_factor != 1) {
  2456. $size_string .= sprintf " (%s%%)", $model_instance->scaling_factor * 100;
  2457. }
  2458. $self->{object_info_size}->SetLabel($size_string);
  2459. }
  2460. $self->{object_info_materials}->SetLabel($model_object->materials_count);
  2461. my $raw_mesh = $model_object->raw_mesh;
  2462. $raw_mesh->repair; # this calculates number_of_parts
  2463. if (my $stats = $raw_mesh->stats) {
  2464. $self->{object_info_volume}->SetLabel(sprintf('%.2f', $raw_mesh->volume * ($model_instance->scaling_factor**3)));
  2465. $self->{object_info_facets}->SetLabel(sprintf('%d (%d shells)', $model_object->facets_count, $stats->{number_of_parts}));
  2466. if (my $errors = sum(@$stats{qw(degenerate_facets edges_fixed facets_removed facets_added facets_reversed backwards_edges)})) {
  2467. $self->{object_info_manifold}->SetLabel(sprintf("Auto-repaired (%d errors)", $errors));
  2468. $self->{object_info_manifold_warning_icon}->Show;
  2469. # we don't show normals_fixed because we never provide normals
  2470. # to admesh, so it generates normals for all facets
  2471. my $message = sprintf '%d degenerate facets, %d edges fixed, %d facets removed, %d facets added, %d facets reversed, %d backwards edges',
  2472. @$stats{qw(degenerate_facets edges_fixed facets_removed facets_added facets_reversed backwards_edges)};
  2473. $self->{object_info_manifold}->SetToolTipString($message);
  2474. $self->{object_info_manifold_warning_icon}->SetToolTipString($message);
  2475. } else {
  2476. $self->{object_info_manifold}->SetLabel("Yes");
  2477. }
  2478. } else {
  2479. $self->{object_info_facets}->SetLabel($object->facets);
  2480. }
  2481. } else {
  2482. $self->{object_info_choice}->SetSelection(-1);
  2483. $self->{"object_info_$_"}->SetLabel("") for qw(copies size volume facets materials manifold);
  2484. $self->{object_info_manifold_warning_icon}->Hide;
  2485. $self->{object_info_manifold}->SetToolTipString("");
  2486. }
  2487. $self->Layout;
  2488. }
  2489. # prepagate the event to the frame (a custom Wx event would be cleaner)
  2490. $self->GetFrame->on_plater_selection_changed($have_sel);
  2491. }
  2492. sub select_object {
  2493. my ($self, $obj_idx) = @_;
  2494. $_->selected(0) for @{ $self->{objects} };
  2495. $_->selected_instance(-1) for @{ $self->{objects} };
  2496. if (defined $obj_idx) {
  2497. $self->{objects}->[$obj_idx]->selected(1);
  2498. $self->{objects}->[$obj_idx]->selected_instance(0);
  2499. }
  2500. $self->selection_changed(1);
  2501. }
  2502. sub select_next {
  2503. my ($self) = @_;
  2504. return if !@{$self->{objects}};
  2505. my ($obj_idx, $object) = $self->selected_object;
  2506. if (!defined $obj_idx || $obj_idx == $#{$self->{objects}}) {
  2507. $obj_idx = 0;
  2508. } else {
  2509. $obj_idx++;
  2510. }
  2511. $self->select_object($obj_idx);
  2512. $self->refresh_canvases;
  2513. }
  2514. sub select_prev {
  2515. my ($self) = @_;
  2516. return if !@{$self->{objects}};
  2517. my ($obj_idx, $object) = $self->selected_object;
  2518. if (!defined $obj_idx || $obj_idx == 0) {
  2519. $obj_idx = $#{$self->{objects}};
  2520. } else {
  2521. $obj_idx--;
  2522. }
  2523. $self->select_object($obj_idx);
  2524. $self->refresh_canvases;
  2525. }
  2526. sub selected_object {
  2527. my $self = shift;
  2528. my $obj_idx = first { $self->{objects}[$_]->selected } 0..$#{ $self->{objects} };
  2529. return undef if !defined $obj_idx;
  2530. return ($obj_idx, $self->{objects}[$obj_idx]),
  2531. }
  2532. sub refresh_canvases {
  2533. my ($self) = @_;
  2534. $self->{canvas}->Refresh;
  2535. $self->{canvas3D}->update if $self->{canvas3D};
  2536. $self->{preview3D}->reload_print if $self->{preview3D};
  2537. }
  2538. sub validate_config {
  2539. my $self = shift;
  2540. eval {
  2541. $self->config->validate;
  2542. };
  2543. return 0 if Slic3r::GUI::catch_error($self);
  2544. return 1;
  2545. }
  2546. sub statusbar {
  2547. my $self = shift;
  2548. return $self->GetFrame->{statusbar};
  2549. }
  2550. sub object_menu {
  2551. my ($self) = @_;
  2552. my $frame = $self->GetFrame;
  2553. my $menu = Wx::Menu->new;
  2554. wxTheApp->append_menu_item($menu, "Delete\tCtrl+Del", 'Remove the selected object', sub {
  2555. $self->remove;
  2556. }, undef, 'brick_delete.png');
  2557. wxTheApp->append_menu_item($menu, "Increase copies\tCtrl++", 'Place one more copy of the selected object', sub {
  2558. $self->increase;
  2559. }, undef, 'add.png');
  2560. wxTheApp->append_menu_item($menu, "Decrease copies\tCtrl+-", 'Remove one copy of the selected object', sub {
  2561. $self->decrease;
  2562. }, undef, 'delete.png');
  2563. wxTheApp->append_menu_item($menu, "Set number of copies…", 'Change the number of copies of the selected object', sub {
  2564. $self->set_number_of_copies;
  2565. }, undef, 'textfield.png');
  2566. $menu->AppendSeparator();
  2567. wxTheApp->append_menu_item($menu, "Move to bed center", 'Center object around bed center', sub {
  2568. $self->center_selected_object_on_bed;
  2569. }, undef, 'arrow_in.png');
  2570. wxTheApp->append_menu_item($menu, "Rotate 45° clockwise", 'Rotate the selected object by 45° clockwise', sub {
  2571. $self->rotate(-45);
  2572. }, undef, 'arrow_rotate_clockwise.png');
  2573. wxTheApp->append_menu_item($menu, "Rotate 45° counter-clockwise", 'Rotate the selected object by 45° counter-clockwise', sub {
  2574. $self->rotate(+45);
  2575. }, undef, 'arrow_rotate_anticlockwise.png');
  2576. wxTheApp->append_menu_item($menu, "Rotate Face to Plane", 'Rotates the selected object to have the selected face parallel with a plane', sub {
  2577. $self->rotate_face;
  2578. }, undef, 'rotate_face.png');
  2579. {
  2580. my $rotateMenu = Wx::Menu->new;
  2581. wxTheApp->append_menu_item($rotateMenu, "Around X axis…", 'Rotate the selected object by an arbitrary angle around X axis', sub {
  2582. $self->rotate(undef, X);
  2583. }, undef, 'bullet_red.png');
  2584. wxTheApp->append_menu_item($rotateMenu, "Around Y axis…", 'Rotate the selected object by an arbitrary angle around Y axis', sub {
  2585. $self->rotate(undef, Y);
  2586. }, undef, 'bullet_green.png');
  2587. wxTheApp->append_menu_item($rotateMenu, "Around Z axis…", 'Rotate the selected object by an arbitrary angle around Z axis', sub {
  2588. $self->rotate(undef, Z);
  2589. }, undef, 'bullet_blue.png');
  2590. wxTheApp->append_submenu($menu, "Rotate", 'Rotate the selected object by an arbitrary angle', $rotateMenu, undef, 'textfield.png');
  2591. }
  2592. {
  2593. my $mirrorMenu = Wx::Menu->new;
  2594. wxTheApp->append_menu_item($mirrorMenu, "Along X axis…", 'Mirror the selected object along the X axis', sub {
  2595. $self->mirror(X);
  2596. }, undef, 'bullet_red.png');
  2597. wxTheApp->append_menu_item($mirrorMenu, "Along Y axis…", 'Mirror the selected object along the Y axis', sub {
  2598. $self->mirror(Y);
  2599. }, undef, 'bullet_green.png');
  2600. wxTheApp->append_menu_item($mirrorMenu, "Along Z axis…", 'Mirror the selected object along the Z axis', sub {
  2601. $self->mirror(Z);
  2602. }, undef, 'bullet_blue.png');
  2603. wxTheApp->append_submenu($menu, "Mirror", 'Mirror the selected object', $mirrorMenu, undef, 'shape_flip_horizontal.png');
  2604. }
  2605. {
  2606. my $scaleMenu = Wx::Menu->new;
  2607. wxTheApp->append_menu_item($scaleMenu, "Uniformly…", 'Scale the selected object along the XYZ axes', sub {
  2608. $self->changescale(undef);
  2609. });
  2610. wxTheApp->append_menu_item($scaleMenu, "Along X axis…", 'Scale the selected object along the X axis', sub {
  2611. $self->changescale(X);
  2612. }, undef, 'bullet_red.png');
  2613. wxTheApp->append_menu_item($scaleMenu, "Along Y axis…", 'Scale the selected object along the Y axis', sub {
  2614. $self->changescale(Y);
  2615. }, undef, 'bullet_green.png');
  2616. wxTheApp->append_menu_item($scaleMenu, "Along Z axis…", 'Scale the selected object along the Z axis', sub {
  2617. $self->changescale(Z);
  2618. }, undef, 'bullet_blue.png');
  2619. wxTheApp->append_submenu($menu, "Scale", 'Scale the selected object by a given factor', $scaleMenu, undef, 'arrow_out.png');
  2620. }
  2621. {
  2622. my $scaleToSizeMenu = Wx::Menu->new;
  2623. wxTheApp->append_menu_item($scaleToSizeMenu, "Uniformly…", 'Scale the selected object along the XYZ axes', sub {
  2624. $self->changescale(undef, 1);
  2625. });
  2626. wxTheApp->append_menu_item($scaleToSizeMenu, "Along X axis…", 'Scale the selected object along the X axis', sub {
  2627. $self->changescale(X, 1);
  2628. }, undef, 'bullet_red.png');
  2629. wxTheApp->append_menu_item($scaleToSizeMenu, "Along Y axis…", 'Scale the selected object along the Y axis', sub {
  2630. $self->changescale(Y, 1);
  2631. }, undef, 'bullet_green.png');
  2632. wxTheApp->append_menu_item($scaleToSizeMenu, "Along Z axis…", 'Scale the selected object along the Z axis', sub {
  2633. $self->changescale(Z, 1);
  2634. }, undef, 'bullet_blue.png');
  2635. wxTheApp->append_submenu($menu, "Scale to size", 'Scale the selected object to match a given size', $scaleToSizeMenu, undef, 'arrow_out.png');
  2636. }
  2637. wxTheApp->append_menu_item($menu, "Split", 'Split the selected object into individual parts', sub {
  2638. $self->split_object;
  2639. }, undef, 'shape_ungroup.png');
  2640. wxTheApp->append_menu_item($menu, "Cut…", 'Open the 3D cutting tool', sub {
  2641. $self->object_cut_dialog;
  2642. }, undef, 'package.png');
  2643. wxTheApp->append_menu_item($menu, "Layer heights…", 'Open the dynamic layer height control', sub {
  2644. $self->object_layers_dialog;
  2645. }, undef, 'variable_layer_height.png');
  2646. $menu->AppendSeparator();
  2647. wxTheApp->append_menu_item($menu, "Settings…", 'Open the object editor dialog', sub {
  2648. $self->object_settings_dialog;
  2649. }, undef, 'cog.png');
  2650. $menu->AppendSeparator();
  2651. wxTheApp->append_menu_item($menu, "Reload from Disk", 'Reload the selected file from Disk', sub {
  2652. $self->reload_from_disk;
  2653. }, undef, 'arrow_refresh.png');
  2654. wxTheApp->append_menu_item($menu, "Export object as STL…", 'Export this single object as STL file', sub {
  2655. $self->export_object_stl;
  2656. }, undef, 'brick_go.png');
  2657. wxTheApp->append_menu_item($menu, "Export object and modifiers as AMF…", 'Export this single object and all associated modifiers as AMF file', sub {
  2658. $self->export_object_amf;
  2659. }, undef, 'brick_go.png');
  2660. wxTheApp->append_menu_item($menu, "Export object and modifiers as 3MF…", 'Export this single object and all associated modifiers as 3MF file', sub {
  2661. $self->export_object_tmf;
  2662. }, undef, 'brick_go.png');
  2663. return $menu;
  2664. }
  2665. # Set a camera direction, zoom to all objects.
  2666. sub select_view {
  2667. my ($self, $direction) = @_;
  2668. my $idx_page = $self->{preview_notebook}->GetSelection;
  2669. my $page = ($idx_page == &Wx::wxNOT_FOUND) ? '3D' : $self->{preview_notebook}->GetPageText($idx_page);
  2670. if ($page eq 'Preview') {
  2671. $self->{preview3D}->canvas->select_view($direction);
  2672. $self->{canvas3D}->set_viewport_from_scene($self->{preview3D}->canvas);
  2673. } else {
  2674. $self->{canvas3D}->select_view($direction);
  2675. $self->{preview3D}->canvas->set_viewport_from_scene($self->{canvas3D});
  2676. }
  2677. }
  2678. sub zoom{
  2679. my ($self, $direction) = @_;
  2680. #Apply Zoom to the current active tab
  2681. my ($currentSelection) = $self->{preview_notebook}->GetSelection;
  2682. if($currentSelection == 0){
  2683. $self->{canvas3D}->zoom($direction) if($self->{canvas3D});
  2684. }
  2685. elsif($currentSelection == 2){ #3d Preview tab
  2686. $self->{preview3D}->canvas->zoom($direction) if($self->{preview3D});
  2687. }
  2688. elsif($currentSelection == 3) { #2D toolpaths tab
  2689. $self->{toolpaths2D}->{canvas}->zoom($direction) if($self->{toolpaths2D});
  2690. }
  2691. }
  2692. package Slic3r::GUI::Plater::DropTarget;
  2693. use Wx::DND;
  2694. use base 'Wx::FileDropTarget';
  2695. sub new {
  2696. my $class = shift;
  2697. my ($window) = @_;
  2698. my $self = $class->SUPER::new;
  2699. $self->{window} = $window;
  2700. return $self;
  2701. }
  2702. sub OnDropFiles {
  2703. my $self = shift;
  2704. my ($x, $y, $filenames) = @_;
  2705. # stop scalars leaking on older perl
  2706. # https://rt.perl.org/rt3/Public/Bug/Display.html?id=70602
  2707. @_ = ();
  2708. # only accept STL, OBJ and AMF files
  2709. return 0 if grep !/\.(?:stl|obj|amf(?:\.xml)?)$/i, @$filenames;
  2710. $self->{window}->load_file($_) for @$filenames;
  2711. }
  2712. # 2D preview of an object. Each object is previewed by its convex hull.
  2713. package Slic3r::GUI::Plater::Object;
  2714. use Moo;
  2715. use List::Util qw(first);
  2716. use Slic3r::Geometry qw(X Y Z MIN MAX deg2rad);
  2717. has 'name' => (is => 'rw', required => 1);
  2718. has 'identifier' => (is => 'rw', required => 1);
  2719. has 'input_file' => (is => 'rw');
  2720. has 'input_file_obj_idx' => (is => 'rw');
  2721. has 'thumbnail' => (is => 'rw'); # ExPolygon::Collection in scaled model units with no transforms
  2722. has 'transformed_thumbnail' => (is => 'rw');
  2723. has 'remaking_thumbnail' => (is => 'rw', default => sub { 0 });
  2724. has 'instance_thumbnails' => (is => 'ro', default => sub { [] }); # array of ExPolygon::Collection objects, each one representing the actual placed thumbnail of each instance in pixel units
  2725. has 'selected' => (is => 'rw', default => sub { 0 });
  2726. has 'selected_instance' => (is => 'rw', default => sub { -1 });
  2727. sub make_thumbnail {
  2728. my ($self, $model, $obj_idx) = @_;
  2729. # make method idempotent
  2730. $self->thumbnail->clear;
  2731. my $mesh = $model->objects->[$obj_idx]->raw_mesh;
  2732. # Apply x, y rotations and scaling vector in case of reading a 3MF model object.
  2733. my $model_instance = $model->objects->[$obj_idx]->instances->[0];
  2734. $mesh->rotate_x($model_instance->x_rotation);
  2735. $mesh->rotate_y($model_instance->y_rotation);
  2736. $mesh->scale_xyz($model_instance->scaling_vector);
  2737. if ($mesh->facets_count <= 5000) {
  2738. # remove polygons with area <= 1mm
  2739. my $area_threshold = Slic3r::Geometry::scale 1;
  2740. $self->thumbnail->append(
  2741. grep $_->area >= $area_threshold,
  2742. @{ $mesh->horizontal_projection }, # horizontal_projection returns scaled expolygons
  2743. );
  2744. $self->thumbnail->simplify(0.5);
  2745. } else {
  2746. my $convex_hull = Slic3r::ExPolygon->new($mesh->convex_hull);
  2747. $self->thumbnail->append($convex_hull);
  2748. }
  2749. return $self->thumbnail;
  2750. }
  2751. sub transform_thumbnail {
  2752. my ($self, $model, $obj_idx) = @_;
  2753. return unless defined $self->thumbnail;
  2754. my $model_object = $model->objects->[$obj_idx];
  2755. my $model_instance = $model_object->instances->[0];
  2756. # the order of these transformations MUST be the same everywhere, including
  2757. # in Slic3r::Print->add_model_object()
  2758. my $t = $self->thumbnail->clone;
  2759. $t->rotate($model_instance->rotation, Slic3r::Point->new(0,0));
  2760. $t->scale($model_instance->scaling_factor);
  2761. $self->transformed_thumbnail($t);
  2762. }
  2763. package Slic3r::GUI::Plater::OctoPrintSpoolDialog;
  2764. use Wx qw(:dialog :id :misc :sizer :icon wxTheApp);
  2765. use Wx::Event qw(EVT_BUTTON EVT_TEXT_ENTER);
  2766. use base 'Wx::Dialog';
  2767. sub new {
  2768. my $class = shift;
  2769. my ($parent, $filename) = @_;
  2770. my $self = $class->SUPER::new($parent, -1, "Send to Server", wxDefaultPosition,
  2771. [400, -1]);
  2772. $self->{filename} = $filename;
  2773. $Slic3r::GUI::Settings->{octoprint} //= {};
  2774. my $optgroup;
  2775. $optgroup = Slic3r::GUI::OptionsGroup->new(
  2776. parent => $self,
  2777. title => 'Send to Server',
  2778. on_change => sub {
  2779. my ($opt_id) = @_;
  2780. if ($opt_id eq 'filename') {
  2781. $self->{filename} = $optgroup->get_value($opt_id);
  2782. } else {
  2783. $Slic3r::GUI::Settings->{octoprint}{$opt_id} = $optgroup->get_value($opt_id);
  2784. }
  2785. },
  2786. label_width => 200,
  2787. );
  2788. $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  2789. opt_id => 'filename',
  2790. type => 's',
  2791. label => 'File name',
  2792. width => 200,
  2793. tooltip => 'The name used for labelling the print job.',
  2794. default => $filename,
  2795. ));
  2796. $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  2797. opt_id => 'overwrite',
  2798. type => 'bool',
  2799. label => 'Overwrite existing file',
  2800. tooltip => 'If selected, any existing file with the same name will be overwritten without confirmation.',
  2801. default => $Slic3r::GUI::Settings->{octoprint}{overwrite} // 0,
  2802. ));
  2803. $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  2804. opt_id => 'start',
  2805. type => 'bool',
  2806. label => 'Start print',
  2807. tooltip => 'If selected, print will start after the upload.',
  2808. default => $Slic3r::GUI::Settings->{octoprint}{start} // 0,
  2809. ));
  2810. my $sizer = Wx::BoxSizer->new(wxVERTICAL);
  2811. $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxTOP | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  2812. my $buttons = $self->CreateStdDialogButtonSizer(wxOK | wxCANCEL);
  2813. $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  2814. EVT_BUTTON($self, wxID_OK, sub {
  2815. wxTheApp->save_settings;
  2816. $self->EndModal(wxID_OK);
  2817. $self->Destroy;
  2818. });
  2819. $self->SetSizer($sizer);
  2820. $sizer->SetSizeHints($self);
  2821. return $self;
  2822. }
  2823. 1;