diff --git a/README.md b/README.md index 984931402..f24fdfc66 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ The author of the Silk icon set is Mark James. Only retract before travel moves of this length in mm (default: 2) --retract-lift Lift Z by the given distance in mm when retracting (default: 0) --retract-layer-change - Enforce a retraction before each Z move (default: yes) + Enforce a retraction before each Z move (default: no) --wipe Wipe the nozzle while doing a retraction (default: no) Retraction options for multi-extruder setups: diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 60d7ac4a5..bcb3aac03 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -32,8 +32,6 @@ warn "Running Slic3r under Perl 5.16 is not supported nor recommended\n" use FindBin; our $var = "$FindBin::Bin/var"; -use Encode; -use Encode::Locale; use Moo 1.003001; use Slic3r::XS; # import all symbols (constants etc.) before they get parsed @@ -262,13 +260,17 @@ sub resume_all_threads { } sub encode_path { - my ($filename) = @_; - return encode('locale_fs', $filename); + my ($path) = @_; + + utf8::downgrade($path) if $^O eq 'MSWin32'; + return $path; } sub decode_path { - my ($filename) = @_; - return decode('locale_fs', $filename); + my ($path) = @_; + + utf8::upgrade($path) if $^O eq 'MSWin32'; + return $path; } sub open { diff --git a/lib/Slic3r/Fill/Concentric.pm b/lib/Slic3r/Fill/Concentric.pm index 1bc9b79f9..ca1837c4e 100644 --- a/lib/Slic3r/Fill/Concentric.pm +++ b/lib/Slic3r/Fill/Concentric.pm @@ -6,6 +6,8 @@ extends 'Slic3r::Fill::Base'; use Slic3r::Geometry qw(scale unscale X); use Slic3r::Geometry::Clipper qw(offset offset2 union_pt_chained); +sub no_sort { 1 } + sub fill_surface { my $self = shift; my ($surface, %params) = @_; @@ -36,7 +38,7 @@ sub fill_surface { @loops = map Slic3r::Polygon->new(@$_), reverse @{union_pt_chained(\@loops)}; - # order paths using a nearest neighbor search + # split paths using a nearest neighbor search my @paths = (); my $last_pos = Slic3r::Point->new(0,0); foreach my $loop (@loops) { diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index 27f5dbe53..5398be0a8 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -21,7 +21,7 @@ has 'avoid_crossing_perimeters' => (is => 'rw', default => sub { Slic3r::GCode:: has 'enable_loop_clipping' => (is => 'rw', default => sub {1}); has 'enable_cooling_markers' => (is =>'rw', default => sub {0}); has 'layer_count' => (is => 'ro'); -has '_layer_index' => (is => 'rw', default => sub {-1}); # just a counter +has 'layer_index' => (is => 'rw', default => sub {-1}); # just a counter has 'layer' => (is => 'rw'); has '_seam_position' => (is => 'ro', default => sub { {} }); # $object => pos has 'first_layer' => (is => 'rw', default => sub {0}); # this flag triggers first layer speeds @@ -76,7 +76,7 @@ sub change_layer { my ($self, $layer) = @_; $self->layer($layer); - $self->_layer_index($self->_layer_index + 1); + $self->layer_index($self->layer_index + 1); $self->first_layer($layer->id == 0); # avoid computing islands and overhangs if they're not needed @@ -88,14 +88,14 @@ sub change_layer { my $gcode = ""; if (defined $self->layer_count) { - $gcode .= $self->writer->update_progress($self->_layer_index, $self->layer_count); + $gcode .= $self->writer->update_progress($self->layer_index, $self->layer_count); } my $z = $layer->print_z + $self->config->z_offset; # in unscaled coordinates if ($self->config->get_at('retract_layer_change', $self->writer->extruder->id) && $self->writer->will_move_z($z)) { $gcode .= $self->retract; } - $gcode .= $self->writer->travel_to_z($z, 'move to next layer (' . $self->layer->id . ')'); + $gcode .= $self->writer->travel_to_z($z, 'move to next layer (' . $self->layer_index . ')'); # forget last wiping path as wiping after raising Z is pointless $self->wipe->path(undef); diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 88dafd467..a7d8fdfe6 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -81,7 +81,7 @@ sub OnInit { $self->{notifier} = Slic3r::GUI::Notifier->new; # locate or create data directory - $datadir ||= Wx::StandardPaths::Get->GetUserDataDir; + $datadir ||= Slic3r::decode_path(Wx::StandardPaths::Get->GetUserDataDir); my $enc_datadir = Slic3r::encode_path($datadir); Slic3r::debugf "Data directory: %s\n", $datadir; @@ -297,7 +297,7 @@ sub open_model { $dialog->Destroy; return; } - my @input_files = $dialog->GetPaths; + my @input_files = map Slic3r::decode_path($_), $dialog->GetPaths; $dialog->Destroy; return @input_files; diff --git a/lib/Slic3r/GUI/3DScene.pm b/lib/Slic3r/GUI/3DScene.pm index 794de1fc1..7628a6c54 100644 --- a/lib/Slic3r/GUI/3DScene.pm +++ b/lib/Slic3r/GUI/3DScene.pm @@ -593,7 +593,7 @@ sub Resize { -$x/2, $x/2, -$y/2, $y/2, -$depth, 2*$depth, ); - + glMatrixMode(GL_MODELVIEW); } @@ -937,7 +937,7 @@ package Slic3r::GUI::3DScene; use base qw(Slic3r::GUI::3DScene::Base); use OpenGL qw(:glconstants :gluconstants :glufunctions); -use List::Util qw(first); +use List::Util qw(first min max); use Slic3r::Geometry qw(scale unscale epsilon); use Slic3r::Print::State ':steps'; @@ -1079,6 +1079,59 @@ sub load_print_object_slices { ); } +sub load_print_toolpaths { + my ($self, $print) = @_; + + return if !$print->step_done(STEP_SKIRT); + return if !$print->step_done(STEP_BRIM); + return if !$print->has_skirt && $print->config->brim_width == 0; + + my $qverts = Slic3r::GUI::_3DScene::GLVertexArray->new; + my $tverts = Slic3r::GUI::_3DScene::GLVertexArray->new; + my %offsets = (); # print_z => [ qverts, tverts ] + + my $skirt_height = 0; # number of layers + if ($print->has_infinite_skirt) { + $skirt_height = $print->total_layer_count; + } else { + $skirt_height = min($print->config->skirt_height, $print->total_layer_count); + } + $skirt_height ||= 1 if $print->config->brim_width > 0; + + # get first $skirt_height layers (maybe this should be moved to a PrintObject method?) + my $object0 = $print->get_object(0); + my @layers = (); + push @layers, map $object0->get_layer($_-1), 1..min($skirt_height, $object0->layer_count); + push @layers, map $object0->get_support_layer($_-1), 1..min($skirt_height, $object0->support_layer_count); + @layers = sort { $a->print_z <=> $b->print_z } @layers; + @layers = @layers[0..($skirt_height-1)]; + + foreach my $i (0..($skirt_height-1)) { + my $top_z = $layers[$i]->print_z; + $offsets{$top_z} = [$qverts->size, $tverts->size]; + + if ($i == 0) { + $self->_extrusionentity_to_verts($print->brim, $top_z, Slic3r::Point->new(0,0), $qverts, $tverts); + } + + $self->_extrusionentity_to_verts($print->skirt, $top_z, Slic3r::Point->new(0,0), $qverts, $tverts); + } + + my $bb = Slic3r::Geometry::BoundingBoxf3->new; + { + my $pbb = $print->bounding_box; + $bb->merge_point(Slic3r::Pointf3->new_unscale(@{$pbb->min_point})); + $bb->merge_point(Slic3r::Pointf3->new_unscale(@{$pbb->max_point})); + } + push @{$self->volumes}, Slic3r::GUI::3DScene::Volume->new( + bounding_box => $bb, + color => COLORS->[2], + qverts => $qverts, + tverts => $tverts, + offsets => { %offsets }, + ); +} + sub load_print_object_toolpaths { my ($self, $object) = @_; diff --git a/lib/Slic3r/GUI/AboutDialog.pm b/lib/Slic3r/GUI/AboutDialog.pm index b64bbea40..815da9e15 100644 --- a/lib/Slic3r/GUI/AboutDialog.pm +++ b/lib/Slic3r/GUI/AboutDialog.pm @@ -3,7 +3,7 @@ use strict; use warnings; use utf8; -use Wx qw(:font :html :misc :sizer :systemsettings); +use Wx qw(:font :html :misc :dialog :sizer :systemsettings); use Wx::Event qw(EVT_HTML_LINK_CLICKED); use Wx::Print; use Wx::Html; @@ -12,7 +12,7 @@ use base 'Wx::Dialog'; sub new { my $class = shift; my ($parent) = @_; - my $self = $class->SUPER::new($parent, -1, 'About Slic3r', wxDefaultPosition, [600, 300]); + my $self = $class->SUPER::new($parent, -1, 'About Slic3r', wxDefaultPosition, [600, 300], &Wx::wxCLOSE_BOX); $self->SetBackgroundColour(Wx::wxWHITE); my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL); diff --git a/lib/Slic3r/GUI/BedShapeDialog.pm b/lib/Slic3r/GUI/BedShapeDialog.pm index 7b5c3ca92..fa6429023 100644 --- a/lib/Slic3r/GUI/BedShapeDialog.pm +++ b/lib/Slic3r/GUI/BedShapeDialog.pm @@ -386,7 +386,7 @@ sub _load_stl { $dialog->Destroy; return; } - my $input_file = $dialog->GetPaths; + my $input_file = Slic3r::decode_path($dialog->GetPaths); $dialog->Destroy; my $model = Slic3r::Model->read_from_file($input_file); diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 9882888c0..ab63510f6 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -141,8 +141,8 @@ sub _init_tabpanel { if ($self->{plater}) { $self->{plater}->on_select_preset(sub { - my ($group, $preset) = @_; - $self->{options_tabs}{$group}->select_preset($preset); + my ($group, $i) = @_; + $self->{options_tabs}{$group}->select_preset($i); }); # load initial config @@ -158,38 +158,44 @@ sub _init_menubar { { $self->_append_menu_item($fileMenu, "&Load Config…\tCtrl+L", 'Load exported configuration file', sub { $self->load_config_file; - }); + }, undef, 'plugin_add.png'); $self->_append_menu_item($fileMenu, "&Export Config…\tCtrl+E", 'Export current configuration to file', sub { $self->export_config; - }); + }, undef, 'plugin_go.png'); $self->_append_menu_item($fileMenu, "&Load Config Bundle…", 'Load presets from a bundle', sub { $self->load_configbundle; - }); + }, undef, 'lorry_add.png'); $self->_append_menu_item($fileMenu, "&Export Config Bundle…", 'Export all presets to file', sub { $self->export_configbundle; - }); + }, undef, 'lorry_go.png'); $fileMenu->AppendSeparator(); my $repeat; $self->_append_menu_item($fileMenu, "Q&uick Slice…\tCtrl+U", 'Slice file', sub { - $self->quick_slice; - $repeat->Enable(defined $Slic3r::GUI::MainFrame::last_input_file); - }); + wxTheApp->CallAfter(sub { + $self->quick_slice; + $repeat->Enable(defined $Slic3r::GUI::MainFrame::last_input_file); + }); + }, undef, 'cog_go.png'); $self->_append_menu_item($fileMenu, "Quick Slice and Save &As…\tCtrl+Alt+U", 'Slice file and save as', sub { - $self->quick_slice(save_as => 1); - $repeat->Enable(defined $Slic3r::GUI::MainFrame::last_input_file); - }); + wxTheApp->CallAfter(sub { + $self->quick_slice(save_as => 1); + $repeat->Enable(defined $Slic3r::GUI::MainFrame::last_input_file); + }); + }, undef, 'cog_go.png'); $repeat = $self->_append_menu_item($fileMenu, "&Repeat Last Quick Slice\tCtrl+Shift+U", 'Repeat last quick slice', sub { - $self->quick_slice(reslice => 1); - }); + wxTheApp->CallAfter(sub { + $self->quick_slice(reslice => 1); + }); + }, undef, 'cog_go.png'); $repeat->Enable(0); $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, "Slice to SV&G…\tCtrl+G", 'Slice file to SVG', sub { $self->quick_slice(save_as => 1, export_svg => 1); - }); + }, undef, 'shape_handles.png'); $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, "Repair STL file…", 'Automatically repair an STL file', sub { $self->repair_stl; - }); + }, undef, 'wrench.png'); $fileMenu->AppendSeparator(); $self->_append_menu_item($fileMenu, "Preferences…", 'Application preferences', sub { Slic3r::GUI::Preferences->new($self)->ShowModal; @@ -207,13 +213,13 @@ sub _init_menubar { $self->{plater_menu} = Wx::Menu->new; $self->_append_menu_item($self->{plater_menu}, "Export G-code...", 'Export current plate as G-code', sub { $plater->export_gcode; - }); + }, undef, 'cog_go.png'); $self->_append_menu_item($self->{plater_menu}, "Export plate as STL...", 'Export current plate as STL', sub { $plater->export_stl; - }); + }, undef, 'brick_go.png'); $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->{object_menu} = $self->{plater}->object_menu; $self->on_plater_selection_changed(0); @@ -226,22 +232,22 @@ sub _init_menubar { if (!$self->{no_plater}) { $self->_append_menu_item($windowMenu, "Select &Plater Tab\tCtrl+1", 'Show the plater', sub { $self->select_tab(0); - }); + }, undef, 'application_view_tile.png'); $self->_append_menu_item($windowMenu, "Select &Controller Tab\tCtrl+T", 'Show the printer controller', sub { $self->select_tab(1); - }); + }, undef, 'printer_empty.png'); $windowMenu->AppendSeparator(); $tab_offset += 2; } $self->_append_menu_item($windowMenu, "Select P&rint Settings Tab\tCtrl+2", 'Show the print settings', sub { $self->select_tab($tab_offset+0); - }); + }, undef, 'cog.png'); $self->_append_menu_item($windowMenu, "Select &Filament Settings Tab\tCtrl+3", 'Show the filament settings', sub { $self->select_tab($tab_offset+1); - }); + }, undef, 'spool.png'); $self->_append_menu_item($windowMenu, "Select Print&er Settings Tab\tCtrl+4", 'Show the printer settings', sub { $self->select_tab($tab_offset+2); - }); + }, undef, 'printer_empty.png'); } # Help menu @@ -313,7 +319,7 @@ sub quick_slice { $dialog->Destroy; return; } - $input_file = $dialog->GetPaths; + $input_file = Slic3r::decode_path($dialog->GetPaths); $dialog->Destroy; $last_input_file = $input_file unless $params{export_svg}; } else { @@ -373,7 +379,7 @@ sub quick_slice { $dlg->Destroy; return; } - $output_file = $dlg->GetPath; + $output_file = Slic3r::decode_path($dlg->GetPath); $last_output_file = $output_file unless $params{export_svg}; $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($output_file); wxTheApp->save_settings; @@ -420,7 +426,7 @@ sub repair_stl { $dialog->Destroy; return; } - $input_file = $dialog->GetPaths; + $input_file = Slic3r::decode_path($dialog->GetPaths); $dialog->Destroy; } @@ -433,7 +439,7 @@ sub repair_stl { $dlg->Destroy; return undef; } - $output_file = $dlg->GetPath; + $output_file = Slic3r::decode_path($dlg->GetPath); $dlg->Destroy; } @@ -470,7 +476,7 @@ sub export_config { my $dlg = Wx::FileDialog->new($self, 'Save configuration as:', $dir, $filename, &Slic3r::GUI::FILE_WILDCARDS->{ini}, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if ($dlg->ShowModal == wxID_OK) { - my $file = $dlg->GetPath; + my $file = Slic3r::decode_path($dlg->GetPath); $Slic3r::GUI::Settings->{recent}{config_directory} = dirname($file); wxTheApp->save_settings; $last_config = $file; @@ -489,7 +495,7 @@ sub load_config_file { my $dlg = Wx::FileDialog->new($self, 'Select configuration to load:', $dir, "config.ini", &Slic3r::GUI::FILE_WILDCARDS->{ini}, wxFD_OPEN | wxFD_FILE_MUST_EXIST); return unless $dlg->ShowModal == wxID_OK; - ($file) = $dlg->GetPaths; + $file = Slic3r::decode_path($dlg->GetPaths); $dlg->Destroy; } $Slic3r::GUI::Settings->{recent}{config_directory} = dirname($file); @@ -514,7 +520,7 @@ sub export_configbundle { my $dlg = Wx::FileDialog->new($self, 'Save presets bundle as:', $dir, $filename, &Slic3r::GUI::FILE_WILDCARDS->{ini}, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); if ($dlg->ShowModal == wxID_OK) { - my $file = $dlg->GetPath; + my $file = Slic3r::decode_path($dlg->GetPath); $Slic3r::GUI::Settings->{recent}{config_directory} = dirname($file); wxTheApp->save_settings; @@ -547,7 +553,7 @@ sub load_configbundle { my $dlg = Wx::FileDialog->new($self, 'Select configuration to load:', $dir, "config.ini", &Slic3r::GUI::FILE_WILDCARDS->{ini}, wxFD_OPEN | wxFD_FILE_MUST_EXIST); return unless $dlg->ShowModal == wxID_OK; - my ($file) = $dlg->GetPaths; + my $file = Slic3r::decode_path($dlg->GetPaths); $dlg->Destroy; $Slic3r::GUI::Settings->{recent}{config_directory} = dirname($file); @@ -601,6 +607,9 @@ sub load_config { foreach my $tab (values %{$self->{options_tabs}}) { $tab->load_config($config); } + if ($self->{plater}) { + $self->{plater}->on_config_change($config); + } } sub config_wizard { @@ -643,13 +652,19 @@ sub config { if (!$self->{plater} || $self->{plater}->filament_presets == 1 || $self->{mode} eq 'simple') { $filament_config = $self->{options_tabs}{filament}->config; } else { - # TODO: handle dirty presets. - # perhaps plater shouldn't expose dirty presets at all in multi-extruder environments. my $i = -1; foreach my $preset_idx ($self->{plater}->filament_presets) { $i++; - my $preset = $self->{options_tabs}{filament}->get_preset($preset_idx); - my $config = $self->{options_tabs}{filament}->get_preset_config($preset); + my $config; + if ($preset_idx == $self->{options_tabs}{filament}->current_preset) { + # the selected preset for this extruder is the one in the tab + # use the tab's config instead of the preset in case it is dirty + # perhaps plater shouldn't expose dirty presets at all in multi-extruder environments. + $config = $self->{options_tabs}{filament}->config; + } else { + my $preset = $self->{options_tabs}{filament}->get_preset($preset_idx); + $config = $self->{options_tabs}{filament}->get_preset_config($preset); + } if (!$filament_config) { $filament_config = $config->clone; next; @@ -719,12 +734,23 @@ sub select_tab { } sub _append_menu_item { - my ($self, $menu, $string, $description, $cb, $id) = @_; + my ($self, $menu, $string, $description, $cb, $id, $icon) = @_; $id //= &Wx::NewId(); my $item = $menu->Append($id, $string, $description); + $self->_set_menu_item_icon($item, $icon); + EVT_MENU($self, $id, $cb); return $item; } +sub _set_menu_item_icon { + my ($self, $menuItem, $icon) = @_; + + # SetBitmap was not available on OS X before Wx 0.9927 + if ($icon && $menuItem->can('SetBitmap')) { + $menuItem->SetBitmap(Wx::Bitmap->new("$Slic3r::var/$icon", wxBITMAP_TYPE_PNG)); + } +} + 1; diff --git a/lib/Slic3r/GUI/OptionsGroup.pm b/lib/Slic3r/GUI/OptionsGroup.pm index e8f02ba68..f73fdf697 100644 --- a/lib/Slic3r/GUI/OptionsGroup.pm +++ b/lib/Slic3r/GUI/OptionsGroup.pm @@ -178,6 +178,11 @@ sub _build_field { parent => $self->parent, option => $opt, ); + } elsif ($type eq 'color') { + $field = Slic3r::GUI::OptionsGroup::Field::ColourPicker->new( + parent => $self->parent, + option => $opt, + ); } elsif ($type =~ /^(f|s|s@|percent)$/) { $field = Slic3r::GUI::OptionsGroup::Field::TextCtrl->new( parent => $self->parent, diff --git a/lib/Slic3r/GUI/OptionsGroup/Field.pm b/lib/Slic3r/GUI/OptionsGroup/Field.pm index 8a181355d..18f5b2b56 100644 --- a/lib/Slic3r/GUI/OptionsGroup/Field.pm +++ b/lib/Slic3r/GUI/OptionsGroup/Field.pm @@ -7,7 +7,6 @@ has 'parent' => (is => 'ro', required => 1); has 'option' => (is => 'ro', required => 1); # Slic3r::GUI::OptionsGroup::Option has 'on_change' => (is => 'rw', default => sub { sub {} }); has 'on_kill_focus' => (is => 'rw', default => sub { sub {} }); -has 'wxSsizer' => (is => 'rw'); # alternatively, wxSizer object has 'disable_change_event' => (is => 'rw', default => sub { 0 }); # This method should not fire the on_change event @@ -128,6 +127,8 @@ extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; use Wx qw(:misc); use Wx::Event qw(EVT_SPINCTRL EVT_TEXT EVT_KILL_FOCUS); +has 'tmp_value' => (is => 'rw'); + sub BUILD { my ($self) = @_; @@ -139,13 +140,27 @@ sub BUILD { $self->_on_change($self->option->opt_id); }); EVT_TEXT($self->parent, $field, sub { + my ($s, $event) = @_; + + # On OSX/Cocoa, wxSpinCtrl::GetValue() doesn't return the new value + # when it was changed from the text control, so the on_change callback + # gets the old one, and on_kill_focus resets the control to the old value. + # As a workaround, we get the new value from $event->GetString and store + # here temporarily so that we can return it from $self->get_value + $self->tmp_value($event->GetString); $self->_on_change($self->option->opt_id); + $self->tmp_value(undef); }); EVT_KILL_FOCUS($field, sub { $self->_on_kill_focus($self->option->opt_id, @_); }); } +sub get_value { + my ($self) = @_; + return $self->tmp_value // $self->wxWindow->GetValue; +} + package Slic3r::GUI::OptionsGroup::Field::TextCtrl; use Moo; @@ -261,6 +276,9 @@ use List::Util qw(first); use Wx qw(wxTheApp :misc :combobox); use Wx::Event qw(EVT_COMBOBOX EVT_TEXT); +# if option has no 'values', indices are values +# if option has no 'labels', values are labels + sub BUILD { my ($self) = @_; @@ -274,15 +292,15 @@ sub BUILD { my $disable_change_event = $self->disable_change_event; $self->disable_change_event(1); - my $value = $field->GetSelection; + my $idx = $field->GetSelection; # get index of selected value my $label; - if ($self->option->values) { - $label = $value = $self->option->values->[$value]; - } elsif ($value <= $#{$self->option->labels}) { - $label = $self->option->labels->[$value]; + if ($self->option->labels && $idx <= $#{$self->option->labels}) { + $label = $self->option->labels->[$idx]; + } elsif ($self->option->values && $idx <= $#{$self->option->values}) { + $label = $self->option->values->[$idx]; } else { - $label = $value; + $label = $idx; } # The MSW implementation of wxComboBox will leave the field blank if we call @@ -322,8 +340,8 @@ sub set_value { $self->disable_change_event(0); return; } - } - if ($self->option->labels && $value <= $#{$self->option->labels}) { + } elsif ($self->option->labels && $value <= $#{$self->option->labels}) { + # if we have no values, we expect value to be an index $field->SetValue($self->option->labels->[$value]); $self->disable_change_event(0); return; @@ -351,6 +369,47 @@ sub get_value { } +package Slic3r::GUI::OptionsGroup::Field::ColourPicker; +use Moo; +extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; + +use Wx qw(:misc :colour); +use Wx::Event qw(EVT_COLOURPICKER_CHANGED); + +sub BUILD { + my ($self) = @_; + + my $field = Wx::ColourPickerCtrl->new($self->parent, -1, + $self->_string_to_colour($self->option->default), wxDefaultPosition, + $self->_default_size); + $self->wxWindow($field); + + EVT_COLOURPICKER_CHANGED($self->parent, $field, sub { + $self->_on_change($self->option->opt_id); + }); +} + +sub set_value { + my ($self, $value) = @_; + + $self->disable_change_event(1); + $self->wxWindow->SetColour($self->_string_to_colour($value)); + $self->disable_change_event(0); +} + +sub get_value { + my ($self) = @_; + return $self->wxWindow->GetColour->GetAsString(wxC2S_HTML_SYNTAX); +} + +sub _string_to_colour { + my ($self, $string) = @_; + + $string =~ s/^#//; + return Wx::Colour->new(unpack 'C*', pack 'H*', $string); +} + + package Slic3r::GUI::OptionsGroup::Field::wxSizer; use Moo; extends 'Slic3r::GUI::OptionsGroup::Field'; diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index bb344e50e..d38c83d6f 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -4,14 +4,14 @@ use warnings; use utf8; use File::Basename qw(basename dirname); -use List::Util qw(sum first); +use List::Util qw(sum first max); use Slic3r::Geometry qw(X Y Z MIN MAX scale unscale deg2rad); use threads::shared qw(shared_clone); use Wx qw(:button :cursor :dialog :filedialog :keycode :icon :font :id :listctrl :misc - :panel :sizer :toolbar :window wxTheApp :notebook); + :panel :sizer :toolbar :window wxTheApp :notebook :combobox); use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL - EVT_CHOICE EVT_TIMER EVT_NOTEBOOK_PAGE_CHANGED); + EVT_CHOICE EVT_COMBOBOX EVT_TIMER EVT_NOTEBOOK_PAGE_CHANGED); use base 'Wx::Panel'; use constant TB_ADD => &Wx::NewId; @@ -36,7 +36,7 @@ our $ERROR_EVENT : shared = Wx::NewEventType; our $EXPORT_COMPLETED_EVENT : shared = Wx::NewEventType; our $PROCESS_COMPLETED_EVENT : shared = Wx::NewEventType; -use constant FILAMENT_CHOOSERS_SPACING => 3; +use constant FILAMENT_CHOOSERS_SPACING => 0; use constant PROCESS_DELAY => 0.5 * 1000; # milliseconds my $PreventListEvents = 0; @@ -335,12 +335,11 @@ sub new { for my $group (qw(print filament printer)) { my $text = Wx::StaticText->new($self, -1, "$group_labels{$group}:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT); $text->SetFont($Slic3r::GUI::small_font); - my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, wxDefaultSize, []); - $choice->SetFont($Slic3r::GUI::small_font); + my $choice = Wx::BitmapComboBox->new($self, -1, "", wxDefaultPosition, wxDefaultSize, [], wxCB_READONLY); $self->{preset_choosers}{$group} = [$choice]; - EVT_CHOICE($choice, $choice, sub { $self->_on_select_preset($group, @_) }); + EVT_COMBOBOX($choice, $choice, sub { $self->_on_select_preset($group, @_) }); $presets->Add($text, 0, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); - $presets->Add($choice, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxBOTTOM, 8); + $presets->Add($choice, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxBOTTOM, 0); } } @@ -446,13 +445,33 @@ sub GetFrame { sub update_presets { my $self = shift; - my ($group, $items, $selected) = @_; + my ($group, $presets, $selected) = @_; foreach my $choice (@{ $self->{preset_choosers}{$group} }) { my $sel = $choice->GetSelection; $choice->Clear; - $choice->Append($_) for @$items; - $choice->SetSelection($sel) if $sel <= $#$items; + foreach my $preset (@$presets) { + my $bitmap; + if ($group eq 'filament') { + my $config = $preset->config(['filament_colour']); + my $rgb_hex = $config->filament_colour->[0]; + if ($preset->default) { + $bitmap = Wx::Bitmap->new("$Slic3r::var/spool.png", wxBITMAP_TYPE_PNG); + } else { + $rgb_hex =~ s/^#//; + my @rgb = unpack 'C*', pack 'H*', $rgb_hex; + my $image = Wx::Image->new(16,16); + $image->SetRGB(Wx::Rect->new(0,0,16,16), @rgb); + $bitmap = Wx::Bitmap->new($image); + } + } elsif ($group eq 'print') { + $bitmap = Wx::Bitmap->new("$Slic3r::var/cog.png", wxBITMAP_TYPE_PNG); + } elsif ($group eq 'printer') { + $bitmap = Wx::Bitmap->new("$Slic3r::var/printer_empty.png", wxBITMAP_TYPE_PNG); + } + $choice->AppendString($preset->name, $bitmap); + } + $choice->SetSelection($sel) if $sel <= $#$presets; } $self->{preset_choosers}{$group}[0]->SetSelection($selected); } @@ -497,8 +516,11 @@ sub load_model_objects { my ($self, @model_objects) = @_; my $bed_centerf = $self->bed_centerf; + my $bed_shape = Slic3r::Polygon->new_scale(@{$self->{config}->bed_shape}); + my $bed_size = $bed_shape->bounding_box->size; my $need_arrange = 0; + my $scaled_down = 0; my @obj_idx = (); foreach my $model_object (@model_objects) { my $o = $self->{model}->add_object($model_object); @@ -516,6 +538,16 @@ sub load_model_objects { $o->center_around_origin; # also aligns object to Z = 0 $o->add_instance(offset => $bed_centerf); } + + { + # if the object is too large (more than 5 times the bed), scale it down + my $size = $o->bounding_box->size; + my $ratio = max(@$size[X,Y]) / unscale(max(@$bed_size[X,Y])); + if ($ratio > 5) { + $_->set_scaling_factor(1/$ratio) for @{$o->instances}; + $scaled_down = 1; + } + } $self->{print}->auto_assign_extruders($o); $self->{print}->add_model_object($o); @@ -526,14 +558,15 @@ sub load_model_objects { $need_arrange = 0; } - $self->objects_loaded(\@obj_idx, no_arrange => !$need_arrange); -} - -sub objects_loaded { - my $self = shift; - my ($obj_idxs, %params) = @_; + if ($scaled_down) { + Slic3r::GUI::show_info( + $self, + 'Your object appears to be too large, so it was automatically scaled down to fit your print bed.', + 'Object too large?', + ); + } - foreach my $obj_idx (@$obj_idxs) { + foreach my $obj_idx (@obj_idx) { my $object = $self->{objects}[$obj_idx]; my $model_object = $self->{model}->objects->[$obj_idx]; $self->{list}->InsertStringItem($obj_idx, $object->name); @@ -545,7 +578,7 @@ sub objects_loaded { $self->make_thumbnail($obj_idx); } - $self->arrange unless $params{no_arrange}; + $self->arrange if $need_arrange; $self->update; # zoom to objects @@ -553,7 +586,7 @@ sub objects_loaded { if $self->{canvas3D}; $self->{list}->Update; - $self->{list}->Select($obj_idxs->[-1], 1); + $self->{list}->Select($obj_idx[-1], 1); $self->object_list_changed; $self->schedule_background_process; @@ -691,6 +724,7 @@ sub rotate { my $self = shift; my ($angle, $axis) = @_; + # angle is in degrees $axis //= Z; my ($obj_idx, $object) = $self->selected_object; @@ -712,14 +746,14 @@ sub rotate { $self->stop_background_process; if ($axis == Z) { - my $new_angle = $model_instance->rotation + $angle; + my $new_angle = $model_instance->rotation + deg2rad($angle); $_->set_rotation($new_angle) for @{ $model_object->instances }; $object->transform_thumbnail($self->{model}, $obj_idx); } else { # rotation around X and Y needs to be performed on mesh # so we first apply any Z rotation if ($model_instance->rotation != 0) { - $model_object->rotate(deg2rad($model_instance->rotation), Z); + $model_object->rotate($model_instance->rotation, Z); $_->set_rotation(0) for @{ $model_object->instances }; } $model_object->rotate(deg2rad($angle), $axis); @@ -749,7 +783,7 @@ sub flip { # apply Z rotation before flipping if ($model_instance->rotation != 0) { - $model_object->rotate(deg2rad($model_instance->rotation), Z); + $model_object->rotate($model_instance->rotation, Z); $_->set_rotation(0) for @{ $model_object->instances }; } @@ -788,7 +822,7 @@ sub changescale { # apply Z rotation before scaling if ($model_instance->rotation != 0) { - $model_object->rotate(deg2rad($model_instance->rotation), Z); + $model_object->rotate($model_instance->rotation, Z); $_->set_rotation(0) for @{ $model_object->instances }; } @@ -1054,9 +1088,10 @@ sub export_gcode { $dlg->Destroy; return; } - $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($dlg->GetPath); + my $path = Slic3r::decode_path($dlg->GetPath); + $Slic3r::GUI::Settings->{_}{last_output_path} = dirname($path); wxTheApp->save_settings; - $self->{export_gcode_output_file} = $Slic3r::GUI::MainFrame::last_output_file = $dlg->GetPath; + $self->{export_gcode_output_file} = $Slic3r::GUI::MainFrame::last_output_file = $path; $dlg->Destroy; } @@ -1066,6 +1101,11 @@ sub export_gcode { $self->statusbar->SetCancelCallback(sub { $self->stop_background_process; $self->statusbar->SetStatusText("Export cancelled"); + $self->{export_gcode_output_file} = undef; + $self->{send_gcode_file} = undef; + + # this updates buttons status + $self->object_list_changed; }); # start background process, whose completion event handler @@ -1275,7 +1315,7 @@ sub _get_export_file { $dlg->Destroy; return undef; } - $output_file = $Slic3r::GUI::MainFrame::last_output_file = $dlg->GetPath; + $output_file = $Slic3r::GUI::MainFrame::last_output_file = Slic3r::decode_path($dlg->GetPath); $dlg->Destroy; } return $output_file; @@ -1344,11 +1384,10 @@ sub on_extruders_change { my $choices = $self->{preset_choosers}{filament}; while (@$choices < $num_extruders) { my @presets = $choices->[0]->GetStrings; - push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [@presets]); - $choices->[-1]->SetFont($Slic3r::GUI::small_font); + push @$choices, Wx::BitmapComboBox->new($self, -1, "", wxDefaultPosition, wxDefaultSize, [@presets], wxCB_READONLY); $self->{presets_sizer}->Insert(4 + ($#$choices-1)*2, 0, 0); $self->{presets_sizer}->Insert(5 + ($#$choices-1)*2, $choices->[-1], 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); - EVT_CHOICE($choices->[-1], $choices->[-1], sub { $self->_on_select_preset('filament', @_) }); + EVT_COMBOBOX($choices->[-1], $choices->[-1], sub { $self->_on_select_preset('filament', @_) }); my $i = first { $choices->[-1]->GetString($_) eq ($Slic3r::GUI::Settings->{presets}{"filament_" . $#$choices} || '') } 0 .. $#presets; $choices->[-1]->SetSelection($i || 0); } @@ -1616,26 +1655,27 @@ sub object_menu { my $menu = Wx::Menu->new; $frame->_append_menu_item($menu, "Delete\tCtrl+Del", 'Remove the selected object', sub { $self->remove; - }); + }, undef, 'brick_delete.png'); $frame->_append_menu_item($menu, "Increase copies\tCtrl++", 'Place one more copy of the selected object', sub { $self->increase; - }); + }, undef, 'add.png'); $frame->_append_menu_item($menu, "Decrease copies\tCtrl+-", 'Remove one copy of the selected object', sub { $self->decrease; - }); + }, undef, 'delete.png'); $frame->_append_menu_item($menu, "Set number of copies…", 'Change the number of copies of the selected object', sub { $self->set_number_of_copies; - }); + }, undef, 'textfield.png'); $menu->AppendSeparator(); $frame->_append_menu_item($menu, "Rotate 45° clockwise", 'Rotate the selected object by 45° clockwise', sub { $self->rotate(-45); - }); + }, undef, 'arrow_rotate_clockwise.png'); $frame->_append_menu_item($menu, "Rotate 45° counter-clockwise", 'Rotate the selected object by 45° counter-clockwise', sub { $self->rotate(+45); - }); + }, undef, 'arrow_rotate_anticlockwise.png'); my $rotateMenu = Wx::Menu->new; - $menu->AppendSubMenu($rotateMenu, "Rotate", 'Rotate the selected object by an arbitrary angle'); + my $rotateMenuItem = $menu->AppendSubMenu($rotateMenu, "Rotate", 'Rotate the selected object by an arbitrary angle'); + $frame->_set_menu_item_icon($rotateMenuItem, 'textfield.png'); $frame->_append_menu_item($rotateMenu, "Around X axis…", 'Rotate the selected object by an arbitrary angle around X axis', sub { $self->rotate(undef, X); }); @@ -1647,7 +1687,8 @@ sub object_menu { }); my $flipMenu = Wx::Menu->new; - $menu->AppendSubMenu($flipMenu, "Flip", 'Mirror the selected object'); + my $flipMenuItem = $menu->AppendSubMenu($flipMenu, "Flip", 'Mirror the selected object'); + $frame->_set_menu_item_icon($flipMenuItem, 'shape_flip_horizontal.png'); $frame->_append_menu_item($flipMenu, "Along X axis…", 'Mirror the selected object along the X axis', sub { $self->flip(X); }); @@ -1659,7 +1700,8 @@ sub object_menu { }); my $scaleMenu = Wx::Menu->new; - $menu->AppendSubMenu($scaleMenu, "Scale", 'Scale the selected object along a single axis'); + my $scaleMenuItem = $menu->AppendSubMenu($scaleMenu, "Scale", 'Scale the selected object along a single axis'); + $frame->_set_menu_item_icon($scaleMenuItem, 'arrow_out.png'); $frame->_append_menu_item($scaleMenu, "Uniformly…", 'Scale the selected object along the XYZ axes', sub { $self->changescale(undef); }); @@ -1675,18 +1717,18 @@ sub object_menu { $frame->_append_menu_item($menu, "Split", 'Split the selected object into individual parts', sub { $self->split_object; - }); + }, undef, 'shape_ungroup.png'); $frame->_append_menu_item($menu, "Cut…", 'Open the 3D cutting tool', sub { $self->object_cut_dialog; - }); + }, undef, 'package.png'); $menu->AppendSeparator(); $frame->_append_menu_item($menu, "Settings…", 'Open the object editor dialog', sub { $self->object_settings_dialog; - }); + }, undef, 'cog.png'); $menu->AppendSeparator(); $frame->_append_menu_item($menu, "Export object as STL…", 'Export this single object as STL file', sub { $self->export_object_stl; - }); + }, undef, 'brick_go.png'); return $menu; } @@ -1763,7 +1805,7 @@ sub transform_thumbnail { # the order of these transformations MUST be the same everywhere, including # in Slic3r::Print->add_model_object() my $t = $self->thumbnail->clone; - $t->rotate(deg2rad($model_instance->rotation), Slic3r::Point->new(0,0)); + $t->rotate($model_instance->rotation, Slic3r::Point->new(0,0)); $t->scale($model_instance->scaling_factor); $self->transformed_thumbnail($t); diff --git a/lib/Slic3r/GUI/Plater/2DToolpaths.pm b/lib/Slic3r/GUI/Plater/2DToolpaths.pm index 8ab289855..3daa3b94c 100644 --- a/lib/Slic3r/GUI/Plater/2DToolpaths.pm +++ b/lib/Slic3r/GUI/Plater/2DToolpaths.pm @@ -84,6 +84,7 @@ sub reload_print { } $self->{canvas}->bb($self->print->total_bounding_box); + $self->{canvas}->_dirty(1); my %z = (); # z => 1 foreach my $object (@{$self->{print}->objects}) { @@ -115,15 +116,23 @@ sub set_z { package Slic3r::GUI::Plater::2DToolpaths::Canvas; -use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_ERASE_BACKGROUND EVT_MOUSEWHEEL EVT_MOUSE_EVENTS); +use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_IDLE EVT_MOUSEWHEEL EVT_MOUSE_EVENTS); use OpenGL qw(:glconstants :glfunctions :glufunctions :gluconstants); use base qw(Wx::GLCanvas Class::Accessor); use Wx::GLCanvas qw(:all); -use List::Util qw(min first); -use Slic3r::Geometry qw(scale unscale epsilon); +use List::Util qw(min max first); +use Slic3r::Geometry qw(scale unscale epsilon X Y); use Slic3r::Print::State ':steps'; -__PACKAGE__->mk_accessors(qw(print z layers color init bb)); +__PACKAGE__->mk_accessors(qw( + print z layers color init + bb + _camera_bb + _dirty + _zoom + _camera_target + _drag_start_xy +)); # make OpenGL::Array thread-safe { @@ -136,20 +145,99 @@ sub new { my $self = $class->SUPER::new($parent); $self->print($print); + $self->_zoom(1); + + # 2D point in model space + $self->_camera_target(Slic3r::Pointf->new(0,0)); EVT_PAINT($self, sub { my $dc = Wx::PaintDC->new($self); $self->Render($dc); }); - EVT_SIZE($self, sub { + EVT_SIZE($self, sub { $self->_dirty(1) }); + EVT_IDLE($self, sub { + return unless $self->_dirty; return if !$self->IsShownOnScreen; - $self->Resize( $self->GetSizeWH ); + $self->Resize; $self->Refresh; }); + EVT_MOUSEWHEEL($self, sub { + my ($self, $e) = @_; + + my $old_zoom = $self->_zoom; + + # Calculate the zoom delta and apply it to the current zoom factor + my $zoom = $e->GetWheelRotation() / $e->GetWheelDelta(); + $zoom = max(min($zoom, 4), -4); + $zoom /= 10; + $self->_zoom($self->_zoom / (1-$zoom)); + $self->_zoom(1) if $self->_zoom > 1; # prevent from zooming out too much + + { + # In order to zoom around the mouse point we need to translate + # the camera target. This math is almost there but not perfect yet... + my $camera_bb_size = $self->_camera_bb->size; + my $size = Slic3r::Pointf->new($self->GetSizeWH); + my $pos = Slic3r::Pointf->new($e->GetPositionXY); + + # calculate the zooming center in pixel coordinates relative to the viewport center + my $vec = Slic3r::Pointf->new($pos->x - $size->x/2, $pos->y - $size->y/2); #- + + # calculate where this point will end up after applying the new zoom + my $vec2 = $vec->clone; + $vec2->scale($old_zoom / $self->_zoom); + + # move the camera target by the difference of the two positions + $self->_camera_target->translate( + -($vec->x - $vec2->x) * $camera_bb_size->x / $size->x, + ($vec->y - $vec2->y) * $camera_bb_size->y / $size->y, #// + ); + } + + $self->_dirty(1); + $self->Refresh; + }); + EVT_MOUSE_EVENTS($self, \&mouse_event); return $self; } +sub mouse_event { + my ($self, $e) = @_; + + my $pos = Slic3r::Pointf->new($e->GetPositionXY); + if ($e->Entering && &Wx::wxMSW) { + # wxMSW needs focus in order to catch mouse wheel events + $self->SetFocus; + } elsif ($e->Dragging) { + if ($e->LeftIsDown || $e->MiddleIsDown || $e->RightIsDown) { + # if dragging, translate view + + if (defined $self->_drag_start_xy) { + my $move = $self->_drag_start_xy->vector_to($pos); # in pixels + + # get viewport and camera size in order to convert pixel to model units + my ($x, $y) = $self->GetSizeWH; + my $camera_bb_size = $self->_camera_bb->size; + + # compute translation in model units + $self->_camera_target->translate( + -$move->x * $camera_bb_size->x / $x, + $move->y * $camera_bb_size->y / $y, # /** + ); + + $self->_dirty(1); + $self->Refresh; + } + $self->_drag_start_xy($pos); + } + } elsif ($e->LeftUp || $e->MiddleUp || $e->RightUp) { + $self->_drag_start_xy(undef); + } else { + $e->Skip(); + } +} + sub set_z { my ($self, $z) = @_; @@ -197,22 +285,6 @@ sub Render { return; } - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - my $bb = $self->bb; - my ($x1, $y1, $x2, $y2) = ($bb->x_min, $bb->y_min, $bb->x_max, $bb->y_max); - my ($x, $y) = $self->GetSizeWH; - if (($x2 - $x1)/($y2 - $y1) > $x/$y) { - # adjust Y - my $new_y = $y * ($x2 - $x1) / $x; - $y1 = ($y2 + $y1)/2 - $new_y/2; - $y2 = $y1 + $new_y; - } else { - my $new_x = $x * ($y2 - $y1) / $y; - $x1 = ($x2 + $x1)/2 - $new_x/2; - $x2 = $x1 + $new_x; - } - glOrtho($x1, $x2, $y1, $y2, 0, 1); glDisable(GL_DEPTH_TEST); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); @@ -398,12 +470,71 @@ sub SetCurrent { } sub Resize { - my ($self, $x, $y) = @_; + my ($self) = @_; return unless $self->GetContext; + return unless $self->bb; + $self->_dirty(0); $self->SetCurrent($self->GetContext); + my ($x, $y) = $self->GetSizeWH; glViewport(0, 0, $x, $y); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + + my $bb = $self->bb->clone; + + # center bounding box around origin before scaling it + my $bb_center = $bb->center; + $bb->translate(@{$bb_center->negative}); + + # scale bounding box according to zoom factor + $bb->scale($self->_zoom); + + # reposition bounding box around original center + $bb->translate(@{$bb_center}); + + # translate camera + $bb->translate(@{$self->_camera_target}); + + # keep camera_bb within total bb + # (i.e. prevent user from panning outside the bounding box) + { + my @translate = (0,0); + if ($bb->x_min < $self->bb->x_min) { + $translate[X] += $self->bb->x_min - $bb->x_min; + } + if ($bb->y_min < $self->bb->y_min) { + $translate[Y] += $self->bb->y_min - $bb->y_min; + } + if ($bb->x_max > $self->bb->x_max) { + $translate[X] -= $bb->x_max - $self->bb->x_max; + } + if ($bb->y_max > $self->bb->y_max) { + $translate[Y] -= $bb->y_max - $self->bb->y_max; + } + $self->_camera_target->translate(@translate); + $bb->translate(@translate); + } + + # save camera + $self->_camera_bb($bb); + + my ($x1, $y1, $x2, $y2) = ($bb->x_min, $bb->y_min, $bb->x_max, $bb->y_max); + if (($x2 - $x1)/($y2 - $y1) > $x/$y) { + # adjust Y + my $new_y = $y * ($x2 - $x1) / $x; + $y1 = ($y2 + $y1)/2 - $new_y/2; + $y2 = $y1 + $new_y; + } else { + my $new_x = $x * ($y2 - $y1) / $y; + $x1 = ($x2 + $x1)/2 - $new_x/2; + $x2 = $x1 + $new_x; + } + glOrtho($x1, $x2, $y1, $y2, 0, 1); + + glMatrixMode(GL_MODELVIEW); } sub line { diff --git a/lib/Slic3r/GUI/Plater/3DPreview.pm b/lib/Slic3r/GUI/Plater/3DPreview.pm index be9b5f572..596b5195f 100644 --- a/lib/Slic3r/GUI/Plater/3DPreview.pm +++ b/lib/Slic3r/GUI/Plater/3DPreview.pm @@ -116,6 +116,9 @@ sub load_print { } if ($self->IsShown) { + # load skirt and brim + $self->canvas->load_print_toolpaths($self->print); + foreach my $object (@{$self->print->objects}) { $self->canvas->load_print_object_toolpaths($object); diff --git a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm index c0f942f12..6437be4b9 100644 --- a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm @@ -161,6 +161,7 @@ sub perform_cut { push @{$self->{new_model_objects}}, $lower_object; if ($self->{cut_options}{rotate_lower}) { $lower_object->rotate(PI, X); + $lower_object->center_around_origin; # align to Z = 0 } } diff --git a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm index 9956134ce..fb36bca39 100644 --- a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm +++ b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm @@ -99,6 +99,7 @@ sub new { }); EVT_TREE_SEL_CHANGED($self, $tree, sub { my ($self, $event) = @_; + return if $self->{disable_tree_sel_changed_event}; $self->selection_changed; }); EVT_BUTTON($self, $self->{btn_load_part}, sub { $self->on_btn_load(0) }); @@ -118,7 +119,14 @@ sub reload_tree { my $tree = $self->{tree}; my $rootId = $tree->GetRootItem; + # despite wxWidgets states that DeleteChildren "will not generate any events unlike Delete() method", + # the MSW implementation of DeleteChildren actually calls Delete() for each item, so + # EVT_TREE_SEL_CHANGED is being called, with bad effects (the event handler is called; this + # subroutine is never continued; an invisible EndModal is called on the dialog causing Plater + # to continue its logic and rescheduling the background process etc. GH #2774) + $self->{disable_tree_sel_changed_event} = 1; $tree->DeleteChildren($rootId); + $self->{disable_tree_sel_changed_event} = 0; my $selectedId = $rootId; foreach my $volume_id (0..$#{$object->volumes}) { @@ -136,9 +144,13 @@ sub reload_tree { } $tree->ExpandAll; - # This will trigger the selection_changed() event Slic3r::GUI->CallAfter(sub { $self->{tree}->SelectItem($selectedId); + + # SelectItem() should trigger EVT_TREE_SEL_CHANGED as per wxWidgets docs, + # but in fact it doesn't if the given item is already selected (this happens + # on first load) + $self->selection_changed; }); } diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index 7026ea9fb..8aeacc971 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -177,7 +177,7 @@ sub _update {} sub _on_presets_changed { my $self = shift; - $self->{on_presets_changed}->([$self->{presets_choice}->GetStrings], $self->{presets_choice}->GetSelection) + $self->{on_presets_changed}->($self->{presets}, $self->{presets_choice}->GetSelection) if $self->{on_presets_changed}; } @@ -360,10 +360,10 @@ sub load_presets { $self->current_preset(undef); $self->{presets_choice}->Clear; - $self->{presets_choice}->Append($_->{name}) for @{$self->{presets}}; + $self->{presets_choice}->Append($_->name) for @{$self->{presets}}; { # load last used preset - my $i = first { basename($self->{presets}[$_]{file}) eq ($Slic3r::GUI::Settings->{presets}{$self->name} || '') } 1 .. $#{$self->{presets}}; + my $i = first { basename($self->{presets}[$_]->file) eq ($Slic3r::GUI::Settings->{presets}{$self->name} || '') } 1 .. $#{$self->{presets}}; $self->select_preset($i || 0); } $self->_on_presets_changed; @@ -813,7 +813,7 @@ sub build { my $self = shift; $self->init_config_options(qw( - filament_diameter extrusion_multiplier + filament_colour filament_diameter extrusion_multiplier temperature first_layer_temperature bed_temperature first_layer_bed_temperature fan_always_on cooling min_fan_speed max_fan_speed bridge_fan_speed disable_fan_first_layers @@ -824,6 +824,7 @@ sub build { my $page = $self->add_options_page('Filament', 'spool.png'); { my $optgroup = $page->new_optgroup('Filament'); + $optgroup->append_single_option_line('filament_colour', 0); $optgroup->append_single_option_line('filament_diameter', 0); $optgroup->append_single_option_line('extrusion_multiplier', 0); } diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index 42fab2d89..1fa63f3b8 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -27,9 +27,6 @@ our @EXPORT_OK = qw( use constant PI => 4 * atan2(1, 1); use constant A => 0; use constant B => 1; -use constant X => 0; -use constant Y => 1; -use constant Z => 2; use constant X1 => 0; use constant Y1 => 1; use constant X2 => 2; @@ -605,119 +602,4 @@ sub douglas_peucker2 { return [ map $points->[$_], sort keys %keep ]; } -sub arrange { - my ($total_parts, $partx, $party, $dist, $bb) = @_; - - my $linint = sub { - my ($value, $oldmin, $oldmax, $newmin, $newmax) = @_; - return ($value - $oldmin) * ($newmax - $newmin) / ($oldmax - $oldmin) + $newmin; - }; - - # use actual part size (the largest) plus separation distance (half on each side) in spacing algorithm - $partx += $dist; - $party += $dist; - - my ($areax, $areay); - if (defined $bb) { - my $size = $bb->size; - ($areax, $areay) = @$size[X,Y]; - } else { - # bogus area size, large enough not to trigger the error below - $areax = $partx * $total_parts; - $areay = $party * $total_parts; - } - - # this is how many cells we have available into which to put parts - my $cellw = int(($areax + $dist) / $partx); - my $cellh = int(($areay + $dist) / $party); - - die "$total_parts parts won't fit in your print area!\n" if $total_parts > ($cellw * $cellh); - - # width and height of space used by cells - my $w = $cellw * $partx; - my $h = $cellh * $party; - - # left and right border positions of space used by cells - my $l = ($areax - $w) / 2; - my $r = $l + $w; - - # top and bottom border positions - my $t = ($areay - $h) / 2; - my $b = $t + $h; - - # list of cells, sorted by distance from center - my @cellsorder; - - # work out distance for all cells, sort into list - for my $i (0..$cellw-1) { - for my $j (0..$cellh-1) { - my $cx = $linint->($i + 0.5, 0, $cellw, $l, $r); - my $cy = $linint->($j + 0.5, 0, $cellh, $t, $b); - - my $xd = abs(($areax / 2) - $cx); - my $yd = abs(($areay / 2) - $cy); - - my $c = { - location => [$cx, $cy], - index => [$i, $j], - distance => $xd * $xd + $yd * $yd - abs(($cellw / 2) - ($i + 0.5)), - }; - - BINARYINSERTIONSORT: { - my $index = $c->{distance}; - my $low = 0; - my $high = @cellsorder; - while ($low < $high) { - my $mid = ($low + (($high - $low) / 2)) | 0; - my $midval = $cellsorder[$mid]->[0]; - - if ($midval < $index) { - $low = $mid + 1; - } elsif ($midval > $index) { - $high = $mid; - } else { - splice @cellsorder, $mid, 0, [$index, $c]; - last BINARYINSERTIONSORT; - } - } - splice @cellsorder, $low, 0, [$index, $c]; - } - } - } - - # the extents of cells actually used by objects - my ($lx, $ty, $rx, $by) = (0, 0, 0, 0); - - # now find cells actually used by objects, map out the extents so we can position correctly - for my $i (1..$total_parts) { - my $c = $cellsorder[$i - 1]; - my $cx = $c->[1]->{index}->[0]; - my $cy = $c->[1]->{index}->[1]; - if ($i == 1) { - $lx = $rx = $cx; - $ty = $by = $cy; - } else { - $rx = $cx if $cx > $rx; - $lx = $cx if $cx < $lx; - $by = $cy if $cy > $by; - $ty = $cy if $cy < $ty; - } - } - # now we actually place objects into cells, positioned such that the left and bottom borders are at 0 - my @positions = (); - for (1..$total_parts) { - my $c = shift @cellsorder; - my $cx = $c->[1]->{index}->[0] - $lx; - my $cy = $c->[1]->{index}->[1] - $ty; - - push @positions, [$cx * $partx, $cy * $party]; - } - - if (defined $bb) { - $_->[X] += $bb->x_min for @positions; - $_->[Y] += $bb->y_min for @positions; - } - return @positions; -} - 1; diff --git a/lib/Slic3r/Layer.pm b/lib/Slic3r/Layer.pm index a3e72994e..2e65c147e 100644 --- a/lib/Slic3r/Layer.pm +++ b/lib/Slic3r/Layer.pm @@ -32,11 +32,6 @@ sub regions { return [ map $self->get_region($_), 0..($self->region_count-1) ]; } -sub merge_slices { - my ($self) = @_; - $_->merge_slices for @{$self->regions}; -} - sub make_perimeters { my $self = shift; Slic3r::debugf "Making perimeters for layer %d\n", $self->id; diff --git a/lib/Slic3r/Layer/PerimeterGenerator.pm b/lib/Slic3r/Layer/PerimeterGenerator.pm index a5b737466..acb3bbba0 100644 --- a/lib/Slic3r/Layer/PerimeterGenerator.pm +++ b/lib/Slic3r/Layer/PerimeterGenerator.pm @@ -95,7 +95,7 @@ sub process { my $loop_number = $self->config->perimeters + ($surface->extra_perimeters || 0); $loop_number--; # 0-indexed loops - my @gaps = (); # ExPolygons + my @gaps = (); # Polygons my @last = @{$surface->expolygon->simplify_p(&Slic3r::SCALED_RESOLUTION)}; if ($loop_number >= 0) { # no loops = -1 @@ -133,12 +133,12 @@ sub process { # the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width # (actually, something larger than that still may exist due to mitering or other causes) - my $min_width = $pwidth / 4; + my $min_width = $ext_pwidth / 4; @thin_walls = @{offset2_ex($diff, -$min_width/2, +$min_width/2)}; - + # the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop - @thin_walls = grep $_->length > $pwidth*2, - map @{$_->medial_axis($pwidth + $pspacing, $min_width)}, @thin_walls; + @thin_walls = grep $_->length > $ext_pwidth*2, + map @{$_->medial_axis($ext_pwidth + $ext_pspacing, $min_width)}, @thin_walls; Slic3r::debugf " %d thin walls detected\n", scalar(@thin_walls) if $Slic3r::debug; if (0) { @@ -174,10 +174,10 @@ sub process { # (but still long enough to escape the area threshold) that gap fill # won't be able to fill but we'd still remove from infill area my $diff = diff_ex( - offset(\@last, -0.5*$pspacing), - offset(\@offsets, +0.5*$pspacing + 10), # safety offset + offset(\@last, -0.5*$distance), + offset(\@offsets, +0.5*$distance + 10), # safety offset ); - push @gaps, grep abs($_->area) >= $gap_area_threshold, @$diff; + push @gaps, map $_->clone, map @$_, grep abs($_->area) >= $gap_area_threshold, @$diff; } } @@ -278,20 +278,20 @@ sub process { require "Slic3r/SVG.pm"; Slic3r::SVG::output( "gaps.svg", - expolygons => \@gaps, + expolygons => union_ex(\@gaps), ); } - # where $pwidth < thickness < 2*$pspacing, infill with width = 1.5*$pwidth - # where 0.5*$pwidth < thickness < $pwidth, infill with width = 0.5*$pwidth + # where $pwidth < thickness < 2*$pspacing, infill with width = 2*$pwidth + # where 0.1*$pwidth < thickness < $pwidth, infill with width = 1*$pwidth my @gap_sizes = ( - [ $pwidth, 2*$pspacing, unscale 1.5*$pwidth ], - [ 0.1*$pwidth, $pwidth, unscale 0.5*$pwidth ], + [ $pwidth, 2*$pspacing, unscale 2*$pwidth ], + [ 0.1*$pwidth, $pwidth, unscale 1*$pwidth ], ); foreach my $gap_size (@gap_sizes) { my @gap_fill = $self->_fill_gaps(@$gap_size, \@gaps); $self->gap_fill->append($_) for @gap_fill; - + # Make sure we don't infill narrow parts that are already gap-filled # (we only consider this surface's gaps to reduce the diff() complexity). # Growing actual extrusions ensures that gaps not filled by medial axis @@ -304,6 +304,7 @@ sub process { ->grow(scale $w/2)}; } @gap_fill; @last = @{diff(\@last, \@filled)}; + @gaps = @{diff(\@gaps, \@filled)}; # prevent more gap fill here } } @@ -454,8 +455,8 @@ sub _fill_gaps { $min *= (1 - &Slic3r::INSET_OVERLAP_TOLERANCE); my $this = diff_ex( - offset2([ map @$_, @$gaps ], -$min/2, +$min/2), - offset2([ map @$_, @$gaps ], -$max/2, +$max/2), + offset2($gaps, -$min/2, +$min/2), + offset2($gaps, -$max/2, +$max/2), 1, ); diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm index 1a2f62afd..162c3a3d1 100644 --- a/lib/Slic3r/Model.pm +++ b/lib/Slic3r/Model.pm @@ -150,15 +150,19 @@ sub duplicate { sub _arrange { my ($self, $sizes, $distance, $bb) = @_; + + $bb //= Slic3r::Geometry::BoundingBoxf->new; # we supply unscaled data to arrange() - return Slic3r::Geometry::arrange( + return @{Slic3r::Geometry::arrange( scalar(@$sizes), # number of parts - max(map $_->x, @$sizes), # cell width - max(map $_->y, @$sizes), # cell height , + Slic3r::Pointf->new( + max(map $_->x, @$sizes), # cell width + max(map $_->y, @$sizes), # cell height , + ), $distance, # distance between cells $bb, # bounding box of the area to fill (can be undef) - ); + )}; } sub print_info { @@ -262,37 +266,6 @@ sub add_instance { } } -sub rotate { - my ($self, $angle, $axis) = @_; - - # we accept angle in radians but mesh currently uses degrees - $angle = rad2deg($angle); - - if ($axis == X) { - $_->mesh->rotate_x($angle) for @{$self->volumes}; - } elsif ($axis == Y) { - $_->mesh->rotate_y($angle) for @{$self->volumes}; - } elsif ($axis == Z) { - $_->mesh->rotate_z($angle) for @{$self->volumes}; - } - $self->set_origin_translation(Slic3r::Pointf3->new(0,0,0)); - $self->invalidate_bounding_box; -} - -sub flip { - my ($self, $axis) = @_; - - if ($axis == X) { - $_->mesh->flip_x for @{$self->volumes}; - } elsif ($axis == Y) { - $_->mesh->flip_y for @{$self->volumes}; - } elsif ($axis == Z) { - $_->mesh->flip_z for @{$self->volumes}; - } - $self->set_origin_translation(Slic3r::Pointf3->new(0,0,0)); - $self->invalidate_bounding_box; -} - sub mesh_stats { my $self = shift; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index 2837e0db7..77488e559 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -225,8 +225,8 @@ sub make_skirt { my $skirt_height_z = -1; foreach my $object (@{$self->objects}) { my $skirt_height = $self->has_infinite_skirt - ? scalar(@{$object->layers}) - : min($self->config->skirt_height, scalar(@{$object->layers})); + ? $object->layer_count + : min($self->config->skirt_height, $object->layer_count); my $highest_layer = $object->get_layer($skirt_height - 1); $skirt_height_z = max($skirt_height_z, $highest_layer->print_z); } @@ -432,6 +432,11 @@ sub expanded_output_filepath { $self->placeholder_parser->set(input_filename => $filename); $self->placeholder_parser->set(input_filename_base => $filename_base); + # set other variables from model object + $self->placeholder_parser->set_multiple( + scale => [ map $_->model_object->instances->[0]->scaling_factor * 100 . "%", @{$self->objects} ], + ); + if ($path && -d $path) { # if output path is an existing directory, we take that and append # the specified filename format diff --git a/lib/Slic3r/Print/GCode.pm b/lib/Slic3r/Print/GCode.pm index d9246ebb4..d9534101f 100644 --- a/lib/Slic3r/Print/GCode.pm +++ b/lib/Slic3r/Print/GCode.pm @@ -196,6 +196,7 @@ sub export { if ($finished_objects > 0) { $gcodegen->set_origin(Slic3r::Pointf->new(map unscale $copy->[$_], X,Y)); $gcodegen->enable_cooling_markers(0); # we're not filtering these moves through CoolingBuffer + $gcodegen->avoid_crossing_perimeters->use_external_mp_once(1); print $fh $gcodegen->retract; print $fh $gcodegen->travel_to( Slic3r::Point->new(0,0), @@ -203,6 +204,9 @@ sub export { 'move to origin position for next object', ); $gcodegen->enable_cooling_markers(1); + + # disable motion planner when traveling to first object point + $gcodegen->avoid_crossing_perimeters->disable_once(1); } my @layers = sort { $a->print_z <=> $b->print_z } @{$object->layers}, @{$object->support_layers}; @@ -305,8 +309,8 @@ sub process_layer { ($layer->id > 0 || $self->print->config->brim_width == 0) && ($layer->id >= $self->print->config->skirt_height && !$self->print->has_infinite_skirt) && !defined(first { $_->config->bottom_solid_layers > $layer->id } @{$layer->regions}) - && !defined(first { @{$_->perimeters} > 1 } @{$layer->regions}) - && !defined(first { @{$_->fills} > 0 } @{$layer->regions}) + && !defined(first { $_->perimeters->items_count > 1 } @{$layer->regions}) + && !defined(first { $_->fills->items_count > 0 } @{$layer->regions}) ); } @@ -326,19 +330,20 @@ sub process_layer { # set new layer - this will change Z and force a retraction if retract_layer_change is enabled $gcode .= $self->_gcodegen->placeholder_parser->process($self->print->config->before_layer_gcode, { - layer_num => $layer->id, + layer_num => $self->_gcodegen->layer_index + 1, layer_z => $layer->print_z, }) . "\n" if $self->print->config->before_layer_gcode; - $gcode .= $self->_gcodegen->change_layer($layer); + $gcode .= $self->_gcodegen->change_layer($layer); # this will increase $self->_gcodegen->layer_index $gcode .= $self->_gcodegen->placeholder_parser->process($self->print->config->layer_gcode, { - layer_num => $layer->id, + layer_num => $self->_gcodegen->layer_index, layer_z => $layer->print_z, }) . "\n" if $self->print->config->layer_gcode; - # extrude skirt + # extrude skirt along raft layers and normal object layers + # (not along interlaced support material layers) if (((values %{$self->_skirt_done}) < $self->print->config->skirt_height || $self->print->has_infinite_skirt) && !$self->_skirt_done->{$layer->print_z} - && !$layer->isa('Slic3r::Layer::Support')) { + && (!$layer->isa('Slic3r::Layer::Support') || $layer->id < $object->config->raft_layers)) { $self->_gcodegen->set_origin(Slic3r::Pointf->new(0,0)); $self->_gcodegen->avoid_crossing_perimeters->use_external_mp(1); my @extruder_ids = map { $_->id } @{$self->_gcodegen->writer->extruders}; diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm index aa24eedf3..cbee4359b 100644 --- a/lib/Slic3r/Print/Object.pm +++ b/lib/Slic3r/Print/Object.pm @@ -57,16 +57,36 @@ sub slice { # plus the extra distance required by the support material logic my $first_layer_height = $self->config->get_value('first_layer_height'); $print_z += $first_layer_height; - $print_z += $self->config->layer_height * ($self->config->raft_layers - 1); + + # use a large height + my $support_material_layer_height; + { + my @nozzle_diameters = ( + map $self->print->config->get_at('nozzle_diameter', $_), + $self->config->support_material_extruder, + $self->config->support_material_interface_extruder, + ); + $support_material_layer_height = 0.75 * min(@nozzle_diameters); + } + $print_z += $support_material_layer_height * ($self->config->raft_layers - 1); - # at this stage we don't know which nozzles are actually used for the first layer - # so we compute the average of all of them - my $nozzle_diameter = sum(@{$self->print->config->nozzle_diameter})/@{$self->print->config->nozzle_diameter}; - my $distance = $self->_support_material->contact_distance($first_layer_height, $nozzle_diameter); + # compute the average of all nozzles used for printing the object + my $nozzle_diameter; + { + my @nozzle_diameters = ( + map $self->print->config->get_at('nozzle_diameter', $_), @{$self->print->object_extruders} + ); + $nozzle_diameter = sum(@nozzle_diameters)/@nozzle_diameters; + } + my $distance = $self->_support_material->contact_distance($self->config->layer_height, $nozzle_diameter); # force first layer print_z according to the contact distance # (the loop below will raise print_z by such height) - $first_object_layer_height = $nozzle_diameter; + if ($self->config->support_material_contact_distance == 0) { + $first_object_layer_height = $distance; + } else { + $first_object_layer_height = $nozzle_diameter; + } $first_object_layer_distance = $distance; } diff --git a/slic3r.pl b/slic3r.pl index ede37dbc0..a5957a98f 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -15,6 +15,7 @@ use POSIX qw(setlocale LC_NUMERIC); use Slic3r; use Time::HiRes qw(gettimeofday tv_interval); $|++; +binmode STDOUT, ':utf8'; our %opt = (); my %cli_options = (); @@ -93,7 +94,7 @@ my $gui; if ((!@ARGV || $opt{gui}) && !$opt{save} && eval "require Slic3r::GUI; 1") { { no warnings 'once'; - $Slic3r::GUI::datadir = Slic3r::decode_path($opt{datadir}); + $Slic3r::GUI::datadir = Slic3r::decode_path($opt{datadir} // ''); $Slic3r::GUI::no_plater = $opt{no_plater}; $Slic3r::GUI::mode = $opt{gui_mode}; $Slic3r::GUI::autosave = $opt{autosave}; @@ -103,6 +104,7 @@ if ((!@ARGV || $opt{gui}) && !$opt{save} && eval "require Slic3r::GUI; 1") { $gui->{mainframe}->load_config_file($_) for @{$opt{load}}; $gui->{mainframe}->load_config($cli_config); foreach my $input_file (@ARGV) { + $input_file = Slic3r::decode_path($input_file); $gui->{mainframe}{plater}->load_file($input_file) unless $opt{no_plater}; } $gui->MainLoop; @@ -434,7 +436,7 @@ $j Only retract before travel moves of this length in mm (default: $config->{retract_before_travel}[0]) --retract-lift Lift Z by the given distance in mm when retracting (default: $config->{retract_lift}[0]) --retract-layer-change - Enforce a retraction before each Z move (default: yes) + Enforce a retraction before each Z move (default: no) --wipe Wipe the nozzle while doing a retraction (default: no) Retraction options for multi-extruder setups: diff --git a/t/custom_gcode.t b/t/custom_gcode.t index 6423c394d..cbd5741f4 100644 --- a/t/custom_gcode.t +++ b/t/custom_gcode.t @@ -1,4 +1,4 @@ -use Test::More tests => 13; +use Test::More tests => 15; use strict; use warnings; @@ -7,6 +7,7 @@ BEGIN { use lib "$FindBin::Bin/../lib"; } +use List::Util qw(first); use Slic3r; use Slic3r::Test; @@ -104,4 +105,29 @@ use Slic3r::Test; } } +{ + my $config = Slic3r::Config->new_from_defaults; + $config->set('before_layer_gcode', ';BEFORE [layer_num]'); + $config->set('layer_gcode', ';CHANGE [layer_num]'); + $config->set('support_material', 1); + $config->set('layer_height', 0.2); + my $print = Slic3r::Test::init_print('overhang', config => $config); + my $gcode = Slic3r::Test::gcode($print); + + my @before = (); + my @change = (); + foreach my $line (split /\R+/, $gcode) { + if ($line =~ /;BEFORE (\d+)/) { + push @before, $1; + } elsif ($line =~ /;CHANGE (\d+)/) { + push @change, $1; + fail 'inconsistent layer_num before and after layer change' + if $1 != $before[-1]; + } + } + is_deeply \@before, \@change, 'layer_num is consistent before and after layer changes'; + ok !defined(first { $change[$_] != $change[$_-1]+1 } 1..$#change), + 'layer_num grows continously'; # i.e. no duplicates or regressions +} + __END__ diff --git a/t/gcode.t b/t/gcode.t index 79ab1a7de..fb85ee5e4 100644 --- a/t/gcode.t +++ b/t/gcode.t @@ -1,4 +1,4 @@ -use Test::More tests => 22; +use Test::More tests => 23; use strict; use warnings; @@ -200,4 +200,21 @@ use Slic3r::Test; like $gcode, qr/START:20mm_cube/, '[input_filename] is also available in custom G-code'; } +{ + my $config = Slic3r::Config->new_from_defaults; + $config->set('spiral_vase', 1); + my $print = Slic3r::Test::init_print('cube_with_hole', config => $config); + + my $spiral = 0; + Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub { + my ($self, $cmd, $args, $info) = @_; + + if ($cmd eq 'G1' && exists $args->{E} && exists $args->{Z}) { + $spiral = 1; + } + }); + + ok !$spiral, 'spiral vase is correctly disabled on layers with multiple loops'; +} + __END__ diff --git a/t/support.t b/t/support.t index 2091b4432..3eba6e64b 100644 --- a/t/support.t +++ b/t/support.t @@ -1,4 +1,4 @@ -use Test::More tests => 25; +use Test::More tests => 27; use strict; use warnings; @@ -221,12 +221,41 @@ use Slic3r::Test; { my $config = Slic3r::Config->new_from_defaults; - $config->set('raft_layers', 3); + $config->set('skirts', 0); + $config->set('start_gcode', ''); + $config->set('raft_layers', 8); $config->set('nozzle_diameter', [0.4, 1]); + $config->set('layer_height', 0.1); $config->set('first_layer_height', 0.8); $config->set('support_material_extruder', 2); + $config->set('support_material_interface_extruder', 2); + $config->set('support_material_contact_distance', 0); my $print = Slic3r::Test::init_print('20mm_cube', config => $config); - ok Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers'; + ok my $gcode = Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers'; + + my $tool = undef; + my @z = (0); + my %layer_heights_by_tool = (); # tool => [ lh, lh... ] + Slic3r::GCode::Reader->new->parse($gcode, sub { + my ($self, $cmd, $args, $info) = @_; + + if ($cmd =~ /^T(\d+)/) { + $tool = $1; + } elsif ($cmd eq 'G1' && exists $args->{Z} && $args->{Z} != $self->Z) { + push @z, $args->{Z}; + } elsif ($info->{extruding} && $info->{dist_XY} > 0) { + $layer_heights_by_tool{$tool} ||= []; + push @{ $layer_heights_by_tool{$tool} }, $z[-1] - $z[-2]; + } + }); + + ok !defined(first { $_ > $config->nozzle_diameter->[0] + epsilon } + @{ $layer_heights_by_tool{$config->perimeter_extruder-1} }), + 'no object layer is thicker than nozzle diameter'; + + ok !defined(first { abs($_ - $config->layer_height) < epsilon } + @{ $layer_heights_by_tool{$config->support_material_extruder-1} }), + 'no support material layer is as thin as object layers'; } __END__ diff --git a/t/thin.t b/t/thin.t index 49a6ef427..0b3843a10 100644 --- a/t/thin.t +++ b/t/thin.t @@ -1,4 +1,4 @@ -use Test::More tests => 14; +use Test::More tests => 21; use strict; use warnings; @@ -133,4 +133,40 @@ if (0) { ok sum(map $_->length, @$polylines) > $perimeter/2/4*3, 'medial axis has a reasonable length'; } +{ + my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale( + [50, 100], + [300, 102], + [50, 104], + )); + my $res = $expolygon->medial_axis(scale 4, scale 0.5); + is scalar(@$res), 1, 'medial axis of a narrow triangle is a single line'; + ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length'; +} + +{ + # GH #2474 + my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new( + [91294454,31032190],[11294481,31032190],[11294481,29967810],[44969182,29967810],[89909960,29967808],[91294454,29967808] + )); + my $polylines = $expolygon->medial_axis(1871238, 500000); + + is scalar(@$polylines), 1, 'medial axis is a single polyline'; + my $polyline = $polylines->[0]; + + my $expected_y = $expolygon->bounding_box->center->y; #;; + ok abs(sum(map $_->y, @$polyline) / @$polyline - $expected_y) < scaled_epsilon, #,, + 'medial axis is horizontal and is centered'; + + # order polyline from left to right + $polyline->reverse if $polyline->first_point->x > $polyline->last_point->x; + + my $polyline_bb = $polyline->bounding_box; + is $polyline->first_point->x, $polyline_bb->x_min, 'expected x_min'; + is $polyline->last_point->x, $polyline_bb->x_max, 'expected x_max'; + + is_deeply [ map $_->x, @$polyline ], [ sort map $_->x, @$polyline ], + 'medial axis is not self-overlapping'; +} + __END__ diff --git a/utils/view-toolpaths.pl b/utils/view-toolpaths.pl index 7e1d305bc..4bbbaab4c 100755 --- a/utils/view-toolpaths.pl +++ b/utils/view-toolpaths.pl @@ -33,7 +33,6 @@ my %opt = (); # load config my $config = Slic3r::Config->new_from_defaults; - $config->set('skirts', 0); if ($opt{load}) { $config->apply(Slic3r::Config->load($opt{load})); } @@ -84,6 +83,7 @@ sub OnInit { if ($d3) { $canvas = Slic3r::GUI::3DScene->new($panel); $canvas->set_bed_shape($print->config->bed_shape); + $canvas->load_print_toolpaths($print); foreach my $object (@{$print->objects}) { #$canvas->load_print_object_slices($object); diff --git a/var/application_view_tile.png b/var/application_view_tile.png new file mode 100755 index 000000000..3bc0bd32f Binary files /dev/null and b/var/application_view_tile.png differ diff --git a/var/lorry_add.png b/var/lorry_add.png new file mode 100755 index 000000000..a2c512499 Binary files /dev/null and b/var/lorry_add.png differ diff --git a/var/lorry_go.png b/var/lorry_go.png new file mode 100755 index 000000000..1c296a6aa Binary files /dev/null and b/var/lorry_go.png differ diff --git a/var/plugin_add.png b/var/plugin_add.png new file mode 100755 index 000000000..ae43690ec Binary files /dev/null and b/var/plugin_add.png differ diff --git a/var/plugin_go.png b/var/plugin_go.png new file mode 100755 index 000000000..41da9913d Binary files /dev/null and b/var/plugin_go.png differ diff --git a/var/shape_flip_horizontal.png b/var/shape_flip_horizontal.png new file mode 100755 index 000000000..8667c81f8 Binary files /dev/null and b/var/shape_flip_horizontal.png differ diff --git a/var/shape_handles.png b/var/shape_handles.png new file mode 100755 index 000000000..ce27fe3a0 Binary files /dev/null and b/var/shape_handles.png differ diff --git a/var/textfield.png b/var/textfield.png new file mode 100755 index 000000000..d37e7304e Binary files /dev/null and b/var/textfield.png differ diff --git a/xs/src/libslic3r/Config.hpp b/xs/src/libslic3r/Config.hpp index 313b524fa..49e999bc0 100644 --- a/xs/src/libslic3r/Config.hpp +++ b/xs/src/libslic3r/Config.hpp @@ -26,8 +26,14 @@ class ConfigOption { virtual void setInt(int val) {}; }; +class ConfigOptionVectorBase : public ConfigOption { + public: + virtual ~ConfigOptionVectorBase() {}; + virtual std::vector vserialize() const = 0; +}; + template -class ConfigOptionVector +class ConfigOptionVector : public ConfigOptionVectorBase { public: virtual ~ConfigOptionVector() {}; @@ -63,7 +69,7 @@ class ConfigOptionFloat : public ConfigOption }; }; -class ConfigOptionFloats : public ConfigOption, public ConfigOptionVector +class ConfigOptionFloats : public ConfigOptionVector { public: @@ -76,6 +82,16 @@ class ConfigOptionFloats : public ConfigOption, public ConfigOptionVector vserialize() const { + std::vector vv; + for (std::vector::const_iterator it = this->values.begin(); it != this->values.end(); ++it) { + std::ostringstream ss; + ss << *it; + vv.push_back(ss.str()); + } + return vv; + }; + bool deserialize(std::string str) { this->values.clear(); std::istringstream is(str); @@ -112,7 +128,7 @@ class ConfigOptionInt : public ConfigOption }; }; -class ConfigOptionInts : public ConfigOption, public ConfigOptionVector +class ConfigOptionInts : public ConfigOptionVector { public: @@ -125,6 +141,16 @@ class ConfigOptionInts : public ConfigOption, public ConfigOptionVector return ss.str(); }; + std::vector vserialize() const { + std::vector vv; + for (std::vector::const_iterator it = this->values.begin(); it != this->values.end(); ++it) { + std::ostringstream ss; + ss << *it; + vv.push_back(ss.str()); + } + return vv; + }; + bool deserialize(std::string str) { this->values.clear(); std::istringstream is(str); @@ -174,7 +200,7 @@ class ConfigOptionString : public ConfigOption }; // semicolon-separated strings -class ConfigOptionStrings : public ConfigOption, public ConfigOptionVector +class ConfigOptionStrings : public ConfigOptionVector { public: @@ -187,6 +213,10 @@ class ConfigOptionStrings : public ConfigOption, public ConfigOptionVector vserialize() const { + return this->values; + }; + bool deserialize(std::string str) { this->values.clear(); std::istringstream is(str); @@ -279,7 +309,7 @@ class ConfigOptionPoint : public ConfigOption }; }; -class ConfigOptionPoints : public ConfigOption, public ConfigOptionVector +class ConfigOptionPoints : public ConfigOptionVector { public: @@ -294,6 +324,16 @@ class ConfigOptionPoints : public ConfigOption, public ConfigOptionVector vserialize() const { + std::vector vv; + for (Pointfs::const_iterator it = this->values.begin(); it != this->values.end(); ++it) { + std::ostringstream ss; + ss << *it; + vv.push_back(ss.str()); + } + return vv; + }; + bool deserialize(std::string str) { this->values.clear(); std::istringstream is(str); @@ -332,7 +372,7 @@ class ConfigOptionBool : public ConfigOption }; }; -class ConfigOptionBools : public ConfigOption, public ConfigOptionVector +class ConfigOptionBools : public ConfigOptionVector { public: @@ -345,6 +385,16 @@ class ConfigOptionBools : public ConfigOption, public ConfigOptionVector return ss.str(); }; + std::vector vserialize() const { + std::vector vv; + for (std::vector::const_iterator it = this->values.begin(); it != this->values.end(); ++it) { + std::ostringstream ss; + ss << (*it ? "1" : "0"); + vv.push_back(ss.str()); + } + return vv; + }; + bool deserialize(std::string str) { this->values.clear(); std::istringstream is(str); diff --git a/xs/src/libslic3r/ExPolygon.cpp b/xs/src/libslic3r/ExPolygon.cpp index a03447bba..53a1e9333 100644 --- a/xs/src/libslic3r/ExPolygon.cpp +++ b/xs/src/libslic3r/ExPolygon.cpp @@ -191,12 +191,25 @@ ExPolygon::medial_axis(double max_width, double min_width, Polylines* polylines) // unless they represent closed loops for (Polylines::iterator polyline = polylines->begin(); polyline != polylines->end(); ++polyline) { if (polyline->points.front().distance_to(polyline->points.back()) < min_width) continue; + // TODO: we should *not* extend endpoints where other polylines start/end + // (such as T joints, which are returned as three polylines by MedialAxis) polyline->extend_start(max_width); polyline->extend_end(max_width); } // clip again after extending endpoints to prevent them from exceeding the expolygon boundaries intersection(*polylines, *this, polylines); + + // remove too short polylines + // (we can't do this check before endpoints extension and clipping because we don't + // know how long will the endpoints be extended since it depends on polygon thickness + // which is variable - extension will be <= max_width/2 on each side) + for (size_t i = 0; i < polylines->size(); ++i) { + if ((*polylines)[i].length() < max_width) { + polylines->erase(polylines->begin() + i); + --i; + } + } } void diff --git a/xs/src/libslic3r/ExtrusionEntity.hpp b/xs/src/libslic3r/ExtrusionEntity.hpp index 323d935a1..b5262eae0 100644 --- a/xs/src/libslic3r/ExtrusionEntity.hpp +++ b/xs/src/libslic3r/ExtrusionEntity.hpp @@ -35,9 +35,15 @@ enum ExtrusionLoopRole { class ExtrusionEntity { public: + virtual bool is_collection() const { + return false; + }; virtual bool is_loop() const { return false; }; + virtual bool can_reverse() const { + return true; + }; virtual ExtrusionEntity* clone() const = 0; virtual ~ExtrusionEntity() {}; virtual void reverse() = 0; @@ -92,6 +98,9 @@ class ExtrusionLoop : public ExtrusionEntity bool is_loop() const { return true; }; + bool can_reverse() const { + return false; + }; ExtrusionLoop* clone() const; bool make_clockwise(); bool make_counter_clockwise(); diff --git a/xs/src/libslic3r/ExtrusionEntityCollection.cpp b/xs/src/libslic3r/ExtrusionEntityCollection.cpp index 4ceef0387..075429d9d 100644 --- a/xs/src/libslic3r/ExtrusionEntityCollection.cpp +++ b/xs/src/libslic3r/ExtrusionEntityCollection.cpp @@ -86,7 +86,7 @@ ExtrusionEntityCollection::chained_path_from(Point start_near, ExtrusionEntityCo Points endpoints; for (ExtrusionEntitiesPtr::iterator it = my_paths.begin(); it != my_paths.end(); ++it) { endpoints.push_back((*it)->first_point()); - if (no_reverse) { + if (no_reverse || !(*it)->can_reverse()) { endpoints.push_back((*it)->first_point()); } else { endpoints.push_back((*it)->last_point()); @@ -99,7 +99,7 @@ ExtrusionEntityCollection::chained_path_from(Point start_near, ExtrusionEntityCo int path_index = start_index/2; ExtrusionEntity* entity = my_paths.at(path_index); // never reverse loops, since it's pointless for chained path and callers might depend on orientation - if (start_index % 2 && !no_reverse && !entity->is_loop()) { + if (start_index % 2 && !no_reverse && entity->can_reverse()) { entity->reverse(); } retval->entities.push_back(my_paths.at(path_index)); @@ -121,6 +121,22 @@ ExtrusionEntityCollection::grow() const return pp; } +/* Recursively count paths and loops contained in this collection */ +size_t +ExtrusionEntityCollection::items_count() const +{ + size_t count = 0; + for (ExtrusionEntitiesPtr::const_iterator it = this->entities.begin(); it != this->entities.end(); ++it) { + if ((*it)->is_collection()) { + ExtrusionEntityCollection* collection = dynamic_cast(*it); + count += collection->items_count(); + } else { + ++count; + } + } + return count; +} + #ifdef SLIC3RXS // there is no ExtrusionLoop::Collection or ExtrusionEntity::Collection REGISTER_CLASS(ExtrusionEntityCollection, "ExtrusionPath::Collection"); diff --git a/xs/src/libslic3r/ExtrusionEntityCollection.hpp b/xs/src/libslic3r/ExtrusionEntityCollection.hpp index be557bb5f..18e15e318 100644 --- a/xs/src/libslic3r/ExtrusionEntityCollection.hpp +++ b/xs/src/libslic3r/ExtrusionEntityCollection.hpp @@ -16,6 +16,12 @@ class ExtrusionEntityCollection : public ExtrusionEntity ExtrusionEntityCollection(): no_sort(false) {}; ExtrusionEntityCollection(const ExtrusionEntityCollection &collection); ExtrusionEntityCollection& operator= (const ExtrusionEntityCollection &other); + bool is_collection() const { + return true; + }; + bool can_reverse() const { + return !this->no_sort; + }; void swap (ExtrusionEntityCollection &c); void chained_path(ExtrusionEntityCollection* retval, bool no_reverse = false, std::vector* orig_indices = NULL) const; void chained_path_from(Point start_near, ExtrusionEntityCollection* retval, bool no_reverse = false, std::vector* orig_indices = NULL) const; @@ -23,6 +29,7 @@ class ExtrusionEntityCollection : public ExtrusionEntity Point first_point() const; Point last_point() const; Polygons grow() const; + size_t items_count() const; }; } diff --git a/xs/src/libslic3r/Geometry.cpp b/xs/src/libslic3r/Geometry.cpp index 3dc11c0cb..5a7b2e3dc 100644 --- a/xs/src/libslic3r/Geometry.cpp +++ b/xs/src/libslic3r/Geometry.cpp @@ -161,6 +161,133 @@ simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval) Slic3r::simplify_polygons(pp, retval); } +double +linint(double value, double oldmin, double oldmax, double newmin, double newmax) +{ + return (value - oldmin) * (newmax - newmin) / (oldmax - oldmin) + newmin; +} + +Pointfs +arrange(size_t total_parts, Pointf part, coordf_t dist, const BoundingBoxf &bb) +{ + // use actual part size (the largest) plus separation distance (half on each side) in spacing algorithm + part.x += dist; + part.y += dist; + + Pointf area; + if (bb.defined) { + area = bb.size(); + } else { + // bogus area size, large enough not to trigger the error below + area.x = part.x * total_parts; + area.y = part.y * total_parts; + } + + // this is how many cells we have available into which to put parts + size_t cellw = floor((area.x + dist) / part.x); + size_t cellh = floor((area.x + dist) / part.x); + + if (total_parts > (cellw * cellh)) + CONFESS("%zu parts won't fit in your print area!\n", total_parts); + + // total space used by cells + Pointf cells(cellw * part.x, cellh * part.y); + + // bounding box of total space used by cells + BoundingBoxf cells_bb; + cells_bb.merge(Pointf(0,0)); // min + cells_bb.merge(cells); // max + + // center bounding box to area + cells_bb.translate( + -(area.x - cells.x) / 2, + -(area.y - cells.y) / 2 + ); + + // list of cells, sorted by distance from center + std::vector cellsorder; + + // work out distance for all cells, sort into list + for (size_t i = 0; i <= cellw-1; ++i) { + for (size_t j = 0; j <= cellh-1; ++j) { + coordf_t cx = linint(i + 0.5, 0, cellw, cells_bb.min.x, cells_bb.max.x); + coordf_t cy = linint(j + 0.5, 0, cellh, cells_bb.max.y, cells_bb.min.y); + + coordf_t xd = fabs((area.x / 2) - cx); + coordf_t yd = fabs((area.y / 2) - cy); + + ArrangeItem c; + c.pos.x = cx; + c.pos.y = cy; + c.index_x = i; + c.index_y = j; + c.dist = xd * xd + yd * yd - fabs((cellw / 2) - (i + 0.5)); + + // binary insertion sort + { + coordf_t index = c.dist; + size_t low = 0; + size_t high = cellsorder.size(); + while (low < high) { + size_t mid = (low + ((high - low) / 2)) | 0; + coordf_t midval = cellsorder[mid].index; + + if (midval < index) { + low = mid + 1; + } else if (midval > index) { + high = mid; + } else { + cellsorder.insert(cellsorder.begin() + mid, ArrangeItemIndex(index, c)); + goto ENDSORT; + } + } + cellsorder.insert(cellsorder.begin() + low, ArrangeItemIndex(index, c)); + } + ENDSORT: true; + } + } + + // the extents of cells actually used by objects + coordf_t lx = 0; + coordf_t ty = 0; + coordf_t rx = 0; + coordf_t by = 0; + + // now find cells actually used by objects, map out the extents so we can position correctly + for (size_t i = 1; i <= total_parts; ++i) { + ArrangeItemIndex c = cellsorder[i - 1]; + coordf_t cx = c.item.index_x; + coordf_t cy = c.item.index_y; + if (i == 1) { + lx = rx = cx; + ty = by = cy; + } else { + if (cx > rx) rx = cx; + if (cx < lx) lx = cx; + if (cy > by) by = cy; + if (cy < ty) ty = cy; + } + } + // now we actually place objects into cells, positioned such that the left and bottom borders are at 0 + Pointfs positions; + for (size_t i = 1; i <= total_parts; ++i) { + ArrangeItemIndex c = cellsorder.front(); + cellsorder.erase(cellsorder.begin()); + coordf_t cx = c.item.index_x - lx; + coordf_t cy = c.item.index_y - ty; + + positions.push_back(Pointf(cx * part.x, cy * part.y)); + } + + if (bb.defined) { + for (Pointfs::iterator p = positions.begin(); p != positions.end(); ++p) { + p->x += bb.min.x; + p->y += bb.min.y; + } + } + return positions; +} + Line MedialAxis::edge_to_line(const VD::edge_type &edge) const { @@ -198,8 +325,11 @@ MedialAxis::build(Polylines* polylines) } */ + typedef const VD::vertex_type vert_t; + typedef const VD::edge_type edge_t; + // collect valid edges (i.e. prune those not belonging to MAT) - // note: this keeps twins, so it contains twice the number of the valid edges + // note: this keeps twins, so it inserts twice the number of the valid edges this->edges.clear(); for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) { // if we only process segments representing closed loops, none if the @@ -209,61 +339,64 @@ MedialAxis::build(Polylines* polylines) } // count valid segments for each vertex - std::map< const VD::vertex_type*,std::set > vertex_edges; - std::set entry_nodes; - for (VD::const_vertex_iterator vertex = this->vd.vertices().begin(); vertex != this->vd.vertices().end(); ++vertex) { - // get a reference to the list of valid edges originating from this vertex - std::set& edges = vertex_edges[&*vertex]; + std::map< vert_t*,std::set > vertex_edges; // collects edges connected for each vertex + std::set startpoints; // collects all vertices having a single starting edge + for (VD::const_vertex_iterator it = this->vd.vertices().begin(); it != this->vd.vertices().end(); ++it) { + vert_t* vertex = &*it; - // get one random edge originating from this vertex - const VD::edge_type* edge = vertex->incident_edge(); + // loop through all edges originating from this vertex + // starting from a random one + edge_t* edge = vertex->incident_edge(); do { - if (this->edges.count(edge) > 0) // only count valid edges - edges.insert(edge); - edge = edge->rot_next(); // next edge originating from this vertex + // if this edge was not pruned by our filter above, + // add it to vertex_edges + if (this->edges.count(edge) > 0) + vertex_edges[vertex].insert(edge); + + // continue looping next edge originating from this vertex + edge = edge->rot_next(); } while (edge != vertex->incident_edge()); - // if there's only one edge starting at this vertex then it's a leaf - size_t edge_count = edges.size(); - if (edge_count == 1) { - entry_nodes.insert(&*vertex); + // if there's only one edge starting at this vertex then it's an endpoint + if (vertex_edges[vertex].size() == 1) { + startpoints.insert(vertex); } } - // prune recursively - while (!entry_nodes.empty()) { + // prune startpoints recursively if extreme segments are not valid + while (!startpoints.empty()) { // get a random entry node - const VD::vertex_type* v = *entry_nodes.begin(); + vert_t* v = *startpoints.begin(); // get edge starting from v - assert(!vertex_edges[v].empty()); - const VD::edge_type* edge = *vertex_edges[v].begin(); + assert(vertex_edges[v].size() == 1); + edge_t* edge = *vertex_edges[v].begin(); if (!this->is_valid_edge(*edge)) { - // if edge is not valid, erase it from edge list + // if edge is not valid, erase it and its twin from edge list (void)this->edges.erase(edge); (void)this->edges.erase(edge->twin()); // decrement edge counters for the affected nodes - const VD::vertex_type* v1 = edge->vertex1(); + vert_t* v1 = edge->vertex1(); (void)vertex_edges[v].erase(edge); (void)vertex_edges[v1].erase(edge->twin()); // also, check whether the end vertex is a new leaf if (vertex_edges[v1].size() == 1) { - entry_nodes.insert(v1); + startpoints.insert(v1); } else if (vertex_edges[v1].empty()) { - entry_nodes.erase(v1); + startpoints.erase(v1); } } // remove node from the set to prevent it from being visited again - entry_nodes.erase(v); + startpoints.erase(v); } // iterate through the valid edges to build polylines while (!this->edges.empty()) { - const VD::edge_type& edge = **this->edges.begin(); + edge_t &edge = **this->edges.begin(); // start a polyline Polyline polyline; @@ -278,13 +411,14 @@ MedialAxis::build(Polylines* polylines) this->process_edge_neighbors(edge, &polyline.points); // get previous points - Points pp; - this->process_edge_neighbors(*edge.twin(), &pp); - polyline.points.insert(polyline.points.begin(), pp.rbegin(), pp.rend()); + { + Points pp; + this->process_edge_neighbors(*edge.twin(), &pp); + polyline.points.insert(polyline.points.begin(), pp.rbegin(), pp.rend()); + } - // append polyline to result if it's not too small - if (polyline.length() > this->max_width) - polylines->push_back(polyline); + // append polyline to result + polylines->push_back(polyline); } } @@ -322,69 +456,59 @@ MedialAxis::is_valid_edge(const VD::edge_type& edge) const "thin" nature of our input, these edges will be very short and not part of our wanted output. */ + // retrieve the original line segments which generated the edge we're checking const VD::cell_type &cell1 = *edge.cell(); const VD::cell_type &cell2 = *edge.twin()->cell(); - if (cell1.contains_segment() && cell2.contains_segment()) { - Line segment1 = this->retrieve_segment(cell1); - Line segment2 = this->retrieve_segment(cell2); - if (segment1.a == segment2.b || segment1.b == segment2.a) return false; - - // calculate relative angle between the two boundary segments - double angle = fabs(segment2.orientation() - segment1.orientation()); - - // fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction) - // we're interested only in segments close to the second case (facing segments) - // so we allow some tolerance (say, 30°) - if (angle < PI*2/3 ) { - return false; - } - - // each vertex is equidistant to both cell segments - // but such distance might differ between the two vertices; - // in this case it means the shape is getting narrow (like a corner) - // and we might need to skip the edge since it's not really part of - // our skeleton - Point v0( edge.vertex0()->x(), edge.vertex0()->y() ); - Point v1( edge.vertex1()->x(), edge.vertex1()->y() ); - double dist0 = v0.perp_distance_to(segment1); - double dist1 = v1.perp_distance_to(segment1); - - /* - double diff = fabs(dist1 - dist0); - double dist_between_segments1 = segment1.a.distance_to(segment2); - double dist_between_segments2 = segment1.b.distance_to(segment2); - printf("w = %f/%f, dist0 = %f, dist1 = %f, diff = %f, seg1len = %f, seg2len = %f, edgelen = %f, s2s = %f / %f\n", - unscale(this->max_width), unscale(this->min_width), - unscale(dist0), unscale(dist1), unscale(diff), - unscale(segment1.length()), unscale(segment2.length()), - unscale(this->edge_to_line(edge).length()), - unscale(dist_between_segments1), unscale(dist_between_segments2) - ); - */ - - // if this segment is the centerline for a very thin area, we might want to skip it - // in case the area is too thin - if (dist0 < this->min_width/2 || dist1 < this->min_width/2) { - //printf(" => too thin, skipping\n"); - return false; - } - - /* - // if distance between this edge and the thin area boundary is greater - // than half the max width, then it's not a true medial axis segment - if (dist1 > this->width*2) { - printf(" => too fat, skipping\n"); - //return false; - } - */ - - return true; + if (!cell1.contains_segment() || !cell2.contains_segment()) return false; + const Line &segment1 = this->retrieve_segment(cell1); + const Line &segment2 = this->retrieve_segment(cell2); + + // calculate the relative angle between the two boundary segments + double angle = fabs(segment2.orientation() - segment1.orientation()); + + // fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction) + // we're interested only in segments close to the second case (facing segments) + // so we allow some tolerance. + // this filter ensures that we're dealing with a narrow/oriented area (longer than thick) + if (fabs(angle - PI) > PI/5) { + return false; } - return false; + // each edge vertex is equidistant to both cell segments + // but such distance might differ between the two vertices; + // in this case it means the shape is getting narrow (like a corner) + // and we might need to skip the edge since it's not really part of + // our skeleton + + // get perpendicular distance of each edge vertex to the segment(s) + double dist0 = segment1.a.distance_to(segment2.b); + double dist1 = segment1.b.distance_to(segment2.a); + + /* + Line line = this->edge_to_line(edge); + double diff = fabs(dist1 - dist0); + double dist_between_segments1 = segment1.a.distance_to(segment2); + double dist_between_segments2 = segment1.b.distance_to(segment2); + printf("w = %f/%f, dist0 = %f, dist1 = %f, diff = %f, seg1len = %f, seg2len = %f, edgelen = %f, s2s = %f / %f\n", + unscale(this->max_width), unscale(this->min_width), + unscale(dist0), unscale(dist1), unscale(diff), + unscale(segment1.length()), unscale(segment2.length()), + unscale(line.length()), + unscale(dist_between_segments1), unscale(dist_between_segments2) + ); + */ + + // if this edge is the centerline for a very thin area, we might want to skip it + // in case the area is too thin + if (dist0 < this->min_width && dist1 < this->min_width) { + //printf(" => too thin, skipping\n"); + return false; + } + + return true; } -Line +const Line& MedialAxis::retrieve_segment(const VD::cell_type& cell) const { VD::cell_type::source_index_type index = cell.source_index() - this->points.size(); diff --git a/xs/src/libslic3r/Geometry.hpp b/xs/src/libslic3r/Geometry.hpp index c2cacfa33..cbc3811e2 100644 --- a/xs/src/libslic3r/Geometry.hpp +++ b/xs/src/libslic3r/Geometry.hpp @@ -23,6 +23,21 @@ double rad2deg_dir(double angle); double deg2rad(double angle); void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval); +class ArrangeItem { + public: + Pointf pos; + size_t index_x, index_y; + coordf_t dist; +}; +class ArrangeItemIndex { + public: + coordf_t index; + ArrangeItem item; + ArrangeItemIndex(coordf_t _index, ArrangeItem _item) : index(_index), item(_item) {}; +}; +double linint(double value, double oldmin, double oldmax, double newmin, double newmax); +Pointfs arrange(size_t total_parts, Pointf part, coordf_t dist, const BoundingBoxf &bb = BoundingBoxf()); + class MedialAxis { public: Points points; @@ -39,7 +54,7 @@ class MedialAxis { Line edge_to_line(const VD::edge_type &edge) const; void process_edge_neighbors(const voronoi_diagram::edge_type& edge, Points* points); bool is_valid_edge(const voronoi_diagram::edge_type& edge) const; - Line retrieve_segment(const voronoi_diagram::cell_type& cell) const; + const Line& retrieve_segment(const voronoi_diagram::cell_type& cell) const; }; } } diff --git a/xs/src/libslic3r/Layer.cpp b/xs/src/libslic3r/Layer.cpp index 2e1a8c50e..250572fb7 100644 --- a/xs/src/libslic3r/Layer.cpp +++ b/xs/src/libslic3r/Layer.cpp @@ -126,6 +126,14 @@ Layer::make_slices() } } +void +Layer::merge_slices() +{ + FOREACH_LAYERREGION(this, layerm) { + (*layerm)->merge_slices(); + } +} + template bool Layer::any_internal_region_slice_contains(const T &item) const diff --git a/xs/src/libslic3r/Layer.hpp b/xs/src/libslic3r/Layer.hpp index 6dd6e81a5..d34abec36 100644 --- a/xs/src/libslic3r/Layer.hpp +++ b/xs/src/libslic3r/Layer.hpp @@ -47,7 +47,7 @@ class LayerRegion PolylineCollection unsupported_bridge_edges; // ordered collection of extrusion paths/loops to build all perimeters - // (this collection contains both ExtrusionPath and ExtrusionLoop objects) + // (this collection contains only ExtrusionEntityCollection objects) ExtrusionEntityCollection perimeters; // ordered collection of extrusion paths to fill surfaces @@ -95,6 +95,7 @@ class Layer { LayerRegion* add_region(PrintRegion* print_region); void make_slices(); + void merge_slices(); template bool any_internal_region_slice_contains(const T &item) const; template bool any_bottom_region_slice_contains(const T &item) const; diff --git a/xs/src/libslic3r/Model.cpp b/xs/src/libslic3r/Model.cpp index d03a2916c..8d98b4d6b 100644 --- a/xs/src/libslic3r/Model.cpp +++ b/xs/src/libslic3r/Model.cpp @@ -1,4 +1,5 @@ #include "Model.hpp" +#include "Geometry.hpp" namespace Slic3r { @@ -481,7 +482,12 @@ ModelObject::center_around_origin() if (!this->instances.empty()) { for (ModelInstancePtrs::const_iterator i = this->instances.begin(); i != this->instances.end(); ++i) { - (*i)->offset.translate(-vector.x, -vector.y); + // apply rotation and scaling to vector as well before translating instance, + // in order to leave final position unaltered + Vectorf3 v = vector.negative(); + v.rotate((*i)->rotation, (*i)->offset); + v.scale((*i)->scaling_factor); + (*i)->offset.translate(v.x, v.y); } this->update_bounding_box(); } @@ -514,6 +520,26 @@ ModelObject::scale(const Pointf3 &versor) this->invalidate_bounding_box(); } +void +ModelObject::rotate(float angle, const Axis &axis) +{ + for (ModelVolumePtrs::const_iterator v = this->volumes.begin(); v != this->volumes.end(); ++v) { + (*v)->mesh.rotate(angle, axis); + } + this->origin_translation = Pointf3(0,0,0); + this->invalidate_bounding_box(); +} + +void +ModelObject::flip(const Axis &axis) +{ + for (ModelVolumePtrs::const_iterator v = this->volumes.begin(); v != this->volumes.end(); ++v) { + (*v)->mesh.flip(axis); + } + this->origin_translation = Pointf3(0,0,0); + this->invalidate_bounding_box(); +} + size_t ModelObject::materials_count() const { diff --git a/xs/src/libslic3r/Model.hpp b/xs/src/libslic3r/Model.hpp index 2ec7c5b08..e5683cb57 100644 --- a/xs/src/libslic3r/Model.hpp +++ b/xs/src/libslic3r/Model.hpp @@ -129,6 +129,8 @@ class ModelObject void translate(const Vectorf3 &vector); void translate(coordf_t x, coordf_t y, coordf_t z); void scale(const Pointf3 &versor); + void rotate(float angle, const Axis &axis); + void flip(const Axis &axis); size_t materials_count() const; size_t facets_count() const; bool needed_repair() const; @@ -175,7 +177,7 @@ class ModelInstance { friend class ModelObject; public: - double rotation; // around mesh center point + double rotation; // in radians around mesh center point double scaling_factor; Pointf offset; // in unscaled coordinates diff --git a/xs/src/libslic3r/PlaceholderParser.cpp b/xs/src/libslic3r/PlaceholderParser.cpp index 31056dd2c..583cf51e6 100644 --- a/xs/src/libslic3r/PlaceholderParser.cpp +++ b/xs/src/libslic3r/PlaceholderParser.cpp @@ -29,22 +29,14 @@ PlaceholderParser::update_timestamp() ss << std::setw(2) << std::setfill('0') << timeinfo->tm_hour; ss << std::setw(2) << std::setfill('0') << timeinfo->tm_min; ss << std::setw(2) << std::setfill('0') << timeinfo->tm_sec; - this->_single["timestamp"] = ss.str(); + this->set("timestamp", ss.str()); } - this->_single["year"] = this->_int_to_string(1900 + timeinfo->tm_year); - this->_single["month"] = this->_int_to_string(1 + timeinfo->tm_mon); - this->_single["day"] = this->_int_to_string(timeinfo->tm_mday); - this->_single["hour"] = this->_int_to_string(timeinfo->tm_hour); - this->_single["minute"] = this->_int_to_string(timeinfo->tm_min); - this->_single["second"] = this->_int_to_string(timeinfo->tm_sec); -} - -std::string -PlaceholderParser::_int_to_string(int value) const -{ - std::ostringstream ss; - ss << value; - return ss.str(); + this->set("year", 1900 + timeinfo->tm_year); + this->set("month", 1 + timeinfo->tm_mon); + this->set("day", timeinfo->tm_mday); + this->set("hour", timeinfo->tm_hour); + this->set("minute", timeinfo->tm_min); + this->set("second", timeinfo->tm_sec); } void PlaceholderParser::apply_config(DynamicPrintConfig &config) @@ -66,53 +58,20 @@ void PlaceholderParser::apply_config(DynamicPrintConfig &config) i != opt_keys.end(); ++i) { const t_config_option_key &key = *i; - - // set placeholders for options with multiple values - const ConfigOptionDef &def = (*config.def)[key]; - switch (def.type) { - case coFloats: - this->set_multiple_from_vector(key, - *(ConfigOptionFloats*)config.option(key)); - break; - - case coInts: - this->set_multiple_from_vector(key, - *(ConfigOptionInts*)config.option(key)); - break; - - case coStrings: - this->set_multiple_from_vector(key, - *(ConfigOptionStrings*)config.option(key)); - break; - - case coPoints: - this->set_multiple_from_vector(key, - *(ConfigOptionPoints*)config.option(key)); - break; - - case coBools: - this->set_multiple_from_vector(key, - *(ConfigOptionBools*)config.option(key)); - break; - - case coPoint: - { - const ConfigOptionPoint &opt = - *(ConfigOptionPoint*)config.option(key); - - this->_single[key] = opt.serialize(); - - Pointf val = opt; - this->_multiple[key + "_X"] = val.x; - this->_multiple[key + "_Y"] = val.y; - } - - break; - - default: + const ConfigOption* opt = config.option(key); + + if (const ConfigOptionVectorBase* optv = dynamic_cast(opt)) { + // set placeholders for options with multiple values + this->set(key, optv->vserialize()); + } else if (const ConfigOptionPoint* optp = dynamic_cast(opt)) { + this->_single[key] = optp->serialize(); + + Pointf val = *optp; + this->_multiple[key + "_X"] = val.x; + this->_multiple[key + "_Y"] = val.y; + } else { // set single-value placeholders - this->_single[key] = config.serialize(key); - break; + this->_single[key] = opt->serialize(); } } } @@ -121,34 +80,30 @@ void PlaceholderParser::set(const std::string &key, const std::string &value) { this->_single[key] = value; + this->_multiple.erase(key); } -std::ostream& operator<<(std::ostream &stm, const Pointf &pointf) +void +PlaceholderParser::set(const std::string &key, int value) { - return stm << pointf.x << "," << pointf.y; + std::ostringstream ss; + ss << value; + this->set(key, ss.str()); } -template -void PlaceholderParser::set_multiple_from_vector(const std::string &key, - ConfigOptionVector &opt) +void +PlaceholderParser::set(const std::string &key, const std::vector &values) { - const std::vector &vals = opt.values; - - for (size_t i = 0; i < vals.size(); ++i) { - std::stringstream multikey_stm; - multikey_stm << key << "_" << i; - - std::stringstream val_stm; - val_stm << vals[i]; - - this->_multiple[multikey_stm.str()] = val_stm.str(); - } - - if (vals.size() > 0) { - std::stringstream val_stm; - val_stm << vals[0]; - this->_multiple[key] = val_stm.str(); + for (std::vector::const_iterator v = values.begin(); v != values.end(); ++v) { + std::stringstream ss; + ss << key << "_" << (v - values.begin()); + + this->_multiple[ ss.str() ] = *v; + if (v == values.begin()) { + this->_multiple[key] = *v; + } } + this->_single.erase(key); } #ifdef SLIC3RXS diff --git a/xs/src/libslic3r/PlaceholderParser.hpp b/xs/src/libslic3r/PlaceholderParser.hpp index eb061fab2..25d1bcdc3 100644 --- a/xs/src/libslic3r/PlaceholderParser.hpp +++ b/xs/src/libslic3r/PlaceholderParser.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "PrintConfig.hpp" @@ -20,12 +21,8 @@ class PlaceholderParser void update_timestamp(); void apply_config(DynamicPrintConfig &config); void set(const std::string &key, const std::string &value); - - private: - template - void set_multiple_from_vector( - const std::string &key, ConfigOptionVector &opt); - std::string _int_to_string(int value) const; + void set(const std::string &key, int value); + void set(const std::string &key, const std::vector &values); }; } diff --git a/xs/src/libslic3r/Point.cpp b/xs/src/libslic3r/Point.cpp index 9a564f879..9707cd931 100644 --- a/xs/src/libslic3r/Point.cpp +++ b/xs/src/libslic3r/Point.cpp @@ -3,7 +3,6 @@ #include "MultiPoint.hpp" #include #include -#include namespace Slic3r { @@ -340,6 +339,12 @@ REGISTER_CLASS(Point3, "Point3"); #endif +std::ostream& +operator<<(std::ostream &stm, const Pointf &pointf) +{ + return stm << pointf.x << "," << pointf.y; +} + void Pointf::scale(double factor) { diff --git a/xs/src/libslic3r/Point.hpp b/xs/src/libslic3r/Point.hpp index 327199690..f9850c774 100644 --- a/xs/src/libslic3r/Point.hpp +++ b/xs/src/libslic3r/Point.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Slic3r { @@ -77,6 +78,8 @@ class Point3 : public Point explicit Point3(coord_t _x = 0, coord_t _y = 0, coord_t _z = 0): Point(_x, _y), z(_z) {}; }; +std::ostream& operator<<(std::ostream &stm, const Pointf &pointf); + class Pointf { public: diff --git a/xs/src/libslic3r/Polyline.cpp b/xs/src/libslic3r/Polyline.cpp index 710ca9bd1..22f190dbd 100644 --- a/xs/src/libslic3r/Polyline.cpp +++ b/xs/src/libslic3r/Polyline.cpp @@ -139,11 +139,31 @@ Polyline::simplify_by_visibility(const T &area) Points &pp = this->points; // find first point in area - size_t start = 0; - while (start < pp.size() && !area.contains(pp[start])) { - start++; + size_t s = 0; + while (s < pp.size() && !area.contains(pp[s])) { + ++s; } + // find last point in area + size_t e = pp.size()-1; + while (e > 0 && !area.contains(pp[e])) { + --e; + } + + // this ugly algorithm resembles a binary search + while (e > s + 1) { + size_t mid = (s + e) / 2; + if (area.contains(Line(pp[s], pp[mid]))) { + pp.erase(pp.begin() + s + 1, pp.begin() + mid); + // repeat recursively until no further simplification is possible + ++s; + e = pp.size()-1; + } else { + e = mid; + } + } + /* + // The following implementation is complete but it's not efficient at all: for (size_t s = start; s < pp.size() && !pp.empty(); ++s) { // find the farthest point to which we can build // a line that is contained in the supplied area @@ -158,6 +178,7 @@ Polyline::simplify_by_visibility(const T &area) } } } + */ } template void Polyline::simplify_by_visibility(const ExPolygon &area); template void Polyline::simplify_by_visibility(const ExPolygonCollection &area); diff --git a/xs/src/libslic3r/Print.cpp b/xs/src/libslic3r/Print.cpp index d3bb43b3c..3ee2d0c1f 100644 --- a/xs/src/libslic3r/Print.cpp +++ b/xs/src/libslic3r/Print.cpp @@ -172,7 +172,8 @@ Print::invalidate_state_by_config_options(const std::vector } else if (*opt_key == "brim_width") { steps.insert(psBrim); steps.insert(psSkirt); - } else if (*opt_key == "nozzle_diameter") { + } else if (*opt_key == "nozzle_diameter" + || *opt_key == "resolution") { osteps.insert(posSlice); } else if (*opt_key == "avoid_crossing_perimeters" || *opt_key == "bed_shape" @@ -300,7 +301,7 @@ Print::step_done(PrintObjectStep step) const // returns 0-based indices of used extruders std::set -Print::extruders() const +Print::object_extruders() const { std::set extruders; @@ -316,6 +317,16 @@ Print::extruders() const if ((*region)->config.top_solid_layers.value > 0 || (*region)->config.bottom_solid_layers.value > 0) extruders.insert((*region)->config.solid_infill_extruder - 1); } + + return extruders; +} + +// returns 0-based indices of used extruders +std::set +Print::support_material_extruders() const +{ + std::set extruders; + FOREACH_OBJECT(this, object) { if ((*object)->has_support_material()) { extruders.insert((*object)->config.support_material_extruder - 1); @@ -326,6 +337,18 @@ Print::extruders() const return extruders; } +// returns 0-based indices of used extruders +std::set +Print::extruders() const +{ + std::set extruders = this->object_extruders(); + + std::set s_extruders = this->support_material_extruders(); + extruders.insert(s_extruders.begin(), s_extruders.end()); + + return extruders; +} + void Print::_simplify_slices(double distance) { diff --git a/xs/src/libslic3r/Print.hpp b/xs/src/libslic3r/Print.hpp index 47defe770..05a60001b 100644 --- a/xs/src/libslic3r/Print.hpp +++ b/xs/src/libslic3r/Print.hpp @@ -199,6 +199,8 @@ class Print Flow brim_flow() const; Flow skirt_flow() const; + std::set object_extruders() const; + std::set support_material_extruders() const; std::set extruders() const; void _simplify_slices(double distance); double max_allowed_layer_height() const; diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 4bf981473..d356fcc89 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -232,6 +232,12 @@ PrintConfigDef::build_def() { Options["fan_below_layer_time"].min = 0; Options["fan_below_layer_time"].max = 1000; + Options["filament_colour"].type = coStrings; + Options["filament_colour"].label = "Color"; + Options["filament_colour"].tooltip = "This is only used in the Slic3r interface as a visual help."; + Options["filament_colour"].cli = "filament-color=s@"; + Options["filament_colour"].gui_type = "color"; + Options["filament_diameter"].type = coFloats; Options["filament_diameter"].label = "Diameter"; Options["filament_diameter"].tooltip = "Enter your filament diameter here. Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average."; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index 8952afa8d..3599c2219 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -409,6 +409,7 @@ class PrintConfig : public GCodeConfig ConfigOptionPoints extruder_offset; ConfigOptionBool fan_always_on; ConfigOptionInt fan_below_layer_time; + ConfigOptionStrings filament_colour; ConfigOptionFloat first_layer_acceleration; ConfigOptionInt first_layer_bed_temperature; ConfigOptionFloatOrPercent first_layer_extrusion_width; @@ -464,6 +465,8 @@ class PrintConfig : public GCodeConfig this->extruder_offset.values[0] = Pointf(0,0); this->fan_always_on.value = false; this->fan_below_layer_time.value = 60; + this->filament_colour.values.resize(1); + this->filament_colour.values[0] = "#FFFFFF"; this->first_layer_acceleration.value = 0; this->first_layer_bed_temperature.value = 0; this->first_layer_extrusion_width.value = 200; @@ -523,6 +526,7 @@ class PrintConfig : public GCodeConfig if (opt_key == "extruder_offset") return &this->extruder_offset; if (opt_key == "fan_always_on") return &this->fan_always_on; if (opt_key == "fan_below_layer_time") return &this->fan_below_layer_time; + if (opt_key == "filament_colour") return &this->filament_colour; if (opt_key == "first_layer_acceleration") return &this->first_layer_acceleration; if (opt_key == "first_layer_bed_temperature") return &this->first_layer_bed_temperature; if (opt_key == "first_layer_extrusion_width") return &this->first_layer_extrusion_width; diff --git a/xs/src/libslic3r/PrintObject.cpp b/xs/src/libslic3r/PrintObject.cpp index cf1ee20e4..46d08e06f 100644 --- a/xs/src/libslic3r/PrintObject.cpp +++ b/xs/src/libslic3r/PrintObject.cpp @@ -222,8 +222,7 @@ PrintObject::invalidate_state_by_config_options(const std::vectorstl); } +void TriangleMesh::rotate(float angle, const Axis &axis) +{ + // admesh uses degrees + angle = Slic3r::Geometry::rad2deg(angle); + + if (axis == X) { + stl_rotate_x(&(this->stl), angle); + } else if (axis == Y) { + stl_rotate_y(&(this->stl), angle); + } else if (axis == Z) { + stl_rotate_z(&(this->stl), angle); + } + stl_invalidate_shared_vertices(&this->stl); +} + void TriangleMesh::rotate_x(float angle) { - stl_rotate_x(&(this->stl), angle); - stl_invalidate_shared_vertices(&this->stl); + this->rotate(angle, X); } void TriangleMesh::rotate_y(float angle) { - stl_rotate_y(&(this->stl), angle); - stl_invalidate_shared_vertices(&this->stl); + this->rotate(angle, Y); } void TriangleMesh::rotate_z(float angle) { - stl_rotate_z(&(this->stl), angle); + this->rotate(angle, Z); +} + +void TriangleMesh::flip(const Axis &axis) +{ + if (axis == X) { + stl_mirror_yz(&this->stl); + } else if (axis == Y) { + stl_mirror_xz(&this->stl); + } else if (axis == Z) { + stl_mirror_xy(&this->stl); + } stl_invalidate_shared_vertices(&this->stl); } void TriangleMesh::flip_x() { - stl_mirror_yz(&this->stl); - stl_invalidate_shared_vertices(&this->stl); + this->flip(X); } void TriangleMesh::flip_y() { - stl_mirror_xz(&this->stl); - stl_invalidate_shared_vertices(&this->stl); + this->flip(Y); } void TriangleMesh::flip_z() { - stl_mirror_xy(&this->stl); - stl_invalidate_shared_vertices(&this->stl); + this->flip(Z); } void TriangleMesh::align_to_origin() diff --git a/xs/src/libslic3r/TriangleMesh.hpp b/xs/src/libslic3r/TriangleMesh.hpp index 43ac68419..ff12e8427 100644 --- a/xs/src/libslic3r/TriangleMesh.hpp +++ b/xs/src/libslic3r/TriangleMesh.hpp @@ -32,9 +32,11 @@ class TriangleMesh void scale(float factor); void scale(const Pointf3 &versor); void translate(float x, float y, float z); + void rotate(float angle, const Axis &axis); void rotate_x(float angle); void rotate_y(float angle); void rotate_z(float angle); + void flip(const Axis &axis); void flip_x(); void flip_y(); void flip_z(); diff --git a/xs/src/libslic3r/libslic3r.h b/xs/src/libslic3r/libslic3r.h index 25d3c7f48..fc3bd1793 100644 --- a/xs/src/libslic3r/libslic3r.h +++ b/xs/src/libslic3r/libslic3r.h @@ -6,7 +6,7 @@ #include #include -#define SLIC3R_VERSION "1.2.7-dev" +#define SLIC3R_VERSION "1.2.8-dev" #define EPSILON 1e-4 #define SCALING_FACTOR 0.000001 @@ -17,7 +17,12 @@ typedef long coord_t; typedef double coordf_t; -namespace Slic3r {} +namespace Slic3r { + +// TODO: make sure X = 0 +enum Axis { X, Y, Z }; + +} using namespace Slic3r; /* Implementation of CONFESS("foo"): */ diff --git a/xs/t/03_point.t b/xs/t/03_point.t index d983fafc9..cb71f68f5 100644 --- a/xs/t/03_point.t +++ b/xs/t/03_point.t @@ -4,7 +4,7 @@ use strict; use warnings; use Slic3r::XS; -use Test::More tests => 22; +use Test::More tests => 24; my $point = Slic3r::Point->new(10, 15); is_deeply [ @$point ], [10, 15], 'point roundtrip'; @@ -32,12 +32,14 @@ ok !$point->coincides_with($point2), 'coincides_with'; { my $line = Slic3r::Line->new([0,0], [100,0]); - is +Slic3r::Point->new(0,0)->distance_to_line($line), 0, 'distance_to_line()'; + is +Slic3r::Point->new(0,0) ->distance_to_line($line), 0, 'distance_to_line()'; is +Slic3r::Point->new(100,0)->distance_to_line($line), 0, 'distance_to_line()'; - is +Slic3r::Point->new(50,0)->distance_to_line($line), 0, 'distance_to_line()'; + is +Slic3r::Point->new(50,0) ->distance_to_line($line), 0, 'distance_to_line()'; is +Slic3r::Point->new(150,0)->distance_to_line($line), 50, 'distance_to_line()'; - is +Slic3r::Point->new(0,50)->distance_to_line($line), 50, 'distance_to_line()'; + is +Slic3r::Point->new(0,50) ->distance_to_line($line), 50, 'distance_to_line()'; is +Slic3r::Point->new(50,50)->distance_to_line($line), 50, 'distance_to_line()'; + is +Slic3r::Point->new(50,50) ->perp_distance_to_line($line), 50, 'perp_distance_to_line()'; + is +Slic3r::Point->new(150,50)->perp_distance_to_line($line), 50, 'perp_distance_to_line()'; } { diff --git a/xs/t/09_polyline.t b/xs/t/09_polyline.t index fa5a3c48b..0a7b8dcb0 100644 --- a/xs/t/09_polyline.t +++ b/xs/t/09_polyline.t @@ -4,7 +4,7 @@ use strict; use warnings; use Slic3r::XS; -use Test::More tests => 21; +use Test::More tests => 18; my $points = [ [100, 100], @@ -88,7 +88,8 @@ is_deeply $polyline->pp, [ @$points, @$points ], 'append_polyline'; is scalar(@$p2), 4, 'split_at'; } -{ +# disabled because we now use a more efficient but incomplete algorithm +if (0) { my $polyline = Slic3r::Polyline->new( map [$_,10], (0,10,20,30,40,50,60) ); diff --git a/xs/t/12_extrusionpathcollection.t b/xs/t/12_extrusionpathcollection.t index 73608cba7..e7e0b1316 100644 --- a/xs/t/12_extrusionpathcollection.t +++ b/xs/t/12_extrusionpathcollection.t @@ -4,7 +4,7 @@ use strict; use warnings; use Slic3r::XS; -use Test::More tests => 16; +use Test::More tests => 18; my $points = [ [100, 100], @@ -87,4 +87,11 @@ is scalar(@{$collection->[1]}), 1, 'appended collection was duplicated'; pass 'chained_path with no_sort'; } +{ + my $coll2 = $collection->clone; + ok !$coll2->no_sort, 'expected no_sort value'; + $coll2->no_sort(1); + ok $coll2->clone->no_sort, 'no_sort is kept after clone'; +} + __END__ diff --git a/xs/t/14_geometry.t b/xs/t/14_geometry.t index 8dfdecb78..70b623029 100644 --- a/xs/t/14_geometry.t +++ b/xs/t/14_geometry.t @@ -4,7 +4,7 @@ use strict; use warnings; use Slic3r::XS; -use Test::More tests => 8; +use Test::More tests => 9; use constant PI => 4 * atan2(1, 1); @@ -31,4 +31,11 @@ use constant PI => 4 * atan2(1, 1); ok !Slic3r::Geometry::directions_parallel_within(PI/2, PI, 0), 'directions_parallel_within'; ok !Slic3r::Geometry::directions_parallel_within(PI/2, PI, PI/180), 'directions_parallel_within'; } + +{ + my $positions = Slic3r::Geometry::arrange(4, Slic3r::Pointf->new(20, 20), + 5, Slic3r::Geometry::BoundingBoxf->new); + is scalar(@$positions), 4, 'arrange() returns expected number of positions'; +} + __END__ diff --git a/xs/xsp/ExtrusionEntityCollection.xsp b/xs/xsp/ExtrusionEntityCollection.xsp index a7c5a8be0..08577c892 100644 --- a/xs/xsp/ExtrusionEntityCollection.xsp +++ b/xs/xsp/ExtrusionEntityCollection.xsp @@ -7,6 +7,8 @@ %name{Slic3r::ExtrusionPath::Collection} class ExtrusionEntityCollection { %name{_new} ExtrusionEntityCollection(); + Clone clone() + %code{% RETVAL = THIS->clone(); %}; void reverse(); void clear() %code{% THIS->entities.clear(); %}; @@ -24,6 +26,8 @@ Clone last_point(); int count() %code{% RETVAL = THIS->entities.size(); %}; + int items_count() + %code{% RETVAL = THIS->items_count(); %}; bool empty() %code{% RETVAL = THIS->entities.empty(); %}; std::vector orig_indices() diff --git a/xs/xsp/Geometry.xsp b/xs/xsp/Geometry.xsp index 0df74a4bd..c5442a0b6 100644 --- a/xs/xsp/Geometry.xsp +++ b/xs/xsp/Geometry.xsp @@ -8,6 +8,9 @@ %package{Slic3r::Geometry}; +Pointfs arrange(size_t total_parts, Pointf* part, coordf_t dist, BoundingBoxf* bb) + %code{% RETVAL = Slic3r::Geometry::arrange(total_parts, *part, dist, *bb); %}; + %{ bool @@ -87,4 +90,17 @@ simplify_polygons(polygons, tolerance) OUTPUT: RETVAL + +IV +_constant() + ALIAS: + X = X + Y = Y + Z = Z + PROTOTYPE: + CODE: + RETVAL = ix; + OUTPUT: RETVAL + %} + diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp index 611e2cae9..e8372206c 100644 --- a/xs/xsp/Layer.xsp +++ b/xs/xsp/Layer.xsp @@ -71,6 +71,7 @@ %code%{ RETVAL = (int)(intptr_t)THIS; %}; void make_slices(); + void merge_slices(); bool any_internal_region_slice_contains_polyline(Polyline* polyline) %code%{ RETVAL = THIS->any_internal_region_slice_contains(*polyline); %}; bool any_bottom_region_slice_contains_polyline(Polyline* polyline) diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index 9870d7dd5..cf9b974f4 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -182,6 +182,8 @@ ModelMaterial::attributes() void translate(double x, double y, double z); void scale_xyz(Pointf3* versor) %code{% THIS->scale(*versor); %}; + void rotate(float angle, Axis axis); + void flip(Axis axis); Model* cut(double z) %code%{ diff --git a/xs/xsp/PlaceholderParser.xsp b/xs/xsp/PlaceholderParser.xsp index 760f3b71f..b36a47189 100644 --- a/xs/xsp/PlaceholderParser.xsp +++ b/xs/xsp/PlaceholderParser.xsp @@ -14,6 +14,8 @@ void apply_config(DynamicPrintConfig *config) %code%{ THIS->apply_config(*config); %}; void set(std::string key, std::string value); + void set_multiple(std::string key, std::vector values) + %code%{ THIS->set(key, values); %}; void _single_set(std::string k, std::string v) %code%{ THIS->_single[k] = v; %}; diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index c9b93bba3..afcd6f839 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -161,6 +161,22 @@ _constant() void set_step_started(PrintStep step) %code%{ THIS->state.set_started(step); %}; + std::vector object_extruders() + %code%{ + std::set extruders = THIS->object_extruders(); + RETVAL.reserve(extruders.size()); + for (std::set::const_iterator e = extruders.begin(); e != extruders.end(); ++e) { + RETVAL.push_back(*e); + } + %}; + std::vector support_material_extruders() + %code%{ + std::set extruders = THIS->support_material_extruders(); + RETVAL.reserve(extruders.size()); + for (std::set::const_iterator e = extruders.begin(); e != extruders.end(); ++e) { + RETVAL.push_back(*e); + } + %}; std::vector extruders() %code%{ std::set extruders = THIS->extruders(); diff --git a/xs/xsp/my.map b/xs/xsp/my.map index bf5828004..395b630e1 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -188,6 +188,7 @@ Clone O_OBJECT_SLIC3R_T GLVertexArray* O_OBJECT_SLIC3R +Axis T_UV ExtrusionLoopRole T_UV ExtrusionRole T_UV FlowRole T_UV diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index 845b98707..050e61b99 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -176,6 +176,12 @@ %typemap{SupportLayerPtrs*}; +%typemap{Axis}{parsed}{ + %cpp_type{Axis}; + %precall_code{% + $CVar = (Axis)SvUV($PerlVar); + %}; +}; %typemap{SurfaceType}{parsed}{ %cpp_type{SurfaceType}; %precall_code{%