Projector.pm 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. # DLP Projector screen for the SLA (stereolitography) print process
  2. package Slic3r::GUI::Projector;
  3. use strict;
  4. use warnings;
  5. use File::Basename qw(basename dirname);
  6. use Wx qw(:dialog :id :misc :sizer :systemsettings :bitmap :button :icon :filedialog wxTheApp);
  7. use Wx::Event qw(EVT_BUTTON EVT_CLOSE EVT_TEXT_ENTER EVT_SPINCTRL EVT_SLIDER);
  8. use base qw(Wx::Dialog Class::Accessor);
  9. use utf8;
  10. __PACKAGE__->mk_accessors(qw(config config2 manual_control_config screen controller _optgroups));
  11. sub new {
  12. my ($class, $parent) = @_;
  13. my $self = $class->SUPER::new($parent, -1, "Projector for DLP", wxDefaultPosition, wxDefaultSize);
  14. $self->config2({
  15. display => 0,
  16. show_bed => 1,
  17. invert_y => 0,
  18. zoom => 100,
  19. exposure_time => 2,
  20. bottom_exposure_time => 7,
  21. settle_time => 1.5,
  22. bottom_layers => 3,
  23. z_lift => 5,
  24. z_lift_speed => 8,
  25. offset => [0,0],
  26. });
  27. $self->manual_control_config({
  28. xy_travel_speed => 130,
  29. z_travel_speed => 10,
  30. temperature => '',
  31. bed_temperature => '',
  32. });
  33. my $ini = eval { Slic3r::Config->read_ini("$Slic3r::GUI::datadir/DLP.ini") };
  34. if ($ini) {
  35. foreach my $opt_id (keys %{$ini->{_}}) {
  36. my $value = $ini->{_}{$opt_id};
  37. if ($opt_id eq 'offset') {
  38. $value = [ split /,/, $value ];
  39. }
  40. $self->config2->{$opt_id} = $value;
  41. }
  42. }
  43. my $sizer = Wx::BoxSizer->new(wxVERTICAL);
  44. $self->config(Slic3r::Config->new_from_defaults(
  45. qw(serial_port serial_speed bed_shape start_gcode end_gcode z_offset)
  46. ));
  47. $self->config->apply(wxTheApp->{mainframe}->{plater}->config);
  48. my @optgroups = ();
  49. {
  50. push @optgroups, my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
  51. parent => $self,
  52. title => 'USB/Serial connection',
  53. config => $self->config,
  54. label_width => 200,
  55. );
  56. $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  57. {
  58. my $line = Slic3r::GUI::OptionsGroup::Line->new(
  59. label => 'Serial port',
  60. );
  61. my $serial_port = $optgroup->get_option('serial_port');
  62. $serial_port->side_widget(sub {
  63. my ($parent) = @_;
  64. my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new($Slic3r::var->("arrow_rotate_clockwise.png"), wxBITMAP_TYPE_PNG),
  65. wxDefaultPosition, wxDefaultSize, &Wx::wxBORDER_NONE);
  66. $btn->SetToolTipString("Rescan serial ports")
  67. if $btn->can('SetToolTipString');
  68. EVT_BUTTON($self, $btn, sub {
  69. $optgroup->get_field('serial_port')->set_values([ wxTheApp->scan_serial_ports ]);
  70. });
  71. return $btn;
  72. });
  73. $line->append_option($serial_port);
  74. $line->append_option($optgroup->get_option('serial_speed'));
  75. $line->append_button("Test", "wrench.png", sub {
  76. my $sender = Slic3r::GCode::Sender->new;
  77. my $res = $sender->connect(
  78. $self->{config}->serial_port,
  79. $self->{config}->serial_speed,
  80. );
  81. if ($res && $sender->wait_connected) {
  82. Slic3r::GUI::show_info($self, "Connection to printer works correctly.", "Success!");
  83. } else {
  84. Slic3r::GUI::show_error($self, "Connection failed.");
  85. }
  86. }, \$self->{serial_test_btn});
  87. $optgroup->append_line($line);
  88. }
  89. }
  90. {
  91. push @optgroups, my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
  92. parent => $self,
  93. title => 'G-code',
  94. config => $self->config,
  95. label_width => 200,
  96. );
  97. $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  98. {
  99. my $option = $optgroup->get_option('start_gcode');
  100. $option->height(50);
  101. $option->full_width(1);
  102. $optgroup->append_single_option_line($option);
  103. }
  104. {
  105. my $option = $optgroup->get_option('end_gcode');
  106. $option->height(50);
  107. $option->full_width(1);
  108. $optgroup->append_single_option_line($option);
  109. }
  110. }
  111. my $on_change = sub {
  112. my ($opt_id, $value) = @_;
  113. $self->config2->{$opt_id} = $value;
  114. $self->screen->reposition;
  115. $self->show_print_time;
  116. my $serialized = {};
  117. foreach my $opt_id (keys %{$self->config2}) {
  118. my $value = $self->config2->{$opt_id};
  119. if (ref($value) eq 'ARRAY') {
  120. $value = join ',', @$value;
  121. }
  122. $serialized->{$opt_id} = $value;
  123. }
  124. Slic3r::Config->write_ini(
  125. "$Slic3r::GUI::datadir/DLP.ini",
  126. { _ => $serialized });
  127. };
  128. {
  129. push @optgroups, my $optgroup = Slic3r::GUI::OptionsGroup->new(
  130. parent => $self,
  131. title => 'Projection',
  132. on_change => $on_change,
  133. label_width => 200,
  134. );
  135. $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  136. {
  137. my $line = Slic3r::GUI::OptionsGroup::Line->new(
  138. label => 'Display',
  139. );
  140. my @displays = 0 .. (Wx::Display::GetCount()-1);
  141. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  142. opt_id => 'display',
  143. type => 'select',
  144. label => 'Display',
  145. tooltip => '',
  146. labels => [@displays],
  147. values => [@displays],
  148. default => $self->config2->{display},
  149. ));
  150. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  151. opt_id => 'zoom',
  152. type => 'percent',
  153. label => 'Zoom %',
  154. tooltip => '',
  155. default => $self->config2->{zoom},
  156. min => 0.1,
  157. max => 100,
  158. ));
  159. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  160. opt_id => 'offset',
  161. type => 'point',
  162. label => 'Offset',
  163. tooltip => '',
  164. default => $self->config2->{offset},
  165. ));
  166. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  167. opt_id => 'invert_y',
  168. type => 'bool',
  169. label => 'Invert Y',
  170. tooltip => '',
  171. default => $self->config2->{invert_y},
  172. ));
  173. $optgroup->append_line($line);
  174. }
  175. $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  176. opt_id => 'show_bed',
  177. type => 'bool',
  178. label => 'Show bed',
  179. tooltip => '',
  180. default => $self->config2->{show_bed},
  181. ));
  182. }
  183. {
  184. push @optgroups, my $optgroup = Slic3r::GUI::OptionsGroup->new(
  185. parent => $self,
  186. title => 'Print',
  187. on_change => $on_change,
  188. label_width => 200,
  189. );
  190. $sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  191. {
  192. my $line = Slic3r::GUI::OptionsGroup::Line->new(
  193. label => 'Time (seconds)',
  194. );
  195. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  196. opt_id => 'bottom_exposure_time',
  197. type => 'f',
  198. label => 'Bottom exposure',
  199. tooltip => '',
  200. default => $self->config2->{bottom_exposure_time},
  201. ));
  202. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  203. opt_id => 'exposure_time',
  204. type => 'f',
  205. label => 'Exposure',
  206. tooltip => '',
  207. default => $self->config2->{exposure_time},
  208. ));
  209. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  210. opt_id => 'settle_time',
  211. type => 'f',
  212. label => 'Settle',
  213. tooltip => '',
  214. default => $self->config2->{settle_time},
  215. ));
  216. $optgroup->append_line($line);
  217. }
  218. $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
  219. opt_id => 'bottom_layers',
  220. type => 'i',
  221. label => 'Bottom layers',
  222. tooltip => '',
  223. default => $self->config2->{bottom_layers},
  224. ));
  225. {
  226. my $line = Slic3r::GUI::OptionsGroup::Line->new(
  227. label => 'Z Lift',
  228. );
  229. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  230. opt_id => 'z_lift',
  231. type => 'f',
  232. label => 'Distance',
  233. sidetext => 'mm',
  234. tooltip => '',
  235. default => $self->config2->{z_lift},
  236. ));
  237. $line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
  238. opt_id => 'z_lift_speed',
  239. type => 'f',
  240. label => 'Speed',
  241. sidetext => 'mm/s',
  242. tooltip => '',
  243. default => $self->config2->{z_lift_speed},
  244. ));
  245. $optgroup->append_line($line);
  246. }
  247. }
  248. $self->_optgroups([@optgroups]);
  249. {
  250. my $sizer1 = Wx::BoxSizer->new(wxHORIZONTAL);
  251. $sizer->Add($sizer1, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  252. {
  253. my $btn = $self->{btn_manual_control} = Wx::Button->new($self, -1, 'Manual Control', wxDefaultPosition, wxDefaultSize);
  254. if ($Slic3r::GUI::have_button_icons) {
  255. $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("cog.png"), wxBITMAP_TYPE_PNG));
  256. }
  257. $sizer1->Add($btn, 0);
  258. EVT_BUTTON($self, $btn, sub {
  259. my $sender = Slic3r::GCode::Sender->new;
  260. my $res = $sender->connect(
  261. $self->config->serial_port,
  262. $self->config->serial_speed,
  263. );
  264. if (!$res || !$sender->wait_connected) {
  265. Slic3r::GUI::show_error(undef, "Connection failed. Check serial port and speed.");
  266. return;
  267. }
  268. my $dlg = Slic3r::GUI::Controller::ManualControlDialog->new
  269. ($self, $self->config, $sender, $self->manual_control_config);
  270. $dlg->ShowModal;
  271. $sender->disconnect;
  272. });
  273. }
  274. {
  275. my $btn = $self->{btn_print} = Wx::Button->new($self, -1, 'Print', wxDefaultPosition, wxDefaultSize);
  276. if ($Slic3r::GUI::have_button_icons) {
  277. $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("control_play.png"), wxBITMAP_TYPE_PNG));
  278. }
  279. $sizer1->Add($btn, 0);
  280. EVT_BUTTON($self, $btn, sub {
  281. $self->controller->start_print;
  282. $self->_update_buttons;
  283. $self->_set_status('');
  284. });
  285. }
  286. {
  287. my $btn = $self->{btn_stop} = Wx::Button->new($self, -1, 'Stop/Black', wxDefaultPosition, wxDefaultSize);
  288. if ($Slic3r::GUI::have_button_icons) {
  289. $btn->SetBitmap(Wx::Bitmap->new($Slic3r::var->("control_stop.png"), wxBITMAP_TYPE_PNG));
  290. }
  291. $sizer1->Add($btn, 0);
  292. EVT_BUTTON($self, $btn, sub {
  293. $self->controller->stop_print;
  294. $self->_update_buttons;
  295. $self->_set_status('');
  296. });
  297. }
  298. {
  299. {
  300. my $text = Wx::StaticText->new($self, -1, "Layer:", wxDefaultPosition, wxDefaultSize);
  301. $text->SetFont($Slic3r::GUI::small_font);
  302. $sizer1->Add($text, 0, wxEXPAND | wxLEFT, 10);
  303. }
  304. {
  305. my $spin = $self->{layers_spinctrl} = Wx::SpinCtrl->new($self, -1, 0, wxDefaultPosition, [60,-1],
  306. 0, 0, 300, 0);
  307. $sizer1->Add($spin, 0);
  308. EVT_SPINCTRL($self, $spin, sub {
  309. my $value = $spin->GetValue;
  310. $self->{layers_slider}->SetValue($value);
  311. $self->controller->project_layer($value);
  312. $self->_update_buttons;
  313. });
  314. }
  315. {
  316. my $slider = $self->{layers_slider} = Wx::Slider->new(
  317. $self, -1,
  318. 0, # default
  319. 0, # min
  320. 300, # max
  321. wxDefaultPosition,
  322. wxDefaultSize,
  323. );
  324. $sizer1->Add($slider, 1);
  325. EVT_SLIDER($self, $slider, sub {
  326. my $value = $slider->GetValue;
  327. $self->{layers_spinctrl}->SetValue($value);
  328. $self->controller->project_layer($value);
  329. $self->_update_buttons;
  330. });
  331. }
  332. }
  333. my $sizer2 = Wx::BoxSizer->new(wxHORIZONTAL);
  334. $sizer->Add($sizer2, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  335. {
  336. $self->{status_text} = Wx::StaticText->new($self, -1, "", wxDefaultPosition, wxDefaultSize);
  337. $self->{status_text}->SetFont($Slic3r::GUI::small_font);
  338. $sizer2->Add($self->{status_text}, 1 | wxEXPAND);
  339. }
  340. }
  341. {
  342. # should be wxCLOSE but it crashes on Linux, maybe it's a Wx bug
  343. my $buttons = Wx::BoxSizer->new(wxHORIZONTAL);
  344. {
  345. my $btn = Wx::Button->new($self, -1, "Export SVG…");
  346. EVT_BUTTON($self, $btn, sub {
  347. $self->_export_svg;
  348. });
  349. $buttons->Add($btn, 0);
  350. }
  351. $buttons->AddStretchSpacer(1);
  352. {
  353. my $btn = Wx::Button->new($self, -1, "Close");
  354. $btn->SetDefault;
  355. EVT_BUTTON($self, $btn, sub {
  356. $self->_close;
  357. });
  358. $buttons->Add($btn, 0);
  359. }
  360. $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
  361. }
  362. EVT_CLOSE($self, sub {
  363. $self->_close;
  364. });
  365. $self->SetSizer($sizer);
  366. $sizer->SetSizeHints($self);
  367. # reuse existing screen if any
  368. if ($Slic3r::GUI::DLP_projection_screen) {
  369. $self->screen($Slic3r::GUI::DLP_projection_screen);
  370. $self->screen->config($self->config);
  371. $self->screen->config2($self->config2);
  372. } else {
  373. $self->screen(Slic3r::GUI::Projector::Screen->new($parent, $self->config, $self->config2));
  374. $Slic3r::GUI::DLP_projection_screen = $self->screen;
  375. }
  376. $self->screen->reposition;
  377. $self->screen->Show;
  378. wxTheApp->{mainframe}->Hide;
  379. # initialize controller
  380. $self->controller(Slic3r::GUI::Projector::Controller->new(
  381. config => $self->config,
  382. config2 => $self->config2,
  383. screen => $self->screen,
  384. on_project_layer => sub {
  385. my ($layer_num) = @_;
  386. $self->{layers_spinctrl}->SetValue($layer_num);
  387. $self->{layers_slider}->SetValue($layer_num);
  388. my $duration = $self->controller->remaining_print_time;
  389. $self->_set_status(sprintf "Printing layer %d/%d (z = %.2f); %d minutes and %d seconds left",
  390. $layer_num, $self->controller->_print->layer_count,
  391. $self->controller->current_layer_height,
  392. int($duration/60), ($duration - int($duration/60)*60)); # % truncates to integer
  393. },
  394. on_print_completed => sub {
  395. $self->_update_buttons;
  396. $self->_set_status('');
  397. Wx::Bell();
  398. },
  399. ));
  400. {
  401. my $max = $self->controller->_print->layer_count-1;
  402. $self->{layers_spinctrl}->SetRange(0, $max);
  403. $self->{layers_slider}->SetRange(0, $max);
  404. }
  405. $self->_update_buttons;
  406. $self->show_print_time;
  407. return $self;
  408. }
  409. sub _update_buttons {
  410. my ($self) = @_;
  411. my $is_printing = $self->controller->is_printing;
  412. my $is_projecting = $self->controller->is_projecting;
  413. $self->{btn_manual_control}->Show(!$is_printing);
  414. $self->{btn_print}->Show(!$is_printing && !$is_projecting);
  415. $self->{btn_stop}->Show($is_printing || $is_projecting);
  416. $self->{layers_spinctrl}->Enable(!$is_printing);
  417. $self->{layers_slider}->Enable(!$is_printing);
  418. if ($is_printing) {
  419. $_->disable for @{$self->_optgroups};
  420. } else {
  421. $_->enable for @{$self->_optgroups};
  422. }
  423. $self->Layout;
  424. }
  425. sub _export_svg {
  426. my ($self) = @_;
  427. my $output_file = 'print.svg';
  428. my $dlg = Wx::FileDialog->new(
  429. $self,
  430. 'Save SVG file as:',
  431. wxTheApp->output_path(dirname($output_file)),
  432. basename($output_file),
  433. &Slic3r::GUI::FILE_WILDCARDS->{svg},
  434. wxFD_SAVE | wxFD_OVERWRITE_PROMPT,
  435. );
  436. if ($dlg->ShowModal != wxID_OK) {
  437. $dlg->Destroy;
  438. return;
  439. }
  440. $output_file = Slic3r::decode_path($dlg->GetPath);
  441. $self->controller->_print->write_svg($output_file);
  442. }
  443. sub _set_status {
  444. my ($self, $status) = @_;
  445. $self->{status_text}->SetLabel($status // '');
  446. $self->{status_text}->Wrap($self->{status_text}->GetSize->GetWidth);
  447. $self->{status_text}->Refresh;
  448. $self->Layout;
  449. }
  450. sub show_print_time {
  451. my ($self) = @_;
  452. my $duration = $self->controller->print_time;
  453. $self->_set_status(sprintf "Estimated print time: %d minutes and %d seconds - %.2f liters",
  454. int($duration/60), ($duration - int($duration/60)*60), # % truncates to integer
  455. $self->controller->total_resin);
  456. }
  457. sub _close {
  458. my $self = shift;
  459. # if projection screen is not on the same display as our dialog,
  460. # ask the user whether they want to keep it open
  461. my $keep_screen = 0;
  462. my $display_area = Wx::Display->new($self->config2->{display})->GetGeometry;
  463. if (!$display_area->Contains($self->GetScreenPosition)) {
  464. my $res = Wx::MessageDialog->new($self, "Do you want to keep the black screen open?", 'Black screen', wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION)->ShowModal;
  465. $keep_screen = ($res == wxID_YES);
  466. }
  467. if ($keep_screen) {
  468. $self->screen->config(undef);
  469. $self->screen->config2(undef);
  470. $self->screen->Refresh;
  471. } else {
  472. $self->screen->Destroy;
  473. $self->screen(undef);
  474. $Slic3r::GUI::DLP_projection_screen = undef;
  475. }
  476. wxTheApp->{mainframe}->Show;
  477. $self->EndModal(wxID_OK);
  478. }
  479. package Slic3r::GUI::Projector::Controller;
  480. use Moo;
  481. use Wx qw(wxTheApp :id :timer);
  482. use Wx::Event qw(EVT_TIMER);
  483. use Slic3r::Geometry qw(unscale);
  484. use Slic3r::Print::State ':steps';
  485. use Time::HiRes qw(gettimeofday tv_interval);
  486. has 'config' => (is => 'ro', required => 1);
  487. has 'config2' => (is => 'ro', required => 1);
  488. has 'screen' => (is => 'ro', required => 1);
  489. has 'on_project_layer' => (is => 'rw');
  490. has 'on_print_completed' => (is => 'rw');
  491. has 'sender' => (is => 'rw');
  492. has 'timer' => (is => 'rw');
  493. has 'is_printing' => (is => 'rw', default => sub { 0 });
  494. has '_print' => (is => 'rw');
  495. has '_heights' => (is => 'rw');
  496. has '_layer_num' => (is => 'rw');
  497. has '_timer_cb' => (is => 'rw');
  498. sub BUILD {
  499. my ($self) = @_;
  500. Slic3r::GUI::disable_screensaver();
  501. # init print
  502. {
  503. my $print = Slic3r::SLAPrint->new(wxTheApp->{mainframe}->{plater}->{model});
  504. $print->apply_config(wxTheApp->{mainframe}->{plater}->config);
  505. $self->_print($print);
  506. $self->screen->print($print);
  507. # make sure layers were sliced
  508. {
  509. my $progress_dialog = Wx::ProgressDialog->new('Slicing…', "Processing layers…", 100, undef, 0);
  510. $progress_dialog->Pulse;
  511. $print->slice;
  512. $progress_dialog->Destroy;
  513. }
  514. $self->_heights($print->heights);
  515. }
  516. # projection timer
  517. my $timer_id = &Wx::NewId();
  518. $self->timer(Wx::Timer->new($self->screen, $timer_id));
  519. EVT_TIMER($self->screen, $timer_id, sub {
  520. my $cb = $self->_timer_cb;
  521. $self->_timer_cb(undef);
  522. $cb->();
  523. });
  524. }
  525. sub delay {
  526. my ($self, $wait, $cb) = @_;
  527. $self->_timer_cb($cb);
  528. $self->timer->Start($wait * 1000, wxTIMER_ONE_SHOT);
  529. }
  530. sub current_layer_height {
  531. my ($self) = @_;
  532. return $self->_heights->[$self->_layer_num];
  533. }
  534. sub start_print {
  535. my ($self) = @_;
  536. {
  537. $self->sender(Slic3r::GCode::Sender->new);
  538. my $res = $self->sender->connect(
  539. $self->config->serial_port,
  540. $self->config->serial_speed,
  541. );
  542. if (!$res || !$self->sender->wait_connected) {
  543. Slic3r::GUI::show_error(undef, "Connection failed. Check serial port and speed.");
  544. return;
  545. }
  546. Slic3r::debugf "connected to " . $self->config->serial_port . "\n";
  547. # TODO: this wait should be handled by GCodeSender
  548. sleep 4;
  549. # send custom start G-code
  550. $self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->start_gcode;
  551. $self->sender->("G90", 1); # set absolute positioning
  552. }
  553. $self->is_printing(1);
  554. # TODO: block until the G1 command has been performed
  555. # we could do this with M400 + M115 but maybe it's not portable
  556. $self->delay(5, sub {
  557. # start with black
  558. Slic3r::debugf "starting black projection\n";
  559. $self->_layer_num(-1);
  560. $self->screen->project_layer(undef);
  561. $self->delay($self->config2->{settle_time}, sub {
  562. $self->project_next_layer;
  563. });
  564. });
  565. }
  566. sub stop_print {
  567. my ($self) = @_;
  568. if ($self->sender) {
  569. $self->sender->disconnect;
  570. }
  571. $self->is_printing(0);
  572. $self->timer->Stop;
  573. $self->_timer_cb(undef);
  574. $self->screen->project_layer(undef);
  575. }
  576. sub print_completed {
  577. my ($self) = @_;
  578. # send custom end G-code
  579. if ($self->sender) {
  580. $self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->end_gcode;
  581. }
  582. # call this before the on_print_completed callback otherwise buttons
  583. # won't be updated correctly
  584. $self->stop_print;
  585. $self->on_print_completed->()
  586. if $self->is_printing && $self->on_print_completed;
  587. }
  588. sub is_projecting {
  589. my ($self) = @_;
  590. return defined $self->screen->layer_num;
  591. }
  592. sub project_layer {
  593. my ($self, $layer_num) = @_;
  594. if (!defined $layer_num || $layer_num >= $self->_print->layer_count) {
  595. $self->screen->project_layer(undef);
  596. return;
  597. }
  598. $self->screen->project_layer($layer_num);
  599. }
  600. sub project_next_layer {
  601. my ($self) = @_;
  602. $self->_layer_num($self->_layer_num + 1);
  603. Slic3r::debugf "projecting layer %d\n", $self->_layer_num;
  604. if ($self->_layer_num >= $self->_print->layer_count) {
  605. $self->print_completed;
  606. return;
  607. }
  608. $self->on_project_layer->($self->_layer_num) if $self->on_project_layer;
  609. if ($self->sender) {
  610. my $z = $self->current_layer_height + $self->config->z_offset;
  611. my $F = $self->config2->{z_lift_speed} * 60;
  612. if ($self->config2->{z_lift} != 0) {
  613. $self->sender->send(sprintf("G1 Z%.5f F%d", $z + $self->config2->{z_lift}, $F), 1);
  614. }
  615. $self->sender->send(sprintf("G1 Z%.5f F%d", $z, $F), 1);
  616. }
  617. # TODO: we should block until G1 commands have been performed, see note below
  618. $self->delay($self->config2->{settle_time}, sub {
  619. $self->project_layer($self->_layer_num);
  620. # get exposure time
  621. my $time = $self->config2->{exposure_time};
  622. if ($self->_layer_num < $self->config2->{bottom_layers}) {
  623. $time = $self->config2->{bottom_exposure_time};
  624. }
  625. $self->delay($time, sub {
  626. $self->screen->project_layer(undef);
  627. $self->project_next_layer;
  628. });
  629. });
  630. }
  631. sub remaining_print_time {
  632. my ($self) = @_;
  633. my $remaining_layers = @{$self->_heights} - $self->_layer_num;
  634. my $remaining_bottom_layers = $self->_layer_num >= $self->config2->{bottom_layers}
  635. ? 0
  636. : $self->config2->{bottom_layers} - $self->_layer_num;
  637. return $remaining_bottom_layers * $self->config2->{bottom_exposure_time}
  638. + ($remaining_layers - $remaining_bottom_layers) * $self->config2->{exposure_time}
  639. + $remaining_layers * $self->config2->{settle_time};
  640. }
  641. sub print_time {
  642. my ($self) = @_;
  643. return $self->config2->{bottom_layers} * $self->config2->{bottom_exposure_time}
  644. + ($self->_print->layer_count - $self->config2->{bottom_layers}) * $self->config2->{exposure_time}
  645. + $self->_print->layer_count * $self->config2->{settle_time};
  646. }
  647. sub total_resin {
  648. my ($self) = @_;
  649. my $vol = 0; # mm^3
  650. for my $i (0..($self->_print->layer_count-1)) {
  651. my $lh = $self->_heights->[$i] - ($i == 0 ? 0 : $self->_heights->[$i-1]);
  652. $vol += unscale(unscale($_->area)) * $lh for @{ $self->_print->layer_slices($i) };
  653. }
  654. return $vol/1000/1000; # liters
  655. }
  656. sub DESTROY {
  657. my ($self) = @_;
  658. $self->timer->Stop if $self->timer;
  659. $self->sender->disconnect if $self->sender;
  660. Slic3r::GUI::enable_screensaver();
  661. }
  662. package Slic3r::GUI::Projector::Screen;
  663. use Wx qw(:dialog :id :misc :sizer :colour :pen :brush :font wxBG_STYLE_CUSTOM);
  664. use Wx::Event qw(EVT_PAINT EVT_SIZE);
  665. use base qw(Wx::Dialog Class::Accessor);
  666. use List::Util qw(min);
  667. use Slic3r::Geometry qw(X Y unscale scale);
  668. use Slic3r::Geometry::Clipper qw(intersection_pl union_ex);
  669. __PACKAGE__->mk_accessors(qw(config config2 scaling_factor bed_origin print layer_num));
  670. sub new {
  671. my ($class, $parent, $config, $config2) = @_;
  672. my $self = $class->SUPER::new($parent, -1, "Projector", wxDefaultPosition, wxDefaultSize, 0);
  673. $self->config($config);
  674. $self->config2($config2);
  675. $self->SetBackgroundStyle(wxBG_STYLE_CUSTOM);
  676. EVT_SIZE($self, \&_resize);
  677. EVT_PAINT($self, \&_repaint);
  678. $self->_resize;
  679. return $self;
  680. }
  681. sub reposition {
  682. my ($self) = @_;
  683. my $display = Wx::Display->new($self->config2->{display});
  684. my $area = $display->GetGeometry;
  685. $self->Move($area->GetPosition);
  686. # ShowFullScreen doesn't use the right screen
  687. #$self->ShowFullScreen($self->config2->{fullscreen});
  688. $self->SetSize($area->GetSize);
  689. $self->_resize;
  690. $self->Refresh;
  691. }
  692. sub _resize {
  693. my ($self) = @_;
  694. return if !$self->config;
  695. my ($cw, $ch) = $self->GetSizeWH;
  696. # get bed shape polygon
  697. my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
  698. my $bb = $bed_polygon->bounding_box;
  699. my $size = $bb->size;
  700. my $center = $bb->center;
  701. # calculate the scaling factor needed for constraining print bed area inside preview
  702. # scaling_factor is expressed in pixel / mm
  703. $self->scaling_factor(min($cw / unscale($size->x), $ch / unscale($size->y))); #)
  704. # apply zoom to scaling factor
  705. if ($self->config2->{zoom} != 0) {
  706. # TODO: make sure min and max in the option config are enforced
  707. $self->scaling_factor($self->scaling_factor * ($self->config2->{zoom}/100));
  708. }
  709. # calculate the displacement needed for centering bed on screen
  710. $self->bed_origin([
  711. $cw/2 - (unscale($center->x) - $self->config2->{offset}->[X]) * $self->scaling_factor,
  712. $ch/2 - (unscale($center->y) - $self->config2->{offset}->[Y]) * $self->scaling_factor, #))
  713. ]);
  714. $self->Refresh;
  715. }
  716. sub project_layer {
  717. my ($self, $layer_num) = @_;
  718. $self->layer_num($layer_num);
  719. $self->Refresh;
  720. }
  721. sub _repaint {
  722. my ($self) = @_;
  723. my $dc = Wx::AutoBufferedPaintDC->new($self);
  724. my ($cw, $ch) = $self->GetSizeWH;
  725. return if $cw == 0; # when canvas is not rendered yet, size is 0,0
  726. $dc->SetPen(Wx::Pen->new(wxBLACK, 1, wxSOLID));
  727. $dc->SetBrush(Wx::Brush->new(wxBLACK, wxSOLID));
  728. $dc->DrawRectangle(0, 0, $cw, $ch);
  729. return if !$self->config;
  730. # turn size into max visible coordinates
  731. # TODO: or should we use ClientArea?
  732. $cw--;
  733. $ch--;
  734. # draw bed
  735. if ($self->config2->{show_bed}) {
  736. $dc->SetPen(Wx::Pen->new(wxRED, 2, wxSOLID));
  737. $dc->SetBrush(Wx::Brush->new(wxWHITE, wxTRANSPARENT));
  738. # draw contour
  739. my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
  740. $dc->DrawPolygon($self->scaled_points_to_pixel($bed_polygon), 0, 0);
  741. # draw grid
  742. $dc->SetPen(Wx::Pen->new(wxRED, 1, wxSOLID));
  743. {
  744. my $bb = $bed_polygon->bounding_box;
  745. my $step = scale 10; # 1cm grid
  746. my @polylines = ();
  747. for (my $x = $bb->x_min - ($bb->x_min % $step) + $step; $x < $bb->x_max; $x += $step) {
  748. push @polylines, Slic3r::Polyline->new([$x, $bb->y_min], [$x, $bb->y_max]);
  749. }
  750. for (my $y = $bb->y_min - ($bb->y_min % $step) + $step; $y < $bb->y_max; $y += $step) {
  751. push @polylines, Slic3r::Polyline->new([$bb->x_min, $y], [$bb->x_max, $y]);
  752. }
  753. $dc->DrawLine(map @$_, @$_)
  754. for map $self->scaled_points_to_pixel([ @$_[0,-1] ]),
  755. @{intersection_pl(\@polylines, [$bed_polygon])};
  756. }
  757. # draw axes orientation
  758. $dc->SetPen(Wx::Pen->new(wxWHITE, 4, wxSOLID));
  759. {
  760. foreach my $endpoint ([10, 0], [0, 10]) {
  761. $dc->DrawLine(
  762. map @{$self->unscaled_point_to_pixel($_)}, [0,0], $endpoint
  763. );
  764. }
  765. $dc->SetTextForeground(wxWHITE);
  766. $dc->SetFont(Wx::Font->new(20, wxDEFAULT, wxNORMAL, wxNORMAL));
  767. my $p = $self->unscaled_point_to_pixel([10, 0]);
  768. $dc->DrawText("X", $p->[X], $p->[Y]);
  769. $p = $self->unscaled_point_to_pixel([0, 10]);
  770. $dc->DrawText("Y", $p->[X]-20, $p->[Y]-10);
  771. }
  772. }
  773. # get layers at this height
  774. # draw layers
  775. $dc->SetPen(Wx::Pen->new(wxWHITE, 1, wxSOLID));
  776. return if !$self->print || !defined $self->layer_num;
  777. if ($self->print->layer_solid($self->layer_num)) {
  778. $self->_paint_expolygon($_, $dc) for @{$self->print->layer_slices($self->layer_num)};
  779. } else {
  780. # perimeters first, because their "hole" is painted black
  781. $self->_paint_expolygon($_, $dc) for
  782. @{$self->print->layer_perimeters($self->layer_num)},
  783. @{$self->print->layer_solid_infill($self->layer_num)};
  784. $self->_paint_expolygon($_, $dc)
  785. for @{union_ex($self->print->layer_infill($self->layer_num)->grow)};
  786. }
  787. # draw support material
  788. my $sm_radius = $self->print->config->get_abs_value_over('support_material_extrusion_width', $self->print->config->layer_height)/2;
  789. $dc->SetBrush(Wx::Brush->new(wxWHITE, wxSOLID));
  790. foreach my $pillar (@{$self->print->sm_pillars}) {
  791. next unless $pillar->{top_layer} >= $self->layer_num
  792. && $pillar->{bottom_layer} <= $self->layer_num;
  793. my $radius = min(
  794. $sm_radius,
  795. ($pillar->{top_layer} - $self->layer_num + 1) * $self->print->config->layer_height,
  796. );
  797. $dc->DrawCircle(
  798. @{$self->scaled_points_to_pixel([$pillar->{point}])->[0]},
  799. $radius * $self->scaling_factor,
  800. );
  801. }
  802. }
  803. sub _paint_expolygon {
  804. my ($self, $expolygon, $dc) = @_;
  805. my @polygons = sort { $a->contains_point($b->first_point) ? -1 : 1 } @$expolygon;
  806. $self->_paint_polygon($_, $dc) for @polygons;
  807. }
  808. sub _paint_polygon {
  809. my ($self, $polygon, $dc) = @_;
  810. if ($polygon->is_counter_clockwise) {
  811. $dc->SetBrush(Wx::Brush->new(wxWHITE, wxSOLID));
  812. } else {
  813. $dc->SetBrush(Wx::Brush->new(wxBLACK, wxSOLID));
  814. }
  815. $dc->DrawPolygon($self->scaled_points_to_pixel($polygon->pp), 0, 0);
  816. }
  817. # convert a model coordinate into a pixel coordinate
  818. sub unscaled_point_to_pixel {
  819. my ($self, $point) = @_;
  820. my $zero = $self->bed_origin;
  821. my $p = [
  822. $point->[X] * $self->scaling_factor + $zero->[X],
  823. $point->[Y] * $self->scaling_factor + $zero->[Y],
  824. ];
  825. if (!$self->config2->{invert_y}) {
  826. my $ch = $self->GetSize->GetHeight;
  827. $p->[Y] = $ch - $p->[Y];
  828. }
  829. return $p;
  830. }
  831. sub scaled_points_to_pixel {
  832. my ($self, $points) = @_;
  833. return [
  834. map $self->unscaled_point_to_pixel($_),
  835. map Slic3r::Pointf->new_unscale(@$_),
  836. @$points
  837. ];
  838. }
  839. 1;