Plater.pm 120 KB

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