diff --git a/README.markdown b/README.markdown index 41c60d293..f12e44d3b 100644 --- a/README.markdown +++ b/README.markdown @@ -79,6 +79,9 @@ The author is Alessandro Ranellucci (me). Usage: slic3r.pl [ OPTIONS ] file.stl --help Output this usage screen and exit + --save Save configuration to the specified file + --load Load configuration from the specified file + Printer options: --nozzle-diameter Diameter of nozzle in mm (default: 0.55) diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index d4b40c0eb..8a065eae9 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -4,6 +4,176 @@ use warnings; use constant PI => 4 * atan2(1, 1); +our $Options = { + + # printer options + 'nozzle_diameter' => { + label => 'Nozzle diameter', + type => 'f', + }, + 'print_center' => { + label => 'Print center', + type => 'point', + serialize => sub { join ',', @{$_[0]} }, + deserialize => sub { [ split /,/, $_[0] ] }, + }, + 'use_relative_e_distances' => { + label => 'Use relative E distances', + type => 'bool', + }, + 'z_offset' => { + label => 'Z offset', + type => 'f', + }, + + # filament options + 'filament_diameter' => { + label => 'Diameter (mm)', + type => 'f', + }, + 'filament_packing_density' => { + label => 'Packing density (mm)', + type => 'f', + }, + + # speed options + 'print_feed_rate' => { + label => 'Print feed rate (mm/s)', + type => 'f', + }, + 'travel_feed_rate' => { + label => 'Travel feed rate (mm/s)', + type => 'f', + }, + 'perimeter_feed_rate' => { + label => 'Perimeter feed rate (mm/s)', + type => 'f', + }, + 'bottom_layer_speed_ratio' => { + label => 'Bottom layer ratio', + type => 'f', + }, + + # accuracy options + 'layer_height' => { + label => 'Layer height (mm)', + type => 'f', + }, + + # print options + 'perimeter_offsets' => { + label => 'Perimeters', + type => 'i', + }, + 'solid_layers' => { + label => 'Solid layers', + type => 'i', + }, + 'fill_density' => { + label => 'Fill density', + type => 'f', + }, + 'fill_angle' => { + label => 'Fill angle (°)', + type => 'i', + }, + 'temperature' => { + label => 'Temperature (°C)', + type => 'i', + }, + + # retraction options + 'retract_length' => { + label => 'Length (mm)', + type => 'f', + }, + 'retract_speed' => { + label => 'Speed (mm/s)', + type => 'i', + }, + 'retract_restart_extra' => { + label => 'Extra length on restart (mm)', + type => 'f', + }, + 'retract_before_travel' => { + label => 'Minimum travel after retraction (mm)', + type => 'f', + }, + + # skirt options + 'skirts' => { + label => 'Loops', + type => 'i', + }, + 'skirt_distance' => { + label => 'Distance from object (mm)', + type => 'i', + }, + + # transform options + 'scale' => { + label => 'Scale', + type => 'f', + }, + 'rotate' => { + label => 'Rotate (°)', + type => 'i', + }, + 'multiply_x' => { + label => 'Multiply along X', + type => 'i', + }, + 'multiply_y' => { + label => 'Multiply along Y', + type => 'i', + }, + 'multiply_distance' => { + label => 'Multiply distance', + type => 'i', + }, +}; + +sub get { + my $class = @_ == 2 ? shift : undef; + my ($opt_key) = @_; + no strict 'refs'; + return ${"Slic3r::$opt_key"}; +} + +sub set { + my $class = @_ == 3 ? shift : undef; + my ($opt_key, $value) = @_; + no strict 'refs'; + ${"Slic3r::$opt_key"} = $value; +} + +sub save { + my $class = shift; + my ($file) = @_; + + open my $fh, '>', $file; + foreach my $opt (sort keys %$Options) { + my $value = get($opt); + $value = $Options->{$opt}{serialize}->($value) if $Options->{$opt}{serialize}; + printf $fh "%s = %s\n", $opt, $value; + } + close $fh; +} + +sub load { + my $class = shift; + my ($file) = @_; + + open my $fh, '<', $file; + while (<$fh>) { + next if /^\s*#/; + /^(\w+) = (.+)/ or die "Unreadable configuration file (invalid data at line $.)\n"; + my $opt = $Options->{$1} or die "Unknown option $1 at like $.\n"; + set($1, $opt->{deserialize} ? $opt->{deserialize}->($2) : $2); + } + close $fh; +} + sub validate { my $class = shift; diff --git a/lib/Slic3r/GUI/OptionsGroup.pm b/lib/Slic3r/GUI/OptionsGroup.pm index fbef061d9..b5ad7797d 100644 --- a/lib/Slic3r/GUI/OptionsGroup.pm +++ b/lib/Slic3r/GUI/OptionsGroup.pm @@ -6,6 +6,9 @@ use Wx qw(:sizer); use Wx::Event qw(EVT_TEXT EVT_CHECKBOX); use base 'Wx::StaticBoxSizer'; +# not very elegant, but this solution is temporary waiting for a better GUI +our @reload_callbacks = (); + sub new { my $class = shift; my ($parent, %p) = @_; @@ -15,28 +18,43 @@ sub new { my $grid_sizer = Wx::FlexGridSizer->new(scalar(@{$p{options}}), 2, 2, 0); - foreach my $opt (@{$p{options}}) { + foreach my $opt_key (@{$p{options}}) { + my $opt = $Slic3r::Config::Options->{$opt_key}; my $label = Wx::StaticText->new($parent, -1, "$opt->{label}:", Wx::wxDefaultPosition, [180,-1]); $label->Wrap(180); # needed to avoid Linux/GTK bug my $field; if ($opt->{type} =~ /^(i|f)$/) { - $field = Wx::TextCtrl->new($parent, -1, ${$opt->{value}}); - EVT_TEXT($parent, $field, sub { ${$opt->{value}} = $field->GetValue }); + $field = Wx::TextCtrl->new($parent, -1, Slic3r::Config->get($opt_key)); + EVT_TEXT($parent, $field, sub { Slic3r::Config->set($opt_key, $field->GetValue) }); + push @reload_callbacks, sub { $field->SetValue(Slic3r::Config->get($opt_key)) }; } elsif ($opt->{type} eq 'bool') { $field = Wx::CheckBox->new($parent, -1, ""); - $field->SetValue(${$opt->{value}}); - EVT_TEXT($parent, $field, sub { ${$opt->{value}} = $field->GetValue }); + $field->SetValue(Slic3r::Config->get($opt_key)); + EVT_CHECKBOX($parent, $field, sub { Slic3r::Config->set($opt_key, $field->GetValue) }); + push @reload_callbacks, sub { $field->SetValue(Slic3r::Config->get($opt_key)) }; } elsif ($opt->{type} eq 'point') { $field = Wx::BoxSizer->new(wxHORIZONTAL); my $field_size = Wx::Size->new(40, -1); + my $value = Slic3r::Config->get($opt_key); $field->Add($_) for ( Wx::StaticText->new($parent, -1, "x:"), - my $x_field = Wx::TextCtrl->new($parent, -1, ${$opt->{value}}->[0], Wx::wxDefaultPosition, $field_size), + my $x_field = Wx::TextCtrl->new($parent, -1, $value->[0], Wx::wxDefaultPosition, $field_size), Wx::StaticText->new($parent, -1, " y:"), - my $y_field = Wx::TextCtrl->new($parent, -1, ${$opt->{value}}->[1], Wx::wxDefaultPosition, $field_size), + my $y_field = Wx::TextCtrl->new($parent, -1, $value->[1], Wx::wxDefaultPosition, $field_size), ); - EVT_TEXT($parent, $x_field, sub { ${$opt->{value}}->[0] = $x_field->GetValue }); - EVT_TEXT($parent, $y_field, sub { ${$opt->{value}}->[1] = $y_field->GetValue }); + my $set_value = sub { + my ($i, $value) = @_; + my $val = Slic3r::Config->get($opt_key); + $val->[$i] = $value; + Slic3r::Config->set($opt_key, $val); + }; + EVT_TEXT($parent, $x_field, sub { $set_value->(0, $x_field->GetValue) }); + EVT_TEXT($parent, $y_field, sub { $set_value->(1, $y_field->GetValue) }); + push @reload_callbacks, sub { + my $value = Slic3r::Config->get($opt_key); + $x_field->SetValue($value->[0]); + $y_field->SetValue($value->[1]); + }; } else { die "Unsupported option type: " . $opt->{type}; } diff --git a/lib/Slic3r/GUI/SkeinPanel.pm b/lib/Slic3r/GUI/SkeinPanel.pm index eb250072b..471013f66 100644 --- a/lib/Slic3r/GUI/SkeinPanel.pm +++ b/lib/Slic3r/GUI/SkeinPanel.pm @@ -4,7 +4,8 @@ use warnings; use utf8; use File::Basename qw(basename); -use Wx qw(:sizer :progressdialog wxOK wxICON_INFORMATION wxICON_ERROR wxID_OK wxFD_OPEN); +use Wx qw(:sizer :progressdialog wxOK wxICON_INFORMATION wxICON_ERROR wxID_OK wxFD_OPEN + wxFD_SAVE wxDEFAULT wxNORMAL); use Wx::Event qw(EVT_BUTTON); use base 'Wx::Panel'; @@ -16,187 +17,38 @@ sub new { my %panels = ( printer => Slic3r::GUI::OptionsGroup->new($self, title => 'Printer', - options => [ - { - label => 'Nozzle diameter', - value => \$Slic3r::nozzle_diameter, - type => 'f', - }, - { - label => 'Print center', - value => \$Slic3r::print_center, - type => 'point', - }, - { - label => 'Use relative E distances', - value => \$Slic3r::use_relative_e_distances, - type => 'bool', - }, - { - label => 'Z offset', - value => \$Slic3r::z_offset, - type => 'f', - }, - ], + options => [qw(nozzle_diameter print_center use_relative_e_distances z_offset)], ), - filament => Slic3r::GUI::OptionsGroup->new($self, title => 'Filament', - options => [ - { - label => 'Diameter (mm)', - value => \$Slic3r::filament_diameter, - type => 'f', - }, - { - label => 'Packing density (mm)', - value => \$Slic3r::filament_packing_density, - type => 'f', - }, - ], + options => [qw(filament_diameter filament_packing_density)], ), - speed => Slic3r::GUI::OptionsGroup->new($self, title => 'Speed', - options => [ - { - label => 'Print feed rate (mm/s)', - value => \$Slic3r::print_feed_rate, - type => 'f', - }, - { - label => 'Travel feed rate (mm/s)', - value => \$Slic3r::travel_feed_rate, - type => 'f', - }, - { - label => 'Perimeter feed rate (mm/s)', - value => \$Slic3r::perimeter_feed_rate, - type => 'f', - }, - { - label => 'Bottom layer ratio', - value => \$Slic3r::bottom_layer_speed_ratio, - type => 'f', - }, - ], + options => [qw(print_feed_rate travel_feed_rate perimeter_feed_rate bottom_layer_speed_ratio)], ), - accuracy => Slic3r::GUI::OptionsGroup->new($self, title => 'Accuracy', - options => [ - { - label => 'Layer height (mm)', - value => \$Slic3r::layer_height, - type => 'f', - }, - ], + options => [qw(layer_height)], ), - print => Slic3r::GUI::OptionsGroup->new($self, title => 'Print settings', - options => [ - { - label => 'Perimeters', - value => \$Slic3r::perimeter_offsets, - type => 'i', - }, - { - label => 'Solid layers', - value => \$Slic3r::solid_layers, - type => 'i', - }, - { - label => 'Fill density', - value => \$Slic3r::fill_density, - type => 'f', - }, - { - label => 'Fill angle (°)', - value => \$Slic3r::fill_angle, - type => 'i', - }, - { - label => 'Temperature (°C)', - value => \$Slic3r::temperature, - type => 'i', - }, - ], + options => [qw(perimeter_offsets solid_layers fill_density fill_angle temperature)], ), - retract => Slic3r::GUI::OptionsGroup->new($self, title => 'Retraction', - options => [ - { - label => 'Length (mm)', - value => \$Slic3r::retract_length, - type => 'f', - }, - { - label => 'Speed (mm/s)', - value => \$Slic3r::retract_speed, - type => 'i', - }, - { - label => 'Extra length on restart (mm)', - value => \$Slic3r::retract_restart_extra, - type => 'f', - }, - { - label => 'Minimum travel after retraction (mm)', - value => \$Slic3r::retract_before_travel, - type => 'f', - }, - ], + options => [qw(retract_length retract_speed retract_restart_extra retract_before_travel)], ), - skirt => Slic3r::GUI::OptionsGroup->new($self, title => 'Skirt', - options => [ - { - label => 'Loops', - value => \$Slic3r::skirts, - type => 'i', - }, - { - label => 'Distance from object (mm)', - value => \$Slic3r::skirt_distance, - type => 'i', - }, - ], + options => [qw(skirts skirt_distance)], ), - transform => Slic3r::GUI::OptionsGroup->new($self, title => 'Transform', - options => [ - { - label => 'Scale', - value => \$Slic3r::scale, - type => 'f', - }, - { - label => 'Rotate (°)', - value => \$Slic3r::rotate, - type => 'i', - }, - { - label => 'Multiply along X', - value => \$Slic3r::multiply_x, - type => 'i', - }, - { - label => 'Multiply along Y', - value => \$Slic3r::multiply_y, - type => 'i', - }, - { - label => 'Multiply distance', - value => \$Slic3r::multiply_distance, - type => 'i', - }, - ], + options => [qw(scale rotate multiply_x multiply_y multiply_distance)], ), ); + $self->{panels} = \%panels; $panels{slice} = Wx::BoxSizer->new(wxVERTICAL); my $slice_button = Wx::Button->new($self, -1, "Slice..."); @@ -207,13 +59,35 @@ sub new { [qw(printer filament speed transform)], [qw(accuracy print retract skirt slice)], ); - my $sizer = Wx::BoxSizer->new(wxHORIZONTAL); + my $config_buttons_sizer; + { + $config_buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); + + my $save_button = Wx::Button->new($self, -1, "Save configuration..."); + $config_buttons_sizer->Add($save_button, 0); + EVT_BUTTON($self, $save_button, \&save_config); + + my $load_button = Wx::Button->new($self, -1, "Load configuration..."); + $config_buttons_sizer->Add($load_button, 0); + EVT_BUTTON($self, $load_button, \&load_config); + + my $text = Wx::StaticText->new($self, -1, "Remember to check for updates at http://slic3r.org/", Wx::wxDefaultPosition, Wx::wxDefaultSize, wxALIGN_RIGHT); + my $font = Wx::Font->new(10, wxDEFAULT, wxNORMAL, wxNORMAL); + $text->SetFont($font); + $config_buttons_sizer->Add($text, 1, wxEXPAND | wxALIGN_RIGHT); + } + + my $skein_options_sizer = Wx::BoxSizer->new(wxHORIZONTAL); foreach my $col (@cols) { my $vertical_sizer = Wx::BoxSizer->new(wxVERTICAL); $vertical_sizer->Add($panels{$_}, 0, wxEXPAND | wxALL, 10) for @$col; - $sizer->Add($vertical_sizer); + $skein_options_sizer->Add($vertical_sizer); } + my $sizer = Wx::BoxSizer->new(wxVERTICAL); + $sizer->Add($config_buttons_sizer, 0, wxEXPAND | wxALL, 10); + $sizer->Add($skein_options_sizer); + $sizer->SetSizeHints($self); $self->SetSizer($sizer); $self->Layout; @@ -249,9 +123,40 @@ sub do_slice { Wx::MessageDialog->new($self, "$input_file_basename was successfully sliced.", 'Done!', wxOK | wxICON_INFORMATION)->ShowModal; }; + $self->catch_error(sub { $process_dialog->Destroy if $process_dialog }); +} + +my $ini_wildcard = "INI files *.ini|*.ini;*.INI"; + +sub save_config { + my $self = shift; + my $dlg = Wx::FileDialog->new($self, 'Save configuration as:', "", "config.ini", + $ini_wildcard, wxFD_SAVE); + if ($dlg->ShowModal == wxID_OK) { + Slic3r::Config->save($dlg->GetPath); + } +} + +sub load_config { + my $self = shift; + + my $dlg = Wx::FileDialog->new($self, 'Select configuration to load:', "", "config.ini", + $ini_wildcard, wxFD_OPEN); + if ($dlg->ShowModal == wxID_OK) { + my ($file) = $dlg->GetPaths; + eval { + Slic3r::Config->load($file); + }; + $self->catch_error(); + $_->() for @Slic3r::GUI::OptionsGroup::reload_callbacks; + } +} + +sub catch_error { + my ($self, $cb) = @_; if (my $err = $@) { - $process_dialog->Destroy if $process_dialog; + $cb->() if $cb; Wx::MessageDialog->new($self, $err, 'Error', wxOK | wxICON_ERROR)->ShowModal; } } diff --git a/slic3r.pl b/slic3r.pl index 23b81067b..182ef1bcd 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -19,6 +19,9 @@ GetOptions( 'debug' => \$Slic3r::debug, 'o|output' => \$opt{output}, + 'save=s' => \$opt{save}, + 'load=s' => \$opt{load}, + # printer options 'nozzle-diameter=f' => \$Slic3r::nozzle_diameter, 'print-center=s' => \$Slic3r::print_center, @@ -63,25 +66,37 @@ GetOptions( 'multiply-distance=i' => \$Slic3r::multiply_distance, ); +# load configuration +if ($opt{load}) { + -e $opt{load} or die "Cannot find specified configuration file.\n"; + Slic3r::Config->load($opt{load}); +} + # validate configuration Slic3r::Config->validate; +# save configuration +Slic3r::Config->save($opt{save}) if $opt{save}; + # start GUI -if (!@ARGV && eval "require Slic3r::GUI; 1") { +if (!@ARGV && !$opt{save} && eval "require Slic3r::GUI; 1") { Slic3r::GUI->new->MainLoop; exit; } -my $action = 'skein'; +if ($ARGV[0]) { -if ($action eq 'skein') { - my $input_file = $ARGV[0] or usage(1); + # skein + my $input_file = $ARGV[0]; my $skein = Slic3r::Skein->new( input_file => $input_file, output_file => $opt{output}, ); $skein->go; + +} else { + usage(1) unless $opt{save}; } sub usage { @@ -94,6 +109,8 @@ written by Alessandro Ranellucci - http://slic3r.org/ Usage: slic3r.pl [ OPTIONS ] file.stl --help Output this usage screen and exit + --save Save configuration to the specified file + --load Load configuration from the specified file Printer options: --nozzle-diameter Diameter of nozzle in mm (default: $Slic3r::nozzle_diameter)