From 6bfa2cfaecd9629e75875938758279abb1b774f5 Mon Sep 17 00:00:00 2001
From: Alessandro Ranellucci <aar@cpan.org>
Date: Sun, 15 Nov 2015 21:08:14 +0100
Subject: [PATCH] Projector for DLP

---
 lib/Slic3r/GUI.pm                             |   3 +
 .../GUI/Controller/ManualControlDialog.pm     |   2 +-
 lib/Slic3r/GUI/MainFrame.pm                   |   7 +
 lib/Slic3r/GUI/OptionsGroup.pm                |   8 +-
 lib/Slic3r/GUI/OptionsGroup/Field.pm          |   4 +-
 lib/Slic3r/GUI/Projector.pm                   | 619 ++++++++++++++++++
 var/film.png                                  | Bin 0 -> 653 bytes
 7 files changed, 636 insertions(+), 7 deletions(-)
 create mode 100644 lib/Slic3r/GUI/Projector.pm
 create mode 100644 var/film.png

diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm
index 782645f83..dca7bb6f4 100644
--- a/lib/Slic3r/GUI.pm
+++ b/lib/Slic3r/GUI.pm
@@ -26,6 +26,7 @@ use Slic3r::GUI::Plater::ObjectSettingsDialog;
 use Slic3r::GUI::Plater::OverrideSettingsPanel;
 use Slic3r::GUI::Preferences;
 use Slic3r::GUI::ProgressStatusBar;
+use Slic3r::GUI::Projector;
 use Slic3r::GUI::OptionsGroup;
 use Slic3r::GUI::OptionsGroup::Field;
 use Slic3r::GUI::SimpleTab;
@@ -77,6 +78,8 @@ our $grey = Wx::Colour->new(200,200,200);
 
 our $VERSION_CHECK_EVENT : shared = Wx::NewEventType;
 
+our $DLP_projection_screen;
+
 sub OnInit {
     my ($self) = @_;
     
diff --git a/lib/Slic3r/GUI/Controller/ManualControlDialog.pm b/lib/Slic3r/GUI/Controller/ManualControlDialog.pm
index d22d86d79..b7f6160d4 100644
--- a/lib/Slic3r/GUI/Controller/ManualControlDialog.pm
+++ b/lib/Slic3r/GUI/Controller/ManualControlDialog.pm
@@ -60,7 +60,7 @@ sub new {
             my ($pos) = @_;
             
             # delete any pending commands to get a smoother movement
-            $self->purge_queue(1);
+            $self->sender->purge_queue(1);
             $self->abs_xy_move($pos);
         });
         $bed_sizer->Add($canvas, 0, wxEXPAND | wxRIGHT, 3);
diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm
index 9b9d18583..3ad443572 100644
--- a/lib/Slic3r/GUI/MainFrame.pm
+++ b/lib/Slic3r/GUI/MainFrame.pm
@@ -226,6 +226,13 @@ sub _init_menubar {
         $self->_append_menu_item($self->{plater_menu}, "Export plate as AMF...", 'Export current plate as AMF', sub {
             $plater->export_amf;
         }, undef, 'brick_go.png');
+        $self->_append_menu_item($self->{plater_menu}, "Open DLP Projector…\tCtrl+L", 'Open projector window for DLP printing', sub {
+            my $projector = Slic3r::GUI::Projector->new($self);
+            
+            # this double invocation is needed for properly hiding the MainFrame
+            $projector->Show;
+            $projector->ShowModal;
+        }, undef, 'film.png');
         
         $self->{object_menu} = $self->{plater}->object_menu;
         $self->on_plater_selection_changed(0);
diff --git a/lib/Slic3r/GUI/OptionsGroup.pm b/lib/Slic3r/GUI/OptionsGroup.pm
index 00b19180a..e980d44bb 100644
--- a/lib/Slic3r/GUI/OptionsGroup.pm
+++ b/lib/Slic3r/GUI/OptionsGroup.pm
@@ -247,8 +247,8 @@ sub set_value {
 }
 
 sub _on_change {
-    my ($self, $opt_id) = @_;
-    $self->on_change->($opt_id);
+    my ($self, $opt_id, $value) = @_;
+    $self->on_change->($opt_id, $value);
 }
 
 sub _on_kill_focus {
@@ -408,7 +408,7 @@ sub _get_config_value {
 }
 
 sub _on_change {
-    my ($self, $opt_id) = @_;
+    my ($self, $opt_id, $value) = @_;
     
     if (exists $self->_opt_map->{$opt_id}) {
         my ($opt_key, $opt_index) = @{ $self->_opt_map->{$opt_id} };
@@ -430,7 +430,7 @@ sub _on_change {
         }
     }
     
-    $self->SUPER::_on_change($opt_id);
+    $self->SUPER::_on_change($opt_id, $value);
 }
 
 sub _on_kill_focus {
diff --git a/lib/Slic3r/GUI/OptionsGroup/Field.pm b/lib/Slic3r/GUI/OptionsGroup/Field.pm
index 3a7d184db..ae6660067 100644
--- a/lib/Slic3r/GUI/OptionsGroup/Field.pm
+++ b/lib/Slic3r/GUI/OptionsGroup/Field.pm
@@ -35,7 +35,7 @@ sub toggle {
 sub _on_change {
     my ($self, $opt_id) = @_;
     
-    $self->on_change->($opt_id)
+    $self->on_change->($opt_id, $self->get_value)
         unless $self->disable_change_event;
 }
 
@@ -219,7 +219,7 @@ sub BUILD {
     my ($self) = @_;
     
     my $style = 0;
-    $style |= wxCB_READONLY if $self->option->gui_type ne 'select_open';
+    $style |= wxCB_READONLY if defined $self->option->gui_type && $self->option->gui_type ne 'select_open';
     my $field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, $self->_default_size,
         $self->option->labels || $self->option->values || [], $style);
     $self->wxWindow($field);
diff --git a/lib/Slic3r/GUI/Projector.pm b/lib/Slic3r/GUI/Projector.pm
new file mode 100644
index 000000000..19afa8a52
--- /dev/null
+++ b/lib/Slic3r/GUI/Projector.pm
@@ -0,0 +1,619 @@
+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   => 4,
+        settle_time     => 4,
+        z_lift          => 15,
+        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)));
+        $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 $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 @displays = 0 .. (Wx::Display::GetCount()-1);
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'display',
+            type        => 'select',
+            label       => 'Display',
+            tooltip     => '',
+            labels      => [@displays],
+            values      => [@displays],
+            default     => $self->config2->{display},
+        ));
+        $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},
+        ));
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'zoom',
+            type        => 'percent',
+            label       => 'Zoom',
+            sidetext    => '%',
+            tooltip     => '',
+            default     => $self->config2->{zoom},
+            min         => 0.1,
+            max         => 100,
+        ));
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'offset',
+            type        => 'point',
+            label       => 'Offset',
+            tooltip     => '',
+            default     => $self->config2->{offset},
+        ));
+    }
+    
+    {
+        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);
+        
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'exposure_time',
+            type        => 'i',
+            label       => 'Exposure time',
+            sidetext    => 'seconds',
+            tooltip     => '',
+            default     => $self->config2->{exposure_time},
+        ));
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'settle_time',
+            type        => 'i',
+            label       => 'Settle time',
+            sidetext    => 'seconds',
+            tooltip     => '',
+            default     => $self->config2->{settle_time},
+        ));
+        $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new(
+            opt_id      => 'z_lift',
+            type        => 'f',
+            label       => 'Z Lift',
+            sidetext    => 'mm',
+            tooltip     => '',
+            default     => $self->config2->{z_lift},
+        ));
+    }
+    
+    {
+        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);
+
+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) = @_;
+    
+    $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) = @_;
+    
+    if (0) {
+        $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($self, "Connection failed. Check serial port and speed.");
+            return;
+        }
+        Slic3r::debugf "connected to " . $self->config->serial_port . "\n";
+        
+        # TODO: replace this with customizable start G-code
+        $self->sender->send("G28 Z", 1);
+        $self->sender->send(sprintf("G1 Z%.5f F5000", $self->config2->{z_lift}), 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);
+    $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;
+        $self->sender->send(sprintf("G1 Z%.5f F5000", $z + $self->config2->{z_lift}), 1);
+        $self->sender->send(sprintf("G1 Z%.5f F5000", $z), 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] } };
+        printf "id = %d, height = %s\n", $self->_layer_num, $self->_heights->[$self->_layer_num];
+        $self->screen->project_layers([ @layers ]);
+        $self->delay($self->config2->{exposure_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;
diff --git a/var/film.png b/var/film.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0ce7bb198a3b268bd634d2b26e9b710f3797d37
GIT binary patch
literal 653
zcmV;80&@L{P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ
zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!A4x<(R5;6}
zlTT<9K^Vo~Zo8Yz8k>WO3(`_cf+b25@DJ#zdQm}8GzWtq2-QnZ8W6mB^kfeK5f%S{
zUW%tGMCwrwic~ZrQcG=4f?5bkV+3dRk8hw6bk~y$KX#b!y*J<R?HXYi;(wmO{-Ro~
zz7K+6lzgkzYPmRZxm^CBl*)OYw^Fay3x5RAnD>4EJ~>;dRASqrSu;ZpM>?P}K~6AT
zWv6Dmq?v&9LdXC(m%WCO6ma_di$R(v$@ad_>@R41N3N5lSJq9@6CGhX84-$%Xrd_6
z;){?{E|Ytt5$S-&Au>t4wDlIxdkfe-a22LMj``McG};r8@{GsRPm*+8fFey6C)@<E
zXHyDoIgRf>ifDBXVyT<e_Ya8HxRC&E%JqO9huj=&!-0n}^wO)qmInw7h1*hMJz!^C
zzz{j+x^8Ly;^&wdn7o4ArZrj{7qCX=n$4y+etB+};1rY(NDg!=a|6RE1N-*BUSyY#
zsVhgOz;v68k%5;#uVBqzLE!6P;8S-qhP*fW5L0XTwj&*|V6mW!#=oYZ)FV@C*o778
zfQ1g3C6=DTQGPj2W*Xkc3s?=G;90k=H5SdYq!ZUciGovP>w(N@Xd41b45OFg6x_QA
zpwLiigyy~cVoPxW^r~C7ZQpr%>1$*HKmv~AY-qJw4;gUecS--wnqslISSS=^KA&Ic
n@BK|Onfz#3R%n{$a)0j^sqv5F(1NTL00000NkvXXu0mjf3S}fX

literal 0
HcmV?d00001