PrusaSlicer-NonPlainar/lib/Slic3r/GUI/Projector.pm

718 lines
23 KiB
Perl

package Slic3r::GUI::Projector;
use strict;
use warnings;
use Wx qw(:dialog :id :misc :sizer :systemsettings :bitmap :button :icon wxTheApp);
use Wx::Event qw(EVT_BUTTON EVT_TEXT_ENTER);
use base qw(Wx::Dialog Class::Accessor);
__PACKAGE__->mk_accessors(qw(config config2 screen controller));
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, "Projector for DLP", wxDefaultPosition, wxDefaultSize);
$self->config2({
display => 0,
show_bed => 1,
zoom => 100,
exposure_time => 2,
bottom_exposure_time => 7,
settle_time => 1.5,
bottom_layers => 3,
z_lift => 5,
z_lift_speed => 8,
offset => [0,0],
});
my $ini = eval { Slic3r::Config->read_ini("$Slic3r::GUI::datadir/DLP.ini") };
if ($ini) {
foreach my $opt_id (keys %{$ini->{_}}) {
my $value = $ini->{_}{$opt_id};
if ($opt_id eq 'offset') {
$value = [ split /,/, $value ];
}
$self->config2->{$opt_id} = $value;
}
}
my $sizer = Wx::BoxSizer->new(wxVERTICAL);
$self->config(Slic3r::Config->new_from_defaults(
qw(serial_port serial_speed bed_shape start_gcode end_gcode)
));
$self->config->apply(wxTheApp->{mainframe}->config);
{
my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
parent => $self,
title => 'USB/Serial connection',
config => $self->config,
label_width => 200,
);
$sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
{
my $line = Slic3r::GUI::OptionsGroup::Line->new(
label => 'Serial port',
);
my $serial_port = $optgroup->get_option('serial_port');
$serial_port->side_widget(sub {
my ($parent) = @_;
my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/arrow_rotate_clockwise.png", wxBITMAP_TYPE_PNG),
wxDefaultPosition, wxDefaultSize, &Wx::wxBORDER_NONE);
$btn->SetToolTipString("Rescan serial ports")
if $btn->can('SetToolTipString');
EVT_BUTTON($self, $btn, sub {
$optgroup->get_field('serial_port')->set_values([ wxTheApp->scan_serial_ports ]);
});
return $btn;
});
my $serial_test = sub {
my ($parent) = @_;
my $btn = $self->{serial_test_btn} = Wx::Button->new($parent, -1,
"Test", wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBU_EXACTFIT);
$btn->SetFont($Slic3r::GUI::small_font);
if ($Slic3r::GUI::have_button_icons) {
$btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/wrench.png", wxBITMAP_TYPE_PNG));
}
EVT_BUTTON($self, $btn, sub {
my $sender = Slic3r::GCode::Sender->new;
my $res = $sender->connect(
$self->{config}->serial_port,
$self->{config}->serial_speed,
);
if ($res && $sender->wait_connected) {
Slic3r::GUI::show_info($self, "Connection to printer works correctly.", "Success!");
} else {
Slic3r::GUI::show_error($self, "Connection failed.");
}
});
return $btn;
};
$line->append_option($serial_port);
$line->append_option($optgroup->get_option('serial_speed'));
$line->append_widget($serial_test);
$optgroup->append_line($line);
}
}
{
my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
parent => $self,
title => 'G-code',
config => $self->config,
label_width => 200,
);
$sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
{
my $option = $optgroup->get_option('start_gcode');
$option->height(50);
$option->full_width(1);
$optgroup->append_single_option_line($option);
}
{
my $option = $optgroup->get_option('end_gcode');
$option->height(50);
$option->full_width(1);
$optgroup->append_single_option_line($option);
}
}
my $on_change = sub {
my ($opt_id, $value) = @_;
$self->config2->{$opt_id} = $value;
$self->position_screen;
my $serialized = {};
foreach my $opt_id (keys %{$self->config2}) {
my $value = $self->config2->{$opt_id};
if (ref($value) eq 'ARRAY') {
$value = join ',', @$value;
}
$serialized->{$opt_id} = $value;
}
Slic3r::Config->write_ini(
"$Slic3r::GUI::datadir/DLP.ini",
{ _ => $serialized });
};
{
my $optgroup = Slic3r::GUI::OptionsGroup->new(
parent => $self,
title => 'Projection',
on_change => $on_change,
label_width => 200,
);
$sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
{
my $line = Slic3r::GUI::OptionsGroup::Line->new(
label => 'Display',
);
my @displays = 0 .. (Wx::Display::GetCount()-1);
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'display',
type => 'select',
label => 'Display',
tooltip => '',
labels => [@displays],
values => [@displays],
default => $self->config2->{display},
));
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'zoom',
type => 'percent',
label => 'Zoom %',
tooltip => '',
default => $self->config2->{zoom},
min => 0.1,
max => 100,
));
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'offset',
type => 'point',
label => 'Offset',
tooltip => '',
default => $self->config2->{offset},
));
$optgroup->append_line($line);
}
$optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'show_bed',
type => 'bool',
label => 'Show bed',
tooltip => '',
default => $self->config2->{show_bed},
));
}
{
my $optgroup = Slic3r::GUI::OptionsGroup->new(
parent => $self,
title => 'Print',
on_change => $on_change,
label_width => 200,
);
$sizer->Add($optgroup->sizer, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
{
my $line = Slic3r::GUI::OptionsGroup::Line->new(
label => 'Time (seconds)',
);
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'bottom_exposure_time',
type => 'f',
label => 'Bottom exposure',
tooltip => '',
default => $self->config2->{bottom_exposure_time},
));
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'exposure_time',
type => 'f',
label => 'Exposure',
tooltip => '',
default => $self->config2->{exposure_time},
));
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'settle_time',
type => 'f',
label => 'Settle',
tooltip => '',
default => $self->config2->{settle_time},
));
$optgroup->append_line($line);
}
$optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'bottom_layers',
type => 'i',
label => 'Bottom layers',
tooltip => '',
default => $self->config2->{bottom_layers},
));
{
my $line = Slic3r::GUI::OptionsGroup::Line->new(
label => 'Z Lift',
);
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'z_lift',
type => 'f',
label => 'Distance',
sidetext => 'mm',
tooltip => '',
default => $self->config2->{z_lift},
));
$line->append_option(Slic3r::GUI::OptionsGroup::Option->new(
opt_id => 'z_lift_speed',
type => 'f',
label => 'Speed',
sidetext => 'mm/s',
tooltip => '',
default => $self->config2->{z_lift_speed},
));
$optgroup->append_line($line);
}
}
{
my $buttons = Wx::BoxSizer->new(wxHORIZONTAL);
$sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
{
my $btn = $self->{btn_print} = Wx::Button->new($self, -1, 'Print', wxDefaultPosition, wxDefaultSize);
if ($Slic3r::GUI::have_button_icons) {
$btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/control_play.png", wxBITMAP_TYPE_PNG));
}
$buttons->Add($btn, 0);
EVT_BUTTON($self, $btn, sub {
$self->controller->start_print;
$self->_update_buttons;
$self->_set_status('');
});
}
{
my $btn = $self->{btn_stop} = Wx::Button->new($self, -1, 'Stop', wxDefaultPosition, wxDefaultSize);
if ($Slic3r::GUI::have_button_icons) {
$btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/control_stop.png", wxBITMAP_TYPE_PNG));
}
$buttons->Add($btn, 0);
EVT_BUTTON($self, $btn, sub {
$self->controller->stop_print;
$self->_update_buttons;
$self->_set_status('');
});
}
$self->{status_text} = Wx::StaticText->new($self, -1, "", wxDefaultPosition, [200,-1]);
$buttons->Add($self->{status_text}, 0);
}
{
my $buttons = $self->CreateStdDialogButtonSizer(wxCLOSE);
EVT_BUTTON($self, wxID_CLOSE, sub {
$self->_close;
});
$sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
}
$self->SetSizer($sizer);
$sizer->SetSizeHints($self);
# reuse existing screen if any
if ($Slic3r::GUI::DLP_projection_screen) {
$self->screen($Slic3r::GUI::DLP_projection_screen);
$self->screen->config($self->config);
$self->screen->config2($self->config2);
} else {
$self->screen(Slic3r::GUI::Projector::Screen->new($parent, $self->config, $self->config2));
$Slic3r::GUI::DLP_projection_screen = $self->screen;
}
$self->position_screen;
$self->screen->Show;
wxTheApp->{mainframe}->Hide;
# initialize controller
$self->controller(Slic3r::GUI::Projector::Controller->new(
config => $self->config,
config2 => $self->config2,
screen => $self->screen,
on_project_layer => sub {
my ($layer_num) = @_;
$self->_set_status(sprintf "Printing layer %d/%d (z = %.2f)",
$layer_num, $self->controller->layer_count,
$self->controller->current_layer_height);
},
on_print_completed => sub {
$self->_update_buttons;
$self->_set_status('');
},
));
$self->_update_buttons;
return $self;
}
sub _update_buttons {
my ($self) = @_;
my $is_printing = $self->controller->is_printing;
$self->{btn_print}->Show(!$is_printing);
$self->{btn_stop}->Show($is_printing);
$self->Layout;
}
sub _set_status {
my ($self, $status) = @_;
$self->{status_text}->SetLabel($status // '');
$self->{status_text}->Wrap($self->{status_text}->GetSize->GetWidth);
$self->{status_text}->Refresh;
$self->Layout;
}
sub position_screen {
my ($self) = @_;
my $display = Wx::Display->new($self->config2->{display});
my $area = $display->GetGeometry;
$self->screen->Move($area->GetPosition);
# ShowFullScreen doesn't use the right screen
#$self->screen->ShowFullScreen($self->config2->{fullscreen});
$self->screen->SetSize($area->GetSize);
$self->screen->_resize;
$self->screen->Refresh;
}
sub _close {
my $self = shift;
# if projection screen is not on the same display as our dialog,
# ask the user whether they want to keep it open
my $keep_screen = 0;
my $display_area = Wx::Display->new($self->config2->{display})->GetGeometry;
if (!$display_area->Contains($self->GetScreenPosition)) {
my $res = Wx::MessageDialog->new($self, "Do you want to keep the black screen open?", 'Black screen', wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION)->ShowModal;
$keep_screen = ($res == wxID_YES);
}
if ($keep_screen) {
$self->screen->config(undef);
$self->screen->config2(undef);
$self->screen->Refresh;
} else {
$self->screen->Destroy;
$self->screen(undef);
$Slic3r::GUI::DLP_projection_screen = undef;
}
wxTheApp->{mainframe}->Show;
$self->EndModal(wxID_OK);
}
package Slic3r::GUI::Projector::Controller;
use Moo;
use Wx qw(wxTheApp :id :timer);
use Wx::Event qw(EVT_TIMER);
use Slic3r::Print::State ':steps';
has 'config' => (is => 'ro', required => 1);
has 'config2' => (is => 'ro', required => 1);
has 'screen' => (is => 'ro', required => 1);
has 'on_project_layer' => (is => 'rw');
has 'on_print_completed' => (is => 'rw');
has 'sender' => (is => 'rw');
has 'timer' => (is => 'rw');
has '_print' => (is => 'rw');
has '_layers' => (is => 'rw');
has '_heights' => (is => 'rw');
has '_layer_num' => (is => 'rw');
has '_timer_cb' => (is => 'rw');
sub BUILD {
my ($self) = @_;
$self->set_print(wxTheApp->{mainframe}->{plater}->{print});
# projection timer
my $timer_id = &Wx::NewId();
$self->timer(Wx::Timer->new($self->screen, $timer_id));
EVT_TIMER($self->screen, $timer_id, sub {
my $cb = $self->_timer_cb;
$self->_timer_cb(undef);
$cb->();
});
}
sub delay {
my ($self, $wait, $cb) = @_;
$self->_timer_cb($cb);
$self->timer->Start($wait * 1000, wxTIMER_ONE_SHOT);
}
sub is_printing {
my ($self) = @_;
return $self->timer->IsRunning;
}
sub set_print {
my ($self, $print) = @_;
# make sure layers were sliced
{
my $progress_dialog;
foreach my $object (@{$print->objects}) {
next if $object->step_done(STEP_SLICE);
$progress_dialog //= Wx::ProgressDialog->new('Slicing…', "Processing layers…", 100, undef, 0);
$progress_dialog->Pulse;
$object->slice;
}
$progress_dialog->Destroy if $progress_dialog;
}
$self->_print($print);
# sort layers by Z
my %layers = ();
foreach my $layer (map { @{$_->layers}, @{$_->support_layers} } @{$print->objects}) {
my $height = $layer->print_z;
$layers{$height} //= [];
push @{$layers{$height}}, $layer;
}
$self->_layers({ %layers });
$self->_heights([ sort { $a <=> $b } keys %layers ]);
}
sub layer_count {
my ($self) = @_;
return scalar @{$self->_heights};
}
sub current_layer_height {
my ($self) = @_;
return $self->_heights->[$self->_layer_num];
}
sub start_print {
my ($self) = @_;
{
$self->sender(Slic3r::GCode::Sender->new);
my $res = $self->sender->connect(
$self->config->serial_port,
$self->config->serial_speed,
);
if (!$res || !$self->sender->wait_connected) {
Slic3r::GUI::show_error(undef, "Connection failed. Check serial port and speed.");
return;
}
Slic3r::debugf "connected to " . $self->config->serial_port . "\n";
# send custom start G-code
$self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->start_gcode;
$self->sender->send(sprintf("G1 Z%.5f F%d",
$self->config2->{z_lift}, $self->config2->{z_lift_speed}*60), 1);
}
# TODO: block until the G1 command has been performed
# we could do this with M400 + M115 but maybe it's not portable
$self->delay(2, sub {
# start with black
Slic3r::debugf "starting black projection\n";
$self->_layer_num(-1);
$self->screen->project_layers(undef);
$self->delay($self->config2->{settle_time}, sub {
$self->project_next_layer;
});
});
}
sub stop_print {
my ($self) = @_;
$self->timer->Stop;
$self->_timer_cb(undef);
$self->screen->project_layers(undef);
# send custom end G-code
$self->sender->send($_, 1) for grep !/^;/, split /\n/, $self->config->end_gcode;
$self->sender->disconnect if $self->sender;
}
sub project_next_layer {
my ($self) = @_;
Slic3r::debugf "projecting next layer\n";
$self->_layer_num($self->_layer_num + 1);
if ($self->_layer_num >= $self->layer_count) {
$self->on_print_completed->() if $self->on_print_completed;
return;
}
$self->on_project_layer->($self->_layer_num) if $self->on_project_layer;
if ($self->sender) {
my $z = $self->current_layer_height;
my $F = $self->config2->{z_lift_speed} * 60;
if ($self->config2->{z_lift} != 0) {
$self->sender->send(sprintf("G1 Z%.5f F%d", $z + $self->config2->{z_lift}, $F), 1);
}
$self->sender->send(sprintf("G1 Z%.5f F%d", $z, $F), 1);
}
# TODO: we should block until G1 commands have been performed, see note below
# TODO: subtract this waiting time from the settle_time
$self->delay(2, sub {
my @layers = @{ $self->_layers->{ $self->_heights->[$self->_layer_num] } };
$self->screen->project_layers([ @layers ]);
# get exposure time
my $time = $self->config2->{exposure_time};
if ($self->_layer_num < $self->config2->{bottom_layers}) {
$time = $self->config2->{bottom_exposure_time};
}
$self->delay($time, sub {
$self->settle;
});
});
}
sub settle {
my ($self) = @_;
Slic3r::debugf "settling\n";
$self->screen->project_layers(undef);
$self->delay($self->config2->{settle_time}, sub {
$self->project_next_layer;
});
}
sub DESTROY {
my ($self) = @_;
$self->timer->Stop if $self->timer;
}
package Slic3r::GUI::Projector::Screen;
use Wx qw(:dialog :id :misc :sizer :colour :pen :brush);
use Wx::Event qw(EVT_PAINT EVT_SIZE);
use base qw(Wx::Dialog Class::Accessor);
use List::Util qw(min);
use Slic3r::Geometry qw(X Y unscale scale);
__PACKAGE__->mk_accessors(qw(config config2 scaling_factor bed_origin layers));
sub new {
my ($class, $parent, $config, $config2) = @_;
my $self = $class->SUPER::new($parent, -1, "Projector", wxDefaultPosition, wxDefaultSize, 0);
$self->config($config);
$self->config2($config2);
EVT_SIZE($self, \&_resize);
EVT_PAINT($self, \&_repaint);
$self->_resize;
return $self;
}
sub _resize {
my ($self) = @_;
return if !$self->config;
my ($cw, $ch) = $self->GetSizeWH;
# get bed shape polygon
my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
my $bb = $bed_polygon->bounding_box;
my $size = $bb->size;
my $center = $bb->center;
# calculate the scaling factor needed for constraining print bed area inside preview
# scaling_factor is expressed in pixel / mm
$self->scaling_factor(min($cw / unscale($size->x), $ch / unscale($size->y))); #)
# apply zoom to scaling factor
if ($self->config2->{zoom} != 0) {
# TODO: make sure min and max in the option config are enforced
$self->scaling_factor($self->scaling_factor * ($self->config2->{zoom}/100));
}
# calculate the displacement needed for centering bed on screen
$self->bed_origin([
$cw/2 - (unscale($center->x) - $self->config2->{offset}->[X]) * $self->scaling_factor,
$ch/2 - (unscale($center->y) - $self->config2->{offset}->[Y]) * $self->scaling_factor, #))
]);
$self->Refresh;
}
sub project_layers {
my ($self, $layers) = @_;
$self->layers($layers);
$self->Refresh;
}
sub _repaint {
my ($self) = @_;
my $dc = Wx::PaintDC->new($self);
my ($cw, $ch) = $self->GetSizeWH;
return if $cw == 0; # when canvas is not rendered yet, size is 0,0
$dc->SetPen(Wx::Pen->new(wxBLACK, 1, wxSOLID));
$dc->SetBrush(Wx::Brush->new(wxBLACK, wxSOLID));
$dc->DrawRectangle(0, 0, $cw, $ch);
return if !$self->config;
# turn size into max visible coordinates
# TODO: or should we use ClientArea?
$cw--;
$ch--;
# draw bed
if ($self->config2->{show_bed}) {
$dc->SetPen(Wx::Pen->new(wxRED, 2, wxSOLID));
$dc->SetBrush(Wx::Brush->new(wxWHITE, wxTRANSPARENT));
my $bed_polygon = Slic3r::Polygon->new_scale(@{$self->config->bed_shape});
$dc->DrawPolygon($self->scaled_points_to_pixel($bed_polygon), 0, 0);
}
return if !defined $self->layers;
# get layers at this height
# draw layers
$dc->SetPen(Wx::Pen->new(wxWHITE, 1, wxSOLID));
$dc->SetBrush(Wx::Brush->new(wxWHITE, wxSOLID));
foreach my $layer (@{$self->layers}) {
foreach my $copy (@{$layer->object->_shifted_copies}) {
foreach my $expolygon (@{ $layer->slices }) {
$expolygon = $expolygon->clone;
$expolygon->translate(@$copy);
foreach my $points (@{$expolygon->pp}) {
$dc->DrawPolygon($self->scaled_points_to_pixel($points), 0, 0);
}
}
}
}
}
# convert a model coordinate into a pixel coordinate
sub unscaled_point_to_pixel {
my ($self, $point) = @_;
my $ch = $self->GetSize->GetHeight;
my $zero = $self->bed_origin;
return [
$point->[X] * $self->scaling_factor + $zero->[X],
$ch - ($point->[Y] * $self->scaling_factor + $zero->[Y]),
];
}
sub scaled_points_to_pixel {
my ($self, $points) = @_;
return [
map $self->unscaled_point_to_pixel($_),
map Slic3r::Pointf->new_unscale(@$_),
@$points
];
}
1;