diff --git a/Build.PL b/Build.PL index d7d4d8e8ab..522cf2284f 100644 --- a/Build.PL +++ b/Build.PL @@ -7,12 +7,12 @@ my $build = Module::Build->new( dist_version => '0.1', license => 'perl', requires => { - 'Boost::Geometry::Utils' => '0.08', + 'Boost::Geometry::Utils' => '0.12', 'Encode::Locale' => '0', 'File::Basename' => '0', 'File::Spec' => '0', 'Getopt::Long' => '0', - 'Math::Clipper' => '1.21', + 'Math::Clipper' => '1.22', 'Math::ConvexHull::MonotoneChain' => '0.01', 'Math::Geometry::Voronoi' => '1.3', 'Math::PlanePath' => '53', diff --git a/MANIFEST b/MANIFEST index bc338a5cea..86390ed372 100644 --- a/MANIFEST +++ b/MANIFEST @@ -25,10 +25,12 @@ lib/Slic3r/Format/OBJ.pm lib/Slic3r/Format/STL.pm lib/Slic3r/GCode.pm lib/Slic3r/GCode/CoolingBuffer.pm +lib/Slic3r/GCode/Layer.pm lib/Slic3r/GCode/MotionPlanner.pm lib/Slic3r/GCode/Reader.pm lib/Slic3r/GCode/SpiralVase.pm lib/Slic3r/Geometry.pm +lib/Slic3r/Geometry/BoundingBox.pm lib/Slic3r/Geometry/Clipper.pm lib/Slic3r/GUI.pm lib/Slic3r/GUI/AboutDialog.pm @@ -37,6 +39,7 @@ lib/Slic3r/GUI/OptionsGroup.pm lib/Slic3r/GUI/Plater.pm lib/Slic3r/GUI/Plater/ObjectDialog.pm lib/Slic3r/GUI/Preferences.pm +lib/Slic3r/GUI/PreviewCanvas.pm lib/Slic3r/GUI/SkeinPanel.pm lib/Slic3r/GUI/SimpleTab.pm lib/Slic3r/GUI/Tab.pm @@ -64,6 +67,7 @@ t/clean_polylines.t t/clipper.t t/collinear.t t/combineinfill.t +t/cooling.t t/custom_gcode.t t/dynamic.t t/fill.t @@ -72,11 +76,14 @@ t/geometry.t t/layers.t t/loops.t t/polyclip.t +t/print.t t/retraction.t t/serialize.t t/shells.t t/slice.t +t/skirt_brim.t t/support.t +t/svg.t t/vibrationlimit.t utils/amf-to-stl.pl utils/file_info.pl @@ -88,6 +95,7 @@ utils/post-processing/decimate.pl utils/post-processing/flowrate.pl utils/split_stl.pl utils/stl-to-amf.pl +utils/view-mesh.pl utils/zsh/functions/_slic3r utils/zsh/README.markdown var/add.png diff --git a/README.markdown b/README.markdown index 01c43d22da..4fe41b8790 100644 --- a/README.markdown +++ b/README.markdown @@ -7,7 +7,7 @@ A: Yes. ## What's it? Slic3r is a G-code generator for 3D printers. It's compatible with RepRaps, -Makerbots, Ultimakers and many more machines. +makerwares, Ultimakers and many more machines. See the [project homepage](http://slic3r.org/) at slic3r.org and the [documentation](https://github.com/alexrj/Slic3r/wiki/Documentation) on the Slic3r wiki for more information. @@ -93,6 +93,7 @@ The author of the Silk icon set is Mark James. GUI options: --no-plater Disable the plater tab --gui-mode Overrides the configured mode (simple/expert) + --autosave Automatically export current configuration to the specified file Output options: --output-filename-format @@ -111,7 +112,7 @@ The author of the Silk icon set is Mark James. (default: 100,100) --z-offset Additional height in mm to add to vertical coordinates (+/-, default: 0) - --gcode-flavor The type of G-code to generate (reprap/teacup/makerbot/sailfish/mach3/no-extrusion, + --gcode-flavor The type of G-code to generate (reprap/teacup/makerware/sailfish/mach3/no-extrusion, default: reprap) --use-relative-e-distances Enable this to get relative E values --gcode-arcs Use G2/G3 commands for native arcs (experimental, not supported diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 1dfaf8859e..d4caf4b460 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -7,7 +7,7 @@ use strict; use warnings; require v5.10; -our $VERSION = "0.9.10-dev"; +our $VERSION = "0.9.11-dev"; our $debug = 0; sub debugf { @@ -29,7 +29,7 @@ our $var = "$FindBin::Bin/var"; use Encode; use Encode::Locale; -use Boost::Geometry::Utils 0.08; +use Boost::Geometry::Utils 0.12; use Moo 0.091009; use Slic3r::Config; @@ -46,10 +46,13 @@ use Slic3r::Format::OBJ; use Slic3r::Format::STL; use Slic3r::GCode; use Slic3r::GCode::CoolingBuffer; +use Slic3r::GCode::Layer; use Slic3r::GCode::MotionPlanner; use Slic3r::GCode::Reader; use Slic3r::GCode::SpiralVase; use Slic3r::Geometry qw(PI); +use Slic3r::Geometry::BoundingBox; +use Slic3r::Geometry::Clipper; use Slic3r::Layer; use Slic3r::Layer::Region; use Slic3r::Line; @@ -68,7 +71,6 @@ use constant SCALING_FACTOR => 0.000001; use constant RESOLUTION => 0.0125; use constant SCALED_RESOLUTION => RESOLUTION / SCALING_FACTOR; use constant OVERLAP_FACTOR => 1; -use constant BRIDGE_OVERLAP_FACTOR => 0.2; use constant SMALL_PERIMETER_LENGTH => (6.5 / SCALING_FACTOR) * 2 * PI; use constant LOOP_CLIPPING_LENGTH_OVER_SPACING => 0.15; use constant INFILL_OVERLAP_OVER_SPACING => 0.45; @@ -84,6 +86,7 @@ sub parallelize { $q->enqueue(@items, (map undef, 1..$Config->threads)); my $thread_cb = sub { $params{thread_cb}->($q) }; + @_ = (); foreach my $th (map threads->create($thread_cb), 1..$Config->threads) { $params{collect_cb}->($th->join); } diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index 3fc8c28355..db9e35b9e0 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -9,6 +9,7 @@ use List::Util qw(first); our @Ignore = qw(duplicate_x duplicate_y multiply_x multiply_y support_material_tool acceleration); my $serialize_comma = sub { join ',', @{$_[0]} }; +my $serialize_comma_bool = sub { join ',', map $_ // 0, @{$_[0]} }; my $deserialize_comma = sub { [ split /,/, $_[0] ] }; our $Options = { @@ -73,8 +74,8 @@ our $Options = { tooltip => 'Some G/M-code commands, including temperature control and others, are not universal. Set this option to your printer\'s firmware to get a compatible output. The "No extrusion" flavor prevents Slic3r from exporting any extrusion value at all.', cli => 'gcode-flavor=s', type => 'select', - values => [qw(reprap teacup makerbot sailfish mach3 no-extrusion)], - labels => ['RepRap (Marlin/Sprinter)', 'Teacup', 'MakerBot', 'Sailfish', 'Mach3/EMC', 'No extrusion'], + values => [qw(reprap teacup makerware sailfish mach3 no-extrusion)], + labels => ['RepRap (Marlin/Sprinter/Repetier)', 'Teacup', 'MakerWare (MakerBot)', 'Sailfish (MakerBot)', 'Mach3/EMC', 'No extrusion'], default => 'reprap', }, 'use_relative_e_distances' => { @@ -595,7 +596,7 @@ our $Options = { }, 'only_retract_when_crossing_perimeters' => { label => 'Only retract when crossing perimeters', - tooltip => 'Disables retraction when travelling between infill paths inside the same island.', + tooltip => 'Disables retraction when the travel path does not exceed the upper layer\'s perimeters (and thus any ooze will be probably invisible).', cli => 'only-retract-when-crossing-perimeters!', type => 'bool', default => 1, @@ -797,7 +798,7 @@ END tooltip => 'This flag enforces a retraction whenever a Z move is done.', cli => 'retract-layer-change!', type => 'bool', - serialize => $serialize_comma, + serialize => $serialize_comma_bool, deserialize => $deserialize_comma, default => [1], }, @@ -806,7 +807,7 @@ END tooltip => 'This flag will move the nozzle while retracting to minimize the possible blob on leaky extruders.', cli => 'wipe!', type => 'bool', - serialize => $serialize_comma, + serialize => $serialize_comma_bool, deserialize => $deserialize_comma, default => [0], }, @@ -1140,7 +1141,7 @@ sub set { my ($opt_key, $value, $deserialize) = @_; # handle legacy options - return if $opt_key ~~ @Ignore; + return if first { $_ eq $opt_key } @Ignore; if ($opt_key =~ /^(extrusion_width|bottom_layer_speed|first_layer_height)_ratio$/) { $opt_key = $1; $opt_key =~ s/^bottom_layer_speed$/first_layer_speed/; @@ -1149,6 +1150,9 @@ sub set { if ($opt_key eq 'threads' && !$Slic3r::have_threads) { $value = 1; } + if ($opt_key eq 'gcode_flavor' && $value eq 'makerbot') { + $value = 'makerware'; + } # For historical reasons, the world's full of configs having these very low values; # to avoid unexpected behavior we need to ignore them. Banning these two hard-coded @@ -1270,6 +1274,10 @@ sub validate { die "Invalid value for --top-solid-layers\n" if $self->top_solid_layers < 0; die "Invalid value for --bottom-solid-layers\n" if $self->bottom_solid_layers < 0; + # --gcode-flavor + die "Invalid value for --gcode-flavor\n" + if !first { $_ eq $self->gcode_flavor } @{$Options->{gcode_flavor}{values}}; + # --print-center die "Invalid value for --print-center\n" if !ref $self->print_center @@ -1328,6 +1336,10 @@ sub validate { if $self->extruder_clearance_radius <= 0; die "Invalid value for --extruder-clearance-height\n" if $self->extruder_clearance_height <= 0; + + # --extrusion-multiplier + die "Invalid value for --extrusion-multiplier\n" + if defined first { $_ <= 0 } @{$self->extrusion_multiplier}; } sub replace_options { @@ -1408,7 +1420,7 @@ sub read_ini { my $ini = { _ => {} }; my $category = '_'; - while (my $_ = <$fh>) { + while (<$fh>) { s/\R+$//; next if /^\s+/; next if /^$/; diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm index 45b59acec8..5190357f36 100644 --- a/lib/Slic3r/ExPolygon.pm +++ b/lib/Slic3r/ExPolygon.pm @@ -335,11 +335,25 @@ sub align_to_origin { my @bb = Slic3r::Geometry::bounding_box([ map @$_, map @$_, @{$self->expolygons} ]); $_->translate(-$bb[X1], -$bb[Y1]) for @{$self->expolygons}; + $self; +} + +sub scale { + my $self = shift; + $_->scale(@_) for @{$self->expolygons}; + $self; } sub rotate { my $self = shift; $_->rotate(@_) for @{$self->expolygons}; + $self; +} + +sub translate { + my $self = shift; + $_->translate(@_) for @{$self->expolygons}; + $self; } sub size { diff --git a/lib/Slic3r/Extruder.pm b/lib/Slic3r/Extruder.pm index 526ac5048a..91c6314cb2 100644 --- a/lib/Slic3r/Extruder.pm +++ b/lib/Slic3r/Extruder.pm @@ -14,6 +14,7 @@ has 'id' => (is => 'rw', required => 1); has $_ => (is => 'ro', required => 1) for @{&OPTIONS}; has 'bridge_flow' => (is => 'lazy'); +has 'e' => (is => 'rw', default => sub {0} ); has 'retracted' => (is => 'rw', default => sub {0} ); has 'restart_extra' => (is => 'rw', default => sub {0} ); has 'e_per_mm3' => (is => 'lazy'); @@ -38,7 +39,9 @@ sub _build_retract_speed_mm_min { sub _build_scaled_wipe_distance { my $self = shift; - return scale $self->retract_length / $self->retract_speed * $Slic3r::Config->travel_speed; + # reduce feedrate a bit; travel speed is often too high to move on existing material + # too fast = ripping of existing material; too slow = short wipe path, thus more blob + return scale($self->retract_length / $self->retract_speed * $Slic3r::Config->travel_speed * 0.8); } sub make_flow { diff --git a/lib/Slic3r/Fill.pm b/lib/Slic3r/Fill.pm index 68b677c9fc..ecd0c024d2 100644 --- a/lib/Slic3r/Fill.pm +++ b/lib/Slic3r/Fill.pm @@ -17,7 +17,7 @@ use Slic3r::Geometry::Clipper qw(union_ex diff diff_ex intersection_ex offset); use Slic3r::Surface ':types'; -has 'print' => (is => 'ro', required => 1, weak_ref => 1); +has 'object' => (is => 'ro', required => 1, weak_ref => 1); has 'fillers' => (is => 'rw', default => sub { {} }); our %FillTypes = ( @@ -40,7 +40,7 @@ sub filler { } $self->fillers->{$filler} ||= $FillTypes{$filler}->new( - bounding_box => [ $self->print->bounding_box ], + bounding_box => [ $self->object->bounding_box ], ); return $self->fillers->{$filler}; } diff --git a/lib/Slic3r/Fill/Base.pm b/lib/Slic3r/Fill/Base.pm index adc1e74c6f..7633082325 100644 --- a/lib/Slic3r/Fill/Base.pm +++ b/lib/Slic3r/Fill/Base.pm @@ -36,6 +36,7 @@ sub infill_direction { return [\@rotate, \@shift]; } +# this method accepts any object that implements rotate() and translate() sub rotate_points { my $self = shift; my ($expolygon, $rotate_vector) = @_; diff --git a/lib/Slic3r/Fill/Honeycomb.pm b/lib/Slic3r/Fill/Honeycomb.pm index 1985ddb993..333273203f 100644 --- a/lib/Slic3r/Fill/Honeycomb.pm +++ b/lib/Slic3r/Fill/Honeycomb.pm @@ -38,10 +38,12 @@ sub fill_surface { # adjust actual bounding box to the nearest multiple of our hex pattern # and align it so that it matches across layers + my $bounding_box = [ @{$self->bounding_box} ]; # clone $bounding_box->[$_] = 0 for X1, Y1; { my $bb_polygon = Slic3r::Polygon->new_from_bounding_box($bounding_box); + $bb_polygon->scale(sqrt 2); $bb_polygon->rotate($rotate_vector->[0][0], $hex_center); $bounding_box = [ Slic3r::Geometry::bounding_box($bb_polygon) ]; # $bounding_box->[X1] and [Y1] represent the displacement between new bounding box offset and old one @@ -77,13 +79,14 @@ sub fill_surface { $self->cache->{$cache_id} = [@polygons]; } - # build polylines from polygons without re-appending the initial point: + # consider polygons as polylines without re-appending the initial point: # this cuts the last segment on purpose, so that the jump to the next # path is more straight - my @paths = map Slic3r::Polyline->new(@$_), map @$_, @{intersection_ex( - $self->cache->{$cache_id}, - $expolygon, - )}; + my @paths = map Slic3r::Polyline->new($_), + @{ Boost::Geometry::Utils::polygon_multi_linestring_intersection( + $expolygon, + $self->cache->{$cache_id}, + ) }; return { flow_spacing => $params{flow_spacing} }, Slic3r::Polyline::Collection->new(polylines => \@paths)->chained_path; diff --git a/lib/Slic3r/Fill/Rectilinear.pm b/lib/Slic3r/Fill/Rectilinear.pm index a65bcbc077..4ad3ff78fa 100644 --- a/lib/Slic3r/Fill/Rectilinear.pm +++ b/lib/Slic3r/Fill/Rectilinear.pm @@ -25,17 +25,17 @@ sub fill_surface { my $line_oscillation = $distance_between_lines - $min_spacing; my $is_line_pattern = $self->isa('Slic3r::Fill::Line'); - my $cache_id = sprintf "d%s_s%s_a%s", + my $cache_id = sprintf "d%s_s%.2f_a%.2f", $params{density}, $params{flow_spacing}, $rotate_vector->[0][0]; if (!$self->cache->{$cache_id}) { # compute bounding box - my $bounding_box = [ @{$self->bounding_box} ]; # clone - $bounding_box->[$_] = 0 for X1, Y1; + my $bounding_box; { - my $bb_expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_from_bounding_box($bounding_box)); - $self->rotate_points($bb_expolygon, $rotate_vector); - $bounding_box = [ $bb_expolygon->bounding_box ]; + my $bb_polygon = Slic3r::Polygon->new_from_bounding_box($self->bounding_box); + $bb_polygon->scale(sqrt 2); + $self->rotate_points($bb_polygon, $rotate_vector); + $bounding_box = [ $bb_polygon->bounding_box ]; } # define flow spacing according to requested density diff --git a/lib/Slic3r/Flow.pm b/lib/Slic3r/Flow.pm index d34d04a91a..39ad23273d 100644 --- a/lib/Slic3r/Flow.pm +++ b/lib/Slic3r/Flow.pm @@ -98,8 +98,7 @@ sub _build_width { sub _build_spacing { my $self = shift; - my $width = $self->width; - return $width - (&Slic3r::BRIDGE_OVERLAP_FACTOR * $width); + return $self->width + 0.05; } 1; diff --git a/lib/Slic3r/Format/OBJ.pm b/lib/Slic3r/Format/OBJ.pm index c5cc085558..05a141db1d 100644 --- a/lib/Slic3r/Format/OBJ.pm +++ b/lib/Slic3r/Format/OBJ.pm @@ -8,7 +8,7 @@ sub read_file { Slic3r::open(\my $fh, '<', $file) or die "Failed to open $file\n"; my $vertices = []; my $facets = []; - while (my $_ = <$fh>) { + while (<$fh>) { if (/^v ([^ ]+)\s+([^ ]+)\s+([^ ]+)/) { push @$vertices, [$1, $2, $3]; } elsif (/^f (\d+).*? (\d+).*? (\d+).*?/) { diff --git a/lib/Slic3r/Format/STL.pm b/lib/Slic3r/Format/STL.pm index 0d0331c856..cf07b1cad2 100644 --- a/lib/Slic3r/Format/STL.pm +++ b/lib/Slic3r/Format/STL.pm @@ -53,7 +53,7 @@ sub _read_ascii { my $facet; my %vertices_map = (); seek $fh, 0, 0; - while (my $_ = <$fh>) { + while (<$fh>) { if (!$facet) { /^\s*facet\s+normal\s+/ or next; $facet = []; # ignore normal @@ -88,7 +88,7 @@ sub _read_binary { my %vertices_map = (); binmode $fh; seek $fh, 80 + 4, 0; - while (read $fh, my $_, 4*4*3+2) { + while (read $fh, $_, 4*4*3+2) { push @$facets, my $facet = []; for (unpack 'x[f3](a[f3])3') { # ignore normal my $vertex_idx; diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index 48d32e789a..e77b5ba1b6 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -6,6 +6,7 @@ use Slic3r::ExtrusionPath ':roles'; use Slic3r::Geometry qw(scale unscale scaled_epsilon points_coincide PI X Y B); use Slic3r::Geometry::Clipper qw(union_ex); +has 'config' => (is => 'ro', required => 1); has 'multiple_extruders' => (is => 'ro', default => sub {0} ); has 'layer_count' => (is => 'ro', required => 1 ); has 'layer' => (is => 'rw'); @@ -15,12 +16,12 @@ has 'shift_y' => (is => 'rw', default => sub {0} ); has 'z' => (is => 'rw'); has 'speed' => (is => 'rw'); +has 'speeds' => (is => 'lazy'); # mm/min has 'external_mp' => (is => 'rw'); has 'layer_mp' => (is => 'rw'); has 'new_object' => (is => 'rw', default => sub {0}); has 'straight_once' => (is => 'rw', default => sub {1}); has 'extruder' => (is => 'rw'); -has 'extrusion_distance' => (is => 'rw', default => sub {0} ); has 'elapsed_time' => (is => 'rw', default => sub {0} ); # seconds has 'total_extrusion_length' => (is => 'rw', default => sub {0} ); has 'lifted' => (is => 'rw', default => sub {0} ); @@ -35,15 +36,14 @@ has 'dec' => (is => 'ro', default => sub { 3 } ); has 'last_dir' => (is => 'ro', default => sub { [0,0] }); has 'dir_time' => (is => 'ro', default => sub { [0,0] }); -# calculate speeds (mm/min) -has 'speeds' => ( - is => 'ro', - default => sub {+{ - map { $_ => 60 * $Slic3r::Config->get_value("${_}_speed") } +sub _build_speeds { + my $self = shift; + return { + map { $_ => 60 * $self->config->get_value("${_}_speed") } qw(travel perimeter small_perimeter external_perimeter infill solid_infill top_solid_infill support_material bridge gap_fill retract), - }}, -); + }; +} # assign speeds to roles my %role_speeds = ( @@ -82,17 +82,17 @@ sub change_layer { my ($layer) = @_; $self->layer($layer); - if ($Slic3r::Config->avoid_crossing_perimeters) { + if ($self->config->avoid_crossing_perimeters) { $self->layer_mp(Slic3r::GCode::MotionPlanner->new( islands => union_ex([ map @$_, @{$layer->slices} ], undef, 1), )); } my $gcode = ""; - if ($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/) { + if ($self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/) { $gcode .= sprintf "M73 P%s%s\n", int(99 * ($layer->id / ($self->layer_count - 1))), - ($Slic3r::Config->gcode_comments ? ' ; update progress' : ''); + ($self->config->gcode_comments ? ' ; update progress' : ''); } return $gcode; } @@ -103,7 +103,7 @@ sub move_z { my ($z, $comment) = @_; $z *= &Slic3r::SCALING_FACTOR; - $z += $Slic3r::Config->z_offset; + $z += $self->config->z_offset; my $gcode = ""; my $current_z = $self->z; @@ -137,9 +137,9 @@ sub extrude_loop { # find the point of the loop that is closest to the current extruder position # or randomize if requested my $last_pos = $self->last_pos; - if ($Slic3r::Config->randomize_start && $loop->role == EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER) { - $last_pos = Slic3r::Point->new(scale $Slic3r::Config->print_center->[X], scale $Slic3r::Config->bed_size->[Y]); - $last_pos->rotate(rand(2*PI), $Slic3r::Config->print_center); + if ($self->config->randomize_start && $loop->role == EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER) { + $last_pos = Slic3r::Point->new(scale $self->config->print_center->[X], scale $self->config->bed_size->[Y]); + $last_pos->rotate(rand(2*PI), $self->config->print_center); } my $start_index = $loop->nearest_point_index_to($last_pos); @@ -157,7 +157,7 @@ sub extrude_loop { $self->wipe_path($extrusion_path->polyline); # make a little move inwards before leaving loop - if ($loop->role == EXTR_ROLE_EXTERNAL_PERIMETER && $Slic3r::Config->perimeters > 1) { + if ($loop->role == EXTR_ROLE_EXTERNAL_PERIMETER && $self->config->perimeters > 1) { # detect angle between last and first segment # the side depends on the original winding order of the polygon (left for contours, right for holes) my @points = $was_clockwise ? (-2, 1) : (1, -2); @@ -188,7 +188,7 @@ sub extrude_path { $path->simplify(&Slic3r::SCALED_RESOLUTION); # detect arcs - if ($Slic3r::Config->gcode_arcs && !$recursive) { + if ($self->config->gcode_arcs && !$recursive) { my $gcode = ""; foreach my $arc_path ($path->detect_arcs) { $gcode .= $self->extrude_path($arc_path, $description, 1); @@ -205,12 +205,12 @@ sub extrude_path { # adjust acceleration my $acceleration; - if ($Slic3r::Config->perimeter_acceleration && $path->is_perimeter) { - $acceleration = $Slic3r::Config->perimeter_acceleration; - } elsif ($Slic3r::Config->infill_acceleration && $path->is_fill) { - $acceleration = $Slic3r::Config->infill_acceleration; - } elsif ($Slic3r::Config->infill_acceleration && $path->is_bridge) { - $acceleration = $Slic3r::Config->bridge_acceleration; + if ($self->config->perimeter_acceleration && $path->is_perimeter) { + $acceleration = $self->config->perimeter_acceleration; + } elsif ($self->config->infill_acceleration && $path->is_fill) { + $acceleration = $self->config->infill_acceleration; + } elsif ($self->config->infill_acceleration && $path->is_bridge) { + $acceleration = $self->config->bridge_acceleration; } $gcode .= $self->set_acceleration($acceleration) if $acceleration; @@ -252,19 +252,19 @@ sub extrude_path { if $self->extruder->wipe; } - if ($Slic3r::Config->cooling) { + if ($self->config->cooling) { my $path_time = $path_length / $self->speeds->{$self->last_speed} * 60; if ($self->layer->id == 0) { - $path_time = $Slic3r::Config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/ + $path_time = $self->config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/ ? $path_time / ($1/100) - : $path_length / $Slic3r::Config->first_layer_speed * 60; + : $path_length / $self->config->first_layer_speed * 60; } $self->elapsed_time($self->elapsed_time + $path_time); } # reset acceleration - $gcode .= $self->set_acceleration($Slic3r::Config->default_acceleration) - if $acceleration && $Slic3r::Config->default_acceleration; + $gcode .= $self->set_acceleration($self->config->default_acceleration) + if $acceleration && $self->config->default_acceleration; return $gcode; } @@ -283,46 +283,18 @@ sub travel_to { $travel->translate(-$self->shift_x, -$self->shift_y); if ($travel->length < scale $self->extruder->retract_before_travel - || ($Slic3r::Config->only_retract_when_crossing_perimeters && first { $_->encloses_line($travel, scaled_epsilon) } @{$self->layer->slices}) + || ($self->config->only_retract_when_crossing_perimeters && first { $_->encloses_line($travel, scaled_epsilon) } @{$self->layer->upper_layer_slices}) || ($role == EXTR_ROLE_SUPPORTMATERIAL && $self->layer->support_islands_enclose_line($travel)) ) { $self->straight_once(0); $self->speed('travel'); $gcode .= $self->G0($point, undef, 0, $comment || ""); - } elsif (!$Slic3r::Config->avoid_crossing_perimeters || $self->straight_once) { + } elsif (!$self->config->avoid_crossing_perimeters || $self->straight_once) { $self->straight_once(0); $gcode .= $self->retract(travel_to => $point); $self->speed('travel'); $gcode .= $self->G0($point, undef, 0, $comment || ""); } else { - my $plan = sub { - my $mp = shift; - - my $gcode = ""; - my @travel = $mp->shortest_path($self->last_pos, $point)->lines; - - # if the path is not contained in a single island we need to retract - my $need_retract = !$Slic3r::Config->only_retract_when_crossing_perimeters; - if (!$need_retract) { - $need_retract = 1; - foreach my $slice (@{$self->layer->slices}) { - # discard the island if at any line is not enclosed in it - next if first { !$slice->encloses_line($_, scaled_epsilon) } @travel; - # okay, this island encloses the full travel path - $need_retract = 0; - last; - } - } - - # do the retract (the travel_to argument is broken) - $gcode .= $self->retract(travel_to => $point) if $need_retract; - - # append the actual path and return - $self->speed('travel'); - $gcode .= join '', map $self->G0($_->[B], undef, 0, $comment || ""), @travel; - return $gcode; - }; - if ($self->new_object) { $self->new_object(0); @@ -333,16 +305,46 @@ sub travel_to { # calculate path (external_mp uses G-code coordinates so we temporary need a null shift) $self->set_shift(0,0); - $gcode .= $plan->($self->external_mp); + $gcode .= $self->_plan($self->external_mp, $point, $comment); $self->set_shift(@shift); } else { - $gcode .= $plan->($self->layer_mp); + $gcode .= $self->_plan($self->layer_mp, $point, $comment); } } return $gcode; } +sub _plan { + my $self = shift; + my ($mp, $point, $comment) = @_; + + my $gcode = ""; + my @travel = $mp->shortest_path($self->last_pos, $point)->lines; + + # if the path is not contained in a single island we need to retract + my $need_retract = !$self->config->only_retract_when_crossing_perimeters; + if (!$need_retract) { + $need_retract = 1; + foreach my $slice (@{$self->layer->upper_layer_slices}) { + # discard the island if at any line is not enclosed in it + next if first { !$slice->encloses_line($_, scaled_epsilon) } @travel; + # okay, this island encloses the full travel path + $need_retract = 0; + last; + } + } + + # do the retract (the travel_to argument is broken) + $gcode .= $self->retract(travel_to => $point) if $need_retract; + + # append the actual path and return + $self->speed('travel'); + # use G1 because we rely on paths being straight (G0 may make round paths) + $gcode .= join '', map $self->G1($_->[B], undef, 0, $comment || ""), @travel; + return $gcode; +} + sub retract { my $self = shift; my %params = @_; @@ -370,7 +372,7 @@ sub retract { ? undef : [undef, $self->z + $self->extruder->retract_lift, 0, 'lift plate during travel']; - if (($Slic3r::Config->g0 || $Slic3r::Config->gcode_flavor eq 'mach3') && $params{travel_to}) { + if (($self->config->g0 || $self->config->gcode_flavor eq 'mach3') && $params{travel_to}) { $self->speed('travel'); if ($lift) { # combine lift and retract @@ -381,7 +383,7 @@ sub retract { my $travel = [$params{travel_to}, undef, $retract->[2], "travel and $comment"]; $gcode .= $self->G0(@$travel); } - } elsif (($Slic3r::Config->g0 || $Slic3r::Config->gcode_flavor eq 'mach3') && defined $params{move_z}) { + } elsif (($self->config->g0 || $self->config->gcode_flavor eq 'mach3') && defined $params{move_z}) { # combine Z change and retraction $self->speed('travel'); my $travel = [undef, $params{move_z}, $retract->[2], "change layer and $comment"]; @@ -417,7 +419,9 @@ sub retract { # reset extrusion distance during retracts # this makes sure we leave sufficient precision in the firmware - $gcode .= $self->reset_e if $Slic3r::Config->gcode_flavor !~ /^(?:mach3|makerbot)$/; + $gcode .= $self->reset_e; + + $gcode .= "M103 ; extruder off\n" if $self->config->gcode_flavor eq 'makerware'; return $gcode; } @@ -426,6 +430,7 @@ sub unretract { my $self = shift; my $gcode = ""; + $gcode .= "M101 ; extruder on\n" if $self->config->gcode_flavor eq 'makerware'; if ($self->lifted) { $self->speed('travel'); @@ -436,7 +441,8 @@ sub unretract { my $to_unretract = $self->extruder->retracted + $self->extruder->restart_extra; if ($to_unretract) { $self->speed('retract'); - $gcode .= $self->G0(undef, undef, $to_unretract, "compensate retraction"); + # use G1 instead of G0 because G0 will blend the restart with the previous travel move + $gcode .= $self->G1(undef, undef, $to_unretract, "compensate retraction"); $self->extruder->retracted(0); $self->extruder->restart_extra(0); } @@ -446,10 +452,11 @@ sub unretract { sub reset_e { my $self = shift; + return "" if $self->config->gcode_flavor =~ /^(?:mach3|makerware)$/; - $self->extrusion_distance(0); - return sprintf "G92 %s0%s\n", $Slic3r::Config->extrusion_axis, ($Slic3r::Config->gcode_comments ? ' ; reset extrusion distance' : '') - if $Slic3r::Config->extrusion_axis && !$Slic3r::Config->use_relative_e_distances; + $self->extruder->e(0) if $self->extruder; + return sprintf "G92 %s0%s\n", $self->config->extrusion_axis, ($self->config->gcode_comments ? ' ; reset extrusion distance' : '') + if $self->config->extrusion_axis && !$self->config->use_relative_e_distances; } sub set_acceleration { @@ -458,12 +465,12 @@ sub set_acceleration { return "" if !$acceleration; return sprintf "M204 S%s%s\n", - $acceleration, ($Slic3r::Config->gcode_comments ? ' ; adjust acceleration' : ''); + $acceleration, ($self->config->gcode_comments ? ' ; adjust acceleration' : ''); } sub G0 { my $self = shift; - return $self->G1(@_) if !($Slic3r::Config->g0 || $Slic3r::Config->gcode_flavor eq 'mach3'); + return $self->G1(@_) if !($self->config->g0 || $self->config->gcode_flavor eq 'mach3'); return $self->_G0_G1("G0", @_); } @@ -533,9 +540,9 @@ sub _Gx { ? ($self->extruder->retract_speed_mm_min) : $self->speeds->{$self->speed} // $self->speed; if ($e && $self->layer && $self->layer->id == 0 && $comment !~ /retract/) { - $F = $Slic3r::Config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/ + $F = $self->config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/ ? ($F * $1/100) - : $Slic3r::Config->first_layer_speed * 60; + : $self->config->first_layer_speed * 60; } $self->last_speed($self->speed); $self->last_f($F); @@ -543,14 +550,14 @@ sub _Gx { $gcode .= sprintf " F%.${dec}f", $F if defined $F; # output extrusion distance - if ($e && $Slic3r::Config->extrusion_axis) { - $self->extrusion_distance(0) if $Slic3r::Config->use_relative_e_distances; - $self->extrusion_distance($self->extrusion_distance + $e); + if ($e && $self->config->extrusion_axis) { + $self->extruder->e(0) if $self->config->use_relative_e_distances; + $self->extruder->e($self->extruder->e + $e); $self->total_extrusion_length($self->total_extrusion_length + $e); - $gcode .= sprintf " %s%.5f", $Slic3r::Config->extrusion_axis, $self->extrusion_distance; + $gcode .= sprintf " %s%.5f", $self->config->extrusion_axis, $self->extruder->e; } - $gcode .= sprintf " ; %s", $comment if $comment && $Slic3r::Config->gcode_comments; + $gcode .= sprintf " ; %s", $comment if $comment && $self->config->gcode_comments; if ($append_bridge_off) { $gcode .= "\n;_BRIDGE_FAN_END"; } @@ -575,8 +582,8 @@ sub set_extruder { $gcode .= $self->retract(toolchange => 1) if defined $self->extruder; # append custom toolchange G-code - if (defined $self->extruder && $Slic3r::Config->toolchange_gcode) { - $gcode .= sprintf "%s\n", $Slic3r::Config->replace_options($Slic3r::Config->toolchange_gcode, { + if (defined $self->extruder && $self->config->toolchange_gcode) { + $gcode .= sprintf "%s\n", $self->config->replace_options($self->config->toolchange_gcode, { previous_extruder => $self->extruder->id, next_extruder => $extruder->id, }); @@ -584,18 +591,16 @@ sub set_extruder { # set the new extruder $self->extruder($extruder); - my $toolchange_gcode = sprintf "%s%d%s\n", - ($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/ ? 'M108 T' : 'T'), + $gcode .= sprintf "%s%d%s\n", + ($self->config->gcode_flavor eq 'makerware' + ? 'M135 T' + : $self->config->gcode_flavor eq 'sailfish' + ? 'M108 T' + : 'T'), $extruder->id, - ($Slic3r::Config->gcode_comments ? ' ; change extruder' : ''); + ($self->config->gcode_comments ? ' ; change extruder' : ''); - if ($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/) { - $gcode .= $self->reset_e; - $gcode .= $toolchange_gcode; - } else { - $gcode .= $toolchange_gcode; - $gcode .= $self->reset_e; - } + $gcode .= $self->reset_e; return $gcode; } @@ -607,18 +612,18 @@ sub set_fan { if ($self->last_fan_speed != $speed || $dont_save) { $self->last_fan_speed($speed) if !$dont_save; if ($speed == 0) { - my $code = $Slic3r::Config->gcode_flavor eq 'teacup' + my $code = $self->config->gcode_flavor eq 'teacup' ? 'M106 S0' - : $Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/ + : $self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/ ? 'M127' : 'M107'; - return sprintf "$code%s\n", ($Slic3r::Config->gcode_comments ? ' ; disable fan' : ''); + return sprintf "$code%s\n", ($self->config->gcode_comments ? ' ; disable fan' : ''); } else { - if ($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/) { - return sprintf "M126%s\n", ($Slic3r::Config->gcode_comments ? ' ; enable fan' : ''); + if ($self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/) { + return sprintf "M126%s\n", ($self->config->gcode_comments ? ' ; enable fan' : ''); } else { - return sprintf "M106 %s%d%s\n", ($Slic3r::Config->gcode_flavor eq 'mach3' ? 'P' : 'S'), - (255 * $speed / 100), ($Slic3r::Config->gcode_comments ? ' ; enable fan' : ''); + return sprintf "M106 %s%d%s\n", ($self->config->gcode_flavor eq 'mach3' ? 'P' : 'S'), + (255 * $speed / 100), ($self->config->gcode_comments ? ' ; enable fan' : ''); } } } @@ -629,17 +634,17 @@ sub set_temperature { my $self = shift; my ($temperature, $wait, $tool) = @_; - return "" if $wait && $Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/; + return "" if $wait && $self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/; - my ($code, $comment) = ($wait && $Slic3r::Config->gcode_flavor ne 'teacup') + my ($code, $comment) = ($wait && $self->config->gcode_flavor ne 'teacup') ? ('M109', 'wait for temperature to be reached') : ('M104', 'set temperature'); my $gcode = sprintf "$code %s%d %s; $comment\n", - ($Slic3r::Config->gcode_flavor eq 'mach3' ? 'P' : 'S'), $temperature, - (defined $tool && ($self->multiple_extruders || $Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/)) ? "T$tool " : ""; + ($self->config->gcode_flavor eq 'mach3' ? 'P' : 'S'), $temperature, + (defined $tool && ($self->multiple_extruders || $self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/)) ? "T$tool " : ""; $gcode .= "M116 ; wait for temperature to be reached\n" - if $Slic3r::Config->gcode_flavor eq 'teacup' && $wait; + if $self->config->gcode_flavor eq 'teacup' && $wait; return $gcode; } @@ -648,14 +653,14 @@ sub set_bed_temperature { my $self = shift; my ($temperature, $wait) = @_; - my ($code, $comment) = ($wait && $Slic3r::Config->gcode_flavor ne 'teacup') - ? (($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/ ? 'M109' : 'M190'), 'wait for bed temperature to be reached') + my ($code, $comment) = ($wait && $self->config->gcode_flavor ne 'teacup') + ? (($self->config->gcode_flavor =~ /^(?:makerware|sailfish)$/ ? 'M109' : 'M190'), 'wait for bed temperature to be reached') : ('M140', 'set bed temperature'); my $gcode = sprintf "$code %s%d ; $comment\n", - ($Slic3r::Config->gcode_flavor eq 'mach3' ? 'P' : 'S'), $temperature; + ($self->config->gcode_flavor eq 'mach3' ? 'P' : 'S'), $temperature; $gcode .= "M116 ; wait for bed temperature to be reached\n" - if $Slic3r::Config->gcode_flavor eq 'teacup' && $wait; + if $self->config->gcode_flavor eq 'teacup' && $wait; return $gcode; } @@ -665,8 +670,8 @@ sub _limit_frequency { my $self = shift; my ($point) = @_; - return '' if $Slic3r::Config->vibration_limit == 0; - my $min_time = 1 / ($Slic3r::Config->vibration_limit * 60); # in minutes + return '' if $self->config->vibration_limit == 0; + my $min_time = 1 / ($self->config->vibration_limit * 60); # in minutes # calculate the move vector and move direction my $vector = Slic3r::Line->new($self->last_pos, $point)->vector; diff --git a/lib/Slic3r/GCode/CoolingBuffer.pm b/lib/Slic3r/GCode/CoolingBuffer.pm index a04554f58c..a4f44fe260 100644 --- a/lib/Slic3r/GCode/CoolingBuffer.pm +++ b/lib/Slic3r/GCode/CoolingBuffer.pm @@ -6,7 +6,7 @@ has 'gcodegen' => (is => 'ro', required => 1); has 'gcode' => (is => 'rw', default => sub {""}); has 'elapsed_time' => (is => 'rw', default => sub {0}); has 'layer_id' => (is => 'rw'); -has 'last_z' => (is => 'rw'); +has 'last_z' => (is => 'rw', default => sub { {} }); # obj_id => z (basically a 'last seen' table) has 'min_print_speed' => (is => 'lazy'); sub _build_min_print_speed { @@ -16,15 +16,17 @@ sub _build_min_print_speed { sub append { my $self = shift; - my ($gcode, $layer) = @_; + my ($gcode, $obj_id, $layer_id, $print_z) = @_; + + # TODO: differentiate $obj_id between normal layers and support layers my $return = ""; - if (defined $self->last_z && $self->last_z != $layer->print_z) { + if (exists $self->last_z->{$obj_id} && $self->last_z->{$obj_id} != $print_z) { $return = $self->flush; } - $self->layer_id($layer->id); - $self->last_z($layer->print_z); + $self->layer_id($layer_id); + $self->last_z->{$obj_id} = $print_z; $self->gcode($self->gcode . $gcode); $self->elapsed_time($self->elapsed_time + $self->gcodegen->elapsed_time); $self->gcodegen->elapsed_time(0); @@ -39,6 +41,7 @@ sub flush { my $elapsed = $self->elapsed_time; $self->gcode(""); $self->elapsed_time(0); + $self->last_z({}); # reset the whole table otherwise we would compute overlapping times my $fan_speed = $self->config->fan_always_on ? $self->config->min_fan_speed : 0; my $speed_factor = 1; diff --git a/lib/Slic3r/GCode/Layer.pm b/lib/Slic3r/GCode/Layer.pm new file mode 100644 index 0000000000..5d398f463d --- /dev/null +++ b/lib/Slic3r/GCode/Layer.pm @@ -0,0 +1,207 @@ +package Slic3r::GCode::Layer; +use Moo; + +use List::Util qw(first); +use Slic3r::Geometry qw(X Y unscale); + +has 'print' => (is => 'ro', required => 1, handles => [qw(extruders)]); +has 'gcodegen' => (is => 'ro', required => 1); +has 'shift' => (is => 'ro', required => 1); + +has 'spiralvase' => (is => 'lazy'); +has 'skirt_done' => (is => 'rw', default => sub { {} }); # print_z => 1 +has 'brim_done' => (is => 'rw'); +has 'second_layer_things_done' => (is => 'rw'); +has '_last_obj_copy' => (is => 'rw'); + +sub _build_spiralvase { + my $self = shift; + + return $Slic3r::Config->spiral_vase + ? Slic3r::GCode::SpiralVase->new + : undef; +} + +sub process_layer { + my $self = shift; + my ($layer, $object_copies) = @_; + my $gcode = ""; + + if (!$self->second_layer_things_done && $layer->id == 1) { + for my $t (grep $self->extruders->[$_], 0 .. $#{$Slic3r::Config->temperature}) { + $gcode .= $self->gcodegen->set_temperature($self->extruders->[$t]->temperature, 0, $t) + if $self->print->extruders->[$t]->temperature && $self->extruders->[$t]->temperature != $self->extruders->[$t]->first_layer_temperature; + } + $gcode .= $self->gcodegen->set_bed_temperature($Slic3r::Config->bed_temperature) + if $Slic3r::Config->bed_temperature && $Slic3r::Config->bed_temperature != $Slic3r::Config->first_layer_bed_temperature; + $self->second_layer_things_done(1); + } + + # set new layer, but don't move Z as support material contact areas may need an intermediate one + $gcode .= $self->gcodegen->change_layer($layer); + + # prepare callback to call as soon as a Z command is generated + $self->gcodegen->move_z_callback(sub { + $self->gcodegen->move_z_callback(undef); # circular ref or not? + return "" if !$Slic3r::Config->layer_gcode; + return $Slic3r::Config->replace_options($Slic3r::Config->layer_gcode) . "\n"; + }); + + # extrude skirt + if ((values %{$self->skirt_done}) < $Slic3r::Config->skirt_height && !$self->skirt_done->{$layer->print_z}) { + $self->gcodegen->set_shift(@{$self->shift}); + $gcode .= $self->gcodegen->set_extruder($self->extruders->[0]); # move_z requires extruder + $gcode .= $self->gcodegen->move_z($layer->print_z); + # skip skirt if we have a large brim + if ($layer->id < $Slic3r::Config->skirt_height) { + # distribute skirt loops across all extruders + for my $i (0 .. $#{$self->print->skirt}) { + # when printing layers > 0 ignore 'min_skirt_length' and + # just use the 'skirts' setting; also just use the current extruder + last if ($layer->id > 0) && ($i >= $Slic3r::Config->skirts); + $gcode .= $self->gcodegen->set_extruder($self->extruders->[ ($i/@{$self->extruders}) % @{$self->extruders} ]) + if $layer->id == 0; + $gcode .= $self->gcodegen->extrude_loop($self->print->skirt->[$i], 'skirt'); + } + } + $self->skirt_done->{$layer->print_z} = 1; + $self->gcodegen->straight_once(1); + } + + # extrude brim + if (!$self->brim_done) { + $gcode .= $self->gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]); # move_z requires extruder + $gcode .= $self->gcodegen->move_z($layer->print_z); + $self->gcodegen->set_shift(@{$self->shift}); + $gcode .= $self->gcodegen->extrude_loop($_, 'brim') for @{$self->print->brim}; + $self->brim_done(1); + $self->gcodegen->straight_once(1); + } + + for my $copy (@$object_copies) { + $self->gcodegen->new_object(1) if ($self->_last_obj_copy // '') ne "$copy"; + $self->_last_obj_copy("$copy"); + + $self->gcodegen->set_shift(map $self->shift->[$_] + unscale $copy->[$_], X,Y); + + # extrude support material before other things because it might use a lower Z + # and also because we avoid travelling on other things when printing it + if ($self->print->has_support_material) { + $gcode .= $self->gcodegen->move_z($layer->support_material_contact_z) + if ($layer->support_contact_fills && @{ $layer->support_contact_fills->paths }); + $gcode .= $self->gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]); + if ($layer->support_contact_fills) { + $gcode .= $self->gcodegen->extrude_path($_, 'support material contact area') + for $layer->support_contact_fills->chained_path($self->gcodegen->last_pos); + } + + $gcode .= $self->gcodegen->move_z($layer->print_z); + if ($layer->support_fills) { + $gcode .= $self->gcodegen->extrude_path($_, 'support material') + for $layer->support_fills->chained_path($self->gcodegen->last_pos); + } + } + + # set actual Z - this will force a retraction + $gcode .= $self->gcodegen->move_z($layer->print_z); + + # tweak region ordering to save toolchanges + my @region_ids = 0 .. ($self->print->regions_count-1); + if ($self->gcodegen->multiple_extruders) { + my $last_extruder = $self->gcodegen->extruder; + my $best_region_id = first { $self->print->regions->[$_]->extruders->{perimeter} eq $last_extruder } @region_ids; + @region_ids = ($best_region_id, grep $_ != $best_region_id, @region_ids) if $best_region_id; + } + + foreach my $region_id (@region_ids) { + my $layerm = $layer->regions->[$region_id]; + my $region = $self->print->regions->[$region_id]; + + my @islands = (); + if ($Slic3r::Config->avoid_crossing_perimeters) { + push @islands, { perimeters => [], fills => [] } + for 1 .. (@{$layer->slices} || 1); # make sure we have at least one island hash to avoid failure of the -1 subscript below + PERIMETER: foreach my $perimeter (@{$layerm->perimeters}) { + my $p = $perimeter->unpack; + for my $i (0 .. $#{$layer->slices}-1) { + if ($layer->slices->[$i]->contour->encloses_point($p->first_point)) { + push @{ $islands[$i]{perimeters} }, $p; + next PERIMETER; + } + } + push @{ $islands[-1]{perimeters} }, $p; # optimization + } + FILL: foreach my $fill (@{$layerm->fills}) { + my $f = $fill->unpack; + for my $i (0 .. $#{$layer->slices}-1) { + if ($layer->slices->[$i]->contour->encloses_point($f->first_point)) { + push @{ $islands[$i]{fills} }, $f; + next FILL; + } + } + push @{ $islands[-1]{fills} }, $f; # optimization + } + } else { + push @islands, { + perimeters => $layerm->perimeters, + fills => $layerm->fills, + }; + } + + foreach my $island (@islands) { + # give priority to infill if we were already using its extruder and it wouldn't + # be good for perimeters + if ($Slic3r::Config->infill_first + || ($self->gcodegen->multiple_extruders && $region->extruders->{infill} eq $self->gcodegen->extruder) && $region->extruders->{infill} ne $region->extruders->{perimeter}) { + $gcode .= $self->_extrude_infill($island, $region); + $gcode .= $self->_extrude_perimeters($island, $region); + } else { + $gcode .= $self->_extrude_perimeters($island, $region); + $gcode .= $self->_extrude_infill($island, $region); + } + } + } + } + + # apply spiral vase post-processing if this layer contains suitable geometry + $gcode = $self->spiralvase->process_layer($gcode, $layer) + if defined $self->spiralvase + && ($layer->id > 0 || $Slic3r::Config->brim_width == 0) + && ($layer->id >= $Slic3r::Config->skirt_height) + && ($layer->id >= $Slic3r::Config->bottom_solid_layers); + + return $gcode; +} + +sub _extrude_perimeters { + my $self = shift; + my ($island, $region) = @_; + + return "" if !@{ $island->{perimeters} }; + + my $gcode = ""; + $gcode .= $self->gcodegen->set_extruder($region->extruders->{perimeter}); + $gcode .= $self->gcodegen->extrude($_, 'perimeter') for @{ $island->{perimeters} }; + return $gcode; +} + +sub _extrude_infill { + my $self = shift; + my ($island, $region) = @_; + + return "" if !@{ $island->{fills} }; + + my $gcode = ""; + $gcode .= $self->gcodegen->set_extruder($region->extruders->{infill}); + for my $fill (@{ $island->{fills} }) { + if ($fill->isa('Slic3r::ExtrusionPath::Collection')) { + $gcode .= $self->gcodegen->extrude($_, 'fill') + for $fill->chained_path($self->gcodegen->last_pos); + } else { + $gcode .= $self->gcodegen->extrude($fill, 'fill') ; + } + } + return $gcode; +} + +1; diff --git a/lib/Slic3r/GCode/MotionPlanner.pm b/lib/Slic3r/GCode/MotionPlanner.pm index 0cde4e1af6..026485357b 100644 --- a/lib/Slic3r/GCode/MotionPlanner.pm +++ b/lib/Slic3r/GCode/MotionPlanner.pm @@ -10,6 +10,7 @@ has '_contours_ex' => (is => 'rw', default => sub { [] }); # arrayref of array has '_pointmap' => (is => 'rw', default => sub { {} }); # { id => $point } has '_edges' => (is => 'rw', default => sub { {} }); # node_idx => { node_idx => distance, ... } has '_crossing_edges' => (is => 'rw', default => sub { {} }); # edge_idx => bool +has '_tolerance' => (is => 'lazy'); use List::Util qw(first); use Slic3r::Geometry qw(A B scale epsilon nearest_point); @@ -30,31 +31,14 @@ use constant CROSSING_FACTOR => 20; use constant INFINITY => 'inf'; +sub _build__tolerance { scale epsilon } + # setup our configuration space sub BUILD { my $self = shift; my $edges = $self->_edges; my $crossing_edges = $self->_crossing_edges; - my $tolerance = scale epsilon; - - # given an expolygon, this subroutine connects all its visible points - my $add_expolygon = sub { - my ($expolygon, $crosses_perimeter) = @_; - my @points = map @$_, @$expolygon; - for my $i (0 .. $#points) { - for my $j (($i+1) .. $#points) { - my $line = Slic3r::Line->new($points[$i], $points[$j]); - if ($expolygon->encloses_line($line, $tolerance)) { - my $dist = $line->length * ($crosses_perimeter ? CROSSING_FACTOR : 1); - $edges->{$points[$i]}{$points[$j]} = $dist; - $edges->{$points[$j]}{$points[$i]} = $dist; - $crossing_edges->{$points[$i]}{$points[$j]} = 1; - $crossing_edges->{$points[$j]}{$points[$i]} = 1; - } - } - } - }; # simplify islands @{$self->islands} = map $_->simplify($self->_inner_margin), @{$self->islands}; @@ -81,21 +65,17 @@ sub BUILD { ); # lines enclosed in inner expolygons are visible - $add_expolygon->($_) for @{ $self->_inner->[$i] }; + $self->_add_expolygon($_) for @{ $self->_inner->[$i] }; # lines enclosed in expolygons covering perimeters are visible # (but discouraged) - $add_expolygon->($_, 1) for @{ $self->_contours_ex->[$i] }; + $self->_add_expolygon($_, 1) for @{ $self->_contours_ex->[$i] }; } } - my $intersects = sub { - my ($polygon, $line) = @_; - @{Boost::Geometry::Utils::polygon_multi_linestring_intersection([$polygon], [$line])} > 0; - }; - { my @outer = (map @$_, @{$self->_outer}); + my @outer_ex = map [$_], @outer; # as ExPolygons # lines of outer polygons connect visible points for my $i (0 .. $#outer) { @@ -112,7 +92,7 @@ sub BUILD { for my $m (0 .. $#{$outer[$i]}) { for my $n (0 .. $#{$outer[$j]}) { my $line = Slic3r::Line->new($outer[$i][$m], $outer[$j][$n]); - if (!first { $intersects->($_, $line) } @outer) { + if (!@{Boost::Geometry::Utils::multi_polygon_multi_linestring_intersection(\@outer_ex, [$line])}) { # this line does not cross any polygon my $dist = $line->length; $edges->{$outer[$i][$m]}{$outer[$j][$n]} = $dist; @@ -127,12 +107,13 @@ sub BUILD { # lines connecting inner polygons contours are visible but discouraged if (!$self->no_internal) { my @inner = (map $_->contour, map @$_, @{$self->_inner}); + my @inner_ex = map [$_], @inner; # as ExPolygons for my $i (0 .. $#inner) { for my $j (($i+1) .. $#inner) { for my $m (0 .. $#{$inner[$i]}) { for my $n (0 .. $#{$inner[$j]}) { my $line = Slic3r::Line->new($inner[$i][$m], $inner[$j][$n]); - if (!first { $intersects->($_, $line) } @inner) { + if (!@{Boost::Geometry::Utils::multi_polygon_multi_linestring_intersection(\@inner_ex, [$line])}) { # this line does not cross any polygon my $dist = $line->length * CROSSING_FACTOR; $edges->{$inner[$i][$m]}{$inner[$j][$n]} = $dist; @@ -183,6 +164,29 @@ sub BUILD { } } +# given an expolygon, this subroutine connects all its visible points +sub _add_expolygon { + my $self = shift; + my ($expolygon, $crosses_perimeter) = @_; + + my $edges = $self->_edges; + my $crossing_edges = $self->_crossing_edges; + + my @points = map @$_, @$expolygon; + for my $i (0 .. $#points) { + for my $j (($i+1) .. $#points) { + my $line = Slic3r::Line->new($points[$i], $points[$j]); + if ($expolygon->encloses_line($line, $self->_tolerance)) { + my $dist = $line->length * ($crosses_perimeter ? CROSSING_FACTOR : 1); + $edges->{$points[$i]}{$points[$j]} = $dist; + $edges->{$points[$j]}{$points[$i]} = $dist; + $crossing_edges->{$points[$i]}{$points[$j]} = 1; + $crossing_edges->{$points[$j]}{$points[$i]} = 1; + } + } + } +} + sub find_node { my $self = shift; my ($point, $near_to) = @_; diff --git a/lib/Slic3r/GCode/Reader.pm b/lib/Slic3r/GCode/Reader.pm index 235a0ad741..20a1313b07 100644 --- a/lib/Slic3r/GCode/Reader.pm +++ b/lib/Slic3r/GCode/Reader.pm @@ -26,7 +26,7 @@ sub parse { my ($command, @args) = split /\s+/, $line; my %args = map { /([A-Z])(.*)/; ($1 => $2) } @args; - # check retraction + # check motion if ($command =~ /^G[01]$/) { foreach my $axis (@AXES) { if (exists $args{$axis}) { diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 1c60f85732..00e56f4faa 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -14,6 +14,8 @@ use Slic3r::GUI::SkeinPanel; use Slic3r::GUI::SimpleTab; use Slic3r::GUI::Tab; +our $have_OpenGL = 0 && eval "use Slic3r::GUI::PreviewCanvas; 1"; + use Wx 0.9901 qw(:bitmap :dialog :frame :icon :id :misc :systemsettings :toplevelwindow); use Wx::Event qw(EVT_CLOSE EVT_MENU); use base 'Wx::App'; @@ -43,6 +45,7 @@ use constant MI_DOCUMENTATION => &Wx::NewId; our $datadir; our $no_plater; our $mode; +our $autosave; our $Settings = { _ => { @@ -116,12 +119,12 @@ sub OnInit { $fileMenu->Append(wxID_EXIT, "&Quit", 'Quit Slic3r'); EVT_MENU($frame, MI_LOAD_CONF, sub { $self->{skeinpanel}->load_config_file }); EVT_MENU($frame, MI_EXPORT_CONF, sub { $self->{skeinpanel}->export_config }); - EVT_MENU($frame, MI_QUICK_SLICE, sub { $self->{skeinpanel}->do_slice; + EVT_MENU($frame, MI_QUICK_SLICE, sub { $self->{skeinpanel}->quick_slice; $repeat->Enable(defined $Slic3r::GUI::SkeinPanel::last_input_file) }); - EVT_MENU($frame, MI_REPEAT_QUICK, sub { $self->{skeinpanel}->do_slice(reslice => 1) }); - EVT_MENU($frame, MI_QUICK_SAVE_AS, sub { $self->{skeinpanel}->do_slice(save_as => 1); + EVT_MENU($frame, MI_REPEAT_QUICK, sub { $self->{skeinpanel}->quick_slice(reslice => 1) }); + EVT_MENU($frame, MI_QUICK_SAVE_AS, sub { $self->{skeinpanel}->quick_slice(save_as => 1); $repeat->Enable(defined $Slic3r::GUI::SkeinPanel::last_input_file) }); - EVT_MENU($frame, MI_SLICE_SVG, sub { $self->{skeinpanel}->do_slice(save_as => 1, export_svg => 1) }); + EVT_MENU($frame, MI_SLICE_SVG, sub { $self->{skeinpanel}->quick_slice(save_as => 1, export_svg => 1) }); EVT_MENU($frame, MI_COMBINE_STLS, sub { $self->{skeinpanel}->combine_stls }); EVT_MENU($frame, wxID_PREFERENCES, sub { Slic3r::GUI::Preferences->new($frame)->ShowModal }); EVT_MENU($frame, wxID_EXIT, sub {$_[0]->Close(0)}); @@ -250,6 +253,7 @@ sub warning_catcher { my ($self, $message_dialog) = @_; return sub { my $message = shift; + return if $message =~ /GLUquadricObjPtr|Attempt to free unreferenced scalar/; my @params = ($message, 'Warning', wxOK | wxICON_WARNING); $message_dialog ? $message_dialog->(@params) @@ -286,6 +290,7 @@ sub check_version { my %p = @_; Slic3r::debugf "Checking for updates...\n"; + @_ = (); threads->create(sub { my $ua = LWP::UserAgent->new; $ua->timeout(10); diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index ae66ba8e71..8ce659486a 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -159,6 +159,7 @@ sub new { EVT_COMMAND($self, -1, $THUMBNAIL_DONE_EVENT, sub { my ($self, $event) = @_; my ($obj_idx, $thumbnail) = @{$event->GetData}; + return if !$self->{objects}[$obj_idx]; # object was deleted before thumbnail generation completed $self->{objects}[$obj_idx]->thumbnail($thumbnail->clone); $self->on_thumbnail_made($obj_idx); }); @@ -325,7 +326,7 @@ sub load_file { $object->check_manifoldness; # we only consider the rotation of the first instance for now - $object->set_rotation($model->objects->[$i]->instances->[0]->rotation) + $object->rotate($model->objects->[$i]->instances->[0]->rotation) if $model->objects->[$i]->instances; push @{ $self->{objects} }, $object; @@ -424,9 +425,10 @@ sub rotate { if (!defined $angle) { $angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", $object->rotate, -364, 364, $self); return if !$angle || $angle == -1; + $angle = 0 - $angle; # rotate clockwise (be consistent with button icon) } - $object->set_rotation($object->rotate + $angle); + $object->rotate($object->rotate + $angle); $self->recenter; $self->{canvas}->Refresh; } @@ -436,12 +438,15 @@ sub changescale { my ($obj_idx, $object) = $self->selected_object; + # we need thumbnail to be computed before allowing scaling + return if !$object->thumbnail; + # max scale factor should be above 2540 to allow importing files exported in inches my $scale = Wx::GetNumberFromUser("", "Enter the scale % for the selected object:", "Scale", $object->scale*100, 0, 100000, $self); return if !$scale || $scale == -1; $self->{list}->SetItem($obj_idx, 2, "$scale%"); - $object->set_scale($scale / 100); + $object->scale($scale / 100); $self->arrange; } @@ -451,7 +456,7 @@ sub arrange { my $total_parts = sum(map $_->instances_count, @{$self->{objects}}) or return; my @size = (); for my $a (X,Y) { - $size[$a] = max(map $_->size->[$a], @{$self->{objects}}); + $size[$a] = max(map $_->transformed_size->[$a], @{$self->{objects}}); } eval { @@ -476,7 +481,7 @@ sub split_object { my $model_object = $current_object->get_model_object; if (@{$model_object->volumes} > 1) { - Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be splitted because it contains more than one volume/material."); + Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains more than one volume/material."); return; } @@ -485,7 +490,7 @@ sub split_object { my @new_meshes = $mesh->split_mesh; if (@new_meshes == 1) { - Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be splitted because it already contains a single part."); + Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it already contains a single part."); return; } @@ -544,7 +549,13 @@ sub export_gcode { } $self->statusbar->StartBusy; + + # It looks like declaring a local $SIG{__WARN__} prevents the ugly + # "Attempt to free unreferenced scalar" warning... + local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); + if ($Slic3r::have_threads) { + @_ = (); $self->{export_thread} = threads->create(sub { $self->export_gcode2( $print, @@ -693,11 +704,6 @@ sub make_model { foreach my $plater_object (@{$self->{objects}}) { my $model_object = $plater_object->get_model_object; - # if we need to alter the mesh, clone it first - if ($plater_object->scale != 1) { - $model_object = $model_object->clone; - } - my $new_model_object = $model->add_object( vertices => $model_object->vertices, input_file => $plater_object->input_file, @@ -710,13 +716,15 @@ sub make_model { ); $model->set_material($volume->material_id || 0, {}); } - $new_model_object->scale($plater_object->scale); + $new_model_object->align_to_origin; $new_model_object->add_instance( - rotation => $plater_object->rotate, - offset => [ @$_ ], + rotation => $plater_object->rotate, # around center point + scaling_factor => $plater_object->scale, + offset => Slic3r::Point->new($_), ) for @{$plater_object->instances}; } + $model->align_to_origin; return $model; } @@ -738,6 +746,7 @@ sub make_thumbnail { } }; + @_ = (); $Slic3r::have_threads ? threads->create($cb)->detach : $cb->(); } @@ -759,15 +768,17 @@ sub recenter { my @print_bb = Slic3r::Geometry::bounding_box([ map { my $obj = $_; - map { - my $instance = $_; - $instance, [ map $instance->[$_] + $obj->size->[$_], X,Y ]; - } @{$obj->instances}; + my $bb = $obj->transformed_bounding_box; + my @points = ($bb->min_point, $bb->max_point); + map Slic3r::Geometry::move_points($_, @points), @{$obj->instances}; } @{$self->{objects}}, ]); + + # $self->{shift} contains the offset in pixels to add to object instances in order to center them + # it is expressed in upwards Y $self->{shift} = [ - ($self->{canvas}->GetSize->GetWidth - $self->to_pixel($print_bb[X2] + $print_bb[X1])) / 2, - ($self->{canvas}->GetSize->GetHeight - $self->to_pixel($print_bb[Y2] + $print_bb[Y1])) / 2, + $self->to_pixel(-$print_bb[X1]) + ($self->{canvas}->GetSize->GetWidth - $self->to_pixel($print_bb[X2] - $print_bb[X1])) / 2, + $self->to_pixel(-$print_bb[Y1]) + ($self->{canvas}->GetSize->GetHeight - $self->to_pixel($print_bb[Y2] - $print_bb[Y1])) / 2, ]; } @@ -801,10 +812,10 @@ sub _update_bed_size { # supposing the preview canvas is square, calculate the scaling factor # to constrain print bed area inside preview - my $bed_size = $self->{config}->bed_size; - my $canvas_side = CANVAS_SIZE->[X]; # when the canvas is not rendered yet, its GetSize() method returns 0,0 - my $bed_largest_side = $bed_size->[X] > $bed_size->[Y] ? $bed_size->[X] : $bed_size->[Y]; - $self->{scaling_factor} = $canvas_side / $bed_largest_side; + # when the canvas is not rendered yet, its GetSize() method returns 0,0 + $self->{scaling_factor} = CANVAS_SIZE->[X] / max(@{ $self->{config}->bed_size }); + $_->thumbnail_scaling_factor($self->{scaling_factor}) for @{ $self->{objects} }; + $self->recenter; } # this is called on the canvas @@ -857,9 +868,12 @@ sub repaint { next unless $object->thumbnail && @{$object->thumbnail->expolygons}; for my $instance_idx (0 .. $#{$object->instances}) { my $instance = $object->instances->[$instance_idx]; - push @{$parent->{object_previews}}, [ $obj_idx, $instance_idx, $object->thumbnail->clone ]; - $_->translate(map $parent->to_pixel($instance->[$_]) + $parent->{shift}[$_], (X,Y)) - for @{$parent->{object_previews}->[-1][2]->expolygons}; + + my $thumbnail = $object->transformed_thumbnail + ->clone + ->translate(map $parent->to_pixel($instance->[$_]) + $parent->{shift}[$_], (X,Y)); + + push @{$parent->{object_previews}}, [ $obj_idx, $instance_idx, $thumbnail ]; my $drag_object = $self->{drag_object}; if (defined $drag_object && $obj_idx == $drag_object->[0] && $instance_idx == $drag_object->[1]) { @@ -874,7 +888,7 @@ sub repaint { # if sequential printing is enabled and we have more than one object if ($parent->{config}->complete_objects && (map @{$_->instances}, @{$parent->{objects}}) > 1) { my $convex_hull = Slic3r::Polygon->new(convex_hull([ map @{$_->contour}, @{$parent->{object_previews}->[-1][2]->expolygons} ])); - my ($clearance) = offset([$convex_hull], $parent->{config}->extruder_clearance_radius / 2 * $parent->{scaling_factor}, 1, JT_ROUND); + my ($clearance) = @{offset([$convex_hull], $parent->{config}->extruder_clearance_radius / 2 * $parent->{scaling_factor}, 100, JT_ROUND)}; $dc->SetPen($parent->{clearance_pen}); $dc->SetBrush($parent->{transparent_brush}); $dc->DrawPolygon($parent->_y($clearance), 0, 0); @@ -885,7 +899,7 @@ sub repaint { # draw skirt if (@{$parent->{object_previews}} && $parent->{config}->skirts) { my $convex_hull = Slic3r::Polygon->new(convex_hull([ map @{$_->contour}, map @{$_->[2]->expolygons}, @{$parent->{object_previews}} ])); - ($convex_hull) = offset([$convex_hull], $parent->{config}->skirt_distance * $parent->{scaling_factor}, 1, JT_ROUND); + ($convex_hull) = @{offset([$convex_hull], $parent->{config}->skirt_distance * $parent->{scaling_factor}, 100, JT_ROUND)}; $dc->SetPen($parent->{skirt_pen}); $dc->SetBrush($parent->{transparent_brush}); $dc->DrawPolygon($parent->_y($convex_hull), 0, 0) if $convex_hull; @@ -931,7 +945,6 @@ sub mouse_event { my ($obj_idx, $instance_idx, $thumbnail) = @$preview; my $instance = $parent->{objects}[$obj_idx]->instances->[$instance_idx]; $instance->[$_] = $parent->to_units($pos->[$_] - $self->{drag_start_pos}[$_] - $parent->{shift}[$_]) for X,Y; - $instance = $parent->_y([$instance])->[0]; $parent->Refresh; } } elsif ($event->Moving) { @@ -1054,19 +1067,21 @@ sub OnDropFiles { package Slic3r::GUI::Plater::Object; use Moo; -use Math::ConvexHull::MonotoneChain qw(convex_hull); -use Slic3r::Geometry qw(X Y Z); +use Math::ConvexHull::MonotoneChain qw(); +use Slic3r::Geometry qw(X Y Z MIN MAX deg2rad); has 'name' => (is => 'rw', required => 1); has 'input_file' => (is => 'rw', required => 1); has 'input_file_object_id' => (is => 'rw'); # undef means keep model object has 'model_object' => (is => 'rw', required => 1, trigger => 1); -has 'size' => (is => 'rw'); -has 'scale' => (is => 'rw', default => sub { 1 }); -has 'rotate' => (is => 'rw', default => sub { 0 }); +has 'bounding_box' => (is => 'rw'); # 3D bb of original object (aligned to origin) with no rotation or scaling +has 'convex_hull' => (is => 'rw'); # 2D convex hull of original object (aligned to origin) with no rotation or scaling +has 'scale' => (is => 'rw', default => sub { 1 }, trigger => \&_transform_thumbnail); +has 'rotate' => (is => 'rw', default => sub { 0 }, trigger => \&_transform_thumbnail); # around object center point has 'instances' => (is => 'rw', default => sub { [] }); # upward Y axis -has 'thumbnail' => (is => 'rw'); -has 'thumbnail_scaling_factor' => (is => 'rw'); +has 'thumbnail' => (is => 'rw', trigger => \&_transform_thumbnail); +has 'transformed_thumbnail' => (is => 'rw'); +has 'thumbnail_scaling_factor' => (is => 'rw', trigger => \&_transform_thumbnail); has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] # statistics @@ -1078,10 +1093,14 @@ has 'is_manifold' => (is => 'rw'); sub _trigger_model_object { my $self = shift; if ($self->model_object) { + $self->model_object->align_to_origin; + $self->bounding_box($self->model_object->bounding_box); + my $mesh = $self->model_object->mesh; - $self->size([$mesh->size]); + $self->convex_hull(Slic3r::Polygon->new(Math::ConvexHull::MonotoneChain::convex_hull($mesh->used_vertices))); $self->facets(scalar @{$mesh->facets}); $self->vertices(scalar @{$mesh->vertices}); + $self->materials($self->model_object->materials_count); } } @@ -1120,55 +1139,58 @@ sub instances_count { sub make_thumbnail { my $self = shift; - my @points = map [ @$_[X,Y] ], @{$self->model_object->mesh->vertices}; - my $mesh = $self->model_object->mesh; + my $mesh = $self->model_object->mesh; # $self->model_object is already aligned to origin my $thumbnail = Slic3r::ExPolygon::Collection->new( expolygons => (@{$mesh->facets} <= 5000) ? $mesh->horizontal_projection - : [ Slic3r::ExPolygon->new(convex_hull($mesh->vertices)) ], + : [ Slic3r::ExPolygon->new($self->convex_hull) ], ); - for (map @$_, map @$_, @{$thumbnail->expolygons}) { - @$_ = map $_ * $self->thumbnail_scaling_factor, @$_; - } + # only simplify expolygons larger than the threshold - @{$thumbnail->expolygons} = map { ($_->area >= 1) ? $_->simplify(0.5) : $_ } @{$thumbnail->expolygons}; - foreach my $expolygon (@{$thumbnail->expolygons}) { - $expolygon->rotate(Slic3r::Geometry::deg2rad($self->rotate)); - $expolygon->scale($self->scale); - } - @{$thumbnail->expolygons} = grep @$_, @{$thumbnail->expolygons}; - $thumbnail->align_to_origin; + @{$thumbnail->expolygons} = grep @$_, + map { ($_->area >= 1) ? $_->simplify(0.5) : $_ } + @{$thumbnail->expolygons}; + $self->thumbnail($thumbnail); # ignored in multi-threaded environments $self->free_model_object; return $thumbnail; } -sub set_rotation { +sub _transform_thumbnail { my $self = shift; - my ($angle) = @_; - if ($self->thumbnail) { - $self->thumbnail->rotate(Slic3r::Geometry::deg2rad($angle - $self->rotate)); - $self->thumbnail->align_to_origin; - my $z_size = $self->size->[Z]; - $self->size([ (map $_ / $self->thumbnail_scaling_factor, @{$self->thumbnail->size}), $z_size ]); - } - $self->rotate($angle); + return unless $self->thumbnail; + my $t = $self->_apply_transform($self->thumbnail); + $t->scale($self->thumbnail_scaling_factor); + + $self->transformed_thumbnail($t); } -sub set_scale { +# bounding box with applied rotation and scaling +sub transformed_bounding_box { my $self = shift; - my ($scale) = @_; - my $factor = $scale / $self->scale; - return if $factor == 1; - $self->size->[$_] *= $factor for X,Y,Z; - if ($self->thumbnail) { - $_->scale($factor) for @{$self->thumbnail->expolygons}; - $self->thumbnail->align_to_origin; - } - $self->scale($scale); + my $bb = Slic3r::Geometry::BoundingBox->new_from_points($self->_apply_transform($self->convex_hull)); + $bb->extents->[Z] = $self->bounding_box->clone->extents->[Z]; + return $bb; +} + +sub _apply_transform { + my $self = shift; + my ($entity) = @_; # can be anything that implements ->clone(), ->rotate() and ->scale() + + # the order of these transformations MUST be the same everywhere, including + # in Slic3r::Print->add_model() + return $entity + ->clone + ->rotate(deg2rad($self->rotate), $self->bounding_box->center_2D) + ->scale($self->scale); +} + +sub transformed_size { + my $self = shift; + return $self->transformed_bounding_box->size; } 1; diff --git a/lib/Slic3r/GUI/Plater/ObjectDialog.pm b/lib/Slic3r/GUI/Plater/ObjectDialog.pm index 6543d50da1..4fc8388412 100644 --- a/lib/Slic3r/GUI/Plater/ObjectDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectDialog.pm @@ -10,10 +10,12 @@ use base 'Wx::Dialog'; sub new { my $class = shift; my ($parent, %params) = @_; - my $self = $class->SUPER::new($parent, -1, "Object", wxDefaultPosition, [500,350]); + my $self = $class->SUPER::new($parent, -1, "Object", wxDefaultPosition, [500,350], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); $self->{object} = $params{object}; $self->{tabpanel} = Wx::Notebook->new($self, -1, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL); + $self->{tabpanel}->AddPage($self->{preview} = Slic3r::GUI::Plater::ObjectDialog::PreviewTab->new($self->{tabpanel}, object => $self->{object}), "Preview") + if $Slic3r::GUI::have_OpenGL; $self->{tabpanel}->AddPage($self->{info} = Slic3r::GUI::Plater::ObjectDialog::InfoTab->new($self->{tabpanel}, object => $self->{object}), "Info"); $self->{tabpanel}->AddPage($self->{layers} = Slic3r::GUI::Plater::ObjectDialog::LayersTab->new($self->{tabpanel}, object => $self->{object}), "Layers"); @@ -33,6 +35,7 @@ sub new { $sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10); $self->SetSizer($sizer); + $self->SetMinSize($self->GetSize); return $self; } @@ -75,7 +78,7 @@ sub get_properties { my $object = $self->{object}; return [ ['Name' => $object->name], - ['Size' => sprintf "%.2f x %.2f x %.2f", @{$object->size}], + ['Size' => sprintf "%.2f x %.2f x %.2f", @{$object->transformed_size}], ['Facets' => $object->facets], ['Vertices' => $object->vertices], ['Materials' => $object->materials], @@ -83,6 +86,24 @@ sub get_properties { ]; } +package Slic3r::GUI::Plater::ObjectDialog::PreviewTab; +use Wx qw(:dialog :id :misc :sizer :systemsettings); +use base 'Wx::Panel'; + +sub new { + my $class = shift; + my ($parent, %params) = @_; + my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); + $self->{object} = $params{object}; + + my $sizer = Wx::BoxSizer->new(wxVERTICAL); + $sizer->Add(Slic3r::GUI::PreviewCanvas->new($self, $self->{object}->get_model_object->mesh), 1, wxEXPAND, 0); + $self->SetSizer($sizer); + $sizer->SetSizeHints($self); + + return $self; +} + package Slic3r::GUI::Plater::ObjectDialog::LayersTab; use Wx qw(:dialog :id :misc :sizer :systemsettings); use Wx::Grid; diff --git a/lib/Slic3r/GUI/PreviewCanvas.pm b/lib/Slic3r/GUI/PreviewCanvas.pm new file mode 100644 index 0000000000..f70a0bba66 --- /dev/null +++ b/lib/Slic3r/GUI/PreviewCanvas.pm @@ -0,0 +1,200 @@ +package Slic3r::GUI::PreviewCanvas; +use strict; +use warnings; + +use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_ERASE_BACKGROUND EVT_IDLE EVT_TIMER EVT_MOUSEWHEEL); +# must load OpenGL *before* Wx::GLCanvas +use OpenGL qw(:glconstants :glfunctions); +use base qw(Wx::GLCanvas Class::Accessor); +use Slic3r::Geometry qw(X Y Z MIN MAX triangle_normal normalize deg2rad tan); +use Wx::GLCanvas qw(:all); + +__PACKAGE__->mk_accessors( qw(timer x_rot y_rot dirty init mesh_center zoom + verts norms) ); + +sub new { + my ($class, $parent, $mesh) = @_; + my $self = $class->SUPER::new($parent); + + # prepare mesh + { + $self->mesh_center($mesh->center); + $self->zoom(0.1); + + my @verts = map $self->zoom * $_, map @{ $mesh->vertices->[$_] }, map @$_, @{$mesh->facets}; + $self->verts(OpenGL::Array->new_list(GL_FLOAT, @verts)); + + my @norms = map { @$_, @$_, @$_ } map normalize(triangle_normal(map $mesh->vertices->[$_], @$_)), @{$mesh->facets}; + $self->norms(OpenGL::Array->new_list(GL_FLOAT, @norms)); + } + + my $timer = $self->timer( Wx::Timer->new($self) ); + $timer->Start(50); + + $self->x_rot(0); + $self->y_rot(0); + + EVT_PAINT($self, sub { + my $dc = Wx::PaintDC->new($self); + $self->Render($dc); + }); + EVT_SIZE($self, sub { $self->dirty(1) }); + EVT_IDLE($self, sub { + return unless $self->dirty; + return if !$self->IsShownOnScreen; + $self->Resize( $self->GetSizeWH ); + $self->Refresh; + }); + EVT_TIMER($self, -1, sub { + my ($self, $e) = @_; + + $self->x_rot( $self->x_rot - 1 ); + $self->y_rot( $self->y_rot + 2 ); + + $self->dirty(1); + Wx::WakeUpIdle; + }); + EVT_MOUSEWHEEL($self, sub { + my ($self, $e) = @_; + + my $zoom = $self->zoom * (1.0 - $e->GetWheelRotation() / $e->GetWheelDelta() / 10); + $zoom = 0.001 if $zoom < 0.001; + $zoom = 0.1 if $zoom > 0.1; + $self->zoom($zoom); + + $self->Refresh; + }); + + return $self; +} + +sub GetContext { + my ($self) = @_; + + if (Wx::wxVERSION >= 2.009) { + return $self->{context} ||= Wx::GLContext->new($self); + } else { + return $self->SUPER::GetContext; + } +} + +sub SetCurrent { + my ($self, $context) = @_; + + if (Wx::wxVERSION >= 2.009) { + return $self->SUPER::SetCurrent($context); + } else { + return $self->SUPER::SetCurrent; + } +} + +sub Resize { + my ($self, $x, $y) = @_; + + return unless $self->GetContext; + $self->dirty(0); + + $self->SetCurrent($self->GetContext); + glViewport(0, 0, $x, $y); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + my_gluPerspective(45, $x/$y, .5, 100); + + glMatrixMode(GL_MODELVIEW); +} + +sub my_gluPerspective { + my ($fov, $ratio, $near, $far) = @_; + + my $top = tan(deg2rad($fov)*0.5) * $near; + my $bottom = -$top; + my $left = $ratio * $bottom; + my $right = $ratio * $top; + + glFrustum( $left, $right, $bottom, $top, $near, $far ); +} + +sub DESTROY { + my $self = shift; + + $self->timer->Stop; + $self->timer(undef); +} + +sub InitGL { + my $self = shift; + + return if $self->init; + return unless $self->GetContext; + $self->init(1); + + glEnable(GL_NORMALIZE); + glEnable(GL_LIGHTING); + glDepthFunc(GL_LESS); + glEnable(GL_DEPTH_TEST); + + # Settings for our light. + my @LightPos = (0, 0, 2, 1.0); + my @LightAmbient = (0.1, 0.1, 0.1, 1.0); + my @LightDiffuse = (0.7, 0.5, 0.5, 1.0); + my @LightSpecular = (0.1, 0.1, 0.1, 0.1); + + # Enables Smooth Color Shading; try GL_FLAT for (lack of) fun. + glShadeModel(GL_SMOOTH); + + # Set up a light, turn it on. + glLightfv_p(GL_LIGHT1, GL_POSITION, @LightPos); + glLightfv_p(GL_LIGHT1, GL_AMBIENT, @LightAmbient); + glLightfv_p(GL_LIGHT1, GL_DIFFUSE, @LightDiffuse); + glLightfv_p(GL_LIGHT1, GL_SPECULAR, @LightSpecular); + glEnable(GL_LIGHT1); + + # A handy trick -- have surface material mirror the color. + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); + glEnable(GL_COLOR_MATERIAL); +} + +sub Render { + my ($self, $dc) = @_; + + return unless $self->GetContext; + $self->SetCurrent($self->GetContext); + $self->InitGL; + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glPushMatrix(); + glTranslatef( 0, 0, -5 ); + + # this needs to get a lot better... + glRotatef( $self->x_rot, 1, 0, 0 ); + glRotatef( $self->y_rot, 0, 0, 1 ); + glTranslatef(map -$_ * $self->zoom, @{ $self->mesh_center }); + + $self->draw_mesh; + + glPopMatrix(); + glFlush(); + + $self->SwapBuffers(); +} + +sub draw_mesh { + my $self = shift; + + glEnable(GL_CULL_FACE); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_NORMAL_ARRAY); + + glVertexPointer_p(3, $self->verts); + + glCullFace(GL_BACK); + glNormalPointer_p($self->norms); + glDrawArrays(GL_TRIANGLES, 0, $self->verts->elements / 3); + + glDisableClientState(GL_NORMAL_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); +} + +1; diff --git a/lib/Slic3r/GUI/SimpleTab.pm b/lib/Slic3r/GUI/SimpleTab.pm index 0463a3d4e6..c1fee5439e 100644 --- a/lib/Slic3r/GUI/SimpleTab.pm +++ b/lib/Slic3r/GUI/SimpleTab.pm @@ -78,6 +78,17 @@ sub load_config { } } +sub set_value { + my $self = shift; + my ($opt_key, $value) = @_; + + my $changed = 0; + foreach my $optgroup (@{$self->{optgroups}}) { + $changed = 1 if $optgroup->set_value($opt_key, $value); + } + return $changed; +} + sub is_dirty { 0 } sub config { $_[0]->{config}->clone } diff --git a/lib/Slic3r/GUI/SkeinPanel.pm b/lib/Slic3r/GUI/SkeinPanel.pm index 5defd274cf..6c4842eabf 100644 --- a/lib/Slic3r/GUI/SkeinPanel.pm +++ b/lib/Slic3r/GUI/SkeinPanel.pm @@ -49,9 +49,12 @@ sub new { $self->{tabpanel}, on_value_change => sub { $self->{plater}->on_config_change(@_) if $self->{plater}; # propagate config change events to the plater - if ($self->{mode} eq 'simple' && $init) { # don't save while loading for the first time - # save config - $self->config->save("$Slic3r::GUI::datadir/simple.ini"); + if ($init) { # don't save while loading for the first time + if ($self->{mode} eq 'simple') { + # save config + $self->config->save("$Slic3r::GUI::datadir/simple.ini"); + } + $self->config->save($Slic3r::GUI::autosave) if $Slic3r::GUI::autosave; } }, on_presets_changed => sub { @@ -73,7 +76,7 @@ sub new { return $self; } -sub do_slice { +sub quick_slice { my $self = shift; my %params = @_; diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index 4c1e23b9fe..e0848aa86b 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -7,7 +7,7 @@ our @ISA = qw(Exporter); our @EXPORT_OK = qw( PI X Y Z A B X1 Y1 X2 Y2 MIN MAX epsilon slope line_atan lines_parallel line_point_belongs_to_segment points_coincide distance_between_points - chained_path_items chained_path_points + chained_path_items chained_path_points normalize tan move_points_3D line_length midpoint point_in_polygon point_in_segment segment_in_segment point_is_on_left_of_segment polyline_lines polygon_lines nearest_point point_along_segment polygon_segment_having_point polygon_has_subsegment @@ -45,6 +45,11 @@ sub scaled_epsilon () { epsilon / &Slic3r::SCALING_FACTOR } sub scale ($) { $_[0] / &Slic3r::SCALING_FACTOR } sub unscale ($) { $_[0] * &Slic3r::SCALING_FACTOR } +sub tan { + my ($angle) = @_; + return (sin $angle) / (cos $angle); +} + sub slope { my ($line) = @_; return undef if abs($line->[B][X] - $line->[A][X]) < epsilon; # line is vertical @@ -383,6 +388,15 @@ sub move_points { return map Slic3r::Point->new($shift->[X] + $_->[X], $shift->[Y] + $_->[Y]), @points; } +sub move_points_3D { + my ($shift, @points) = @_; + return map [ + $shift->[X] + $_->[X], + $shift->[Y] + $_->[Y], + $shift->[Z] + $_->[Z], + ], @points; +} + # implementation of Liang-Barsky algorithm # polygon must be convex and ccw sub clip_segment_polygon { @@ -461,6 +475,14 @@ sub triangle_normal { return normal($u, $v); } +sub normalize { + my ($line) = @_; + + my $len = sqrt( ($line->[X]**2) + ($line->[Y]**2) + ($line->[Z]**2) ) + or return [0, 0, 0]; # to avoid illegal division by zero + return [ map $_ / $len, @$line ]; +} + # 2D dot product sub dot { my ($u, $v) = @_; diff --git a/lib/Slic3r/Geometry/BoundingBox.pm b/lib/Slic3r/Geometry/BoundingBox.pm new file mode 100644 index 0000000000..c2d27a1fe8 --- /dev/null +++ b/lib/Slic3r/Geometry/BoundingBox.pm @@ -0,0 +1,87 @@ +package Slic3r::Geometry::BoundingBox; +use Moo; +use Slic3r::Geometry qw(X Y Z MIN MAX X1 Y1 X2 Y2); +use Storable qw(); + +has 'extents' => (is => 'ro', required => 1); + +sub clone { Storable::dclone($_[0]) } + +# 2D +sub new_from_points { + my $class = shift; + my ($points) = @_; + + my $bb = [ Slic3r::Geometry::bounding_box($points) ]; + return $class->new(extents => [ + [ $bb->[X1], $bb->[X2] ], + [ $bb->[Y1], $bb->[Y2] ], + ]); +} + +# 3D +sub new_from_points_3D { + my $class = shift; + my ($points) = @_; + + return $class->new(extents => [ Slic3r::Geometry::bounding_box_3D($points) ]); +} + +# four-arguments 2D bb +sub bb { + my $self = shift; + my $extents = $self->extents; + return [ $extents->[X][MIN], $extents->[Y][MIN], $extents->[X][MAX], $extents->[Y][MAX] ]; +} + +sub polygon { + my $self = shift; + return Slic3r::Polygon->new_from_bounding_box($self->bb); +} + +# note to $self +sub rotate { + die "Rotating an axis-aligned bounding box doesn't make any sense"; +} + +sub scale { + my $self = shift; + my ($factor) = @_; + + for (@{$self->extents}) { + $_ *= $factor for @$_[MIN,MAX]; + } + + $self; +} + +sub size { + my $self = shift; + + my $extents = $self->extents; + return [ map $extents->[$_][MAX] - $extents->[$_][MIN], grep $extents->[$_], (X,Y,Z) ]; +} + +sub center { + my $self = shift; + + my $extents = $self->extents; + return [ map +($extents->[$_][MAX] + $extents->[$_][MIN])/2, grep $extents->[$_], (X,Y,Z) ]; +} + +sub center_2D { + my $self = shift; + return Slic3r::Point->new(@{$self->center}[X,Y]); +} + +sub min_point { + my $self = shift; + return Slic3r::Point->new($self->extents->[X][MIN], $self->extents->[Y][MIN]); +} + +sub max_point { + my $self = shift; + return Slic3r::Point->new($self->extents->[X][MAX], $self->extents->[Y][MAX]); +} + +1; diff --git a/lib/Slic3r/Geometry/Clipper.pm b/lib/Slic3r/Geometry/Clipper.pm index 7e5e4b4847..276a300b8d 100644 --- a/lib/Slic3r/Geometry/Clipper.pm +++ b/lib/Slic3r/Geometry/Clipper.pm @@ -9,7 +9,7 @@ our @EXPORT_OK = qw(safety_offset safety_offset_ex offset offset_ex collapse_ex JT_SQUARE is_counter_clockwise union_pt offset2 offset2_ex traverse_pt intersection); -use Math::Clipper 1.21 qw(:cliptypes :polyfilltypes :jointypes is_counter_clockwise area); +use Math::Clipper 1.22 qw(:cliptypes :polyfilltypes :jointypes is_counter_clockwise area); use Slic3r::Geometry qw(scale); our $clipper = Math::Clipper->new; diff --git a/lib/Slic3r/Layer.pm b/lib/Slic3r/Layer.pm index 074baef554..9803ce2b53 100644 --- a/lib/Slic3r/Layer.pm +++ b/lib/Slic3r/Layer.pm @@ -54,6 +54,13 @@ sub support_material_contact_z { return $self->print_z - ($self->height - $self->support_material_contact_height) / &Slic3r::SCALING_FACTOR; } +sub upper_layer_slices { + my $self = shift; + + my $upper_layer = $self->object->layers->[ $self->id + 1 ] or return []; + return $upper_layer->slices; +} + sub region { my $self = shift; my ($region_id) = @_; diff --git a/lib/Slic3r/Layer/Region.pm b/lib/Slic3r/Layer/Region.pm index 89975317bf..e1fefef757 100644 --- a/lib/Slic3r/Layer/Region.pm +++ b/lib/Slic3r/Layer/Region.pm @@ -252,7 +252,7 @@ sub make_perimeters { )}; my @loops = (); - foreach my $polynode (@$polynodes) { + foreach my $polynode (@nodes) { push @loops, $traverse->($polynode->{children}, $depth+1, $is_contour); my $polygon = Slic3r::Polygon->new($polynode->{outer} // [ reverse @{$polynode->{hole}} ]); @@ -317,7 +317,7 @@ sub _fill_gaps { return unless $Slic3r::Config->gap_fill_speed > 0 && $Slic3r::Config->fill_density > 0 && @$gaps; - my $filler = $self->layer->object->print->fill_maker->filler('rectilinear'); + my $filler = $self->layer->object->fill_maker->filler('rectilinear'); $filler->layer_id($self->layer->id); # we should probably use this code to handle thin walls and remove that logic from @@ -331,8 +331,12 @@ sub _fill_gaps { 1, )}; + # medial axis-based gap fill should benefit from detection of larger gaps too, so + # we could try with 1.5*$w for example, but that doesn't work well for zigzag fill + # because it tends to create very sparse points along the gap when the infill direction + # is not parallel to the gap (1.5*$w thus may only work well with a straight line) my $w = $self->perimeter_flow->width; - my @widths = (1.5 * $w, $w, 0.4 * $w); # worth trying 0.2 too? + my @widths = ($w, 0.4 * $w); # worth trying 0.2 too? foreach my $width (@widths) { my $flow = $self->perimeter_flow->clone(width => $width); @@ -373,7 +377,7 @@ sub _fill_gaps { push @{ $self->thin_fills }, map { - $_->polyline->simplify($flow->scaled_width / 3); + $_->simplify($flow->scaled_width/3); $_->pack; } map Slic3r::ExtrusionPath->new( diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm index 58473fa9fb..b0f422ea17 100644 --- a/lib/Slic3r/Model.pm +++ b/lib/Slic3r/Model.pm @@ -1,7 +1,8 @@ package Slic3r::Model; use Moo; -use Slic3r::Geometry qw(X Y Z); +use List::Util qw(first max); +use Slic3r::Geometry qw(X Y Z MIN move_points); has 'materials' => (is => 'ro', default => sub { {} }); has 'objects' => (is => 'ro', default => sub { [] }); @@ -19,6 +20,38 @@ sub read_from_file { return $model; } +sub merge { + my $class = shift; + my @models = @_; + + my $new_model = $class->new; + foreach my $model (@models) { + # merge material attributes (should we rename them in case of duplicates?) + $new_model->set_material($_, { %{$model->materials->{$_}}, %{$model->materials->{$_} || {}} }) + for keys %{$model->materials}; + + foreach my $object (@{$model->objects}) { + my $new_object = $new_model->add_object( + input_file => $object->input_file, + vertices => $object->vertices, + layer_height_ranges => $object->layer_height_ranges, + ); + + $new_object->add_volume( + material_id => $_->material_id, + facets => $_->facets, + ) for @{$object->volumes}; + + $new_object->add_instance( + offset => $_->offset, + rotation => $_->rotation, + ) for @{ $object->instances // [] }; + } + } + + return $new_model; +} + sub add_object { my $self = shift; @@ -39,10 +72,137 @@ sub set_material { sub scale { my $self = shift; - $_->scale(@_) for @{$self->objects}; } +sub arrange_objects { + my $self = shift; + my ($config) = @_; + + # do we have objects with no position? + if (first { !defined $_->instances } @{$self->objects}) { + # we shall redefine positions for all objects + + my ($copies, @positions) = $self->_arrange( + config => $config, + items => $self->objects, + ); + + # apply positions to objects + foreach my $object (@{$self->objects}) { + $object->align_to_origin; + + $object->instances([]); + $object->add_instance( + offset => $_, + rotation => 0, + ) for splice @positions, 0, $copies; + } + + } else { + # we only have objects with defined position + + # align the whole model to origin as it is + $self->align_to_origin; + + # arrange this model as a whole + my ($copies, @positions) = $self->_arrange( + config => $config, + items => [$self], + ); + + # apply positions to objects by translating the current positions + foreach my $object (@{$self->objects}) { + my @old_instances = @{$object->instances}; + $object->instances([]); + foreach my $instance (@old_instances) { + $object->add_instance( + offset => $_, + rotation => $instance->rotation, + scaling_factor => $instance->scaling_factor, + ) for move_points($instance->offset, @positions); + } + } + } +} + +sub _arrange { + my $self = shift; + my %params = @_; + + my $config = $params{config}; + my @items = @{$params{items}}; # can be Model or Object objects, they have to implement size() + + if ($config->duplicate_grid->[X] > 1 || $config->duplicate_grid->[Y] > 1) { + if (@items > 1) { + die "Grid duplication is not supported with multiple objects\n"; + } + my @positions = (); + my $size = $items[0]->size; + my $dist = $config->duplicate_distance; + for my $x_copy (1..$config->duplicate_grid->[X]) { + for my $y_copy (1..$config->duplicate_grid->[Y]) { + push @positions, [ + ($size->[X] + $dist) * ($x_copy-1), + ($size->[Y] + $dist) * ($y_copy-1), + ]; + } + } + return ($config->duplicate_grid->[X] * $config->duplicate_grid->[Y]), @positions; + } else { + my $total_parts = $config->duplicate * @items; + my $partx = max(map $_->size->[X], @items); + my $party = max(map $_->size->[Y], @items); + return $config->duplicate, + Slic3r::Geometry::arrange + ($total_parts, $partx, $party, (map $_, @{$config->bed_size}), + $config->min_object_distance, $config); + } +} + +sub vertices { + my $self = shift; + return [ map @{$_->vertices}, @{$self->objects} ]; +} + +sub used_vertices { + my $self = shift; + return [ map @{$_->used_vertices}, @{$self->objects} ]; +} + +sub size { + my $self = shift; + return [ Slic3r::Geometry::size_3D($self->used_vertices) ]; +} + +sub extents { + my $self = shift; + return Slic3r::Geometry::bounding_box_3D($self->used_vertices); +} + +sub align_to_origin { + my $self = shift; + + # calculate the displacements needed to + # have lowest value for each axis at coordinate 0 + { + my @extents = $self->extents; + $self->move(map -$extents[$_][MIN], X,Y,Z); + } + + # align all instances to 0,0 as well + { + my @instances = map @{$_->instances}, @{$self->objects}; + my @extents = Slic3r::Geometry::bounding_box_3D([ map $_->offset, @instances ]); + $_->offset->translate(-$extents[X][MIN], -$extents[Y][MIN]) for @instances; + } +} + +sub move { + my $self = shift; + $_->move(@_) for @{$self->objects}; +} + # flattens everything to a single mesh sub mesh { my $self = shift; @@ -54,6 +214,7 @@ sub mesh { my $mesh = $object->mesh->clone; if ($instance) { $mesh->rotate($instance->rotation); + $mesh->scale($instance->scaling_factor); $mesh->align_to_origin; $mesh->move(@{$instance->offset}); } @@ -64,6 +225,48 @@ sub mesh { return Slic3r::TriangleMesh->merge(@meshes); } +# this method splits objects into multiple distinct objects by walking their meshes +sub split_meshes { + my $self = shift; + + my @objects = @{$self->objects}; + @{$self->objects} = (); + + foreach my $object (@objects) { + if (@{$object->volumes} > 1) { + # We can't split meshes if there's more than one material, because + # we can't group the resulting meshes by object afterwards + push @{$self->objects}, $object; + next; + } + + my $volume = $object->volumes->[0]; + foreach my $mesh ($volume->mesh->split_mesh) { + my $new_object = $self->add_object( + input_file => $object->input_file, + layer_height_ranges => $object->layer_height_ranges, + ); + $new_object->add_volume( + vertices => $mesh->vertices, + facets => $mesh->facets, + material_id => $volume->material_id, + ); + + # let's now align the new object to the origin and put its displacement + # (extents) in the instances info + my @extents = $mesh->extents; + $new_object->align_to_origin; + + # add one instance per original instance applying the displacement + $new_object->add_instance( + offset => [ $_->offset->[X] + $extents[X][MIN], $_->offset->[Y] + $extents[Y][MIN] ], + rotation => $_->rotation, + scaling_factor => $_->scaling_factor, + ) for @{ $object->instances // [] }; + } + } +} + package Slic3r::Model::Region; use Moo; @@ -74,7 +277,7 @@ package Slic3r::Model::Object; use Moo; use List::Util qw(first); -use Slic3r::Geometry qw(X Y Z); +use Slic3r::Geometry qw(X Y Z MIN MAX move_points move_points_3D); use Storable qw(dclone); has 'input_file' => (is => 'rw'); @@ -123,6 +326,49 @@ sub mesh { ); } +sub used_vertices { + my $self = shift; + return [ map $self->vertices->[$_], map @$_, map @{$_->facets}, @{$self->volumes} ]; +} + +sub size { + my $self = shift; + return [ Slic3r::Geometry::size_3D($self->used_vertices) ]; +} + +sub extents { + my $self = shift; + return Slic3r::Geometry::bounding_box_3D($self->used_vertices); +} + +sub center { + my $self = shift; + + my @extents = $self->extents; + return [ map +($extents[$_][MAX] + $extents[$_][MIN])/2, X,Y,Z ]; +} + +sub bounding_box { + my $self = shift; + return Slic3r::Geometry::BoundingBox->new(extents => [ $self->extents ]); +} + +sub align_to_origin { + my $self = shift; + + # calculate the displacements needed to + # have lowest value for each axis at coordinate 0 + my @extents = $self->extents; + my @shift = map -$extents[$_][MIN], X,Y,Z; + $self->move(@shift); + return @shift; +} + +sub move { + my $self = shift; + @{$self->vertices} = move_points_3D([ @_ ], @{$self->vertices}); +} + sub scale { my $self = shift; my ($factor) = @_; @@ -134,6 +380,19 @@ sub scale { } } +sub rotate { + my $self = shift; + my ($deg) = @_; + return if $deg == 0; + + my $rad = Slic3r::Geometry::deg2rad($deg); + + # transform vertex coordinates + foreach my $vertex (@{$self->vertices}) { + @$vertex = (@{ +(Slic3r::Geometry::rotate_points($rad, undef, [ $vertex->[X], $vertex->[Y] ]))[0] }, $vertex->[Z]); + } +} + sub materials_count { my $self = shift; @@ -167,7 +426,8 @@ package Slic3r::Model::Instance; use Moo; has 'object' => (is => 'ro', weak_ref => 1, required => 1); -has 'rotation' => (is => 'rw', default => sub { 0 }); -has 'offset' => (is => 'rw'); +has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point +has 'scaling_factor' => (is => 'rw', default => sub { 1 }); +has 'offset' => (is => 'rw'); # must be Slic3r::Point object 1; diff --git a/lib/Slic3r/Point.pm b/lib/Slic3r/Point.pm index 26e79045af..b820a1e83f 100644 --- a/lib/Slic3r/Point.pm +++ b/lib/Slic3r/Point.pm @@ -35,6 +35,13 @@ sub distance_to { return Slic3r::Geometry::distance_between_points($self, $point); } +sub scale { + my $self = shift; + my ($factor) = @_; + $_ *= $factor for @$self; + $self; +} + sub rotate { my $self = shift; my ($angle, $center) = @_; diff --git a/lib/Slic3r/Polyline.pm b/lib/Slic3r/Polyline.pm index 6646d1aaeb..2de79893a7 100644 --- a/lib/Slic3r/Polyline.pm +++ b/lib/Slic3r/Polyline.pm @@ -156,11 +156,12 @@ sub translate { sub scale { my $self = shift; my ($factor) = @_; - return if $factor == 1; # transform point coordinates - foreach my $point (@$self) { - $point->[$_] *= $factor for X,Y; + if ($factor != 1) { + foreach my $point (@$self) { + $point->[$_] *= $factor for X,Y; + } } return $self; } diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index b5482f7a07..956de69925 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -6,7 +6,8 @@ use File::Spec; use List::Util qw(max first); use Math::ConvexHull::MonotoneChain qw(convex_hull); use Slic3r::ExtrusionPath ':roles'; -use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN PI scale unscale move_points nearest_point); +use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN MAX PI scale unscale move_points + nearest_point chained_path); use Slic3r::Geometry::Clipper qw(diff_ex union_ex union_pt intersection_ex offset offset2 traverse_pt JT_ROUND JT_SQUARE PFT_EVENODD); use Time::HiRes qw(gettimeofday tv_interval); @@ -21,7 +22,6 @@ has 'regions' => (is => 'rw', default => sub {[]}); has 'support_material_flow' => (is => 'rw'); has 'first_layer_support_material_flow' => (is => 'rw'); has 'has_support_material' => (is => 'lazy'); -has 'fill_maker' => (is => 'lazy'); # ordered collection of extrusion paths to build skirt loops has 'skirt' => ( @@ -82,11 +82,8 @@ sub _build_has_support_material { || $self->config->support_material_enforce_layers > 0; } -sub _build_fill_maker { - my $self = shift; - return Slic3r::Fill->new(print => $self); -} - +# caller is responsible for supplying models whose objects don't collide +# and have explicit instance positions sub add_model { my $self = shift; my ($model) = @_; @@ -103,57 +100,61 @@ sub add_model { } } + # optimization: if avoid_crossing_perimeters is enabled, split + # this mesh into distinct objects so that we reduce the complexity + # of the graphs + $model->split_meshes if $Slic3r::Config->avoid_crossing_perimeters && !$Slic3r::Config->complete_objects; + foreach my $object (@{ $model->objects }) { - my @meshes = (); # by region_id + # we align object to origin before applying transformations + my @align = $object->align_to_origin; + # extract meshes by material + my @meshes = (); # by region_id foreach my $volume (@{$object->volumes}) { - # should the object contain multiple volumes of the same material, merge them my $region_id = defined $volume->material_id ? $materials{$volume->material_id} : 0; my $mesh = $volume->mesh->clone; + # should the object contain multiple volumes of the same material, merge them $meshes[$region_id] = $meshes[$region_id] ? Slic3r::TriangleMesh->merge($meshes[$region_id], $mesh) : $mesh; } - foreach my $mesh (@meshes) { - next unless $mesh; + foreach my $mesh (grep $_, @meshes) { $mesh->check_manifoldness; - if ($object->instances) { - # we ignore the per-instance rotation currently and only - # consider the first one - $mesh->rotate($object->instances->[0]->rotation); + # the order of these transformations must be the same as the one used in plater + # to make the object positioning consistent with the visual preview + + # we ignore the per-instance transformations currently and only + # consider the first one + if ($object->instances && @{$object->instances}) { + $mesh->rotate($object->instances->[0]->rotation, $object->center); + $mesh->scale($object->instances->[0]->scaling_factor); } - $mesh->rotate($Slic3r::Config->rotate); - $mesh->scale($Slic3r::Config->scale / &Slic3r::SCALING_FACTOR); + $mesh->scale(1 / &Slic3r::SCALING_FACTOR); } - my @defined_meshes = grep defined $_, @meshes; - my $complete_mesh = @defined_meshes == 1 ? $defined_meshes[0] : Slic3r::TriangleMesh->merge(@defined_meshes); + # we also align object after transformations so that we only work with positive coordinates + # and the assumption that bounding_box === size works + my $bb = Slic3r::Geometry::BoundingBox->new_from_points_3D([ map @{$_->used_vertices}, grep $_, @meshes ]); + my @align2 = map -$bb->extents->[$_][MIN], (X,Y,Z); + $_->move(@align2) for grep $_, @meshes; # initialize print object - my $print_object = Slic3r::Print::Object->new( + push @{$self->objects}, Slic3r::Print::Object->new( print => $self, meshes => [ @meshes ], - size => [ $complete_mesh->size ], + copies => [ + $object->instances + ? (map [ scale($_->offset->[X] - $align[X]) - $align2[X], scale($_->offset->[Y] - $align[Y]) - $align2[Y] ], @{$object->instances}) + : [0,0], + ], + size => $bb->size, # transformed size input_file => $object->input_file, layer_height_ranges => $object->layer_height_ranges, ); - push @{$self->objects}, $print_object; - - # align object to origin - { - my @extents = $complete_mesh->extents; - foreach my $mesh (grep defined $_, @meshes) { - $mesh->move(map -$extents[$_][MIN], X,Y,Z); - } - } - - if ($object->instances) { - # replace the default [0,0] instance with the custom ones - $print_object->copies([ map [ scale $_->offset->[X], scale $_->offset->[Y] ], @{$object->instances} ]); - } } } @@ -282,54 +283,12 @@ sub regions_count { return scalar @{$self->regions}; } -sub duplicate { - my $self = shift; - - if ($Slic3r::Config->duplicate_grid->[X] > 1 || $Slic3r::Config->duplicate_grid->[Y] > 1) { - if (@{$self->objects} > 1) { - die "Grid duplication is not supported with multiple objects\n"; - } - my $object = $self->objects->[0]; - - # generate offsets for copies - my $dist = scale $Slic3r::Config->duplicate_distance; - @{$self->objects->[0]->copies} = (); - for my $x_copy (1..$Slic3r::Config->duplicate_grid->[X]) { - for my $y_copy (1..$Slic3r::Config->duplicate_grid->[Y]) { - push @{$self->objects->[0]->copies}, [ - ($object->size->[X] + $dist) * ($x_copy-1), - ($object->size->[Y] + $dist) * ($y_copy-1), - ]; - } - } - } elsif ($Slic3r::Config->duplicate > 1) { - foreach my $object (@{$self->objects}) { - @{$object->copies} = map [0,0], 1..$Slic3r::Config->duplicate; - } - $self->arrange_objects; - } -} - -sub arrange_objects { - my $self = shift; - - my $total_parts = scalar map @{$_->copies}, @{$self->objects}; - my $partx = max(map $_->size->[X], @{$self->objects}); - my $party = max(map $_->size->[Y], @{$self->objects}); - - my @positions = Slic3r::Geometry::arrange - ($total_parts, $partx, $party, (map scale $_, @{$Slic3r::Config->bed_size}), scale $Slic3r::Config->min_object_distance, $self->config); - - @{$_->copies} = splice @positions, 0, scalar @{$_->copies} for @{$self->objects}; -} - sub bounding_box { my $self = shift; my @points = (); - foreach my $obj_idx (0 .. $#{$self->objects}) { - my $object = $self->objects->[$obj_idx]; - foreach my $copy (@{$self->objects->[$obj_idx]->copies}) { + foreach my $object (@{$self->objects}) { + foreach my $copy (@{$object->copies}) { push @points, [ $copy->[X], $copy->[Y] ], [ $copy->[X] + $object->size->[X], $copy->[Y] ], @@ -370,6 +329,13 @@ sub export_gcode { $status_cb->(10, "Processing triangulated mesh"); $_->slice for @{$self->objects}; + # remove empty layers and abort if there are no more + # as some algorithms assume all objects have at least one layer + # note: this will change object indexes + @{$self->objects} = grep @{$_->layers}, @{$self->objects}; + die "No layers were detected. You might want to repair your STL file(s) or check their size and retry.\n" + if !@{$self->objects}; + if ($Slic3r::Config->resolution) { $status_cb->(15, "Simplifying input"); $self->_simplify_slices(scale $Slic3r::Config->resolution); @@ -416,7 +382,6 @@ sub export_gcode { # this will generate extrusion paths for each layer $status_cb->(80, "Infilling layers"); { - my $fill_maker = $self->fill_maker; Slic3r::parallelize( items => sub { my @items = (); # [obj_idx, layer_id] @@ -433,10 +398,11 @@ sub export_gcode { my $fills = {}; while (defined (my $obj_layer = $q->dequeue)) { my ($obj_idx, $layer_id, $region_id) = @$obj_layer; + my $object = $self->objects->[$obj_idx]; $fills->{$obj_idx} ||= {}; $fills->{$obj_idx}{$layer_id} ||= {}; $fills->{$obj_idx}{$layer_id}{$region_id} = [ - $fill_maker->make_fill($self->objects->[$obj_idx]->layers->[$layer_id]->regions->[$region_id]), + $object->fill_maker->make_fill($object->layers->[$layer_id]->regions->[$region_id]), ]; } return $fills; @@ -455,7 +421,7 @@ sub export_gcode { }, no_threads_cb => sub { foreach my $layerm (map @{$_->regions}, map @{$_->layers}, @{$self->objects}) { - $layerm->fills([ $fill_maker->make_fill($layerm) ]); + $layerm->fills([ $layerm->layer->object->fill_maker->make_fill($layerm) ]); } }, ); @@ -528,13 +494,15 @@ sub export_svg { $self->init_extruders; $_->slice for @{$self->objects}; - $self->arrange_objects; - my $output_file = $self->expanded_output_filepath($params{output_file}); - $output_file =~ s/\.gcode$/.svg/i; + my $fh = $params{output_fh}; + if ($params{output_file}) { + my $output_file = $self->expanded_output_filepath($params{output_file}); + $output_file =~ s/\.gcode$/.svg/i; + Slic3r::open(\$fh, ">", $output_file) or die "Failed to open $output_file for writing\n"; + print "Exporting to $output_file..." unless $params{quiet}; + } - Slic3r::open(\my $fh, ">", $output_file) or die "Failed to open $output_file for writing\n"; - print "Exporting to $output_file..."; my $print_size = $self->size; print $fh sprintf <<"EOF", unscale($print_size->[X]), unscale($print_size->[Y]); @@ -603,7 +571,7 @@ EOF print $fh "\n"; close $fh; - print "Done.\n"; + print "Done.\n" unless $params{quiet}; } sub make_skirt { @@ -748,10 +716,11 @@ sub write_gcode { # set up our extruder object my $gcodegen = Slic3r::GCode->new( + config => $self->config, multiple_extruders => (@{$self->extruders} > 1), layer_count => $self->layer_count, ); - print $fh "G21 ; set units to millimeters\n"; + print $fh "G21 ; set units to millimeters\n" if $Slic3r::Config->gcode_flavor ne 'makerware'; print $fh $gcodegen->set_fan(0, 1) if $Slic3r::Config->cooling && $Slic3r::Config->disable_fan_first_layers; # write start commands to file @@ -769,15 +738,13 @@ sub write_gcode { printf $fh $gcodegen->set_temperature($self->extruders->[$t]->first_layer_temperature, 1, $t) if $self->extruders->[$t]->first_layer_temperature && $Slic3r::Config->start_gcode !~ /M(?:109|104)/i; } - print $fh "G90 ; use absolute coordinates\n"; + print $fh "G90 ; use absolute coordinates\n" if $Slic3r::Config->gcode_flavor ne 'makerware'; if ($Slic3r::Config->gcode_flavor =~ /^(?:reprap|teacup)$/) { printf $fh $gcodegen->reset_e; - if ($Slic3r::Config->gcode_flavor =~ /^(?:reprap|makerbot|sailfish)$/) { - if ($Slic3r::Config->use_relative_e_distances) { - print $fh "M83 ; use relative distances for extrusion\n"; - } else { - print $fh "M82 ; use absolute distances for extrusion\n"; - } + if ($Slic3r::Config->use_relative_e_distances) { + print $fh "M83 ; use relative distances for extrusion\n"; + } else { + print $fh "M82 ; use absolute distances for extrusion\n"; } } @@ -813,190 +780,19 @@ sub write_gcode { )); } - # prepare the SpiralVase processor if it's possible - my $spiralvase = $Slic3r::Config->spiral_vase - ? Slic3r::GCode::SpiralVase->new - : undef; - - # prepare the logic to print one layer - my $skirt_done = 0; # count of skirt layers done - my $brim_done = 0; - my $second_layer_things_done = 0; - my $last_obj_copy = ""; - my $extrude_layer = sub { - my ($layer, $object_copies) = @_; - my $gcode = ""; - - if (!$second_layer_things_done && $layer->id == 1) { - for my $t (grep $self->extruders->[$_], 0 .. $#{$Slic3r::Config->temperature}) { - $gcode .= $gcodegen->set_temperature($self->extruders->[$t]->temperature, 0, $t) - if $self->extruders->[$t]->temperature && $self->extruders->[$t]->temperature != $self->extruders->[$t]->first_layer_temperature; - } - $gcode .= $gcodegen->set_bed_temperature($Slic3r::Config->bed_temperature) - if $Slic3r::Config->bed_temperature && $Slic3r::Config->bed_temperature != $Slic3r::Config->first_layer_bed_temperature; - $second_layer_things_done = 1; - } - - # set new layer, but don't move Z as support material contact areas may need an intermediate one - $gcode .= $gcodegen->change_layer($layer); - - # prepare callback to call as soon as a Z command is generated - $gcodegen->move_z_callback(sub { - $gcodegen->move_z_callback(undef); # circular ref or not? - return "" if !$Slic3r::Config->layer_gcode; - return $Slic3r::Config->replace_options($Slic3r::Config->layer_gcode) . "\n"; - }); - - # extrude skirt - if ($skirt_done < $Slic3r::Config->skirt_height) { - $gcodegen->set_shift(@shift); - $gcode .= $gcodegen->set_extruder($self->extruders->[0]); # move_z requires extruder - $gcode .= $gcodegen->move_z($gcodegen->layer->print_z); - # skip skirt if we have a large brim - if ($layer->id < $Slic3r::Config->skirt_height) { - # distribute skirt loops across all extruders - for my $i (0 .. $#{$self->skirt}) { - # when printing layers > 0 ignore 'min_skirt_length' and - # just use the 'skirts' setting; also just use the current extruder - last if ($layer->id > 0) && ($i >= $Slic3r::Config->skirts); - $gcode .= $gcodegen->set_extruder($self->extruders->[ ($i/@{$self->extruders}) % @{$self->extruders} ]) - if $layer->id == 0; - $gcode .= $gcodegen->extrude_loop($self->skirt->[$i], 'skirt'); - } - } - $skirt_done++; - $gcodegen->straight_once(1); - } - - # extrude brim - if (!$brim_done) { - $gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]); # move_z requires extruder - $gcode .= $gcodegen->move_z($gcodegen->layer->print_z); - $gcodegen->set_shift(@shift); - $gcode .= $gcodegen->extrude_loop($_, 'brim') for @{$self->brim}; - $brim_done = 1; - $gcodegen->straight_once(1); - } - - for my $copy (@$object_copies) { - $gcodegen->new_object(1) if $last_obj_copy && $last_obj_copy ne "$copy"; - $last_obj_copy = "$copy"; - - $gcodegen->set_shift(map $shift[$_] + unscale $copy->[$_], X,Y); - - # extrude support material before other things because it might use a lower Z - # and also because we avoid travelling on other things when printing it - if ($self->has_support_material) { - $gcode .= $gcodegen->move_z($layer->support_material_contact_z) - if ($layer->support_contact_fills && @{ $layer->support_contact_fills->paths }); - $gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]); - if ($layer->support_contact_fills) { - $gcode .= $gcodegen->extrude_path($_, 'support material contact area') - for $layer->support_contact_fills->chained_path($gcodegen->last_pos); - } - - $gcode .= $gcodegen->move_z($layer->print_z); - if ($layer->support_fills) { - $gcode .= $gcodegen->extrude_path($_, 'support material') - for $layer->support_fills->chained_path($gcodegen->last_pos); - } - } - - # set actual Z - this will force a retraction - $gcode .= $gcodegen->move_z($layer->print_z); - - # tweak region ordering to save toolchanges - my @region_ids = 0 .. ($self->regions_count-1); - if ($gcodegen->multiple_extruders) { - my $last_extruder = $gcodegen->extruder; - my $best_region_id = first { $self->regions->[$_]->extruders->{perimeter} eq $last_extruder } @region_ids; - @region_ids = ($best_region_id, grep $_ != $best_region_id, @region_ids) if $best_region_id; - } - - foreach my $region_id (@region_ids) { - my $layerm = $layer->regions->[$region_id]; - my $region = $self->regions->[$region_id]; - - my @islands = (); - if ($Slic3r::Config->avoid_crossing_perimeters) { - push @islands, map +{ perimeters => [], fills => [] }, @{$layer->slices}; - PERIMETER: foreach my $perimeter (@{$layerm->perimeters}) { - my $p = $perimeter->unpack; - for my $i (0 .. $#{$layer->slices}-1) { - if ($layer->slices->[$i]->contour->encloses_point($p->first_point)) { - push @{ $islands[$i]{perimeters} }, $p; - next PERIMETER; - } - } - push @{ $islands[-1]{perimeters} }, $p; # optimization - } - FILL: foreach my $fill (@{$layerm->fills}) { - my $f = $fill->unpack; - for my $i (0 .. $#{$layer->slices}-1) { - if ($layer->slices->[$i]->contour->encloses_point($f->first_point)) { - push @{ $islands[$i]{fills} }, $f; - next FILL; - } - } - push @{ $islands[-1]{fills} }, $f; # optimization - } - } else { - push @islands, { - perimeters => $layerm->perimeters, - fills => $layerm->fills, - }; - } - - foreach my $island (@islands) { - my $extrude_perimeters = sub { - return if !@{ $island->{perimeters} }; - $gcode .= $gcodegen->set_extruder($region->extruders->{perimeter}); - $gcode .= $gcodegen->extrude($_, 'perimeter') for @{ $island->{perimeters} }; - }; - - my $extrude_fills = sub { - return if !@{ $island->{fills} }; - $gcode .= $gcodegen->set_extruder($region->extruders->{infill}); - for my $fill (@{ $island->{fills} }) { - if ($fill->isa('Slic3r::ExtrusionPath::Collection')) { - $gcode .= $gcodegen->extrude($_, 'fill') - for $fill->chained_path($gcodegen->last_pos); - } else { - $gcode .= $gcodegen->extrude($fill, 'fill') ; - } - } - }; - - # give priority to infill if we were already using its extruder and it wouldn't - # be good for perimeters - if ($Slic3r::Config->infill_first - || ($gcodegen->multiple_extruders && $region->extruders->{infill} eq $gcodegen->extruder) && $region->extruders->{infill} ne $region->extruders->{perimeter}) { - $extrude_fills->(); - $extrude_perimeters->(); - } else { - $extrude_perimeters->(); - $extrude_fills->(); - } - } - } - } - - # apply spiral vase post-processing if this layer contains suitable geometry - $gcode = $spiralvase->process_layer($gcode, $layer) - if defined $spiralvase - && ($layer->id > 0 || $Slic3r::Config->brim_width == 0) - && ($layer->id >= $Slic3r::Config->skirt_height) - && ($layer->id >= $Slic3r::Config->bottom_solid_layers); - - return $gcode; - }; + # prepare the layer processor + my $layer_gcode = Slic3r::GCode::Layer->new( + print => $self, + gcodegen => $gcodegen, + shift => \@shift, + ); # do all objects for each layer if ($Slic3r::Config->complete_objects) { # print objects from the smallest to the tallest to avoid collisions # when moving onto next object starting point - my @obj_idx = sort { $self->objects->[$a]->layer_count <=> $self->objects->[$b]->layer_count } 0..$#{$self->objects}; + my @obj_idx = sort { $self->objects->[$a]->size->[Z] <=> $self->objects->[$b]->size->[Z] } 0..$#{$self->objects}; my $finished_objects = 0; for my $obj_idx (@obj_idx) { @@ -1024,19 +820,45 @@ sub write_gcode { if $Slic3r::Config->first_layer_bed_temperature; $print_first_layer_temperature->(); } - print $fh $buffer->append($extrude_layer->($layer, [$copy]), $layer); + print $fh $buffer->append( + $layer_gcode->process_layer($layer, [$copy]), + $layer->object."", + $layer->id, + $layer->print_z, + ); } print $fh $buffer->flush; $finished_objects++; } } } else { + # order objects using a nearest neighbor search + my @obj_idx = chained_path([ map $_->copies->[0], @{$self->objects} ]); + + # sort layers by Z + my %layers = (); # print_z => [ layer, layer, layer ] by obj_idx + foreach my $obj_idx (0 .. $#{$self->objects}) { + foreach my $layer (@{$self->objects->[$obj_idx]->layers}) { + $layers{ $layer->print_z } ||= []; + $layers{ $layer->print_z }[$obj_idx] = $layer; # turn this into [$layer] when merging support layers + } + } + my $buffer = Slic3r::GCode::CoolingBuffer->new( config => $Slic3r::Config, gcodegen => $gcodegen, ); - print $fh $buffer->append($extrude_layer->($_, $_->object->copies), $_) - for sort { $a->print_z <=> $b->print_z } map @{$_->layers}, @{$self->objects}; + foreach my $print_z (sort { $a <=> $b } keys %layers) { + foreach my $obj_idx (@obj_idx) { + next unless my $layer = $layers{$print_z}[$obj_idx]; + print $fh $buffer->append( + $layer_gcode->process_layer($layer, $layer->object->copies), + $layer->object."", + $layer->id, + $layer->print_z, + ); + } + } print $fh $buffer->flush; } diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm index 7b21db8745..1143a23972 100644 --- a/lib/Slic3r/Print/Object.pm +++ b/lib/Slic3r/Print/Object.pm @@ -4,16 +4,18 @@ use Moo; use List::Util qw(min sum first); use Slic3r::ExtrusionPath ':roles'; use Slic3r::Geometry qw(Z PI scale unscale deg2rad rad2deg scaled_epsilon chained_path_points); -use Slic3r::Geometry::Clipper qw(diff_ex intersection_ex union_ex offset collapse_ex); +use Slic3r::Geometry::Clipper qw(diff_ex intersection_ex union_ex offset collapse_ex + offset2 diff intersection); use Slic3r::Surface ':types'; has 'print' => (is => 'ro', weak_ref => 1, required => 1); has 'input_file' => (is => 'rw', required => 0); has 'meshes' => (is => 'rw', default => sub { [] }); # by region_id -has 'size' => (is => 'rw', required => 1); -has 'copies' => (is => 'rw', default => sub {[ [0,0] ]}, trigger => 1); +has 'size' => (is => 'rw', required => 1); # XYZ in scaled coordinates +has 'copies' => (is => 'rw', trigger => 1); # in scaled coordinates has 'layers' => (is => 'rw', default => sub { [] }); has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] +has 'fill_maker' => (is => 'lazy'); sub BUILD { my $self = shift; @@ -76,6 +78,12 @@ sub BUILD { } } +sub _build_fill_maker { + my $self = shift; + return Slic3r::Fill->new(object => $self); +} + +# This should be probably moved in Print.pm at the point where we sort Layer objects sub _trigger_copies { my $self = shift; return unless @{$self->copies} > 1; @@ -126,6 +134,13 @@ sub get_layer_range { return ($min_layer, $max_layer); } +sub bounding_box { + my $self = shift; + + # since the object is aligned to origin, bounding box coincides with size + return Slic3r::Geometry::bounding_box([ [0,0], $self->size ]); +} + sub slice { my $self = shift; my %params = @_; @@ -166,14 +181,15 @@ sub slice { } }, ); + + $self->meshes->[$region_id] = undef; # free memory } - die "Invalid input file\n" if !@{$self->layers}; # free memory $self->meshes(undef); # remove last layer(s) if empty - pop @{$self->layers} while !map @{$_->lines}, @{$self->layers->[-1]->regions}; + pop @{$self->layers} while @{$self->layers} && (!map @{$_->lines}, @{$self->layers->[-1]->regions}); foreach my $layer (@{ $self->layers }) { # make sure all layers contain layer region objects for all regions @@ -260,9 +276,6 @@ sub slice { $self->layers->[$i]->id($i); } } - - warn "No layers were detected. You might want to repair your STL file and retry.\n" - if !@{$self->layers}; } sub make_perimeters { @@ -284,19 +297,17 @@ sub make_perimeters { my $overlap = $perimeter_spacing; # one perimeter - my $diff = diff_ex( + my $diff = diff( [ offset([ map @{$_->expolygon}, @{$layerm->slices} ], -($Slic3r::Config->perimeters * $perimeter_spacing)) ], [ offset([ map @{$_->expolygon}, @{$upper_layerm->slices} ], -$overlap) ], ); next if !@$diff; # if we need more perimeters, $diff should contain a narrow region that we can collapse - $diff = diff_ex( - [ map @$_, @$diff ], - [ offset( - [ offset([ map @$_, @$diff ], -$perimeter_spacing) ], - +$perimeter_spacing - ) ], + $diff = diff( + $diff, + [ offset2($diff, -$perimeter_spacing, +$perimeter_spacing) ], + 1, ); next if !@$diff; # diff contains the collapsed area @@ -307,18 +318,14 @@ sub make_perimeters { # compute polygons representing the thickness of the hypotetical new internal perimeter # of our slice $extra_perimeters++; - my $hypothetical_perimeter = diff_ex( + my $hypothetical_perimeter = diff( [ offset($slice->expolygon, -($perimeter_spacing * ($Slic3r::Config->perimeters + $extra_perimeters-1))) ], [ offset($slice->expolygon, -($perimeter_spacing * ($Slic3r::Config->perimeters + $extra_perimeters))) ], ); last CYCLE if !@$hypothetical_perimeter; # no extra perimeter is possible # only add the perimeter if there's an intersection with the collapsed area - my $intersection = intersection_ex( - [ map @$_, @$diff ], - [ map @$_, @$hypothetical_perimeter ], - ); - last CYCLE if !@$intersection; + last CYCLE if !@{ intersection($diff, $hypothetical_perimeter) }; Slic3r::debugf " adding one more perimeter at layer %d\n", $layer_id; $slice->extra_perimeters($extra_perimeters); } @@ -376,7 +383,6 @@ sub detect_surfaces_type { 1, ); return map Slic3r::Surface->new(expolygon => $_, surface_type => $result_type), - grep $_->is_printable($layerm->perimeter_flow->scaled_width), @$expolygons; }; @@ -590,7 +596,8 @@ sub discover_horizontal_shells { for (my $i = 0; $i < $self->layer_count; $i++) { my $layerm = $self->layers->[$i]->regions->[$region_id]; - if ($Slic3r::Config->solid_infill_every_layers && ($i % $Slic3r::Config->solid_infill_every_layers) == 0) { + if ($Slic3r::Config->solid_infill_every_layers && $Slic3r::Config->fill_density > 0 + && ($i % $Slic3r::Config->solid_infill_every_layers) == 0) { $_->surface_type(S_TYPE_INTERNALSOLID) for grep $_->surface_type == S_TYPE_INTERNAL, @{$layerm->fill_surfaces}; } @@ -928,7 +935,7 @@ sub generate_support_material { push @angles, $angles[0] + 90; } - my $filler = $self->print->fill_maker->filler($pattern); + my $filler = $self->fill_maker->filler($pattern); my $make_pattern = sub { my ($expolygon, $density) = @_; @@ -1013,7 +1020,7 @@ sub generate_support_material { # make a solid base on bottom layer if ($layer_id == 0) { - my $filler = $self->print->fill_maker->filler('rectilinear'); + my $filler = $self->fill_maker->filler('rectilinear'); $filler->angle($Slic3r::Config->support_material_angle + 90); foreach my $expolygon (@$islands) { my @paths = $filler->fill_surface( diff --git a/lib/Slic3r/SVG.pm b/lib/Slic3r/SVG.pm index a79094854f..0ac2863951 100644 --- a/lib/Slic3r/SVG.pm +++ b/lib/Slic3r/SVG.pm @@ -9,7 +9,7 @@ use constant Y => 1; our $filltype = 'evenodd'; -sub factor {return 30; +sub factor { return &Slic3r::SCALING_FACTOR * 10; } diff --git a/lib/Slic3r/Test.pm b/lib/Slic3r/Test.pm index d6a10f374a..f645e38217 100644 --- a/lib/Slic3r/Test.pm +++ b/lib/Slic3r/Test.pm @@ -16,7 +16,7 @@ my %cuboids = ( ); sub model { - my ($model_name) = @_; + my ($model_name, %params) = @_; my ($vertices, $facets); if ($cuboids{$model_name}) { @@ -27,10 +27,22 @@ sub model { $facets = [ [0,1,2], [0,2,3], [4,5,6], [4,6,7], [0,4,7], [0,7,1], [1,7,6], [1,6,2], [2,6,5], [2,5,3], [4,0,3], [4,3,5], ], + } elsif ($model_name eq 'V') { + $vertices = [ + [-14,0,20],[-14,15,20],[0,0,0],[0,15,0],[-4,0,20],[-4,15,20],[5,0,7.14286],[10,0,0],[24,0,20],[14,0,20],[10,15,0],[5,15,7.14286],[14,15,20],[24,15,20] + ]; + $facets = [ + [0,1,2],[2,1,3],[1,0,4],[5,1,4],[4,0,2],[6,4,2],[7,6,2],[8,9,7],[9,6,7],[2,3,7],[7,3,10],[1,5,3],[3,5,11],[11,12,13],[11,13,3],[3,13,10],[5,4,6],[11,5,6],[6,9,11],[11,9,12],[12,9,8],[13,12,8],[8,7,10],[13,8,10] + ], } my $model = Slic3r::Model->new; - $model->add_object(vertices => $vertices)->add_volume(facets => $facets); + my $object = $model->add_object(vertices => $vertices); + $object->add_volume(facets => $facets); + $object->add_instance( + offset => [0,0], + rotation => $params{rotation} // 0, + ); return $model; } @@ -42,7 +54,9 @@ sub init_print { $config->set('gcode_comments', 1) if $ENV{SLIC3R_TESTS_GCODE}; my $print = Slic3r::Print->new(config => $config); - $print->add_model(model($model_name)); + + $model_name = [$model_name] if ref($model_name) ne 'ARRAY'; + $print->add_model(model($_, %params)) for @$model_name; $print->validate; return $print; diff --git a/lib/Slic3r/TriangleMesh.pm b/lib/Slic3r/TriangleMesh.pm index f093a3fe21..a58f3c0e60 100644 --- a/lib/Slic3r/TriangleMesh.pm +++ b/lib/Slic3r/TriangleMesh.pm @@ -10,9 +10,9 @@ has 'vertices' => (is => 'ro', required => 1); # id => [$x,$y,$z] has 'facets' => (is => 'ro', required => 1); # id => [ $v1_id, $v2_id, $v3_id ] # private -has 'edges' => (is => 'ro', default => sub { [] }); # id => [ $v1_id, $v2_id ] -has 'facets_edges' => (is => 'ro', default => sub { [] }); # id => [ $e1_id, $e2_id, $e3_id ] -has 'edges_facets' => (is => 'ro', default => sub { [] }); # id => [ $f1_id, $f2_id, (...) ] +has 'edges' => (is => 'rw'); # id => [ $v1_id, $v2_id ] +has 'facets_edges' => (is => 'rw'); # id => [ $e1_id, $e2_id, $e3_id ] +has 'edges_facets' => (is => 'rw'); # id => [ $f1_id, $f2_id, (...) ] use constant MIN => 0; use constant MAX => 1; @@ -29,13 +29,13 @@ use constant I_FACET_EDGE => 6; use constant FE_TOP => 0; use constant FE_BOTTOM => 1; -# always make sure this method is idempotent sub analyze { my $self = shift; - @{$self->edges} = (); - @{$self->facets_edges} = (); - @{$self->edges_facets} = (); + return if defined $self->edges; + $self->edges([]); + $self->facets_edges([]); + $self->edges_facets([]); my %table = (); # edge_coordinates => edge_id for (my $facet_id = 0; $facet_id <= $#{$self->facets}; $facet_id++) { @@ -324,14 +324,14 @@ sub make_loops { sub rotate { my $self = shift; - my ($deg) = @_; + my ($deg, $center) = @_; return if $deg == 0; my $rad = Slic3r::Geometry::deg2rad($deg); # transform vertex coordinates foreach my $vertex (@{$self->vertices}) { - @$vertex = (@{ +(Slic3r::Geometry::rotate_points($rad, undef, [ $vertex->[X], $vertex->[Y] ]))[0] }, $vertex->[Z]); + @$vertex = (@{ +(Slic3r::Geometry::rotate_points($rad, $center, [ $vertex->[X], $vertex->[Y] ]))[0] }, $vertex->[Z]); } } @@ -365,6 +365,19 @@ sub align_to_origin { $self->move(map -$extents[$_][MIN], X,Y,Z); } +sub center_around_origin { + my $self = shift; + + $self->move(map -$_, @{ $self->center }); +} + +sub center { + my $self = shift; + + my @extents = $self->extents; + return [ map +($extents[$_][MAX] + $extents[$_][MIN])/2, X,Y,Z ]; +} + sub duplicate { my $self = shift; my (@shifts) = @_; @@ -385,14 +398,24 @@ sub duplicate { $self->BUILD; } +sub used_vertices { + my $self = shift; + return [ map $self->vertices->[$_], map @$_, @{$self->facets} ]; +} + sub extents { my $self = shift; - return Slic3r::Geometry::bounding_box_3D($self->vertices); + return Slic3r::Geometry::bounding_box_3D($self->used_vertices); +} + +sub bounding_box { + my $self = shift; + return Slic3r::Geometry::BoundingBox->new(extents => [ $self->extents ]); } sub size { my $self = shift; - return Slic3r::Geometry::size_3D($self->vertices); + return Slic3r::Geometry::size_3D($self->used_vertices); } sub slice_facet { diff --git a/slic3r.pl b/slic3r.pl index b3dec4969c..0b05cd0436 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -26,6 +26,7 @@ my %cli_options = (); 'save=s' => \$opt{save}, 'load=s@' => \$opt{load}, + 'autosave=s' => \$opt{autosave}, 'ignore-nonexistent-config' => \$opt{ignore_nonexistent_config}, 'no-plater' => \$opt{no_plater}, 'gui-mode=s' => \$opt{gui_mode}, @@ -78,6 +79,7 @@ if (!@ARGV && !$opt{save} && eval "require Slic3r::GUI; 1") { $Slic3r::GUI::datadir = $opt{datadir}; $Slic3r::GUI::no_plater = $opt{no_plater}; $Slic3r::GUI::mode = $opt{gui_mode}; + $Slic3r::GUI::autosave = $opt{autosave}; } $gui = Slic3r::GUI->new; $gui->{skeinpanel}->load_config_file($_) for @{$opt{load}}; @@ -91,13 +93,19 @@ if (@ARGV) { # slicing from command line $config->validate; while (my $input_file = shift @ARGV) { - my $print = Slic3r::Print->new(config => $config); - $print->add_model(Slic3r::Model->read_from_file($input_file)); + my $model; if ($opt{merge}) { - $print->add_model(Slic3r::Model->read_from_file($_)) for splice @ARGV, 0; + my @models = map Slic3r::Model->read_from_file($_), $input_file, (splice @ARGV, 0); + $model = Slic3r::Model->merge(@models); + } else { + $model = Slic3r::Model->read_from_file($input_file); } - $print->duplicate; - $print->arrange_objects if @{$print->objects} > 1; + $_->scale($config->scale) for @{$model->objects}; + $_->rotate($config->rotate) for @{$model->objects}; + $model->arrange_objects($config); + + my $print = Slic3r::Print->new(config => $config); + $print->add_model($model); $print->validate; my %params = ( output_file => $opt{output}, @@ -146,6 +154,7 @@ $j GUI options: --no-plater Disable the plater tab --gui-mode Overrides the configured mode (simple/expert) + --autosave Automatically export current configuration to the specified file Output options: --output-filename-format @@ -164,7 +173,7 @@ $j (default: $config->{print_center}->[0],$config->{print_center}->[1]) --z-offset Additional height in mm to add to vertical coordinates (+/-, default: $config->{z_offset}) - --gcode-flavor The type of G-code to generate (reprap/teacup/makerbot/sailfish/mach3/no-extrusion, + --gcode-flavor The type of G-code to generate (reprap/teacup/makerware/sailfish/mach3/no-extrusion, default: $config->{gcode_flavor}) --use-relative-e-distances Enable this to get relative E values --gcode-arcs Use G2/G3 commands for native arcs (experimental, not supported diff --git a/t/arcs.t b/t/arcs.t index 972620402b..3b14903c84 100644 --- a/t/arcs.t +++ b/t/arcs.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 12; +plan tests => 13; BEGIN { use FindBin; @@ -20,7 +20,7 @@ use Slic3r::Geometry qw(scaled_epsilon scale X Y); [306517.1,219034.23], [286979.42,248012.49], [258001.16,267550.17], [222515.14,274714.47], [187029.11,267550.17], [158050.85,248012.49], [138513.17,219034.23], [131348.87,183548.2], [86948.77,175149.09], [119825.35,100585], - ), role => EXTR_ROLE_FILL); + ), role => EXTR_ROLE_FILL, flow_spacing => 0.5); my $collection = Slic3r::ExtrusionPath::Collection->new(paths => [$path]); $collection->detect_arcs(30); @@ -42,10 +42,12 @@ use Slic3r::Geometry qw(scaled_epsilon scale X Y); my $path1 = Slic3r::ExtrusionPath->new( polyline => Slic3r::Polyline->new(@points), role => EXTR_ROLE_FILL, + flow_spacing => 0.5, ); my $path2 = Slic3r::ExtrusionPath->new( polyline => Slic3r::Polyline->new(reverse @points), role => EXTR_ROLE_FILL, + flow_spacing => 0.5, ); my $collection1 = Slic3r::ExtrusionPath::Collection->new(paths => [$path1]); @@ -66,6 +68,7 @@ use Slic3r::Geometry qw(scaled_epsilon scale X Y); is $collection1->paths->[0]->orientation, 'cw', 'cw orientation was correctly detected'; is $collection2->paths->[0]->orientation, 'ccw', 'ccw orientation was correctly detected'; + is $collection1->paths->[0]->flow_spacing, $path1->flow_spacing, 'flow spacing was correctly preserved'; my $center1 = [ map sprintf('%.0f', $_), @{ $collection1->paths->[0]->center } ]; ok abs($center1->[X] - scale 10) < scaled_epsilon && abs($center1->[Y] - scale 10) < scaled_epsilon, 'center was correctly detected'; diff --git a/t/cooling.t b/t/cooling.t new file mode 100644 index 0000000000..27cd59b7a3 --- /dev/null +++ b/t/cooling.t @@ -0,0 +1,89 @@ +use Test::More; +use strict; +use warnings; + +plan tests => 8; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Slic3r; + +sub buffer { + my $config = shift || Slic3r::Config->new_from_defaults; + my $buffer = Slic3r::GCode::CoolingBuffer->new( + config => $config, + gcodegen => Slic3r::GCode->new(config => $config, layer_count => 10), + ); + return $buffer; +} + +my $config = Slic3r::Config->new_from_defaults; +$config->set('disable_fan_first_layers', 0); + +{ + my $buffer = buffer($config); + $buffer->gcodegen->elapsed_time($buffer->config->slowdown_below_layer_time + 1); + my $gcode = $buffer->append('G1 X100 E1 F3000', 0, 0, 0.4) . $buffer->flush; + like $gcode, qr/F3000/, 'speed is not altered when elapsed time is greater than slowdown threshold'; +} + +{ + my $buffer = buffer($config); + $buffer->gcodegen->elapsed_time($buffer->config->slowdown_below_layer_time - 1); + my $gcode = $buffer->append("G1 X50 F2500\nG1 X100 E1 F3000\nG1 E4 F400", 0, 0, 0.4) . $buffer->flush; + unlike $gcode, qr/F3000/, 'speed is altered when elapsed time is lower than slowdown threshold'; + like $gcode, qr/F2500/, 'speed is not altered for travel moves'; + like $gcode, qr/F400/, 'speed is not altered for extruder-only moves'; +} + +{ + my $buffer = buffer($config); + $buffer->gcodegen->elapsed_time($buffer->config->fan_below_layer_time + 1); + my $gcode = $buffer->append('G1 X100 E1 F3000', 0, 0, 0.4) . $buffer->flush; + unlike $gcode, qr/M106/, 'fan is not activated when elapsed time is greater than fan threshold'; +} + +{ + my $buffer = buffer($config); + my $gcode = ""; + for my $obj_id (0 .. 1) { + # use an elapsed time which is < the slowdown threshold but greater than it when summed twice + $buffer->gcodegen->elapsed_time($buffer->config->slowdown_below_layer_time - 1); + $gcode .= $buffer->append("G1 X100 E1 F3000\n", $obj_id, 0, 0.4); + } + $gcode .= $buffer->flush; + like $gcode, qr/F3000/, 'slowdown is computed on all objects printing at same Z'; +} + +{ + my $buffer = buffer($config); + my $gcode = ""; + for my $layer_id (0 .. 1) { + for my $obj_id (0 .. 1) { + # use an elapsed time which is < the threshold but greater than it when summed twice + $buffer->gcodegen->elapsed_time($buffer->config->fan_below_layer_time - 1); + $gcode .= $buffer->append("G1 X100 E1 F3000\n", $obj_id, $layer_id, 0.4 + 0.4*$layer_id + 0.1*$obj_id); # print same layer at distinct heights + } + } + $gcode .= $buffer->flush; + unlike $gcode, qr/M106/, 'fan activation is computed on all objects printing at different Z'; +} + +{ + my $buffer = buffer($config); + my $gcode = ""; + for my $layer_id (0 .. 1) { + for my $obj_id (0 .. 1) { + # use an elapsed time which is < the threshold even when summed twice + $buffer->gcodegen->elapsed_time($buffer->config->fan_below_layer_time/2 - 1); + $gcode .= $buffer->append("G1 X100 E1 F3000\n", $obj_id, $layer_id, 0.4 + 0.4*$layer_id + 0.1*$obj_id); # print same layer at distinct heights + } + } + $gcode .= $buffer->flush; + like $gcode, qr/M106/, 'fan activation is computed on all objects printing at different Z'; +} + +__END__ diff --git a/t/fill.t b/t/fill.t index e0fdd3cc08..4665a0c494 100644 --- a/t/fill.t +++ b/t/fill.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 9; +plan tests => 10; BEGIN { use FindBin; @@ -109,14 +109,17 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ } $config->set('top_solid_layers', 0); $config->set('bottom_solid_layers', 0); $config->set('solid_infill_below_area', 20000000); + $config->set('solid_infill_every_layers', 2); my $print = Slic3r::Test::init_print('20mm_cube', config => $config); + my %layers_with_extrusion = (); Slic3r::GCode::Reader->new(gcode => Slic3r::Test::gcode($print))->parse(sub { my ($self, $cmd, $args, $info) = @_; - - fail "solid_infill_below_area should be ignored when fill_density is 0" - if $info->{extruding}; + $layers_with_extrusion{$self->Z} = 1 if $info->{extruding}; }); + + ok !%layers_with_extrusion, + "solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0"; } __END__ diff --git a/t/gcode.t b/t/gcode.t index c41307d4fc..61c940faf2 100644 --- a/t/gcode.t +++ b/t/gcode.t @@ -11,8 +11,10 @@ use Slic3r; use Slic3r::Geometry qw(scale); { - local $Slic3r::Config = Slic3r::Config->new_from_defaults; - my $gcodegen = Slic3r::GCode->new(layer_count => 1); + my $gcodegen = Slic3r::GCode->new( + config => Slic3r::Config->new_from_defaults, + layer_count => 1, + ); $gcodegen->set_shift(10, 10); is_deeply $gcodegen->last_pos, [scale -10, scale -10], 'last_pos is shifted correctly'; } diff --git a/t/geometry.t b/t/geometry.t index c0e1f753ba..edbe7262a3 100644 --- a/t/geometry.t +++ b/t/geometry.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 23; +plan tests => 24; BEGIN { use FindBin; @@ -173,4 +173,12 @@ is Slic3r::Geometry::can_connect_points(@$points, $polygons), 0, 'can_connect_po is_deeply $result, [ [10, 0], [5, 5], [0, 0], [10, 0] ], 'split_at_index'; } -#========================================================== \ No newline at end of file +#========================================================== + +{ + my $bb = Slic3r::Geometry::BoundingBox->new_from_points([ [0, 1], [10, 2], [20, 2] ]); + $bb->scale(2); + is_deeply $bb->extents, [ [0,40], [2,4] ], 'bounding box is scaled correctly'; +} + +#========================================================== diff --git a/t/print.t b/t/print.t new file mode 100644 index 0000000000..485ad12bbb --- /dev/null +++ b/t/print.t @@ -0,0 +1,20 @@ +use Test::More tests => 1; +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use List::Util qw(first); +use Slic3r; +use Slic3r::Test; + +{ + my $print = Slic3r::Test::init_print('20mm_cube', rotation => 45); + ok !(first { $_ < 0 } map @$_, map @{$_->used_vertices}, grep $_, map @{$_->meshes}, @{$print->objects}), + "object is still in positive coordinate space even after rotation"; +} + +__END__ diff --git a/t/shells.t b/t/shells.t index 7977f491e9..56a3ca5950 100644 --- a/t/shells.t +++ b/t/shells.t @@ -1,4 +1,4 @@ -use Test::More tests => 2; +use Test::More tests => 3; use strict; use warnings; @@ -45,4 +45,27 @@ use Slic3r::Test; ok $test->(), "proper number of shells is applied even when fill density is none"; } +# issue #1161 +{ + my $config = Slic3r::Config->new_from_defaults; + $config->set('layer_height', 0.3); + $config->set('first_layer_height', '100%'); + $config->set('bottom_solid_layers', 0); + $config->set('top_solid_layers', 3); + $config->set('cooling', 0); + $config->set('solid_infill_speed', 99); + $config->set('top_solid_infill_speed', 99); + + my $print = Slic3r::Test::init_print('V', config => $config); + my %layers_with_solid_infill = (); # Z => 1 + Slic3r::GCode::Reader->new(gcode => Slic3r::Test::gcode($print))->parse(sub { + my ($self, $cmd, $args, $info) = @_; + + $layers_with_solid_infill{$self->Z} = 1 + if $info->{extruding} && ($args->{F} // $self->F) == $config->solid_infill_speed*60; + }); + is scalar(map $layers_with_solid_infill{$_}, grep $_ <= 7.2, keys %layers_with_solid_infill), 3, + "correct number of top solid shells is generated in V-shaped object"; +} + __END__ diff --git a/t/skirt_brim.t b/t/skirt_brim.t new file mode 100644 index 0000000000..c78bf41e0a --- /dev/null +++ b/t/skirt_brim.t @@ -0,0 +1,46 @@ +use Test::More tests => 1; +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use List::Util qw(first); +use Slic3r; +use Slic3r::Test; + +{ + my $config = Slic3r::Config->new_from_defaults; + $config->set('skirts', 1); + $config->set('skirt_height', 2); + $config->set('perimeters', 0); + $config->set('perimeter_speed', 99); + $config->set('cooling', 0); # to prevent speeds to be altered + $config->set('first_layer_speed', '100%'); # to prevent speeds to be altered + + my $test = sub { + my ($conf) = @_; + $conf ||= $config; + + my $print = Slic3r::Test::init_print(['20mm_cube','20mm_cube'], config => $config); + + my %layers_with_skirt = (); # Z => $count + Slic3r::GCode::Reader->new(gcode => Slic3r::Test::gcode($print))->parse(sub { + my ($self, $cmd, $args, $info) = @_; + + if (defined $self->Z) { + $layers_with_skirt{$self->Z} //= 0; + $layers_with_skirt{$self->Z} = 1 + if $info->{extruding} && ($args->{F} // $self->F) == $config->perimeter_speed*60; + } + }); + fail "wrong number of layers with skirt" + unless (grep $_, values %layers_with_skirt) == $config->skirt_height; + }; + + ok $test->(), "skirt_height is honored when printing multiple objects too"; +} + +__END__ diff --git a/t/slice.t b/t/slice.t index cfa78a60c9..e47929f0fc 100644 --- a/t/slice.t +++ b/t/slice.t @@ -16,8 +16,6 @@ my @lines; my $z = 20; my @points = ([3, 4], [8, 5], [1, 9]); # XY coordinates of the facet vertices -my $mesh = Slic3r::TriangleMesh->new(facets => [], vertices => []); - # NOTE: # the first point of the intersection lines is replaced by -1 because TriangleMesh.pm # is saving memory and doesn't store point A anymore since it's not actually needed. @@ -104,21 +102,23 @@ my @upper = intersect(20, 20, 10); is $lower[0][Slic3r::TriangleMesh::I_FACET_EDGE], Slic3r::TriangleMesh::FE_BOTTOM, 'bottom edge on layer'; is $upper[0][Slic3r::TriangleMesh::I_FACET_EDGE], Slic3r::TriangleMesh::FE_TOP, 'upper edge on layer'; +my $mesh; + +sub intersect { + $mesh = Slic3r::TriangleMesh->new( + facets => [], + vertices => [], + ); + push @{$mesh->facets}, [ [0,0,0], @{vertices(@_)} ]; + $mesh->analyze; + return map Slic3r::TriangleMesh::unpack_line($_), $mesh->intersect_facet($#{$mesh->facets}, $z); +} + sub vertices { push @{$mesh->vertices}, map [ @{$points[$_]}, $_[$_] ], 0..2; [ ($#{$mesh->vertices}-2) .. $#{$mesh->vertices} ] } -sub add_facet { - push @{$mesh->facets}, [ [0,0,0], @{vertices(@_)} ]; - $mesh->analyze; -} - -sub intersect { - add_facet(@_); - return map Slic3r::TriangleMesh::unpack_line($_), $mesh->intersect_facet($#{$mesh->facets}, $z); -} - sub lines { my @lines = intersect(@_); #$_->a->[X] = sprintf('%.0f', $_->a->[X]) for @lines; diff --git a/t/support.t b/t/support.t index 1e9a24a6e3..bee1987f1b 100644 --- a/t/support.t +++ b/t/support.t @@ -14,8 +14,24 @@ use Slic3r::Test; my $config = Slic3r::Config->new_from_defaults; $config->set('raft_layers', 3); $config->set('brim_width', 6); + $config->set('skirts', 0); + $config->set('support_material_extruder', 2); + $config->set('layer_height', 0.4); + $config->set('first_layer_height', '100%'); my $print = Slic3r::Test::init_print('20mm_cube', config => $config); - ok Slic3r::Test::gcode($print), 'no conflict between raft/support and brim'; + ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim'; + + my $tool = 0; + Slic3r::GCode::Reader->new(gcode => $gcode)->parse(sub { + my ($self, $cmd, $args, $info) = @_; + + if ($cmd =~ /^T(\d+)/) { + $tool = $1; + } elsif ($info->{extruding} && $self->Z <= ($config->raft_layers * $config->layer_height)) { + fail 'not extruding raft/brim with support material extruder' + if $tool != ($config->support_material_extruder-1); + } + }); } __END__ diff --git a/t/svg.t b/t/svg.t new file mode 100644 index 0000000000..9e2a8dcb19 --- /dev/null +++ b/t/svg.t @@ -0,0 +1,23 @@ +use Test::More tests => 1; +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Slic3r; +use Slic3r::Test; + +{ + my $print = Slic3r::Test::init_print('20mm_cube'); + eval { + my $fh = IO::Scalar->new(\my $gcode); + $print->export_svg(output_fh => $fh, quiet => 1); + $fh->close; + }; + ok !$@, 'successful SVG export'; +} + +__END__ diff --git a/utils/dump-stl.pl b/utils/dump-stl.pl new file mode 100644 index 0000000000..a5c716b839 --- /dev/null +++ b/utils/dump-stl.pl @@ -0,0 +1,34 @@ +#!/usr/bin/perl +# This script dumps a STL file into Perl syntax for writing tests + +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Slic3r; +$|++; + +$ARGV[0] or usage(1); + +{ + my $model = Slic3r::Format::STL->read_file($ARGV[0]); + my $mesh = $model->mesh; + printf "VERTICES = %s\n", join ',', map "[$_->[0],$_->[1],$_->[2]]", @{$mesh->vertices}; + printf "FACETS = %s\n", join ',', map "[$_->[0],$_->[1],$_->[2]]", @{$mesh->facets}; +} + + +sub usage { + my ($exit_code) = @_; + + print <<"EOF"; +Usage: dump-stl.pl file.stl +EOF + exit ($exit_code || 0); +} + +__END__ diff --git a/utils/view-mesh.pl b/utils/view-mesh.pl new file mode 100644 index 0000000000..5d69525d02 --- /dev/null +++ b/utils/view-mesh.pl @@ -0,0 +1,67 @@ +#!/usr/bin/perl +# This script displays 3D preview of a mesh + +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Getopt::Long qw(:config no_auto_abbrev); +use Slic3r; +use Slic3r::GUI; +$|++; + +my %opt = (); +{ + my %options = ( + 'help' => sub { usage() }, + ); + GetOptions(%options) or usage(1); + $ARGV[0] or usage(1); +} + +{ + my $model = Slic3r::Model->read_from_file($ARGV[0]); + + $Slic3r::ViewMesh::mesh = $model->mesh; + my $app = Slic3r::ViewMesh->new; + $app->MainLoop; +} + + +sub usage { + my ($exit_code) = @_; + + print <<"EOF"; +Usage: view-mesh.pl [ OPTIONS ] file.stl + + --help Output this usage screen and exit + +EOF + exit ($exit_code || 0); +} + +package Slic3r::ViewMesh; +use Wx qw(:sizer); +use base qw(Wx::App); + +our $mesh; + +sub OnInit { + my $self = shift; + + my $frame = Wx::Frame->new(undef, -1, 'Mesh Viewer', [-1, -1], [500, 400]); + my $panel = Wx::Panel->new($frame, -1); + + my $sizer = Wx::BoxSizer->new(wxVERTICAL); + $sizer->Add(Slic3r::GUI::PreviewCanvas->new($panel, $mesh), 1, wxEXPAND, 0); + $panel->SetSizer($sizer); + $sizer->SetSizeHints($panel); + + $frame->Show(1); +} + +__END__ diff --git a/utils/zsh/functions/_slic3r b/utils/zsh/functions/_slic3r index 41b2594bd8..10a0ce05b0 100644 --- a/utils/zsh/functions/_slic3r +++ b/utils/zsh/functions/_slic3r @@ -22,7 +22,7 @@ _arguments -S \ '*--nozzle-diameter[specify nozzle diameter]:nozzle diameter in mm' \ '--print-center[specify print center coordinates]:print center coordinates in mm,mm' \ '--z-offset[specify Z-axis offset]:Z-axis offset in mm' \ - '--gcode-flavor[specify the type of G-code to generate]:G-code flavor:(reprap teacup makerbot sailfish mach3 no-extrusion)' \ + '--gcode-flavor[specify the type of G-code to generate]:G-code flavor:(reprap teacup makerware sailfish mach3 no-extrusion)' \ '(--use-relative-e-distances --no-use-relative-e-distances)'--{no-,}use-relative-e-distances'[disable/enable relative E values]' \ '--extrusion-axis[specify letter associated with the extrusion axis]:extrusion axis letter' \ '(--gcode-arcs --no-gcode-arcs)'--{no-,}gcode-arcs'[disable/enable G2/G3 commands for native arcs]' \