diff --git a/Build.PL b/Build.PL index ead18f55e..80007abaa 100644 --- a/Build.PL +++ b/Build.PL @@ -28,6 +28,8 @@ my %prereqs = qw( ); my %recommends = qw( Class::XSAccessor 0 + LWP::UserAgent 0 + Net::Bonjour 0 XML::SAX::ExpatXS 0 ); @@ -109,7 +111,9 @@ EOF my %modules = (%prereqs, %recommends); foreach my $module (sort keys %modules) { my $version = $modules{$module}; - my @cmd = ($cpanm, @cpanm_args, "$module~$version"); + my @cmd = ($cpanm, @cpanm_args); + push @cmd, '-f', if $module eq 'OpenGL'; # temporary workaround for upstream bug in test + push @cmd, "$module~$version"; if ($module eq 'XML::SAX::ExpatXS' && $^O eq 'MSWin32') { my $mingw = 'C:\dev\CitrusPerl\mingw64'; $mingw = 'C:\dev\CitrusPerl\mingw32' if !-d $mingw; diff --git a/lib/Slic3r/Fill.pm b/lib/Slic3r/Fill.pm index 504230933..1881d461b 100644 --- a/lib/Slic3r/Fill.pm +++ b/lib/Slic3r/Fill.pm @@ -187,7 +187,7 @@ sub make_fill { if ($surface->is_solid) { $density = 100; $filler = 'rectilinear'; - if ($surface->is_external) { + if ($surface->is_external && !$is_bridge) { $filler = $layerm->config->external_fill_pattern; } } else { diff --git a/lib/Slic3r/Fill/3DHoneycomb.pm b/lib/Slic3r/Fill/3DHoneycomb.pm index 256437707..3bf7e547f 100644 --- a/lib/Slic3r/Fill/3DHoneycomb.pm +++ b/lib/Slic3r/Fill/3DHoneycomb.pm @@ -19,12 +19,14 @@ sub fill_surface { my $distance = scale($self->spacing) / $params{density}; - # align bounding box to a multiple of our honeycomb grid + # align bounding box to a multiple of our honeycomb grid module + # (a module is 2*$distance since one $distance half-module is + # growing while the other $distance half-module is shrinking) { my $min = $bb->min_point; $min->translate( - -($bb->x_min % $distance), - -($bb->y_min % $distance), + -($bb->x_min % (2*$distance)), + -($bb->y_min % (2*$distance)), ); $bb->merge_point($min); } @@ -34,8 +36,8 @@ sub fill_surface { makeGrid( scale($self->z), $distance, - ceil($size->x / $distance), - ceil($size->y / $distance), #// + ceil($size->x / $distance) + 1, + ceil($size->y / $distance) + 1, #// (($self->layer_id / $surface->thickness_layers) % 2) + 1, ); diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index 7adec0529..0a8f3951c 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -220,7 +220,7 @@ sub extrude_loop { $point->rotate($angle, $first_segment->a); # generate the travel move - $gcode .= $self->travel_to($point, $paths[-1]->role, "move inwards before travel"); + $gcode .= $self->writer->travel_to_xy($self->point_to_gcode($point), "move inwards before travel"); } return $gcode; @@ -328,6 +328,7 @@ sub _extrude_path { return $gcode; } +# This method accepts $point in print coordinates. sub travel_to { my ($self, $point, $role, $comment) = @_; @@ -355,11 +356,11 @@ sub travel_to { || (defined $role && $role == EXTR_ROLE_SUPPORTMATERIAL && $self->layer->support_islands->contains_line($travel)) ) { # Just perform a straight travel move without any retraction. - $gcode .= $self->writer->travel_to_xy($self->point_to_gcode($point), $comment); + $gcode .= $self->writer->travel_to_xy($self->point_to_gcode($point), $comment || ''); } elsif ($self->config->avoid_crossing_perimeters && !$self->avoid_crossing_perimeters->disable_once) { # If avoid_crossing_perimeters is enabled and the disable_once flag is not set # we need to plan a multi-segment travel move inside the configuration space. - $gcode .= $self->avoid_crossing_perimeters->travel_to($self, $point, $comment); + $gcode .= $self->avoid_crossing_perimeters->travel_to($self, $point, $comment || ''); } else { # If avoid_crossing_perimeters is disabled or the disable_once flag is set, # perform a straight move with a retraction. @@ -473,7 +474,7 @@ sub pre_toolchange { $last_pos->translate(scale +$gcodegen->origin->x, scale +$gcodegen->origin->y); #)) my $standby_point = $last_pos->nearest_point($self->standby_points); $standby_point->translate(scale -$gcodegen->origin->x, scale -$gcodegen->origin->y); #)) - $gcode .= $gcodegen->travel_to($standby_point); + $gcode .= $gcodegen->travel_to($standby_point, undef, 'move to standby position'); } if ($gcodegen->config->standby_temperature_delta != 0) { diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index d089bbf29..e08e6362d 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -7,6 +7,7 @@ use File::Basename qw(basename); use FindBin; use Slic3r::GUI::AboutDialog; use Slic3r::GUI::BedShapeDialog; +use Slic3r::GUI::BonjourBrowser; use Slic3r::GUI::ConfigWizard; use Slic3r::GUI::MainFrame; use Slic3r::GUI::Notifier; @@ -27,6 +28,7 @@ use Slic3r::GUI::SimpleTab; use Slic3r::GUI::Tab; our $have_OpenGL = eval "use Slic3r::GUI::PreviewCanvas; 1"; +our $have_LWP = eval "use LWP::UserAgent; 1"; use Wx 0.9901 qw(:bitmap :dialog :icon :id :misc :systemsettings :toplevelwindow :filedialog); @@ -228,7 +230,7 @@ sub have_version_check { my ($self) = @_; # return an explicit 0 - return ($Slic3r::have_threads && $Slic3r::build && eval "use LWP::UserAgent; 1") || 0; + return ($Slic3r::have_threads && $Slic3r::build && $have_LWP) || 0; } sub check_version { diff --git a/lib/Slic3r/GUI/BonjourBrowser.pm b/lib/Slic3r/GUI/BonjourBrowser.pm new file mode 100644 index 000000000..281277768 --- /dev/null +++ b/lib/Slic3r/GUI/BonjourBrowser.pm @@ -0,0 +1,51 @@ +package Slic3r::GUI::BonjourBrowser; +use strict; +use warnings; +use utf8; + +use Wx qw(:dialog :id :misc :sizer :choicebook wxTAB_TRAVERSAL); +use Wx::Event qw(EVT_CLOSE); +use base 'Wx::Dialog'; + +sub new { + my $class = shift; + my ($parent) = @_; + my $self = $class->SUPER::new($parent, -1, "Device Browser", wxDefaultPosition, [350,700], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); + + # look for devices + eval "use Net::Bonjour; 1"; + my $res = Net::Bonjour->new('http'); + $res->discover; + $self->{devices} = [ $res->entries ]; + + # label + my $text = Wx::StaticText->new($self, -1, "Choose an OctoPrint device in your network:", wxDefaultPosition, wxDefaultSize); + + # selector + $self->{choice} = my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, wxDefaultSize, + [ map $_->name, @{$self->{devices}} ]); + + my $main_sizer = Wx::BoxSizer->new(wxVERTICAL); + $main_sizer->Add($text, 1, wxEXPAND | wxALL, 10); + $main_sizer->Add($choice, 1, wxEXPAND | wxALL, 10); + $main_sizer->Add($self->CreateButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND); + + $self->SetSizer($main_sizer); + $self->SetMinSize($self->GetSize); + $main_sizer->SetSizeHints($self); + + # needed to actually free memory + EVT_CLOSE($self, sub { + $self->EndModal(wxID_OK); + $self->Destroy; + }); + + return $self; +} + +sub GetValue { + my ($self) = @_; + return $self->{devices}[ $self->{choice}->GetSelection ]->address; +} + +1; diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 31b9ff0a3..4cd4db706 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -673,7 +673,11 @@ sub config { sub check_unsaved_changes { my $self = shift; - my @dirty = map $_->title, grep $_->is_dirty, values %{$self->{options_tabs}}; + my @dirty = (); + foreach my $tab (values %{$self->{options_tabs}}) { + push @dirty, $tab->title if $tab->is_dirty; + } + if (@dirty) { my $titles = join ', ', @dirty; my $confirm = Wx::MessageDialog->new($self, "You have unsaved changes ($titles). Discard changes and continue anyway?", diff --git a/lib/Slic3r/GUI/OptionsGroup.pm b/lib/Slic3r/GUI/OptionsGroup.pm index 46d8aa678..d6a486828 100644 --- a/lib/Slic3r/GUI/OptionsGroup.pm +++ b/lib/Slic3r/GUI/OptionsGroup.pm @@ -83,7 +83,7 @@ sub append_line { # if we have a single option with no sidetext just add it directly to the grid sizer my @options = @{$line->get_options}; $self->_options->{$_->opt_id} = $_ for @options; - if (@options == 1 && !$options[0]->sidetext) { + if (@options == 1 && !$options[0]->sidetext && !@{$line->get_extra_widgets}) { my $option = $options[0]; my $field = $self->_build_field($option); $grid_sizer->Add($field, 0, ($option->full_width ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0); @@ -114,9 +114,14 @@ sub append_line { $sizer->Add($sidetext, 0, wxLEFT | wxALIGN_CENTER_VERTICAL , 4); } } + + # add extra sizers if any + foreach my $extra_widget (@{$line->get_extra_widgets}) { + $sizer->Add($extra_widget->($self->parent), 0, wxLEFT | wxALIGN_CENTER_VERTICAL , 4); + } } -sub append_single_option_line { +sub create_single_option_line { my ($self, $option) = @_; my $line = Slic3r::GUI::OptionsGroup::Line->new( @@ -125,11 +130,15 @@ sub append_single_option_line { ); $option->label(""); $line->append_option($option); - $self->append_line($line); return $line; } +sub append_single_option_line { + my ($self, $option) = @_; + return $self->append_line($self->create_single_option_line($option)); +} + sub _build_field { my $self = shift; my ($opt) = @_; @@ -241,6 +250,7 @@ has 'label_tooltip' => (is => 'rw', default => sub { "" }); has 'sizer' => (is => 'rw'); has 'widget' => (is => 'rw'); has '_options' => (is => 'ro', default => sub { [] }); +has '_extra_widgets' => (is => 'ro', default => sub { [] }); # this method accepts a Slic3r::GUI::OptionsGroup::Option object sub append_option { @@ -248,11 +258,21 @@ sub append_option { push @{$self->_options}, $option; } +sub append_widget { + my ($self, $widget) = @_; + push @{$self->_extra_widgets}, $widget; +} + sub get_options { my ($self) = @_; return [ @{$self->_options} ]; } +sub get_extra_widgets { + my ($self) = @_; + return [ @{$self->_extra_widgets} ]; +} + package Slic3r::GUI::OptionsGroup::Option; use Moo; @@ -320,7 +340,7 @@ sub get_option { ); } -sub append_single_option_line { +sub create_single_option_line { my ($self, $opt_key, $opt_index) = @_; my $option; @@ -329,7 +349,12 @@ sub append_single_option_line { } else { $option = $self->get_option($opt_key, $opt_index); } - return $self->SUPER::append_single_option_line($option); + return $self->SUPER::create_single_option_line($option); +} + +sub append_single_option_line { + my ($self, $option, $opt_index) = @_; + return $self->append_line($self->create_single_option_line($option, $opt_index)); } sub reload_config { diff --git a/lib/Slic3r/GUI/OptionsGroup/Field.pm b/lib/Slic3r/GUI/OptionsGroup/Field.pm index fcf97eb3f..13b86cdfc 100644 --- a/lib/Slic3r/GUI/OptionsGroup/Field.pm +++ b/lib/Slic3r/GUI/OptionsGroup/Field.pm @@ -347,6 +347,7 @@ sub BUILD { $self->wxSizer($sizer); my $field_size = Wx::Size->new(40, -1); + $self->x_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[X], wxDefaultPosition, $field_size)); $self->y_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[Y], wxDefaultPosition, $field_size)); diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 42b875036..2517cd893 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -48,6 +48,7 @@ sub new { my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); $self->{config} = Slic3r::Config->new_from_defaults(qw( bed_shape complete_objects extruder_clearance_radius skirts skirt_distance brim_width + octoprint_host octoprint_apikey )); $self->{model} = Slic3r::Model->new; $self->{print} = Slic3r::Print->new; @@ -88,16 +89,7 @@ sub new { }; my $on_instance_moved = sub { my ($obj_idx, $instance_idx) = @_; - $self->update; - - $self->pause_background_process; - my $invalidated = $self->{print}->objects->[$obj_idx]->reload_model_instances(); - if ($invalidated) { - $self->schedule_background_process; - } else { - $self->resume_background_process; - } }; # Initialize 2D preview canvas @@ -181,9 +173,11 @@ sub new { # right pane buttons $self->{btn_export_gcode} = Wx::Button->new($self, -1, "Export G-code…", wxDefaultPosition, [-1, 30], wxBU_LEFT); + $self->{btn_send_gcode} = Wx::Button->new($self, -1, "Send to printer", wxDefaultPosition, [-1, 30], wxBU_LEFT); $self->{btn_export_stl} = Wx::Button->new($self, -1, "Export STL…", wxDefaultPosition, [-1, 30], wxBU_LEFT); #$self->{btn_export_gcode}->SetFont($Slic3r::GUI::small_font); #$self->{btn_export_stl}->SetFont($Slic3r::GUI::small_font); + $self->{btn_send_gcode}->Hide; if ($Slic3r::GUI::have_button_icons) { my %icons = qw( @@ -192,6 +186,7 @@ sub new { reset cross.png arrange bricks.png export_gcode cog_go.png + send_gcode arrow_up.png export_stl brick_go.png increase add.png @@ -213,7 +208,11 @@ sub new { $self->export_gcode; Slic3r::thread_cleanup(); }); - #EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); + EVT_BUTTON($self, $self->{btn_send_gcode}, sub { + $self->{send_gcode_file} = $self->export_gcode(Wx::StandardPaths::Get->GetTempDir()); + Slic3r::thread_cleanup(); + }); + EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); if ($self->{htoolbar}) { EVT_TOOL($self, TB_ADD, sub { $self->add; }); @@ -355,6 +354,7 @@ sub new { my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); $buttons_sizer->AddStretchSpacer(1); $buttons_sizer->Add($self->{btn_export_stl}, 0, wxALIGN_RIGHT, 0); + $buttons_sizer->Add($self->{btn_send_gcode}, 0, wxALIGN_RIGHT, 0); $buttons_sizer->Add($self->{btn_export_gcode}, 0, wxALIGN_RIGHT, 0); my $right_sizer = Wx::BoxSizer->new(wxVERTICAL); @@ -555,7 +555,6 @@ sub remove { $self->select_object(undef); $self->update; - $self->schedule_background_process; } @@ -666,10 +665,10 @@ sub rotate { $model_object->update_bounding_box; # update print and start background processing $self->{print}->add_model_object($model_object, $obj_idx); - $self->schedule_background_process; $self->selection_changed; # refresh info (size etc.) $self->update; + $self->schedule_background_process; } sub flip { @@ -694,11 +693,10 @@ sub flip { # update print and start background processing $self->stop_background_process; $self->{print}->add_model_object($model_object, $obj_idx); - $self->schedule_background_process; $self->selection_changed; # refresh info (size etc.) $self->update; - $self->refresh_canvases; + $self->schedule_background_process; } sub changescale { @@ -749,11 +747,10 @@ sub changescale { # update print and start background processing $self->stop_background_process; $self->{print}->add_model_object($model_object, $obj_idx); - $self->schedule_background_process; $self->selection_changed(1); # refresh info (size, volume etc.) $self->update; - $self->refresh_canvases; + $self->schedule_background_process; } sub arrange { @@ -769,17 +766,6 @@ sub arrange { # when parts don't fit in print bed $self->update(1); - - my $invalidated = 0; - foreach my $object (@{$self->{print}->objects}) { - $invalidated = 1 if $object->reload_model_instances; - } - if ($invalidated) { - $self->schedule_background_process; - } else { - $self->resume_background_process; - } - $self->refresh_canvases; } sub split_object { @@ -788,19 +774,22 @@ sub split_object { my ($obj_idx, $current_object) = $self->selected_object; # we clone model object because split_object() adds the split volumes - # into the same model object, thus causing duplicated when we call load_model_objects() - my $current_model_object = $self->{model}->clone->objects->[$obj_idx]; + # into the same model object, thus causing duplicates when we call load_model_objects() + my $new_model = $self->{model}->clone; # store this before calling get_object() + my $current_model_object = $new_model->get_object($obj_idx); - if (@{$current_model_object->volumes} > 1) { + if ($current_model_object->volumes_count > 1) { Slic3r::GUI::warning_catcher($self)->("The selected object can't be split because it contains more than one volume/material."); return; } - $self->stop_background_process; + $self->pause_background_process; my @model_objects = @{$current_model_object->split_object}; if (@model_objects == 1) { + $self->resume_background_process; Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains only one part."); + $self->resume_background_process; return; } @@ -944,7 +933,7 @@ sub resume_background_process { } sub export_gcode { - my $self = shift; + my ($self, $output_file) = @_; return if !@{$self->{objects}}; @@ -976,14 +965,14 @@ sub export_gcode { } # select output file - $self->{export_gcode_output_file} = $main::opt{output}; - { - my $default_output_file = $self->{print}->expanded_output_filepath($self->{export_gcode_output_file}); + if ($output_file) { + $self->{export_gcode_output_file} = $self->{print}->expanded_output_filepath($output_file); + } else { + my $default_output_file = $self->{print}->expanded_output_filepath($main::opt{output}); my $dlg = Wx::FileDialog->new($self, 'Save G-code file as:', wxTheApp->output_path(dirname($default_output_file)), basename($default_output_file), &Slic3r::GUI::FILE_WILDCARDS->{gcode}, wxFD_SAVE); if ($dlg->ShowModal != wxID_OK) { $dlg->Destroy; - $self->{export_gcode_output_file} = undef; return; } $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($dlg->GetPath); @@ -1011,6 +1000,8 @@ sub export_gcode { my $result = !Slic3r::GUI::catch_error($self); $self->on_export_completed($result); } + + return $self->{export_gcode_output_file}; } # This gets called only if we have threads. @@ -1072,14 +1063,52 @@ sub on_export_completed { $self->{export_thread} = undef; my $message; + my $send_gcode = 0; if ($result) { - $message = "G-code file exported to " . $self->{export_gcode_output_file}; + if ($self->{send_gcode_file}) { + $message = "Sending G-code file to the OctoPrint server..."; + $send_gcode = 1; + } else { + $message = "G-code file exported to " . $self->{export_gcode_output_file}; + } } else { $message = "Export failed"; } $self->{export_gcode_output_file} = undef; $self->statusbar->SetStatusText($message); wxTheApp->notify($message); + + $self->send_gcode if $send_gcode; + $self->{send_gcode_file} = undef; +} + +sub send_gcode { + my ($self) = @_; + + $self->statusbar->StartBusy; + + my $ua = LWP::UserAgent->new; + $ua->timeout(10); + + my $res = $ua->post( + "http://" . $self->{config}->octoprint_host . "/api/files/local", + Content_Type => 'form-data', + 'X-Api-Key' => $self->{config}->octoprint_apikey, + Content => [ + # OctoPrint doesn't like Windows paths + file => [ $self->{send_gcode_file}, basename($self->{send_gcode_file}) ], + ], + ); + + $self->statusbar->StopBusy; + + if ($res->is_success) { + $self->statusbar->SetStatusText("G-code file successfully uploaded to the OctoPrint server"); + } else { + my $message = "Error while uploading to the OctoPrint server: " . $res->status_line; + Slic3r::GUI::show_error($self, $message); + $self->statusbar->SetStatusText($message); + } } sub export_stl { @@ -1159,7 +1188,6 @@ sub on_thumbnail_made { my ($obj_idx) = @_; $self->{objects}[$obj_idx]->transform_thumbnail($self->{model}, $obj_idx); - $self->update; $self->refresh_canvases; } @@ -1172,9 +1200,23 @@ sub update { $self->{model}->center_instances_around_point($self->bed_centerf); } + $self->pause_background_process; + my $invalidated = $self->{print}->reload_model_instances(); + if ($invalidated) { + $self->schedule_background_process; + } else { + $self->resume_background_process; + } + $self->refresh_canvases; } +sub on_model_instances_changed { + my ($self) = @_; + + +} + sub on_extruders_change { my ($self, $num_extruders) = @_; @@ -1208,6 +1250,13 @@ sub on_config_change { $self->{canvas}->update_bed_size; $self->{canvas3D}->update_bed_size if $self->{canvas3D}; $self->update; + } elsif ($opt_key eq 'octoprint_host') { + if ($config->get('octoprint_host')) { + $self->{btn_send_gcode}->Show; + } else { + $self->{btn_send_gcode}->Hide; + } + $self->Layout; } } @@ -1313,7 +1362,7 @@ sub object_list_changed { my $have_objects = @{$self->{objects}} ? 1 : 0; my $method = $have_objects ? 'Enable' : 'Disable'; $self->{"btn_$_"}->$method - for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl); + for grep $self->{"btn_$_"}, qw(reset arrange export_gcode export_stl send_gcode); if ($self->{htoolbar}) { $self->{htoolbar}->EnableTool($_, $have_objects) diff --git a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm index fb4b0291c..255a1685a 100644 --- a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm +++ b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm @@ -224,13 +224,7 @@ sub on_btn_load { } } - $self->reload_tree; - if ($self->{canvas}) { - $self->{canvas}->reset_objects; - $self->{canvas}->load_object($self->{model_object}); - $self->{canvas}->set_bounding_box($self->{model_object}->bounding_box); - $self->{canvas}->Render; - } + $self->_parts_changed; } sub on_btn_delete { @@ -250,9 +244,17 @@ sub on_btn_delete { $self->{parts_changed} = 1; } + $self->_parts_changed; +} + +sub _parts_changed { + my ($self) = @_; + $self->reload_tree; if ($self->{canvas}) { + $self->{canvas}->reset_objects; $self->{canvas}->load_object($self->{model_object}); + $self->{canvas}->zoom_to_volumes; $self->{canvas}->Render; } } diff --git a/lib/Slic3r/GUI/Preferences.pm b/lib/Slic3r/GUI/Preferences.pm index aab037ce7..74846cfc9 100644 --- a/lib/Slic3r/GUI/Preferences.pm +++ b/lib/Slic3r/GUI/Preferences.pm @@ -5,7 +5,7 @@ use base 'Wx::Dialog'; sub new { my ($class, $parent) = @_; - my $self = $class->SUPER::new($parent, -1, "Preferences", wxDefaultPosition, [500,200]); + my $self = $class->SUPER::new($parent, -1, "Preferences", wxDefaultPosition, wxDefaultSize); $self->{values} = {}; my $optgroup; @@ -16,7 +16,7 @@ sub new { my ($opt_id) = @_; $self->{values}{$opt_id} = $optgroup->get_value($opt_id); }, - label_width => 100, + label_width => 200, ); $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( opt_id => 'mode', @@ -26,6 +26,7 @@ sub new { labels => ['Simple','Expert'], values => ['simple','expert'], default => $Slic3r::GUI::Settings->{_}{mode}, + width => 100, )); $optgroup->append_single_option_line(Slic3r::GUI::OptionsGroup::Option->new( opt_id => 'version_check', diff --git a/lib/Slic3r/GUI/PreviewCanvas.pm b/lib/Slic3r/GUI/PreviewCanvas.pm index d9e418345..f48240392 100644 --- a/lib/Slic3r/GUI/PreviewCanvas.pm +++ b/lib/Slic3r/GUI/PreviewCanvas.pm @@ -151,9 +151,6 @@ sub mouse_event { } } } elsif ($e->Dragging && $e->LeftIsDown && defined($self->_drag_volume_idx)) { - # get volume being dragged - my $volume = $self->volumes->[$self->_drag_volume_idx]; - # get new position at the same Z of the initial click point my $mouse_ray = $self->mouse_ray($e->GetX, $e->GetY); my $cur_pos = $mouse_ray->intersect_plane($self->_drag_start_pos->z); @@ -161,8 +158,14 @@ sub mouse_event { # calculate the translation vector my $vector = $self->_drag_start_pos->vector_to($cur_pos); + # get volume being dragged + my $volume = $self->volumes->[$self->_drag_volume_idx]; + + # get all volumes belonging to the same group but only having the same instance_idx + my @volumes = grep $_->group_id == $volume->group_id && $_->instance_idx == $volume->instance_idx, @{$self->volumes}; + # apply new temporary volume origin and ignore Z - $volume->origin->translate($vector->x, $vector->y, 0); #,, + $_->origin->translate($vector->x, $vector->y, 0) for @volumes; #,, $self->_drag_start_pos($cur_pos); $self->_dragged(1); $self->Refresh; @@ -346,6 +349,7 @@ sub load_object { # sort volumes: non-modifiers first my @volumes = sort { ($a->modifier // 0) <=> ($b->modifier // 0) } @{$object->volumes}; my @volumes_idx = (); + my $group_id = $#{$self->volumes} + 1; foreach my $volume (@volumes) { my @instance_idxs = $all_instances ? (0..$#{$object->instances}) : (0); foreach my $instance_idx (@instance_idxs) { @@ -363,6 +367,7 @@ sub load_object { my $color = [ @{COLORS->[ $color_idx % scalar(@{&COLORS}) ]} ]; push @$color, $volume->modifier ? 0.5 : 1; push @{$self->volumes}, my $v = Slic3r::GUI::PreviewCanvas::Volume->new( + group_id => $group_id, instance_idx => $instance_idx, mesh => $mesh, color => $color, @@ -888,6 +893,7 @@ use Moo; has 'mesh' => (is => 'ro', required => 1); has 'color' => (is => 'ro', required => 1); +has 'group_id' => (is => 'ro', required => 1); has 'instance_idx' => (is => 'ro', default => sub { 0 }); has 'origin' => (is => 'rw', default => sub { Slic3r::Pointf3->new(0,0,0) }); has 'verts' => (is => 'rw'); diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index 398aecac2..6aac1c862 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -923,6 +923,7 @@ sub build { $self->init_config_options(qw( bed_shape z_offset gcode_flavor use_relative_e_distances + octoprint_host octoprint_apikey use_firmware_retraction pressure_advance vibration_limit start_gcode end_gcode layer_gcode toolchange_gcode nozzle_diameter extruder_offset @@ -996,6 +997,45 @@ sub build { } }); } + { + my $optgroup = $page->new_optgroup('OctoPrint upload'); + + # append a button to the Host line + my $octoprint_host_widget = sub { + my ($parent) = @_; + + my $btn = Wx::Button->new($parent, -1, "Browse…", wxDefaultPosition, wxDefaultSize, wxBU_LEFT); + $btn->SetFont($Slic3r::GUI::small_font); + if ($Slic3r::GUI::have_button_icons) { + $btn->SetBitmap(Wx::Bitmap->new("$Slic3r::var/zoom.png", wxBITMAP_TYPE_PNG)); + } + + if (!eval "use Net::Bonjour; 1") { + $btn->Disable; + } + + my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $sizer->Add($btn); + + EVT_BUTTON($self, $btn, sub { + my $dlg = Slic3r::GUI::BonjourBrowser->new($self); + if ($dlg->ShowModal == wxID_OK) { + my $value = $dlg->GetValue; + $self->{config}->set('octoprint_host', $value); + $self->update_dirty; + $self->_on_value_change('octoprint_host', $value); + $self->reload_config; + } + }); + + return $sizer; + }; + + my $host_line = $optgroup->create_single_option_line('octoprint_host'); + $host_line->append_widget($octoprint_host_widget); + $optgroup->append_line($host_line); + $optgroup->append_single_option_line('octoprint_apikey'); + } { my $optgroup = $page->new_optgroup('Advanced'); $optgroup->append_single_option_line('use_firmware_retraction'); @@ -1129,6 +1169,8 @@ sub _update { my $config = $self->{config}; + $self->get_field('octoprint_apikey')->toggle($config->get('octoprint_host')); + my $have_multiple_extruders = $self->{extruders_count} > 1; $self->get_field('toolchange_gcode')->toggle($have_multiple_extruders); @@ -1319,8 +1361,8 @@ sub config { } # apply preset values on top of defaults + my $config = Slic3r::Config->new_from_defaults(@$keys); my $external_config = Slic3r::Config->load($self->file); - my $config = Slic3r::Config->new; $config->set($_, $external_config->get($_)) for grep $external_config->has($_), @$keys; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index 69df1c900..b68e191a5 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -219,7 +219,7 @@ sub make_skirt { # $skirt_height_z in this case is the highest possible skirt height for safety. my $skirt_height_z = -1; foreach my $object (@{$self->objects}) { - my $skirt_height = ($self->config->skirt_height == -1) + my $skirt_height = ($self->config->skirt_height == -1 || $self->config->ooze_prevention) ? scalar(@{$object->layers}) : min($self->config->skirt_height, scalar(@{$object->layers})); diff --git a/lib/Slic3r/Print/GCode.pm b/lib/Slic3r/Print/GCode.pm index dae9eb3a7..a40a64e28 100644 --- a/lib/Slic3r/Print/GCode.pm +++ b/lib/Slic3r/Print/GCode.pm @@ -147,15 +147,26 @@ sub export { my $outer_skirt = convex_hull(\@skirt_points); my @skirts = (); foreach my $extruder_id (@{$self->print->extruders}) { + my $extruder_offset = $self->config->get_at('extruder_offset', $extruder_id); push @skirts, my $s = $outer_skirt->clone; - $s->translate(map scale($_), @{$self->config->get_at('extruder_offset', $extruder_id)}); + $s->translate(-scale($extruder_offset->x), -scale($extruder_offset->y)); #) } my $convex_hull = convex_hull([ map @$_, @skirts ]); $gcodegen->ooze_prevention->enable(1); $gcodegen->ooze_prevention->standby_points( - [ map $_->clone, map @$_, map $_->subdivide(scale 10), @{offset([$convex_hull], scale 3)} ] + [ map @{$_->equally_spaced_points(scale 10)}, @{offset([$convex_hull], scale 3)} ] ); + + if (0) { + require "Slic3r/SVG.pm"; + Slic3r::SVG::output( + "ooze_prevention.svg", + polygons => [$outer_skirt], + red_polygons => \@skirts, + points => $gcodegen->ooze_prevention->standby_points, + ); + } } } diff --git a/lib/Slic3r/Print/SupportMaterial.pm b/lib/Slic3r/Print/SupportMaterial.pm index a738c1dd8..7e23a75c7 100644 --- a/lib/Slic3r/Print/SupportMaterial.pm +++ b/lib/Slic3r/Print/SupportMaterial.pm @@ -442,23 +442,25 @@ sub generate_bottom_interface_layers { my $z = $support_z->[$layer_id]; next unless $z > $top_z; - # get the support material area that should be considered interface - my $interface_area = intersection( - $base->{$layer_id}, - $this, - ); + if ($base->{$layer_id}) { + # get the support material area that should be considered interface + my $interface_area = intersection( + $base->{$layer_id}, + $this, + ); - # discard too small areas - $interface_area = [ grep abs($_->area) >= $area_threshold, @$interface_area ]; + # discard too small areas + $interface_area = [ grep abs($_->area) >= $area_threshold, @$interface_area ]; - # subtract new interface area from base - $base->{$layer_id} = diff( - $base->{$layer_id}, - $interface_area, - ); + # subtract new interface area from base + $base->{$layer_id} = diff( + $base->{$layer_id}, + $interface_area, + ); - # add new interface area to interface - push @{$interface->{$layer_id}}, @$interface_area; + # add new interface area to interface + push @{$interface->{$layer_id}}, @$interface_area; + } $interface_layers++; last if $interface_layers == $self->object_config->support_material_interface_layers; diff --git a/t/multi.t b/t/multi.t index 5431370dd..96703bed2 100644 --- a/t/multi.t +++ b/t/multi.t @@ -17,23 +17,25 @@ use Slic3r::Test; my $config = Slic3r::Config->new_from_defaults; $config->set('raft_layers', 2); $config->set('infill_extruder', 2); - $config->set('support_material_extruder', 3); + $config->set('solid_infill_extruder', 3); + $config->set('support_material_extruder', 4); $config->set('ooze_prevention', 1); - $config->set('extruder_offset', [ [0,0], [20,0], [0,20] ]); - $config->set('temperature', [200, 180, 170]); - $config->set('first_layer_temperature', [206, 186, 166]); + $config->set('extruder_offset', [ [0,0], [20,0], [0,20], [20,20] ]); + $config->set('temperature', [200, 180, 170, 160]); + $config->set('first_layer_temperature', [206, 186, 166, 156]); $config->set('toolchange_gcode', ';toolchange'); # test that it doesn't crash when this is supplied my $print = Slic3r::Test::init_print('20mm_cube', config => $config); my $tool = undef; - my @tool_temp = (0,0,0); + my @tool_temp = (0,0,0,0); my @toolchange_points = (); my @extrusion_points = (); Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub { my ($self, $cmd, $args, $info) = @_; if ($cmd =~ /^T(\d+)/) { + # ignore initial toolchange if (defined $tool) { my $expected_temp = $self->Z == ($config->get_value('first_layer_height') + $config->z_offset) ? $config->first_layer_temperature->[$tool] @@ -41,8 +43,8 @@ use Slic3r::Test; die 'standby temperature was not set before toolchange' if $tool_temp[$tool] != $expected_temp + $config->standby_temperature_delta; - # ignore initial toolchange - push @toolchange_points, Slic3r::Point->new_scale($self->X, $self->Y); + push @toolchange_points, my $point = Slic3r::Point->new_scale($self->X, $self->Y); + $point->translate(map +scale($_), @{ $config->extruder_offset->[$tool] }); } $tool = $1; } elsif ($cmd eq 'M104' || $cmd eq 'M109') { @@ -54,11 +56,30 @@ use Slic3r::Test; $tool_temp[$t] = $args->{S}; } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) { push @extrusion_points, my $point = Slic3r::Point->new_scale($args->{X}, $args->{Y}); - $point->translate(map scale($_), @{ $config->extruder_offset->[$tool] }); + $point->translate(map +scale($_), @{ $config->extruder_offset->[$tool] }); } }); my $convex_hull = convex_hull(\@extrusion_points); - ok !(defined first { $convex_hull->contains_point($_) } @toolchange_points), 'all toolchanges happen outside skirt'; + + my @t = (); + foreach my $point (@toolchange_points) { + foreach my $offset (@{$config->extruder_offset}) { + push @t, my $p = $point->clone; + $p->translate(map +scale($_), @$offset); + } + } + ok !(defined first { $convex_hull->contains_point($_) } @t), 'all nozzles are outside skirt at toolchange'; + + if (0) { + require "Slic3r/SVG.pm"; + Slic3r::SVG::output( + "ooze_prevention.svg", + no_arrows => 1, + polygons => [$convex_hull], + points => \@toolchange_points, + red_points => \@t, + ); + } # offset the skirt by the maximum displacement between extruders plus a safety extra margin my $delta = scale(20 * sqrt(2) + 1); diff --git a/var/zoom.png b/var/zoom.png new file mode 100755 index 000000000..908612e39 Binary files /dev/null and b/var/zoom.png differ diff --git a/xs/src/libslic3r/Model.cpp b/xs/src/libslic3r/Model.cpp index 06919cedd..40926eb93 100644 --- a/xs/src/libslic3r/Model.cpp +++ b/xs/src/libslic3r/Model.cpp @@ -13,7 +13,7 @@ Model::Model(const Model &other) // copy objects this->objects.reserve(other.objects.size()); for (ModelObjectPtrs::const_iterator i = other.objects.begin(); i != other.objects.end(); ++i) - this->add_object(**i); + this->add_object(**i, true); } Model& Model::operator= (Model other) @@ -618,6 +618,7 @@ ModelObject::split(ModelObjectPtrs* new_objects) new_volume->material_id(volume->material_id()); new_objects->push_back(new_object); + delete *mesh; } return; diff --git a/xs/src/libslic3r/Print.cpp b/xs/src/libslic3r/Print.cpp index b40430fec..b87d1c8cf 100644 --- a/xs/src/libslic3r/Print.cpp +++ b/xs/src/libslic3r/Print.cpp @@ -116,6 +116,16 @@ Print::reload_object(size_t idx) } } +bool +Print::reload_model_instances() +{ + bool invalidated = false; + FOREACH_OBJECT(this, object) { + if ((*object)->reload_model_instances()) invalidated = true; + } + return invalidated; +} + void Print::clear_regions() { diff --git a/xs/src/libslic3r/Print.hpp b/xs/src/libslic3r/Print.hpp index 0e7334eaf..aa67e1c6d 100644 --- a/xs/src/libslic3r/Print.hpp +++ b/xs/src/libslic3r/Print.hpp @@ -174,6 +174,7 @@ class Print PrintObject* get_object(size_t idx); void delete_object(size_t idx); void reload_object(size_t idx); + bool reload_model_instances(); // methods for handling regions PrintRegion* get_region(size_t idx); diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 4565ba5af..2ca83c15e 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -498,6 +498,16 @@ PrintConfigDef::build_def() { Options["nozzle_diameter"].sidetext = "mm"; Options["nozzle_diameter"].cli = "nozzle-diameter=f@"; + Options["octoprint_apikey"].type = coString; + Options["octoprint_apikey"].label = "API Key"; + Options["octoprint_apikey"].tooltip = "Slic3r can upload G-code files to OctoPrint. This field should contain the API Key required for authentication."; + Options["octoprint_apikey"].cli = "octoprint-apikey=s"; + + Options["octoprint_host"].type = coString; + Options["octoprint_host"].label = "Host or IP"; + Options["octoprint_host"].tooltip = "Slic3r can upload G-code files to OctoPrint. This field should contain the hostname or IP address of the OctoPrint instance."; + Options["octoprint_host"].cli = "octoprint-host=s"; + Options["only_retract_when_crossing_perimeters"].type = coBool; Options["only_retract_when_crossing_perimeters"].label = "Only retract when crossing perimeters"; Options["only_retract_when_crossing_perimeters"].tooltip = "Disables retraction when the travel path does not exceed the upper layer's perimeters (and thus any ooze will be probably invisible)."; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index 48235c7d1..b647acfb8 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -582,7 +582,27 @@ class PrintConfig : public GCodeConfig }; }; -class FullPrintConfig : public PrintObjectConfig, public PrintRegionConfig, public PrintConfig +class HostConfig : public virtual StaticPrintConfig +{ + public: + ConfigOptionString octoprint_host; + ConfigOptionString octoprint_apikey; + + HostConfig() : StaticPrintConfig() { + this->octoprint_host.value = ""; + this->octoprint_apikey.value = ""; + }; + + ConfigOption* option(const t_config_option_key opt_key, bool create = false) { + if (opt_key == "octoprint_host") return &this->octoprint_host; + if (opt_key == "octoprint_apikey") return &this->octoprint_apikey; + + return NULL; + }; +}; + +class FullPrintConfig + : public PrintObjectConfig, public PrintRegionConfig, public PrintConfig, public HostConfig { public: ConfigOption* option(const t_config_option_key opt_key, bool create = false) { @@ -590,6 +610,7 @@ class FullPrintConfig : public PrintObjectConfig, public PrintRegionConfig, publ if ((opt = PrintObjectConfig::option(opt_key, create)) != NULL) return opt; if ((opt = PrintRegionConfig::option(opt_key, create)) != NULL) return opt; if ((opt = PrintConfig::option(opt_key, create)) != NULL) return opt; + if ((opt = HostConfig::option(opt_key, create)) != NULL) return opt; return NULL; }; }; diff --git a/xs/src/libslic3r/libslic3r.h b/xs/src/libslic3r/libslic3r.h index 42bfa6164..4a02ac2cd 100644 --- a/xs/src/libslic3r/libslic3r.h +++ b/xs/src/libslic3r/libslic3r.h @@ -6,7 +6,7 @@ #include #include -#define SLIC3R_VERSION "1.2.2" +#define SLIC3R_VERSION "1.2.4" #define EPSILON 1e-4 #define SCALING_FACTOR 0.000001 diff --git a/xs/src/perlglue.hpp b/xs/src/perlglue.hpp index 833eee6f3..84c9a7ae9 100644 --- a/xs/src/perlglue.hpp +++ b/xs/src/perlglue.hpp @@ -42,9 +42,9 @@ template class Ref { T* val; public: - Ref() {} + Ref() : val(NULL) {} Ref(T* t) : val(t) {} - operator T*() const {return val; } + operator T*() const { return val; } static const char* CLASS() { return ClassTraits::name_ref; } }; @@ -52,10 +52,10 @@ template class Clone { T* val; public: - Clone() : val() {} + Clone() : val(NULL) {} Clone(T* t) : val(new T(*t)) {} Clone(const T& t) : val(new T(t)) {} - operator T*() const {return val; } + operator T*() const { return val; } static const char* CLASS() { return ClassTraits::name; } }; }; diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index c3c9a97c5..fb0d17721 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -143,6 +143,7 @@ _constant() Ref get_object(int idx); void delete_object(int idx); void reload_object(int idx); + bool reload_model_instances(); size_t object_count() %code%{ RETVAL = THIS->objects.size(); %};