diff --git a/Build.PL b/Build.PL index a7f25c3e1..ead18f55e 100644 --- a/Build.PL +++ b/Build.PL @@ -23,6 +23,7 @@ my %prereqs = qw( Test::More 0 Thread::Semaphore 0 IO::Scalar 0 + threads 1.96 Time::HiRes 0 ); my %recommends = qw( diff --git a/README.md b/README.md index 4e35d5572..653e40246 100644 --- a/README.md +++ b/README.md @@ -351,10 +351,11 @@ The author of the Silk icon set is Mark James. --extruder-offset Offset of each extruder, if firmware doesn't handle the displacement (can be specified multiple times, default: 0x0) --perimeter-extruder - Extruder to use for perimeters (1+, default: 1) + Extruder to use for perimeters and brim (1+, default: 1) --infill-extruder Extruder to use for infill (1+, default: 1) + --solid-infill-extruder Extruder to use for solid infill (1+, default: 1) --support-material-extruder - Extruder to use for support material (1+, default: 1) + Extruder to use for support material, raft and skirt (1+, default: 1) --support-material-interface-extruder Extruder to use for support material interface (1+, default: 1) --ooze-prevention Drop temperature and park extruders outside a full skirt for automatic wiping diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index d9b159118..516eb2996 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -19,14 +19,15 @@ our $have_threads; BEGIN { use Config; $have_threads = $Config{useithreads} && eval "use threads; use threads::shared; use Thread::Queue; 1"; + warn "threads.pm >= 1.96 is required, please update\n" if $have_threads && $threads::VERSION < 1.96; ### temporarily disable threads if using the broken Moo version use Moo; $have_threads = 0 if $Moo::VERSION == 1.003000; } -warn "Running Slic3r under Perl >= 5.16 is not supported nor recommended\n" - if $^V >= v5.16; +warn "Running Slic3r under Perl 5.16 is not supported nor recommended\n" + if $^V == v5.16; use FindBin; our $var = "$FindBin::Bin/var"; @@ -48,7 +49,6 @@ use Slic3r::Format::STL; use Slic3r::GCode; use Slic3r::GCode::ArcFitting; use Slic3r::GCode::CoolingBuffer; -use Slic3r::GCode::Layer; use Slic3r::GCode::MotionPlanner; use Slic3r::GCode::PlaceholderParser; use Slic3r::GCode::PressureRegulator; @@ -65,6 +65,7 @@ use Slic3r::Point; use Slic3r::Polygon; use Slic3r::Polyline; use Slic3r::Print; +use Slic3r::Print::GCode; use Slic3r::Print::Object; use Slic3r::Print::Simple; use Slic3r::Print::SupportMaterial; @@ -79,29 +80,40 @@ use constant SMALL_PERIMETER_LENGTH => (6.5 / SCALING_FACTOR) * 2 * PI; use constant LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER => 0.15; use constant INFILL_OVERLAP_OVER_SPACING => 0.45; use constant EXTERNAL_INFILL_MARGIN => 3; -use constant INSET_OVERLAP_TOLERANCE => 0.2; +use constant INSET_OVERLAP_TOLERANCE => 0.4; # keep track of threads we created +my @my_threads = (); my @threads : shared = (); -my $sema = Thread::Semaphore->new; +my $pause_sema = Thread::Semaphore->new; +my $parallel_sema; my $paused = 0; sub spawn_thread { my ($cb) = @_; + my $parent_tid = threads->tid; + lock @threads; + @_ = (); my $thread = threads->create(sub { + @my_threads = (); + + Slic3r::debugf "Starting thread %d (parent: %d)...\n", threads->tid, $parent_tid; local $SIG{'KILL'} = sub { - Slic3r::debugf "Exiting thread...\n"; + Slic3r::debugf "Exiting thread %d...\n", threads->tid; + $parallel_sema->up if $parallel_sema; + kill_all_threads(); Slic3r::thread_cleanup(); threads->exit(); }; local $SIG{'STOP'} = sub { - $sema->down; - $sema->up; + $pause_sema->down; + $pause_sema->up; }; $cb->(); }); + push @my_threads, $thread->tid; push @threads, $thread->tid; return $thread; } @@ -109,15 +121,21 @@ sub spawn_thread { sub parallelize { my %params = @_; + lock @threads; if (!$params{disable} && $Slic3r::have_threads && $params{threads} > 1) { my @items = (ref $params{items} eq 'CODE') ? $params{items}->() : @{$params{items}}; my $q = Thread::Queue->new; $q->enqueue(@items, (map undef, 1..$params{threads})); + $parallel_sema = Thread::Semaphore->new(-$params{threads}); + $parallel_sema->up; my $thread_cb = sub { # execute thread callback $params{thread_cb}->($q); + # signal the parent thread that we're done + $parallel_sema->up; + # cleanup before terminating thread Slic3r::thread_cleanup(); @@ -129,17 +147,17 @@ sub parallelize { # The downside to using this exit is that we can't return # any value to the main thread but we're not doing that # anymore anyway. - # collect_cb is completely useless now - # and should be removed from the codebase. threads->exit; }; - $params{collect_cb} ||= sub {}; @_ = (); my @my_threads = map spawn_thread($thread_cb), 1..$params{threads}; - foreach my $th (@my_threads) { - $params{collect_cb}->($th->join); - } + + # We use a semaphore instead of $th->join because joined threads are + # not listed by threads->list or threads->object anymore, thus can't + # be signalled. + $parallel_sema->down; + $_->detach for @my_threads; } else { $params{no_threads_cb}->(); } @@ -181,6 +199,7 @@ sub thread_cleanup { *Slic3r::Geometry::BoundingBoxf::DESTROY = sub {}; *Slic3r::Geometry::BoundingBoxf3::DESTROY = sub {}; *Slic3r::Line::DESTROY = sub {}; + *Slic3r::Linef3::DESTROY = sub {}; *Slic3r::Model::DESTROY = sub {}; *Slic3r::Model::Object::DESTROY = sub {}; *Slic3r::Point::DESTROY = sub {}; @@ -198,35 +217,45 @@ sub thread_cleanup { } sub get_running_threads { - return grep defined($_), map threads->object($_), @threads; + return grep defined($_), map threads->object($_), @_; } sub kill_all_threads { - # detach any running thread created in the current one - my @killed = (); - foreach my $thread (get_running_threads()) { - $thread->kill('KILL'); - push @killed, $thread; + # if we're the main thread, we send SIGKILL to all the running threads + if (threads->tid == 0) { + lock @threads; + foreach my $thread (get_running_threads(@threads)) { + Slic3r::debugf "Thread %d killing %d...\n", threads->tid, $thread->tid; + $thread->kill('KILL'); + } + + # unlock semaphore before we block on wait + # otherwise we'd get a deadlock if threads were paused + resume_all_threads(); } - # unlock semaphore before we block on wait - # otherwise we'd get a deadlock if threads were paused - resume_threads(); - $_->join for @killed; # block until threads are killed - @threads = (); + # in any thread we wait for our children + foreach my $thread (get_running_threads(@my_threads)) { + Slic3r::debugf " Thread %d waiting for %d...\n", threads->tid, $thread->tid; + $thread->join; # block until threads are killed + Slic3r::debugf " Thread %d finished waiting for %d...\n", threads->tid, $thread->tid; + } + @my_threads = (); } -sub pause_threads { +sub pause_all_threads { return if $paused; + lock @threads; $paused = 1; - $sema->down; - $_->kill('STOP') for get_running_threads(); + $pause_sema->down; + $_->kill('STOP') for get_running_threads(@threads); } -sub resume_threads { +sub resume_all_threads { return unless $paused; + lock @threads; $paused = 0; - $sema->up; + $pause_sema->up; } sub encode_path { diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index 529984729..61d4bb97c 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -250,14 +250,14 @@ sub validate { die "Invalid value for --fill-pattern\n" if !first { $_ eq $self->fill_pattern } @{$Options->{fill_pattern}{values}}; - # --solid-fill-pattern - die "Invalid value for --solid-fill-pattern\n" - if !first { $_ eq $self->solid_fill_pattern } @{$Options->{solid_fill_pattern}{values}}; + # --external-fill-pattern + die "Invalid value for --external-fill-pattern\n" + if !first { $_ eq $self->external_fill_pattern } @{$Options->{external_fill_pattern}{values}}; # --fill-density die "The selected fill pattern is not supposed to work at 100% density\n" if $self->fill_density == 100 - && !first { $_ eq $self->fill_pattern } @{$Options->{solid_fill_pattern}{values}}; + && !first { $_ eq $self->fill_pattern } @{$Options->{external_fill_pattern}{values}}; # --infill-every-layers die "Invalid value for --infill-every-layers\n" diff --git a/lib/Slic3r/Fill.pm b/lib/Slic3r/Fill.pm index de322153d..504230933 100644 --- a/lib/Slic3r/Fill.pm +++ b/lib/Slic3r/Fill.pm @@ -3,14 +3,9 @@ use Moo; use Slic3r::ExtrusionPath ':roles'; use Slic3r::Fill::3DHoneycomb; -use Slic3r::Fill::ArchimedeanChords; use Slic3r::Fill::Base; use Slic3r::Fill::Concentric; -use Slic3r::Fill::Flowsnake; -use Slic3r::Fill::HilbertCurve; use Slic3r::Fill::Honeycomb; -use Slic3r::Fill::Line; -use Slic3r::Fill::OctagramSpiral; use Slic3r::Fill::PlanePath; use Slic3r::Fill::Rectilinear; use Slic3r::Flow ':roles'; @@ -83,7 +78,7 @@ sub make_fill { ? $layerm->flow(FLOW_ROLE_TOP_SOLID_INFILL)->width : $solid_infill_flow->width; $pattern[$i] = $groups[$i][0]->is_external - ? $layerm->config->solid_fill_pattern + ? $layerm->config->external_fill_pattern : 'rectilinear'; } else { $is_solid[$i] = 0; @@ -179,7 +174,6 @@ sub make_fill { } my @fills = (); - my @fills_ordering_points = (); SURFACE: foreach my $surface (@surfaces) { next if $surface->surface_type == S_TYPE_INTERNALVOID; my $filler = $layerm->config->fill_pattern; @@ -190,72 +184,103 @@ sub make_fill { my $is_bridge = $layerm->id > 0 && $surface->is_bridge; my $is_solid = $surface->is_solid; - # force 100% density and rectilinear fill for external surfaces - if ($surface->surface_type != S_TYPE_INTERNAL) { + if ($surface->is_solid) { $density = 100; - $filler = $layerm->config->solid_fill_pattern; - if ($is_bridge) { - $filler = 'rectilinear'; - } elsif ($surface->surface_type == S_TYPE_INTERNALSOLID) { - $filler = 'rectilinear'; + $filler = 'rectilinear'; + if ($surface->is_external) { + $filler = $layerm->config->external_fill_pattern; } } else { next SURFACE unless $density > 0; } + # get filler object + my $f = $self->filler($filler); + + # calculate the actual flow we'll be using for this infill my $h = $surface->thickness == -1 ? $layerm->height : $surface->thickness; my $flow = $layerm->region->flow( $role, $h, - $is_bridge, + $is_bridge || $f->use_bridge_flow, $layerm->id == 0, -1, $layerm->object, ); - my $f = $self->filler($filler); + # calculate flow spacing for infill pattern generation + my $using_internal_flow = 0; + if (!$is_solid && !$is_bridge) { + # it's internal infill, so we can calculate a generic flow spacing + # for all layers, for avoiding the ugly effect of + # misaligned infill on first layer because of different extrusion width and + # layer height + my $internal_flow = $layerm->region->flow( + FLOW_ROLE_INFILL, + $layerm->object->config->layer_height, # TODO: handle infill_every_layers? + 0, # no bridge + 0, # no first layer + -1, # auto width + $layerm->object, + ); + $f->spacing($internal_flow->spacing); + $using_internal_flow = 1; + } else { + $f->spacing($flow->spacing); + } + $f->layer_id($layerm->id); $f->z($layerm->print_z); $f->angle(deg2rad($layerm->config->fill_angle)); - my ($params, @polylines) = $f->fill_surface( + $f->loop_clipping(scale($flow->nozzle_diameter) * &Slic3r::LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER); + my @polylines = $f->fill_surface( $surface, density => $density/100, - flow => $flow, layer_height => $h, ); next unless @polylines; + # calculate actual flow from spacing (which might have been adjusted by the infill + # pattern generator) + if ($using_internal_flow) { + # if we used the internal flow we're not doing a solid infill + # so we can safely ignore the slight variation that might have + # been applied to $f->flow_spacing + } else { + $flow = Slic3r::Flow->new_from_spacing( + spacing => $f->spacing, + nozzle_diameter => $flow->nozzle_diameter, + layer_height => $h, + bridge => $is_bridge || $f->use_bridge_flow, + ); + } my $mm3_per_mm = $flow->mm3_per_mm; # save into layer - push @fills, my $collection = Slic3r::ExtrusionPath::Collection->new; - $collection->no_sort($params->{no_sort}); - - $collection->append( - map Slic3r::ExtrusionPath->new( - polyline => $_, - role => ($is_bridge - ? EXTR_ROLE_BRIDGE - : $is_solid - ? (($surface->surface_type == S_TYPE_TOP) ? EXTR_ROLE_TOPSOLIDFILL : EXTR_ROLE_SOLIDFILL) - : EXTR_ROLE_FILL), - mm3_per_mm => $mm3_per_mm, - width => $flow->width, - height => ($is_bridge ? $flow->width : $h), - ), @polylines, - ); - push @fills_ordering_points, $polylines[0]->first_point; + { + my $role = $is_bridge ? EXTR_ROLE_BRIDGE + : $is_solid ? (($surface->surface_type == S_TYPE_TOP) ? EXTR_ROLE_TOPSOLIDFILL : EXTR_ROLE_SOLIDFILL) + : EXTR_ROLE_FILL; + + push @fills, my $collection = Slic3r::ExtrusionPath::Collection->new; + $collection->no_sort($f->no_sort); + $collection->append( + map Slic3r::ExtrusionPath->new( + polyline => $_, + role => $role, + mm3_per_mm => $mm3_per_mm, + width => $flow->width, + height => $flow->height, + ), @polylines, + ); + } } # add thin fill regions foreach my $thin_fill (@{$layerm->thin_fills}) { push @fills, Slic3r::ExtrusionPath::Collection->new($thin_fill); - push @fills_ordering_points, $thin_fill->first_point; } - # organize infill paths using a nearest-neighbor search - @fills = @fills[ @{chained_path(\@fills_ordering_points)} ]; - return @fills; } diff --git a/lib/Slic3r/Fill/3DHoneycomb.pm b/lib/Slic3r/Fill/3DHoneycomb.pm index bb37d0ea7..256437707 100644 --- a/lib/Slic3r/Fill/3DHoneycomb.pm +++ b/lib/Slic3r/Fill/3DHoneycomb.pm @@ -7,22 +7,17 @@ use POSIX qw(ceil fmod); use Slic3r::Geometry qw(scale scaled_epsilon); use Slic3r::Geometry::Clipper qw(intersection_pl); +# require bridge flow since most of this pattern hangs in air +sub use_bridge_flow { 1 } + sub fill_surface { my ($self, $surface, %params) = @_; - # use bridge flow since most of this pattern hangs in air - my $flow = Slic3r::Flow->new( - width => $params{flow}->width, - height => $params{flow}->height, - nozzle_diameter => $params{flow}->nozzle_diameter, - bridge => 1, - ); - my $expolygon = $surface->expolygon; my $bb = $expolygon->bounding_box; my $size = $bb->size; - my $distance = $flow->scaled_spacing / $params{density}; + my $distance = scale($self->spacing) / $params{density}; # align bounding box to a multiple of our honeycomb grid { @@ -71,7 +66,7 @@ sub fill_surface { } # TODO: return ExtrusionLoop objects to get better chained paths - return { flow => $flow}, @polylines; + return @polylines; } diff --git a/lib/Slic3r/Fill/ArchimedeanChords.pm b/lib/Slic3r/Fill/ArchimedeanChords.pm deleted file mode 100644 index 3c71f0e36..000000000 --- a/lib/Slic3r/Fill/ArchimedeanChords.pm +++ /dev/null @@ -1,7 +0,0 @@ -package Slic3r::Fill::ArchimedeanChords; -use Moo; - -extends 'Slic3r::Fill::PlanePath'; -use Math::PlanePath::ArchimedeanChords; - -1; diff --git a/lib/Slic3r/Fill/Base.pm b/lib/Slic3r/Fill/Base.pm index 34b47243b..75c8e03e6 100644 --- a/lib/Slic3r/Fill/Base.pm +++ b/lib/Slic3r/Fill/Base.pm @@ -4,6 +4,8 @@ use Moo; has 'layer_id' => (is => 'rw'); has 'z' => (is => 'rw'); # in unscaled coordinates has 'angle' => (is => 'rw'); # in radians, ccw, 0 = East +has 'spacing' => (is => 'rw'); # in unscaled coordinates +has 'loop_clipping' => (is => 'rw', default => sub { 0 }); # in scaled coordinates has 'bounding_box' => (is => 'ro', required => 0); # Slic3r::Geometry::BoundingBox object sub adjust_solid_spacing { @@ -17,6 +19,9 @@ sub adjust_solid_spacing { return $params{distance} + $extra_space / ($number_of_lines - 1); } +sub no_sort { 0 } +sub use_bridge_flow { 0 } + package Slic3r::Fill::WithDirection; use Moo::Role; diff --git a/lib/Slic3r/Fill/Concentric.pm b/lib/Slic3r/Fill/Concentric.pm index f122bdbd5..17f3fa225 100644 --- a/lib/Slic3r/Fill/Concentric.pm +++ b/lib/Slic3r/Fill/Concentric.pm @@ -15,22 +15,15 @@ sub fill_surface { my $expolygon = $surface->expolygon; my $bounding_box = $expolygon->bounding_box; - my $flow = $params{flow}; - my $min_spacing = $flow->scaled_spacing; + my $min_spacing = scale($self->spacing); my $distance = $min_spacing / $params{density}; - my $flow_spacing = $flow->spacing; if ($params{density} == 1 && !$params{dont_adjust}) { $distance = $self->adjust_solid_spacing( width => $bounding_box->size->[X], distance => $distance, ); - $flow = Slic3r::Flow->new_from_spacing( - spacing => unscale($distance), - nozzle_diameter => $flow->nozzle_diameter, - layer_height => ($params{layer_height} or die "No layer_height supplied to fill_surface()"), - bridge => $flow->bridge, - ); + $self->spacing(unscale $distance); } # compensate the overlap which is good for rectilinear but harmful for concentric @@ -54,12 +47,11 @@ sub fill_surface { } # clip the paths to prevent the extruder from getting exactly on the first point of the loop - my $clip_length = scale($flow->nozzle_diameter) * &Slic3r::LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER; - $_->clip_end($clip_length) for @paths; + $_->clip_end($self->loop_clipping) for @paths; @paths = grep $_->is_valid, @paths; # remove empty paths (too short, thus eaten by clipping) # TODO: return ExtrusionLoop objects to get better chained paths - return { flow => $flow, no_sort => 1 }, @paths; + return @paths; } 1; diff --git a/lib/Slic3r/Fill/Flowsnake.pm b/lib/Slic3r/Fill/Flowsnake.pm deleted file mode 100644 index c852b81c7..000000000 --- a/lib/Slic3r/Fill/Flowsnake.pm +++ /dev/null @@ -1,18 +0,0 @@ -package Slic3r::Fill::Flowsnake; -use Moo; - -extends 'Slic3r::Fill::PlanePath'; - -use Math::PlanePath::Flowsnake; -use Slic3r::Geometry qw(X); - -# Sorry, this fill is currently broken. - -sub process_polyline { - my $self = shift; - my ($polyline, $bounding_box) = @_; - - $_->[X] += $bounding_box->center->[X] for @$polyline; -} - -1; diff --git a/lib/Slic3r/Fill/HilbertCurve.pm b/lib/Slic3r/Fill/HilbertCurve.pm deleted file mode 100644 index eff1a44ac..000000000 --- a/lib/Slic3r/Fill/HilbertCurve.pm +++ /dev/null @@ -1,7 +0,0 @@ -package Slic3r::Fill::HilbertCurve; -use Moo; - -extends 'Slic3r::Fill::PlanePath'; -use Math::PlanePath::HilbertCurve; - -1; diff --git a/lib/Slic3r/Fill/Honeycomb.pm b/lib/Slic3r/Fill/Honeycomb.pm index 08c0b0710..0fb47db4d 100644 --- a/lib/Slic3r/Fill/Honeycomb.pm +++ b/lib/Slic3r/Fill/Honeycomb.pm @@ -18,11 +18,11 @@ sub fill_surface { my $rotate_vector = $self->infill_direction($surface); # cache hexagons math - my $cache_id = sprintf "d%s_s%s", $params{density}, $params{flow}->spacing; + my $cache_id = sprintf "d%s_s%s", $params{density}, $self->spacing; my $m; if (!($m = $self->cache->{$cache_id})) { $m = $self->cache->{$cache_id} = {}; - my $min_spacing = $params{flow}->scaled_spacing; + my $min_spacing = scale($self->spacing); $m->{distance} = $min_spacing / $params{density}; $m->{hex_side} = $m->{distance} / (sqrt(3)/2); $m->{hex_width} = $m->{distance} * 2; # $m->{hex_width} == $m->{hex_side} * sqrt(3); @@ -123,7 +123,7 @@ sub fill_surface { )}; } - return { flow => $params{flow} }, @paths; + return @paths; } 1; diff --git a/lib/Slic3r/Fill/Line.pm b/lib/Slic3r/Fill/Line.pm deleted file mode 100644 index 97dc17cfd..000000000 --- a/lib/Slic3r/Fill/Line.pm +++ /dev/null @@ -1,8 +0,0 @@ -package Slic3r::Fill::Line; -use Moo; - -extends 'Slic3r::Fill::Rectilinear'; - -# Sorry for breaking OOP, but Line is implemented inside Rectilinear. - -1; diff --git a/lib/Slic3r/Fill/OctagramSpiral.pm b/lib/Slic3r/Fill/OctagramSpiral.pm deleted file mode 100644 index 9c1272444..000000000 --- a/lib/Slic3r/Fill/OctagramSpiral.pm +++ /dev/null @@ -1,9 +0,0 @@ -package Slic3r::Fill::OctagramSpiral; -use Moo; - -extends 'Slic3r::Fill::PlanePath'; -use Math::PlanePath::OctagramSpiral; - -sub multiplier () { sqrt(2) } - -1; diff --git a/lib/Slic3r/Fill/PlanePath.pm b/lib/Slic3r/Fill/PlanePath.pm index dd942aba9..556835ec4 100644 --- a/lib/Slic3r/Fill/PlanePath.pm +++ b/lib/Slic3r/Fill/PlanePath.pm @@ -7,16 +7,9 @@ with qw(Slic3r::Fill::WithDirection); use Slic3r::Geometry qw(scale X1 Y1 X2 Y2); use Slic3r::Geometry::Clipper qw(intersection_pl); +sub angles () { [0] } sub multiplier () { 1 } -sub get_n { - my $self = shift; - my ($path, $bounding_box) = @_; - - my ($n_lo, $n_hi) = $path->rect_to_n_range(@$bounding_box); - return ($n_lo .. $n_hi); -} - sub process_polyline {} sub fill_surface { @@ -28,16 +21,37 @@ sub fill_surface { my $rotate_vector = $self->infill_direction($surface); $self->rotate_points($expolygon, $rotate_vector); - my $flow = $params{flow}; - my $distance_between_lines = $flow->scaled_spacing / $params{density} * $self->multiplier; - my $bounding_box = $expolygon->bounding_box; + my $distance_between_lines = scale($self->spacing) / $params{density} * $self->multiplier; + + # align infill across layers using the object's bounding box + my $bb_polygon = $self->bounding_box->polygon; + $self->rotate_points($bb_polygon, $rotate_vector); + my $bounding_box = $bb_polygon->bounding_box; (ref $self) =~ /::([^:]+)$/; my $path = "Math::PlanePath::$1"->new; - my @n = $self->get_n($path, [ map +($_ / $distance_between_lines), @{$bounding_box->min_point}, @{$bounding_box->max_point} ]); + + my $translate = Slic3r::Point->new(0,0); # vector + if ($path->x_negative || $path->y_negative) { + # if the curve extends on both positive and negative coordinate space, + # center our expolygon around origin + $translate = $bounding_box->center->negative; + } else { + # if the curve does not extend in negative coordinate space, + # move expolygon entirely in positive coordinate space + $translate = $bounding_box->min_point->negative; + } + $expolygon->translate(@$translate); + $bounding_box->translate(@$translate); + + my ($n_lo, $n_hi) = $path->rect_to_n_range( + map { $_ / $distance_between_lines } + @{$bounding_box->min_point}, + @{$bounding_box->max_point}, + ); my $polyline = Slic3r::Polyline->new( - map [ map {$_*$distance_between_lines} $path->n_to_xy($_) ], @n, + map [ map { $_ * $distance_between_lines } $path->n_to_xy($_) ], ($n_lo..$n_hi) ); return {} if @$polyline <= 1; @@ -48,15 +62,57 @@ sub fill_surface { if (0) { require "Slic3r/SVG.pm"; Slic3r::SVG::output("fill.svg", - polygons => $expolygon, - polylines => [map $_->p, @paths], + no_arrows => 1, + polygons => \@$expolygon, + green_polygons => [ $bounding_box->polygon ], + polylines => [ $polyline ], + red_polylines => \@paths, ); } - # paths must be rotated back + # paths must be repositioned and rotated back + $_->translate(@{$translate->negative}) for @paths; $self->rotate_points_back(\@paths, $rotate_vector); - return { flow => $flow }, @paths; + return @paths; } + +package Slic3r::Fill::ArchimedeanChords; +use Moo; +extends 'Slic3r::Fill::PlanePath'; +use Math::PlanePath::ArchimedeanChords; + + +package Slic3r::Fill::Flowsnake; +use Moo; +extends 'Slic3r::Fill::PlanePath'; +use Math::PlanePath::Flowsnake; +use Slic3r::Geometry qw(X); + +# Sorry, this fill is currently broken. + +sub process_polyline { + my $self = shift; + my ($polyline, $bounding_box) = @_; + + $_->[X] += $bounding_box->center->[X] for @$polyline; +} + + +package Slic3r::Fill::HilbertCurve; +use Moo; +extends 'Slic3r::Fill::PlanePath'; +use Math::PlanePath::HilbertCurve; + + +package Slic3r::Fill::OctagramSpiral; +use Moo; +extends 'Slic3r::Fill::PlanePath'; +use Math::PlanePath::OctagramSpiral; + +sub multiplier () { sqrt(2) } + + + 1; diff --git a/lib/Slic3r/Fill/Rectilinear.pm b/lib/Slic3r/Fill/Rectilinear.pm index 8e3eee3a8..a50e12b09 100644 --- a/lib/Slic3r/Fill/Rectilinear.pm +++ b/lib/Slic3r/Fill/Rectilinear.pm @@ -4,10 +4,13 @@ use Moo; extends 'Slic3r::Fill::Base'; with qw(Slic3r::Fill::WithDirection); -has 'cache' => (is => 'rw', default => sub {{}}); +has '_min_spacing' => (is => 'rw'); +has '_line_spacing' => (is => 'rw'); +has '_diagonal_distance' => (is => 'rw'); +has '_line_oscillation' => (is => 'rw'); -use Slic3r::Geometry qw(A B X Y MIN scale unscale scaled_epsilon); -use Slic3r::Geometry::Clipper qw(intersection_pl offset); +use Slic3r::Geometry qw(scale unscale scaled_epsilon); +use Slic3r::Geometry::Clipper qw(intersection_pl); sub fill_surface { my $self = shift; @@ -18,47 +21,32 @@ sub fill_surface { my $rotate_vector = $self->infill_direction($surface); $self->rotate_points($expolygon, $rotate_vector); - my $flow = $params{flow} or die "No flow supplied to fill_surface()"; - my $min_spacing = $flow->scaled_spacing; - my $line_spacing = $min_spacing / $params{density}; - my $line_oscillation = $line_spacing - $min_spacing; - my $is_line_pattern = $self->isa('Slic3r::Fill::Line'); - my $bounding_box = $expolygon->bounding_box; + $self->_min_spacing(scale $self->spacing); + $self->_line_spacing($self->_min_spacing / $params{density}); + $self->_diagonal_distance($self->_line_spacing * 2); + $self->_line_oscillation($self->_line_spacing - $self->_min_spacing); # only for Line infill + my $bounding_box = $expolygon->bounding_box; # define flow spacing according to requested density if ($params{density} == 1 && !$params{dont_adjust}) { - $line_spacing = $self->adjust_solid_spacing( - width => $bounding_box->size->[X], - distance => $line_spacing, - ); - $flow = Slic3r::Flow->new_from_spacing( - spacing => unscale($line_spacing), - nozzle_diameter => $flow->nozzle_diameter, - layer_height => ($params{layer_height} or die "No layer_height supplied to fill_surface()"), - bridge => $flow->bridge, - ); + $self->_line_spacing($self->adjust_solid_spacing( + width => $bounding_box->size->x, + distance => $self->_line_spacing, + )); + $self->spacing(unscale $self->_line_spacing); } else { # extend bounding box so that our pattern will be aligned with other layers $bounding_box->merge_point(Slic3r::Point->new( - $bounding_box->x_min - ($bounding_box->x_min % $line_spacing), - $bounding_box->y_min - ($bounding_box->y_min % $line_spacing), + $bounding_box->x_min - ($bounding_box->x_min % $self->_line_spacing), + $bounding_box->y_min - ($bounding_box->y_min % $self->_line_spacing), )); } # generate the basic pattern - my $i = 0; - my $x = $bounding_box->x_min; - my $x_max = $bounding_box->x_max + scaled_epsilon; + my $x_max = $bounding_box->x_max + scaled_epsilon; my @vertical_lines = (); - while ($x <= $x_max) { - my $vertical_line = [ [$x, $bounding_box->y_max], [$x, $bounding_box->y_min] ]; - if ($is_line_pattern && $i % 2) { - $vertical_line->[A][X] += $line_oscillation; - $vertical_line->[B][X] -= $line_oscillation; - } - push @vertical_lines, Slic3r::Polyline->new(@$vertical_line); - $i++; - $x += $line_spacing; + for (my $x = $bounding_box->x_min; $x <= $x_max; $x += $self->_line_spacing) { + push @vertical_lines, $self->_line($#vertical_lines, $x, $bounding_box->y_min, $bounding_box->y_max); } # clip paths against a slightly larger expolygon, so that the first and last paths @@ -70,19 +58,10 @@ sub fill_surface { # connect lines unless ($params{dont_connect} || !@polylines) { # prevent calling leftmost_point() on empty collections - my ($expolygon_off) = @{$expolygon->offset_ex($min_spacing/2)}; + my ($expolygon_off) = @{$expolygon->offset_ex($self->_min_spacing/2)}; my $collection = Slic3r::Polyline::Collection->new(@polylines); @polylines = (); - my $tolerance = 10 * scaled_epsilon; - my $diagonal_distance = $line_spacing * 2; - my $can_connect = $is_line_pattern - ? sub { - ($_[X] >= ($line_spacing - $line_oscillation) - $tolerance) && ($_[X] <= ($line_spacing + $line_oscillation) + $tolerance) - && $_[Y] <= $diagonal_distance - } - : sub { $_[X] <= $diagonal_distance && $_[Y] <= $diagonal_distance }; - foreach my $polyline (@{$collection->chained_path_from($collection->leftmost_point, 0)}) { if (@polylines) { my $first_point = $polyline->first_point; @@ -91,7 +70,7 @@ sub fill_surface { # TODO: we should also check that both points are on a fill_boundary to avoid # connecting paths on the boundaries of internal regions - if ($can_connect->(@distance) && $expolygon_off->contains_line(Slic3r::Line->new($last_point, $first_point))) { + if ($self->_can_connect(@distance) && $expolygon_off->contains_line(Slic3r::Line->new($last_point, $first_point))) { $polylines[-1]->append_polyline($polyline); next; } @@ -105,7 +84,55 @@ sub fill_surface { # paths must be rotated back $self->rotate_points_back(\@polylines, $rotate_vector); - return { flow => $flow }, @polylines; + return @polylines; +} + +sub _line { + my ($self, $i, $x, $y_min, $y_max) = @_; + + return Slic3r::Polyline->new( + [$x, $y_min], + [$x, $y_max], + ); +} + +sub _can_connect { + my ($self, $dist_X, $dist_Y) = @_; + + return $dist_X <= $self->_diagonal_distance + && $dist_Y <= $self->_diagonal_distance; +} + + +package Slic3r::Fill::Line; +use Moo; +extends 'Slic3r::Fill::Rectilinear'; + +use Slic3r::Geometry qw(scaled_epsilon); + +sub _line { + my ($self, $i, $x, $y_min, $y_max) = @_; + + if ($i % 2) { + return Slic3r::Polyline->new( + [$x - $self->_line_oscillation, $y_min], + [$x + $self->_line_oscillation, $y_max], + ); + } else { + return Slic3r::Polyline->new( + [$x, $y_min], + [$x, $y_max], + ); + } +} + +sub _can_connect { + my ($self, $dist_X, $dist_Y) = @_; + + my $TOLERANCE = 10 * scaled_epsilon; + return ($dist_X >= ($self->_line_spacing - $self->_line_oscillation) - $TOLERANCE) + && ($dist_X <= ($self->_line_spacing + $self->_line_oscillation) + $TOLERANCE) + && $dist_Y <= $self->_diagonal_distance; } 1; diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index 0561e4cd8..7adec0529 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -96,6 +96,10 @@ sub change_layer { $gcode .= $self->retract; } $gcode .= $self->writer->travel_to_z($z, 'move to next layer (' . $self->layer->id . ')'); + + # forget last wiping path as wiping after raising Z is pointless + $self->wipe->path(undef); + return $gcode; } @@ -210,10 +214,10 @@ sub extrude_loop { # create the destination point along the first segment and rotate it # we make sure we don't exceed the segment length because we don't know # the rotation of the second segment so we might cross the object boundary - my $first_segment = Slic3r::Line->new(@$last_path_polyline[0,1]); + my $first_segment = Slic3r::Line->new(@{$paths[0]->polyline}[0,1]); my $distance = min(scale($self->config->get_at('nozzle_diameter', $self->writer->extruder->id)), $first_segment->length); my $point = $first_segment->point_at($distance); - $point->rotate($angle, $last_path_polyline->first_point); + $point->rotate($angle, $first_segment->a); # generate the travel move $gcode .= $self->travel_to($point, $paths[-1]->role, "move inwards before travel"); @@ -256,10 +260,10 @@ sub _extrude_path { $acceleration = $self->config->first_layer_acceleration; } elsif ($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->bridge_acceleration && $path->is_bridge) { $acceleration = $self->config->bridge_acceleration; + } elsif ($self->config->infill_acceleration && $path->is_infill) { + $acceleration = $self->config->infill_acceleration; } else { $acceleration = $self->config->default_acceleration; } @@ -337,30 +341,34 @@ sub travel_to { # Skip retraction at all in the following cases: # - travel length is shorter than the configured threshold # - user has enabled "Only retract when crossing perimeters" and the travel move is - # contained in a single island of the current layer *and* a single island in the - # upper layer (so that ooze will not be visible) + # contained in a single internal fill_surface (this includes the bottom layer when + # bottom_solid_layers == 0) or in a single internal slice (this would exclude such + # bottom layer but preserve perimeter-to-infill moves in all the other layers) # - the path that will be extruded after this travel move is a support material # extrusion and the travel move is contained in a single support material island if ($travel->length < scale $self->config->get_at('retract_before_travel', $self->writer->extruder->id) || ($self->config->only_retract_when_crossing_perimeters && $self->config->fill_density > 0 - && $self->layer->slices->contains_line($travel) - && (!$self->layer->has_upper_layer || $self->layer->upper_layer->slices->contains_line($travel))) + && defined($self->layer) + && ($self->layer->any_internal_region_slice_contains_line($travel) + || $self->layer->any_internal_region_fill_surface_contains_line($travel))) || (defined $role && $role == EXTR_ROLE_SUPPORTMATERIAL && $self->layer->support_islands->contains_line($travel)) ) { # Just perform a straight travel move without any retraction. $gcode .= $self->writer->travel_to_xy($self->point_to_gcode($point), $comment); - } elsif ($self->config->avoid_crossing_perimeters && !$self->avoid_crossing_perimeters->straight_once) { + } elsif ($self->config->avoid_crossing_perimeters && !$self->avoid_crossing_perimeters->disable_once) { + # If avoid_crossing_perimeters is enabled and the disable_once flag is not set + # we need to plan a multi-segment travel move inside the configuration space. $gcode .= $self->avoid_crossing_perimeters->travel_to($self, $point, $comment); } else { - # If avoid_crossing_perimeters is disabled or the straight_once flag is set, + # If avoid_crossing_perimeters is disabled or the disable_once flag is set, # perform a straight move with a retraction. $gcode .= $self->retract; $gcode .= $self->writer->travel_to_xy($self->point_to_gcode($point), $comment || ''); } # Re-allow avoid_crossing_perimeters for the next travel moves - $self->avoid_crossing_perimeters->straight_once(0); + $self->avoid_crossing_perimeters->disable_once(0); return $gcode; } @@ -555,10 +563,13 @@ sub wipe { package Slic3r::GCode::AvoidCrossingPerimeters; use Moo; -has '_external_mp' => (is => 'rw'); -has '_layer_mp' => (is => 'rw'); -has 'new_object' => (is => 'rw', default => sub {0}); # this flag triggers the use of the external configuration space for avoid_crossing_perimeters for the next travel move -has 'straight_once' => (is => 'rw', default => sub {1}); # this flag disables avoid_crossing_perimeters just for the next travel move +has '_external_mp' => (is => 'rw'); +has '_layer_mp' => (is => 'rw'); +has 'use_external_mp' => (is => 'rw', default => sub {0}); +has 'use_external_mp_once' => (is => 'rw', default => sub {0}); # this flag triggers the use of the external configuration space for avoid_crossing_perimeters for the next travel move +has 'disable_once' => (is => 'rw', default => sub {1}); # this flag disables avoid_crossing_perimeters just for the next travel move + +use Slic3r::Geometry qw(scale); sub init_external_mp { my ($self, $islands) = @_; @@ -575,11 +586,8 @@ sub travel_to { my $gcode = ""; - # If avoid_crossing_perimeters is enabled and the straight_once flag is not set - # we need to plan a multi-segment travel move inside the configuration space. - if ($self->new_object) { - # If we're moving to a new object we need to use the external configuration space. - $self->new_object(0); + if ($self->use_external_mp || $self->use_external_mp_once) { + $self->use_external_mp_once(0); # represent $point in G-code coordinates $point = $point->clone; @@ -606,13 +614,12 @@ sub _plan { # if the path is not contained in a single island we need to retract $gcode .= $gcodegen->retract if !$gcodegen->config->only_retract_when_crossing_perimeters - || !$gcodegen->layer->slices->contains_polyline($travel) - || ($gcodegen->layer->has_upper_layer && !$gcodegen->layer->upper_layer->slices->contains_polyline($travel)); + || !$gcodegen->layer->any_internal_region_fill_surface_contains_polyline($travel); # append the actual path and return # use G1 because we rely on paths being straight (G0 may make round paths) $gcode .= join '', - map $gcodegen->writer->travel_to_xy($self->point_to_gcode($_->b), $comment), + map $gcodegen->writer->travel_to_xy($gcodegen->point_to_gcode($_->b), $comment), @{$travel->lines}; return $gcode; diff --git a/lib/Slic3r/GCode/Layer.pm b/lib/Slic3r/GCode/Layer.pm deleted file mode 100644 index b13eab6a0..000000000 --- a/lib/Slic3r/GCode/Layer.pm +++ /dev/null @@ -1,251 +0,0 @@ -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(config)]); -has 'gcodegen' => (is => 'ro', required => 1); -has 'origin' => (is => 'ro', default => sub { Slic3r::Pointf->new(0,0) }); - -has 'spiralvase' => (is => 'lazy'); -has 'vibration_limit' => (is => 'lazy'); -has 'arc_fitting' => (is => 'lazy'); -has 'pressure_regulator' => (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 $self->print->config->spiral_vase - ? Slic3r::GCode::SpiralVase->new(config => $self->print->config) - : undef; -} - -sub _build_vibration_limit { - my $self = shift; - - return $self->print->config->vibration_limit - ? Slic3r::GCode::VibrationLimit->new(config => $self->print->config) - : undef; -} - -sub _build_arc_fitting { - my $self = shift; - - return $self->print->config->gcode_arcs - ? Slic3r::GCode::ArcFitting->new(config => $self->print->config) - : undef; -} - -sub _build_pressure_regulator { - my $self = shift; - - return $self->print->config->pressure_advance > 0 - ? Slic3r::GCode::PressureRegulator->new(config => $self->print->config) - : undef; -} - -sub process_layer { - my $self = shift; - my ($layer, $object_copies) = @_; - my $gcode = ""; - - my $object = $layer->object; - $self->gcodegen->config->apply_object_config($object->config); - - # check whether we're going to apply spiralvase logic - if (defined $self->spiralvase) { - $self->spiralvase->enable( - ($layer->id > 0 || $self->print->config->brim_width == 0) - && ($layer->id >= $self->print->config->skirt_height && $self->print->config->skirt_height != -1) - && !defined(first { $_->config->bottom_solid_layers > $layer->id } @{$layer->regions}) - && !defined(first { @{$_->perimeters} > 1 } @{$layer->regions}) - && !defined(first { @{$_->fills} > 0 } @{$layer->regions}) - ); - } - - # if we're going to apply spiralvase to this layer, disable loop clipping - $self->gcodegen->enable_loop_clipping(!defined $self->spiralvase || !$self->spiralvase->enable); - - if (!$self->second_layer_things_done && $layer->id == 1) { - for my $extruder (@{$self->gcodegen->writer->extruders}) { - my $temperature = $self->config->get_at('temperature', $extruder->id); - $gcode .= $self->gcodegen->writer->set_temperature($temperature, 0, $extruder->id) - if $temperature && $temperature != $self->config->get_at('first_layer_temperature', $extruder->id); - } - $gcode .= $self->gcodegen->writer->set_bed_temperature($self->print->config->bed_temperature) - if $self->print->config->bed_temperature && $self->print->config->bed_temperature != $self->print->config->first_layer_bed_temperature; - $self->second_layer_things_done(1); - } - - # set new layer - this will change Z and force a retraction if retract_layer_change is enabled - $gcode .= $self->gcodegen->change_layer($layer); - $gcode .= $self->gcodegen->placeholder_parser->process($self->print->config->layer_gcode, { - layer_num => $layer->id, - }) . "\n" if $self->print->config->layer_gcode; - - # extrude skirt - if (((values %{$self->skirt_done}) < $self->print->config->skirt_height || $self->print->config->skirt_height == -1) - && !$self->skirt_done->{$layer->print_z}) { - $self->gcodegen->set_origin($self->origin); - my @extruder_ids = map { $_->id } @{$self->gcodegen->writer->extruders}; - $gcode .= $self->gcodegen->set_extruder($extruder_ids[0]); - # skip skirt if we have a large brim - if ($layer->id < $self->print->config->skirt_height || $self->print->config->skirt_height == -1) { - # distribute skirt loops across all extruders - my @skirt_loops = @{$self->print->skirt}; - for my $i (0 .. $#skirt_loops) { - # 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 >= $self->print->config->skirts); - my $extruder_id = $extruder_ids[($i/@extruder_ids) % @extruder_ids]; - $gcode .= $self->gcodegen->set_extruder($extruder_id) - if $layer->id == 0; - $gcode .= $self->gcodegen->extrude_loop($skirt_loops[$i], 'skirt', $object->config->support_material_speed); - } - } - $self->skirt_done->{$layer->print_z} = 1; - $self->gcodegen->avoid_crossing_perimeters->straight_once(1); - } - - # extrude brim - if (!$self->brim_done) { - $gcode .= $self->gcodegen->set_extruder($self->print->objects->[0]->config->support_material_extruder-1); - $self->gcodegen->set_origin($self->origin); - $gcode .= $self->gcodegen->extrude_loop($_, 'brim', $object->config->support_material_speed) - for @{$self->print->brim}; - $self->brim_done(1); - $self->gcodegen->avoid_crossing_perimeters->straight_once(1); - } - - for my $copy (@$object_copies) { - $self->gcodegen->avoid_crossing_perimeters->new_object(1) if ($self->_last_obj_copy // '') ne "$copy"; - $self->_last_obj_copy("$copy"); - - $self->gcodegen->set_origin(Slic3r::Pointf->new(map $self->origin->[$_] + 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 ($layer->isa('Slic3r::Layer::Support')) { - if ($layer->support_interface_fills->count > 0) { - $gcode .= $self->gcodegen->set_extruder($object->config->support_material_interface_extruder-1); - $gcode .= $self->gcodegen->extrude_path($_, 'support material interface', $object->config->get_abs_value('support_material_interface_speed')) - for @{$layer->support_interface_fills->chained_path_from($self->gcodegen->last_pos, 0)}; - } - if ($layer->support_fills->count > 0) { - $gcode .= $self->gcodegen->set_extruder($object->config->support_material_extruder-1); - $gcode .= $self->gcodegen->extrude_path($_, 'support material', $object->config->get_abs_value('support_material_speed')) - for @{$layer->support_fills->chained_path_from($self->gcodegen->last_pos, 0)}; - } - } - - # tweak region ordering to save toolchanges - my @region_ids = 0 .. ($self->print->region_count-1); - if ($self->gcodegen->writer->multiple_extruders) { - my $last_extruder_id = $self->gcodegen->writer->extruder->id; - my $best_region_id = first { $self->print->regions->[$_]->config->perimeter_extruder-1 == $last_extruder_id } @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] or next; - my $region = $self->print->regions->[$region_id]; - $self->gcodegen->config->apply_region_config($region->config); - - # group extrusions by island - my @perimeters_by_island = map [], 0..$#{$layer->slices}; # slice idx => @perimeters - my @infill_by_island = map [], 0..$#{$layer->slices}; # slice idx => @fills - - # NOTE: we assume $layer->slices was already ordered with chained_path()! - - PERIMETER: foreach my $perimeter (@{$layerm->perimeters}) { - for my $i (0 .. $#{$layer->slices}-1) { - if ($layer->slices->[$i]->contour->contains_point($perimeter->first_point)) { - push @{ $perimeters_by_island[$i] }, $perimeter; - next PERIMETER; - } - } - push @{ $perimeters_by_island[-1] }, $perimeter; # optimization - } - FILL: foreach my $fill (@{$layerm->fills}) { - for my $i (0 .. $#{$layer->slices}-1) { - if ($layer->slices->[$i]->contour->contains_point($fill->first_point)) { - push @{ $infill_by_island[$i] }, $fill; - next FILL; - } - } - push @{ $infill_by_island[-1] }, $fill; # optimization - } - - for my $i (0 .. $#{$layer->slices}) { - # give priority to infill if we were already using its extruder and it wouldn't - # be good for perimeters - if ($self->print->config->infill_first - || ($self->gcodegen->writer->multiple_extruders && $region->config->infill_extruder-1 == $self->gcodegen->writer->extruder->id && $region->config->infill_extruder != $region->config->perimeter_extruder)) { - $gcode .= $self->_extrude_infill($infill_by_island[$i], $region); - $gcode .= $self->_extrude_perimeters($perimeters_by_island[$i], $region); - } else { - $gcode .= $self->_extrude_perimeters($perimeters_by_island[$i], $region); - $gcode .= $self->_extrude_infill($infill_by_island[$i], $region); - } - } - } - } - - # apply spiral vase post-processing if this layer contains suitable geometry - # (we must feed all the G-code into the post-processor, including the first - # bottom non-spiral layers otherwise it will mess with positions) - $gcode = $self->spiralvase->process_layer($gcode) - if defined $self->spiralvase; - - # apply vibration limit if enabled - $gcode = $self->vibration_limit->process($gcode) - if $self->print->config->vibration_limit != 0; - - # apply pressure regulation if enabled - $gcode = $self->pressure_regulator->process($gcode) - if $self->print->config->pressure_advance > 0; - - # apply arc fitting if enabled - $gcode = $self->arc_fitting->process($gcode) - if $self->print->config->gcode_arcs; - - return $gcode; -} - -sub _extrude_perimeters { - my $self = shift; - my ($island_perimeters, $region) = @_; - - return "" if !@$island_perimeters; - - my $gcode = ""; - $gcode .= $self->gcodegen->set_extruder($region->config->perimeter_extruder-1); - $gcode .= $self->gcodegen->extrude($_, 'perimeter') for @$island_perimeters; - return $gcode; -} - -sub _extrude_infill { - my $self = shift; - my ($island_fills, $region) = @_; - - return "" if !@$island_fills; - - my $gcode = ""; - $gcode .= $self->gcodegen->set_extruder($region->config->infill_extruder-1); - for my $fill (@$island_fills) { - if ($fill->isa('Slic3r::ExtrusionPath::Collection')) { - $gcode .= $self->gcodegen->extrude($_, 'fill') - for @{$fill->chained_path_from($self->gcodegen->last_pos, 0)}; - } else { - $gcode .= $self->gcodegen->extrude($fill, 'fill') ; - } - } - return $gcode; -} - -1; diff --git a/lib/Slic3r/GCode/Reader.pm b/lib/Slic3r/GCode/Reader.pm index ae436b854..bd82fc5b1 100644 --- a/lib/Slic3r/GCode/Reader.pm +++ b/lib/Slic3r/GCode/Reader.pm @@ -50,7 +50,7 @@ sub parse { if ($command =~ /^G[01]$/) { foreach my $axis (@AXES) { if (exists $args{$axis}) { - $self->$axis = 0 if $axis eq 'E' && $self->config->use_relative_e_distances; + $self->$axis(0) if $axis eq 'E' && $self->config->use_relative_e_distances; $info{"dist_$axis"} = $args{$axis} - $self->$axis; $info{"new_$axis"} = $args{$axis}; } else { @@ -58,7 +58,7 @@ sub parse { $info{"new_$axis"} = $self->$axis; } } - $info{dist_XY} = Slic3r::Geometry::unscale(Slic3r::Line->new_scale([0,0], [@info{qw(dist_X dist_Y)}])->length); + $info{dist_XY} = sqrt(($info{dist_X}**2) + ($info{dist_Y}**2)); if (exists $args{E}) { if ($info{dist_E} > 0) { $info{extruding} = 1; diff --git a/lib/Slic3r/GCode/VibrationLimit.pm b/lib/Slic3r/GCode/VibrationLimit.pm index 0680a3b82..496d1e73a 100644 --- a/lib/Slic3r/GCode/VibrationLimit.pm +++ b/lib/Slic3r/GCode/VibrationLimit.pm @@ -3,7 +3,6 @@ use Moo; extends 'Slic3r::GCode::Reader'; -has 'config' => (is => 'ro', required => 1); has '_min_time' => (is => 'lazy'); has '_last_dir' => (is => 'ro', default => sub { [0,0] }); has '_dir_time' => (is => 'ro', default => sub { [0,0] }); @@ -11,7 +10,6 @@ has '_dir_time' => (is => 'ro', default => sub { [0,0] }); # inspired by http://hydraraptor.blogspot.it/2010/12/frequency-limit.html use List::Util qw(max); -use Slic3r::Geometry qw(X Y); sub _build__min_time { my ($self) = @_; @@ -27,15 +25,16 @@ sub process { my ($reader, $cmd, $args, $info) = @_; if ($cmd eq 'G1' && $info->{dist_XY} > 0) { - my $point = Slic3r::Point->new($args->{X} // $reader->X, $args->{Y} // $reader->Y); + my $point = Slic3r::Pointf->new($args->{X} // $reader->X, $args->{Y} // $reader->Y); my @dir = ( ($point->x <=> $reader->X), ($point->y <=> $reader->Y), #$ ); my $time = $info->{dist_XY} / ($args->{F} // $reader->F); # in minutes + if ($time > 0) { my @pause = (); - foreach my $axis (X,Y) { + foreach my $axis (0..$#dir) { if ($dir[$axis] != 0 && $self->_last_dir->[$axis] != $dir[$axis]) { if ($self->_last_dir->[$axis] != 0) { # this axis is changing direction: check whether we need to pause diff --git a/lib/Slic3r/GUI.pm b/lib/Slic3r/GUI.pm index 969c7fa40..d089bbf29 100644 --- a/lib/Slic3r/GUI.pm +++ b/lib/Slic3r/GUI.pm @@ -59,7 +59,7 @@ our $Settings = { }, }; -our $have_button_icons = &Wx::wxVERSION_STRING =~ / 2\.9\.[1-9]/; +our $have_button_icons = &Wx::wxVERSION_STRING =~ / (?:2\.9\.[1-9]|3\.)/; our $small_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); $small_font->SetPointSize(11) if !&Wx::wxMSW; our $medium_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); diff --git a/lib/Slic3r/GUI/BedShapeDialog.pm b/lib/Slic3r/GUI/BedShapeDialog.pm index 4169d38e6..a055dad24 100644 --- a/lib/Slic3r/GUI/BedShapeDialog.pm +++ b/lib/Slic3r/GUI/BedShapeDialog.pm @@ -82,7 +82,6 @@ sub new { tooltip => 'Distance of the 0,0 G-code coordinate from the front left corner of the rectangle.', default => [0,0], )); - $optgroup->on_change->($_) for qw(rect_size rect_origin); # set defaults } { my $optgroup = $self->_init_shape_options_page('Circular'); @@ -94,7 +93,6 @@ sub new { sidetext => 'mm', default => 200, )); - $optgroup->on_change->($_) for qw(diameter); # set defaults } { my $optgroup = $self->_init_shape_options_page('Custom'); @@ -162,6 +160,7 @@ sub _set_shape { my $optgroup = $self->{optgroups}[SHAPE_RECTANGULAR]; $optgroup->set_value('rect_size', [ $x_max-$x_min, $y_max-$y_min ]); $optgroup->set_value('rect_origin', $origin); + $self->_update_shape; return; } } @@ -172,16 +171,18 @@ sub _set_shape { my $center = $polygon->bounding_box->center; my @vertex_distances = map $center->distance_to($_), @$polygon; my $avg_dist = sum(@vertex_distances)/@vertex_distances; - if (!defined first { abs($_ - $avg_dist) > scaled_epsilon } @vertex_distances) { + if (!defined first { abs($_ - $avg_dist) > 10*scaled_epsilon } @vertex_distances) { # all vertices are equidistant to center $self->{shape_options_book}->SetSelection(SHAPE_CIRCULAR); my $optgroup = $self->{optgroups}[SHAPE_CIRCULAR]; $optgroup->set_value('diameter', sprintf("%.0f", unscale($avg_dist*2))); + $self->_update_shape; return; } } $self->{shape_options_book}->SetSelection(SHAPE_CUSTOM); + $self->_update_shape; } sub _update_shape { @@ -189,13 +190,14 @@ sub _update_shape { my $page_idx = $self->{shape_options_book}->GetSelection; if ($page_idx == SHAPE_RECTANGULAR) { - return if grep !defined($self->{"_$_"}), qw(rect_size rect_origin); # not loaded yet - my ($x, $y) = @{$self->{_rect_size}}; + my $rect_size = $self->{optgroups}[SHAPE_RECTANGULAR]->get_value('rect_size'); + my $rect_origin = $self->{optgroups}[SHAPE_RECTANGULAR]->get_value('rect_origin'); + my ($x, $y) = @$rect_size; return if !$x || !$y; # empty strings my ($x0, $y0) = (0,0); my ($x1, $y1) = ($x,$y); { - my ($dx, $dy) = @{$self->{_rect_origin}}; + my ($dx, $dy) = @$rect_origin; return if $dx eq '' || $dy eq ''; # empty strings $x0 -= $dx; $x1 -= $dx; @@ -209,9 +211,9 @@ sub _update_shape { [$x0,$y1], ]; } elsif ($page_idx == SHAPE_CIRCULAR) { - return if grep !defined($self->{"_$_"}), qw(diameter); # not loaded yet - return if !$self->{_diameter}; - my $r = $self->{_diameter}/2; + my $diameter = $self->{optgroups}[SHAPE_CIRCULAR]->get_value('diameter'); + return if !$diameter; + my $r = $diameter/2; my $twopi = 2*PI; my $edges = 60; my $polygon = Slic3r::Polygon->new_scale( @@ -366,7 +368,7 @@ sub _init_shape_options_page { label_width => 100, on_change => sub { my ($opt_id) = @_; - $self->{"_$opt_id"} = $optgroup->get_value($opt_id); + #$self->{"_$opt_id"} = $optgroup->get_value($opt_id); $self->_update_shape; }, ); diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 76371292a..31b9ff0a3 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -38,16 +38,6 @@ sub new { $self->{loaded} = 1; - # declare events - EVT_CLOSE($self, sub { - my (undef, $event) = @_; - if ($event->CanVeto && !$self->check_unsaved_changes) { - $event->Veto; - return; - } - $event->Skip; - }); - # initialize layout { my $sizer = Wx::BoxSizer->new(wxVERTICAL); @@ -56,11 +46,36 @@ sub new { $self->SetSizer($sizer); $self->Fit; $self->SetMinSize([760, 490]); - $self->SetSize($self->GetMinSize); + if (defined $Slic3r::GUI::Settings->{_}{main_frame_size}) { + $self->SetSize([ split ',', $Slic3r::GUI::Settings->{_}{main_frame_size}, 2 ]); + $self->Move([ split ',', $Slic3r::GUI::Settings->{_}{main_frame_pos}, 2 ]); + $self->Maximize(1) if $Slic3r::GUI::Settings->{_}{main_frame_maximized}; + } else { + $self->SetSize($self->GetMinSize); + } $self->Show; $self->Layout; } + # declare events + EVT_CLOSE($self, sub { + my (undef, $event) = @_; + + if ($event->CanVeto && !$self->check_unsaved_changes) { + $event->Veto; + return; + } + + # save window size + $Slic3r::GUI::Settings->{_}{main_frame_pos} = join ',', $self->GetScreenPositionXY; + $Slic3r::GUI::Settings->{_}{main_frame_size} = join ',', $self->GetSizeWH; + $Slic3r::GUI::Settings->{_}{main_frame_maximized} = $self->IsMaximized; + wxTheApp->save_settings; + + # propagate event + $event->Skip; + }); + return $self; } @@ -192,10 +207,6 @@ sub _init_menubar { $self->_append_menu_item($self->{plater_menu}, "Export AMF...", 'Export current plate as AMF', sub { $plater->export_amf; }); - $self->{plater_menu}->AppendSeparator(); - $self->_append_menu_item($self->{plater_menu}, "Toolpaths preview…", 'Open a viewer with toolpaths preview', sub { - $plater->toolpaths_preview; - }); $self->{object_menu} = $self->{plater}->object_menu; $self->on_plater_selection_changed(0); @@ -308,7 +319,14 @@ sub quick_slice { $Slic3r::GUI::Settings->{recent}{skein_directory} = dirname($input_file); wxTheApp->save_settings; + my $print_center; + { + my $bed_shape = Slic3r::Polygon->new_scale(@{$config->bed_shape}); + $print_center = Slic3r::Pointf->new_unscale(@{$bed_shape->bounding_box->center}); + } + my $sprint = Slic3r::Print::Simple->new( + print_center => $print_center, status_cb => sub { my ($percent, $message) = @_; return if &Wx::wxVERSION_STRING !~ / 2\.(8\.|9\.[2-9])/; @@ -417,7 +435,7 @@ sub extra_variables { my %extra_variables = (); if ($self->{mode} eq 'expert') { - $extra_variables{"${_}_preset"} = $self->{options_tabs}{$_}->current_preset->{name} + $extra_variables{"${_}_preset"} = $self->{options_tabs}{$_}->get_current_preset->name for qw(print filament printer); } return { %extra_variables }; diff --git a/lib/Slic3r/GUI/OptionsGroup/Field.pm b/lib/Slic3r/GUI/OptionsGroup/Field.pm index 76cabb515..fcf97eb3f 100644 --- a/lib/Slic3r/GUI/OptionsGroup/Field.pm +++ b/lib/Slic3r/GUI/OptionsGroup/Field.pm @@ -231,7 +231,7 @@ use Moo; extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow'; use List::Util qw(first); -use Wx qw(:misc :combobox); +use Wx qw(wxTheApp :misc :combobox); use Wx::Event qw(EVT_COMBOBOX EVT_TEXT); sub BUILD { @@ -258,7 +258,17 @@ sub BUILD { $label = $value; } - $field->SetValue($label); + # The MSW implementation of wxComboBox will leave the field blank if we call + # SetValue() in the EVT_COMBOBOX event handler, so we postpone the call. + wxTheApp->CallAfter(sub { + my $dce = $self->disable_change_event; + $self->disable_change_event(1); + + # ChangeValue() is not exported in wxPerl + $field->SetValue($label); + + $self->disable_change_event($dce); + }); $self->disable_change_event($disable_change_event); $self->_on_change($self->option->opt_id); diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index 32645e338..42b875036 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -26,7 +26,7 @@ use constant TB_45CW => &Wx::NewId; use constant TB_45CCW => &Wx::NewId; use constant TB_SCALE => &Wx::NewId; use constant TB_SPLIT => &Wx::NewId; -use constant TB_VIEW => &Wx::NewId; +use constant TB_CUT => &Wx::NewId; use constant TB_SETTINGS => &Wx::NewId; use constant CONFIG_TIMER_ID => &Wx::NewId; @@ -68,27 +68,25 @@ sub new { # Initialize preview notebook $self->{preview_notebook} = Wx::Notebook->new($self, -1, wxDefaultPosition, [335,335], wxNB_BOTTOM); - # Initialize 2D preview canvas - $self->{canvas} = Slic3r::GUI::Plater::2D->new($self->{preview_notebook}, wxDefaultSize, $self->{objects}, $self->{model}, $self->{config}); - $self->{preview_notebook}->AddPage($self->{canvas}, '2D'); - $self->{canvas}->on_select_object(sub { + # Initialize handlers for canvases + my $on_select_object = sub { my ($obj_idx) = @_; $self->select_object($obj_idx); - }); - $self->{canvas}->on_double_click(sub { - $self->object_cut_dialog if $self->selected_object; - }); - $self->{canvas}->on_right_click(sub { - my ($click_pos) = @_; + }; + my $on_double_click = sub { + $self->object_settings_dialog if $self->selected_object; + }; + my $on_right_click = sub { + my ($canvas, $click_pos) = @_; my ($obj_idx, $object) = $self->selected_object; return if !defined $obj_idx; my $menu = $self->object_menu; - $self->{canvas}->PopupMenu($menu, $click_pos); + $canvas->PopupMenu($menu, $click_pos); $menu->Destroy; - }); - $self->{canvas}->on_instance_moved(sub { + }; + my $on_instance_moved = sub { my ($obj_idx, $instance_idx) = @_; $self->update; @@ -100,12 +98,27 @@ sub new { } else { $self->resume_background_process; } - }); + }; - # Initialize 3D preview canvas + # Initialize 2D preview canvas + $self->{canvas} = Slic3r::GUI::Plater::2D->new($self->{preview_notebook}, wxDefaultSize, $self->{objects}, $self->{model}, $self->{config}); + $self->{preview_notebook}->AddPage($self->{canvas}, '2D'); + $self->{canvas}->on_select_object($on_select_object); + $self->{canvas}->on_double_click($on_double_click); + $self->{canvas}->on_right_click(sub { $on_right_click->($self->{canvas}, @_); }); + $self->{canvas}->on_instance_moved($on_instance_moved); + + # Initialize 3D preview and toolpaths preview if ($Slic3r::GUI::have_OpenGL) { $self->{canvas3D} = Slic3r::GUI::Plater::3D->new($self->{preview_notebook}, $self->{objects}, $self->{model}, $self->{config}); $self->{preview_notebook}->AddPage($self->{canvas3D}, '3D'); + $self->{canvas3D}->set_on_select_object($on_select_object); + $self->{canvas3D}->set_on_double_click($on_double_click); + $self->{canvas3D}->set_on_right_click(sub { $on_right_click->($self->{canvas3D}, @_); }); + $self->{canvas3D}->set_on_instance_moved($on_instance_moved); + + $self->{toolpaths2D} = Slic3r::GUI::Plater::2DToolpaths->new($self->{preview_notebook}, $self->{print}); + $self->{preview_notebook}->AddPage($self->{toolpaths2D}, 'Preview'); } # toolbar for object manipulation @@ -124,7 +137,7 @@ sub new { $self->{htoolbar}->AddTool(TB_45CW, "45° cw", Wx::Bitmap->new("$Slic3r::var/arrow_rotate_clockwise.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_SCALE, "Scale…", Wx::Bitmap->new("$Slic3r::var/arrow_out.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_SPLIT, "Split", Wx::Bitmap->new("$Slic3r::var/shape_ungroup.png", wxBITMAP_TYPE_PNG), ''); - $self->{htoolbar}->AddTool(TB_VIEW, "Cut…", Wx::Bitmap->new("$Slic3r::var/package.png", wxBITMAP_TYPE_PNG), ''); + $self->{htoolbar}->AddTool(TB_CUT, "Cut…", Wx::Bitmap->new("$Slic3r::var/package.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddSeparator; $self->{htoolbar}->AddTool(TB_SETTINGS, "Settings…", Wx::Bitmap->new("$Slic3r::var/cog.png", wxBITMAP_TYPE_PNG), ''); } else { @@ -137,20 +150,20 @@ sub new { decrease => "", rotate45ccw => "", rotate45cw => "", - rotate => "Rotate…", changescale => "Scale…", split => "Split", - view => "View/Cut…", + cut => "Cut…", settings => "Settings…", ); $self->{btoolbar} = Wx::BoxSizer->new(wxHORIZONTAL); - for (qw(add remove reset arrange increase decrease rotate45ccw rotate45cw rotate changescale split view settings)) { + for (qw(add remove reset arrange increase decrease rotate45ccw rotate45cw changescale split cut settings)) { $self->{"btn_$_"} = Wx::Button->new($self, -1, $tbar_buttons{$_}, wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT); $self->{btoolbar}->Add($self->{"btn_$_"}); } } - $self->{list} = Wx::ListView->new($self, -1, wxDefaultPosition, [250,-1], wxLC_SINGLE_SEL | wxLC_REPORT | wxBORDER_SUNKEN | wxTAB_TRAVERSAL | wxWANTS_CHARS); + $self->{list} = Wx::ListView->new($self, -1, wxDefaultPosition, wxDefaultSize, + wxLC_SINGLE_SEL | wxLC_REPORT | wxBORDER_SUNKEN | wxTAB_TRAVERSAL | wxWANTS_CHARS ); $self->{list}->InsertColumn(0, "Name", wxLIST_FORMAT_LEFT, 145); $self->{list}->InsertColumn(1, "Copies", wxLIST_FORMAT_CENTER, 45); $self->{list}->InsertColumn(2, "Scale", wxLIST_FORMAT_CENTER, wxLIST_AUTOSIZE_USEHEADER); @@ -169,11 +182,8 @@ sub new { # right pane buttons $self->{btn_export_gcode} = Wx::Button->new($self, -1, "Export G-code…", wxDefaultPosition, [-1, 30], wxBU_LEFT); $self->{btn_export_stl} = Wx::Button->new($self, -1, "Export STL…", wxDefaultPosition, [-1, 30], wxBU_LEFT); - $self->{btn_toolpaths_preview} = Wx::Button->new($self, -1, "Toolpaths preview…", wxDefaultPosition, [-1, 30], wxBU_LEFT); - $self->{btn_export_gcode}->SetFont($Slic3r::GUI::small_font); - $self->{btn_export_stl}->SetFont($Slic3r::GUI::small_font); - $self->{btn_toolpaths_preview}->SetFont($Slic3r::GUI::small_font); - $self->{btn_toolpaths_preview}->Disable; + #$self->{btn_export_gcode}->SetFont($Slic3r::GUI::small_font); + #$self->{btn_export_stl}->SetFont($Slic3r::GUI::small_font); if ($Slic3r::GUI::have_button_icons) { my %icons = qw( @@ -183,7 +193,6 @@ sub new { arrange bricks.png export_gcode cog_go.png export_stl brick_go.png - toolpaths_preview joystick.png increase add.png decrease delete.png @@ -191,7 +200,7 @@ sub new { rotate45ccw arrow_rotate_anticlockwise.png changescale arrow_out.png split shape_ungroup.png - view package.png + cut package.png settings cog.png ); for (grep $self->{"btn_$_"}, keys %icons) { @@ -204,8 +213,7 @@ sub new { $self->export_gcode; Slic3r::thread_cleanup(); }); - EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); - EVT_BUTTON($self, $self->{btn_toolpaths_preview}, \&toolpaths_preview); + #EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); if ($self->{htoolbar}) { EVT_TOOL($self, TB_ADD, sub { $self->add; }); @@ -218,7 +226,7 @@ sub new { EVT_TOOL($self, TB_45CCW, sub { $_[0]->rotate(45) }); EVT_TOOL($self, TB_SCALE, sub { $self->changescale(undef); }); EVT_TOOL($self, TB_SPLIT, sub { $self->split_object; }); - EVT_TOOL($self, TB_VIEW, sub { $_[0]->object_cut_dialog }); + EVT_TOOL($self, TB_CUT, sub { $_[0]->object_cut_dialog }); EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog }); } else { EVT_BUTTON($self, $self->{btn_add}, sub { $self->add; }); @@ -231,12 +239,12 @@ sub new { EVT_BUTTON($self, $self->{btn_rotate45ccw}, sub { $_[0]->rotate(45) }); EVT_BUTTON($self, $self->{btn_changescale}, sub { $self->changescale(undef); }); EVT_BUTTON($self, $self->{btn_split}, sub { $self->split_object; }); - EVT_BUTTON($self, $self->{btn_view}, sub { $_[0]->object_cut_dialog }); + EVT_BUTTON($self, $self->{btn_cut}, sub { $_[0]->object_cut_dialog }); EVT_BUTTON($self, $self->{btn_settings}, sub { $_[0]->object_settings_dialog }); } $_->SetDropTarget(Slic3r::GUI::Plater::DropTarget->new($self)) - for $self, $self->{canvas}, $self->{list}; + for grep defined($_), $self, $self->{canvas}, $self->{canvas3D}, $self->{list}; EVT_COMMAND($self, -1, $THUMBNAIL_DONE_EVENT, sub { my ($self, $event) = @_; @@ -274,32 +282,33 @@ sub new { }); $self->{canvas}->update_bed_size; + if ($self->{canvas3D}) { + $self->{canvas3D}->update_bed_size; + $self->{canvas3D}->zoom_to_bed; + } $self->update; { my $presets; if ($self->GetFrame->{mode} eq 'expert') { - $presets = Wx::BoxSizer->new(wxVERTICAL); + $presets = $self->{presets_sizer} = Wx::FlexGridSizer->new(3, 2, 1, 2); + $presets->AddGrowableCol(1, 1); + $presets->SetFlexibleDirection(wxHORIZONTAL); my %group_labels = ( print => 'Print settings', filament => 'Filament', printer => 'Printer', ); $self->{preset_choosers} = {}; - $self->{preset_choosers_sizers} = {}; for my $group (qw(print filament printer)) { my $text = Wx::StaticText->new($self, -1, "$group_labels{$group}:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT); $text->SetFont($Slic3r::GUI::small_font); - my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, [140, -1], []); + my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, wxDefaultSize, []); $choice->SetFont($Slic3r::GUI::small_font); $self->{preset_choosers}{$group} = [$choice]; EVT_CHOICE($choice, $choice, sub { $self->_on_select_preset($group, @_) }); - - $self->{preset_choosers_sizers}{$group} = Wx::BoxSizer->new(wxVERTICAL); - $self->{preset_choosers_sizers}{$group}->Add($choice, 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); - - $presets->Add($text, 0, wxALIGN_LEFT | wxRIGHT, 4); - $presets->Add($self->{preset_choosers_sizers}{$group}, 0, wxALIGN_CENTER_VERTICAL | wxBOTTOM, 8); + $presets->Add($text, 0, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); + $presets->Add($choice, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxBOTTOM, 8); } } @@ -343,23 +352,20 @@ sub new { } } - my $right_buttons_sizer = Wx::BoxSizer->new(wxVERTICAL); - $right_buttons_sizer->Add($presets, 0, wxEXPAND, 0) if defined $presets; - $right_buttons_sizer->Add($self->{btn_export_gcode}, 0, wxEXPAND | wxTOP, 8); - $right_buttons_sizer->Add($self->{btn_toolpaths_preview}, 0, wxEXPAND | wxTOP, 2); - $right_buttons_sizer->Add($self->{btn_export_stl}, 0, wxEXPAND | wxTOP, 2); - - my $right_top_sizer = Wx::BoxSizer->new(wxHORIZONTAL); - $right_top_sizer->Add($self->{list}, 1, wxEXPAND | wxLEFT, 5); - $right_top_sizer->Add($right_buttons_sizer, 0, wxEXPAND | wxALL, 10); + my $buttons_sizer = Wx::BoxSizer->new(wxHORIZONTAL); + $buttons_sizer->AddStretchSpacer(1); + $buttons_sizer->Add($self->{btn_export_stl}, 0, wxALIGN_RIGHT, 0); + $buttons_sizer->Add($self->{btn_export_gcode}, 0, wxALIGN_RIGHT, 0); my $right_sizer = Wx::BoxSizer->new(wxVERTICAL); - $right_sizer->Add($right_top_sizer, 1, wxEXPAND | wxBOTTOM, 10); - $right_sizer->Add($object_info_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, 5); + $right_sizer->Add($presets, 0, wxEXPAND | wxTOP, 10) if defined $presets; + $right_sizer->Add($buttons_sizer, 0, wxEXPAND | wxBOTTOM, 5); + $right_sizer->Add($self->{list}, 1, wxEXPAND, 5); + $right_sizer->Add($object_info_sizer, 0, wxEXPAND, 0); my $hsizer = Wx::BoxSizer->new(wxHORIZONTAL); $hsizer->Add($self->{preview_notebook}, 1, wxEXPAND | wxTOP, 1); - $hsizer->Add($right_sizer, 0, wxEXPAND | wxBOTTOM, 0); + $hsizer->Add($right_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, 3); my $sizer = Wx::BoxSizer->new(wxVERTICAL); $sizer->Add($self->{htoolbar}, 0, wxEXPAND, 0) if $self->{htoolbar}; @@ -473,7 +479,7 @@ sub load_model_objects { $need_arrange = 1; # add a default instance and center object around origin - $o->center_around_origin; + $o->center_around_origin; # also aligns object to Z = 0 $o->add_instance(offset => $bed_centerf); } @@ -507,6 +513,11 @@ sub objects_loaded { } $self->arrange unless $params{no_arrange}; $self->update; + + # zoom to objects + $self->{canvas3D}->zoom_to_volumes + if $self->{canvas3D}; + $self->{list}->Update; $self->{list}->Select($obj_idxs->[-1], 1); $self->object_list_changed; @@ -528,6 +539,9 @@ sub remove { $self->stop_background_process; + # Prevent toolpaths preview from rendering while we modify the Print object + $self->{toolpaths2D}->enabled(0) if $self->{toolpaths2D}; + # if no object index is supplied, remove the selected one if (!defined $obj_idx) { ($obj_idx, undef) = $self->selected_object; @@ -550,6 +564,9 @@ sub reset { $self->stop_background_process; + # Prevent toolpaths preview from rendering while we modify the Print object + $self->{toolpaths2D}->enabled(0) if $self->{toolpaths2D}; + @{$self->{objects}} = (); $self->{model}->clear_objects; $self->{print}->clear_objects; @@ -653,7 +670,6 @@ sub rotate { $self->selection_changed; # refresh info (size etc.) $self->update; - $self->{canvas}->Refresh; } sub flip { @@ -682,7 +698,7 @@ sub flip { $self->selection_changed; # refresh info (size etc.) $self->update; - $self->{canvas}->Refresh; + $self->refresh_canvases; } sub changescale { @@ -737,7 +753,7 @@ sub changescale { $self->selection_changed(1); # refresh info (size, volume etc.) $self->update; - $self->{canvas}->Refresh; + $self->refresh_canvases; } sub arrange { @@ -763,14 +779,17 @@ sub arrange { } else { $self->resume_background_process; } - $self->{canvas}->Refresh; + $self->refresh_canvases; } sub split_object { my $self = shift; my ($obj_idx, $current_object) = $self->selected_object; - my $current_model_object = $self->{model}->objects->[$obj_idx]; + + # we clone model object because split_object() adds the split volumes + # into the same model object, thus causing duplicated when we call load_model_objects() + my $current_model_object = $self->{model}->clone->objects->[$obj_idx]; if (@{$current_model_object->volumes} > 1) { Slic3r::GUI::warning_catcher($self)->("The selected object can't be split because it contains more than one volume/material."); @@ -781,7 +800,7 @@ sub split_object { my @model_objects = @{$current_model_object->split_object}; if (@model_objects == 1) { - Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it already contains a single part."); + Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains only one part."); return; } @@ -809,7 +828,7 @@ sub schedule_background_process { if (defined $self->{apply_config_timer}) { $self->{apply_config_timer}->Start(PROCESS_DELAY, 1); # 1 = one shot - $self->{btn_toolpaths_preview}->Disable; + $self->{toolpaths2D}->reload_print; } } @@ -853,7 +872,10 @@ sub start_background_process { $self->GetFrame->config->validate; $self->{print}->validate; }; - return if $@; + if ($@) { + $self->statusbar->SetStatusText($@); + return; + } # apply extra variables { @@ -869,9 +891,9 @@ sub start_background_process { }; if ($@) { Slic3r::debugf "Discarding background process error: $@\n"; - Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, 0)); + Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, $@)); } else { - Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, 1)); + Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $PROCESS_COMPLETED_EVENT, undef)); } Slic3r::thread_cleanup(); }); @@ -885,7 +907,7 @@ sub stop_background_process { $self->statusbar->SetCancelCallback(undef); $self->statusbar->StopBusy; $self->statusbar->SetStatusText(""); - $self->{btn_toolpaths_preview}->Disable; + $self->{toolpaths2D}->reload_print; if ($self->{process_thread}) { Slic3r::debugf "Killing background process.\n"; @@ -907,7 +929,7 @@ sub pause_background_process { my ($self) = @_; if ($self->{process_thread} || $self->{export_thread}) { - Slic3r::pause_threads(); + Slic3r::pause_all_threads(); } elsif (defined $self->{apply_config_timer} && $self->{apply_config_timer}->IsRunning) { $self->{apply_config_timer}->Stop; } @@ -917,7 +939,7 @@ sub resume_background_process { my ($self) = @_; if ($self->{process_thread} || $self->{export_thread}) { - Slic3r::resume_threads(); + Slic3r::resume_all_threads(); } } @@ -993,18 +1015,18 @@ sub export_gcode { # This gets called only if we have threads. sub on_process_completed { - my ($self, $result) = @_; + my ($self, $error) = @_; $self->statusbar->SetCancelCallback(undef); $self->statusbar->StopBusy; - $self->statusbar->SetStatusText(""); + $self->statusbar->SetStatusText($error // ""); Slic3r::debugf "Background processing completed.\n"; $self->{process_thread}->detach if $self->{process_thread}; $self->{process_thread} = undef; - return if !$result; - $self->{btn_toolpaths_preview}->Enable; + return if $error; + $self->{toolpaths2D}->reload_print; # if we have an export filename, start a new thread for exporting G-code if ($self->{export_gcode_output_file}) { @@ -1138,7 +1160,7 @@ sub on_thumbnail_made { $self->{objects}[$obj_idx]->transform_thumbnail($self->{model}, $obj_idx); $self->update; - $self->{canvas}->Refresh; + $self->refresh_canvases; } # this method gets called whenever print center is changed or the objects' bounding box changes @@ -1150,8 +1172,7 @@ sub update { $self->{model}->center_instances_around_point($self->bed_centerf); } - $self->{canvas}->Refresh; - $self->{canvas3D}->update if $self->{canvas3D}; + $self->refresh_canvases; } sub on_extruders_change { @@ -1162,13 +1183,15 @@ sub on_extruders_change { my @presets = $choices->[0]->GetStrings; push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [@presets]); $choices->[-1]->SetFont($Slic3r::GUI::small_font); - $self->{preset_choosers_sizers}{filament}->Add($choices->[-1], 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); + $self->{presets_sizer}->Insert(4 + ($#$choices-1)*2, 0, 0); + $self->{presets_sizer}->Insert(5 + ($#$choices-1)*2, $choices->[-1], 0, wxEXPAND | wxBOTTOM, FILAMENT_CHOOSERS_SPACING); EVT_CHOICE($choices->[-1], $choices->[-1], sub { $self->_on_select_preset('filament', @_) }); my $i = first { $choices->[-1]->GetString($_) eq ($Slic3r::GUI::Settings->{presets}{"filament_" . $#$choices} || '') } 0 .. $#presets; $choices->[-1]->SetSelection($i || 0); } while (@$choices > $num_extruders) { - $self->{preset_choosers_sizers}{filament}->Remove(-1); + $self->{presets_sizer}->Remove(4 + ($#$choices-1)*2); # label + $self->{presets_sizer}->Remove(4 + ($#$choices-1)*2); # wxChoice $choices->[-1]->Destroy; pop @$choices; } @@ -1183,6 +1206,7 @@ sub on_config_change { $self->{config}->set($opt_key, $config->get($opt_key)); if ($opt_key eq 'bed_shape') { $self->{canvas}->update_bed_size; + $self->{canvas3D}->update_bed_size if $self->{canvas3D}; $self->update; } } @@ -1199,7 +1223,7 @@ sub list_item_deselected { if ($self->{list}->GetFirstSelected == -1) { $self->select_object(undef); - $self->{canvas}->Refresh; + $self->refresh_canvases; } } @@ -1209,7 +1233,7 @@ sub list_item_selected { my $obj_idx = $event->GetIndex; $self->select_object($obj_idx); - $self->{canvas}->Refresh; + $self->refresh_canvases; } sub list_item_activated { @@ -1266,10 +1290,12 @@ sub object_settings_dialog { $self->pause_background_process; $dlg->ShowModal; - # update thumbnail since parts may have changed - if ($dlg->PartsChanged) { - $self->make_thumbnail($obj_idx); - } + # update thumbnail since parts may have changed + if ($dlg->PartsChanged) { + # recenter and re-align to Z = 0 + $model_object->center_around_origin; + $self->make_thumbnail($obj_idx); + } # update print if ($dlg->PartsChanged || $dlg->PartSettingsChanged) { @@ -1281,19 +1307,6 @@ sub object_settings_dialog { } } -sub toolpaths_preview { - my ($self) = @_; - - # TODO: we should check whether steps are done in $print rather then checking the thread - if ($self->{process_thread}) { - Slic3r::GUI::show_error($self, "Unable to show preview while toolpaths are being generated."); - return; - } - - my $dlg = Slic3r::GUI::Plater::2DToolpaths::Dialog->new($self, $self->{print}); - $dlg->ShowModal; -} - sub object_list_changed { my $self = shift; @@ -1316,11 +1329,11 @@ sub selection_changed { my $method = $have_sel ? 'Enable' : 'Disable'; $self->{"btn_$_"}->$method - for grep $self->{"btn_$_"}, qw(remove increase decrease rotate45cw rotate45ccw changescale split view settings); + for grep $self->{"btn_$_"}, qw(remove increase decrease rotate45cw rotate45ccw changescale split cut settings); if ($self->{htoolbar}) { $self->{htoolbar}->EnableTool($_, $have_sel) - for (TB_REMOVE, TB_MORE, TB_FEWER, TB_45CW, TB_45CCW, TB_SCALE, TB_SPLIT, TB_VIEW, TB_SETTINGS); + for (TB_REMOVE, TB_MORE, TB_FEWER, TB_45CW, TB_45CCW, TB_SCALE, TB_SPLIT, TB_CUT, TB_SETTINGS); } if ($self->{object_info_size}) { # have we already loaded the info pane? @@ -1388,6 +1401,13 @@ sub selected_object { return ($obj_idx, $self->{objects}[$obj_idx]), } +sub refresh_canvases { + my ($self) = @_; + + $self->{canvas}->Refresh; + $self->{canvas3D}->update if $self->{canvas3D}; +} + sub validate_config { my $self = shift; diff --git a/lib/Slic3r/GUI/Plater/2D.pm b/lib/Slic3r/GUI/Plater/2D.pm index e845cbe9a..a43f0a061 100644 --- a/lib/Slic3r/GUI/Plater/2D.pm +++ b/lib/Slic3r/GUI/Plater/2D.pm @@ -210,14 +210,13 @@ sub mouse_event { } } $self->Refresh; - } elsif ($event->ButtonUp(&Wx::wxMOUSE_BTN_LEFT)) { + } elsif ($event->LeftUp) { $self->{on_instance_moved}->(@{ $self->{drag_object} }) if $self->{drag_object}; - $self->Refresh; $self->{drag_start_pos} = undef; $self->{drag_object} = undef; $self->SetCursor(wxSTANDARD_CURSOR); - } elsif ($event->ButtonDClick) { + } elsif ($event->LeftDClick) { $self->{on_double_click}->(); } elsif ($event->Dragging) { return if !$self->{drag_start_pos}; # concurrency problems diff --git a/lib/Slic3r/GUI/Plater/2DToolpaths.pm b/lib/Slic3r/GUI/Plater/2DToolpaths.pm index 8e48a7290..a0721acd0 100644 --- a/lib/Slic3r/GUI/Plater/2DToolpaths.pm +++ b/lib/Slic3r/GUI/Plater/2DToolpaths.pm @@ -3,21 +3,19 @@ use strict; use warnings; use utf8; -use List::Util qw(); -use Slic3r::Geometry qw(); -use Wx qw(:misc :sizer :slider :statictext); +use Slic3r::Print::State ':steps'; +use Wx qw(:misc :sizer :slider :statictext wxWHITE); use Wx::Event qw(EVT_SLIDER EVT_KEY_DOWN); -use base 'Wx::Panel'; +use base qw(Wx::Panel Class::Accessor); + +__PACKAGE__->mk_accessors(qw(print enabled)); sub new { my $class = shift; my ($parent, $print) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition); - - # init print - $self->{print} = $print; - $self->reload_print; + $self->SetBackgroundColour(wxWHITE); # init GUI elements my $canvas = $self->{canvas} = Slic3r::GUI::Plater::2DToolpaths::Canvas->new($self, $print); @@ -25,7 +23,9 @@ sub new { $self, -1, 0, # default 0, # min - scalar(@{$self->{layers_z}})-1, # max + # we set max to a bogus non-zero value because the MSW implementation of wxSlider + # will skip drawing the slider if max <= min: + 1, # max wxDefaultPosition, wxDefaultSize, wxVERTICAL | wxSL_INVERSE, @@ -43,7 +43,8 @@ sub new { $sizer->Add($vsizer, 0, wxTOP | wxBOTTOM | wxEXPAND, 5); EVT_SLIDER($self, $slider, sub { - $self->set_z($self->{layers_z}[$slider->GetValue]); + $self->set_z($self->{layers_z}[$slider->GetValue]) + if $self->enabled; }); EVT_KEY_DOWN($canvas, sub { my ($s, $event) = @_; @@ -62,7 +63,9 @@ sub new { $self->SetMinSize($self->GetSize); $sizer->SetSizeHints($self); - $self->set_z($self->{layers_z}[0]); + # init print + $self->{print} = $print; + $self->reload_print; return $self; } @@ -70,18 +73,41 @@ sub new { sub reload_print { my ($self) = @_; + # we require that there's at least one object and the posSlice step + # is performed on all of them (this ensures that _shifted_copies was + # populated and we know the number of layers) + if (!$self->print->object_step_done(STEP_SLICE)) { + $self->enabled(0); + $self->{slider}->Hide; + $self->{canvas}->Refresh; # clears canvas + return; + } + + $self->{canvas}->bb($self->print->total_bounding_box); + my %z = (); # z => 1 foreach my $object (@{$self->{print}->objects}) { foreach my $layer (@{$object->layers}, @{$object->support_layers}) { $z{$layer->print_z} = 1; } } + $self->enabled(1); $self->{layers_z} = [ sort { $a <=> $b } keys %z ]; + $self->{slider}->SetRange(0, scalar(@{$self->{layers_z}})-1); + if ((my $z_idx = $self->{slider}->GetValue) <= $#{$self->{layers_z}}) { + $self->set_z($self->{layers_z}[$z_idx]); + } else { + $self->{slider}->SetValue(0); + $self->set_z($self->{layers_z}[0]) if @{$self->{layers_z}}; + } + $self->{slider}->Show; + $self->Layout; } sub set_z { my ($self, $z) = @_; + return if !$self->enabled; $self->{z_label}->SetLabel(sprintf '%.2f', $z); $self->{canvas}->set_z($z); } @@ -89,14 +115,15 @@ sub set_z { package Slic3r::GUI::Plater::2DToolpaths::Canvas; -use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_ERASE_BACKGROUND EVT_IDLE EVT_MOUSEWHEEL EVT_MOUSE_EVENTS); -use OpenGL qw(:glconstants :glfunctions :glufunctions); +use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_ERASE_BACKGROUND EVT_MOUSEWHEEL EVT_MOUSE_EVENTS); +use OpenGL qw(:glconstants :glfunctions :glufunctions :gluconstants); use base qw(Wx::GLCanvas Class::Accessor); use Wx::GLCanvas qw(:all); use List::Util qw(min first); use Slic3r::Geometry qw(scale unscale epsilon); +use Slic3r::Print::State ':steps'; -__PACKAGE__->mk_accessors(qw(print z layers color init dirty bb)); +__PACKAGE__->mk_accessors(qw(print z layers color init bb)); # make OpenGL::Array thread-safe { @@ -109,15 +136,12 @@ sub new { my $self = $class->SUPER::new($parent); $self->print($print); - $self->bb($self->print->total_bounding_box); 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; + EVT_SIZE($self, sub { return if !$self->IsShownOnScreen; $self->Resize( $self->GetSizeWH ); $self->Refresh; @@ -152,7 +176,7 @@ sub set_z { $self->z($z); $self->layers([ @layers ]); - $self->dirty(1); + $self->Refresh; } sub Render { @@ -164,6 +188,15 @@ sub Render { $self->SetCurrent($context); $self->InitGL; + glClearColor(1, 1, 1, 0); + glClear(GL_COLOR_BUFFER_BIT); + + if (!$self->GetParent->enabled || !$self->layers) { + glFlush(); + $self->SwapBuffers; + return; + } + glMatrixMode(GL_PROJECTION); glLoadIdentity(); my $bb = $self->bb; @@ -184,8 +217,66 @@ sub Render { glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - glClearColor(1, 1, 1, 0); - glClear(GL_COLOR_BUFFER_BIT); + # anti-alias + if (0) { + glEnable(GL_LINE_SMOOTH); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glHint(GL_LINE_SMOOTH_HINT, GL_DONT_CARE); + glHint(GL_POLYGON_SMOOTH_HINT, GL_DONT_CARE); + } + + my $tess; + if (!(&Wx::wxMSW && $OpenGL::VERSION < 0.6704)) { + # We can't use the GLU tesselator on MSW with older OpenGL versions + # because of an upstream bug: + # http://sourceforge.net/p/pogl/bugs/16/ + $tess = gluNewTess(); + gluTessCallback($tess, GLU_TESS_BEGIN, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_END, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_VERTEX, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_COMBINE, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_ERROR, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_EDGE_FLAG, 'DEFAULT'); + } + + foreach my $layer (@{$self->layers}) { + my $object = $layer->object; + + # only draw the slice for the current layer + next unless abs($layer->print_z - $self->z) < epsilon; + + # draw slice contour + glLineWidth(1); + foreach my $copy (@{ $object->_shifted_copies }) { + glPushMatrix(); + glTranslatef(@$copy, 0); + + foreach my $slice (@{$layer->slices}) { + glColor3f(0.95, 0.95, 0.95); + + if ($tess) { + gluTessBeginPolygon($tess); + foreach my $polygon (@$slice) { + gluTessBeginContour($tess); + gluTessVertex_p($tess, @$_, 0) for @$polygon; + gluTessEndContour($tess); + } + gluTessEndPolygon($tess); + } + + glColor3f(0.9, 0.9, 0.9); + foreach my $polygon (@$slice) { + foreach my $line (@{$polygon->lines}) { + glBegin(GL_LINES); + glVertex2f(@{$line->a}); + glVertex2f(@{$line->b}); + glEnd(); + } + } + } + glPopMatrix(); + } + } my $skirt_drawn = 0; my $brim_drawn = 0; @@ -194,32 +285,41 @@ sub Render { my $print_z = $layer->print_z; # draw brim - if ($layer->id == 0 && !$brim_drawn) { + if ($self->print->step_done(STEP_BRIM) && $layer->id == 0 && !$brim_drawn) { $self->color([0, 0, 0]); $self->_draw(undef, $print_z, $_) for @{$self->print->brim}; $brim_drawn = 1; } - if (($self->print->config->skirt_height == -1 || $self->print->config->skirt_height >= $layer->id) && !$skirt_drawn) { + if ($self->print->step_done(STEP_SKIRT) + && ($self->print->config->skirt_height == -1 || $self->print->config->skirt_height > $layer->id) + && !$skirt_drawn) { $self->color([0, 0, 0]); $self->_draw(undef, $print_z, $_) for @{$self->print->skirt}; $skirt_drawn = 1; } foreach my $layerm (@{$layer->regions}) { - $self->color([0.7, 0, 0]); - $self->_draw($object, $print_z, $_) for @{$layerm->perimeters}; + if ($object->step_done(STEP_PERIMETERS)) { + $self->color([0.7, 0, 0]); + $self->_draw($object, $print_z, $_) for @{$layerm->perimeters}; + } - $self->color([0, 0, 0.7]); - $self->_draw($object, $print_z, $_) for map @$_, @{$layerm->fills}; + if ($object->step_done(STEP_INFILL)) { + $self->color([0, 0, 0.7]); + $self->_draw($object, $print_z, $_) for map @$_, @{$layerm->fills}; + } } - if ($layer->isa('Slic3r::Layer::Support')) { - $self->color([0, 0, 0]); - $self->_draw($object, $print_z, $_) for @{$layer->support_fills}; - $self->_draw($object, $print_z, $_) for @{$layer->support_interface_fills}; + if ($object->step_done(STEP_SUPPORTMATERIAL)) { + if ($layer->isa('Slic3r::Layer::Support')) { + $self->color([0, 0, 0]); + $self->_draw($object, $print_z, $_) for @{$layer->support_fills}; + $self->_draw($object, $print_z, $_) for @{$layer->support_interface_fills}; + } } } + gluDeleteTess($tess) if $tess; glFlush(); $self->SwapBuffers; } @@ -249,13 +349,15 @@ sub _draw_path { if (defined $object) { foreach my $copy (@{ $object->_shifted_copies }) { + glPushMatrix(); + glTranslatef(@$copy, 0); foreach my $line (@{$path->polyline->lines}) { - $line->translate(@$copy); glBegin(GL_LINES); glVertex2f(@{$line->a}); glVertex2f(@{$line->b}); glEnd(); } + glPopMatrix(); } } else { foreach my $line (@{$path->polyline->lines}) { @@ -299,8 +401,7 @@ sub Resize { my ($self, $x, $y) = @_; return unless $self->GetContext; - $self->dirty(0); - + $self->SetCurrent($self->GetContext); glViewport(0, 0, $x, $y); } diff --git a/lib/Slic3r/GUI/Plater/3D.pm b/lib/Slic3r/GUI/Plater/3D.pm index 8f6960b7d..a2b72033b 100644 --- a/lib/Slic3r/GUI/Plater/3D.pm +++ b/lib/Slic3r/GUI/Plater/3D.pm @@ -15,32 +15,98 @@ sub new { my ($parent, $objects, $model, $config) = @_; my $self = $class->SUPER::new($parent); + $self->enable_picking(1); + $self->enable_moving(1); $self->{objects} = $objects; $self->{model} = $model; $self->{config} = $config; $self->{on_select_object} = sub {}; - $self->{on_double_click} = sub {}; - $self->{on_right_click} = sub {}; $self->{on_instance_moved} = sub {}; - + $self->on_select(sub { + my ($volume_idx) = @_; + + my $obj_idx = undef; + if ($volume_idx != -1) { + $obj_idx = $self->{_volumes_inv}{$volume_idx}; + $self->volumes->[$_]->selected(1) for @{$self->{_volumes}{$obj_idx}}; + $self->Refresh; + } + $self->{on_select_object}->($obj_idx) + if $self->{on_select_object}; + }); + $self->on_hover(sub { + my ($volume_idx) = @_; + + my $obj_idx = $self->{_volumes_inv}{$volume_idx}; + $self->volumes->[$_]->hover(1) for @{$self->{_volumes}{$obj_idx}}; + }); + $self->on_move(sub { + my ($volume_idx) = @_; + + my $volume = $self->volumes->[$volume_idx]; + my $obj_idx = $self->{_volumes_inv}{$volume_idx}; + my $model_object = $self->{model}->get_object($obj_idx); + $model_object + ->instances->[$volume->instance_idx] + ->offset + ->translate($volume->origin->x, $volume->origin->y); #)) + $model_object->invalidate_bounding_box; + + $self->{on_instance_moved}->($obj_idx, $volume->instance_idx) + if $self->{on_instance_moved}; + }); return $self; } +sub set_on_select_object { + my ($self, $cb) = @_; + $self->{on_select_object} = $cb; +} + +sub set_on_double_click { + my ($self, $cb) = @_; + $self->on_double_click($cb); +} + +sub set_on_right_click { + my ($self, $cb) = @_; + $self->on_right_click($cb); +} + +sub set_on_instance_moved { + my ($self, $cb) = @_; + $self->{on_instance_moved} = $cb; +} + sub update { my ($self) = @_; + $self->{_volumes} = {}; # obj_idx => [ volume_idx, volume_idx ] + $self->{_volumes_inv} = {}; # volume_idx => obj_idx $self->reset_objects; - return if $self->{model}->objects_count == 0; - $self->set_bounding_box($self->{model}->bounding_box); - $self->set_bed_shape($self->{config}->bed_shape); + $self->update_bed_size; - foreach my $model_object (@{$self->{model}->objects}) { - $self->load_object($model_object, 1); + foreach my $obj_idx (0..$#{$self->{model}->objects}) { + my $model_object = $self->{model}->get_object($obj_idx); + my @volume_idxs = $self->load_object($model_object, 1); + + # store mapping between canvas volumes and model objects + $self->{_volumes}{$obj_idx} = [ @volume_idxs ]; + $self->{_volumes_inv}{$_} = $obj_idx for @volume_idxs; + + if ($self->{objects}[$obj_idx]->selected) { + $self->select_volume($_) for @volume_idxs; + } } } -1; +sub update_bed_size { + my ($self) = @_; + $self->set_bed_shape($self->{config}->bed_shape); +} + +1; \ No newline at end of file diff --git a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm index 169fab717..24a580ab4 100644 --- a/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectCutDialog.pm @@ -88,10 +88,10 @@ sub new { if ($Slic3r::GUI::have_OpenGL) { $canvas = $self->{canvas} = Slic3r::GUI::PreviewCanvas->new($self); $canvas->load_object($self->{model_object}); - $canvas->set_bounding_box($self->{model_object}->bounding_box); $canvas->set_auto_bed_shape; $canvas->SetSize([500,500]); $canvas->SetMinSize($canvas->GetSize); + $canvas->zoom_to_volumes; } $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL); diff --git a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm index d24aa8932..fb4b0291c 100644 --- a/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm +++ b/lib/Slic3r/GUI/Plater/ObjectPartsPanel.pm @@ -70,9 +70,9 @@ sub new { if ($Slic3r::GUI::have_OpenGL) { $canvas = $self->{canvas} = Slic3r::GUI::PreviewCanvas->new($self); $canvas->load_object($self->{model_object}); - $canvas->set_bounding_box($self->{model_object}->bounding_box); $canvas->set_auto_bed_shape; $canvas->SetSize([500,500]); + $canvas->zoom_to_volumes; } $self->{sizer} = Wx::BoxSizer->new(wxHORIZONTAL); @@ -214,7 +214,7 @@ sub on_btn_load { $new_volume->set_name(basename($input_file)); # apply the same translation we applied to the object - $new_volume->mesh->translate(@{$self->{model_object}->origin_translation}, 0); + $new_volume->mesh->translate(@{$self->{model_object}->origin_translation}); # set a default extruder value, since user can't add it manually $new_volume->config->set_ifndef('extruder', 0); @@ -226,7 +226,9 @@ sub on_btn_load { $self->reload_tree; if ($self->{canvas}) { + $self->{canvas}->reset_objects; $self->{canvas}->load_object($self->{model_object}); + $self->{canvas}->set_bounding_box($self->{model_object}->bounding_box); $self->{canvas}->Render; } } diff --git a/lib/Slic3r/GUI/PreviewCanvas.pm b/lib/Slic3r/GUI/PreviewCanvas.pm index 369a1058b..d9e418345 100644 --- a/lib/Slic3r/GUI/PreviewCanvas.pm +++ b/lib/Slic3r/GUI/PreviewCanvas.pm @@ -4,7 +4,7 @@ use warnings; use Wx::Event qw(EVT_PAINT EVT_SIZE EVT_ERASE_BACKGROUND EVT_IDLE EVT_MOUSEWHEEL EVT_MOUSE_EVENTS); # must load OpenGL *before* Wx::GLCanvas -use OpenGL qw(:glconstants :glfunctions :glufunctions); +use OpenGL qw(:glconstants :glfunctions :glufunctions :gluconstants); use base qw(Wx::GLCanvas Class::Accessor); use Math::Trig qw(asin); use List::Util qw(reduce min max first); @@ -12,21 +12,38 @@ use Slic3r::Geometry qw(X Y Z MIN MAX triangle_normal normalize deg2rad tan scal use Slic3r::Geometry::Clipper qw(offset_ex intersection_pl); use Wx::GLCanvas qw(:all); -__PACKAGE__->mk_accessors( qw(quat dirty init mview_init - object_bounding_box - volumes initpos - sphi stheta +__PACKAGE__->mk_accessors( qw(_quat _dirty init + enable_picking + enable_moving + on_hover + on_select + on_double_click + on_right_click + on_move + volumes + print + _sphi _stheta cutting_plane_z cut_lines_vertices + bed_shape bed_triangles bed_grid_lines origin + _mouse_pos + _hover_volume_idx + _drag_volume_idx + _drag_start_pos + _drag_start_xy + _dragged + _camera_target + _zoom ) ); use constant TRACKBALLSIZE => 0.8; use constant TURNTABLE_MODE => 1; use constant GROUND_Z => 0.02; use constant SELECTED_COLOR => [0,1,0,1]; +use constant HOVER_COLOR => [0.8,0.8,0,1]; use constant COLORS => [ [1,1,0], [1,0.5,0.5], [0.5,1,0.5], [0.5,0.5,1] ]; # make OpenGL::Array thread-safe @@ -43,73 +60,236 @@ sub new { my $self = $class->SUPER::new($parent, -1, Wx::wxDefaultPosition, Wx::wxDefaultSize, 0, "", [WX_GL_RGBA, WX_GL_DOUBLEBUFFER, WX_GL_DEPTH_SIZE, 16, 0]); - $self->quat((0, 0, 0, 1)); - $self->sphi(45); - $self->stheta(-45); + $self->_quat((0, 0, 0, 1)); + $self->_stheta(45); + $self->_sphi(45); + $self->_zoom(1); + + # 3D point in model space + $self->_camera_target(Slic3r::Pointf3->new(0,0,0)); $self->reset_objects; EVT_PAINT($self, sub { my $dc = Wx::PaintDC->new($self); - return if !$self->object_bounding_box; $self->Render($dc); }); - EVT_SIZE($self, sub { $self->dirty(1) }); + EVT_SIZE($self, sub { $self->_dirty(1) }); EVT_IDLE($self, sub { - return unless $self->dirty; + return unless $self->_dirty; return if !$self->IsShownOnScreen; - return if !$self->object_bounding_box; $self->Resize( $self->GetSizeWH ); $self->Refresh; }); EVT_MOUSEWHEEL($self, sub { my ($self, $e) = @_; - my $zoom = ($e->GetWheelRotation() / $e->GetWheelDelta() / 10); - $zoom = $zoom > 0 ? (1.0 + $zoom) : 1 / (1.0 - $zoom); - my @pos3d = $self->mouse_to_3d($e->GetX(), $e->GetY()); - $self->ZoomTo($zoom, $pos3d[0], $pos3d[1]); + # Calculate the zoom delta and apply it to the current zoom factor + my $zoom = $e->GetWheelRotation() / $e->GetWheelDelta(); + $zoom = max(min($zoom, 4), -4); + $zoom /= 10; + $self->_zoom($self->_zoom * (1-$zoom)); + # In order to zoom around the mouse point we need to translate + # the camera target + my $size = Slic3r::Pointf->new($self->GetSizeWH); + my $pos = Slic3r::Pointf->new($e->GetX, $size->y - $e->GetY); #- + $self->_camera_target->translate( + # ($pos - $size/2) represents the vector from the viewport center + # to the mouse point. By multiplying it by $zoom we get the new, + # transformed, length of such vector. + # Since we want that point to stay fixed, we move our camera target + # in the opposite direction by the delta of the length of such vector + # ($zoom - 1). We then scale everything by 1/$self->_zoom since + # $self->_camera_target is expressed in terms of model units. + -($pos->x - $size->x/2) * ($zoom) / $self->_zoom, + -($pos->y - $size->y/2) * ($zoom) / $self->_zoom, + 0, + ) if 0; + $self->_dirty(1); $self->Refresh; }); - EVT_MOUSE_EVENTS($self, sub { - my ($self, $e) = @_; - - if ($e->Dragging() && $e->LeftIsDown()) { - $self->handle_rotation($e); - } elsif ($e->Dragging() && $e->RightIsDown()) { - $self->handle_translation($e); - } elsif ($e->LeftUp() || $e->RightUp()) { - $self->initpos(undef); - } else { - $e->Skip(); - } - }); + EVT_MOUSE_EVENTS($self, \&mouse_event); return $self; } +sub mouse_event { + my ($self, $e) = @_; + + my $pos = Slic3r::Pointf->new($e->GetPositionXY); + if ($e->Entering && &Wx::wxMSW) { + # wxMSW needs focus in order to catch mouse wheel events + $self->SetFocus; + } elsif ($e->LeftDClick) { + $self->on_double_click->() + if $self->on_double_click; + } elsif ($e->LeftDown || $e->RightDown) { + # If user pressed left or right button we first check whether this happened + # on a volume or not. + my $volume_idx = $self->_hover_volume_idx // -1; + + # select volume in this 3D canvas + if ($self->enable_picking) { + $self->deselect_volumes; + $self->select_volume($volume_idx); + $self->Refresh; + } + + # propagate event through callback + $self->on_select->($volume_idx) + if $self->on_select; + + if ($volume_idx != -1) { + if ($e->LeftDown && $self->enable_moving) { + $self->_drag_volume_idx($volume_idx); + $self->_drag_start_pos($self->mouse_to_3d(@$pos)); + } elsif ($e->RightDown) { + # if right clicking on volume, propagate event through callback + $self->on_right_click->($e->GetPosition) + if $self->on_right_click; + } + } + } elsif ($e->Dragging && $e->LeftIsDown && defined($self->_drag_volume_idx)) { + # get volume being dragged + my $volume = $self->volumes->[$self->_drag_volume_idx]; + + # get new position at the same Z of the initial click point + my $mouse_ray = $self->mouse_ray($e->GetX, $e->GetY); + my $cur_pos = $mouse_ray->intersect_plane($self->_drag_start_pos->z); + + # calculate the translation vector + my $vector = $self->_drag_start_pos->vector_to($cur_pos); + + # apply new temporary volume origin and ignore Z + $volume->origin->translate($vector->x, $vector->y, 0); #,, + $self->_drag_start_pos($cur_pos); + $self->_dragged(1); + $self->Refresh; + } elsif ($e->Dragging && !defined $self->_hover_volume_idx) { + if ($e->LeftIsDown) { + # if dragging over blank area with left button, rotate + if (defined $self->_drag_start_pos) { + my $orig = $self->_drag_start_pos; + if (TURNTABLE_MODE) { + $self->_sphi($self->_sphi + ($pos->x - $orig->x) * TRACKBALLSIZE); + $self->_stheta($self->_stheta - ($pos->y - $orig->y) * TRACKBALLSIZE); #- + $self->_stheta(150) if $self->_stheta > 150; + $self->_stheta(0) if $self->_stheta < 0; + } else { + my $size = $self->GetClientSize; + my @quat = trackball( + $orig->x / ($size->width / 2) - 1, + 1 - $orig->y / ($size->height / 2), #/ + $pos->x / ($size->width / 2) - 1, + 1 - $pos->y / ($size->height / 2), #/ + ); + $self->_quat(mulquats($self->_quat, \@quat)); + } + $self->Refresh; + } + $self->_drag_start_pos($pos); + } elsif ($e->MiddleIsDown || $e->RightIsDown) { + # if dragging over blank area with right button, translate + + if (defined $self->_drag_start_xy) { + # get point in model space at Z = 0 + my $cur_pos = $self->mouse_ray($e->GetX, $e->GetY)->intersect_plane(0); + my $orig = $self->mouse_ray(@{$self->_drag_start_xy})->intersect_plane(0); + $self->_camera_target->translate( + @{$orig->vector_to($cur_pos)->negative}, + ); + $self->Refresh; + } + $self->_drag_start_xy($pos); + } + } elsif ($e->LeftUp || $e->RightUp) { + if ($self->on_move && defined $self->_drag_volume_idx) { + $self->on_move->($self->_drag_volume_idx) if $self->_dragged; + } + $self->_drag_volume_idx(undef); + $self->_drag_start_pos(undef); + $self->_drag_start_xy(undef); + $self->_dragged(undef); + } elsif ($e->Moving) { + $self->_mouse_pos($pos); + $self->Refresh; + } else { + $e->Skip(); + } +} + sub reset_objects { my ($self) = @_; $self->volumes([]); - $self->dirty(1); + $self->_dirty(1); } -# this method accepts a Slic3r::BoudingBox3f object -sub set_bounding_box { +sub zoom_to_bounding_box { my ($self, $bb) = @_; - $self->object_bounding_box($bb); - $self->dirty(1); + # calculate the zoom factor needed to adjust viewport to + # bounding box + my $max_size = max(@{$bb->size}) * 2; + my $min_viewport_size = min($self->GetSizeWH); + $self->_zoom($min_viewport_size / $max_size); + + # center view around bounding box center + $self->_camera_target($bb->center); +} + +sub zoom_to_bed { + my ($self) = @_; + + if ($self->bed_shape) { + $self->zoom_to_bounding_box($self->bed_bounding_box); + } +} + +sub zoom_to_volume { + my ($self, $volume_idx) = @_; + + my $volume = $self->volumes->[$volume_idx]; + my $bb = $volume->bounding_box; + $self->zoom_to_bounding_box($bb); +} + +sub zoom_to_volumes { + my ($self) = @_; + $self->zoom_to_bounding_box($self->volumes_bounding_box); +} + +sub volumes_bounding_box { + my ($self) = @_; + + my $bb = Slic3r::Geometry::BoundingBoxf3->new; + $bb->merge($_->bounding_box) for @{$self->volumes}; + return $bb; +} + +sub bed_bounding_box { + my ($self) = @_; + + my $bb = Slic3r::Geometry::BoundingBoxf3->new; + $bb->merge_point(Slic3r::Pointf3->new(@$_, 0)) for @{$self->bed_shape}; + return $bb; +} + +sub max_bounding_box { + my ($self) = @_; + + my $bb = $self->bed_bounding_box; + $bb->merge($self->volumes_bounding_box); + return $bb; } sub set_auto_bed_shape { my ($self, $bed_shape) = @_; # draw a default square bed around object center - my $max_size = max(@{ $self->object_bounding_box->size }); - my $center = $self->object_bounding_box->center; + my $max_size = max(@{ $self->volumes_bounding_box->size }); + my $center = $self->volumes_bounding_box->center; $self->set_bed_shape([ [ $center->x - $max_size, $center->y - $max_size ], #-- [ $center->x + $max_size, $center->y - $max_size ], #-- @@ -122,6 +302,8 @@ sub set_auto_bed_shape { sub set_bed_shape { my ($self, $bed_shape) = @_; + $self->bed_shape($bed_shape); + # triangulate bed my $expolygon = Slic3r::ExPolygon->new([ map [map scale($_), @$_], @$bed_shape ]); my $bed_bb = $expolygon->bounding_box; @@ -163,9 +345,11 @@ sub load_object { # sort volumes: non-modifiers first my @volumes = sort { ($a->modifier // 0) <=> ($b->modifier // 0) } @{$object->volumes}; + my @volumes_idx = (); foreach my $volume (@volumes) { - my @instances = $all_instances ? @{$object->instances} : $object->instances->[0]; - foreach my $instance (@instances) { + my @instance_idxs = $all_instances ? (0..$#{$object->instances}) : (0); + foreach my $instance_idx (@instance_idxs) { + my $instance = $object->instances->[$instance_idx]; my $mesh = $volume->mesh->clone; $instance->transform_mesh($mesh); @@ -178,24 +362,40 @@ sub load_object { my $color = [ @{COLORS->[ $color_idx % scalar(@{&COLORS}) ]} ]; push @$color, $volume->modifier ? 0.5 : 1; - push @{$self->volumes}, my $v = { - mesh => $mesh, - color => $color, - z_min => $z_min, - }; + push @{$self->volumes}, my $v = Slic3r::GUI::PreviewCanvas::Volume->new( + instance_idx => $instance_idx, + mesh => $mesh, + color => $color, + origin => Slic3r::Pointf3->new(0,0,-$z_min), + ); + push @volumes_idx, $#{$self->volumes}; { my $vertices = $mesh->vertices; my @verts = map @{ $vertices->[$_] }, map @$_, @{$mesh->facets}; - $v->{verts} = OpenGL::Array->new_list(GL_FLOAT, @verts); + $v->verts(OpenGL::Array->new_list(GL_FLOAT, @verts)); } { my @norms = map { @$_, @$_, @$_ } @{$mesh->normals}; - $v->{norms} = OpenGL::Array->new_list(GL_FLOAT, @norms); + $v->norms(OpenGL::Array->new_list(GL_FLOAT, @norms)); } } } + + return @volumes_idx; +} + +sub deselect_volumes { + my ($self) = @_; + $_->selected(0) for @{$self->volumes}; +} + +sub select_volume { + my ($self, $volume_idx) = @_; + + $self->volumes->[$volume_idx]->selected(1) + if $volume_idx != -1; } sub SetCuttingPlane { @@ -207,7 +407,7 @@ sub SetCuttingPlane { my @verts = (); foreach my $volume (@{$self->volumes}) { foreach my $volume (@{$self->volumes}) { - my $expolygons = $volume->{mesh}->slice([ $z + $volume->{z_min} ])->[0]; + my $expolygons = $volume->mesh->slice([ $z - $volume->origin->z ])->[0]; $expolygons = offset_ex([ map @$_, @$expolygons ], scale 0.1); foreach my $line (map @{$_->lines}, map @$_, @$expolygons) { @@ -328,73 +528,26 @@ sub mulquats { @$q1[3] * @$rq[3] - @$q1[0] * @$rq[0] - @$q1[1] * @$rq[1] - @$q1[2] * @$rq[2]) } -sub handle_rotation { - my ($self, $e) = @_; - - if (not defined $self->initpos) { - $self->initpos($e->GetPosition()); - } else { - my $orig = $self->initpos; - my $new = $e->GetPosition(); - my $size = $self->GetClientSize(); - if (TURNTABLE_MODE) { - $self->sphi($self->sphi + ($new->x - $orig->x)*TRACKBALLSIZE); - $self->stheta($self->stheta + ($new->y - $orig->y)*TRACKBALLSIZE); #- - } else { - my @quat = trackball($orig->x / ($size->width / 2) - 1, - 1 - $orig->y / ($size->height / 2), #/ - $new->x / ($size->width / 2) - 1, - 1 - $new->y / ($size->height / 2), #/ - ); - $self->quat(mulquats($self->quat, \@quat)); - } - $self->initpos($new); - $self->Refresh; - } -} - -sub handle_translation { - my ($self, $e) = @_; - - if (not defined $self->initpos) { - $self->initpos($e->GetPosition()); - } else { - my $new = $e->GetPosition(); - my $orig = $self->initpos; - my @orig3d = $self->mouse_to_3d($orig->x, $orig->y); #)() - my @new3d = $self->mouse_to_3d($new->x, $new->y); #)() - glTranslatef($new3d[0] - $orig3d[0], $new3d[1] - $orig3d[1], 0); - $self->initpos($new); - $self->Refresh; - } -} - sub mouse_to_3d { - my ($self, $x, $y) = @_; + my ($self, $x, $y, $z) = @_; my @viewport = glGetIntegerv_p(GL_VIEWPORT); # 4 items my @mview = glGetDoublev_p(GL_MODELVIEW_MATRIX); # 16 items my @proj = glGetDoublev_p(GL_PROJECTION_MATRIX); # 16 items - - my @projected = gluUnProject_p($x, $viewport[3] - $y, 1.0, @mview, @proj, @viewport); - return @projected; + + $y = $viewport[3] - $y; + $z //= glReadPixels_p($x, $y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT); + my @projected = gluUnProject_p($x, $y, $z, @mview, @proj, @viewport); + return Slic3r::Pointf3->new(@projected); } -sub ZoomTo { - my ($self, $factor, $tox, $toy) = @_; +sub mouse_ray { + my ($self, $x, $y) = @_; - return if !$self->init; - glTranslatef($tox, $toy, 0); - glMatrixMode(GL_MODELVIEW); - $self->Zoom($factor); - glTranslatef(-$tox, -$toy, 0); -} - -sub Zoom { - my ($self, $factor) = @_; - - glMatrixMode(GL_MODELVIEW); - glScalef($factor, $factor, 1); + return Slic3r::Linef3->new( + $self->mouse_to_3d($x, $y, 0), + $self->mouse_to_3d($x, $y, 1), + ); } sub GetContext { @@ -417,37 +570,26 @@ sub SetCurrent { } } -sub ResetModelView { - my ($self, $factor) = @_; - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - my $win_size = $self->GetClientSize(); - my $ratio = $factor * min($win_size->width, $win_size->height) / (2 * max(@{ $self->object_bounding_box->size })); - glScalef($ratio, $ratio, 1); -} - sub Resize { my ($self, $x, $y) = @_; return unless $self->GetContext; - $self->dirty(0); + $self->_dirty(0); $self->SetCurrent($self->GetContext); glViewport(0, 0, $x, $y); + $x /= $self->_zoom; + $y /= $self->_zoom; + glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho( -$x/2, $x/2, -$y/2, $y/2, - -200, 10 * max(@{ $self->object_bounding_box->size }), + -200, 10 * max(@{ $self->max_bounding_box->size }), ); glMatrixMode(GL_MODELVIEW); - unless ($self->mview_init) { - $self->mview_init(1); - $self->ResetModelView(0.9); - } } sub InitGL { @@ -466,6 +608,11 @@ sub InitGL { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + # Set antialiasing/multisampling + glDisable(GL_LINE_SMOOTH); + glDisable(GL_POLYGON_SMOOTH); + glEnable(GL_MULTISAMPLE); + # ambient lighting glLightModelfv_p(GL_LIGHT_MODEL_AMBIENT, 0.1, 0.1, 0.1, 1); @@ -508,22 +655,41 @@ sub Render { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); - glPushMatrix(); + glLoadIdentity(); - my $bb = $self->object_bounding_box; - my $object_size = $bb->size; - glTranslatef(0, 0, -max(@$object_size[0..1])); - my @rotmat = quat_to_rotmatrix($self->quat); - glMultMatrixd_p(@rotmat[0..15]); - glRotatef($self->stheta, 1, 0, 0); - glRotatef($self->sphi, 0, 0, 1); - - # center everything around 0,0 since that's where we're looking at (glOrtho()) - my $center = $bb->center; - glTranslatef(-$center->x, -$center->y, 0); #,, + if (TURNTABLE_MODE) { + glRotatef(-$self->_stheta, 1, 0, 0); # pitch + glRotatef($self->_sphi, 0, 0, 1); # yaw + } else { + my @rotmat = quat_to_rotmatrix($self->quat); + glMultMatrixd_p(@rotmat[0..15]); + } + glTranslatef(@{ $self->_camera_target->negative }); + if ($self->enable_picking) { + glDisable(GL_LIGHTING); + $self->draw_volumes(1); + glFlush(); + glFinish(); + + if (my $pos = $self->_mouse_pos) { + my $col = [ glReadPixels_p($pos->x, $self->GetSize->GetHeight - $pos->y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE) ]; + my $volume_idx = $col->[0] + $col->[1]*256 + $col->[2]*256*256; + $self->_hover_volume_idx(undef); + $_->hover(0) for @{$self->volumes}; + if ($volume_idx <= $#{$self->volumes}) { + $self->_hover_volume_idx($volume_idx); + $self->volumes->[$volume_idx]->hover(1); + $self->on_hover->($volume_idx) if $self->on_hover; + } + } + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glFlush(); + glFinish(); + glEnable(GL_LIGHTING); + } # draw objects - $self->draw_mesh; + $self->draw_volumes; # draw ground and axes glDisable(GL_LIGHTING); @@ -537,7 +703,7 @@ sub Render { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnableClientState(GL_VERTEX_ARRAY); - glColor4f(0.5, 0.5, 0.5, 0.3); + glColor4f(0.6, 0.7, 0.5, 0.3); glNormal3d(0,0,1); glVertexPointer_p(3, $self->bed_triangles); glDrawArrays(GL_TRIANGLES, 0, $self->bed_triangles->elements / 3); @@ -548,18 +714,23 @@ sub Render { # draw grid glTranslatef(0, 0, 0.02); glLineWidth(3); - glColor3f(1.0, 1.0, 1.0); + glColor3f(0.95, 0.95, 0.95); glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer_p(3, $self->bed_grid_lines); glDrawArrays(GL_LINES, 0, $self->bed_grid_lines->elements / 3); glDisableClientState(GL_VERTEX_ARRAY); } + my $volumes_bb = $self->volumes_bounding_box; + { # draw axes $ground_z += 0.02; my $origin = $self->origin; - my $axis_len = 2 * max(@{ $object_size }); + my $axis_len = max( + 0.3 * max(@{ $self->bed_bounding_box->size }), + 2 * max(@{ $volumes_bb->size }), + ); glLineWidth(2); glBegin(GL_LINES); # draw line for x axis @@ -580,6 +751,7 @@ sub Render { # draw cutting plane if (defined $self->cutting_plane_z) { my $plane_z = $z0 + $self->cutting_plane_z; + my $bb = $volumes_bb; glDisable(GL_CULL_FACE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -597,35 +769,107 @@ sub Render { glEnable(GL_LIGHTING); - glPopMatrix(); glFlush(); $self->SwapBuffers(); } -sub draw_mesh { - my $self = shift; +sub draw_volumes { + my ($self, $fakecolor) = @_; glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if (defined($self->print) && !$fakecolor) { + my $tess = gluNewTess(); + gluTessCallback($tess, GLU_TESS_BEGIN, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_END, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_VERTEX, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_COMBINE, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_ERROR, 'DEFAULT'); + gluTessCallback($tess, GLU_TESS_EDGE_FLAG, 'DEFAULT'); + + foreach my $object (@{$self->print->objects}) { + foreach my $layer (@{$object->layers}) { + my $gap = 0; + my $top_z = $layer->print_z; + my $bottom_z = $layer->print_z - $layer->height + $gap; + + foreach my $copy (@{ $object->_shifted_copies }) { + glPushMatrix(); + glTranslatef(map unscale($_), @$copy, 0); + + foreach my $slice (@{$layer->slices}) { + glColor3f(@{COLORS->[0]}); + gluTessBeginPolygon($tess); + glNormal3f(0,0,1); + foreach my $polygon (@$slice) { + gluTessBeginContour($tess); + gluTessVertex_p($tess, (map unscale($_), @$_), $layer->print_z) for @$polygon; + gluTessEndContour($tess); + } + gluTessEndPolygon($tess); + + foreach my $polygon (@$slice) { + foreach my $line (@{$polygon->lines}) { + if (0) { + glLineWidth(1); + glColor3f(0,0,0); + glBegin(GL_LINES); + glVertex3f((map unscale($_), @{$line->a}), $bottom_z); + glVertex3f((map unscale($_), @{$line->b}), $bottom_z); + glEnd(); + } + + glLineWidth(0); + glColor3f(@{COLORS->[0]}); + glBegin(GL_QUADS); + glNormal3f((map $_/$line->length, @{$line->normal}), 0); + glVertex3f((map unscale($_), @{$line->a}), $bottom_z); + glVertex3f((map unscale($_), @{$line->b}), $bottom_z); + glVertex3f((map unscale($_), @{$line->b}), $top_z); + glVertex3f((map unscale($_), @{$line->a}), $top_z); + glEnd(); + } + } + } + + glPopMatrix(); # copy + } + } + } + + gluDeleteTess($tess); + return; + } + glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_NORMAL_ARRAY); - foreach my $volume (@{$self->volumes}) { - glTranslatef(0, 0, -$volume->{z_min}); + foreach my $volume_idx (0..$#{$self->volumes}) { + my $volume = $self->volumes->[$volume_idx]; + glPushMatrix(); + glTranslatef(@{$volume->origin}); - glVertexPointer_p(3, $volume->{verts}); + glVertexPointer_p(3, $volume->verts); glCullFace(GL_BACK); - glNormalPointer_p($volume->{norms}); - if ($volume->{selected}) { + glNormalPointer_p($volume->norms); + if ($fakecolor) { + my $r = ($volume_idx & 0x000000FF) >> 0; + my $g = ($volume_idx & 0x0000FF00) >> 8; + my $b = ($volume_idx & 0x00FF0000) >> 16; + glColor4f($r/255.0, $g/255.0, $b/255.0, 1); + } elsif ($volume->selected) { glColor4f(@{ &SELECTED_COLOR }); + } elsif ($volume->hover) { + glColor4f(@{ &HOVER_COLOR }); } else { - glColor4f(@{ $volume->{color} }); + glColor4f(@{ $volume->color }); } - glDrawArrays(GL_TRIANGLES, 0, $volume->{verts}->elements / 3); + glDrawArrays(GL_TRIANGLES, 0, $volume->verts->elements / 3); - glTranslatef(0, 0, +$volume->{z_min}); + glPopMatrix(); } glDisableClientState(GL_NORMAL_ARRAY); glDisable(GL_BLEND); @@ -639,4 +883,24 @@ sub draw_mesh { glDisableClientState(GL_VERTEX_ARRAY); } +package Slic3r::GUI::PreviewCanvas::Volume; +use Moo; + +has 'mesh' => (is => 'ro', required => 1); +has 'color' => (is => 'ro', required => 1); +has 'instance_idx' => (is => 'ro', default => sub { 0 }); +has 'origin' => (is => 'rw', default => sub { Slic3r::Pointf3->new(0,0,0) }); +has 'verts' => (is => 'rw'); +has 'norms' => (is => 'rw'); +has 'selected' => (is => 'rw', default => sub { 0 }); +has 'hover' => (is => 'rw', default => sub { 0 }); + +sub bounding_box { + my ($self) = @_; + + my $bb = $self->mesh->bounding_box; + $bb->translate(@{$self->origin}); + return $bb; +} + 1; diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index 0121a8b20..ad32fc96a 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -8,7 +8,9 @@ use List::Util qw(first); use Wx qw(:bookctrl :dialog :keycode :icon :id :misc :panel :sizer :treectrl :window wxTheApp); use Wx::Event qw(EVT_BUTTON EVT_CHOICE EVT_KEY_DOWN EVT_TREE_SEL_CHANGED); -use base 'Wx::Panel'; +use base qw(Wx::Panel Class::Accessor); + +__PACKAGE__->mk_accessors(qw(current_preset)); sub new { my $class = shift; @@ -86,18 +88,17 @@ sub new { EVT_BUTTON($self, $self->{btn_save_preset}, sub { $self->save_preset }); EVT_BUTTON($self, $self->{btn_delete_preset}, sub { - my $i = $self->{presets_choice}->GetSelection; + my $i = $self->current_preset; return if $i == 0; # this shouldn't happen but let's trap it anyway my $res = Wx::MessageDialog->new($self, "Are you sure you want to delete the selected preset?", 'Delete Preset', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION)->ShowModal; return unless $res == wxID_YES; - if (-e $self->{presets}[$i]{file}) { - unlink $self->{presets}[$i]{file}; + if (-e $self->{presets}[$i]->file) { + unlink $self->{presets}[$i]->file; } splice @{$self->{presets}}, $i, 1; - $self->set_dirty(0); $self->{presets_choice}->Delete($i); - $self->{presets_choice}->SetSelection(0); - $self->on_select_preset; + $self->current_preset(undef); + $self->select_preset(0); $self->_on_presets_changed; }); @@ -111,14 +112,14 @@ sub new { return $self; } -sub current_preset { +sub get_current_preset { my $self = shift; - return $self->{presets}[ $self->{presets_choice}->GetSelection ]; + return $self->get_preset($self->current_preset); } sub get_preset { - my $self = shift; - return $self->{presets}[ $_[0] ]; + my ($self, $i) = @_; + return $self->{presets}[$i]; } sub save_preset { @@ -130,23 +131,22 @@ sub save_preset { $self->{treectrl}->SetFocus; if (!defined $name) { - my $preset = $self->current_preset; - my $default_name = $preset->{default} ? 'Untitled' : basename($preset->{name}); + my $preset = $self->get_current_preset; + my $default_name = $preset->default ? 'Untitled' : $preset->name; $default_name =~ s/\.ini$//i; my $dlg = Slic3r::GUI::SavePresetWindow->new($self, title => lc($self->title), default => $default_name, - values => [ map { my $name = $_->{name}; $name =~ s/\.ini$//i; $name } @{$self->{presets}} ], + values => [ map $_->name, grep !$_->default && !$_->external, @{$self->{presets}} ], ); return unless $dlg->ShowModal == wxID_OK; $name = $dlg->get_name; } $self->config->save(sprintf "$Slic3r::GUI::datadir/%s/%s.ini", $self->name, $name); - $self->set_dirty(0); $self->load_presets; - $self->select_preset(first { basename($self->{presets}[$_]{file}) eq $name . ".ini" } 1 .. $#{$self->{presets}}); + $self->select_preset_by_name($name); $self->_on_presets_changed; } @@ -185,7 +185,7 @@ sub config { $_[0]->{config}->clone } sub select_default_preset { my $self = shift; - $self->{presets_choice}->SetSelection(0); + $self->select_preset(0); } sub select_preset { @@ -194,21 +194,30 @@ sub select_preset { $self->on_select_preset; } +sub select_preset_by_name { + my ($self, $name) = @_; + $self->select_preset(first { $self->{presets}[$_]->name eq $name } 0 .. $#{$self->{presets}}); +} + sub on_select_preset { my $self = shift; - if ($self->{dirty}) { - my $name = $self->{dirty} == 0 ? 'Default preset' : "Preset \"$self->{presets}[$self->{dirty}]{name}\""; - my $confirm = Wx::MessageDialog->new($self, "$name has unsaved changes. Discard changes and continue anyway?", + if ($self->is_dirty) { + my $old_preset = $self->get_current_preset; + my $name = $old_preset->default ? 'Default preset' : "Preset \"" . $old_preset->name . "\""; + my $changes = join "\n", + map { "- " . ($Slic3r::Config::Options->{$_}{full_label} // $Slic3r::Config::Options->{$_}{label}) } + @{$self->dirty_options}; + my $confirm = Wx::MessageDialog->new($self, "$name has unsaved changes:\n$changes\n\nDiscard changes and continue anyway?", 'Unsaved Changes', wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); if ($confirm->ShowModal == wxID_NO) { - $self->{presets_choice}->SetSelection($self->{dirty}); + $self->{presets_choice}->SetSelection($self->current_preset); return; } - $self->set_dirty(0); } - my $preset = $self->current_preset; + $self->current_preset($self->{presets_choice}->GetSelection); + my $preset = $self->get_current_preset; my $preset_config = $self->get_preset_config($preset); eval { local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); @@ -216,53 +225,33 @@ sub on_select_preset { $self->{config}->set($opt_key, $preset_config->get($opt_key)) if $preset_config->has($opt_key); } - ($preset->{default} || $preset->{external}) + ($preset->default || $preset->external) ? $self->{btn_delete_preset}->Disable : $self->{btn_delete_preset}->Enable; $self->_update; $self->on_preset_loaded; $self->reload_config; - - # use CallAfter because some field triggers schedule on_change calls using CallAfter, - # and we don't want them to be called after this set_dirty(0) as they would mark the - # preset dirty again - wxTheApp->CallAfter(sub { - $self->set_dirty(0); - }); - $Slic3r::GUI::Settings->{presets}{$self->name} = $preset->{file} ? basename($preset->{file}) : ''; + $Slic3r::GUI::Settings->{presets}{$self->name} = $preset->file ? basename($preset->file) : ''; }; if ($@) { $@ = "I was unable to load the selected config file: $@"; Slic3r::GUI::catch_error($self); $self->select_default_preset; } + + # use CallAfter because some field triggers schedule on_change calls using CallAfter, + # and we don't want them to be called after this update_dirty() as they would mark the + # preset dirty again + # (not sure this is true anymore now that update_dirty is idempotent) + wxTheApp->CallAfter(sub { + $self->_on_presets_changed; + $self->update_dirty; + }); wxTheApp->save_settings; } -sub get_preset_config { - my $self = shift; - my ($preset) = @_; - - if ($preset->{default}) { - return Slic3r::Config->new_from_defaults(@{$self->{config}->get_keys}); - } else { - if (!-e $preset->{file}) { - Slic3r::GUI::show_error($self, "The selected preset does not exist anymore ($preset->{file})."); - return; - } - - # apply preset values on top of defaults - my $external_config = Slic3r::Config->load($preset->{file}); - my $config = Slic3r::Config->new; - $config->set($_, $external_config->get($_)) - for grep $external_config->has($_), @{$self->{config}->get_keys}; - - return $config; - } -} - sub init_config_options { my ($self, @opt_keys) = @_; $self->{config}->apply(Slic3r::Config->new_from_defaults(@opt_keys)); @@ -305,53 +294,52 @@ sub update_tree { } } -sub set_dirty { +sub update_dirty { my $self = shift; - my ($dirty) = @_; - - return if $dirty and $self->is_dirty; - return if (not $dirty) and (not $self->is_dirty); - my $selection = $self->{presets_choice}->GetSelection; - my $i = $self->{dirty} // $selection; #/ - my $text = $self->{presets_choice}->GetString($i); - - if ($dirty) { - $self->{dirty} = $i; - if ($text !~ / \(modified\)$/) { - $self->{presets_choice}->SetString($i, "$text (modified)"); - $self->{presets_choice}->SetSelection($selection); # http://trac.wxwidgets.org/ticket/13769 + foreach my $i (0..$#{$self->{presets}}) { + my $preset = $self->get_preset($i); + if ($i == $self->current_preset && $self->is_dirty) { + $self->{presets_choice}->SetString($i, $preset->name . " (modified)"); + } else { + $self->{presets_choice}->SetString($i, $preset->name); } - } else { - $self->{dirty} = undef; - $text =~ s/ \(modified\)$//; - $self->{presets_choice}->SetString($i, $text); - $self->{presets_choice}->SetSelection($selection); # http://trac.wxwidgets.org/ticket/13769 } + $self->{presets_choice}->SetSelection($self->current_preset); # http://trac.wxwidgets.org/ticket/13769 $self->_on_presets_changed; } sub is_dirty { my $self = shift; - return (defined $self->{dirty}); + return @{$self->dirty_options} > 0; +} + +sub dirty_options { + my $self = shift; + + return [] if !defined $self->current_preset; # happens during initialization + return $self->get_preset_config($self->get_current_preset)->diff($self->{config}); } sub load_presets { my $self = shift; - $self->{presets} = [{ - default => 1, - name => '- default -', - }]; + $self->{presets} = [ + Slic3r::GUI::Tab::Preset->new( + default => 1, + name => '- default -', + ), + ]; my %presets = wxTheApp->presets($self->name); foreach my $preset_name (sort keys %presets) { - push @{$self->{presets}}, { + push @{$self->{presets}}, Slic3r::GUI::Tab::Preset->new( name => $preset_name, file => $presets{$preset_name}, - }; + ); } + $self->current_preset(undef); $self->{presets_choice}->Clear; $self->{presets_choice}->Append($_->{name}) for @{$self->{presets}}; { @@ -370,11 +358,11 @@ sub load_config_file { my $i = first { $self->{presets}[$_]{file} eq $file && $self->{presets}[$_]{external} } 1..$#{$self->{presets}}; if (!$i) { my $preset_name = basename($file); # keep the .ini suffix - push @{$self->{presets}}, { + push @{$self->{presets}}, Slic3r::GUI::Tab::Preset->new( file => $file, name => $preset_name, external => 1, - }; + ); $self->{presets_choice}->Append($preset_name); $i = $#{$self->{presets}}; } @@ -389,11 +377,16 @@ sub load_config { foreach my $opt_key (@{$self->{config}->diff($config)}) { $self->{config}->set($opt_key, $config->get($opt_key)); - $self->set_dirty(1); + $self->update_dirty; } $self->reload_config; } +sub get_preset_config { + my ($self, $preset) = @_; + return $preset->config($self->{config}->get_keys); +} + sub get_field { my ($self, $opt_key, $opt_index) = @_; @@ -432,7 +425,7 @@ sub build { top_solid_layers bottom_solid_layers extra_perimeters avoid_crossing_perimeters thin_walls overhangs seam_position external_perimeters_first - fill_density fill_pattern solid_fill_pattern + fill_density fill_pattern external_fill_pattern infill_every_layers infill_only_where_needed solid_infill_every_layers fill_angle solid_infill_below_area only_retract_when_crossing_perimeters infill_first @@ -454,7 +447,8 @@ sub build { complete_objects extruder_clearance_radius extruder_clearance_height gcode_comments output_filename_format post_process - perimeter_extruder infill_extruder support_material_extruder support_material_interface_extruder + perimeter_extruder infill_extruder solid_infill_extruder + support_material_extruder support_material_interface_extruder ooze_prevention standby_temperature_delta interface_shells extrusion_width first_layer_extrusion_width perimeter_extrusion_width @@ -505,7 +499,7 @@ sub build { my $optgroup = $page->new_optgroup('Infill'); $optgroup->append_single_option_line('fill_density'); $optgroup->append_single_option_line('fill_pattern'); - $optgroup->append_single_option_line('solid_fill_pattern'); + $optgroup->append_single_option_line('external_fill_pattern'); } { my $optgroup = $page->new_optgroup('Reducing printing time'); @@ -522,39 +516,6 @@ sub build { } } - { - my $page = $self->add_options_page('Speed', 'time.png'); - { - my $optgroup = $page->new_optgroup('Speed for print moves'); - $optgroup->append_single_option_line('perimeter_speed'); - $optgroup->append_single_option_line('small_perimeter_speed'); - $optgroup->append_single_option_line('external_perimeter_speed'); - $optgroup->append_single_option_line('infill_speed'); - $optgroup->append_single_option_line('solid_infill_speed'); - $optgroup->append_single_option_line('top_solid_infill_speed'); - $optgroup->append_single_option_line('support_material_speed'); - $optgroup->append_single_option_line('support_material_interface_speed'); - $optgroup->append_single_option_line('bridge_speed'); - $optgroup->append_single_option_line('gap_fill_speed'); - } - { - my $optgroup = $page->new_optgroup('Speed for non-print moves'); - $optgroup->append_single_option_line('travel_speed'); - } - { - my $optgroup = $page->new_optgroup('Modifiers'); - $optgroup->append_single_option_line('first_layer_speed'); - } - { - my $optgroup = $page->new_optgroup('Acceleration control (advanced)'); - $optgroup->append_single_option_line('perimeter_acceleration'); - $optgroup->append_single_option_line('infill_acceleration'); - $optgroup->append_single_option_line('bridge_acceleration'); - $optgroup->append_single_option_line('first_layer_acceleration'); - $optgroup->append_single_option_line('default_acceleration'); - } - } - { my $page = $self->add_options_page('Skirt and brim', 'box.png'); { @@ -594,51 +555,35 @@ sub build { } { - my $page = $self->add_options_page('Notes', 'note.png'); + my $page = $self->add_options_page('Speed', 'time.png'); { - my $optgroup = $page->new_optgroup('Notes', - label_width => 0, - ); - my $option = $optgroup->get_option('notes'); - $option->full_width(1); - $option->height(250); - $optgroup->append_single_option_line($option); - } - } - - { - my $page = $self->add_options_page('Output options', 'page_white_go.png'); - { - my $optgroup = $page->new_optgroup('Sequential printing'); - $optgroup->append_single_option_line('complete_objects'); - my $line = Slic3r::GUI::OptionsGroup::Line->new( - label => 'Extruder clearance (mm)', - ); - foreach my $opt_key (qw(extruder_clearance_radius extruder_clearance_height)) { - my $option = $optgroup->get_option($opt_key); - $option->width(60); - $line->append_option($option); - } - $optgroup->append_line($line); + my $optgroup = $page->new_optgroup('Speed for print moves'); + $optgroup->append_single_option_line('perimeter_speed'); + $optgroup->append_single_option_line('small_perimeter_speed'); + $optgroup->append_single_option_line('external_perimeter_speed'); + $optgroup->append_single_option_line('infill_speed'); + $optgroup->append_single_option_line('solid_infill_speed'); + $optgroup->append_single_option_line('top_solid_infill_speed'); + $optgroup->append_single_option_line('support_material_speed'); + $optgroup->append_single_option_line('support_material_interface_speed'); + $optgroup->append_single_option_line('bridge_speed'); + $optgroup->append_single_option_line('gap_fill_speed'); } { - my $optgroup = $page->new_optgroup('Output file'); - $optgroup->append_single_option_line('gcode_comments'); - - { - my $option = $optgroup->get_option('output_filename_format'); - $option->full_width(1); - $optgroup->append_single_option_line($option); - } + my $optgroup = $page->new_optgroup('Speed for non-print moves'); + $optgroup->append_single_option_line('travel_speed'); } { - my $optgroup = $page->new_optgroup('Post-processing scripts', - label_width => 0, - ); - my $option = $optgroup->get_option('post_process'); - $option->full_width(1); - $option->height(50); - $optgroup->append_single_option_line($option); + my $optgroup = $page->new_optgroup('Modifiers'); + $optgroup->append_single_option_line('first_layer_speed'); + } + { + my $optgroup = $page->new_optgroup('Acceleration control (advanced)'); + $optgroup->append_single_option_line('perimeter_acceleration'); + $optgroup->append_single_option_line('infill_acceleration'); + $optgroup->append_single_option_line('bridge_acceleration'); + $optgroup->append_single_option_line('first_layer_acceleration'); + $optgroup->append_single_option_line('default_acceleration'); } } @@ -648,6 +593,7 @@ sub build { my $optgroup = $page->new_optgroup('Extruders'); $optgroup->append_single_option_line('perimeter_extruder'); $optgroup->append_single_option_line('infill_extruder'); + $optgroup->append_single_option_line('solid_infill_extruder'); $optgroup->append_single_option_line('support_material_extruder'); $optgroup->append_single_option_line('support_material_interface_extruder'); } @@ -688,6 +634,55 @@ sub build { $optgroup->append_single_option_line('resolution'); } } + + { + my $page = $self->add_options_page('Output options', 'page_white_go.png'); + { + my $optgroup = $page->new_optgroup('Sequential printing'); + $optgroup->append_single_option_line('complete_objects'); + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Extruder clearance (mm)', + ); + foreach my $opt_key (qw(extruder_clearance_radius extruder_clearance_height)) { + my $option = $optgroup->get_option($opt_key); + $option->width(60); + $line->append_option($option); + } + $optgroup->append_line($line); + } + { + my $optgroup = $page->new_optgroup('Output file'); + $optgroup->append_single_option_line('gcode_comments'); + + { + my $option = $optgroup->get_option('output_filename_format'); + $option->full_width(1); + $optgroup->append_single_option_line($option); + } + } + { + my $optgroup = $page->new_optgroup('Post-processing scripts', + label_width => 0, + ); + my $option = $optgroup->get_option('post_process'); + $option->full_width(1); + $option->height(50); + $optgroup->append_single_option_line($option); + } + } + + { + my $page = $self->add_options_page('Notes', 'note.png'); + { + my $optgroup = $page->new_optgroup('Notes', + label_width => 0, + ); + my $option = $optgroup->get_option('notes'); + $option->full_width(1); + $option->height(250); + $optgroup->append_single_option_line($option); + } + } } sub _update { @@ -713,11 +708,28 @@ sub _update { my $have_perimeters = $config->perimeters > 0; $self->get_field($_)->toggle($have_perimeters) - for qw(extra_perimeters thin_walls overhangs seam_position external_perimeters_first); + for qw(extra_perimeters thin_walls overhangs seam_position external_perimeters_first + external_perimeter_extrusion_width + perimeter_speed small_perimeter_speed external_perimeter_speed); my $have_infill = $config->fill_density > 0; $self->get_field($_)->toggle($have_infill) - for qw(fill_pattern infill_every_layers infill_only_where_needed solid_infill_every_layers); + for qw(fill_pattern infill_every_layers infill_only_where_needed solid_infill_every_layers + solid_infill_below_area infill_extruder); + + my $have_solid_infill = ($config->top_solid_layers > 0) || ($config->bottom_solid_layers > 0); + $self->get_field($_)->toggle($have_solid_infill) + for qw(external_fill_pattern infill_first solid_infill_extruder solid_infill_extrusion_width + solid_infill_speed); + + $self->get_field($_)->toggle($have_infill || $have_solid_infill) + for qw(fill_angle infill_extrusion_width infill_speed bridge_speed); + + $self->get_field('gap_fill_speed')->toggle($have_perimeters && $have_infill); + + my $have_top_solid_infill = $config->top_solid_layers > 0; + $self->get_field($_)->toggle($have_top_solid_infill) + for qw(top_infill_extrusion_width top_solid_infill_speed); my $have_default_acceleration = $config->default_acceleration > 0; $self->get_field($_)->toggle($have_default_acceleration) @@ -727,15 +739,23 @@ sub _update { $self->get_field($_)->toggle($have_skirt) for qw(skirt_distance skirt_height); + my $have_brim = $config->brim_width > 0; + $self->get_field('perimeter_extruder')->toggle($have_perimeters || $have_brim); + my $have_support_material = $config->support_material || $config->raft_layers > 0; my $have_support_interface = $config->support_material_interface_layers > 0; $self->get_field($_)->toggle($have_support_material) for qw(support_material_threshold support_material_enforce_layers support_material_pattern support_material_spacing support_material_angle support_material_interface_layers dont_support_bridges - support_material_extruder); + support_material_extrusion_width); $self->get_field($_)->toggle($have_support_material && $have_support_interface) - for qw(support_material_interface_spacing support_material_interface_extruder); + for qw(support_material_interface_spacing support_material_interface_extruder + support_material_interface_speed); + + $self->get_field('perimeter_extrusion_width')->toggle($have_perimeters || $have_skirt || $have_brim); + $self->get_field('support_material_extruder')->toggle($have_support_material || $have_skirt); + $self->get_field('support_material_speed')->toggle($have_support_material || $have_brim || $have_skirt); my $have_sequential_printing = $config->complete_objects; $self->get_field($_)->toggle($have_sequential_printing) @@ -918,7 +938,7 @@ sub build { if ($dlg->ShowModal == wxID_OK) { my $value = $dlg->GetValue; $self->{config}->set('bed_shape', $value); - $self->set_dirty(1); + $self->update_dirty; $self->_on_value_change('bed_shape', $value); } }); @@ -962,7 +982,7 @@ sub build { $optgroup->on_change(sub { my ($opt_id) = @_; if ($opt_id eq 'extruders_count') { - $self->set_dirty(1); + $self->update_dirty; $self->_extruders_count_changed($optgroup->get_value('extruders_count')); } }); @@ -1100,12 +1120,10 @@ sub _update { my $config = $self->{config}; - $self->get_field('toolchange_gcode')->toggle($self->{extruders_count} > 1); + my $have_multiple_extruders = $self->{extruders_count} > 1; + $self->get_field('toolchange_gcode')->toggle($have_multiple_extruders); for my $i (0 .. ($self->{extruders_count}-1)) { - # disable extruder offset for first extruder - $self->get_field('extruder_offset', $i)->toggle($i != 0); - my $have_retract_length = $config->get_at('retract_length', $i) > 0; # when using firmware retraction, firmware decides retraction length @@ -1124,9 +1142,11 @@ sub _update { $self->get_field($_, $i)->toggle($retraction && !$config->use_firmware_retraction) for qw(retract_speed retract_restart_extra wipe); + $self->get_field('retract_length_toolchange', $i)->toggle($have_multiple_extruders); + my $toolchange_retraction = $config->get_at('retract_length_toolchange', $i) > 0; - $self->get_field($_, $i)->toggle($toolchange_retraction) - for qw(retract_restart_extra_toolchange); + $self->get_field('retract_restart_extra_toolchange', $i)->toggle + ($have_multiple_extruders && $toolchange_retraction); } } @@ -1183,7 +1203,7 @@ sub new_optgroup { config => $self->GetParent->{config}, label_width => $params{label_width} // 200, on_change => sub { - $self->GetParent->set_dirty(1); + $self->GetParent->update_dirty; $self->GetParent->_on_value_change(@_); }, ); @@ -1230,7 +1250,7 @@ sub new { my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, "Save preset", wxDefaultPosition, wxDefaultSize); - my @values = grep $_ ne '- default -', @{$params{values}}; + my @values = @{$params{values}}; my $text = Wx::StaticText->new($self, -1, "Save " . lc($params{title}) . " as:", wxDefaultPosition, wxDefaultSize); $self->{combo} = Wx::ComboBox->new($self, -1, $params{default}, wxDefaultPosition, wxDefaultSize, \@values, @@ -1270,4 +1290,33 @@ sub get_name { return $self->{chosen_name}; } +package Slic3r::GUI::Tab::Preset; +use Moo; + +has 'default' => (is => 'ro', default => sub { 0 }); +has 'external' => (is => 'ro', default => sub { 0 }); +has 'name' => (is => 'rw', required => 1); +has 'file' => (is => 'rw'); + +sub config { + my ($self, $keys) = @_; + + if ($self->default) { + return Slic3r::Config->new_from_defaults(@$keys); + } else { + if (!-e $self->file) { + Slic3r::GUI::show_error($self, "The selected preset does not exist anymore (" . $self->file . ")."); + return; + } + + # apply preset values on top of defaults + my $external_config = Slic3r::Config->load($self->file); + my $config = Slic3r::Config->new; + $config->set($_, $external_config->get($_)) + for grep $external_config->has($_), @$keys; + + return $config; + } +} + 1; diff --git a/lib/Slic3r/Layer/Region.pm b/lib/Slic3r/Layer/Region.pm index 0063095e1..f48282ee3 100644 --- a/lib/Slic3r/Layer/Region.pm +++ b/lib/Slic3r/Layer/Region.pm @@ -246,7 +246,7 @@ sub make_perimeters { my ($polynodes, $depth, $is_contour) = @_; # convert all polynodes to ExtrusionLoop objects - my $collection = Slic3r::ExtrusionPath::Collection->new; + my $collection = Slic3r::ExtrusionPath::Collection->new; # temporary collection my @children = (); foreach my $polynode (@$polynodes) { my $polygon = ($polynode->{outer} // $polynode->{hole})->clone; @@ -303,7 +303,7 @@ sub make_perimeters { # (clone because the collection gets DESTROY'ed) # We allow polyline reversal because Clipper may have randomly # reversed polylines during clipping. - my $collection = Slic3r::ExtrusionPath::Collection->new(@paths); + my $collection = Slic3r::ExtrusionPath::Collection->new(@paths); # temporary collection @paths = map $_->clone, @{$collection->chained_path(0)}; } else { push @paths, Slic3r::ExtrusionPath->new( @@ -350,16 +350,12 @@ sub make_perimeters { # use a nearest neighbor search to order these children # TODO: supply second argument to chained_path() too? - # Optimization: since islands are going to be sorted by slice anyway in the - # G-code export process, we skip chained_path here - my ($sorted_collection, @orig_indices); - if ($is_contour && $depth == 0) { - $sorted_collection = $collection; - @orig_indices = (0..$#$sorted_collection); - } else { - $sorted_collection = $collection->chained_path_indices(0); - @orig_indices = @{$sorted_collection->orig_indices}; - } + # (We used to skip this chiained_path() when $is_contour && + # $depth == 0 because slices are ordered at G_code export + # time, but multiple top-level perimeters might belong to + # the same slice actually, so that was a broken optimization.) + my $sorted_collection = $collection->chained_path_indices(0); + my @orig_indices = @{$sorted_collection->orig_indices}; my @loops = (); foreach my $loop (@$sorted_collection) { @@ -496,7 +492,8 @@ sub process_external_surfaces { $angle = $bridge_detector->angle; if (defined $angle && $self->object->config->support_material) { - $self->bridged->append($_) for @{ $bridge_detector->coverage_with_angle($angle) }; + $self->bridged->append(Slic3r::ExPolygon->new($_)) + for @{ $bridge_detector->coverage_by_angle($angle) }; $self->unsupported_bridge_edges->append($_) for @{ $bridge_detector->unsupported_edges }; } } diff --git a/lib/Slic3r/Polygon.pm b/lib/Slic3r/Polygon.pm index bc0fbc5dd..16b334b3a 100644 --- a/lib/Slic3r/Polygon.pm +++ b/lib/Slic3r/Polygon.pm @@ -7,11 +7,6 @@ use parent 'Slic3r::Polyline'; use Slic3r::Geometry qw(PI); -sub dump_perl { - my $self = shift; - return sprintf "[%s]", join ',', map "[$_->[0],$_->[1]]", @$self; -} - sub grow { my $self = shift; return $self->split_at_first_point->grow(@_); @@ -37,51 +32,4 @@ sub subdivide { return Slic3r::Polygon->new(@new_points); } -sub concave_points { - my ($self, $angle) = @_; - - $angle //= PI; - - # input angle threshold is checked on the internal side of the polygon - # but angle3points measures CCW angle, so we calculate the complementary angle - my $ccw_angle = 2*PI-$angle; - - my @concave = (); - my @points = @$self; - my @points_pp = @{$self->pp}; - - for my $i (-1 .. ($#points-1)) { - # angle is measured in ccw orientation - my $vertex_angle = Slic3r::Geometry::angle3points(@points_pp[$i, $i-1, $i+1]); - if ($vertex_angle <= $ccw_angle) { - push @concave, $points[$i]; - } - } - - return [@concave]; -} - -sub convex_points { - my ($self, $angle) = @_; - - $angle //= PI; - - # input angle threshold is checked on the internal side of the polygon - # but angle3points measures CCW angle, so we calculate the complementary angle - my $ccw_angle = 2*PI-$angle; - - my @convex = (); - my @points = @$self; - my @points_pp = @{$self->pp}; - - for my $i (-1 .. ($#points-1)) { - # angle is measured in ccw orientation - my $vertex_angle = Slic3r::Geometry::angle3points(@points_pp[$i, $i-1, $i+1]); - if ($vertex_angle >= $ccw_angle) { - push @convex, $points[$i]; - } - } - return [@convex]; -} - 1; \ No newline at end of file diff --git a/lib/Slic3r/Polyline.pm b/lib/Slic3r/Polyline.pm index a42b5d1c4..9cc142409 100644 --- a/lib/Slic3r/Polyline.pm +++ b/lib/Slic3r/Polyline.pm @@ -10,4 +10,9 @@ sub new_scale { return $class->new(map [ Slic3r::Geometry::scale($_->[X]), Slic3r::Geometry::scale($_->[Y]) ], @points); } +sub dump_perl { + my $self = shift; + return sprintf "[%s]", join ',', map "[$_->[0],$_->[1]]", @$self; +} + 1; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index dca82fc2b..69df1c900 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -7,8 +7,7 @@ use File::Spec; use List::Util qw(min max first sum); use Slic3r::ExtrusionPath ':roles'; use Slic3r::Flow ':roles'; -use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN MAX PI scale unscale chained_path - convex_hull); +use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN MAX PI scale unscale convex_hull); use Slic3r::Geometry::Clipper qw(diff_ex union_ex intersection_ex intersection offset offset2 union union_pt_chained JT_ROUND JT_SQUARE); use Slic3r::Print::State ':steps'; @@ -41,46 +40,6 @@ sub total_layer_count { return max(map $_->total_layer_count, @{$self->objects}); } -# the bounding box of objects placed in copies position -# (without taking skirt/brim/support material into account) -sub bounding_box { - my $self = shift; - - my @points = (); - foreach my $object (@{$self->objects}) { - foreach my $copy (@{$object->_shifted_copies}) { - push @points, - [ $copy->[X], $copy->[Y] ], - [ $copy->[X] + $object->size->[X], $copy->[Y] + $object->size->[Y] ]; - } - } - return Slic3r::Geometry::BoundingBox->new_from_points([ map Slic3r::Point->new(@$_), @points ]); -} - -# the total bounding box of extrusions, including skirt/brim/support material -sub total_bounding_box { - my ($self) = @_; - - # get objects bounding box - my $bb = $self->bounding_box; - - # check how much we need to increase it - my $extra = 0; - if ($self->has_support_material) { - $extra = &Slic3r::Print::SupportMaterial::MARGIN; - } - $extra = max($extra, $self->config->brim_width); - if ($self->config->skirts > 0) { - my $skirt_flow = $self->skirt_flow; - $extra = max($extra, $self->config->brim_width + $self->config->skirt_distance + ($self->config->skirts * $skirt_flow->spacing)); - } - - if ($extra > 0) { - $bb->offset(scale $extra); - } - return $bb; -} - sub size { my $self = shift; return $self->bounding_box->size; @@ -321,9 +280,9 @@ sub make_skirt { Slic3r::ExtrusionPath->new( polyline => Slic3r::Polygon->new(@$loop)->split_at_first_point, role => EXTR_ROLE_SKIRT, - mm3_per_mm => $mm3_per_mm, + mm3_per_mm => $mm3_per_mm, # this will be overridden at G-code export time width => $flow->width, - height => $first_layer_height, + height => $first_layer_height, # this will be overridden at G-code export time ), )); @@ -373,9 +332,9 @@ sub make_brim { } $self->status_cb->(88, "Generating brim"); - # brim is only printed on first layer and uses support material extruder + # brim is only printed on first layer and uses perimeter extruder my $first_layer_height = $self->skirt_first_layer_height; - my $flow = $self->skirt_flow; + my $flow = $self->brim_flow; my $mm3_per_mm = $flow->mm3_per_mm; my $grow_distance = $flow->scaled_width / 2; @@ -423,23 +382,6 @@ sub make_brim { $self->set_step_done(STEP_BRIM); } -sub skirt_first_layer_height { - my ($self) = @_; - return $self->objects->[0]->config->get_abs_value('first_layer_height'); -} - -sub skirt_flow { - my ($self) = @_; - - return Slic3r::Flow->new_from_width( - width => ($self->config->first_layer_extrusion_width || $self->regions->[0]->config->perimeter_extrusion_width), - role => FLOW_ROLE_PERIMETER, - nozzle_diameter => $self->config->get_at('nozzle_diameter', $self->objects->[0]->config->support_material_extruder-1), - layer_height => $self->skirt_first_layer_height, - bridge_flow_ratio => 0, - ); -} - sub write_gcode { my $self = shift; my ($file) = @_; @@ -456,243 +398,11 @@ sub write_gcode { binmode $fh, ':utf8'; } - - # write some information - my @lt = localtime; - printf $fh "; generated by Slic3r $Slic3r::VERSION on %04d-%02d-%02d at %02d:%02d:%02d\n\n", - $lt[5] + 1900, $lt[4]+1, $lt[3], $lt[2], $lt[1], $lt[0]; - - print $fh "; $_\n" foreach split /\R/, $self->config->notes; - print $fh "\n" if $self->config->notes; - - my $first_object = $self->objects->[0]; - my $layer_height = $first_object->config->layer_height; - for my $region_id (0..$#{$self->regions}) { - printf $fh "; external perimeters extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_EXTERNAL_PERIMETER, $layer_height, 0, 0, -1, $first_object)->width; - printf $fh "; perimeters extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_PERIMETER, $layer_height, 0, 0, -1, $first_object)->width; - printf $fh "; infill extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_INFILL, $layer_height, 0, 0, -1, $first_object)->width; - printf $fh "; solid infill extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_SOLID_INFILL, $layer_height, 0, 0, -1, $first_object)->width; - printf $fh "; top infill extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_TOP_SOLID_INFILL, $layer_height, 0, 0, -1, $first_object)->width; - printf $fh "; support material extrusion width = %.2fmm\n", - $self->objects->[0]->support_material_flow->width - if $self->has_support_material; - printf $fh "; first layer extrusion width = %.2fmm\n", - $self->regions->[$region_id]->flow(FLOW_ROLE_PERIMETER, $layer_height, 0, 1, -1, $self->objects->[0])->width - if $self->regions->[$region_id]->config->first_layer_extrusion_width; - print $fh "\n"; - } - - # prepare the helper object for replacing placeholders in custom G-code and output filename - $self->placeholder_parser->update_timestamp; - - # estimate the total number of layer changes - # TODO: only do this when M73 is enabled - my $layer_count; - if ($self->config->complete_objects) { - $layer_count = sum(map { $_->total_layer_count * @{$_->copies} } @{$self->objects}); - } else { - # if sequential printing is not enable, all copies of the same object share the same layer change command(s) - $layer_count = sum(map { $_->total_layer_count } @{$self->objects}); - } - - # set up our helper object - my $gcodegen = Slic3r::GCode->new( - placeholder_parser => $self->placeholder_parser, - layer_count => $layer_count, - enable_cooling_markers => 1, + my $exporter = Slic3r::Print::GCode->new( + print => $self, + fh => $fh, ); - $gcodegen->apply_print_config($self->config); - $gcodegen->set_extruders($self->extruders); - - print $fh $gcodegen->writer->set_fan(0, 1) if $self->config->cooling && $self->config->disable_fan_first_layers; - - # set bed temperature - if ((my $temp = $self->config->first_layer_bed_temperature) && $self->config->start_gcode !~ /M(?:190|140)/i) { - printf $fh $gcodegen->writer->set_bed_temperature($temp, 1); - } - - # set extruder(s) temperature before and after start G-code - my $print_first_layer_temperature = sub { - my ($wait) = @_; - - return if $self->config->start_gcode =~ /M(?:109|104)/i; - for my $t (@{$self->extruders}) { - my $temp = $self->config->get_at('first_layer_temperature', $t); - $temp += $self->config->standby_temperature_delta if $self->config->ooze_prevention; - printf $fh $gcodegen->writer->set_temperature($temp, $wait, $t) if $temp > 0; - } - }; - $print_first_layer_temperature->(0); - printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->start_gcode); - $print_first_layer_temperature->(1); - - # set other general things - print $fh $gcodegen->preamble; - - # initialize a motion planner for object-to-object travel moves - if ($self->config->avoid_crossing_perimeters) { - my $distance_from_objects = 1; - # compute the offsetted convex hull for each object and repeat it for each copy. - my @islands = (); - foreach my $obj_idx (0 .. ($self->object_count - 1)) { - my $convex_hull = convex_hull([ - map @{$_->contour}, map @{$_->slices}, @{$self->objects->[$obj_idx]->layers}, - ]); - # discard layers only containing thin walls (offset would fail on an empty polygon) - if (@$convex_hull) { - my $expolygon = Slic3r::ExPolygon->new($convex_hull); - my @island = @{$expolygon->offset_ex(scale $distance_from_objects, 1, JT_SQUARE)}; - foreach my $copy (@{ $self->objects->[$obj_idx]->_shifted_copies }) { - push @islands, map { my $c = $_->clone; $c->translate(@$copy); $c } @island; - } - } - } - $gcodegen->avoid_crossing_perimeters->init_external_mp(union_ex([ map @$_, @islands ])); - } - - # calculate wiping points if needed - if ($self->config->ooze_prevention) { - my @skirt_points = map @$_, map @$_, @{$self->skirt}; - if (@skirt_points) { - my $outer_skirt = convex_hull(\@skirt_points); - my @skirts = (); - foreach my $extruder_id (@{$self->extruders}) { - push @skirts, my $s = $outer_skirt->clone; - $s->translate(map scale($_), @{$self->config->get_at('extruder_offset', $extruder_id)}); - } - my $convex_hull = convex_hull([ map @$_, @skirts ]); - - $gcodegen->ooze_prevention->enable(1); - $gcodegen->ooze_prevention->standby_points( - [ map $_->clone, map @$_, map $_->subdivide(scale 10), @{offset([$convex_hull], scale 3)} ] - ); - } - } - - # prepare the layer processor - my $layer_gcode = Slic3r::GCode::Layer->new( - print => $self, - gcodegen => $gcodegen, - ); - - # set initial extruder only after custom start G-code - print $fh $gcodegen->set_extruder($self->extruders->[0]); - - # do all objects for each layer - if ($self->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]->size->[Z] <=> $self->objects->[$b]->size->[Z] } 0..($self->object_count - 1); - - my $finished_objects = 0; - for my $obj_idx (@obj_idx) { - my $object = $self->objects->[$obj_idx]; - for my $copy (@{ $self->objects->[$obj_idx]->_shifted_copies }) { - # move to the origin position for the copy we're going to print. - # this happens before Z goes down to layer 0 again, so that - # no collision happens hopefully. - if ($finished_objects > 0) { - $gcodegen->set_origin(Slic3r::Pointf->new(map unscale $copy->[$_], X,Y)); - print $fh $gcodegen->retract; - print $fh $gcodegen->travel_to( - $object->_copies_shift->negative, - undef, - 'move to origin position for next object', - ); - } - - my $buffer = Slic3r::GCode::CoolingBuffer->new( - config => $self->config, - gcodegen => $gcodegen, - ); - - my @layers = sort { $a->print_z <=> $b->print_z } @{$object->layers}, @{$object->support_layers}; - for my $layer (@layers) { - # if we are printing the bottom layer of an object, and we have already finished - # another one, set first layer temperatures. this happens before the Z move - # is triggered, so machine has more time to reach such temperatures - if ($layer->id == 0 && $finished_objects > 0) { - printf $fh $gcodegen->writer->set_bed_temperature($self->config->first_layer_bed_temperature), - if $self->config->first_layer_bed_temperature; - $print_first_layer_temperature->(0); - } - print $fh $buffer->append( - $layer_gcode->process_layer($layer, [$copy]), - $layer->object->ptr, - $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 Slic3r::Point->new(@{$_->_shifted_copies->[0]}), @{$self->objects} ])}; - - # sort layers by Z - my %layers = (); # print_z => [ [layers], [layers], [layers] ] by obj_idx - foreach my $obj_idx (0 .. ($self->object_count - 1)) { - my $object = $self->objects->[$obj_idx]; - foreach my $layer (@{$object->layers}, @{$object->support_layers}) { - $layers{ $layer->print_z } ||= []; - $layers{ $layer->print_z }[$obj_idx] ||= []; - push @{$layers{ $layer->print_z }[$obj_idx]}, $layer; - } - } - - my $buffer = Slic3r::GCode::CoolingBuffer->new( - config => $self->config, - gcodegen => $gcodegen, - ); - foreach my $print_z (sort { $a <=> $b } keys %layers) { - foreach my $obj_idx (@obj_idx) { - foreach my $layer (@{ $layers{$print_z}[$obj_idx] // [] }) { - print $fh $buffer->append( - $layer_gcode->process_layer($layer, $layer->object->_shifted_copies), - $layer->object->ptr . ref($layer), # differentiate $obj_id between normal layers and support layers - $layer->id, - $layer->print_z, - ); - } - } - } - print $fh $buffer->flush; - } - - # write end commands to file - print $fh $gcodegen->retract; # TODO: process this retract through PressureRegulator in order to discharge fully - print $fh $gcodegen->writer->set_fan(0); - printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->end_gcode); - print $fh $gcodegen->writer->update_progress($gcodegen->layer_count, $gcodegen->layer_count, 1); # 100% - - $self->total_used_filament(0); - $self->total_extruded_volume(0); - foreach my $extruder (@{$gcodegen->writer->extruders}) { - my $used_filament = $extruder->used_filament; - my $extruded_volume = $extruder->extruded_volume; - - printf $fh "; filament used = %.1fmm (%.1fcm3)\n", - $used_filament, $extruded_volume/1000; - - $self->total_used_filament($self->total_used_filament + $used_filament); - $self->total_extruded_volume($self->total_extruded_volume + $extruded_volume); - } - - # append full config - print $fh "\n"; - foreach my $config ($self->config, $self->default_object_config, $self->default_region_config) { - foreach my $opt_key (sort @{$config->get_keys}) { - next if $Slic3r::Config::Options->{$opt_key}{shortcut}; - printf $fh "; %s = %s\n", $opt_key, $config->serialize($opt_key); - } - } + $exporter->export; # close our gcode file close $fh; diff --git a/lib/Slic3r/Print/GCode.pm b/lib/Slic3r/Print/GCode.pm new file mode 100644 index 000000000..dae9eb3a7 --- /dev/null +++ b/lib/Slic3r/Print/GCode.pm @@ -0,0 +1,547 @@ +package Slic3r::Print::GCode; +use Moo; + +has 'print' => (is => 'ro', required => 1, handles => [qw(objects placeholder_parser config)]); +has 'fh' => (is => 'ro', required => 1); + +has '_gcodegen' => (is => 'rw'); +has '_cooling_buffer' => (is => 'rw'); +has '_spiral_vase' => (is => 'rw'); +has '_vibration_limit' => (is => 'rw'); +has '_arc_fitting' => (is => 'rw'); +has '_pressure_regulator' => (is => 'rw'); +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'); + +use List::Util qw(first sum); +use Slic3r::Flow ':roles'; +use Slic3r::Geometry qw(X Y scale unscale chained_path convex_hull); +use Slic3r::Geometry::Clipper qw(JT_SQUARE union_ex offset); + +sub BUILD { + my ($self) = @_; + + { + # estimate the total number of layer changes + # TODO: only do this when M73 is enabled + my $layer_count; + if ($self->config->complete_objects) { + $layer_count = sum(map { $_->total_layer_count * @{$_->copies} } @{$self->objects}); + } else { + # if sequential printing is not enable, all copies of the same object share the same layer change command(s) + $layer_count = sum(map { $_->total_layer_count } @{$self->objects}); + } + + # set up our helper object + my $gcodegen = Slic3r::GCode->new( + placeholder_parser => $self->placeholder_parser, + layer_count => $layer_count, + enable_cooling_markers => 1, + ); + $gcodegen->apply_print_config($self->config); + $gcodegen->set_extruders($self->print->extruders); + $self->_gcodegen($gcodegen); + } + + $self->_cooling_buffer(Slic3r::GCode::CoolingBuffer->new( + config => $self->config, + gcodegen => $self->_gcodegen, + )); + + $self->_spiral_vase(Slic3r::GCode::SpiralVase->new(config => $self->config)) + if $self->config->spiral_vase; + + $self->_vibration_limit(Slic3r::GCode::VibrationLimit->new(config => $self->config)) + if $self->config->vibration_limit > 0; + + $self->_arc_fitting(Slic3r::GCode::ArcFitting->new(config => $self->config)) + if $self->config->gcode_arcs; + + $self->_pressure_regulator(Slic3r::GCode::PressureRegulator->new(config => $self->config)) + if $self->config->pressure_advance > 0; +} + +sub export { + my ($self) = @_; + + my $fh = $self->fh; + my $gcodegen = $self->_gcodegen; + + # write some information + my @lt = localtime; + printf $fh "; generated by Slic3r $Slic3r::VERSION on %04d-%02d-%02d at %02d:%02d:%02d\n\n", + $lt[5] + 1900, $lt[4]+1, $lt[3], $lt[2], $lt[1], $lt[0]; + + print $fh "; $_\n" foreach split /\R/, $self->config->notes; + print $fh "\n" if $self->config->notes; + + my $first_object = $self->objects->[0]; + my $layer_height = $first_object->config->layer_height; + for my $region_id (0..$#{$self->print->regions}) { + my $region = $self->print->regions->[$region_id]; + printf $fh "; external perimeters extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_EXTERNAL_PERIMETER, $layer_height, 0, 0, -1, $first_object)->width; + printf $fh "; perimeters extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_PERIMETER, $layer_height, 0, 0, -1, $first_object)->width; + printf $fh "; infill extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_INFILL, $layer_height, 0, 0, -1, $first_object)->width; + printf $fh "; solid infill extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_SOLID_INFILL, $layer_height, 0, 0, -1, $first_object)->width; + printf $fh "; top infill extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_TOP_SOLID_INFILL, $layer_height, 0, 0, -1, $first_object)->width; + printf $fh "; support material extrusion width = %.2fmm\n", + $self->objects->[0]->support_material_flow->width + if $self->print->has_support_material; + printf $fh "; first layer extrusion width = %.2fmm\n", + $region->flow(FLOW_ROLE_PERIMETER, $layer_height, 0, 1, -1, $self->objects->[0])->width + if $region->config->first_layer_extrusion_width; + print $fh "\n"; + } + + # prepare the helper object for replacing placeholders in custom G-code and output filename + $self->placeholder_parser->update_timestamp; + + print $fh $gcodegen->writer->set_fan(0, 1) + if $self->config->cooling && $self->config->disable_fan_first_layers; + + # set bed temperature + if ((my $temp = $self->config->first_layer_bed_temperature) && $self->config->start_gcode !~ /M(?:190|140)/i) { + printf $fh $gcodegen->writer->set_bed_temperature($temp, 1); + } + + # set extruder(s) temperature before and after start G-code + $self->_print_first_layer_temperature(0); + printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->start_gcode); + $self->_print_first_layer_temperature(1); + + # set other general things + print $fh $gcodegen->preamble; + + # initialize a motion planner for object-to-object travel moves + if ($self->config->avoid_crossing_perimeters) { + my $distance_from_objects = 1; + # compute the offsetted convex hull for each object and repeat it for each copy. + my @islands = (); + foreach my $obj_idx (0 .. ($self->print->object_count - 1)) { + my $convex_hull = convex_hull([ + map @{$_->contour}, map @{$_->slices}, @{$self->objects->[$obj_idx]->layers}, + ]); + # discard layers only containing thin walls (offset would fail on an empty polygon) + if (@$convex_hull) { + my $expolygon = Slic3r::ExPolygon->new($convex_hull); + my @island = @{$expolygon->offset_ex(scale $distance_from_objects, 1, JT_SQUARE)}; + foreach my $copy (@{ $self->objects->[$obj_idx]->_shifted_copies }) { + push @islands, map { my $c = $_->clone; $c->translate(@$copy); $c } @island; + } + } + } + $gcodegen->avoid_crossing_perimeters->init_external_mp(union_ex([ map @$_, @islands ])); + } + + # calculate wiping points if needed + if ($self->config->ooze_prevention) { + my @skirt_points = map @$_, map @$_, @{$self->print->skirt}; + if (@skirt_points) { + my $outer_skirt = convex_hull(\@skirt_points); + my @skirts = (); + foreach my $extruder_id (@{$self->print->extruders}) { + push @skirts, my $s = $outer_skirt->clone; + $s->translate(map scale($_), @{$self->config->get_at('extruder_offset', $extruder_id)}); + } + my $convex_hull = convex_hull([ map @$_, @skirts ]); + + $gcodegen->ooze_prevention->enable(1); + $gcodegen->ooze_prevention->standby_points( + [ map $_->clone, map @$_, map $_->subdivide(scale 10), @{offset([$convex_hull], scale 3)} ] + ); + } + } + + # set initial extruder only after custom start G-code + print $fh $gcodegen->set_extruder($self->print->extruders->[0]); + + # do all objects for each layer + if ($self->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]->size->z <=> $self->objects->[$b]->size->z } 0..($self->print->object_count - 1); + + my $finished_objects = 0; + for my $obj_idx (@obj_idx) { + my $object = $self->objects->[$obj_idx]; + for my $copy (@{ $self->objects->[$obj_idx]->_shifted_copies }) { + # move to the origin position for the copy we're going to print. + # this happens before Z goes down to layer 0 again, so that + # no collision happens hopefully. + if ($finished_objects > 0) { + $gcodegen->set_origin(Slic3r::Pointf->new(map unscale $copy->[$_], X,Y)); + print $fh $gcodegen->retract; + print $fh $gcodegen->travel_to( + $object->_copies_shift->negative, + undef, + 'move to origin position for next object', + ); + } + + my @layers = sort { $a->print_z <=> $b->print_z } @{$object->layers}, @{$object->support_layers}; + for my $layer (@layers) { + # if we are printing the bottom layer of an object, and we have already finished + # another one, set first layer temperatures. this happens before the Z move + # is triggered, so machine has more time to reach such temperatures + if ($layer->id == 0 && $finished_objects > 0) { + printf $fh $gcodegen->writer->set_bed_temperature($self->config->first_layer_bed_temperature), + if $self->config->first_layer_bed_temperature; + $self->_print_first_layer_temperature(0); + } + $self->process_layer($layer, [$copy]); + } + $self->flush_cooling_buffer; + $finished_objects++; + } + } + } else { + # order objects using a nearest neighbor search + my @obj_idx = @{chained_path([ map Slic3r::Point->new(@{$_->_shifted_copies->[0]}), @{$self->objects} ])}; + + # sort layers by Z + my %layers = (); # print_z => [ [layers], [layers], [layers] ] by obj_idx + foreach my $obj_idx (0 .. ($self->print->object_count - 1)) { + my $object = $self->objects->[$obj_idx]; + foreach my $layer (@{$object->layers}, @{$object->support_layers}) { + $layers{ $layer->print_z } ||= []; + $layers{ $layer->print_z }[$obj_idx] ||= []; + push @{$layers{ $layer->print_z }[$obj_idx]}, $layer; + } + } + + foreach my $print_z (sort { $a <=> $b } keys %layers) { + foreach my $obj_idx (@obj_idx) { + foreach my $layer (@{ $layers{$print_z}[$obj_idx] // [] }) { + $self->process_layer($layer, $layer->object->_shifted_copies); + } + } + } + $self->flush_cooling_buffer; + } + + # write end commands to file + print $fh $gcodegen->retract; # TODO: process this retract through PressureRegulator in order to discharge fully + print $fh $gcodegen->writer->set_fan(0); + printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->end_gcode); + print $fh $gcodegen->writer->update_progress($gcodegen->layer_count, $gcodegen->layer_count, 1); # 100% + + $self->print->total_used_filament(0); + $self->print->total_extruded_volume(0); + foreach my $extruder (@{$gcodegen->writer->extruders}) { + my $used_filament = $extruder->used_filament; + my $extruded_volume = $extruder->extruded_volume; + + printf $fh "; filament used = %.1fmm (%.1fcm3)\n", + $used_filament, $extruded_volume/1000; + + $self->print->total_used_filament($self->print->total_used_filament + $used_filament); + $self->print->total_extruded_volume($self->print->total_extruded_volume + $extruded_volume); + } + + # append full config + print $fh "\n"; + foreach my $config ($self->print->config, $self->print->default_object_config, $self->print->default_region_config) { + foreach my $opt_key (sort @{$config->get_keys}) { + next if $Slic3r::Config::Options->{$opt_key}{shortcut}; + printf $fh "; %s = %s\n", $opt_key, $config->serialize($opt_key); + } + } +} + +sub _print_first_layer_temperature { + my ($self, $wait) = @_; + + return if $self->config->start_gcode =~ /M(?:109|104)/i; + for my $t (@{$self->print->extruders}) { + my $temp = $self->config->get_at('first_layer_temperature', $t); + $temp += $self->config->standby_temperature_delta if $self->config->ooze_prevention; + printf {$self->fh} $self->_gcodegen->writer->set_temperature($temp, $wait, $t) if $temp > 0; + } +} + +sub process_layer { + my $self = shift; + my ($layer, $object_copies) = @_; + my $gcode = ""; + + my $object = $layer->object; + $self->_gcodegen->config->apply_object_config($object->config); + + # check whether we're going to apply spiralvase logic + if (defined $self->_spiral_vase) { + $self->_spiral_vase->enable( + ($layer->id > 0 || $self->print->config->brim_width == 0) + && ($layer->id >= $self->print->config->skirt_height && $self->print->config->skirt_height != -1) + && !defined(first { $_->config->bottom_solid_layers > $layer->id } @{$layer->regions}) + && !defined(first { @{$_->perimeters} > 1 } @{$layer->regions}) + && !defined(first { @{$_->fills} > 0 } @{$layer->regions}) + ); + } + + # if we're going to apply spiralvase to this layer, disable loop clipping + $self->_gcodegen->enable_loop_clipping(!defined $self->_spiral_vase || !$self->_spiral_vase->enable); + + if (!$self->_second_layer_things_done && $layer->id == 1) { + for my $extruder (@{$self->_gcodegen->writer->extruders}) { + my $temperature = $self->config->get_at('temperature', $extruder->id); + $gcode .= $self->_gcodegen->writer->set_temperature($temperature, 0, $extruder->id) + if $temperature && $temperature != $self->config->get_at('first_layer_temperature', $extruder->id); + } + $gcode .= $self->_gcodegen->writer->set_bed_temperature($self->print->config->bed_temperature) + if $self->print->config->bed_temperature && $self->print->config->bed_temperature != $self->print->config->first_layer_bed_temperature; + $self->_second_layer_things_done(1); + } + + # set new layer - this will change Z and force a retraction if retract_layer_change is enabled + $gcode .= $self->_gcodegen->change_layer($layer); + $gcode .= $self->_gcodegen->placeholder_parser->process($self->print->config->layer_gcode, { + layer_num => $layer->id, + }) . "\n" if $self->print->config->layer_gcode; + + # extrude skirt + if (((values %{$self->_skirt_done}) < $self->print->config->skirt_height || $self->print->config->skirt_height == -1) + && !$self->_skirt_done->{$layer->print_z}) { + $self->_gcodegen->set_origin(Slic3r::Pointf->new(0,0)); + $self->_gcodegen->avoid_crossing_perimeters->use_external_mp(1); + my @extruder_ids = map { $_->id } @{$self->_gcodegen->writer->extruders}; + $gcode .= $self->_gcodegen->set_extruder($extruder_ids[0]); + # skip skirt if we have a large brim + if ($layer->id < $self->print->config->skirt_height || $self->print->config->skirt_height == -1) { + my $skirt_flow = $self->print->skirt_flow; + + # distribute skirt loops across all extruders + my @skirt_loops = @{$self->print->skirt}; + for my $i (0 .. $#skirt_loops) { + # 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 >= $self->print->config->skirts); + my $extruder_id = $extruder_ids[($i/@extruder_ids) % @extruder_ids]; + $gcode .= $self->_gcodegen->set_extruder($extruder_id) + if $layer->id == 0; + + # adjust flow according to this layer's layer height + my $loop = $skirt_loops[$i]->clone; + { + my $layer_skirt_flow = $skirt_flow->clone; + $layer_skirt_flow->set_height($layer->height); + my $mm3_per_mm = $layer_skirt_flow->mm3_per_mm; + foreach my $path (@$loop) { + $path->height($layer->height); + $path->mm3_per_mm($mm3_per_mm); + } + } + + $gcode .= $self->_gcodegen->extrude_loop($loop, 'skirt', $object->config->support_material_speed); + } + } + $self->_skirt_done->{$layer->print_z} = 1; + $self->_gcodegen->avoid_crossing_perimeters->use_external_mp(0); + $self->_gcodegen->avoid_crossing_perimeters->disable_once(1); + } + + # extrude brim + if (!$self->_brim_done) { + $gcode .= $self->_gcodegen->set_extruder($self->print->regions->[0]->config->perimeter_extruder-1); + $self->_gcodegen->set_origin(Slic3r::Pointf->new(0,0)); + $self->_gcodegen->avoid_crossing_perimeters->use_external_mp(1); + $gcode .= $self->_gcodegen->extrude_loop($_, 'brim', $object->config->support_material_speed) + for @{$self->print->brim}; + $self->_brim_done(1); + $self->_gcodegen->avoid_crossing_perimeters->use_external_mp(0); + $self->_gcodegen->avoid_crossing_perimeters->disable_once(1); + } + + for my $copy (@$object_copies) { + # when starting a new object, use the external motion planner for the first travel move + $self->_gcodegen->avoid_crossing_perimeters->use_external_mp_once(1) if ($self->_last_obj_copy // '') ne "$copy"; + $self->_last_obj_copy("$copy"); + + $self->_gcodegen->set_origin(Slic3r::Pointf->new(map 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 ($layer->isa('Slic3r::Layer::Support')) { + if ($layer->support_interface_fills->count > 0) { + $gcode .= $self->_gcodegen->set_extruder($object->config->support_material_interface_extruder-1); + $gcode .= $self->_gcodegen->extrude_path($_, 'support material interface', $object->config->get_abs_value('support_material_interface_speed')) + for @{$layer->support_interface_fills->chained_path_from($self->_gcodegen->last_pos, 0)}; + } + if ($layer->support_fills->count > 0) { + $gcode .= $self->_gcodegen->set_extruder($object->config->support_material_extruder-1); + $gcode .= $self->_gcodegen->extrude_path($_, 'support material', $object->config->get_abs_value('support_material_speed')) + for @{$layer->support_fills->chained_path_from($self->_gcodegen->last_pos, 0)}; + } + } + + # We now define a strategy for building perimeters and fills. The separation + # between regions doesn't matter in terms of printing order, as we follow + # another logic instead: + # - we group all extrusions by extruder so that we minimize toolchanges + # - we start from the last used extruder + # - for each extruder, we group extrusions by island + # - for each island, we extrude perimeters first, unless user set the infill_first + # option + + # group extrusions by extruder and then by island + my %by_extruder = (); # extruder_id => [ { perimeters => \@perimeters, infill => \@infill } ] + + foreach my $region_id (0..($self->print->region_count-1)) { + my $layerm = $layer->regions->[$region_id] or next; + my $region = $self->print->get_region($region_id); + + # process perimeters + { + my $extruder_id = $region->config->perimeter_extruder-1; + foreach my $perimeter (@{$layerm->perimeters}) { + # init by_extruder item only if we actually use the extruder + $by_extruder{$extruder_id} //= []; + + # $perimeter is an ExtrusionLoop or ExtrusionPath object + for my $i (0 .. $#{$layer->slices}) { + if ($i == $#{$layer->slices} + || $layer->slices->[$i]->contour->contains_point($perimeter->first_point)) { + $by_extruder{$extruder_id}[$i] //= { perimeters => {} }; + $by_extruder{$extruder_id}[$i]{perimeters}{$region_id} //= []; + push @{ $by_extruder{$extruder_id}[$i]{perimeters}{$region_id} }, $perimeter; + last; + } + } + } + } + + # process infill + # $layerm->fills is a collection of ExtrusionPath::Collection objects, each one containing + # the ExtrusionPath objects of a certain infill "group" (also called "surface" + # throughout the code). We can redefine the order of such Collections but we have to + # do each one completely at once. + foreach my $fill (@{$layerm->fills}) { + # init by_extruder item only if we actually use the extruder + my $extruder_id = $fill->[0]->is_solid_infill + ? $region->config->solid_infill_extruder-1 + : $region->config->infill_extruder-1; + + $by_extruder{$extruder_id} //= []; + + # $fill is an ExtrusionPath::Collection object + for my $i (0 .. $#{$layer->slices}) { + if ($i == $#{$layer->slices} + || $layer->slices->[$i]->contour->contains_point($fill->first_point)) { + $by_extruder{$extruder_id}[$i] //= { infill => {} }; + $by_extruder{$extruder_id}[$i]{infill}{$region_id} //= []; + push @{ $by_extruder{$extruder_id}[$i]{infill}{$region_id} }, $fill; + last; + } + } + } + } + + # tweak extruder ordering to save toolchanges + my @extruders = sort keys %by_extruder; + if (@extruders > 1) { + my $last_extruder_id = $self->_gcodegen->writer->extruder->id; + if (exists $by_extruder{$last_extruder_id}) { + @extruders = ( + $last_extruder_id, + grep $_ != $last_extruder_id, @extruders, + ); + } + } + + foreach my $extruder_id (@extruders) { + $gcode .= $self->_gcodegen->set_extruder($extruder_id); + foreach my $island (@{ $by_extruder{$extruder_id} }) { + if ($self->print->config->infill_first) { + $gcode .= $self->_extrude_infill($island->{infill} // {}); + $gcode .= $self->_extrude_perimeters($island->{perimeters} // {}); + } else { + $gcode .= $self->_extrude_perimeters($island->{perimeters} // {}); + $gcode .= $self->_extrude_infill($island->{infill} // {}); + } + } + } + } + + # apply spiral vase post-processing if this layer contains suitable geometry + # (we must feed all the G-code into the post-processor, including the first + # bottom non-spiral layers otherwise it will mess with positions) + # we apply spiral vase at this stage because it requires a full layer + $gcode = $self->_spiral_vase->process_layer($gcode) + if defined $self->_spiral_vase; + + # apply cooling logic; this may alter speeds + $gcode = $self->_cooling_buffer->append( + $gcode, + $layer->object->ptr . ref($layer), # differentiate $obj_id between normal layers and support layers + $layer->id, + $layer->print_z, + ) if defined $self->_cooling_buffer; + + print {$self->fh} $self->filter($gcode); +} + +sub _extrude_perimeters { + my ($self, $entities_by_region) = @_; + + my $gcode = ""; + foreach my $region_id (sort keys %$entities_by_region) { + $self->_gcodegen->config->apply_region_config($self->print->get_region($region_id)->config); + $gcode .= $self->_gcodegen->extrude($_, 'perimeter') + for @{ $entities_by_region->{$region_id} }; + } + return $gcode; +} + +sub _extrude_infill { + my ($self, $entities_by_region) = @_; + + my $gcode = ""; + foreach my $region_id (sort keys %$entities_by_region) { + $self->_gcodegen->config->apply_region_config($self->print->get_region($region_id)->config); + + my $collection = Slic3r::ExtrusionPath::Collection->new(@{ $entities_by_region->{$region_id} }); + for my $fill (@{$collection->chained_path_from($self->_gcodegen->last_pos, 0)}) { + if ($fill->isa('Slic3r::ExtrusionPath::Collection')) { + $gcode .= $self->_gcodegen->extrude($_, 'infill') + for @{$fill->chained_path_from($self->_gcodegen->last_pos, 0)}; + } else { + $gcode .= $self->_gcodegen->extrude($fill, 'infill') ; + } + } + } + return $gcode; +} + +sub flush_cooling_buffer { + my ($self) = @_; + print {$self->fh} $self->filter($self->_cooling_buffer->flush); +} + +sub filter { + my ($self, $gcode) = @_; + + # apply vibration limit if enabled; + # this injects pauses according to time (thus depends on actual speeds) + $gcode = $self->_vibration_limit->process($gcode) + if $self->print->config->vibration_limit != 0; + + # apply pressure regulation if enabled; + # this depends on actual speeds + $gcode = $self->_pressure_regulator->process($gcode) + if $self->print->config->pressure_advance > 0; + + # apply arc fitting if enabled; + # this does not depend on speeds but changes G1 XY commands into G2/G2 IJ + $gcode = $self->_arc_fitting->process($gcode) + if $self->print->config->gcode_arcs; + + return $gcode; +} + +1; diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm index ba03cc6a8..93b69a0fa 100644 --- a/lib/Slic3r/Print/Object.pm +++ b/lib/Slic3r/Print/Object.pm @@ -32,24 +32,6 @@ sub support_layers { return [ map $self->get_support_layer($_), 0..($self->support_layer_count - 1) ]; } -# this is the *total* layer count (including support layers) -# this value is not supposed to be compared with $layer->id -# since they have different semantics -sub total_layer_count { - my $self = shift; - return $self->layer_count + $self->support_layer_count; -} - -sub bounding_box { - my $self = shift; - - # since the object is aligned to origin, bounding box coincides with size - return Slic3r::Geometry::BoundingBox->new_from_points([ - Slic3r::Point->new(0,0), - map Slic3r::Point->new($_->x, $_->y), $self->size #)) - ]); -} - # this should be idempotent sub slice { my $self = shift; @@ -350,7 +332,7 @@ sub _slice_region { # consider the first one $self->model_object->instances->[0]->transform_mesh($mesh, 1); - # align mesh to Z = 0 and apply XY shift + # align mesh to Z = 0 (it should be already aligned actually) and apply XY shift $mesh->translate((map unscale(-$_), @{$self->_copies_shift}), -$self->model_object->bounding_box->z_min); # perform actual slicing @@ -444,7 +426,6 @@ sub make_perimeters { $self->get_layer($i)->make_perimeters; } }, - collect_cb => sub {}, no_threads_cb => sub { $_->make_perimeters for @{$self->layers}; }, @@ -524,7 +505,6 @@ sub infill { $layerm->fills->append($_) for $self->fill_maker->make_fill($layerm); } }, - collect_cb => sub {}, no_threads_cb => sub { foreach my $layerm (map @{$_->regions}, @{$self->layers}) { $layerm->fills->clear; @@ -696,7 +676,8 @@ sub detect_surfaces_type { # Note: this method should be idempotent, but fill_surfaces gets modified # in place. However we're now only using its boundaries (which are invariant) - # so we're safe + # so we're safe. This guarantees idempotence of prepare_infill() also in case + # that combine_infill() turns some fill_surface into VOID surfaces. my $fill_boundaries = [ map $_->clone->p, @{$layerm->fill_surfaces} ]; $layerm->fill_surfaces->clear; foreach my $surface (@{$layerm->slices}) { @@ -712,6 +693,8 @@ sub detect_surfaces_type { } } +# Idempotence of this method is guaranteed by the fact that we don't remove things from +# fill_surfaces but we only turn them into VOID surfaces, thus preserving the boundaries. sub clip_fill_surfaces { my $self = shift; return unless $self->config->infill_only_where_needed; @@ -729,13 +712,13 @@ sub clip_fill_surfaces { # clip this layer's internal surfaces to @overhangs foreach my $layerm (@{$layer->regions}) { - # we assume that this step is run before bridge_over_infill() and combine_infill() - # so these are the only internal types we might have my (@internal, @other) = (); foreach my $surface (map $_->clone, @{$layerm->fill_surfaces}) { - $surface->surface_type == S_TYPE_INTERNAL - ? push @internal, $surface - : push @other, $surface; + if ($surface->surface_type == S_TYPE_INTERNAL) { + push @internal, $surface; + } else { + push @other, $surface; + } } # keep all the original internal surfaces to detect overhangs in this layer @@ -745,10 +728,19 @@ sub clip_fill_surfaces { expolygon => $_, surface_type => S_TYPE_INTERNAL, ), - @{intersection_ex( - $overhangs, - [ map $_->p, @internal ], - )}; + @{intersection_ex( + [ map $_->p, @internal ], + $overhangs, + )}; + + push @new, map Slic3r::Surface->new( + expolygon => $_, + surface_type => S_TYPE_INTERNALVOID, + ), + @{diff_ex( + [ map $_->p, @internal ], + $overhangs, + )}; $layerm->fill_surfaces->clear; $layerm->fill_surfaces->append($_) for (@new, @other); @@ -768,82 +760,6 @@ sub clip_fill_surfaces { } } -sub bridge_over_infill { - my $self = shift; - - for my $region_id (0..($self->print->region_count - 1)) { - my $fill_density = $self->print->regions->[$region_id]->config->fill_density; - next if $fill_density == 100 || $fill_density == 0; - - for my $layer_id (1..($self->layer_count - 1)) { - my $layer = $self->get_layer($layer_id); - my $layerm = $layer->regions->[$region_id]; - my $lower_layer = $self->get_layer($layer_id-1); - - # compute the areas needing bridge math - my @internal_solid = @{$layerm->fill_surfaces->filter_by_type(S_TYPE_INTERNALSOLID)}; - my @lower_internal = map @{$_->fill_surfaces->filter_by_type(S_TYPE_INTERNAL)}, @{$lower_layer->regions}; - my $to_bridge = intersection_ex( - [ map $_->p, @internal_solid ], - [ map $_->p, @lower_internal ], - ); - next unless @$to_bridge; - Slic3r::debugf "Bridging %d internal areas at layer %d\n", scalar(@$to_bridge), $layer_id; - - # build the new collection of fill_surfaces - { - my @new_surfaces = map $_->clone, grep $_->surface_type != S_TYPE_INTERNALSOLID, @{$layerm->fill_surfaces}; - push @new_surfaces, map Slic3r::Surface->new( - expolygon => $_, - surface_type => S_TYPE_INTERNALBRIDGE, - ), @$to_bridge; - push @new_surfaces, map Slic3r::Surface->new( - expolygon => $_, - surface_type => S_TYPE_INTERNALSOLID, - ), @{diff_ex( - [ map $_->p, @internal_solid ], - [ map @$_, @$to_bridge ], - 1, - )}; - $layerm->fill_surfaces->clear; - $layerm->fill_surfaces->append($_) for @new_surfaces; - } - - # exclude infill from the layers below if needed - # see discussion at https://github.com/alexrj/Slic3r/issues/240 - # Update: do not exclude any infill. Sparse infill is able to absorb the excess material. - if (0) { - my $excess = $layerm->extruders->{infill}->bridge_flow->width - $layerm->height; - for (my $i = $layer_id-1; $excess >= $self->get_layer($i)->height; $i--) { - Slic3r::debugf " skipping infill below those areas at layer %d\n", $i; - foreach my $lower_layerm (@{$self->get_layer($i)->regions}) { - my @new_surfaces = (); - # subtract the area from all types of surfaces - foreach my $group (@{$lower_layerm->fill_surfaces->group}) { - push @new_surfaces, map $group->[0]->clone(expolygon => $_), - @{diff_ex( - [ map $_->p, @$group ], - [ map @$_, @$to_bridge ], - )}; - push @new_surfaces, map Slic3r::Surface->new( - expolygon => $_, - surface_type => S_TYPE_INTERNALVOID, - ), @{intersection_ex( - [ map $_->p, @$group ], - [ map @$_, @$to_bridge ], - )}; - } - $lower_layerm->fill_surfaces->clear; - $lower_layerm->fill_surfaces->append($_) for @new_surfaces; - } - - $excess -= $self->get_layer($i)->height; - } - } - } - } -} - sub process_external_surfaces { my ($self) = @_; @@ -1006,42 +922,60 @@ sub discover_horizontal_shells { } # combine fill surfaces across layers +# Idempotence of this method is guaranteed by the fact that we don't remove things from +# fill_surfaces but we only turn them into VOID surfaces, thus preserving the boundaries. sub combine_infill { my $self = shift; - return unless defined first { $_->config->infill_every_layers > 1 && $_->config->fill_density > 0 } @{$self->print->regions}; - - my @layer_heights = map $_->height, @{$self->layers}; + # define the type used for voids + my %voidtype = ( + &S_TYPE_INTERNAL() => S_TYPE_INTERNALVOID, + ); + # work on each region separately for my $region_id (0 .. ($self->print->region_count-1)) { - my $region = $self->print->regions->[$region_id]; + my $region = $self->print->get_region($region_id); my $every = $region->config->infill_every_layers; + next unless $every > 1 && $region->config->fill_density > 0; # limit the number of combined layers to the maximum height allowed by this regions' nozzle - my $nozzle_diameter = $self->print->config->get_at('nozzle_diameter', $region->config->infill_extruder-1); + my $nozzle_diameter = min( + $self->print->config->get_at('nozzle_diameter', $region->config->infill_extruder-1), + $self->print->config->get_at('nozzle_diameter', $region->config->solid_infill_extruder-1), + ); # define the combinations - my @combine = (); # layer_id => thickness in layers + my %combine = (); # layer_idx => number of additional combined lower layers { my $current_height = my $layers = 0; - for my $layer_id (1 .. $#layer_heights) { - my $height = $self->get_layer($layer_id)->height; + for my $layer_idx (0 .. ($self->layer_count-1)) { + my $layer = $self->get_layer($layer_idx); + next if $layer->id == 0; # skip first print layer (which may not be first layer in array because of raft) + my $height = $layer->height; + # check whether the combination of this layer with the lower layers' buffer + # would exceed max layer height or max combined layer count if ($current_height + $height >= $nozzle_diameter || $layers >= $every) { - $combine[$layer_id-1] = $layers; + # append combination to lower layer + $combine{$layer_idx-1} = $layers; $current_height = $layers = 0; } $current_height += $height; $layers++; } + + # append lower layers (if any) to uppermost layer + $combine{$self->layer_count-1} = $layers; } - # skip bottom layer - for my $layer_id (1 .. $#combine) { - next unless ($combine[$layer_id] // 1) > 1; - my @layerms = map $self->get_layer($_)->regions->[$region_id], - ($layer_id - ($combine[$layer_id]-1) .. $layer_id); + # loop through layers to which we have assigned layers to combine + for my $layer_idx (sort keys %combine) { + next unless $combine{$layer_idx} > 1; + + # get all the LayerRegion objects to be combined + my @layerms = map $self->get_layer($_)->get_region($region_id), + ($layer_idx - ($combine{$layer_idx}-1) .. $layer_idx); # only combine internal infill for my $type (S_TYPE_INTERNAL) { @@ -1064,7 +998,7 @@ sub combine_infill { Slic3r::debugf " combining %d %s regions from layers %d-%d\n", scalar(@$intersection), ($type == S_TYPE_INTERNAL ? 'internal' : 'internal-solid'), - $layer_id-($every-1), $layer_id; + $layer_idx-($every-1), $layer_idx; # $intersection now contains the regions that can be combined across the full amount of layers # so let's remove those areas from all layers @@ -1091,7 +1025,7 @@ sub combine_infill { )}; # apply surfaces back with adjusted depth to the uppermost layer - if ($layerm->id == $layer_id) { + if ($layerm->id == $self->get_layer($layer_idx)->id) { push @new_this_type, map Slic3r::Surface->new( expolygon => $_, @@ -1102,8 +1036,8 @@ sub combine_infill { @$intersection; } else { # save void surfaces - push @this_type, - map Slic3r::Surface->new(expolygon => $_, surface_type => S_TYPE_INTERNALVOID), + push @new_this_type, + map Slic3r::Surface->new(expolygon => $_, surface_type => $voidtype{$type}), @{intersection_ex( [ map @{$_->expolygon}, @this_type ], [ @intersection_with_clearance ], diff --git a/lib/Slic3r/Print/Simple.pm b/lib/Slic3r/Print/Simple.pm index 7647bbfc4..5618484fa 100644 --- a/lib/Slic3r/Print/Simple.pm +++ b/lib/Slic3r/Print/Simple.pm @@ -68,6 +68,7 @@ sub set_model { # if all input objects have defined position(s) apply duplication to the whole model $model->duplicate($self->duplicate, $self->_print->config->min_object_distance); } + $_->translate(0,0,-$_->bounding_box->z_min) for @{$model->objects}; $model->center_instances_around_point($self->print_center); foreach my $model_object (@{$model->objects}) { diff --git a/lib/Slic3r/Print/SupportMaterial.pm b/lib/Slic3r/Print/SupportMaterial.pm index 596d87983..a738c1dd8 100644 --- a/lib/Slic3r/Print/SupportMaterial.pm +++ b/lib/Slic3r/Print/SupportMaterial.pm @@ -17,9 +17,6 @@ has 'interface_flow' => (is => 'rw', required => 1); use constant DEBUG_CONTACT_ONLY => 0; -# how much we extend support around the actual contact area -use constant MARGIN => 1.5; - # increment used to reach MARGIN in steps to avoid trespassing thin objects use constant MARGIN_STEP => MARGIN/3; @@ -69,6 +66,10 @@ sub generate { $self->clip_with_object($base, $support_z, $object); $self->clip_with_shape($base, $shape) if @$shape; + # Detect what part of base support layers are "reverse interfaces" because they + # lie above object's top surfaces. + $self->generate_bottom_interface_layers($support_z, $base, $top, $interface); + # Install support layers into object. for my $i (0 .. $#$support_z) { $object->add_support_layer( @@ -265,7 +266,7 @@ sub contact_area { { # get the average nozzle diameter used on this layer my @nozzle_diameters = map $self->print_config->get_at('nozzle_diameter', $_), - map { $_->config->perimeter_extruder-1, $_->config->infill_extruder-1 } + map { $_->config->perimeter_extruder-1, $_->config->infill_extruder-1, $_->config->solid_infill_extruder-1 } @{$layer->regions}; my $nozzle_diameter = sum(@nozzle_diameters)/@nozzle_diameters; @@ -338,7 +339,12 @@ sub support_layers_z { # layer_height > nozzle_diameter * 0.75 my $nozzle_diameter = $self->print_config->get_at('nozzle_diameter', $self->object_config->support_material_extruder-1); my $support_material_height = max($max_object_layer_height, $nozzle_diameter * 0.75); - my @z = sort { $a <=> $b } @$contact_z, @$top_z, (map $_ + $nozzle_diameter, @$top_z); + + # initialize known, fixed, support layers + my @z = sort { $a <=> $b } + @$contact_z, + @$top_z, # TODO: why we have this? + (map $_ + contact_distance($nozzle_diameter), @$top_z); # enforce first layer height my $first_layer_height = $self->object_config->get_value('first_layer_height'); @@ -418,6 +424,48 @@ sub generate_interface_layers { return \%interface; } +sub generate_bottom_interface_layers { + my ($self, $support_z, $base, $top, $interface) = @_; + + my $area_threshold = $self->interface_flow->scaled_spacing ** 2; + + # loop through object's top surfaces + foreach my $top_z (sort keys %$top) { + my $this = $top->{$top_z}; + + # keep a count of the interface layers we generated for this top surface + my $interface_layers = 0; + + # loop through support layers until we find the one(s) right above the top + # surface + foreach my $layer_id (0 .. $#$support_z) { + my $z = $support_z->[$layer_id]; + next unless $z > $top_z; + + # get the support material area that should be considered interface + my $interface_area = intersection( + $base->{$layer_id}, + $this, + ); + + # discard too small areas + $interface_area = [ grep abs($_->area) >= $area_threshold, @$interface_area ]; + + # subtract new interface area from base + $base->{$layer_id} = diff( + $base->{$layer_id}, + $interface_area, + ); + + # add new interface area to interface + push @{$interface->{$layer_id}}, @$interface_area; + + $interface_layers++; + last if $interface_layers == $self->object_config->support_material_interface_layers; + } + } +} + sub generate_base_layers { my ($self, $support_z, $contact, $interface, $top) = @_; @@ -623,6 +671,7 @@ sub generate_toolpaths { # interface and contact infill if (@$interface || @$contact_infill) { $fillers{interface}->angle($interface_angle); + $fillers{interface}->spacing($_interface_flow->spacing); # find centerline of the external loop $interface = offset2($interface, +scaled_epsilon, -(scaled_epsilon + $_interface_flow->scaled_width/2)); @@ -648,20 +697,19 @@ sub generate_toolpaths { my @paths = (); foreach my $expolygon (@{union_ex($interface)}) { - my ($params, @p) = $fillers{interface}->fill_surface( + my @p = $fillers{interface}->fill_surface( Slic3r::Surface->new(expolygon => $expolygon, surface_type => S_TYPE_INTERNAL), - density => $interface_density, - flow => $_interface_flow, + density => $interface_density, layer_height => $layer->height, - complete => 1, + complete => 1, ); - my $mm3_per_mm = $params->{flow}->mm3_per_mm; + my $mm3_per_mm = $_interface_flow->mm3_per_mm; push @paths, map Slic3r::ExtrusionPath->new( polyline => Slic3r::Polyline->new(@$_), role => EXTR_ROLE_SUPPORTMATERIAL_INTERFACE, mm3_per_mm => $mm3_per_mm, - width => $params->{flow}->width, + width => $_interface_flow->width, height => $layer->height, ), @p; } @@ -702,22 +750,22 @@ sub generate_toolpaths { # TODO: use offset2_ex() $to_infill = offset_ex([ map @$_, @$to_infill ], -$_flow->scaled_spacing); } + $filler->spacing($base_flow->spacing); foreach my $expolygon (@$to_infill) { - my ($params, @p) = $filler->fill_surface( + my @p = $filler->fill_surface( Slic3r::Surface->new(expolygon => $expolygon, surface_type => S_TYPE_INTERNAL), density => $density, - flow => $base_flow, layer_height => $layer->height, complete => 1, ); - my $mm3_per_mm = $params->{flow}->mm3_per_mm; + my $mm3_per_mm = $base_flow->mm3_per_mm; push @paths, map Slic3r::ExtrusionPath->new( polyline => Slic3r::Polyline->new(@$_), role => EXTR_ROLE_SUPPORTMATERIAL, mm3_per_mm => $mm3_per_mm, - width => $params->{flow}->width, + width => $base_flow->width, height => $layer->height, ), @p; } diff --git a/slic3r.pl b/slic3r.pl index cd8ddd371..debc5ff79 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -364,7 +364,7 @@ $j --fill-density Infill density (range: 0%-100%, default: $config->{fill_density}%) --fill-angle Infill angle in degrees (range: 0-90, default: $config->{fill_angle}) --fill-pattern Pattern to use to fill non-solid layers (default: $config->{fill_pattern}) - --solid-fill-pattern Pattern to use to fill solid layers (default: $config->{solid_fill_pattern}) + --external-fill-pattern Pattern to use to fill solid layers (default: $config->{external_fill_pattern}) --start-gcode Load initial G-code from the supplied file. This will overwrite the default command (home all axes [G28]). --end-gcode Load final G-code from the supplied file. This will overwrite @@ -503,10 +503,11 @@ $j --extruder-offset Offset of each extruder, if firmware doesn't handle the displacement (can be specified multiple times, default: 0x0) --perimeter-extruder - Extruder to use for perimeters (1+, default: $config->{perimeter_extruder}) + Extruder to use for perimeters and brim (1+, default: $config->{perimeter_extruder}) --infill-extruder Extruder to use for infill (1+, default: $config->{infill_extruder}) + --solid-infill-extruder Extruder to use for solid infill (1+, default: $config->{solid_infill_extruder}) --support-material-extruder - Extruder to use for support material (1+, default: $config->{support_material_extruder}) + Extruder to use for support material, raft and skirt (1+, default: $config->{support_material_extruder}) --support-material-interface-extruder Extruder to use for support material interface (1+, default: $config->{support_material_interface_extruder}) --ooze-prevention Drop temperature and park extruders outside a full skirt for automatic wiping diff --git a/t/avoid_crossing_perimeters.t b/t/avoid_crossing_perimeters.t new file mode 100644 index 000000000..dd6c3e7b6 --- /dev/null +++ b/t/avoid_crossing_perimeters.t @@ -0,0 +1,21 @@ +use Test::More tests => 1; +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use List::Util qw(first sum); +use Slic3r; +use Slic3r::Test; + +{ + my $config = Slic3r::Config->new_from_defaults; + $config->set('avoid_crossing_perimeters', 2); + my $print = Slic3r::Test::init_print('20mm_cube', config => $config, duplicate => 2); + ok my $gcode = Slic3r::Test::gcode($print), "no crash with avoid_crossing_perimeters and multiple objects"; +} + +__END__ diff --git a/t/combineinfill.t b/t/combineinfill.t index 62e22e6b2..24cd2bb71 100644 --- a/t/combineinfill.t +++ b/t/combineinfill.t @@ -9,9 +9,67 @@ BEGIN { use List::Util qw(first); use Slic3r; +use Slic3r::Surface ':types'; use Slic3r::Test; -plan tests => 2; +plan tests => 8; + +{ + my $test = sub { + my ($config) = @_; + + my $print = Slic3r::Test::init_print('20mm_cube', config => $config); + ok my $gcode = Slic3r::Test::gcode($print), "infill_every_layers does not crash"; + + my $tool = undef; + my %layers = (); # layer_z => 1 + my %layer_infill = (); # layer_z => has_infill + Slic3r::GCode::Reader->new->parse($gcode, sub { + my ($self, $cmd, $args, $info) = @_; + + if ($cmd =~ /^T(\d+)/) { + $tool = $1; + } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0 && $tool != $config->support_material_extruder-1) { + $layer_infill{$self->Z} //= 0; + if ($tool == $config->infill_extruder-1) { + $layer_infill{$self->Z} = 1; + } + } + $layers{$args->{Z}} = 1 if $cmd eq 'G1' && $info->{dist_Z} > 0; + }); + + my $layers_with_perimeters = scalar(keys %layer_infill); + my $layers_with_infill = grep $_ > 0, values %layer_infill; + is scalar(keys %layers), $layers_with_perimeters+$config->raft_layers, 'expected number of layers'; + + # first infill layer is never combined, so we don't consider it + $layers_with_infill--; + $layers_with_perimeters--; + + # we expect that infill is generated for half the number of combined layers + # plus for each single layer that was not combined (remainder) + is $layers_with_infill, + int($layers_with_perimeters/$config->infill_every_layers) + ($layers_with_perimeters % $config->infill_every_layers), + 'infill is only present in correct number of layers'; + }; + + my $config = Slic3r::Config->new_from_defaults; + $config->set('layer_height', 0.2); + $config->set('first_layer_height', 0.2); + $config->set('nozzle_diameter', [0.5]); + $config->set('infill_every_layers', 2); + $config->set('perimeter_extruder', 1); + $config->set('infill_extruder', 2); + $config->set('support_material_extruder', 3); + $config->set('support_material_interface_extruder', 3); + $config->set('top_solid_layers', 0); + $config->set('bottom_solid_layers', 0); + $test->($config); + + $config->set('skirts', 0); # prevent usage of perimeter_extruder in raft layers + $config->set('raft_layers', 5); + $test->($config); +} { my $config = Slic3r::Config->new_from_defaults; @@ -19,29 +77,22 @@ plan tests => 2; $config->set('first_layer_height', 0.2); $config->set('nozzle_diameter', [0.5]); $config->set('infill_every_layers', 2); - $config->set('infill_extruder', 2); - $config->set('top_solid_layers', 0); - $config->set('bottom_solid_layers', 0); - my $print = Slic3r::Test::init_print('20mm_cube', config => $config); - ok my $gcode = Slic3r::Test::gcode($print), "infill_every_layers does not crash"; - my $tool = undef; - my %layer_infill = (); # layer_z => has_infill - Slic3r::GCode::Reader->new->parse($gcode, sub { - my ($self, $cmd, $args, $info) = @_; - - if ($cmd =~ /^T(\d+)/) { - $tool = $1; - } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) { - $layer_infill{$self->Z} //= 0; - if ($tool == $config->infill_extruder-1) { - $layer_infill{$self->Z} = 1; - } - } - }); - my $layers_with_infill = grep $_, values %layer_infill; - $layers_with_infill--; # first layer is never combined - is $layers_with_infill, scalar(keys %layer_infill)/2, 'infill is only present in correct number of layers'; + my $print = Slic3r::Test::init_print('20mm_cube', config => $config); + $print->process; + + ok defined(first { @{$_->get_region(0)->fill_surfaces->filter_by_type(S_TYPE_INTERNALVOID)} > 0 } + @{$print->print->get_object(0)->layers}), + 'infill combination produces internal void surfaces'; + + # we disable combination after infill has been generated + $config->set('infill_every_layers', 1); + $print->apply_config($config); + $print->process; + + ok !(defined first { @{$_->get_region(0)->fill_surfaces} == 0 } + @{$print->print->get_object(0)->layers}), + 'infill combination is idempotent'; } # the following needs to be adapted to the new API diff --git a/t/fill.t b/t/fill.t index f0fe6f903..63c3c087f 100644 --- a/t/fill.t +++ b/t/fill.t @@ -49,9 +49,10 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ } height => 0.4, nozzle_diameter => 0.50, ); + $filler->spacing($flow->spacing); foreach my $angle (0, 45) { $surface->expolygon->rotate(Slic3r::Geometry::deg2rad($angle), [0,0]); - my ($params, @paths) = $filler->fill_surface($surface, flow => $flow, layer_height => 0.4, density => 0.4); + my @paths = $filler->fill_surface($surface, layer_height => 0.4, density => 0.4); is scalar @paths, 1, 'one continuous path'; } } @@ -73,15 +74,15 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ } height => 0.4, nozzle_diameter => $flow_spacing, ); - my ($params, @paths) = $filler->fill_surface( + $filler->spacing($flow->spacing); + my @paths = $filler->fill_surface( $surface, - flow => $flow, layer_height => $flow->height, density => $density // 1, ); # check whether any part was left uncovered - my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $params->{flow}->spacing/2)}, @paths; + my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $filler->spacing/2)}, @paths; my $uncovered = diff_ex([ @$expolygon ], [ @grown_paths ], 1); # ignore very small dots @@ -171,7 +172,7 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ } for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) { my $config = Slic3r::Config->new_from_defaults; $config->set('fill_pattern', $pattern); - $config->set('solid_fill_pattern', $pattern); + $config->set('external_fill_pattern', $pattern); $config->set('perimeters', 1); $config->set('skirts', 0); $config->set('fill_density', 20); diff --git a/t/gcode.t b/t/gcode.t index 17393e7cf..aa15098a0 100644 --- a/t/gcode.t +++ b/t/gcode.t @@ -1,4 +1,4 @@ -use Test::More tests => 19; +use Test::More tests => 20; use strict; use warnings; @@ -24,14 +24,24 @@ use Slic3r::Test; { my $config = Slic3r::Config->new_from_defaults; $config->set('wipe', [1]); + $config->set('retract_layer_change', [0]); my $print = Slic3r::Test::init_print('20mm_cube', config => $config); my $have_wipe = 0; my @retract_speeds = (); + my $extruded_on_this_layer = 0; + my $wiping_on_new_layer = 0; Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub { my ($self, $cmd, $args, $info) = @_; - if ($info->{retracting} && $info->{dist_XY} > 0) { + + if ($info->{travel} && $info->{dist_Z}) { + # changing layer + $extruded_on_this_layer = 0; + } elsif ($info->{extruding} && $info->{dist_XY}) { + $extruded_on_this_layer = 1; + } elsif ($info->{retracting} && $info->{dist_XY} > 0) { $have_wipe = 1; + $wiping_on_new_layer = 1 if !$extruded_on_this_layer; my $move_time = $info->{dist_XY} / ($args->{F} // $self->F); push @retract_speeds, abs($info->{dist_E}) / $move_time; } @@ -39,6 +49,7 @@ use Slic3r::Test; ok $have_wipe, "wipe"; ok !defined (first { abs($_ - $config->retract_speed->[0]*60) < 5 } @retract_speeds), 'wipe moves don\'t retract faster than configured speed'; + ok !$wiping_on_new_layer, 'no wiping after layer change'; } { diff --git a/t/geometry.t b/t/geometry.t index 24fbf52d9..d85c8bbcf 100644 --- a/t/geometry.t +++ b/t/geometry.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 33; +plan tests => 38; BEGIN { use FindBin; @@ -213,3 +213,29 @@ my $polygons = [ is scalar(@{$square->concave_points(PI*4/3)}), 0, 'no concave vertices detected in convex polygon'; is scalar(@{$square->convex_points(PI*2/3)}), 4, 'four convex vertices detected in square'; } + +{ + my $triangle = Slic3r::Polygon->new( + [16000170,26257364], [714223,461012], [31286371,461008], + ); + is scalar(@{$triangle->concave_points(PI*4/3)}), 0, 'no concave vertices detected in triangle'; + is scalar(@{$triangle->convex_points(PI*2/3)}), 3, 'three convex vertices detected in triangle'; +} + +{ + my $triangle = Slic3r::Polygon->new( + [16000170,26257364], [714223,461012], [20000000,461012], [31286371,461012], + ); + is scalar(@{$triangle->concave_points(PI*4/3)}), 0, 'no concave vertices detected in triangle having collinear point'; + is scalar(@{$triangle->convex_points(PI*2/3)}), 3, 'three convex vertices detected in triangle having collinear point'; +} + +{ + my $triangle = Slic3r::Polygon->new( + [16000170,26257364], [714223,461012], [31286371,461008], + ); + my $simplified = $triangle->simplify(250000)->[0]; + is scalar(@$simplified), 3, 'triangle is never simplified to less than 3 points'; +} + +__END__ diff --git a/t/perimeters.t b/t/perimeters.t index 17e1b31c7..49217fd82 100644 --- a/t/perimeters.t +++ b/t/perimeters.t @@ -266,7 +266,7 @@ use Slic3r::Test; my $was_extruding = 0; my @seam_points = (); my $print = Slic3r::Test::init_print($model_name, config => $config); - Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub { + Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub { my ($self, $cmd, $args, $info) = @_; if ($info->{extruding}) { diff --git a/t/support.t b/t/support.t index 0fadc1d14..edf5476c5 100644 --- a/t/support.t +++ b/t/support.t @@ -22,15 +22,15 @@ use Slic3r::Test; my $print = Slic3r::Test::init_print('20mm_cube', config => $config); $print->print->init_extruders; my $flow = $print->print->objects->[0]->support_material_flow; - my $support_z = Slic3r::Print::SupportMaterial - ->new( - object_config => $print->print->objects->[0]->config, - print_config => $print->print->config, - flow => $flow, - interface_flow => $flow, - first_layer_flow => $flow, - ) - ->support_layers_z(\@contact_z, \@top_z, $config->layer_height); + my $support = Slic3r::Print::SupportMaterial->new( + object_config => $print->print->objects->[0]->config, + print_config => $print->print->config, + flow => $flow, + interface_flow => $flow, + first_layer_flow => $flow, + ); + my $support_z = $support->support_layers_z(\@contact_z, \@top_z, $config->layer_height); + my $expected_top_spacing = Slic3r::Print::SupportMaterial::contact_distance($config->nozzle_diameter->[0]); is $support_z->[0], $config->first_layer_height, 'first layer height is honored'; @@ -44,9 +44,10 @@ use Slic3r::Test; # find layer index of this top surface my $layer_id = first { abs($support_z->[$_] - $top_z) < epsilon } 0..$#$support_z; - # check that first support layer above this top surface is spaced with nozzle diameter + # check that first support layer above this top surface (or the next one) is spaced with nozzle diameter $wrong_top_spacing = 1 - if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $config->nozzle_diameter->[0]; + if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $expected_top_spacing + && ($support_z->[$layer_id+2] - $support_z->[$layer_id]) != $expected_top_spacing; } ok !$wrong_top_spacing, 'layers above top surfaces are spaced correctly'; }; @@ -67,12 +68,12 @@ use Slic3r::Test; { my $config = Slic3r::Config->new_from_defaults; $config->set('raft_layers', 3); - $config->set('brim_width', 6); + $config->set('brim_width', 0); $config->set('skirts', 0); $config->set('support_material_extruder', 2); $config->set('support_material_interface_extruder', 2); $config->set('layer_height', 0.4); - $config->set('first_layer_height', '100%'); + $config->set('first_layer_height', 0.4); my $print = Slic3r::Test::init_print('overhang', config => $config); ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim'; @@ -84,7 +85,7 @@ use Slic3r::Test; $tool = $1; } elsif ($info->{extruding}) { if ($self->Z <= ($config->raft_layers * $config->layer_height)) { - fail 'not extruding raft/brim with support material extruder' + fail 'not extruding raft with support material extruder' if $tool != ($config->support_material_extruder-1); } else { fail 'support material exceeds raft layers' diff --git a/t/vibrationlimit.t b/t/vibrationlimit.t index 2553e2260..7bfa27acb 100644 --- a/t/vibrationlimit.t +++ b/t/vibrationlimit.t @@ -57,8 +57,11 @@ my $test = sub { my $one_axis_would_trigger_limit_without_pause = 0; foreach my $axis (qw(X Y)) { + # get the direction by comparing the new $axis coordinate with the current one + # 1 = positive, 0 = no change, -1 = negative + my $dir = $info->{"new_$axis"} <=> $self->$axis; + # are we changing direction on this axis? - my $dir = $info->{"dist_$axis"} <=> ($args->{$axis} // $self->$axis); if ($dir != 0 && $dir{$axis} != $dir) { # this move changes direction on this axis if ($dir{$axis} != 0) { diff --git a/utils/view-mesh.pl b/utils/view-mesh.pl index a696d7c85..25fbf3638 100644 --- a/utils/view-mesh.pl +++ b/utils/view-mesh.pl @@ -20,6 +20,7 @@ my %opt = (); my %options = ( 'help' => sub { usage() }, 'cut=f' => \$opt{cut}, + 'enable-moving' => \$opt{enable_moving}, ); GetOptions(%options) or usage(1); $ARGV[0] or usage(1); @@ -32,9 +33,11 @@ my %opt = (); $model->add_default_instances; my $app = Slic3r::ViewMesh->new; + $app->{canvas}->enable_picking(1); + $app->{canvas}->enable_moving($opt{enable_moving}); $app->{canvas}->load_object($model->objects->[0]); - $app->{canvas}->set_bounding_box($model->objects->[0]->bounding_box); $app->{canvas}->set_auto_bed_shape; + $app->{canvas}->zoom_to_volumes; $app->{canvas}->SetCuttingPlane($opt{cut}) if defined $opt{cut}; $app->MainLoop; } diff --git a/utils/view-toolpaths.pl b/utils/view-toolpaths.pl index 4b03bbbea..847ba61d9 100755 --- a/utils/view-toolpaths.pl +++ b/utils/view-toolpaths.pl @@ -20,6 +20,8 @@ my %opt = (); my %options = ( 'help' => sub { usage() }, 'load=s' => \$opt{load}, + '3D' => \$opt{d3}, + 'duplicate=i' => \$opt{duplicate}, ); GetOptions(%options) or usage(1); $ARGV[0] or usage(1); @@ -38,12 +40,14 @@ my %opt = (); # init print my $sprint = Slic3r::Print::Simple->new; + $sprint->duplicate($opt{duplicate} // 1); $sprint->apply_config($config); $sprint->set_model($model); $sprint->process; # visualize toolpaths $Slic3r::ViewToolpaths::print = $sprint->_print; + $Slic3r::ViewToolpaths::d3 = $opt{d3}; my $app = Slic3r::ViewToolpaths->new; $app->MainLoop; } @@ -65,9 +69,10 @@ EOF package Slic3r::ViewToolpaths; use Wx qw(:sizer); -use base qw(Wx::App); +use base qw(Wx::App Class::Accessor); our $print; +our $d3; sub OnInit { my $self = shift; @@ -75,8 +80,23 @@ sub OnInit { my $frame = Wx::Frame->new(undef, -1, 'Toolpaths', [-1, -1], [500, 500]); my $panel = Wx::Panel->new($frame, -1); + my $canvas; + if ($d3) { + $canvas = Slic3r::GUI::PreviewCanvas->new($panel); + $canvas->print($print); + + $canvas->set_bed_shape($print->config->bed_shape); + + foreach my $object (@{$print->objects}) { + $canvas->load_object($object->model_object); + } + $canvas->zoom_to_volumes; + } else { + $canvas = Slic3r::GUI::Plater::2DToolpaths->new($panel, $print); + } + my $sizer = Wx::BoxSizer->new(wxVERTICAL); - $sizer->Add(Slic3r::GUI::Plater::2DToolpaths->new($panel, $print), 1, wxEXPAND, 0); + $sizer->Add($canvas, 1, wxEXPAND, 0); $panel->SetSizer($sizer); $frame->Show(1); diff --git a/utils/wireframe.pl b/utils/wireframe.pl new file mode 100644 index 000000000..053581dec --- /dev/null +++ b/utils/wireframe.pl @@ -0,0 +1,172 @@ +#!/usr/bin/perl +# This script exports experimental G-code for wireframe printing +# (inspired by the brilliant WirePrint concept) + +use strict; +use warnings; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Getopt::Long qw(:config no_auto_abbrev); +use Slic3r; +use Slic3r::ExtrusionPath ':roles'; +use Slic3r::Geometry qw(scale unscale X Y PI); + +my %opt = ( + step_height => 5, + nozzle_angle => 30, + nozzle_width => 10, + first_layer_height => 0.3, +); +{ + my %options = ( + 'help' => sub { usage() }, + 'output|o=s' => \$opt{output_file}, + 'step-height|h=f' => \$opt{step_height}, + 'nozzle-angle|a=f' => \$opt{nozzle_angle}, + 'nozzle-width|w=f' => \$opt{nozzle_width}, + 'first-layer-height=f' => \$opt{first_layer_height}, + ); + GetOptions(%options) or usage(1); + $opt{output_file} or usage(1); + $ARGV[0] or usage(1); +} + +{ + # load model + my $model = Slic3r::Model->read_from_file($ARGV[0]); + $model->add_default_instances; + $model->center_instances_around_point(Slic3r::Pointf->new(100,100)); + my $mesh = $model->mesh; + $mesh->translate(0, 0, -$mesh->bounding_box->z_min); + + # get slices + my @z = (); + my $z_max = $mesh->bounding_box->z_max; + for (my $z = $opt{first_layer_height}; $z <= $z_max; $z += $opt{step_height}) { + push @z, $z; + } + my @slices = @{$mesh->slice(\@z)}; + + my $flow = Slic3r::Flow->new( + width => 0.35, + height => 0.35, + nozzle_diameter => 0.35, + bridge => 1, + ); + + my $config = Slic3r::Config::Print->new; + $config->set('gcode_comments', 1); + + open my $fh, '>', $opt{output_file}; + my $gcodegen = Slic3r::GCode->new( + enable_loop_clipping => 0, # better bonding + ); + $gcodegen->apply_print_config($config); + $gcodegen->set_extruders([0]); + print $fh $gcodegen->set_extruder(0); + print $fh $gcodegen->writer->preamble; + + my $e = $gcodegen->writer->extruder->e_per_mm3 * $flow->mm3_per_mm; + + foreach my $layer_id (0..$#z) { + my $z = $z[$layer_id]; + + foreach my $island (@{$slices[$layer_id]}) { + foreach my $polygon (@$island) { + if ($layer_id > 0) { + # find the lower polygon that we want to connect to this one + my $lower = $slices[$layer_id-1]->[0]->contour; # 't was easy, wasn't it? + my $lower_z = $z[$layer_id-1]; + + { + my @points = (); + + # keep all points with strong angles + { + my @pp = @$polygon; + foreach my $i (0..$#pp) { + push @points, $pp[$i-1] if abs($pp[$i-1]->ccw_angle($pp[$i-2], $pp[$i]) - PI) > PI/3; + } + } + + $polygon = Slic3r::Polygon->new(@points); + } + #$polygon = Slic3r::Polygon->new(@{$polygon->split_at_first_point->equally_spaced_points(scale $opt{nozzle_width})}); + + # find vertical lines + my @vertical = (); + foreach my $point (@{$polygon}) { + push @vertical, Slic3r::Line->new($point->projection_onto_polygon($lower), $point); + } + + next if !@vertical; + + my @points = (); + foreach my $line (@vertical) { + push @points, Slic3r::Pointf3->new( + unscale($line->a->x), + unscale($line->a->y), #)) + $lower_z, + ); + push @points, Slic3r::Pointf3->new( + unscale($line->b->x), + unscale($line->b->y), #)) + $z, + ); + } + + # reappend first point as destination of the last diagonal segment + push @points, Slic3r::Pointf3->new( + unscale($vertical[0]->a->x), + unscale($vertical[0]->a->y), #)) + $lower_z, + ); + + # move to the position of the first vertical line + print $fh $gcodegen->writer->travel_to_xyz(shift @points); + + # extrude segments + foreach my $point (@points) { + print $fh $gcodegen->writer->extrude_to_xyz($point, $e * $gcodegen->writer->get_position->distance_to($point)); + } + } + } + + print $fh $gcodegen->writer->travel_to_z($z); + foreach my $polygon (@$island) { + #my $polyline = $polygon->split_at_vertex(Slic3r::Point->new_scale(@{$gcodegen->writer->get_position}[0,1])); + my $polyline = $polygon->split_at_first_point; + print $fh $gcodegen->writer->travel_to_xy(Slic3r::Pointf->new_unscale(@{ $polyline->first_point }), "move to first contour point"); + + foreach my $line (@{$polyline->lines}) { + my $point = Slic3r::Pointf->new_unscale(@{ $line->b }); + print $fh $gcodegen->writer->extrude_to_xy($point, $e * unscale($line->length)); + } + } + } + } + + close $fh; +} + +sub usage { + my ($exit_code) = @_; + + print <<"EOF"; +Usage: wireframe.pl [ OPTIONS ] file.stl + + --help Output this usage screen and exit + --output, -o Write to the specified file + --step-height, -h Use the specified step height + --nozzle-angle, -a Max nozzle angle + --nozzle-width, -w External nozzle diameter + +EOF + exit ($exit_code || 0); +} + +__END__ diff --git a/xs/MANIFEST b/xs/MANIFEST index 2ef005e1f..808240797 100644 --- a/xs/MANIFEST +++ b/xs/MANIFEST @@ -1723,6 +1723,7 @@ src/libslic3r/PrintConfig.cpp src/libslic3r/PrintConfig.hpp src/libslic3r/PrintObject.cpp src/libslic3r/PrintRegion.cpp +src/libslic3r/SupportMaterial.hpp src/libslic3r/Surface.cpp src/libslic3r/Surface.hpp src/libslic3r/SurfaceCollection.cpp @@ -1794,6 +1795,7 @@ xsp/Polygon.xsp xsp/Polyline.xsp xsp/PolylineCollection.xsp xsp/Print.xsp +xsp/SupportMaterial.xsp xsp/Surface.xsp xsp/SurfaceCollection.xsp xsp/TriangleMesh.xsp diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm index 112c6ebc3..9ac0b35e9 100644 --- a/xs/lib/Slic3r/XS.pm +++ b/xs/lib/Slic3r/XS.pm @@ -210,6 +210,7 @@ for my $class (qw( Slic3r::Layer::Region Slic3r::Layer::Support Slic3r::Line + Slic3r::Linef3 Slic3r::Model Slic3r::Model::Instance Slic3r::Model::Material diff --git a/xs/src/libslic3r/ClipperUtils.cpp b/xs/src/libslic3r/ClipperUtils.cpp index 2a86f2a26..ba243ee82 100644 --- a/xs/src/libslic3r/ClipperUtils.cpp +++ b/xs/src/libslic3r/ClipperUtils.cpp @@ -425,6 +425,18 @@ template void diff(const Slic3r::Polygons & template void diff(const Slic3r::Polylines &subject, const Slic3r::Polygons &clip, Slic3r::Polylines* retval, bool safety_offset_); template void diff(const Slic3r::Lines &subject, const Slic3r::Polygons &clip, Slic3r::Lines* retval, bool safety_offset_); +template +void diff(const SubjectType &subject, const Slic3r::ExPolygons &clip, ResultType* retval, bool safety_offset_) +{ + Slic3r::Polygons pp; + for (Slic3r::ExPolygons::const_iterator ex = clip.begin(); ex != clip.end(); ++ex) { + Slic3r::Polygons ppp = *ex; + pp.insert(pp.end(), ppp.begin(), ppp.end()); + } + diff(subject, pp, retval, safety_offset_); +} +template void diff(const Slic3r::Polygons &subject, const Slic3r::ExPolygons &clip, Slic3r::ExPolygons* retval, bool safety_offset_); + template void intersection(const SubjectType &subject, const Slic3r::Polygons &clip, ResultType* retval, bool safety_offset_) { diff --git a/xs/src/libslic3r/ClipperUtils.hpp b/xs/src/libslic3r/ClipperUtils.hpp index 5b9d356b9..ab144f202 100644 --- a/xs/src/libslic3r/ClipperUtils.hpp +++ b/xs/src/libslic3r/ClipperUtils.hpp @@ -83,6 +83,9 @@ void _clipper(ClipperLib::ClipType clipType, const Slic3r::Lines &subject, template void diff(const SubjectType &subject, const Slic3r::Polygons &clip, ResultType* retval, bool safety_offset_ = false); +template +void diff(const SubjectType &subject, const Slic3r::ExPolygons &clip, ResultType* retval, bool safety_offset_ = false); + template void intersection(const SubjectType &subject, const Slic3r::Polygons &clip, ResultType* retval, bool safety_offset_ = false); diff --git a/xs/src/libslic3r/ExtrusionEntity.cpp b/xs/src/libslic3r/ExtrusionEntity.cpp index 1bc5b4b24..ee49e3357 100644 --- a/xs/src/libslic3r/ExtrusionEntity.cpp +++ b/xs/src/libslic3r/ExtrusionEntity.cpp @@ -76,9 +76,18 @@ ExtrusionPath::is_perimeter() const } bool -ExtrusionPath::is_fill() const +ExtrusionPath::is_infill() const { - return this->role == erInternalInfill + return this->role == erBridgeInfill + || this->role == erInternalInfill + || this->role == erSolidInfill + || this->role == erTopSolidInfill; +} + +bool +ExtrusionPath::is_solid_infill() const +{ + return this->role == erBridgeInfill || this->role == erSolidInfill || this->role == erTopSolidInfill; } @@ -235,9 +244,6 @@ ExtrusionLoop::split_at_vertex(const Point &point) path->polyline.points.insert(path->polyline.points.end(), path->polyline.points.begin() + 1, path->polyline.points.begin() + idx + 1); path->polyline.points.erase(path->polyline.points.begin(), path->polyline.points.begin() + idx); } else { - // if we have multiple paths we assume they have different types, so no need to - // check for continuity as we do for the single path case above - // new paths list starts with the second half of current path ExtrusionPaths new_paths; { @@ -247,10 +253,10 @@ ExtrusionLoop::split_at_vertex(const Point &point) } // then we add all paths until the end of current path list - new_paths.insert(new_paths.end(), this->paths.begin(), path); // not including this path + new_paths.insert(new_paths.end(), path+1, this->paths.end()); // not including this path // then we add all paths since the beginning of current list up to the previous one - new_paths.insert(new_paths.end(), path+1, this->paths.end()); // not including this path + new_paths.insert(new_paths.end(), this->paths.begin(), path); // not including this path // finally we add the first half of current path { @@ -332,6 +338,31 @@ ExtrusionLoop::has_overhang_point(const Point &point) const return false; } +bool +ExtrusionLoop::is_perimeter() const +{ + return this->paths.front().role == erPerimeter + || this->paths.front().role == erExternalPerimeter + || this->paths.front().role == erOverhangPerimeter; +} + +bool +ExtrusionLoop::is_infill() const +{ + return this->paths.front().role == erBridgeInfill + || this->paths.front().role == erInternalInfill + || this->paths.front().role == erSolidInfill + || this->paths.front().role == erTopSolidInfill; +} + +bool +ExtrusionLoop::is_solid_infill() const +{ + return this->paths.front().role == erBridgeInfill + || this->paths.front().role == erSolidInfill + || this->paths.front().role == erTopSolidInfill; +} + #ifdef SLIC3RXS REGISTER_CLASS(ExtrusionLoop, "ExtrusionLoop"); #endif diff --git a/xs/src/libslic3r/ExtrusionEntity.hpp b/xs/src/libslic3r/ExtrusionEntity.hpp index aad36cb3d..65a1aba59 100644 --- a/xs/src/libslic3r/ExtrusionEntity.hpp +++ b/xs/src/libslic3r/ExtrusionEntity.hpp @@ -64,7 +64,8 @@ class ExtrusionPath : public ExtrusionEntity void simplify(double tolerance); double length() const; bool is_perimeter() const; - bool is_fill() const; + bool is_infill() const; + bool is_solid_infill() const; bool is_bridge() const; std::string gcode(Extruder* extruder, double e, double F, double xofs, double yofs, std::string extrusion_axis, @@ -96,6 +97,9 @@ class ExtrusionLoop : public ExtrusionEntity void split_at(const Point &point); void clip_end(double distance, ExtrusionPaths* paths) const; bool has_overhang_point(const Point &point) const; + bool is_perimeter() const; + bool is_infill() const; + bool is_solid_infill() const; }; } diff --git a/xs/src/libslic3r/Flow.cpp b/xs/src/libslic3r/Flow.cpp index 7f87f146e..b9af18184 100644 --- a/xs/src/libslic3r/Flow.cpp +++ b/xs/src/libslic3r/Flow.cpp @@ -13,7 +13,7 @@ Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent &wid float w; if (bridge_flow_ratio > 0) { // if bridge flow was requested, calculate bridge width - w = Flow::_bridge_width(nozzle_diameter, bridge_flow_ratio); + height = w = Flow::_bridge_width(nozzle_diameter, bridge_flow_ratio); } else if (!width.percent && width.value == 0) { // if user left option to 0, calculate a sane default width w = Flow::_auto_width(role, nozzle_diameter, height); diff --git a/xs/src/libslic3r/GCodeWriter.cpp b/xs/src/libslic3r/GCodeWriter.cpp index c36ca4f45..bb8035ac3 100644 --- a/xs/src/libslic3r/GCodeWriter.cpp +++ b/xs/src/libslic3r/GCodeWriter.cpp @@ -491,6 +491,12 @@ GCodeWriter::unlift() return gcode; } +Pointf3 +GCodeWriter::get_position() const +{ + return this->_pos; +} + #ifdef SLIC3RXS REGISTER_CLASS(GCodeWriter, "GCode::Writer"); #endif diff --git a/xs/src/libslic3r/GCodeWriter.hpp b/xs/src/libslic3r/GCodeWriter.hpp index 5500a19f5..03de197d1 100644 --- a/xs/src/libslic3r/GCodeWriter.hpp +++ b/xs/src/libslic3r/GCodeWriter.hpp @@ -45,6 +45,7 @@ class GCodeWriter { std::string unretract(); std::string lift(); std::string unlift(); + Pointf3 get_position() const; private: std::string _extrusion_axis; diff --git a/xs/src/libslic3r/Layer.cpp b/xs/src/libslic3r/Layer.cpp index 17747d06d..47bef8101 100644 --- a/xs/src/libslic3r/Layer.cpp +++ b/xs/src/libslic3r/Layer.cpp @@ -120,6 +120,29 @@ Layer::make_slices() } } +template +bool +Layer::any_internal_region_slice_contains(const T &item) const +{ + FOREACH_LAYERREGION(this, layerm) { + if ((*layerm)->slices.any_internal_contains(item)) return true; + } + return false; +} +template bool Layer::any_internal_region_slice_contains(const Line &item) const; + +template +bool +Layer::any_internal_region_fill_surface_contains(const T &item) const +{ + FOREACH_LAYERREGION(this, layerm) { + if ((*layerm)->fill_surfaces.any_internal_contains(item)) return true; + } + return false; +} +template bool Layer::any_internal_region_fill_surface_contains(const Line &item) const; +template bool Layer::any_internal_region_fill_surface_contains(const Polyline &item) const; + #ifdef SLIC3RXS REGISTER_CLASS(Layer, "Layer"); diff --git a/xs/src/libslic3r/Layer.hpp b/xs/src/libslic3r/Layer.hpp index cd0250089..6a4e905c3 100644 --- a/xs/src/libslic3r/Layer.hpp +++ b/xs/src/libslic3r/Layer.hpp @@ -40,15 +40,18 @@ class LayerRegion // collection of expolygons representing the bridged areas (thus not // needing support material) + // (this could be just a Polygons object) ExPolygonCollection bridged; // collection of polylines representing the unsupported bridge edges PolylineCollection unsupported_bridge_edges; // ordered collection of extrusion paths/loops to build all perimeters + // (this collection contains both ExtrusionPath and ExtrusionLoop objects) ExtrusionEntityCollection perimeters; // ordered collection of extrusion paths to fill surfaces + // (this collection contains only ExtrusionEntityCollection objects) ExtrusionEntityCollection fills; Flow flow(FlowRole role, bool bridge = false, double width = -1) const; @@ -90,6 +93,8 @@ class Layer { LayerRegion* add_region(PrintRegion* print_region); void make_slices(); + template bool any_internal_region_slice_contains(const T &item) const; + template bool any_internal_region_fill_surface_contains(const T &item) const; protected: int _id; // sequential number of layer, 0-based diff --git a/xs/src/libslic3r/Line.cpp b/xs/src/libslic3r/Line.cpp index d1b0310a0..ef8598ada 100644 --- a/xs/src/libslic3r/Line.cpp +++ b/xs/src/libslic3r/Line.cpp @@ -133,6 +133,12 @@ Line::vector() const return Vector(this->b.x - this->a.x, this->b.y - this->a.y); } +Vector +Line::normal() const +{ + return Vector((this->b.y - this->a.y), -(this->b.x - this->a.x)); +} + #ifdef SLIC3RXS REGISTER_CLASS(Line, "Line"); @@ -178,4 +184,18 @@ Line::to_SV_pureperl() const { } #endif +Pointf3 +Linef3::intersect_plane(double z) const +{ + return Pointf3( + this->a.x + (this->b.x - this->a.x) * (z - this->a.z) / (this->b.z - this->a.z), + this->a.y + (this->b.y - this->a.y) * (z - this->a.z) / (this->b.z - this->a.z), + z + ); +} + +#ifdef SLIC3RXS +REGISTER_CLASS(Linef3, "Linef3"); +#endif + } diff --git a/xs/src/libslic3r/Line.hpp b/xs/src/libslic3r/Line.hpp index 3f86ed4a7..52f3ef77f 100644 --- a/xs/src/libslic3r/Line.hpp +++ b/xs/src/libslic3r/Line.hpp @@ -7,6 +7,7 @@ namespace Slic3r { class Line; +class Linef3; class Polyline; class Line @@ -34,6 +35,7 @@ class Line double orientation() const; double direction() const; Vector vector() const; + Vector normal() const; #ifdef SLIC3RXS void from_SV(SV* line_sv); @@ -45,6 +47,22 @@ class Line typedef std::vector Lines; +class Linef3 +{ + public: + Pointf3 a; + Pointf3 b; + Linef3() {}; + explicit Linef3(Pointf3 _a, Pointf3 _b): a(_a), b(_b) {}; + Pointf3 intersect_plane(double z) const; + + #ifdef SLIC3RXS + void from_SV(SV* line_sv); + void from_SV_check(SV* line_sv); + SV* to_SV_pureperl() const; + #endif +}; + } // start Boost diff --git a/xs/src/libslic3r/Model.cpp b/xs/src/libslic3r/Model.cpp index d5e9aa6ee..06919cedd 100644 --- a/xs/src/libslic3r/Model.cpp +++ b/xs/src/libslic3r/Model.cpp @@ -480,26 +480,31 @@ ModelObject::center_around_origin() mesh.bounding_box(&bb); } - // first align to origin on XY - double shift_x = -bb.min.x; - double shift_y = -bb.min.y; + // first align to origin on XYZ + Vectorf3 vector(-bb.min.x, -bb.min.y, -bb.min.z); // then center it on XY Sizef3 size = bb.size(); - shift_x -= size.x/2; - shift_y -= size.y/2; + vector.x -= size.x/2; + vector.y -= size.y/2; - this->translate(shift_x, shift_y, 0); - this->origin_translation.translate(shift_x, shift_y); + this->translate(vector); + this->origin_translation.translate(vector); if (!this->instances.empty()) { for (ModelInstancePtrs::const_iterator i = this->instances.begin(); i != this->instances.end(); ++i) { - (*i)->offset.translate(-shift_x, -shift_y); + (*i)->offset.translate(-vector.x, -vector.y); } this->update_bounding_box(); } } +void +ModelObject::translate(const Vectorf3 &vector) +{ + this->translate(vector.x, vector.y, vector.z); +} + void ModelObject::translate(coordf_t x, coordf_t y, coordf_t z) { diff --git a/xs/src/libslic3r/Model.hpp b/xs/src/libslic3r/Model.hpp index 9cbe6be40..62bb38bfc 100644 --- a/xs/src/libslic3r/Model.hpp +++ b/xs/src/libslic3r/Model.hpp @@ -99,7 +99,7 @@ class ModelObject center_around_origin() method. Callers might want to apply the same translation to new volumes before adding them to this object in order to preset alignment when user expects that. */ - Pointf origin_translation; + Pointf3 origin_translation; // these should be private but we need to expose them via XS until all methods are ported BoundingBoxf3 _bounding_box; @@ -126,6 +126,7 @@ class ModelObject void raw_bounding_box(BoundingBoxf3* bb) const; void instance_bounding_box(size_t instance_idx, BoundingBoxf3* bb) const; void center_around_origin(); + void translate(const Vectorf3 &vector); void translate(coordf_t x, coordf_t y, coordf_t z); void scale(const Pointf3 &versor); size_t materials_count() const; diff --git a/xs/src/libslic3r/MultiPoint.hpp b/xs/src/libslic3r/MultiPoint.hpp index ba3c72e50..eaca9b8eb 100644 --- a/xs/src/libslic3r/MultiPoint.hpp +++ b/xs/src/libslic3r/MultiPoint.hpp @@ -17,6 +17,8 @@ class MultiPoint Points points; operator Points() const; + MultiPoint() {}; + explicit MultiPoint(const Points &_points): points(_points) {}; void scale(double factor); void translate(double x, double y); void translate(const Point &vector); diff --git a/xs/src/libslic3r/Point.cpp b/xs/src/libslic3r/Point.cpp index 9debe9e8e..77f4224d5 100644 --- a/xs/src/libslic3r/Point.cpp +++ b/xs/src/libslic3r/Point.cpp @@ -41,7 +41,7 @@ Point::translate(double x, double y) } void -Point::translate(const Point &vector) +Point::translate(const Vector &vector) { this->translate(vector.x, vector.y); } @@ -160,6 +160,18 @@ Point::ccw(const Line &line) const return this->ccw(line.a, line.b); } +// returns the CCW angle between this-p1 and this-p2 +// i.e. this assumes a CCW rotation from p1 to p2 around this +double +Point::ccw_angle(const Point &p1, const Point &p2) const +{ + double angle = atan2(p1.x - this->x, p1.y - this->y) + - atan2(p2.x - this->x, p2.y - this->y); + + // we only want to return only positive angles + return angle <= 0 ? angle + 2*PI : angle; +} + Point Point::projection_onto(const MultiPoint &poly) const { @@ -211,6 +223,12 @@ Point::negative() const return Point(-this->x, -this->y); } +Vector +Point::vector_to(const Point &point) const +{ + return Vector(point.x - this->x, point.y - this->y); +} + Point operator+(const Point& point1, const Point& point2) { @@ -286,6 +304,18 @@ Pointf::rotate(double angle, const Pointf ¢er) this->y = center.y + cos(angle) * (cur_y - center.y) + sin(angle) * (cur_x - center.x); } +Pointf +Pointf::negative() const +{ + return Pointf(-this->x, -this->y); +} + +Vectorf +Pointf::vector_to(const Pointf &point) const +{ + return Vectorf(point.x - this->x, point.y - this->y); +} + #ifdef SLIC3RXS REGISTER_CLASS(Pointf, "Pointf"); @@ -333,6 +363,12 @@ Pointf3::scale(double factor) this->z *= factor; } +void +Pointf3::translate(const Vectorf3 &vector) +{ + this->translate(vector.x, vector.y, vector.z); +} + void Pointf3::translate(double x, double y, double z) { @@ -340,6 +376,27 @@ Pointf3::translate(double x, double y, double z) this->z += z; } +double +Pointf3::distance_to(const Pointf3 &point) const +{ + double dx = ((double)point.x - this->x); + double dy = ((double)point.y - this->y); + double dz = ((double)point.z - this->z); + return sqrt(dx*dx + dy*dy + dz*dz); +} + +Pointf3 +Pointf3::negative() const +{ + return Pointf3(-this->x, -this->y, -this->z); +} + +Vectorf3 +Pointf3::vector_to(const Pointf3 &point) const +{ + return Vectorf3(point.x - this->x, point.y - this->y, point.z - this->z); +} + #ifdef SLIC3RXS REGISTER_CLASS(Pointf3, "Pointf3"); #endif diff --git a/xs/src/libslic3r/Point.hpp b/xs/src/libslic3r/Point.hpp index 514eb51cd..368fb1af8 100644 --- a/xs/src/libslic3r/Point.hpp +++ b/xs/src/libslic3r/Point.hpp @@ -14,6 +14,8 @@ class Point; class Pointf; class Pointf3; typedef Point Vector; +typedef Pointf Vectorf; +typedef Pointf3 Vectorf3; typedef std::vector Points; typedef std::vector PointPtrs; typedef std::vector PointConstPtrs; @@ -36,7 +38,7 @@ class Point std::string wkt() const; void scale(double factor); void translate(double x, double y); - void translate(const Point &vector); + void translate(const Vector &vector); void rotate(double angle, const Point ¢er); bool coincides_with(const Point &point) const; bool coincides_with_epsilon(const Point &point) const; @@ -48,9 +50,11 @@ class Point double distance_to(const Line &line) const; double ccw(const Point &p1, const Point &p2) const; double ccw(const Line &line) const; + double ccw_angle(const Point &p1, const Point &p2) const; Point projection_onto(const MultiPoint &poly) const; Point projection_onto(const Line &line) const; Point negative() const; + Vector vector_to(const Point &point) const; #ifdef SLIC3RXS void from_SV(SV* point_sv); @@ -78,6 +82,8 @@ class Pointf void scale(double factor); void translate(double x, double y); void rotate(double angle, const Pointf ¢er); + Pointf negative() const; + Vectorf vector_to(const Pointf &point) const; #ifdef SLIC3RXS bool from_SV(SV* point_sv); @@ -92,7 +98,11 @@ class Pointf3 : public Pointf coordf_t z; explicit Pointf3(coordf_t _x = 0, coordf_t _y = 0, coordf_t _z = 0): Pointf(_x, _y), z(_z) {}; void scale(double factor); + void translate(const Vectorf3 &vector); void translate(double x, double y, double z); + double distance_to(const Pointf3 &point) const; + Pointf3 negative() const; + Vectorf3 vector_to(const Pointf3 &point) const; }; } diff --git a/xs/src/libslic3r/Polygon.cpp b/xs/src/libslic3r/Polygon.cpp index 7fc6f0091..4c73de092 100644 --- a/xs/src/libslic3r/Polygon.cpp +++ b/xs/src/libslic3r/Polygon.cpp @@ -158,8 +158,12 @@ Polygon::contains(const Point &point) const Polygons Polygon::simplify(double tolerance) const { - Polygon p = *this; - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + // repeat first point at the end in order to apply Douglas-Peucker + // on the whole polygon + Points points = this->points; + points.push_back(points.front()); + Polygon p(MultiPoint::_douglas_peucker(points, tolerance)); + p.points.pop_back(); Polygons pp; pp.push_back(p); @@ -222,6 +226,58 @@ Polygon::wkt() const return wkt.str(); } +// find all concave vertices (i.e. having an internal angle greater than the supplied angle) */ +void +Polygon::concave_points(double angle, Points* points) const +{ + angle = 2*PI - angle; + + // check whether first point forms a concave angle + if (this->points.front().ccw_angle(this->points.back(), *(this->points.begin()+1)) <= angle) + points->push_back(this->points.front()); + + // check whether points 1..(n-1) form concave angles + for (Points::const_iterator p = this->points.begin()+1; p != this->points.end()-1; ++p) { + if (p->ccw_angle(*(p-1), *(p+1)) <= angle) points->push_back(*p); + } + + // check whether last point forms a concave angle + if (this->points.back().ccw_angle(*(this->points.end()-2), this->points.front()) <= angle) + points->push_back(this->points.back()); +} + +void +Polygon::concave_points(Points* points) const +{ + this->concave_points(PI, points); +} + +// find all convex vertices (i.e. having an internal angle smaller than the supplied angle) */ +void +Polygon::convex_points(double angle, Points* points) const +{ + angle = 2*PI - angle; + + // check whether first point forms a convex angle + if (this->points.front().ccw_angle(this->points.back(), *(this->points.begin()+1)) >= angle) + points->push_back(this->points.front()); + + // check whether points 1..(n-1) form convex angles + for (Points::const_iterator p = this->points.begin()+1; p != this->points.end()-1; ++p) { + if (p->ccw_angle(*(p-1), *(p+1)) >= angle) points->push_back(*p); + } + + // check whether last point forms a convex angle + if (this->points.back().ccw_angle(*(this->points.end()-2), this->points.front()) >= angle) + points->push_back(this->points.back()); +} + +void +Polygon::convex_points(Points* points) const +{ + this->convex_points(PI, points); +} + #ifdef SLIC3RXS REGISTER_CLASS(Polygon, "Polygon"); diff --git a/xs/src/libslic3r/Polygon.hpp b/xs/src/libslic3r/Polygon.hpp index 5220220f3..778825f3e 100644 --- a/xs/src/libslic3r/Polygon.hpp +++ b/xs/src/libslic3r/Polygon.hpp @@ -19,6 +19,9 @@ class Polygon : public MultiPoint { operator Polyline() const; Point& operator[](Points::size_type idx); const Point& operator[](Points::size_type idx) const; + + Polygon() {}; + explicit Polygon(const Points &points): MultiPoint(points) {}; Point last_point() const; Lines lines() const; void lines(Lines* lines) const; @@ -38,6 +41,10 @@ class Polygon : public MultiPoint { void triangulate_convex(Polygons* polygons) const; Point centroid() const; std::string wkt() const; + void concave_points(double angle, Points* points) const; + void concave_points(Points* points) const; + void convex_points(double angle, Points* points) const; + void convex_points(Points* points) const; #ifdef SLIC3RXS void from_SV_check(SV* poly_sv); diff --git a/xs/src/libslic3r/Print.cpp b/xs/src/libslic3r/Print.cpp index d0175ebac..b40430fec 100644 --- a/xs/src/libslic3r/Print.cpp +++ b/xs/src/libslic3r/Print.cpp @@ -1,7 +1,9 @@ #include "Print.hpp" #include "BoundingBox.hpp" #include "ClipperUtils.hpp" +#include "Flow.hpp" #include "Geometry.hpp" +#include "SupportMaterial.hpp" #include namespace Slic3r { @@ -74,46 +76,20 @@ Print::get_object(size_t idx) return objects.at(idx); } -PrintObject* -Print::add_object(ModelObject *model_object, const BoundingBoxf3 &modobj_bbox) -{ - PrintObject *object = new PrintObject(this, model_object, modobj_bbox); - objects.push_back(object); - - // invalidate steps - this->invalidate_step(psSkirt); - this->invalidate_step(psBrim); - - return object; -} - -PrintObject* -Print::set_new_object(size_t idx, ModelObject *model_object, const BoundingBoxf3 &modobj_bbox) -{ - if (idx >= this->objects.size()) throw "bad idx"; - - PrintObjectPtrs::iterator old_it = this->objects.begin() + idx; - // before deleting object, invalidate all of its steps in order to - // invalidate all of the dependent ones in Print - (*old_it)->invalidate_all_steps(); - delete *old_it; - - PrintObject *object = new PrintObject(this, model_object, modobj_bbox); - this->objects[idx] = object; - return object; -} - void Print::delete_object(size_t idx) { PrintObjectPtrs::iterator i = this->objects.begin() + idx; + + // before deleting object, invalidate all of its steps in order to + // invalidate all of the dependent ones in Print + (*i)->invalidate_all_steps(); + + // destroy object and remove it from our container delete *i; this->objects.erase(i); // TODO: purge unused regions - - this->state.invalidate(psSkirt); - this->state.invalidate(psBrim); } void @@ -173,6 +149,7 @@ bool Print::invalidate_state_by_config_options(const std::vector &opt_keys) { std::set steps; + std::set osteps; // this method only accepts PrintConfig option keys for (std::vector::const_iterator opt_key = opt_keys.begin(); opt_key != opt_keys.end(); ++opt_key) { @@ -223,6 +200,7 @@ Print::invalidate_state_by_config_options(const std::vector || *opt_key == "output_filename_format" || *opt_key == "perimeter_acceleration" || *opt_key == "post_process" + || *opt_key == "pressure_advance" || *opt_key == "retract_before_travel" || *opt_key == "retract_layer_change" || *opt_key == "retract_length" @@ -245,6 +223,12 @@ Print::invalidate_state_by_config_options(const std::vector || *opt_key == "wipe" || *opt_key == "z_offset") { // these options only affect G-code export, so nothing to invalidate + } else if (*opt_key == "first_layer_extrusion_width") { + osteps.insert(posPerimeters); + osteps.insert(posInfill); + osteps.insert(posSupportMaterial); + steps.insert(psSkirt); + steps.insert(psBrim); } else { // for legacy, if we can't handle this option let's invalidate all steps return this->invalidate_all_steps(); @@ -255,6 +239,11 @@ Print::invalidate_state_by_config_options(const std::vector for (std::set::const_iterator step = steps.begin(); step != steps.end(); ++step) { if (this->invalidate_step(*step)) invalidated = true; } + for (std::set::const_iterator ostep = osteps.begin(); ostep != osteps.end(); ++ostep) { + FOREACH_OBJECT(this, object) { + if ((*object)->invalidate_step(*ostep)) invalidated = true; + } + } return invalidated; } @@ -290,6 +279,19 @@ Print::invalidate_all_steps() return invalidated; } +// returns true if an object step is done on all objects +// and there's at least one object +bool +Print::step_done(PrintObjectStep step) const +{ + if (this->objects.empty()) return false; + FOREACH_OBJECT(this, object) { + if (!(*object)->state.is_done(step)) + return false; + } + return true; +} + // returns 0-based indices of used extruders std::set Print::extruders() const @@ -299,6 +301,7 @@ Print::extruders() const FOREACH_REGION(this, region) { extruders.insert((*region)->config.perimeter_extruder - 1); extruders.insert((*region)->config.infill_extruder - 1); + extruders.insert((*region)->config.solid_infill_extruder - 1); } FOREACH_OBJECT(this, object) { extruders.insert((*object)->config.support_material_extruder - 1); @@ -347,9 +350,23 @@ Print::add_model_object(ModelObject* model_object, int idx) { BoundingBoxf3 bb; model_object->raw_bounding_box(&bb); - o = (idx != -1) - ? this->set_new_object(idx, model_object, bb) - : this->add_object(model_object, bb); + if (idx != -1) { + // replacing existing object + PrintObjectPtrs::iterator old_it = this->objects.begin() + idx; + // before deleting object, invalidate all of its steps in order to + // invalidate all of the dependent ones in Print + (*old_it)->invalidate_all_steps(); + delete *old_it; + + this->objects[idx] = o = new PrintObject(this, model_object, bb); + } else { + o = new PrintObject(this, model_object, bb); + objects.push_back(o); + + // invalidate steps + this->invalidate_step(psSkirt); + this->invalidate_step(psBrim); + } } for (ModelVolumePtrs::const_iterator v_i = model_object->volumes.begin(); v_i != model_object->volumes.end(); ++v_i) { @@ -446,7 +463,7 @@ Print::apply_config(DynamicPrintConfig config) std::vector ®ion_volumes = object->region_volumes[region_id]; for (std::vector::const_iterator volume_id = region_volumes.begin(); volume_id != region_volumes.end(); ++volume_id) { - ModelVolume* volume = object->model_object()->volumes[*volume_id]; + ModelVolume* volume = object->model_object()->volumes.at(*volume_id); PrintRegionConfig new_config = this->_region_config_from_model_volume(*volume); @@ -615,6 +632,112 @@ Print::validate() const } } +// the bounding box of objects placed in copies position +// (without taking skirt/brim/support material into account) +BoundingBox +Print::bounding_box() const +{ + BoundingBox bb; + FOREACH_OBJECT(this, object) { + for (Points::const_iterator copy = (*object)->_shifted_copies.begin(); copy != (*object)->_shifted_copies.end(); ++copy) { + bb.merge(*copy); + + Point p = *copy; + p.translate((*object)->size); + bb.merge(p); + } + } + return bb; +} + +// the total bounding box of extrusions, including skirt/brim/support material +// this methods needs to be called even when no steps were processed, so it should +// only use configuration values +BoundingBox +Print::total_bounding_box() const +{ + // get objects bounding box + BoundingBox bb = this->bounding_box(); + + // we need to offset the objects bounding box by at least half the perimeters extrusion width + Flow perimeter_flow = this->objects.front()->get_layer(0)->get_region(0)->flow(frPerimeter); + double extra = perimeter_flow.width/2; + + // consider support material + if (this->has_support_material()) { + extra = std::max(extra, SUPPORT_MATERIAL_MARGIN); + } + + // consider brim and skirt + if (this->config.brim_width.value > 0) { + Flow brim_flow = this->brim_flow(); + extra = std::max(extra, this->config.brim_width.value + brim_flow.width/2); + } + if (this->config.skirts.value > 0) { + Flow skirt_flow = this->skirt_flow(); + extra = std::max( + extra, + this->config.brim_width.value + + this->config.skirt_distance.value + + this->config.skirts.value * skirt_flow.spacing() + + skirt_flow.width/2 + ); + } + + if (extra > 0) + bb.offset(scale_(extra)); + + return bb; +} + +double +Print::skirt_first_layer_height() const +{ + if (this->objects.empty()) CONFESS("skirt_first_layer_height() can't be called without PrintObjects"); + return this->objects.front()->config.get_abs_value("first_layer_height"); +} + +Flow +Print::brim_flow() const +{ + ConfigOptionFloatOrPercent width = this->config.first_layer_extrusion_width; + if (width.value == 0) width = this->regions.front()->config.perimeter_extrusion_width; + + /* We currently use a random region's perimeter extruder. + While this works for most cases, we should probably consider all of the perimeter + extruders and take the one with, say, the smallest index. + The same logic should be applied to the code that selects the extruder during G-code + generation as well. */ + return Flow::new_from_config_width( + frPerimeter, + width, + this->config.nozzle_diameter.get_at(this->regions.front()->config.perimeter_extruder-1), + this->skirt_first_layer_height(), + 0 + ); +} + +Flow +Print::skirt_flow() const +{ + ConfigOptionFloatOrPercent width = this->config.first_layer_extrusion_width; + if (width.value == 0) width = this->regions.front()->config.perimeter_extrusion_width; + + /* We currently use a random object's support material extruder. + While this works for most cases, we should probably consider all of the support material + extruders and take the one with, say, the smallest index; + The same logic should be applied to the code that selects the extruder during G-code + generation as well. */ + return Flow::new_from_config_width( + frPerimeter, + width, + this->config.nozzle_diameter.get_at(this->objects.front()->config.support_material_extruder-1), + this->skirt_first_layer_height(), + 0 + ); +} + + PrintRegionConfig Print::_region_config_from_model_volume(const ModelVolume &volume) { diff --git a/xs/src/libslic3r/Print.hpp b/xs/src/libslic3r/Print.hpp index 0c4d7c24f..0e7334eaf 100644 --- a/xs/src/libslic3r/Print.hpp +++ b/xs/src/libslic3r/Print.hpp @@ -5,6 +5,7 @@ #include #include #include +#include "BoundingBox.hpp" #include "Flow.hpp" #include "PrintConfig.hpp" #include "Point.hpp" @@ -75,8 +76,10 @@ class PrintObject friend class Print; public: - // vector of (vectors of volume ids), indexed by region_id - std::vector > region_volumes; + // map of (vectors of volume ids), indexed by region_id + /* (we use map instead of vector so that we don't have to worry about + resizing it and the [] operator adds new items automagically) */ + std::map< size_t,std::vector > region_volumes; PrintObjectConfig config; t_layer_height_ranges layer_height_ranges; @@ -108,17 +111,19 @@ class PrintObject bool delete_all_copies(); bool set_copies(const Points &points); bool reload_model_instances(); + void bounding_box(BoundingBox* bb) const; // adds region_id, too, if necessary void add_region_volume(int region_id, int volume_id); - size_t layer_count(); + size_t total_layer_count() const; + size_t layer_count() const; void clear_layers(); Layer* get_layer(int idx); Layer* add_layer(int id, coordf_t height, coordf_t print_z, coordf_t slice_z); void delete_layer(int idx); - size_t support_layer_count(); + size_t support_layer_count() const; void clear_support_layers(); SupportLayer* get_support_layer(int idx); SupportLayer* add_support_layer(int id, coordf_t height, coordf_t print_z, coordf_t slice_z); @@ -129,6 +134,8 @@ class PrintObject bool invalidate_step(PrintObjectStep step); bool invalidate_all_steps(); + void bridge_over_infill(); + private: Print* _print; ModelObject* _model_object; @@ -165,8 +172,6 @@ class Print // methods for handling objects void clear_objects(); PrintObject* get_object(size_t idx); - PrintObject* add_object(ModelObject *model_object, const BoundingBoxf3 &modobj_bbox); - PrintObject* set_new_object(size_t idx, ModelObject *model_object, const BoundingBoxf3 &modobj_bbox); void delete_object(size_t idx); void reload_object(size_t idx); @@ -178,11 +183,17 @@ class Print bool invalidate_state_by_config_options(const std::vector &opt_keys); bool invalidate_step(PrintStep step); bool invalidate_all_steps(); + bool step_done(PrintObjectStep step) const; void add_model_object(ModelObject* model_object, int idx = -1); bool apply_config(DynamicPrintConfig config); void init_extruders(); void validate() const; + BoundingBox bounding_box() const; + BoundingBox total_bounding_box() const; + double skirt_first_layer_height() const; + Flow brim_flow() const; + Flow skirt_flow() const; std::set extruders() const; void _simplify_slices(double distance); diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index 0ced2c832..4565ba5af 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -115,6 +115,24 @@ PrintConfigDef::build_def() { Options["end_gcode"].full_width = true; Options["end_gcode"].height = 120; + Options["external_fill_pattern"].type = coEnum; + Options["external_fill_pattern"].label = "Top/bottom fill pattern"; + Options["external_fill_pattern"].category = "Infill"; + Options["external_fill_pattern"].tooltip = "Fill pattern for top/bottom infill. This only affects the external visible layer, and not its adjacent solid shells."; + Options["external_fill_pattern"].cli = "external-fill-pattern=s"; + Options["external_fill_pattern"].enum_keys_map = ConfigOptionEnum::get_enum_values(); + Options["external_fill_pattern"].enum_values.push_back("rectilinear"); + Options["external_fill_pattern"].enum_values.push_back("concentric"); + Options["external_fill_pattern"].enum_values.push_back("hilbertcurve"); + Options["external_fill_pattern"].enum_values.push_back("archimedeanchords"); + Options["external_fill_pattern"].enum_values.push_back("octagramspiral"); + Options["external_fill_pattern"].enum_labels.push_back("rectilinear"); + Options["external_fill_pattern"].enum_labels.push_back("concentric"); + Options["external_fill_pattern"].enum_labels.push_back("hilbertcurve (slow)"); + Options["external_fill_pattern"].enum_labels.push_back("archimedeanchords (slow)"); + Options["external_fill_pattern"].enum_labels.push_back("octagramspiral (slow)"); + Options["external_fill_pattern"].aliases.push_back("solid_fill_pattern"); + Options["external_perimeter_extrusion_width"].type = coFloatOrPercent; Options["external_perimeter_extrusion_width"].label = "External perimeters"; Options["external_perimeter_extrusion_width"].category = "Extrusion Width"; @@ -280,9 +298,9 @@ PrintConfigDef::build_def() { Options["fill_pattern"].enum_labels.push_back("concentric"); Options["fill_pattern"].enum_labels.push_back("honeycomb"); Options["fill_pattern"].enum_labels.push_back("3D honeycomb"); - Options["fill_pattern"].enum_labels.push_back("hilbertcurve (slow)"); - Options["fill_pattern"].enum_labels.push_back("archimedeanchords (slow)"); - Options["fill_pattern"].enum_labels.push_back("octagramspiral (slow)"); + Options["fill_pattern"].enum_labels.push_back("hilbertcurve"); + Options["fill_pattern"].enum_labels.push_back("archimedeanchords"); + Options["fill_pattern"].enum_labels.push_back("octagramspiral"); Options["first_layer_acceleration"].type = coFloat; Options["first_layer_acceleration"].label = "First layer"; @@ -511,7 +529,7 @@ PrintConfigDef::build_def() { Options["perimeter_extruder"].type = coInt; Options["perimeter_extruder"].label = "Perimeter extruder"; Options["perimeter_extruder"].category = "Extruders"; - Options["perimeter_extruder"].tooltip = "The extruder to use when printing perimeters. First extruder is 1."; + Options["perimeter_extruder"].tooltip = "The extruder to use when printing perimeters and brim. First extruder is 1."; Options["perimeter_extruder"].cli = "perimeter-extruder=i"; Options["perimeter_extruder"].aliases.push_back("perimeters_extruder"); Options["perimeter_extruder"].min = 1; @@ -534,9 +552,10 @@ PrintConfigDef::build_def() { Options["perimeter_speed"].min = 0; Options["perimeters"].type = coInt; - Options["perimeters"].label = "Perimeters (minimum)"; + Options["perimeters"].label = "Perimeters"; Options["perimeters"].category = "Layers and Perimeters"; Options["perimeters"].tooltip = "This option sets the number of perimeters to generate for each layer. Note that Slic3r may increase this number automatically when it detects sloping surfaces which benefit from a higher number of perimeters if the Extra Perimeters option is enabled."; + Options["perimeters"].sidetext = "(minimum)"; Options["perimeters"].cli = "perimeters=i"; Options["perimeters"].aliases.push_back("perimeter_offsets"); Options["perimeters"].min = 0; @@ -646,8 +665,8 @@ PrintConfigDef::build_def() { Options["skirt_height"].cli = "skirt-height=i"; Options["skirts"].type = coInt; - Options["skirts"].label = "Loops"; - Options["skirts"].tooltip = "Number of loops for this skirt, in other words its thickness. Set this to zero to disable skirt."; + Options["skirts"].label = "Loops (minimum)"; + Options["skirts"].tooltip = "Number of loops for the skirt. If the Minimum Extrusion Length option is set, the number of loops might be greater than the one configured here. Set this to zero to disable skirt completely."; Options["skirts"].cli = "skirts=i"; Options["skirts"].min = 0; @@ -668,23 +687,6 @@ PrintConfigDef::build_def() { Options["small_perimeter_speed"].cli = "small-perimeter-speed=s"; Options["small_perimeter_speed"].ratio_over = "perimeter_speed"; - Options["solid_fill_pattern"].type = coEnum; - Options["solid_fill_pattern"].label = "Top/bottom fill pattern"; - Options["solid_fill_pattern"].category = "Infill"; - Options["solid_fill_pattern"].tooltip = "Fill pattern for top/bottom infill."; - Options["solid_fill_pattern"].cli = "solid-fill-pattern=s"; - Options["solid_fill_pattern"].enum_keys_map = ConfigOptionEnum::get_enum_values(); - Options["solid_fill_pattern"].enum_values.push_back("rectilinear"); - Options["solid_fill_pattern"].enum_values.push_back("concentric"); - Options["solid_fill_pattern"].enum_values.push_back("hilbertcurve"); - Options["solid_fill_pattern"].enum_values.push_back("archimedeanchords"); - Options["solid_fill_pattern"].enum_values.push_back("octagramspiral"); - Options["solid_fill_pattern"].enum_labels.push_back("rectilinear"); - Options["solid_fill_pattern"].enum_labels.push_back("concentric"); - Options["solid_fill_pattern"].enum_labels.push_back("hilbertcurve (slow)"); - Options["solid_fill_pattern"].enum_labels.push_back("archimedeanchords (slow)"); - Options["solid_fill_pattern"].enum_labels.push_back("octagramspiral (slow)"); - Options["solid_infill_below_area"].type = coFloat; Options["solid_infill_below_area"].label = "Solid infill threshold area"; Options["solid_infill_below_area"].category = "Infill"; @@ -693,6 +695,13 @@ PrintConfigDef::build_def() { Options["solid_infill_below_area"].cli = "solid-infill-below-area=f"; Options["solid_infill_below_area"].min = 0; + Options["solid_infill_extruder"].type = coInt; + Options["solid_infill_extruder"].label = "Solid infill extruder"; + Options["solid_infill_extruder"].category = "Extruders"; + Options["solid_infill_extruder"].tooltip = "The extruder to use when printing solid infill."; + Options["solid_infill_extruder"].cli = "solid-infill-extruder=i"; + Options["solid_infill_extruder"].min = 1; + Options["solid_infill_every_layers"].type = coInt; Options["solid_infill_every_layers"].label = "Solid infill every"; Options["solid_infill_every_layers"].category = "Infill"; @@ -771,9 +780,9 @@ PrintConfigDef::build_def() { Options["support_material_enforce_layers"].min = 0; Options["support_material_extruder"].type = coInt; - Options["support_material_extruder"].label = "Support material extruder"; + Options["support_material_extruder"].label = "Support material/raft/skirt extruder"; Options["support_material_extruder"].category = "Extruders"; - Options["support_material_extruder"].tooltip = "The extruder to use when printing support material. This affects brim and raft too."; + Options["support_material_extruder"].tooltip = "The extruder to use when printing support material, raft and skirt."; Options["support_material_extruder"].cli = "support-material-extruder=i"; Options["support_material_extruder"].min = 1; @@ -785,7 +794,7 @@ PrintConfigDef::build_def() { Options["support_material_extrusion_width"].cli = "support-material-extrusion-width=s"; Options["support_material_interface_extruder"].type = coInt; - Options["support_material_interface_extruder"].label = "Support material interface extruder"; + Options["support_material_interface_extruder"].label = "Support material/raft interface extruder"; Options["support_material_interface_extruder"].category = "Extruders"; Options["support_material_interface_extruder"].tooltip = "The extruder to use when printing support material interface. This affects raft too."; Options["support_material_interface_extruder"].cli = "support-material-interface-extruder=i"; @@ -871,8 +880,7 @@ PrintConfigDef::build_def() { Options["threads"].type = coInt; Options["threads"].label = "Threads"; - Options["threads"].tooltip = "Threads are used to parallelize long-running tasks. Optimal threads number is slightly above the number of available cores/processors. Beware that more threads consume more memory."; - Options["threads"].sidetext = "(more speed but more memory usage)"; + Options["threads"].tooltip = "Threads are used to parallelize long-running tasks. Optimal threads number is slightly above the number of available cores/processors."; Options["threads"].cli = "threads|j=i"; Options["threads"].readonly = true; Options["threads"].min = 1; diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index c1e6ac69f..48235c7d1 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -93,6 +93,10 @@ class DynamicPrintConfig : public DynamicConfig this->option("support_material_interface_extruder", true)->setInt(extruder); } } + + if (!this->has("solid_infill_extruder") && this->has("infill_extruder")) + this->option("solid_infill_extruder", true)->setInt(this->option("infill_extruder")->getInt()); + if (this->has("spiral_vase") && this->opt("spiral_vase", true)->value) { { // this should be actually done only on the spiral layers instead of all @@ -205,6 +209,7 @@ class PrintRegionConfig : public virtual StaticPrintConfig ConfigOptionInt bottom_solid_layers; ConfigOptionFloat bridge_flow_ratio; ConfigOptionFloat bridge_speed; + ConfigOptionEnum external_fill_pattern; ConfigOptionFloatOrPercent external_perimeter_extrusion_width; ConfigOptionFloatOrPercent external_perimeter_speed; ConfigOptionBool external_perimeters_first; @@ -223,8 +228,8 @@ class PrintRegionConfig : public virtual StaticPrintConfig ConfigOptionFloat perimeter_speed; ConfigOptionInt perimeters; ConfigOptionFloatOrPercent small_perimeter_speed; - ConfigOptionEnum solid_fill_pattern; ConfigOptionFloat solid_infill_below_area; + ConfigOptionInt solid_infill_extruder; ConfigOptionFloatOrPercent solid_infill_extrusion_width; ConfigOptionInt solid_infill_every_layers; ConfigOptionFloatOrPercent solid_infill_speed; @@ -237,6 +242,7 @@ class PrintRegionConfig : public virtual StaticPrintConfig this->bottom_solid_layers.value = 3; this->bridge_flow_ratio.value = 1; this->bridge_speed.value = 60; + this->external_fill_pattern.value = ipRectilinear; this->external_perimeter_extrusion_width.value = 0; this->external_perimeter_extrusion_width.percent = false; this->external_perimeter_speed.value = 70; @@ -258,9 +264,9 @@ class PrintRegionConfig : public virtual StaticPrintConfig this->perimeter_extrusion_width.percent = false; this->perimeter_speed.value = 30; this->perimeters.value = 3; + this->solid_infill_extruder.value = 1; this->small_perimeter_speed.value = 30; this->small_perimeter_speed.percent = false; - this->solid_fill_pattern.value = ipRectilinear; this->solid_infill_below_area.value = 70; this->solid_infill_extrusion_width.value = 0; this->solid_infill_extrusion_width.percent = false; @@ -279,6 +285,7 @@ class PrintRegionConfig : public virtual StaticPrintConfig if (opt_key == "bottom_solid_layers") return &this->bottom_solid_layers; if (opt_key == "bridge_flow_ratio") return &this->bridge_flow_ratio; if (opt_key == "bridge_speed") return &this->bridge_speed; + if (opt_key == "external_fill_pattern") return &this->external_fill_pattern; if (opt_key == "external_perimeter_extrusion_width") return &this->external_perimeter_extrusion_width; if (opt_key == "external_perimeter_speed") return &this->external_perimeter_speed; if (opt_key == "external_perimeters_first") return &this->external_perimeters_first; @@ -297,8 +304,8 @@ class PrintRegionConfig : public virtual StaticPrintConfig if (opt_key == "perimeter_speed") return &this->perimeter_speed; if (opt_key == "perimeters") return &this->perimeters; if (opt_key == "small_perimeter_speed") return &this->small_perimeter_speed; - if (opt_key == "solid_fill_pattern") return &this->solid_fill_pattern; if (opt_key == "solid_infill_below_area") return &this->solid_infill_below_area; + if (opt_key == "solid_infill_extruder") return &this->solid_infill_extruder; if (opt_key == "solid_infill_extrusion_width") return &this->solid_infill_extrusion_width; if (opt_key == "solid_infill_every_layers") return &this->solid_infill_every_layers; if (opt_key == "solid_infill_speed") return &this->solid_infill_speed; diff --git a/xs/src/libslic3r/PrintObject.cpp b/xs/src/libslic3r/PrintObject.cpp index 54ddea33d..1d33fbc00 100644 --- a/xs/src/libslic3r/PrintObject.cpp +++ b/xs/src/libslic3r/PrintObject.cpp @@ -1,5 +1,6 @@ #include "Print.hpp" #include "BoundingBox.hpp" +#include "ClipperUtils.hpp" #include "Geometry.hpp" namespace Slic3r { @@ -9,8 +10,6 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Bounding _model_object(model_object), typed_slices(false) { - region_volumes.resize(this->_print->regions.size()); - // Compute the translation to be applied to our meshes so that we work with smaller coordinates { // Translate meshes so that our toolpath generation algorithms work with smaller @@ -111,18 +110,33 @@ PrintObject::reload_model_instances() return this->set_copies(copies); } +void +PrintObject::bounding_box(BoundingBox* bb) const +{ + // since the object is aligned to origin, bounding box coincides with size + Points pp; + pp.push_back(Point(0,0)); + pp.push_back(this->size); + *bb = BoundingBox(pp); +} + void PrintObject::add_region_volume(int region_id, int volume_id) { - if (region_id >= region_volumes.size()) { - region_volumes.resize(region_id + 1); - } - region_volumes[region_id].push_back(volume_id); } +/* This is the *total* layer count (including support layers) + this value is not supposed to be compared with Layer::id + since they have different semantics */ size_t -PrintObject::layer_count() +PrintObject::total_layer_count() const +{ + return this->layer_count() + this->support_layer_count(); +} + +size_t +PrintObject::layer_count() const { return this->layers.size(); } @@ -157,7 +171,7 @@ PrintObject::delete_layer(int idx) } size_t -PrintObject::support_layer_count() +PrintObject::support_layer_count() const { return this->support_layers.size(); } @@ -203,6 +217,7 @@ PrintObject::invalidate_state_by_config_options(const std::vector_print, region) { + size_t region_id = region - this->_print->regions.begin(); + + double fill_density = (*region)->config.fill_density.value; + if (fill_density == 100 || fill_density == 0) continue; + + FOREACH_LAYER(this, layer_it) { + if (layer_it == this->layers.begin()) continue; + + Layer* layer = *layer_it; + Layer* lower_layer = *(layer_it - 1); + LayerRegion* layerm = layer->get_region(region_id); + + // compute the areas needing bridge math + Polygons internal_solid, lower_internal; + layerm->fill_surfaces.filter_by_type(stInternalSolid, &internal_solid); + FOREACH_LAYERREGION(lower_layer, lower_layerm_it) + (*lower_layerm_it)->fill_surfaces.filter_by_type(stInternal, &lower_internal); + + ExPolygons to_bridge; + intersection(internal_solid, lower_internal, &to_bridge); + if (to_bridge.empty()) continue; + + ExPolygons not_to_bridge; + diff(internal_solid, to_bridge, ¬_to_bridge, true); + + #ifdef SLIC3R_DEBUG + printf("Bridging %zu internal areas at layer %d\n", to_bridge.size(), layer->id()); + #endif + + // build the new collection of fill_surfaces + { + Surfaces new_surfaces; + for (Surfaces::const_iterator surface = layerm->fill_surfaces.surfaces.begin(); surface != layerm->fill_surfaces.surfaces.end(); ++surface) { + if (surface->surface_type != stInternalSolid) + new_surfaces.push_back(*surface); + } + + for (ExPolygons::const_iterator ex = to_bridge.begin(); ex != to_bridge.end(); ++ex) + new_surfaces.push_back(Surface(stInternalBridge, *ex)); + + for (ExPolygons::const_iterator ex = not_to_bridge.begin(); ex != not_to_bridge.end(); ++ex) + new_surfaces.push_back(Surface(stInternalSolid, *ex)); + + layerm->fill_surfaces.surfaces = new_surfaces; + } + + /* + # exclude infill from the layers below if needed + # see discussion at https://github.com/alexrj/Slic3r/issues/240 + # Update: do not exclude any infill. Sparse infill is able to absorb the excess material. + if (0) { + my $excess = $layerm->extruders->{infill}->bridge_flow->width - $layerm->height; + for (my $i = $layer_id-1; $excess >= $self->get_layer($i)->height; $i--) { + Slic3r::debugf " skipping infill below those areas at layer %d\n", $i; + foreach my $lower_layerm (@{$self->get_layer($i)->regions}) { + my @new_surfaces = (); + # subtract the area from all types of surfaces + foreach my $group (@{$lower_layerm->fill_surfaces->group}) { + push @new_surfaces, map $group->[0]->clone(expolygon => $_), + @{diff_ex( + [ map $_->p, @$group ], + [ map @$_, @$to_bridge ], + )}; + push @new_surfaces, map Slic3r::Surface->new( + expolygon => $_, + surface_type => S_TYPE_INTERNALVOID, + ), @{intersection_ex( + [ map $_->p, @$group ], + [ map @$_, @$to_bridge ], + )}; + } + $lower_layerm->fill_surfaces->clear; + $lower_layerm->fill_surfaces->append($_) for @new_surfaces; + } + + $excess -= $self->get_layer($i)->height; + } + } + */ + } + } +} + #ifdef SLIC3RXS REGISTER_CLASS(PrintObject, "Print::Object"); diff --git a/xs/src/libslic3r/PrintRegion.cpp b/xs/src/libslic3r/PrintRegion.cpp index 4a84de15a..e5b9092e4 100644 --- a/xs/src/libslic3r/PrintRegion.cpp +++ b/xs/src/libslic3r/PrintRegion.cpp @@ -53,8 +53,10 @@ PrintRegion::flow(FlowRole role, double layer_height, bool bridge, bool first_la size_t extruder; // 1-based if (role == frPerimeter || role == frExternalPerimeter) { extruder = this->config.perimeter_extruder; - } else if (role == frInfill || role == frSolidInfill || role == frTopSolidInfill) { + } else if (role == frInfill) { extruder = this->config.infill_extruder; + } else if (role == frSolidInfill || role == frTopSolidInfill) { + extruder = this->config.solid_infill_extruder; } else { CONFESS("Unknown role $role"); } diff --git a/xs/src/libslic3r/SupportMaterial.hpp b/xs/src/libslic3r/SupportMaterial.hpp new file mode 100644 index 000000000..edea22695 --- /dev/null +++ b/xs/src/libslic3r/SupportMaterial.hpp @@ -0,0 +1,11 @@ +#ifndef slic3r_SupportMaterial_hpp_ +#define slic3r_SupportMaterial_hpp_ + +namespace Slic3r { + +// how much we extend support around the actual contact area +#define SUPPORT_MATERIAL_MARGIN 1.5 + +} + +#endif diff --git a/xs/src/libslic3r/Surface.cpp b/xs/src/libslic3r/Surface.cpp index a53cb2513..e4625f799 100644 --- a/xs/src/libslic3r/Surface.cpp +++ b/xs/src/libslic3r/Surface.cpp @@ -14,7 +14,8 @@ Surface::is_solid() const return this->surface_type == stTop || this->surface_type == stBottom || this->surface_type == stBottomBridge - || this->surface_type == stInternalSolid; + || this->surface_type == stInternalSolid + || this->surface_type == stInternalBridge; } bool @@ -25,6 +26,15 @@ Surface::is_external() const || this->surface_type == stBottomBridge; } +bool +Surface::is_internal() const +{ + return this->surface_type == stInternal + || this->surface_type == stInternalBridge + || this->surface_type == stInternalSolid + || this->surface_type == stInternalVoid; +} + bool Surface::is_bottom() const { diff --git a/xs/src/libslic3r/Surface.hpp b/xs/src/libslic3r/Surface.hpp index c98567cf0..28a90799a 100644 --- a/xs/src/libslic3r/Surface.hpp +++ b/xs/src/libslic3r/Surface.hpp @@ -24,6 +24,7 @@ class Surface double area() const; bool is_solid() const; bool is_external() const; + bool is_internal() const; bool is_bottom() const; bool is_bridge() const; diff --git a/xs/src/libslic3r/SurfaceCollection.cpp b/xs/src/libslic3r/SurfaceCollection.cpp index 1590e7a21..ccf1eaa4e 100644 --- a/xs/src/libslic3r/SurfaceCollection.cpp +++ b/xs/src/libslic3r/SurfaceCollection.cpp @@ -68,6 +68,39 @@ SurfaceCollection::group(std::vector *retval) } } +template +bool +SurfaceCollection::any_internal_contains(const T &item) const +{ + for (Surfaces::const_iterator surface = this->surfaces.begin(); surface != this->surfaces.end(); ++surface) { + if (surface->is_internal() && surface->expolygon.contains(item)) return true; + } + return false; +} +template bool SurfaceCollection::any_internal_contains(const Line &item) const; +template bool SurfaceCollection::any_internal_contains(const Polyline &item) const; + +SurfacesPtr +SurfaceCollection::filter_by_type(SurfaceType type) +{ + SurfacesPtr ss; + for (Surfaces::iterator surface = this->surfaces.begin(); surface != this->surfaces.end(); ++surface) { + if (surface->surface_type == type) ss.push_back(&*surface); + } + return ss; +} + +void +SurfaceCollection::filter_by_type(SurfaceType type, Polygons* polygons) +{ + for (Surfaces::iterator surface = this->surfaces.begin(); surface != this->surfaces.end(); ++surface) { + if (surface->surface_type == type) { + Polygons pp = surface->expolygon; + polygons->insert(polygons->end(), pp.begin(), pp.end()); + } + } +} + #ifdef SLIC3RXS REGISTER_CLASS(SurfaceCollection, "Surface::Collection"); #endif diff --git a/xs/src/libslic3r/SurfaceCollection.hpp b/xs/src/libslic3r/SurfaceCollection.hpp index fe3fae8c6..09a46449e 100644 --- a/xs/src/libslic3r/SurfaceCollection.hpp +++ b/xs/src/libslic3r/SurfaceCollection.hpp @@ -15,6 +15,9 @@ class SurfaceCollection operator ExPolygons() const; void simplify(double tolerance); void group(std::vector *retval); + template bool any_internal_contains(const T &item) const; + SurfacesPtr filter_by_type(SurfaceType type); + void filter_by_type(SurfaceType type, Polygons* polygons); }; } diff --git a/xs/t/08_extrusionloop.t b/xs/t/08_extrusionloop.t index dd657d156..0657766c8 100644 --- a/xs/t/08_extrusionloop.t +++ b/xs/t/08_extrusionloop.t @@ -5,7 +5,7 @@ use warnings; use List::Util qw(sum); use Slic3r::XS; -use Test::More tests => 45; +use Test::More tests => 46; { my $square = [ @@ -119,4 +119,29 @@ use Test::More tests => 45; } } +{ + my @polylines = ( + Slic3r::Polyline->new([59312736,4821067],[64321068,4821067],[64321068,4821067],[64321068,9321068],[59312736,9321068]), + Slic3r::Polyline->new([59312736,9321068],[9829401,9321068]), + Slic3r::Polyline->new([9829401,9321068],[4821067,9321068],[4821067,4821067],[9829401,4821067]), + Slic3r::Polyline->new([9829401,4821067],[59312736,4821067]), + ); + my $loop = Slic3r::ExtrusionLoop->new; + $loop->append($_) for ( + Slic3r::ExtrusionPath->new(polyline => $polylines[0], role => Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, mm3_per_mm => 1), + Slic3r::ExtrusionPath->new(polyline => $polylines[1], role => Slic3r::ExtrusionPath::EXTR_ROLE_OVERHANG_PERIMETER, mm3_per_mm => 1), + Slic3r::ExtrusionPath->new(polyline => $polylines[2], role => Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, mm3_per_mm => 1), + Slic3r::ExtrusionPath->new(polyline => $polylines[3], role => Slic3r::ExtrusionPath::EXTR_ROLE_OVERHANG_PERIMETER, mm3_per_mm => 1), + ); + my $point = Slic3r::Point->new(4821067,9321068); + $loop->split_at_vertex($point) or $loop->split_at($point); + is_deeply [ map $_->role, @$loop ], [ + Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, + Slic3r::ExtrusionPath::EXTR_ROLE_OVERHANG_PERIMETER, + Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, + Slic3r::ExtrusionPath::EXTR_ROLE_OVERHANG_PERIMETER, + Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, + ], 'order is correctly preserved after splitting'; +} + __END__ diff --git a/xs/t/12_extrusionpathcollection.t b/xs/t/12_extrusionpathcollection.t index 6933f965a..73608cba7 100644 --- a/xs/t/12_extrusionpathcollection.t +++ b/xs/t/12_extrusionpathcollection.t @@ -26,7 +26,9 @@ my $loop = Slic3r::ExtrusionLoop->new_from_paths( ), ); -my $collection = Slic3r::ExtrusionPath::Collection->new($path); +my $collection = Slic3r::ExtrusionPath::Collection->new( + $path, +); isa_ok $collection, 'Slic3r::ExtrusionPath::Collection', 'collection object with items in constructor'; ok !$collection->no_sort, 'no_sort is false by default'; diff --git a/xs/t/15_config.t b/xs/t/15_config.t index cd4dc2654..e051d9a86 100644 --- a/xs/t/15_config.t +++ b/xs/t/15_config.t @@ -4,7 +4,7 @@ use strict; use warnings; use Slic3r::XS; -use Test::More tests => 107; +use Test::More tests => 108; foreach my $config (Slic3r::Config->new, Slic3r::Config::Full->new) { $config->set('layer_height', 0.3); @@ -182,6 +182,13 @@ foreach my $config (Slic3r::Config->new, Slic3r::Config::Full->new) { is $config->get('perimeter_extruder'), 3, 'defined extruder is not overwritten by default extruder'; } +{ + my $config = Slic3r::Config->new; + $config->set('infill_extruder', 2); + $config->normalize; + is $config->get('solid_infill_extruder'), 2, 'undefined solid infill extruder is populated with infill extruder'; +} + { my $config = Slic3r::Config->new; $config->set('spiral_vase', 1); diff --git a/xs/t/19_model.t b/xs/t/19_model.t index 56b3631af..d6f6d97a1 100644 --- a/xs/t/19_model.t +++ b/xs/t/19_model.t @@ -10,9 +10,9 @@ use Test::More tests => 4; my $model = Slic3r::Model->new; my $object = $model->_add_object; isa_ok $object, 'Slic3r::Model::Object::Ref'; - isa_ok $object->origin_translation, 'Slic3r::Pointf::Ref'; - $object->origin_translation->translate(10,0); - is_deeply \@{$object->origin_translation}, [10,0], 'origin_translation is modified by ref'; + isa_ok $object->origin_translation, 'Slic3r::Pointf3::Ref'; + $object->origin_translation->translate(10,0,0); + is_deeply \@{$object->origin_translation}, [10,0,0], 'origin_translation is modified by ref'; my $lhr = [ [ 5, 10, 0.1 ] ]; $object->set_layer_height_ranges($lhr); diff --git a/xs/xsp/BoundingBox.xsp b/xs/xsp/BoundingBox.xsp index 61995a328..eafe55766 100644 --- a/xs/xsp/BoundingBox.xsp +++ b/xs/xsp/BoundingBox.xsp @@ -83,6 +83,7 @@ new_from_points(CLASS, points) Clone clone() %code{% RETVAL = THIS; %}; void merge(BoundingBoxf3* bb) %code{% THIS->merge(*bb); %}; + void merge_point(Pointf3* point) %code{% THIS->merge(*point); %}; void scale(double factor); void translate(double x, double y, double z); Clone size(); diff --git a/xs/xsp/Config.xsp b/xs/xsp/Config.xsp index d62a3a688..4ec282edd 100644 --- a/xs/xsp/Config.xsp +++ b/xs/xsp/Config.xsp @@ -23,6 +23,8 @@ %code{% THIS->apply(*other, true); %}; std::vector diff(DynamicPrintConfig* other) %code{% RETVAL = THIS->diff(*other); %}; + bool equals(DynamicPrintConfig* other) + %code{% RETVAL = THIS->equals(*other); %}; void apply_static(FullPrintConfig* other) %code{% THIS->apply(*other, true); %}; std::vector get_keys() diff --git a/xs/xsp/ExtrusionLoop.xsp b/xs/xsp/ExtrusionLoop.xsp index 6767c7d3b..2a8d1f9f4 100644 --- a/xs/xsp/ExtrusionLoop.xsp +++ b/xs/xsp/ExtrusionLoop.xsp @@ -28,6 +28,9 @@ %code{% THIS->clip_end(distance, &RETVAL); %}; bool has_overhang_point(Point* point) %code{% RETVAL = THIS->has_overhang_point(*point); %}; + bool is_perimeter(); + bool is_infill(); + bool is_solid_infill(); %{ SV* diff --git a/xs/xsp/ExtrusionPath.xsp b/xs/xsp/ExtrusionPath.xsp index d3a48ac24..4fbaea2d6 100644 --- a/xs/xsp/ExtrusionPath.xsp +++ b/xs/xsp/ExtrusionPath.xsp @@ -23,7 +23,8 @@ void simplify(double tolerance); double length(); bool is_perimeter(); - bool is_fill(); + bool is_infill(); + bool is_solid_infill(); bool is_bridge(); std::string gcode(Extruder* extruder, double e, double F, double xofs, double yofs, std::string extrusion_axis, diff --git a/xs/xsp/GCodeWriter.xsp b/xs/xsp/GCodeWriter.xsp index ee9049e56..a76c0e55f 100644 --- a/xs/xsp/GCodeWriter.xsp +++ b/xs/xsp/GCodeWriter.xsp @@ -44,6 +44,7 @@ std::string unretract(); std::string lift(); std::string unlift(); + Clone get_position() const; %{ SV* diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp index e0f7e8a17..cc38b0847 100644 --- a/xs/xsp/Layer.xsp +++ b/xs/xsp/Layer.xsp @@ -69,6 +69,12 @@ %code%{ RETVAL = (int)(intptr_t)THIS; %}; void make_slices(); + bool any_internal_region_slice_contains_line(Line* line) + %code%{ RETVAL = THIS->any_internal_region_slice_contains(*line); %}; + bool any_internal_region_fill_surface_contains_line(Line* line) + %code%{ RETVAL = THIS->any_internal_region_fill_surface_contains(*line); %}; + bool any_internal_region_fill_surface_contains_polyline(Polyline* polyline) + %code%{ RETVAL = THIS->any_internal_region_fill_surface_contains(*polyline); %}; }; %name{Slic3r::Layer::Support} class SupportLayer { @@ -114,4 +120,11 @@ Ref slices() %code%{ RETVAL = &THIS->slices; %}; + + bool any_internal_region_slice_contains_line(Line* line) + %code%{ RETVAL = THIS->any_internal_region_slice_contains(*line); %}; + bool any_internal_region_fill_surface_contains_line(Line* line) + %code%{ RETVAL = THIS->any_internal_region_fill_surface_contains(*line); %}; + bool any_internal_region_fill_surface_contains_polyline(Polyline* polyline) + %code%{ RETVAL = THIS->any_internal_region_fill_surface_contains(*polyline); %}; }; diff --git a/xs/xsp/Line.xsp b/xs/xsp/Line.xsp index d0552315f..69d9fbce1 100644 --- a/xs/xsp/Line.xsp +++ b/xs/xsp/Line.xsp @@ -32,6 +32,7 @@ Clone point_at(double distance); Polyline* as_polyline() %code{% RETVAL = new Polyline(*THIS); %}; + Clone normal(); %{ Line* @@ -65,3 +66,17 @@ Line::coincides_with(line_sv) %} }; + + +%name{Slic3r::Linef3} class Linef3 { + Linef3(Pointf3* a, Pointf3* b) + %code{% RETVAL = new Linef3(*a, *b); %}; + ~Linef3(); + Clone clone() + %code{% RETVAL = THIS; %}; + Ref a() + %code{% RETVAL = &THIS->a; %}; + Ref b() + %code{% RETVAL = &THIS->b; %}; + Clone intersect_plane(double z); +}; diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index fd31a93d4..2252d357d 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -20,6 +20,8 @@ void clear_objects(); size_t objects_count() %code%{ RETVAL = THIS->objects.size(); %}; + Ref get_object(int idx) + %code%{ RETVAL = THIS->objects.at(idx); %}; Ref get_material(t_model_material_id material_id) %code%{ @@ -188,9 +190,9 @@ ModelMaterial::attributes() void set_layer_height_ranges(t_layer_height_ranges ranges) %code%{ THIS->layer_height_ranges = ranges; %}; - Ref origin_translation() + Ref origin_translation() %code%{ RETVAL = &THIS->origin_translation; %}; - void set_origin_translation(Pointf* point) + void set_origin_translation(Pointf3* point) %code%{ THIS->origin_translation = *point; %}; bool needed_repair() const; diff --git a/xs/xsp/Point.xsp b/xs/xsp/Point.xsp index e1163ea5c..e45c41213 100644 --- a/xs/xsp/Point.xsp +++ b/xs/xsp/Point.xsp @@ -31,6 +31,8 @@ %code{% RETVAL = THIS->distance_to(*line); %}; double ccw(Point* p1, Point* p2) %code{% RETVAL = THIS->ccw(*p1, *p2); %}; + double ccw_angle(Point* p1, Point* p2) + %code{% RETVAL = THIS->ccw_angle(*p1, *p2); %}; Clone projection_onto_polygon(Polygon* polygon) %code{% RETVAL = new Point(THIS->projection_onto(*polygon)); %}; Clone projection_onto_polyline(Polyline* polyline) @@ -93,10 +95,18 @@ Point::coincides_with(point_sv) %code{% RETVAL = THIS->x; %}; double y() %code{% RETVAL = THIS->y; %}; + void set_x(double val) + %code{% THIS->x = val; %}; + void set_y(double val) + %code{% THIS->y = val; %}; void translate(double x, double y); void scale(double factor); void rotate(double angle, Pointf* center) %code{% THIS->rotate(angle, *center); %}; + Clone negative() + %code{% RETVAL = THIS->negative(); %}; + Clone vector_to(Pointf* point) + %code{% RETVAL = THIS->vector_to(*point); %}; }; %name{Slic3r::Pointf3} class Pointf3 { @@ -116,4 +126,12 @@ Point::coincides_with(point_sv) %code{% THIS->y = val; %}; void set_z(double val) %code{% THIS->z = val; %}; + void translate(double x, double y, double z); + void scale(double factor); + double distance_to(Pointf3* point) + %code{% RETVAL = THIS->distance_to(*point); %}; + Clone negative() + %code{% RETVAL = THIS->negative(); %}; + Clone vector_to(Pointf3* point) + %code{% RETVAL = THIS->vector_to(*point); %}; }; diff --git a/xs/xsp/Polygon.xsp b/xs/xsp/Polygon.xsp index 99ff1d644..6a8eac99c 100644 --- a/xs/xsp/Polygon.xsp +++ b/xs/xsp/Polygon.xsp @@ -47,6 +47,10 @@ THIS->bounding_box(RETVAL); %}; std::string wkt(); + Points concave_points(double angle) + %code{% THIS->concave_points(angle, &RETVAL); %}; + Points convex_points(double angle) + %code{% THIS->convex_points(angle, &RETVAL); %}; %{ Polygon* diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp index 556e87396..c3c9a97c5 100644 --- a/xs/xsp/Print.xsp +++ b/xs/xsp/Print.xsp @@ -61,6 +61,11 @@ _constant() %code%{ RETVAL = THIS->layer_height_ranges; %}; Ref size() %code%{ RETVAL = &THIS->size; %}; + BoundingBox* bounding_box() + %code{% + RETVAL = new BoundingBox(); + THIS->bounding_box(RETVAL); + %}; Ref _copies_shift() %code%{ RETVAL = &THIS->_copies_shift; %}; @@ -83,6 +88,7 @@ _constant() void set_layer_height_ranges(t_layer_height_ranges layer_height_ranges) %code%{ THIS->layer_height_ranges = layer_height_ranges; %}; + size_t total_layer_count(); size_t layer_count(); void clear_layers(); Ref get_layer(int idx); @@ -106,6 +112,8 @@ _constant() void set_step_started(PrintObjectStep step) %code%{ THIS->state.set_started(step); %}; + void bridge_over_infill(); + int ptr() %code%{ RETVAL = (int)(intptr_t)THIS; %}; }; @@ -133,12 +141,6 @@ _constant() %code%{ RETVAL = &THIS->objects; %}; void clear_objects(); Ref get_object(int idx); - Ref add_object(ModelObject* model_object, - BoundingBoxf3 *modobj_bbox) - %code%{ RETVAL = THIS->add_object(model_object, *modobj_bbox); %}; - Ref set_new_object(size_t idx, ModelObject* model_object, - BoundingBoxf3 *modobj_bbox) - %code%{ RETVAL = THIS->set_new_object(idx, model_object, *modobj_bbox); %}; void delete_object(int idx); void reload_object(int idx); size_t object_count() @@ -156,6 +158,8 @@ _constant() bool invalidate_all_steps(); bool step_done(PrintStep step) %code%{ RETVAL = THIS->state.is_done(step); %}; + bool object_step_done(PrintObjectStep step) + %code%{ RETVAL = THIS->step_done(step); %}; void set_step_done(PrintStep step) %code%{ THIS->state.set_done(step); %}; void set_step_started(PrintStep step) @@ -185,6 +189,11 @@ _constant() croak("%s\n", e.what()); } %}; + Clone bounding_box(); + Clone total_bounding_box(); + double skirt_first_layer_height(); + Clone brim_flow(); + Clone skirt_flow(); %{ double diff --git a/xs/xsp/SupportMaterial.xsp b/xs/xsp/SupportMaterial.xsp new file mode 100644 index 000000000..a0301f248 --- /dev/null +++ b/xs/xsp/SupportMaterial.xsp @@ -0,0 +1,16 @@ +%module{Slic3r::XS}; + +#include +#include "libslic3r/SupportMaterial.hpp" + +%package{Slic3r::Print::SupportMaterial}; +%{ + +SV* +MARGIN() + PROTOTYPE: + CODE: + RETVAL = newSVnv(SUPPORT_MATERIAL_MARGIN); + OUTPUT: RETVAL + +%} \ No newline at end of file diff --git a/xs/xsp/Surface.xsp b/xs/xsp/Surface.xsp index cd114c0df..ffde83298 100644 --- a/xs/xsp/Surface.xsp +++ b/xs/xsp/Surface.xsp @@ -17,6 +17,7 @@ double area(); bool is_solid() const; bool is_external() const; + bool is_internal() const; bool is_bottom() const; bool is_bridge() const; %{ diff --git a/xs/xsp/my.map b/xs/xsp/my.map index 768db818e..4391c3bfc 100644 --- a/xs/xsp/my.map +++ b/xs/xsp/my.map @@ -73,6 +73,10 @@ Line* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T Clone O_OBJECT_SLIC3R_T +Linef3* O_OBJECT_SLIC3R +Ref O_OBJECT_SLIC3R_T +Clone O_OBJECT_SLIC3R_T + Polyline* O_OBJECT_SLIC3R Ref O_OBJECT_SLIC3R_T Clone O_OBJECT_SLIC3R_T diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt index 1467540a3..00d3c08d9 100644 --- a/xs/xsp/typemap.xspt +++ b/xs/xsp/typemap.xspt @@ -60,6 +60,9 @@ %typemap{Line*}; %typemap{Ref}{simple}; %typemap{Clone}{simple}; +%typemap{Linef3*}; +%typemap{Ref}{simple}; +%typemap{Clone}{simple}; %typemap{Polyline*}; %typemap{Ref}{simple}; %typemap{Clone}{simple};