Merge branch 'master' into avoid-crossing-perimeters

Conflicts:
	lib/Slic3r/GCode.pm
	lib/Slic3r/GUI/Plater.pm
	lib/Slic3r/Print.pm
	lib/Slic3r/SVG.pm
medial-thinwall
Alessandro Ranellucci 2013-01-12 19:00:18 +01:00
commit 48e00a4c40
52 changed files with 2388 additions and 821 deletions

View File

@ -11,7 +11,7 @@ my $build = Module::Build->new(
'File::Basename' => '0',
'File::Spec' => '0',
'Getopt::Long' => '0',
'Math::Clipper' => '1.09',
'Math::Clipper' => '1.15',
'Math::ConvexHull::MonotoneChain' => '0.01',
'Math::Geometry::Voronoi' => '1.3',
'Math::PlanePath' => '53',
@ -22,11 +22,12 @@ my $build = Module::Build->new(
},
build_requires => {
'Test::More' => '0.10',
'IO::Scalar' => '0.10',
},
recommends => {
'Growl::GNTP' => '0.15',
'Net::DBus' => '0',
'XML::SAX' => '0',
'XML::SAX::ExpatXS' => '0',
'Wx' => '0.9901',
},
script_files => ['slic3r.pl'],

View File

@ -46,6 +46,7 @@ lib/Slic3r/Print/Region.pm
lib/Slic3r/Print/Object.pm
lib/Slic3r/Surface.pm
lib/Slic3r/SVG.pm
lib/Slic3r/Test.pm
lib/Slic3r/TriangleMesh.pm
MANIFEST This list of files
README.markdown
@ -55,16 +56,25 @@ t/arcs.t
t/clean_polylines.t
t/clipper.t
t/collinear.t
t/custom_gcode.t
t/dynamic.t
t/fill.t
t/geometry.t
t/layers.t
t/loops.t
t/polyclip.t
t/retraction.t
t/serialize.t
t/stl.t
t/shells.t
t/slice.t
t/vibrationlimit.t
utils/amf-to-stl.pl
utils/file_info.pl
utils/post-processing/filament-weight.pl
utils/post-processing/prowl-notification.pl
utils/post-processing/z-every-line.pl
utils/post-processing/decimate.pl
utils/post-processing/flowrate.pl
utils/split_stl.pl
utils/stl-to-amf.pl
utils/zsh/functions/_slic3r

View File

@ -116,6 +116,8 @@ The author of the Silk icon set is Mark James.
--g0 Use G0 commands for retraction (experimental, not supported by all
firmwares)
--gcode-comments Make G-code verbose by adding comments (default: no)
--vibration-limit Limit the frequency of moves on X and Y axes (Hz, set zero to disable;
default: 0)
Filament options:
--filament-diameter Diameter in mm of your raw filament (default: 3)
@ -144,10 +146,24 @@ The author of the Silk icon set is Mark James.
(default: 60)
--top-solid-infill-speed Speed of print moves for top surfaces in mm/s or % over solid infill speed
(default: 50)
--support-material-speed
Speed of support material print moves in mm/s (default: 60)
--bridge-speed Speed of bridge print moves in mm/s (default: 60)
--gap-fill-speed Speed of gap fill print moves in mm/s (default: 20)
--first-layer-speed Speed of print moves for bottom layer, expressed either as an absolute
value or as a percentage over normal speeds (default: 30%)
Acceleration options:
--perimeter-acceleration
Overrides firmware's default acceleration for perimeters. (mm/s^2, set zero
to disable; default: 0)
--infill-acceleration
Overrides firmware's default acceleration for infill. (mm/s^2, set zero
to disable; default: 0)
--default-acceleration
Acceleration will be reset to this value after the specific settings above
have been applied. (mm/s^2, set zero to disable; default: 130)
Accuracy options:
--layer-height Layer height in mm (default: 0.4)
--first-layer-height Layer height for first layer (mm or %, default: 100%)
@ -158,8 +174,9 @@ The author of the Silk icon set is Mark James.
Print options:
--perimeters Number of perimeters/horizontal skins (range: 0+, default: 3)
--solid-layers Number of solid layers to do for top/bottom surfaces
(range: 1+, default: 3)
--top-solid-layers Number of solid layers to do for top surfaces (range: 0+, default: 3)
--bottom-solid-layers Number of solid layers to do for bottom surfaces (range: 0+, default: 3)
--solid-layers Shortcut for setting the two options above at once
--fill-density Infill density (range: 0-1, default: 0.4)
--fill-angle Infill angle in degrees (range: 0-90, default: 45)
--fill-pattern Pattern to use to fill non-solid layers (default: rectilinear)
@ -170,6 +187,7 @@ The author of the Silk icon set is Mark James.
the default commands (turn off temperature [M104 S0],
home X axis [G28 X], disable motors [M84]).
--layer-gcode Load layer-change G-code from the supplied file (default: nothing).
--toolchange-gcode Load tool-change G-code from the supplied file (default: nothing).
--extra-perimeters Add more perimeters when needed (default: yes)
--randomize-start Randomize starting point across layers (default: yes)
--avoid-crossing-perimeters Optimize travel moves so that no perimeters are crossed (default: no)
@ -183,7 +201,8 @@ The author of the Silk icon set is Mark James.
Support material options:
--support-material Generate support material for overhangs
--support-material-threshold
Overhang threshold angle (range: 0-90, default: 45)
Overhang threshold angle (range: 0-90, set 0 for automatic detection,
default: 0)
--support-material-pattern
Pattern to use for support material (default: rectilinear)
--support-material-spacing
@ -227,6 +246,8 @@ The author of the Silk icon set is Mark James.
--skirt-distance Distance in mm between innermost skirt and object
(default: 6)
--skirt-height Height of skirts to draw (expressed in layers, 0+, default: 1)
--min-skirt-length Generate no less than the number of loops required to consume this length
of filament on the first layer, for each extruder (mm, 0+, default: 0)
--brim-width Width of the brim that will get added to each object to help adhesion
(mm, default: 0)
@ -261,7 +282,7 @@ The author of the Silk icon set is Mark James.
--support-material-extrusion-width
Set a different extrusion width for support material
--bridge-flow-ratio Multiplier for extrusion when bridging (> 0, default: 1)
Multiple extruder options:
--extruder-offset Offset of each extruder, if firmware doesn't handle the displacement
(can be specified multiple times, default: 0x0)

View File

@ -7,7 +7,7 @@ use strict;
use warnings;
require v5.10;
our $VERSION = "0.9.4-dev";
our $VERSION = "0.9.8-dev";
our $debug = 0;
sub debugf {
@ -58,9 +58,12 @@ use Slic3r::TriangleMesh;
eval "use Slic3r::Build";
use constant SCALING_FACTOR => 0.000001;
use constant RESOLUTION => 0.01;
use constant OVERLAP_FACTOR => 0.5;
use constant RESOLUTION => 0.0125;
use constant SCALED_RESOLUTION => RESOLUTION / SCALING_FACTOR;
use constant OVERLAP_FACTOR => 1;
use constant SMALL_PERIMETER_LENGTH => (6.5 / SCALING_FACTOR) * 2 * PI;
use constant LOOP_CLIPPING_LENGTH_OVER_SPACING => 0.15;
use constant PERIMETER_INFILL_OVERLAP_OVER_SPACING => 0.45;
# The following variables hold the objects used throughout the slicing
# process. They should belong to the Print object, but we are keeping

View File

@ -5,10 +5,8 @@ use utf8;
use List::Util qw(first);
use constant PI => 4 * atan2(1, 1);
# cemetery of old config settings
our @Ignore = qw(duplicate_x duplicate_y multiply_x multiply_y support_material_tool);
our @Ignore = qw(duplicate_x duplicate_y multiply_x multiply_y support_material_tool acceleration);
my $serialize_comma = sub { join ',', @{$_[0]} };
my $deserialize_comma = sub { [ split /,/, $_[0] ] };
@ -155,7 +153,7 @@ our $Options = {
default => [1],
},
'temperature' => {
label => 'Temperature',
label => 'Other layers',
tooltip => 'Extruder temperature for layers after the first one. Set this to zero to disable temperature control commands in the output.',
sidetext => '°C',
cli => 'temperature=i@',
@ -166,7 +164,7 @@ our $Options = {
default => [200],
},
'first_layer_temperature' => {
label => 'First layer temperature',
label => 'First layer',
tooltip => 'Extruder temperature for first layer. If you want to control temperature manually during print, set this to zero to disable temperature control commands in the output file.',
sidetext => '°C',
cli => 'first-layer-temperature=i@',
@ -203,7 +201,7 @@ our $Options = {
# filament options
'first_layer_bed_temperature' => {
label => 'First layer bed temperature',
label => 'First layer',
tooltip => 'Heated build plate temperature for the first layer. Set this to zero to disable bed temperature control commands in the output.',
sidetext => '°C',
cli => 'first-layer-bed-temperature=i',
@ -212,7 +210,7 @@ our $Options = {
default => 0,
},
'bed_temperature' => {
label => 'Bed Temperature',
label => 'Other layers',
tooltip => 'Bed temperature for layers after the first one. Set this to zero to disable bed temperature control commands in the output.',
sidetext => '°C',
cli => 'bed-temperature=i',
@ -286,6 +284,14 @@ our $Options = {
ratio_over => 'solid_infill_speed',
default => 50,
},
'support_material_speed' => {
label => 'Support material',
tooltip => 'Speed for printing support material.',
sidetext => 'mm/s',
cli => 'support-material-speed=f',
type => 'f',
default => 60,
},
'bridge_speed' => {
label => 'Bridges',
tooltip => 'Speed for printing bridges.',
@ -295,6 +301,14 @@ our $Options = {
aliases => [qw(bridge_feed_rate)],
default => 60,
},
'gap_fill_speed' => {
label => 'Gap fill',
tooltip => 'Speed for filling small gaps using short zigzag moves. Keep this reasonably low to avoid too much shaking and resonance issues. Set zero to disable gaps filling.',
sidetext => 'mm/s',
cli => 'gap-fill-speed=f',
type => 'f',
default => 20,
},
'first_layer_speed' => {
label => 'First layer speed',
tooltip => 'If expressed as absolute value in mm/s, this speed will be applied to all the print moves of the first layer, regardless of their type. If expressed as a percentage (for example: 40%) it will scale the default speeds.',
@ -305,25 +319,29 @@ our $Options = {
},
# acceleration options
'acceleration' => {
label => 'Enable acceleration control',
cli => 'acceleration!',
type => 'bool',
'default_acceleration' => {
label => 'Default',
tooltip => 'This is the acceleration your printer will be reset to after the role-specific acceleration values are used (perimeter/infill). Set zero to prevent resetting acceleration at all.',
sidetext => 'mm/s²',
cli => 'default-acceleration',
type => 'f',
default => 0,
},
'perimeter_acceleration' => {
label => 'Perimeters',
tooltip => 'This is the acceleration your printer will use for perimeters. A high value like 9000 usually gives good results if your hardware is up to the job. Set zero to disable acceleration control for perimeters.',
sidetext => 'mm/s²',
cli => 'perimeter-acceleration',
type => 'f',
default => 25,
default => 0,
},
'infill_acceleration' => {
label => 'Infill',
tooltip => 'This is the acceleration your printer will use for infill. Set zero to disable acceleration control for infill.',
sidetext => 'mm/s²',
cli => 'infill-acceleration',
type => 'f',
default => 50,
default => 0,
},
# accuracy options
@ -412,6 +430,14 @@ our $Options = {
type => 'f',
default => 1,
},
'vibration_limit' => {
label => 'Vibration limit',
tooltip => 'This experimental option will slow down those moves hitting the configured frequency limit. The purpose of limiting vibrations is to avoid mechanical resonance. Set zero to disable.',
sidetext => 'Hz',
cli => 'vibration-limit=f',
type => 'f',
default => 0,
},
# print options
'perimeters' => {
@ -424,9 +450,23 @@ our $Options = {
},
'solid_layers' => {
label => 'Solid layers',
tooltip => 'Number of solid layers to generate on top and bottom.',
tooltip => 'Number of solid layers to generate on top and bottom surfaces.',
cli => 'solid-layers=i',
type => 'i',
shortcut => [qw(top_solid_layers bottom_solid_layers)],
},
'top_solid_layers' => {
label => 'Top',
tooltip => 'Number of solid layers to generate on top surfaces.',
cli => 'top-solid-layers=i',
type => 'i',
default => 3,
},
'bottom_solid_layers' => {
label => 'Bottom',
tooltip => 'Number of solid layers to generate on bottom surfaces.',
cli => 'bottom-solid-layers=i',
type => 'i',
default => 3,
},
'fill_pattern' => {
@ -507,11 +547,11 @@ our $Options = {
},
'support_material_threshold' => {
label => 'Overhang threshold',
tooltip => 'Support material will not generated for overhangs whose slope angle is above the given threshold.',
tooltip => 'Support material will not generated for overhangs whose slope angle is above the given threshold. Set to zero for automatic detection.',
sidetext => '°',
cli => 'support-material-threshold=i',
type => 'i',
default => 45,
default => 0,
},
'support_material_pattern' => {
label => 'Pattern',
@ -578,6 +618,18 @@ END
deserialize => sub { join "\n", split /\\n/, $_[0] },
default => '',
},
'toolchange_gcode' => {
label => 'Tool change G-code',
tooltip => 'This custom code is inserted at every extruder change. Note that you can use placeholder variables for all Slic3r settings as well as [previous_extruder] and [next_extruder].',
cli => 'toolchange-gcode=s',
type => 's',
multiline => 1,
full_width => 1,
height => 50,
serialize => sub { join '\n', split /\R+/, $_[0] },
deserialize => sub { join "\n", split /\\n/, $_[0] },
default => '',
},
'post_process' => {
label => 'Post-processing scripts',
tooltip => 'If you want to process the output G-code through custom scripts, just list their absolute paths here. Separate multiple scripts with a semicolon. Scripts will be passed the absolute path to the G-code file as the first argument, and they can access the Slic3r config settings by reading environment variables.',
@ -673,7 +725,7 @@ END
default => 0,
},
'min_fan_speed' => {
label => 'Min fan speed',
label => 'Min',
tooltip => 'This setting represents the minimum PWM your fan needs to work.',
sidetext => '%',
cli => 'min-fan-speed=i',
@ -682,7 +734,7 @@ END
default => 35,
},
'max_fan_speed' => {
label => 'Max fan speed',
label => 'Max',
tooltip => 'This setting represents the maximum speed of your fan.',
sidetext => '%',
cli => 'max-fan-speed=i',
@ -691,7 +743,7 @@ END
default => 100,
},
'bridge_fan_speed' => {
label => 'Bridge fan speed',
label => 'Bridges fan speed',
tooltip => 'This fan speed is enforced during all bridges.',
sidetext => '%',
cli => 'bridge-fan-speed=i',
@ -753,6 +805,15 @@ END
type => 'i',
default => 1,
},
'min_skirt_length' => {
label => 'Minimum extrusion length',
tooltip => 'Generate no less than the number of skirt loops required to consume the specified amount of filament on the bottom layer. For multi-extruder machines, this minimum applies to each extruder.',
sidetext => 'mm',
cli => 'min-skirt-length=f',
type => 'f',
default => 0,
min => 0,
},
'skirt_distance' => {
label => 'Distance from object',
tooltip => 'Distance between skirt and object(s). Set this to zero to attach the skirt to the object(s) and get a brim for better adhesion.',
@ -837,7 +898,7 @@ END
default => 0,
},
'extruder_clearance_radius' => {
label => 'Extruder clearance radius',
label => 'Radius',
tooltip => 'Set this to the clearance radius around your extruder. If the extruder is not centered, choose the largest value for safety. This setting is used to check for collisions and to display the graphical preview in the plater.',
sidetext => 'mm',
cli => 'extruder-clearance-radius=f',
@ -845,7 +906,7 @@ END
default => 20,
},
'extruder_clearance_height' => {
label => 'Extruder clearance height',
label => 'Height',
tooltip => 'Set this to the vertical distance between your nozzle tip and (usually) the X carriage rods. In other words, this is the height of the clearance cylinder around your extruder, and it represents the maximum depth the extruder can peek before colliding with other printed objects.',
sidetext => 'mm',
cli => 'extruder-clearance-height=f',
@ -874,7 +935,11 @@ sub new {
sub new_from_defaults {
my $class = shift;
my @opt_keys =
return $class->new(map { $_ => $Options->{$_}{default} } (@_ ? @_ : keys %$Options));
return $class->new(
map { $_ => $Options->{$_}{default} }
grep !$Options->{$_}{shortcut},
(@_ ? @_ : keys %$Options)
);
}
sub new_from_cli {
@ -883,7 +948,7 @@ sub new_from_cli {
delete $args{$_} for grep !defined $args{$_}, keys %args;
for (qw(start end layer)) {
for (qw(start end layer toolchange)) {
my $opt_key = "${_}_gcode";
if ($args{$opt_key}) {
die "Invalid value for --${_}-gcode: file does not exist\n"
@ -967,8 +1032,12 @@ sub set {
}
if (!exists $Options->{$opt_key}) {
$opt_key = +(grep { $Options->{$_}{aliases} && grep $_ eq $opt_key, @{$Options->{$_}{aliases}} } keys %$Options)[0]
or warn "Unknown option $opt_key\n";
my @keys = grep { $Options->{$_}{aliases} && grep $_ eq $opt_key, @{$Options->{$_}{aliases}} } keys %$Options;
if (!@keys) {
warn "Unknown option $opt_key\n";
return;
}
$opt_key = $keys[0];
}
# clone arrayrefs
@ -979,6 +1048,10 @@ sub set {
if $deserialize && $Options->{$opt_key}{deserialize};
$self->{$opt_key} = $value;
if ($Options->{$opt_key}{shortcut}) {
$self->set($_, $value, $deserialize) for @{$Options->{$opt_key}{shortcut}};
}
}
sub set_ifndef {
@ -1010,6 +1083,7 @@ sub save {
my $ini = { _ => {} };
foreach my $opt_key (sort keys %$self) {
next if $Options->{$opt_key}{shortcut};
next if $Options->{$opt_key}{gui_only};
$ini->{_}{$opt_key} = $self->serialize($opt_key);
}
@ -1062,8 +1136,9 @@ sub validate {
if $self->perimeters < 0;
# --solid-layers
die "Invalid value for --solid-layers\n"
if $self->solid_layers < 0;
die "Invalid value for --solid-layers\n" if defined $self->solid_layers && $self->solid_layers < 0;
die "Invalid value for --top-solid-layers\n" if $self->top_solid_layers < 0;
die "Invalid value for --bottom-solid-layers\n" if $self->bottom_solid_layers < 0;
# --print-center
die "Invalid value for --print-center\n"
@ -1132,9 +1207,11 @@ sub replace_options {
my $self = shift;
my ($string, $more_variables) = @_;
if ($more_variables) {
my $variables = join '|', keys %$more_variables;
$string =~ s/\[($variables)\]/$more_variables->{$1}/eg;
$more_variables ||= {};
$more_variables->{$_} = $ENV{$_} for grep /^SLIC3R_/, keys %ENV;
{
my $variables_regex = join '|', keys %$more_variables;
$string =~ s/\[($variables_regex)\]/$more_variables->{$1}/eg;
}
my @lt = localtime; $lt[5] += 1900; $lt[4] += 1;
@ -1148,13 +1225,21 @@ sub replace_options {
$string =~ s/\[version\]/$Slic3r::VERSION/eg;
# build a regexp to match the available options
my $options = join '|',
grep !$Slic3r::Config::Options->{$_}{multiline},
my @options = grep !$Slic3r::Config::Options->{$_}{multiline},
grep $self->has($_),
keys %{$Slic3r::Config::Options};
my $options_regex = join '|', @options;
# use that regexp to search and replace option names with option values
$string =~ s/\[($options)\]/$self->serialize($1)/eg;
$string =~ s/\[($options_regex)\]/$self->serialize($1)/eg;
foreach my $opt_key (grep ref $self->$_ eq 'ARRAY', @options) {
my $value = $self->$opt_key;
$string =~ s/\[${opt_key}_${_}\]/$value->[$_]/eg for 0 .. $#$value;
if ($Options->{$opt_key}{type} eq 'point') {
$string =~ s/\[${opt_key}_X\]/$value->[0]/eg;
$string =~ s/\[${opt_key}_Y\]/$value->[1]/eg;
}
}
return $string;
}

View File

@ -6,7 +6,7 @@ use warnings;
use Boost::Geometry::Utils;
use Math::Geometry::Voronoi;
use Slic3r::Geometry qw(X Y A B point_in_polygon same_line line_length scale epsilon);
use Slic3r::Geometry qw(X Y A B point_in_polygon same_line line_length epsilon);
use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);
# the constructor accepts an array of polygons
@ -82,6 +82,13 @@ sub safety_offset {
);
}
sub noncollapsing_offset_ex {
my $self = shift;
my ($distance, @params) = @_;
return $self->offset_ex($distance + 1, @params);
}
sub encloses_point {
my $self = shift;
my ($point) = @_;
@ -137,6 +144,11 @@ sub bounding_box_polygon {
]);
}
sub bounding_box_center {
my $self = shift;
return Slic3r::Geometry::bounding_box_center($self->contour);
}
sub clip_line {
my $self = shift;
my ($line) = @_; # line must be a Slic3r::Line object
@ -153,6 +165,11 @@ sub simplify {
$self;
}
sub scale {
my $self = shift;
$_->scale(@_) for @$self;
}
sub translate {
my $self = shift;
$_->translate(@_) for @$self;
@ -291,4 +308,34 @@ sub medial_axis {
return @result;
}
package Slic3r::ExPolygon::Collection;
use Moo;
use Slic3r::Geometry qw(X1 Y1);
has 'expolygons' => (is => 'ro', default => sub { [] });
sub clone {
my $self = shift;
return (ref $self)->new(
expolygons => [ map $_->clone, @{$self->expolygons} ],
);
}
sub align_to_origin {
my $self = shift;
my @bb = Slic3r::Geometry::bounding_box([ map @$_, map @$_, @{$self->expolygons} ]);
$_->translate(-$bb[X1], -$bb[Y1]) for @{$self->expolygons};
}
sub rotate {
my $self = shift;
$_->rotate(@_) for @{$self->expolygons};
}
sub size {
my $self = shift;
return [ Slic3r::Geometry::size_2D([ map @$_, map @$_, @{$self->expolygons} ]) ];
}
1;

View File

@ -14,6 +14,7 @@ has 'id' => (is => 'rw', required => 1);
has $_ => (is => 'ro', required => 1) for @{&OPTIONS};
has 'retracted' => (is => 'rw', default => sub {0} );
has 'restart_extra' => (is => 'rw', default => sub {0} );
has 'e_per_mm3' => (is => 'lazy');
has 'retract_speed_mm_min' => (is => 'lazy');
has '_mm3_per_mm_cache' => (is => 'ro', default => sub {{}});
@ -55,4 +56,10 @@ sub mm3_per_mm {
return $self->_mm3_per_mm_cache->{$cache_key};
}
sub e_per_mm {
my $self = shift;
my ($s, $h) = @_;
return $self->mm3_per_mm($s, $h) * $self->e_per_mm3;
}
1;

View File

@ -10,7 +10,7 @@ has 'polygon' => (
handles => [qw(is_printable nearest_point_index_to reverse)],
);
has 'flow_spacing' => (is => 'rw');
has 'flow_spacing' => (is => 'rw', required => 1);
# see EXTR_ROLE_* constants in ExtrusionPath.pm
has 'role' => (is => 'rw', required => 1);
@ -37,36 +37,22 @@ sub pack {
sub split_at_index {
my $self = shift;
my ($index) = @_;
my @new_points = ();
push @new_points, @{$self->polygon}[$index .. $#{$self->polygon}];
push @new_points, @{$self->polygon}[0 .. $index];
return Slic3r::ExtrusionPath->new(
polyline => Slic3r::Polyline->new(\@new_points),
role => $self->role,
flow_spacing => $self->flow_spacing,
polyline => $self->polygon->split_at_index(@_),
role => $self->role,
flow_spacing => $self->flow_spacing,
);
}
sub split_at {
my $self = shift;
my ($point) = @_;
$point = Slic3r::Point->new($point);
# find index of point
my $i = -1;
for (my $n = 0; $n <= $#{$self->polygon}; $n++) {
if (same_point($point, $self->polygon->[$n])) {
$i = $n;
last;
}
}
die "Point not found" if $i == -1;
return $self->split_at_index($i);
return Slic3r::ExtrusionPath->new(
polyline => $self->polygon->split_at(@_),
role => $self->role,
flow_spacing => $self->flow_spacing,
);
}
sub split_at_first_point {
@ -74,20 +60,6 @@ sub split_at_first_point {
return $self->split_at_index(0);
}
# although a loop doesn't have endpoints, this method is provided to allow
# ExtrusionLoop objects to be added to an ExtrusionPath::Collection and
# sorted by the ->shortest_path() method
sub endpoints {
my $self = shift;
return ($self->polygon->[0], $self->polygon->[-1]);
}
# provided for ExtrusionPath::Collection->shortest_path()
sub points {
my $self = shift;
return $self->polygon;
}
package Slic3r::ExtrusionLoop::Packed;
sub unpack {
my $self = shift;

View File

@ -3,10 +3,10 @@ use Moo;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(EXTR_ROLE_PERIMETER EXTR_ROLE_SMALLPERIMETER EXTR_ROLE_EXTERNAL_PERIMETER
our @EXPORT_OK = qw(EXTR_ROLE_PERIMETER EXTR_ROLE_EXTERNAL_PERIMETER
EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER
EXTR_ROLE_FILL EXTR_ROLE_SOLIDFILL EXTR_ROLE_TOPSOLIDFILL EXTR_ROLE_BRIDGE EXTR_ROLE_SKIRT
EXTR_ROLE_SUPPORTMATERIAL);
EXTR_ROLE_SUPPORTMATERIAL EXTR_ROLE_GAPFILL);
our %EXPORT_TAGS = (roles => \@EXPORT_OK);
use Slic3r::Geometry qw(PI X Y epsilon deg2rad rotate_points);
@ -15,16 +15,15 @@ use Slic3r::Geometry qw(PI X Y epsilon deg2rad rotate_points);
has 'polyline' => (
is => 'rw',
required => 1,
handles => [qw(merge_continuous_lines lines length reverse)],
handles => [qw(merge_continuous_lines lines length reverse clip_end simplify)],
);
# depth_layers is the vertical thickness of the extrusion expressed in layers
has 'depth_layers' => (is => 'ro', default => sub {1});
# height is the vertical thickness of the extrusion expressed in mm
has 'height' => (is => 'rw');
has 'flow_spacing' => (is => 'rw');
has 'role' => (is => 'rw', required => 1);
use constant EXTR_ROLE_PERIMETER => 0;
use constant EXTR_ROLE_SMALLPERIMETER => 1;
use constant EXTR_ROLE_EXTERNAL_PERIMETER => 2;
use constant EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER => 3;
use constant EXTR_ROLE_FILL => 4;
@ -33,8 +32,9 @@ use constant EXTR_ROLE_TOPSOLIDFILL => 6;
use constant EXTR_ROLE_BRIDGE => 7;
use constant EXTR_ROLE_SKIRT => 8;
use constant EXTR_ROLE_SUPPORTMATERIAL => 9;
use constant EXTR_ROLE_GAPFILL => 10;
use constant PACK_FMT => 'cfca*';
use constant PACK_FMT => 'ffca*';
# class or object method
sub pack {
@ -42,11 +42,11 @@ sub pack {
my %args = @_;
if (ref $self) {
%args = map { $_ => $self->$_ } qw(depth_layers flow_spacing role polyline);
%args = map { $_ => $self->$_ } qw(height flow_spacing role polyline);
}
my $o = \ pack PACK_FMT,
$args{depth_layers} || 1,
$args{height} // -1,
$args{flow_spacing} || -1,
$args{role} // (die "Missing mandatory attribute 'role'"), #/
$args{polyline}->serialize;
@ -58,26 +58,6 @@ sub pack {
# no-op, this allows to use both packed and non-packed objects in Collections
sub unpack { $_[0] }
sub clip_end {
my $self = shift;
my ($distance) = @_;
while ($distance > 0) {
my $last_point = pop @{$self->points};
last if !@{$self->points};
my $last_segment_length = $last_point->distance_to($self->points->[-1]);
if ($last_segment_length <= $distance) {
$distance -= $last_segment_length;
next;
}
my $new_point = Slic3r::Geometry::point_along_segment($last_point, $self->points->[-1], $distance);
push @{$self->points}, Slic3r::Point->new($new_point);
$distance = 0;
}
}
sub clip_with_polygon {
my $self = shift;
my ($polygon) = @_;
@ -93,7 +73,7 @@ sub clip_with_expolygon {
foreach my $polyline ($self->polyline->clip_with_expolygon($expolygon)) {
push @paths, (ref $self)->new(
polyline => $polyline,
depth_layers => $self->depth_layers,
height => $self->height,
flow_spacing => $self->flow_spacing,
role => $self->role,
);
@ -137,7 +117,7 @@ sub split_at_acute_angles {
push @paths, (ref $self)->new(
polyline => Slic3r::Polyline->new(\@p),
role => $self->role,
depth_layers => $self->depth_layers,
height => $self->height,
);
@p = ($p3);
push @p, grep $_, shift @points or last;
@ -148,7 +128,7 @@ sub split_at_acute_angles {
push @paths, (ref $self)->new(
polyline => Slic3r::Polyline->new(\@p),
role => $self->role,
depth_layers => $self->depth_layers,
height => $self->height,
) if @p > 1;
return @paths;
@ -246,7 +226,7 @@ sub detect_arcs {
push @paths, (ref $self)->new(
polyline => Slic3r::Polyline->new(@points[0..$i]),
role => $self->role,
depth_layers => $self->depth_layers,
height => $self->height,
) if $i > 0;
# add our arc
@ -265,7 +245,7 @@ sub detect_arcs {
push @paths, (ref $self)->new(
polyline => Slic3r::Polyline->new(\@points),
role => $self->role,
depth_layers => $self->depth_layers,
height => $self->height,
) if @points > 1;
return @paths;
@ -275,11 +255,11 @@ package Slic3r::ExtrusionPath::Packed;
sub unpack {
my $self = shift;
my ($depth_layers, $flow_spacing, $role, $polyline_s)
my ($height, $flow_spacing, $role, $polyline_s)
= unpack Slic3r::ExtrusionPath::PACK_FMT, $$self;
return Slic3r::ExtrusionPath->new(
depth_layers => $depth_layers,
height => ($height == -1) ? undef : $height,
flow_spacing => ($flow_spacing == -1) ? undef : $flow_spacing,
role => $role,
polyline => Slic3r::Polyline->deserialize($polyline_s),

View File

@ -12,26 +12,15 @@ sub shortest_path {
my $self = shift;
my ($start_near) = @_;
my @my_paths = map $_->unpack, @{$self->paths};
# make sure we pass the same path objects to the Collection constructor
# and the ->shortest_path() method because the latter will reverse the
# paths in-place when needed and we need to return them that way
my @paths = map $_->unpack, @{$self->paths};
my $collection = Slic3r::Polyline::Collection->new(
polylines => [ map $_->polyline, @paths ],
);
my @paths = ();
my $start_at;
my $endpoints = [ map $_->endpoints, @my_paths ];
while (@my_paths) {
# find nearest point
my $start_index = $start_near
? Slic3r::Geometry::nearest_point_index($start_near, $endpoints)
: 0;
my $path_index = int($start_index/2);
if ($start_index%2) { # index is end so reverse to make it the start
$my_paths[$path_index]->reverse;
}
push @paths, splice @my_paths, $path_index, 1;
splice @$endpoints, $path_index*2, 2;
$start_near = $paths[-1]->points->[-1];
}
return @paths;
return $collection->shortest_path($start_near, \@paths);
}
sub cleanup {

View File

@ -12,13 +12,12 @@ use Slic3r::Fill::OctagramSpiral;
use Slic3r::Fill::PlanePath;
use Slic3r::Fill::Rectilinear;
use Slic3r::ExtrusionPath ':roles';
use Slic3r::Geometry qw(X Y scale shortest_path);
use Slic3r::Geometry qw(X Y PI scale shortest_path);
use Slic3r::Geometry::Clipper qw(union_ex diff_ex);
use Slic3r::Surface ':types';
has 'print' => (is => 'ro', required => 1);
has 'max_print_dimension' => (is => 'rw');
has 'fillers' => (is => 'rw', default => sub { {} });
our %FillTypes = (
@ -32,22 +31,17 @@ our %FillTypes = (
honeycomb => 'Slic3r::Fill::Honeycomb',
);
sub BUILD {
my $self = shift;
my $print_size = $self->print->size;
my $max_print_dimension = ($print_size->[X] > $print_size->[Y] ? $print_size->[X] : $print_size->[Y]) * sqrt(2);
$self->max_print_dimension($max_print_dimension);
$self->filler($_) for ('rectilinear', $Slic3r::Config->fill_pattern, $Slic3r::Config->solid_fill_pattern);
}
sub filler {
my $self = shift;
my ($filler) = @_;
if (!ref $self) {
return $FillTypes{$filler}->new;
}
if (!$self->fillers->{$filler}) {
$self->fillers->{$filler} = $FillTypes{$filler}->new(print => $self->print);
$self->fillers->{$filler}->max_print_dimension($self->max_print_dimension);
my $f = $self->fillers->{$filler} = $FillTypes{$filler}->new;
$f->bounding_box([ $self->print->bounding_box ]) if $f->can('bounding_box');
}
return $self->fillers->{$filler};
}
@ -56,8 +50,6 @@ sub make_fill {
my $self = shift;
my ($layer) = @_;
$_->layer($layer) for values %{$self->fillers};
Slic3r::debugf "Filling layer %d:\n", $layer->id;
# merge overlapping surfaces
@ -143,7 +135,7 @@ sub make_fill {
$filler = $Slic3r::Config->solid_fill_pattern;
if ($is_bridge) {
$filler = 'rectilinear';
$flow_spacing = sqrt($Slic3r::Config->bridge_flow_ratio * ($layer->infill_flow->nozzle_diameter**2));
$flow_spacing = $layer->infill_flow->bridge_spacing;
} elsif ($surface->surface_type == S_TYPE_INTERNALSOLID) {
$filler = 'rectilinear';
}
@ -151,13 +143,22 @@ sub make_fill {
next SURFACE unless $density > 0;
}
my @paths = $self->fillers->{$filler}->fill_surface(
$surface,
density => $density,
flow_spacing => $flow_spacing,
);
my @paths;
{
my $f = $self->filler($filler);
$f->layer_id($layer->id);
@paths = $f->fill_surface(
$surface,
density => $density,
flow_spacing => $flow_spacing,
dont_adjust => $is_bridge,
);
}
my $params = shift @paths;
# ugly hack(tm) to get the right amount of flow (GCode.pm should be fixed)
$params->{flow_spacing} = $layer->infill_flow->bridge_width if $is_bridge;
# save into layer
next unless @paths;
push @fills, Slic3r::ExtrusionPath::Collection->new(
@ -169,7 +170,7 @@ sub make_fill {
: $is_solid
? ($surface->surface_type == S_TYPE_TOP ? EXTR_ROLE_TOPSOLIDFILL : EXTR_ROLE_SOLIDFILL)
: EXTR_ROLE_FILL),
depth_layers => $surface->depth_layers,
height => $surface->depth_layers * $layer->height,
flow_spacing => $params->{flow_spacing} || (warn "Warning: no flow_spacing was returned by the infill engine, please report this to the developer\n"),
), @paths,
],
@ -178,18 +179,8 @@ sub make_fill {
}
# add thin fill regions
{
my %args = (
role => EXTR_ROLE_SOLIDFILL,
flow_spacing => $layer->perimeter_flow->spacing,
);
push @fills, map {
$_->isa('Slic3r::Polygon')
? (map $_->pack, Slic3r::ExtrusionLoop->new(polygon => $_, %args)->split_at_first_point)
: Slic3r::ExtrusionPath->pack(polyline => $_, %args),
} @{$layer->thin_fills};
}
push @fills_ordering_points, map $_->[0], @{$layer->thin_fills};
push @fills, @{$layer->thin_fills};
push @fills_ordering_points, map $_->unpack->points->[0], @{$layer->thin_fills};
# organize infill paths using a shortest path search
@fills = @{shortest_path([

View File

@ -1,14 +1,11 @@
package Slic3r::Fill::Base;
use Moo;
use Slic3r::Geometry qw(PI);
has 'print' => (is => 'rw');
has 'layer' => (is => 'rw');
has 'max_print_dimension' => (is => 'rw');
has 'layer_id' => (is => 'rw');
has 'angle' => (is => 'rw', default => sub { $Slic3r::Config->fill_angle });
use constant PI => 4 * atan2(1, 1);
sub angles () { [0, PI/2] }
sub infill_direction {
@ -18,12 +15,12 @@ sub infill_direction {
# set infill angle
my (@rotate, @shift);
$rotate[0] = Slic3r::Geometry::deg2rad($self->angle);
$rotate[1] = [ $self->max_print_dimension * sqrt(2) / 2, $self->max_print_dimension * sqrt(2) / 2 ];
$rotate[1] = $surface->expolygon->bounding_box_center;
@shift = @{$rotate[1]};
if ($self->layer) {
if (defined $self->layer_id) {
# alternate fill direction
my $layer_num = $self->layer->id / $surface->depth_layers;
my $layer_num = $self->layer_id / $surface->depth_layers;
my $angle = $self->angles->[$layer_num % @{$self->angles}];
$rotate[0] = Slic3r::Geometry::deg2rad($self->angle) + $angle if $angle;
}

View File

@ -19,7 +19,7 @@ sub fill_surface {
my $distance = $min_spacing / $params{density};
my $flow_spacing = $params{flow_spacing};
if ($params{density} == 1) {
if ($params{density} == 1 && !$params{dont_adjust}) {
$distance = $self->adjust_solid_spacing(
width => $bounding_box->[X2] - $bounding_box->[X1],
distance => $distance,
@ -51,21 +51,21 @@ sub fill_surface {
($bounding_box->[X1] + $bounding_box->[X2]) / 2,
($bounding_box->[Y1] + $bounding_box->[Y2]) / 2,
);
foreach my $loop (map Slic3r::ExtrusionLoop->new(polygon => $_, role => EXTR_ROLE_FILL), @loops) {
foreach my $loop (@loops) {
# extrude all loops ccw
$loop->polygon->make_counter_clockwise;
$loop->make_counter_clockwise;
# find the point of the loop that is closest to the current extruder position
my $index = $loop->nearest_point_index_to($cur_pos);
$cur_pos = $loop->polygon->[0];
$cur_pos = $loop->[0];
# split the loop at the starting point and make a path
my $path = $loop->split_at_index($index);
# clip the path to avoid the extruder to get exactly on the first point of the loop
$path->clip_end(($self->layer ? $self->layer->flow->scaled_width : $Slic3r::flow->scaled_width) * 0.15);
$path->clip_end(scale $flow_spacing * &Slic3r::LOOP_CLIPPING_LENGTH_OVER_SPACING);
push @paths, $path->points if @{$path->points};
push @paths, $path if @$path;
}
return { flow_spacing => $flow_spacing }, @paths;

View File

@ -3,7 +3,8 @@ use Moo;
extends 'Slic3r::Fill::Base';
has 'cache' => (is => 'rw', default => sub {{}});
has 'bounding_box' => (is => 'rw');
has 'cache' => (is => 'rw', default => sub {{}});
use Slic3r::Geometry qw(PI X1 Y1 X2 Y2 X Y scale);
use Slic3r::Geometry::Clipper qw(intersection_ex);
@ -21,7 +22,7 @@ sub fill_surface {
# infill math
my $min_spacing = scale $params{flow_spacing};
my $distance = $min_spacing / $params{density};
my $overlap_distance = ($self->layer ? $self->layer->flow->scaled_width : $Slic3r::flow->scaled_width) * 0.4;
my $overlap_distance = scale $params{flow_spacing} * &Slic3r::PERIMETER_INFILL_OVERLAP_OVER_SPACING;
my $cache_id = sprintf "d%s_s%s_a%s",
$params{density}, $params{flow_spacing}, $rotate_vector->[0][0];
@ -39,8 +40,8 @@ sub fill_surface {
# adjust actual bounding box to the nearest multiple of our hex pattern
# and align it so that it matches across layers
my $print_bounding_box = [ $self->print->bounding_box ];
my $bounding_box = [ 0, 0, $print_bounding_box->[X2], $print_bounding_box->[Y2] ];
$self->bounding_box([ $expolygon->bounding_box ]) if !defined $self->bounding_box;
my $bounding_box = [ 0, 0, $self->bounding_box->[X2], $self->bounding_box->[Y2] ];
{
my $bb_polygon = Slic3r::Polygon->new([
[ $bounding_box->[X1], $bounding_box->[Y1] ],
@ -90,12 +91,9 @@ sub fill_surface {
$self->cache->{$cache_id},
[ map @$_, $expolygon->offset_ex($overlap_distance) ],
)};
my $collection = Slic3r::ExtrusionPath::Collection->new(
paths => [ map Slic3r::ExtrusionPath->pack(polyline => $_, role => -1), @paths ],
);
return { flow_spacing => $params{flow_spacing} },
map $_->polyline, $collection->shortest_path;
Slic3r::Polyline::Collection->new(polylines => \@paths)->shortest_path;
}
1;

View File

@ -3,7 +3,7 @@ use Moo;
extends 'Slic3r::Fill::Base';
use Slic3r::Geometry qw(scale bounding_box X1 Y1 X2 Y2);
use Slic3r::Geometry qw(scale X1 Y1 X2 Y2);
sub multiplier () { 1 }
@ -27,7 +27,7 @@ sub fill_surface {
$self->rotate_points($expolygon, $rotate_vector);
my $distance_between_lines = scale $params{flow_spacing} / $params{density} * $self->multiplier;
my $bounding_box = [ bounding_box(map @$_, $expolygon) ];
my $bounding_box = [ Slic3r::Geometry::bounding_box([map @$_, @$expolygon]) ];
my $bounding_box_polygon = Slic3r::Polygon->new([
[ $bounding_box->[X1], $bounding_box->[Y1] ],
[ $bounding_box->[X2], $bounding_box->[Y1] ],
@ -51,7 +51,7 @@ sub fill_surface {
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "fill.svg",
Slic3r::SVG::output("fill.svg",
polygons => $expolygon,
polylines => [map $_->p, @paths],
);

View File

@ -23,7 +23,7 @@ sub fill_surface {
my $line_oscillation = $distance_between_lines - $min_spacing;
my $flow_spacing = $params{flow_spacing};
if ($params{density} == 1) {
if ($params{density} == 1 && !$params{dont_adjust}) {
$distance_between_lines = $self->adjust_solid_spacing(
width => $bounding_box->[X2] - $bounding_box->[X1],
distance => $distance_between_lines,
@ -31,7 +31,7 @@ sub fill_surface {
$flow_spacing = unscale $distance_between_lines;
}
my $overlap_distance = ($self->layer ? $self->layer->flow->scaled_width : $Slic3r::flow->scaled_width) * 0.4;
my $overlap_distance = scale $params{flow_spacing} * &Slic3r::PERIMETER_INFILL_OVERLAP_OVER_SPACING;
my $x = $bounding_box->[X1];
my $is_line_pattern = $self->isa('Slic3r::Fill::Line');
@ -58,9 +58,9 @@ sub fill_surface {
}
# connect lines
{
my $collection = Slic3r::ExtrusionPath::Collection->new(
paths => [ map Slic3r::ExtrusionPath->new(polyline => Slic3r::Polyline->new(@$_), role => -1), @paths ],
unless ($params{dont_connect}) {
my $collection = Slic3r::Polyline::Collection->new(
polylines => [ map Slic3r::Polyline->new(@$_), @paths ],
);
@paths = ();
@ -75,17 +75,17 @@ sub fill_surface {
foreach my $path ($collection->shortest_path) {
if (@paths) {
my @distance = map abs($path->points->[0][$_] - $paths[-1][-1][$_]), (X,Y);
my @distance = map abs($path->[0][$_] - $paths[-1][-1][$_]), (X,Y);
# 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, $paths[-1][-1], $path->points->[0])
&& $expolygon_off->encloses_line(Slic3r::Line->new($paths[-1][-1], $path->points->[0]), $tolerance)) {
push @{$paths[-1]}, @{$path->points};
if ($can_connect->(@distance, $paths[-1][-1], $path->[0])
&& $expolygon_off->encloses_line(Slic3r::Line->new($paths[-1][-1], $path->[0]), $tolerance)) {
push @{$paths[-1]}, @$path;
next;
}
}
push @paths, $path->points;
push @paths, $path;
}
}

View File

@ -1,6 +1,7 @@
package Slic3r::Flow;
use Moo;
use List::Util qw(max);
use Slic3r::Geometry qw(PI scale);
has 'nozzle_diameter' => (is => 'ro', required => 1);
@ -8,6 +9,8 @@ has 'layer_height' => (is => 'ro', default => sub { $Slic3r::Config->layer_
has 'width' => (is => 'rwp', builder => 1);
has 'spacing' => (is => 'lazy');
has 'bridge_width' => (is => 'lazy');
has 'bridge_spacing' => (is => 'lazy');
has 'scaled_width' => (is => 'lazy');
has 'scaled_spacing' => (is => 'lazy');
@ -35,8 +38,11 @@ sub _build_width {
$width = $self->nozzle_diameter * ($self->nozzle_diameter/$self->layer_height - 4/PI + 1);
}
my $min = $self->nozzle_diameter * 1.05;
my $max = $self->nozzle_diameter * 1.4;
my $min = max(
((($self->nozzle_diameter/2) ** 2) / $self->layer_height * 0.8),
($self->nozzle_diameter * 1.05),
);
my $max = $self->nozzle_diameter * 1.6;
$width = $max if $width > $max;
$width = $min if $width < $min;
@ -57,6 +63,27 @@ sub _build_spacing {
return $self->width - &Slic3r::OVERLAP_FACTOR * ($self->width - $min_flow_spacing);
}
sub clone {
my $self = shift;
return (ref $self)->new(
nozzle_diameter => $self->nozzle_diameter,
layer_height => $self->layer_height,
@_,
);
}
sub _build_bridge_width {
my $self = shift;
return sqrt($Slic3r::Config->bridge_flow_ratio * ($self->nozzle_diameter**2));
}
sub _build_bridge_spacing {
my $self = shift;
my $width = $self->bridge_width;
return $width + &Slic3r::OVERLAP_FACTOR * ($width * PI / 4 - $width);
}
sub _build_scaled_width {
my $self = shift;
return scale $self->width;

View File

@ -7,14 +7,17 @@ sub read_file {
my $self = shift;
my ($file) = @_;
eval "require Slic3r::Format::AMF::Parser; 1"
or die "AMF parsing requires XML::SAX\n";
eval qq{
require Slic3r::Format::AMF::Parser;
use XML::SAX::ParserFactory;
1;
} or die "AMF parsing requires XML::SAX\n";
open my $fh, '<', $file or die "Failed to open $file\n";
my $model = Slic3r::Model->new;
XML::SAX::PurePerl
->new(Handler => Slic3r::Format::AMF::Parser->new(_model => $model))
XML::SAX::ParserFactory
->parser(Handler => Slic3r::Format::AMF::Parser->new(_model => $model))
->parse_file($fh);
close $fh;

View File

@ -2,8 +2,6 @@ package Slic3r::Format::AMF::Parser;
use strict;
use warnings;
use XML::SAX::PurePerl;
use base 'XML::SAX::Base';
my %xyz_index = (x => 0, y => 1, z => 2); #=

View File

@ -41,77 +41,14 @@ sub read_file {
my $vertices = [];
{
my %vertices_map = (); # given a vertex's coordinates, what's its index?
my @vertices_facets = (); # given a vertex index, what are the indexes of its tangent facets?
for (my $f = 0; $f <= $#$facets; $f++) {
for (-3..-1) {
my $point_id = join ',', @{$facets->[$f][$_]};
if (exists $vertices_map{$point_id}) {
$facets->[$f][$_] = $vertices_map{$point_id};
### push @{$vertices_facets[$facets->[$f][$_]]}, $f;
} else {
push @$vertices, $facets->[$f][$_];
$facets->[$f][$_] = $vertices_map{$point_id} = $#$vertices;
### $vertices_facets[$#$vertices] = [$f];
}
}
}
# The following loop checks that @vertices_facets only groups facets that
# are really connected together (i.e. neighbors or sharing neighbors);
# in other words it takes care of multiple vertices occupying the same
# point in space. It enforces topological correctness which is needed by
# the slicing algorithm.
# I'm keeping it disabled until I find a good test case.
# The two lines above commented out with '###' need to be
# uncommented for this to work.
if (0) {
my $vertices_count = $#$vertices; # store it to avoid processing newly created vertices
for (my $v = 0; $v <= $vertices_count; $v++) {
my $more_than_one_vertex_in_this_point = 0;
while (@{$vertices_facets[$v]}) {
my @facets_indexes = @{$vertices_facets[$v]};
@{$vertices_facets[$v]} = ();
my @this_f = shift @facets_indexes;
CYCLE: while (@facets_indexes && @this_f) {
# look for a facet that is connected to $this_f[-1] and whose common line contains $v
my @other_vertices_indexes = grep $_ != $v, @{$facets->[$this_f[-1]]}[-3..-1];
OTHER: for my $other_f (@facets_indexes) {
# facet is connected if it shares one more point
for (grep $_ != $v, @{$facets->[$other_f]}[-3..-1]) {
if ($_ ~~ @other_vertices_indexes) {
#printf "facet %d is connected to $other_f (sharing vertices $v and $_)\n", $this_f[-1];
# TODO: we should ensure that the common edge has a different orientation
# for each of the two adjacent facets
push @this_f, $other_f;
@facets_indexes = grep $_ != $other_f, @facets_indexes;
next CYCLE;
}
}
}
# if we're here, then we couldn't find any facet connected to $this_f[-1]
# so we should move this one to a different cluster (that is, a new vertex)
# (or ignore it if it turns to be a non-manifold facet)
if (@this_f > 1) {
push @{$vertices_facets[$v]}, $this_f[-1];
pop @this_f;
$more_than_one_vertex_in_this_point++;
} else {
last CYCLE;
}
}
if ($more_than_one_vertex_in_this_point) {
Slic3r::debugf " more than one vertex in the same point\n";
push @$vertices, $vertices->[$v];
for my $f (@this_f) {
$facets->[$f][$_] = $#$vertices for grep $facets->[$f][$_] == $v, -3..-1;
}
}
}
}
}

View File

@ -1,16 +1,17 @@
package Slic3r::GCode;
use Moo;
use List::Util qw(first);
use List::Util qw(max first);
use Slic3r::ExtrusionPath ':roles';
use Slic3r::Geometry qw(scale unscale scaled_epsilon points_coincide PI X Y);
use Slic3r::Geometry qw(scale unscale scaled_epsilon points_coincide PI X Y B);
use Slic3r::Geometry::Clipper qw(union_ex);
has 'multiple_extruders' => (is => 'ro', default => sub {0} );
has 'layer' => (is => 'rw');
has 'move_z_callback' => (is => 'rw');
has 'shift_x' => (is => 'rw', default => sub {0} );
has 'shift_y' => (is => 'rw', default => sub {0} );
has 'z' => (is => 'rw', default => sub {0} );
has 'z' => (is => 'rw');
has 'speed' => (is => 'rw');
has 'external_mp' => (is => 'rw');
@ -24,27 +25,27 @@ has 'total_extrusion_length' => (is => 'rw', default => sub {0} );
has 'lifted' => (is => 'rw', default => sub {0} );
has 'last_pos' => (is => 'rw', default => sub { Slic3r::Point->new(0,0) } );
has 'last_speed' => (is => 'rw', default => sub {""});
has 'last_f' => (is => 'rw', default => sub {""});
has 'last_fan_speed' => (is => 'rw', default => sub {0});
has 'dec' => (is => 'ro', default => sub { 3 } );
# used for vibration limit:
has 'last_dir' => (is => 'ro', default => sub { [0,0] });
has 'dir_time' => (is => 'ro', default => sub { [0,0] });
# calculate speeds (mm/min)
has 'speeds' => (
is => 'ro',
default => sub {{
travel => 60 * $Slic3r::Config->get_value('travel_speed'),
perimeter => 60 * $Slic3r::Config->get_value('perimeter_speed'),
small_perimeter => 60 * $Slic3r::Config->get_value('small_perimeter_speed'),
external_perimeter => 60 * $Slic3r::Config->get_value('external_perimeter_speed'),
infill => 60 * $Slic3r::Config->get_value('infill_speed'),
solid_infill => 60 * $Slic3r::Config->get_value('solid_infill_speed'),
top_solid_infill => 60 * $Slic3r::Config->get_value('top_solid_infill_speed'),
bridge => 60 * $Slic3r::Config->get_value('bridge_speed'),
default => sub {+{
map { $_ => 60 * $Slic3r::Config->get_value("${_}_speed") }
qw(travel perimeter small_perimeter external_perimeter infill
solid_infill top_solid_infill support_material bridge gap_fill),
}},
);
# assign speeds to roles
my %role_speeds = (
&EXTR_ROLE_PERIMETER => 'perimeter',
&EXTR_ROLE_SMALLPERIMETER => 'small_perimeter',
&EXTR_ROLE_EXTERNAL_PERIMETER => 'external_perimeter',
&EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER => 'perimeter',
&EXTR_ROLE_FILL => 'infill',
@ -52,18 +53,18 @@ my %role_speeds = (
&EXTR_ROLE_TOPSOLIDFILL => 'top_solid_infill',
&EXTR_ROLE_BRIDGE => 'bridge',
&EXTR_ROLE_SKIRT => 'perimeter',
&EXTR_ROLE_SUPPORTMATERIAL => 'perimeter',
&EXTR_ROLE_SUPPORTMATERIAL => 'support_material',
&EXTR_ROLE_GAPFILL => 'gap_fill',
);
sub set_shift {
my $self = shift;
my @shift = @_;
# adjust last position
$self->last_pos($self->last_pos->clone->translate(
scale($self->shift_x - $shift[X]),
scale($self->shift_y - $shift[Y]),
));
$self->last_pos->translate(
scale ($shift[X] - $self->shift_x),
scale ($shift[Y] - $self->shift_y),
);
$self->shift_x($shift[X]);
$self->shift_y($shift[Y]);
@ -79,16 +80,25 @@ sub change_layer {
islands => union_ex([ map @$_, @{$layer->slices} ], undef, 1),
));
}
my $z = $Slic3r::Config->z_offset + $layer->print_z * &Slic3r::SCALING_FACTOR;
}
# this method accepts Z in scaled coordinates
sub move_z {
my $self = shift;
my ($z, $comment) = @_;
$z *= &Slic3r::SCALING_FACTOR;
$z += $Slic3r::Config->z_offset;
my $gcode = "";
$gcode .= $self->retract(move_z => $z);
$gcode .= $self->G0(undef, $z, 0, 'move to next layer (' . $layer->id . ')')
if $self->z != $z && !$self->lifted;
$gcode .= $Slic3r::Config->replace_options($Slic3r::Config->layer_gcode) . "\n"
if $Slic3r::Config->layer_gcode;
my $current_z = $self->z;
if (!defined $current_z || $current_z != ($z + $self->lifted)) {
$gcode .= $self->retract(move_z => $z);
$self->speed('travel');
$gcode .= $self->G0(undef, $z, 0, $comment || ('move to next layer (' . $self->layer->id . ')'))
unless ($current_z // -1) != ($self->z // -1);
$gcode .= $self->move_z_callback->() if defined $self->move_z_callback;
}
return $gcode;
}
@ -107,7 +117,7 @@ sub extrude_loop {
# extrude all loops ccw
$loop = $loop->unpack if $loop->isa('Slic3r::ExtrusionLoop::Packed');
$loop->polygon->make_counter_clockwise;
my $was_clockwise = $loop->polygon->make_counter_clockwise;
# find the point of the loop that is closest to the current extruder position
# or randomize if requested
@ -124,11 +134,31 @@ sub extrude_loop {
# clip the path to avoid the extruder to get exactly on the first point of the loop;
# if polyline was shorter than the clipping distance we'd get a null polyline, so
# we discard it in that case
$extrusion_path->clip_end($self->layer ? $self->layer->flow->scaled_width : $Slic3r::flow->scaled_width * 0.15);
$extrusion_path->clip_end(scale $extrusion_path->flow_spacing * &Slic3r::LOOP_CLIPPING_LENGTH_OVER_SPACING);
return '' if !@{$extrusion_path->polyline};
# extrude along the path
return $self->extrude_path($extrusion_path, $description);
my $gcode = $self->extrude_path($extrusion_path, $description);
# make a little move inwards before leaving loop
if ($loop->role == EXTR_ROLE_EXTERNAL_PERIMETER) {
# detect angle between last and first segment
# the side depends on the original winding order of the polygon (left for contours, right for holes)
my @points = $was_clockwise ? (-2, 1) : (1, -2);
my $angle = Slic3r::Geometry::angle3points(@{$extrusion_path->polyline}[0, @points]) / 3;
$angle *= -1 if $was_clockwise;
# create the destination point along the first segment and rotate it
my $point = Slic3r::Geometry::point_along_segment(@{$extrusion_path->polyline}[0,1], scale $extrusion_path->flow_spacing);
bless $point, 'Slic3r::Point';
$point->rotate($angle, $extrusion_path->polyline->[0]);
# generate the travel move
$self->speed('travel');
$gcode .= $self->G0($point, undef, 0, "move inwards before travel");
}
return $gcode;
}
sub extrude_path {
@ -136,7 +166,7 @@ sub extrude_path {
my ($path, $description, $recursive) = @_;
$path = $path->unpack if $path->isa('Slic3r::ExtrusionPath::Packed');
$path->merge_continuous_lines;
$path->simplify(&Slic3r::SCALED_RESOLUTION);
# detect arcs
if ($Slic3r::Config->gcode_arcs && !$recursive) {
@ -149,11 +179,16 @@ sub extrude_path {
my $gcode = "";
# retract if distance from previous position is greater or equal to the one
# specified by the user
# skip retract for support material
{
my $travel = Slic3r::Line->new($self->last_pos, $path->points->[0]);
if ($travel->length >= scale $self->extruder->retract_before_travel) {
# retract if distance from previous position is greater or equal to the one specified by the user
my $travel = Slic3r::Line->new($self->last_pos->clone, $path->points->[0]->clone);
if ($travel->length >= scale $self->extruder->retract_before_travel
&& ($path->role != EXTR_ROLE_SUPPORTMATERIAL || !$self->layer->support_islands_enclose_line($travel))) {
# move travel back to original layer coordinates.
# note that we're only considering the current object's islands, while we should
# build a more complete configuration space
$travel->translate(-$self->shift_x, -$self->shift_y);
if (!$Slic3r::Config->only_retract_when_crossing_perimeters || $path->role != EXTR_ROLE_FILL || !first { $_->encloses_line($travel, scaled_epsilon) } @{$self->layer->slices}) {
$gcode .= $self->retract(travel_to => $path->points->[0]);
}
@ -161,45 +196,54 @@ sub extrude_path {
}
# go to first point of extrusion path
$gcode .= $self->travel_to($path->points->[0], "move to first $description point");
$self->speed('travel');
$gcode .= $self->G0($path->points->[0], undef, 0, "move to first $description point")
if !points_coincide($self->last_pos, $path->points->[0]);
# compensate retraction
$gcode .= $self->unretract if $self->extruder->retracted;
$gcode .= $self->unretract;
my $area; # mm^3 of extrudate per mm of tool movement
if ($path->role == EXTR_ROLE_BRIDGE) {
my $s = $path->flow_spacing || $self->extruder->nozzle_diameter;
my $s = $path->flow_spacing;
$area = ($s**2) * PI/4;
} else {
my $s = $path->flow_spacing || ($self->layer ? $self->layer->flow->spacing : $Slic3r::flow->spacing);
my $h = $path->depth_layers * $self->layer->height;
my $s = $path->flow_spacing;
my $h = $path->height // $self->layer->height;
$area = $self->extruder->mm3_per_mm($s, $h);
}
# calculate extrusion length per distance unit
my $e = $self->extruder->e_per_mm3 * $area;
# extrude arc or line
# set speed
$self->speed( $role_speeds{$path->role} || die "Unknown role: " . $path->role );
if ($path->role == EXTR_ROLE_PERIMETER || $path->role == EXTR_ROLE_EXTERNAL_PERIMETER || $path->role == EXTR_ROLE_CONTOUR_INTERNAL_PERIMETER) {
if (abs($path->length) <= &Slic3r::SMALL_PERIMETER_LENGTH) {
$self->speed('small_perimeter');
}
}
# extrude arc or line
my $path_length = 0;
if ($path->isa('Slic3r::ExtrusionPath::Arc')) {
$path_length = $path->length;
$path_length = unscale $path->length;
$gcode .= $self->G2_G3($path->points->[-1], $path->orientation,
$path->center, $e * unscale $path_length, $description);
} else {
foreach my $line ($path->lines) {
my $line_length = $line->length;
my $line_length = unscale $line->length;
$path_length += $line_length;
$gcode .= $self->G1($line->b, undef, $e * unscale $line_length, $description);
$gcode .= $self->G1($line->[B], undef, $e * $line_length, $description);
}
}
if ($Slic3r::Config->cooling) {
my $path_time = unscale($path_length) / $self->speeds->{$self->last_speed} * 60;
my $path_time = $path_length / $self->speeds->{$self->last_speed} * 60;
if ($self->layer->id == 0) {
$path_time = $Slic3r::Config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/
? $path_time / ($1/100)
: unscale($path_length) / $Slic3r::Config->first_layer_speed * 60;
: $path_length / $Slic3r::Config->first_layer_speed * 60;
}
$self->elapsed_time($self->elapsed_time + $path_time);
}
@ -258,7 +302,7 @@ sub retract {
my $retract = [undef, undef, -$length, $comment];
my $lift = ($self->extruder->retract_lift == 0 || defined $params{move_z})
? undef
: [undef, $self->z + $self->extruder->retract_lift, 0, 'lift plate during retraction'];
: [undef, $self->z + $self->extruder->retract_lift, 0, 'lift plate during travel'];
my $gcode = "";
if (($Slic3r::Config->g0 || $Slic3r::Config->gcode_flavor eq 'mach3') && $params{travel_to}) {
@ -285,7 +329,8 @@ sub retract {
$gcode .= $self->G1(@$lift);
}
}
$self->extruder->retracted($self->extruder->retracted + $length + $restart_extra);
$self->extruder->retracted($self->extruder->retracted + $length);
$self->extruder->restart_extra($restart_extra);
$self->lifted($self->extruder->retract_lift) if $lift;
# reset extrusion distance during retracts
@ -301,13 +346,18 @@ sub unretract {
my $gcode = "";
if ($self->lifted) {
$self->speed('travel');
$gcode .= $self->G0(undef, $self->z - $self->lifted, 0, 'restore layer Z');
$self->lifted(0);
}
$self->speed('retract');
$gcode .= $self->G0(undef, undef, $self->extruder->retracted, "compensate retraction");
$self->extruder->retracted(0);
my $to_unretract = $self->extruder->retracted + $self->extruder->restart_extra;
if ($to_unretract) {
$self->speed('retract');
$gcode .= $self->G0(undef, undef, $to_unretract, "compensate retraction");
$self->extruder->retracted(0);
$self->extruder->restart_extra(0);
}
return $gcode;
}
@ -323,9 +373,9 @@ sub reset_e {
sub set_acceleration {
my $self = shift;
my ($acceleration) = @_;
return "" unless $Slic3r::Config->acceleration;
return "" if !$acceleration;
return sprintf "M201 E%s%s\n",
return sprintf "M204 S%s%s\n",
$acceleration, ($Slic3r::Config->gcode_comments ? ' ; adjust acceleration' : '');
}
@ -349,9 +399,10 @@ sub _G0_G1 {
$gcode .= sprintf " X%.${dec}f Y%.${dec}f",
($point->x * &Slic3r::SCALING_FACTOR) + $self->shift_x - $self->extruder->extruder_offset->[X],
($point->y * &Slic3r::SCALING_FACTOR) + $self->shift_y - $self->extruder->extruder_offset->[Y]; #**
$self->last_pos($point);
$gcode = $self->_limit_frequency($point) . $gcode;
$self->last_pos($point->clone);
}
if (defined $z && $z != $self->z) {
if (defined $z && (!defined $self->z || $z != $self->z)) {
$self->z($z);
$gcode .= sprintf " Z%.${dec}f", $z;
}
@ -384,31 +435,30 @@ sub _Gx {
my ($gcode, $e, $comment) = @_;
my $dec = $self->dec;
# determine speed
my $speed = ($e ? $self->speed : 'travel');
# output speed if it's different from last one used
# (goal: reduce gcode size)
my $append_bridge_off = 0;
if ($speed ne $self->last_speed) {
if ($speed eq 'bridge') {
my $F;
if ($self->speed ne $self->last_speed) {
if ($self->speed eq 'bridge') {
$gcode = ";_BRIDGE_FAN_START\n$gcode";
} elsif ($self->last_speed eq 'bridge') {
$append_bridge_off = 1;
}
# apply the speed reduction for print moves on bottom layer
my $speed_f = $speed eq 'retract'
$F = $self->speed eq 'retract'
? ($self->extruder->retract_speed_mm_min)
: $self->speeds->{$speed};
: $self->speeds->{$self->speed} // $self->speed;
if ($e && $self->layer && $self->layer->id == 0 && $comment !~ /retract/) {
$speed_f = $Slic3r::Config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/
? ($speed_f * $1/100)
$F = $Slic3r::Config->first_layer_speed =~ /^(\d+(?:\.\d+)?)%$/
? ($F * $1/100)
: $Slic3r::Config->first_layer_speed * 60;
}
$gcode .= sprintf " F%.${dec}f", $speed_f;
$self->last_speed($speed);
$self->last_speed($self->speed);
$self->last_f($F);
}
$gcode .= sprintf " F%.${dec}f", $F if defined $F;
# output extrusion distance
if ($e && $Slic3r::Config->extrusion_axis) {
@ -430,7 +480,7 @@ sub set_extruder {
my ($extruder) = @_;
# return nothing if this extruder was already selected
return "" if (defined $self->extruder) && ($self->extruder->id == $extruder);
return "" if (defined $self->extruder) && ($self->extruder->id == $extruder->id);
# if we are running a single-extruder setup, just set the extruder and return nothing
if (!$self->multiple_extruders) {
@ -442,6 +492,14 @@ sub set_extruder {
my $gcode = "";
$gcode .= $self->retract(toolchange => 1) if defined $self->extruder;
# append custom toolchange G-code
if (defined $self->extruder && $Slic3r::Config->toolchange_gcode) {
$gcode .= sprintf "%s\n", $Slic3r::Config->replace_options($Slic3r::Config->toolchange_gcode, {
previous_extruder => $self->extruder->id,
next_extruder => $extruder->id,
});
}
# set the new extruder
$self->extruder($extruder);
$gcode .= sprintf "T%d%s\n", $extruder->id, ($Slic3r::Config->gcode_comments ? ' ; change extruder' : '');
@ -457,7 +515,10 @@ sub set_fan {
if ($self->last_fan_speed != $speed || $dont_save) {
$self->last_fan_speed($speed) if !$dont_save;
if ($speed == 0) {
return sprintf "M107%s\n", ($Slic3r::Config->gcode_comments ? ' ; disable fan' : '');
my $code = $Slic3r::Config->gcode_flavor eq 'teacup'
? 'M106 S0'
: 'M107';
return sprintf "$code%s\n", ($Slic3r::Config->gcode_comments ? ' ; disable fan' : '');
} else {
return sprintf "M106 %s%d%s\n", ($Slic3r::Config->gcode_flavor eq 'mach3' ? 'P' : 'S'),
(255 * $speed / 100), ($Slic3r::Config->gcode_comments ? ' ; enable fan' : '');
@ -502,4 +563,41 @@ sub set_bed_temperature {
return $gcode;
}
# http://hydraraptor.blogspot.it/2010/12/frequency-limit.html
sub _limit_frequency {
my $self = shift;
my ($point) = @_;
return '' if $Slic3r::Config->vibration_limit == 0;
my $min_time = 1 / ($Slic3r::Config->vibration_limit * 60); # in minutes
# calculate the move vector and move direction
my $vector = Slic3r::Line->new($self->last_pos, $point)->vector;
my @dir = map { $vector->[B][$_] <=> 0 } X,Y;
my $time = (unscale $vector->length) / $self->speeds->{$self->speed}; # in minutes
if ($time > 0) {
my @pause = ();
foreach my $axis (X,Y) {
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
if ($self->dir_time->[$axis] < $min_time) {
push @pause, ($min_time - $self->dir_time->[$axis]);
}
}
$self->last_dir->[$axis] = $dir[$axis];
$self->dir_time->[$axis] = 0;
}
$self->dir_time->[$axis] += $time;
}
if (@pause) {
return sprintf "G4 P%d\n", max(@pause) * 60 * 1000;
}
}
return '';
}
1;

View File

@ -234,7 +234,7 @@ sub notify {
}
sub save_settings {
my $self = shift;
my $class = shift;
Slic3r::Config->write_ini("$datadir/slic3r.ini", $Settings);
}

View File

@ -44,6 +44,7 @@ Slic3r::GUI::OptionsGroup - pre-filled Wx::StaticBoxSizer wrapper containing one
has 'parent' => (is => 'ro', required => 1);
has 'title' => (is => 'ro', required => 1);
has 'options' => (is => 'ro', required => 1, trigger => 1);
has 'lines' => (is => 'lazy');
has 'on_change' => (is => 'ro', default => sub { sub {} });
has 'no_labels' => (is => 'ro', default => sub { 0 });
has 'label_width' => (is => 'ro', default => sub { 180 });
@ -66,87 +67,149 @@ sub BUILD {
$grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
$grid_sizer->AddGrowableCol($self->no_labels ? 0 : 1);
my $sidetext_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
foreach my $opt (@{$self->options}) {
my $opt_key = $opt->{opt_key};
$self->_triggers->{$opt_key} = $opt->{on_change} || sub {};
my $label;
if (!$self->no_labels) {
$label = Wx::StaticText->new($self->parent, -1, "$opt->{label}:", wxDefaultPosition, [$self->label_width, -1]);
$label->Wrap($self->label_width) ; # needed to avoid Linux/GTK bug
$grid_sizer->Add($label, 0, wxALIGN_CENTER_VERTICAL, 0);
}
my $field;
if ($opt->{type} =~ /^(i|f|s|s@)$/) {
my $style = 0;
$style = wxTE_MULTILINE if $opt->{multiline};
my $size = Wx::Size->new($opt->{width} || -1, $opt->{height} || -1);
$field = $opt->{type} eq 'i'
? Wx::SpinCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style, $opt->{min} || 0, $opt->{max} || 2147483647, $opt->{default})
: Wx::TextCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style);
$field->Disable if $opt->{readonly};
$self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) };
my $on_change = sub { $self->_on_change($opt_key, $field->GetValue) };
$opt->{type} eq 'i'
? EVT_SPINCTRL ($self->parent, $field, $on_change)
: EVT_TEXT ($self->parent, $field, $on_change);
} elsif ($opt->{type} eq 'bool') {
$field = Wx::CheckBox->new($self->parent, -1, "");
$field->SetValue($opt->{default});
EVT_CHECKBOX($self->parent, $field, sub { $self->_on_change($opt_key, $field->GetValue); });
$self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) };
} elsif ($opt->{type} eq 'point') {
$field = Wx::BoxSizer->new(wxHORIZONTAL);
my $field_size = Wx::Size->new(40, -1);
my @items = (
Wx::StaticText->new($self->parent, -1, "x:"),
my $x_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[0], wxDefaultPosition, $field_size),
Wx::StaticText->new($self->parent, -1, " y:"),
my $y_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[1], wxDefaultPosition, $field_size),
);
$field->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for @items;
if ($opt->{tooltip}) {
$_->SetToolTipString($opt->{tooltip}) for @items;
}
EVT_TEXT($self->parent, $_, sub { $self->_on_change($opt_key, [ $x_field->GetValue, $y_field->GetValue ]) })
for $x_field, $y_field;
$self->_setters->{$opt_key} = sub {
$x_field->SetValue($_[0][0]);
$y_field->SetValue($_[0][1]);
};
} elsif ($opt->{type} eq 'select') {
$field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, wxDefaultSize, $opt->{labels} || $opt->{values}, wxCB_READONLY);
EVT_COMBOBOX($self->parent, $field, sub {
$self->_on_change($opt_key, $opt->{values}[$field->GetSelection]);
});
$self->_setters->{$opt_key} = sub {
$field->SetSelection(grep $opt->{values}[$_] eq $_[0], 0..$#{$opt->{values}});
};
$self->_setters->{$opt_key}->($opt->{default});
} else {
die "Unsupported option type: " . $opt->{type};
}
$label->SetToolTipString($opt->{tooltip}) if $label && $opt->{tooltip};
$field->SetToolTipString($opt->{tooltip}) if $opt->{tooltip} && $field->can('SetToolTipString');
if ($opt->{sidetext}) {
my $sizer = Wx::BoxSizer->new(wxHORIZONTAL);
$sizer->Add($field, 0, wxALIGN_CENTER_VERTICAL, 0);
my $sidetext = Wx::StaticText->new($self->parent, -1, $opt->{sidetext}, wxDefaultPosition, wxDefaultSize);
$sidetext->SetFont($sidetext_font);
$sizer->Add($sidetext, 0, wxLEFT | wxALIGN_CENTER_VERTICAL , 4);
$grid_sizer->Add($sizer);
} else {
$grid_sizer->Add($field, 0, ($opt->{full_width} ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0);
}
}
# TODO: border size may be related to wxWidgets 2.8.x vs. 2.9.x instead of wxMAC specific
$self->sizer->Add($grid_sizer, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 5);
$self->{sidetext_font} = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
foreach my $line (@{$self->lines}) {
if ($line->{widget}) {
my $window = $line->{widget}->GetWindow($self->parent);
$self->sizer->Add($window, 0, wxEXPAND | wxALL, &Wx::wxMAC ? 0 : 15);
} else {
$self->_build_line($line, $grid_sizer);
}
}
}
# default behavior: one option per line
sub _build_lines {
my $self = shift;
my $lines = [];
foreach my $opt (@{$self->options}) {
push @$lines, {
label => $opt->{label},
sidetext => $opt->{sidetext},
full_width => $opt->{full_width},
options => [$opt->{opt_key}],
};
}
return $lines;
}
sub single_option_line {
my $class = shift;
my ($opt_key) = @_;
return {
label => $Slic3r::Config::Options->{$opt_key}{label},
sidetext => $Slic3r::Config::Options->{$opt_key}{sidetext},
options => [$opt_key],
};
}
sub _build_line {
my $self = shift;
my ($line, $grid_sizer) = @_;
my $label;
if (!$self->no_labels) {
$label = Wx::StaticText->new($self->parent, -1, $line->{label} ? "$line->{label}:" : "", wxDefaultPosition, [$self->label_width, -1]);
$label->Wrap($self->label_width) ; # needed to avoid Linux/GTK bug
$grid_sizer->Add($label, 0, wxALIGN_CENTER_VERTICAL, 0);
$label->SetToolTipString($line->{tooltip}) if $line->{tooltip};
}
my @fields = ();
my @field_labels = ();
foreach my $opt_key (@{$line->{options}}) {
my $opt = first { $_->{opt_key} eq $opt_key } @{$self->options};
push @fields, $self->_build_field($opt);
push @field_labels, $opt->{label};
}
if (@fields > 1 || $line->{sidetext}) {
my $sizer = Wx::BoxSizer->new(wxHORIZONTAL);
for my $i (0 .. $#fields) {
if (@fields > 1 && $field_labels[$i]) {
my $field_label = Wx::StaticText->new($self->parent, -1, "$field_labels[$i]:", wxDefaultPosition, wxDefaultSize);
$field_label->SetFont($self->{sidetext_font});
$sizer->Add($field_label, 0, wxALIGN_CENTER_VERTICAL, 0);
}
$sizer->Add($fields[$i], 0, wxALIGN_CENTER_VERTICAL, 0);
}
if ($line->{sidetext}) {
my $sidetext = Wx::StaticText->new($self->parent, -1, $line->{sidetext}, wxDefaultPosition, wxDefaultSize);
$sidetext->SetFont($self->{sidetext_font});
$sizer->Add($sidetext, 0, wxLEFT | wxALIGN_CENTER_VERTICAL , 4);
}
$grid_sizer->Add($sizer);
} else {
$grid_sizer->Add($fields[0], 0, ($line->{full_width} ? wxEXPAND : 0) | wxALIGN_CENTER_VERTICAL, 0);
}
}
sub _build_field {
my $self = shift;
my ($opt) = @_;
my $opt_key = $opt->{opt_key};
$self->_triggers->{$opt_key} = $opt->{on_change} || sub {};
my $field;
if ($opt->{type} =~ /^(i|f|s|s@)$/) {
my $style = 0;
$style = wxTE_MULTILINE if $opt->{multiline};
my $size = Wx::Size->new($opt->{width} || -1, $opt->{height} || -1);
$field = $opt->{type} eq 'i'
? Wx::SpinCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style, $opt->{min} || 0, $opt->{max} || 2147483647, $opt->{default})
: Wx::TextCtrl->new($self->parent, -1, $opt->{default}, wxDefaultPosition, $size, $style);
$field->Disable if $opt->{readonly};
$self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) };
my $on_change = sub { $self->_on_change($opt_key, $field->GetValue) };
$opt->{type} eq 'i'
? EVT_SPINCTRL ($self->parent, $field, $on_change)
: EVT_TEXT ($self->parent, $field, $on_change);
} elsif ($opt->{type} eq 'bool') {
$field = Wx::CheckBox->new($self->parent, -1, "");
$field->SetValue($opt->{default});
EVT_CHECKBOX($self->parent, $field, sub { $self->_on_change($opt_key, $field->GetValue); });
$self->_setters->{$opt_key} = sub { $field->SetValue($_[0]) };
} elsif ($opt->{type} eq 'point') {
$field = Wx::BoxSizer->new(wxHORIZONTAL);
my $field_size = Wx::Size->new(40, -1);
my @items = (
Wx::StaticText->new($self->parent, -1, "x:"),
my $x_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[0], wxDefaultPosition, $field_size),
Wx::StaticText->new($self->parent, -1, " y:"),
my $y_field = Wx::TextCtrl->new($self->parent, -1, $opt->{default}->[1], wxDefaultPosition, $field_size),
);
$field->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for @items;
if ($opt->{tooltip}) {
$_->SetToolTipString($opt->{tooltip}) for @items;
}
EVT_TEXT($self->parent, $_, sub { $self->_on_change($opt_key, [ $x_field->GetValue, $y_field->GetValue ]) })
for $x_field, $y_field;
$self->_setters->{$opt_key} = sub {
$x_field->SetValue($_[0][0]);
$y_field->SetValue($_[0][1]);
};
} elsif ($opt->{type} eq 'select') {
$field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, wxDefaultSize, $opt->{labels} || $opt->{values}, wxCB_READONLY);
EVT_COMBOBOX($self->parent, $field, sub {
$self->_on_change($opt_key, $opt->{values}[$field->GetSelection]);
});
$self->_setters->{$opt_key} = sub {
$field->SetSelection(grep $opt->{values}[$_] eq $_[0], 0..$#{$opt->{values}});
};
$self->_setters->{$opt_key}->($opt->{default});
} else {
die "Unsupported option type: " . $opt->{type};
}
$field->SetToolTipString($opt->{tooltip}) if $opt->{tooltip} && $field->can('SetToolTipString');
return $field;
}
sub _option {
@ -308,4 +371,27 @@ sub _config_methods {
: qw(get 0);
}
package Slic3r::GUI::OptionsGroup::StaticTextLine;
use Moo;
use Wx qw(:misc :systemsettings);
sub GetWindow {
my $self = shift;
my ($parent) = @_;
$self->{statictext} = Wx::StaticText->new($parent, -1, "foo", wxDefaultPosition, wxDefaultSize);
my $font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
$self->{statictext}->SetFont($font);
return $self->{statictext};
}
sub SetText {
my $self = shift;
my ($value) = @_;
$self->{statictext}->SetLabel($value);
$self->{statictext}->Wrap(400);
$self->{statictext}->GetParent->Layout;
}
1;

View File

@ -4,13 +4,13 @@ use warnings;
use utf8;
use File::Basename qw(basename dirname);
use List::Util qw(max sum);
use List::Util qw(max sum first);
use Math::ConvexHull::MonotoneChain qw(convex_hull);
use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN MAX);
use Slic3r::Geometry::Clipper qw(JT_ROUND);
use threads::shared qw(shared_clone);
use Wx qw(:bitmap :brush :button :cursor :dialog :filedialog :font :keycode :icon :id :listctrl :misc :panel :pen :sizer :toolbar :window);
use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL EVT_CHOICE);
use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL EVT_CHOICE);
use base 'Wx::Panel';
use constant TB_MORE => &Wx::NewId;
@ -87,6 +87,7 @@ sub new {
$self->{list}->InsertColumn(2, "Scale", wxLIST_FORMAT_CENTER, wxLIST_AUTOSIZE_USEHEADER);
EVT_LIST_ITEM_SELECTED($self, $self->{list}, \&list_item_selected);
EVT_LIST_ITEM_DESELECTED($self, $self->{list}, \&list_item_deselected);
EVT_LIST_ITEM_ACTIVATED($self, $self->{list}, \&list_item_activated);
EVT_KEY_DOWN($self->{list}, sub {
my ($list, $event) = @_;
if ($event->GetKeyCode == WXK_TAB) {
@ -222,11 +223,7 @@ sub new {
my $text = Wx::StaticText->new($self, -1, "$group_labels{$group}:", wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT);
my $choice = Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], []);
$self->{preset_choosers}{$group} = [$choice];
EVT_CHOICE($choice, $choice, sub {
my $choice = shift; # avoid leaks
return if $group eq 'filament' && @{$self->{preset_choosers}{filament}} > 1; #/
$self->skeinpanel->{options_tabs}{$group}->select_preset($choice->GetSelection);
});
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);
@ -246,6 +243,21 @@ sub new {
return $self;
}
sub on_select_preset {
my $self = shift;
my ($group, $choice) = @_;
if ($group eq 'filament' && @{$self->{preset_choosers}{filament}} > 1) {
my @filament_presets = $self->filament_presets;
$Slic3r::GUI::Settings->{presets}{filament} = $choice->GetString($filament_presets[0]) . ".ini";
$Slic3r::GUI::Settings->{presets}{"filament_${_}"} = $choice->GetString($filament_presets[$_])
for 1 .. $#filament_presets;
Slic3r::GUI->save_settings;
return;
}
$self->skeinpanel->{options_tabs}{$group}->select_preset($choice->GetSelection);
}
sub skeinpanel {
my $self = shift;
return $self->GetParent->GetParent;
@ -308,7 +320,7 @@ sub load_file {
: [0,0],
],
);
$object->mesh->check_manifoldness;
$object->check_manifoldness;
# we only consider the rotation of the first instance for now
$object->set_rotation($model->objects->[$i]->instances->[0]->rotation)
@ -404,6 +416,9 @@ sub rotate {
my ($obj_idx, $object) = $self->selected_object;
# we need thumbnail to be computed before allowing rotation
return if !$object->thumbnail;
if (!defined $angle) {
$angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", $object->rotate, -364, 364, $self);
return if !$angle || $angle == -1;
@ -434,7 +449,7 @@ sub arrange {
my $total_parts = sum(map $_->instances_count, @{$self->{objects}}) or return;
my @size = ();
for my $a (X,Y) {
$size[$a] = max(map $_->rotated_size->[$a], @{$self->{objects}});
$size[$a] = max(map $_->size->[$a], @{$self->{objects}});
}
eval {
@ -477,13 +492,18 @@ sub split_object {
# thumbnail thread returns)
$self->remove($obj_idx);
# create a bogus Model object, we only need to instantiate the new Model::Object objects
my $new_model = Slic3r::Model->new;
foreach my $mesh (@new_meshes) {
my @extents = $mesh->extents;
my $model_object = $new_model->add_object(vertices => $mesh->vertices);
$model_object->add_volume(facets => $mesh->facets);
my $object = Slic3r::GUI::Plater::Object->new(
name => basename($current_object->input_file),
input_file => $current_object->input_file,
input_file_object_id => undef,
mesh => $mesh,
model_object => $model_object,
instances => [ map [$extents[X][MIN], $extents[Y][MIN]], 1..$current_copies_num ],
);
push @{ $self->{objects} }, $object;
@ -688,7 +708,7 @@ sub make_model {
material_id => $volume->material_id,
facets => $volume->facets,
);
$model->materials->{$volume->material_id || 0} ||= {};
$model->set_material($volume->material_id || 0, {});
}
$new_model_object->scale($plater_object->scale);
$new_model_object->add_instance(
@ -704,9 +724,11 @@ sub make_thumbnail {
my $self = shift;
my ($obj_idx) = @_;
my $object = $self->{objects}[$obj_idx];
$object->thumbnail_scaling_factor($self->{scaling_factor});
my $cb = sub {
my $object = $self->{objects}[$obj_idx];
my $thumbnail = $object->make_thumbnail(scaling_factor => $self->{scaling_factor});
$Slic3r::Geometry::Clipper::clipper = Math::Clipper->new if $Slic3r::have_threads;
my $thumbnail = $object->make_thumbnail;
if ($Slic3r::have_threads) {
Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx, $thumbnail ])));
@ -739,7 +761,7 @@ sub recenter {
my $obj = $_;
map {
my $instance = $_;
$instance, [ map $instance->[$_] + $obj->rotated_size->[$_], X,Y ];
$instance, [ map $instance->[$_] + $obj->size->[$_], X,Y ];
} @{$obj->instances};
} @{$self->{objects}},
]);
@ -755,8 +777,12 @@ sub on_config_change {
if ($opt_key eq 'extruders_count' && defined $value) {
my $choices = $self->{preset_choosers}{filament};
while (@$choices < $value) {
push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [$choices->[0]->GetStrings]);
my @presets = $choices->[0]->GetStrings;
push @$choices, Wx::Choice->new($self, -1, wxDefaultPosition, [150, -1], [@presets]);
$self->{preset_choosers_sizers}{filament}->Add($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 > $value) {
$self->{preset_choosers_sizers}{filament}->Remove(-1);
@ -832,7 +858,8 @@ sub repaint {
for my $instance_idx (0 .. $#{$object->instances}) {
my $instance = $object->instances->[$instance_idx];
push @{$parent->{object_previews}}, [ $obj_idx, $instance_idx, $object->thumbnail->clone ];
$parent->{object_previews}->[-1][2]->translate(map $parent->to_pixel($instance->[$_]) + $parent->{shift}[$_], (X,Y));
$_->translate(map $parent->to_pixel($instance->[$_]) + $parent->{shift}[$_], (X,Y))
for @{$parent->{object_previews}->[-1][2]->expolygons};
my $drag_object = $self->{drag_object};
if (defined $drag_object && $obj_idx == $drag_object->[0] && $instance_idx == $drag_object->[1]) {
@ -842,11 +869,12 @@ sub repaint {
} else {
$dc->SetBrush($parent->{objects_brush});
}
$dc->DrawPolygon($parent->_y($parent->{object_previews}->[-1][2]), 0, 0);
$dc->DrawPolygon($parent->_y($_), 0, 0) for map $_->contour, @{ $parent->{object_previews}->[-1][2]->expolygons };
# if sequential printing is enabled and we have more than one object
if ($parent->{config}->complete_objects && (map @{$_->instances}, @{$parent->{objects}}) > 1) {
my $clearance = +($parent->{object_previews}->[-1][2]->offset($parent->{config}->extruder_clearance_radius / 2 * $parent->{scaling_factor}, 1, JT_ROUND))[0];
my $convex_hull = Slic3r::Polygon->new(convex_hull([ map @{$_->contour}, @{$parent->{object_previews}->[-1][2]->expolygons} ]));
my $clearance = +($convex_hull->offset($parent->{config}->extruder_clearance_radius / 2 * $parent->{scaling_factor}, 1, JT_ROUND))[0];
$dc->SetPen($parent->{clearance_pen});
$dc->SetBrush($parent->{transparent_brush});
$dc->DrawPolygon($parent->_y($clearance), 0, 0);
@ -856,7 +884,7 @@ sub repaint {
# draw skirt
if (@{$parent->{object_previews}} && $parent->{config}->skirts) {
my $convex_hull = Slic3r::Polygon->new(convex_hull([ map @{$_->[2]}, @{$parent->{object_previews}} ]));
my $convex_hull = Slic3r::Polygon->new(convex_hull([ map @{$_->contour}, map @{$_->[2]->expolygons}, @{$parent->{object_previews}} ]));
$convex_hull = +($convex_hull->offset($parent->{config}->skirt_distance * $parent->{scaling_factor}, 1, JT_ROUND))[0];
$dc->SetPen($parent->{skirt_pen});
$dc->SetBrush($parent->{transparent_brush});
@ -878,7 +906,7 @@ sub mouse_event {
$parent->selection_changed(0);
for my $preview (@{$parent->{object_previews}}) {
my ($obj_idx, $instance_idx, $thumbnail) = @$preview;
if ($thumbnail->encloses_point($pos)) {
if (first { $_->contour->encloses_point($pos) } @{$thumbnail->expolygons}) {
$parent->{selected_objects} = [ [$obj_idx, $instance_idx] ];
$parent->{list}->Select($obj_idx, 1);
$parent->selection_changed(1);
@ -894,6 +922,9 @@ sub mouse_event {
$self->{drag_start_pos} = undef;
$self->{drag_object} = undef;
$self->SetCursor(wxSTANDARD_CURSOR);
} elsif ($event->ButtonDClick) {
$parent->list_item_activated(undef, $parent->{selected_objects}->[0][0])
if @{$parent->{selected_objects}};
} elsif ($event->Dragging) {
return if !$self->{drag_start_pos}; # concurrency problems
for my $preview ($self->{drag_object}) {
@ -906,7 +937,7 @@ sub mouse_event {
} elsif ($event->Moving) {
my $cursor = wxSTANDARD_CURSOR;
for my $preview (@{$parent->{object_previews}}) {
if ($preview->[2]->encloses_point($pos)) {
if (first { $_->contour->encloses_point($pos) } @{ $preview->[2]->expolygons }) {
$cursor = Wx::Cursor->new(wxCURSOR_HAND);
last;
}
@ -934,6 +965,16 @@ sub list_item_selected {
$self->selection_changed(1);
}
sub list_item_activated {
my ($self, $event, $obj_idx) = @_;
$obj_idx //= $event->GetIndex;
my $dlg = Slic3r::GUI::Plater::ObjectInfoDialog->new($self,
object => $self->{objects}[$obj_idx],
);
$dlg->ShowModal;
}
sub object_list_changed {
my $self = shift;
@ -1014,7 +1055,7 @@ package Slic3r::GUI::Plater::Object;
use Moo;
use Math::ConvexHull::MonotoneChain qw(convex_hull);
use Slic3r::Geometry qw(X Y);
use Slic3r::Geometry qw(X Y Z);
has 'name' => (is => 'rw', required => 1);
has 'input_file' => (is => 'rw', required => 1);
@ -1025,10 +1066,30 @@ has 'scale' => (is => 'rw', default => sub { 1 });
has 'rotate' => (is => 'rw', default => sub { 0 });
has 'instances' => (is => 'rw', default => sub { [] }); # upward Y axis
has 'thumbnail' => (is => 'rw');
has 'thumbnail_scaling_factor' => (is => 'rw');
# statistics
has 'facets' => (is => 'rw');
has 'vertices' => (is => 'rw');
has 'materials' => (is => 'rw');
has 'is_manifold' => (is => 'rw');
sub _trigger_model_object {
my $self = shift;
$self->size([$self->model_object->mesh->size]) if $self->model_object;
if ($self->model_object) {
my $mesh = $self->model_object->mesh;
$self->size([$mesh->size]);
$self->facets(scalar @{$mesh->facets});
$self->vertices(scalar @{$mesh->vertices});
$self->materials($self->model_object->materials_count);
}
}
sub check_manifoldness {
my $self = shift;
$self->is_manifold($self->get_model_object->mesh->check_manifoldness);
return $self->is_manifold;
}
sub free_model_object {
@ -1054,22 +1115,29 @@ sub instances_count {
sub make_thumbnail {
my $self = shift;
my %params = @_;
my @points = map [ @$_[X,Y] ], @{$self->model_object->mesh->vertices};
my $convex_hull = Slic3r::Polygon->new(convex_hull(\@points));
for (@$convex_hull) {
@$_ = map $_ * $params{scaling_factor}, @$_;
my $mesh = $self->model_object->mesh;
my $thumbnail = Slic3r::ExPolygon::Collection->new(
expolygons => (@{$mesh->facets} <= 5000)
? $mesh->horizontal_projection
: [ Slic3r::ExPolygon->new(convex_hull($mesh->vertices)) ],
);
for (map @$_, map @$_, @{$thumbnail->expolygons}) {
@$_ = map $_ * $self->thumbnail_scaling_factor, @$_;
}
$convex_hull->simplify(0.3);
$convex_hull->rotate(Slic3r::Geometry::deg2rad($self->rotate));
$convex_hull->scale($self->scale);
$convex_hull->align_to_origin;
$self->thumbnail($convex_hull); # ignored in multi-threaded environments
foreach my $expolygon (@{$thumbnail->expolygons}) {
@$expolygon = grep $_->area >= 1, @$expolygon;
$expolygon->simplify(0.5);
$expolygon->rotate(Slic3r::Geometry::deg2rad($self->rotate));
$expolygon->scale($self->scale);
}
@{$thumbnail->expolygons} = grep @$_, @{$thumbnail->expolygons};
$thumbnail->align_to_origin;
$self->thumbnail($thumbnail); # ignored in multi-threaded environments
$self->free_model_object;
return $convex_hull;
return $thumbnail;
}
sub set_rotation {
@ -1079,6 +1147,8 @@ sub set_rotation {
if ($self->thumbnail) {
$self->thumbnail->rotate(Slic3r::Geometry::deg2rad($angle - $self->rotate));
$self->thumbnail->align_to_origin;
my $z_size = $self->size->[Z];
$self->size([ (map $_ / $self->thumbnail_scaling_factor, @{$self->thumbnail->size}), $z_size ]);
}
$self->rotate($angle);
}
@ -1089,20 +1159,67 @@ sub set_scale {
my $factor = $scale / $self->scale;
return if $factor == 1;
$self->size->[$_] *= $factor for X,Y;
$self->size->[$_] *= $factor for X,Y,Z;
if ($self->thumbnail) {
$self->thumbnail->scale($factor);
$self->thumbnail->align_to_origin;
$_->scale($factor) for @{$self->thumbnail->expolygons};
$self->thumbnail->align_to_origin;
}
$self->scale($scale);
}
sub rotated_size {
my $self = shift;
package Slic3r::GUI::Plater::ObjectInfoDialog;
use Wx qw(:dialog :id :misc :sizer :systemsettings);
use Wx::Event qw(EVT_BUTTON EVT_TEXT_ENTER);
use base 'Wx::Dialog';
sub new {
my $class = shift;
my ($parent, %params) = @_;
my $self = $class->SUPER::new($parent, -1, "Object Info", wxDefaultPosition, wxDefaultSize);
$self->{object} = $params{object};
my $properties_box = Wx::StaticBox->new($self, -1, "Info", wxDefaultPosition, [400,200]);
my $grid_sizer = Wx::FlexGridSizer->new(3, 2, 10, 5);
$properties_box->SetSizer($grid_sizer);
$grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
$grid_sizer->AddGrowableCol(1);
return Slic3r::Polygon->new([0,0], [$self->size->[X], 0], [@{$self->size}], [0, $self->size->[Y]])
->rotate(Slic3r::Geometry::deg2rad($self->rotate))
->size;
my $label_font = Wx::SystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
$label_font->SetPointSize(10);
my $properties = $self->get_properties;
foreach my $property (@$properties) {
my $label = Wx::StaticText->new($properties_box, -1, $property->[0] . ":");
my $value = Wx::StaticText->new($properties_box, -1, $property->[1]);
$label->SetFont($label_font);
$grid_sizer->Add($label, 1, wxALIGN_BOTTOM);
$grid_sizer->Add($value, 0);
}
my $buttons = $self->CreateStdDialogButtonSizer(wxOK);
EVT_BUTTON($self, wxID_OK, sub { $self->EndModal(wxID_OK); });
my $sizer = Wx::BoxSizer->new(wxVERTICAL);
$sizer->Add($properties_box, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
$sizer->Add($buttons, 0, wxEXPAND | wxBOTTOM | wxLEFT | wxRIGHT, 10);
$self->SetSizer($sizer);
$sizer->SetSizeHints($self);
return $self;
}
sub get_properties {
my $self = shift;
return [
['Name' => $self->{object}->name],
['Size' => sprintf "%.2f x %.2f x %.2f", @{$self->{object}->size}],
['Facets' => $self->{object}->facets],
['Vertices' => $self->{object}->vertices],
['Materials' => $self->{object}->materials],
['Two-Manifold' => $self->{object}->is_manifold ? 'Yes' : 'No'],
];
}
1;

View File

@ -396,7 +396,13 @@ sub build {
},
{
title => 'Horizontal shells',
options => [qw(solid_layers)],
options => [qw(top_solid_layers bottom_solid_layers)],
lines => [
{
label => 'Solid layers',
options => [qw(top_solid_layers bottom_solid_layers)],
},
],
},
{
title => 'Advanced',
@ -418,7 +424,7 @@ sub build {
$self->add_options_page('Speed', 'time.png', optgroups => [
{
title => 'Speed for print moves',
options => [qw(perimeter_speed small_perimeter_speed external_perimeter_speed infill_speed solid_infill_speed top_solid_infill_speed bridge_speed)],
options => [qw(perimeter_speed small_perimeter_speed external_perimeter_speed infill_speed solid_infill_speed top_solid_infill_speed support_material_speed bridge_speed gap_fill_speed)],
},
{
title => 'Speed for non-print moves',
@ -428,12 +434,16 @@ sub build {
title => 'Modifiers',
options => [qw(first_layer_speed)],
},
{
title => 'Acceleration control (advanced)',
options => [qw(perimeter_acceleration infill_acceleration default_acceleration)],
},
]);
$self->add_options_page('Skirt and brim', 'box.png', optgroups => [
{
title => 'Skirt',
options => [qw(skirts skirt_distance skirt_height)],
options => [qw(skirts skirt_distance skirt_height min_skirt_length)],
},
{
title => 'Brim',
@ -460,6 +470,13 @@ sub build {
{
title => 'Sequential printing',
options => [qw(complete_objects extruder_clearance_radius extruder_clearance_height)],
lines => [
Slic3r::GUI::OptionsGroup->single_option_line('complete_objects'),
{
label => 'Extruder clearance (mm)',
options => [qw(extruder_clearance_radius extruder_clearance_height)],
},
],
},
{
title => 'Output file',
@ -513,8 +530,18 @@ sub build {
options => ['filament_diameter#0', 'extrusion_multiplier#0'],
},
{
title => 'Temperature',
title => 'Temperature (°C)',
options => ['temperature#0', 'first_layer_temperature#0', qw(bed_temperature first_layer_bed_temperature)],
lines => [
{
label => 'Extruder',
options => ['first_layer_temperature#0', 'temperature#0'],
},
{
label => 'Bed',
options => [qw(first_layer_bed_temperature bed_temperature)],
},
],
},
]);
@ -522,10 +549,26 @@ sub build {
{
title => 'Enable',
options => [qw(cooling)],
lines => [
Slic3r::GUI::OptionsGroup->single_option_line('cooling'),
{
label => '',
widget => ($self->{description_line} = Slic3r::GUI::OptionsGroup::StaticTextLine->new),
},
],
},
{
title => 'Fan settings',
options => [qw(min_fan_speed max_fan_speed bridge_fan_speed disable_fan_first_layers fan_always_on)],
lines => [
{
label => 'Fan speed',
options => [qw(min_fan_speed max_fan_speed)],
},
Slic3r::GUI::OptionsGroup->single_option_line('bridge_fan_speed'),
Slic3r::GUI::OptionsGroup->single_option_line('disable_fan_first_layers'),
Slic3r::GUI::OptionsGroup->single_option_line('fan_always_on'),
],
},
{
title => 'Cooling thresholds',
@ -535,6 +578,36 @@ sub build {
]);
}
sub _update_description {
my $self = shift;
my $config = $self->config;
my $msg = "";
if ($config->cooling) {
$msg = sprintf "If estimated layer time is below ~%ds, fan will run at 100%% and print speed will be reduced so that no less than %ds are spent on that layer (however, speed will never be reduced below %dmm/s).",
$config->slowdown_below_layer_time, $config->slowdown_below_layer_time, $config->min_print_speed;
if ($config->fan_below_layer_time > $config->slowdown_below_layer_time) {
$msg .= sprintf "\nIf estimated layer time is greater, but still below ~%ds, fan will run at a proportionally decreasing speed between %d%% and %d%%.",
$config->fan_below_layer_time, $config->max_fan_speed, $config->min_fan_speed;
}
if ($config->fan_always_on) {
$msg .= sprintf "\nDuring the other layers, fan will always run at %d%%.", $config->min_fan_speed;
} else {
$msg .= "\nDuring the other layers, fan will be turned off."
}
}
$self->{description_line}->SetText($msg);
}
sub on_value_change {
my $self = shift;
my ($opt_key) = @_;
$self->SUPER::on_value_change(@_);
$self->_update_description;
}
package Slic3r::GUI::Tab::Printer;
use base 'Slic3r::GUI::Tab';
@ -569,6 +642,10 @@ sub build {
},
],
},
{
title => 'Advanced',
options => [qw(vibration_limit)],
},
]);
$self->add_options_page('Custom G-code', 'cog.png', optgroups => [
@ -587,6 +664,11 @@ sub build {
no_labels => 1,
options => [qw(layer_gcode)],
},
{
title => 'Tool change G-code',
no_labels => 1,
options => [qw(toolchange_gcode)],
},
]);
$self->{extruder_pages} = [];
@ -726,7 +808,8 @@ sub append_optgroup {
my $self = shift;
my %params = @_;
my $optgroup = Slic3r::GUI::ConfigOptionsGroup->new(
my $class = $params{class} || 'Slic3r::GUI::ConfigOptionsGroup';
my $optgroup = $class->new(
parent => $self,
config => $self->GetParent->{config},
label_width => 200,

View File

@ -20,7 +20,7 @@ our @EXPORT_OK = qw(
shortest_path collinear scale unscale merge_collinear_lines
rad2deg_dir bounding_box_center line_intersects_any douglas_peucker
polyline_remove_short_segments normal triangle_normal polygon_is_convex
scaled_epsilon bounding_box_3D size_3D
scaled_epsilon bounding_box_3D size_3D size_2D
);
@ -684,8 +684,16 @@ sub bounding_box {
sub bounding_box_center {
my @bounding_box = bounding_box(@_);
return Slic3r::Point->new(
($bounding_box[X2] - $bounding_box[X1]) / 2,
($bounding_box[Y2] - $bounding_box[Y1]) / 2,
($bounding_box[X2] + $bounding_box[X1]) / 2,
($bounding_box[Y2] + $bounding_box[Y1]) / 2,
);
}
sub size_2D {
my @bounding_box = bounding_box(@_);
return (
($bounding_box[X2] - $bounding_box[X1]),
($bounding_box[Y2] - $bounding_box[Y1]),
);
}
@ -728,6 +736,7 @@ sub size_3D {
return map $extents[$_][MAX] - $extents[$_][MIN], (X,Y,Z);
}
# this assumes a CCW rotation from $p2 to $p3 around $p1
sub angle3points {
my ($p1, $p2, $p3) = @_;
# p1 is the center

View File

@ -8,20 +8,20 @@ our @EXPORT_OK = qw(safety_offset offset offset_ex
diff_ex diff union_ex intersection_ex xor_ex PFT_EVENODD JT_MITER JT_ROUND
JT_SQUARE is_counter_clockwise);
use Math::Clipper 1.09 qw(:cliptypes :polyfilltypes :jointypes is_counter_clockwise area);
use Math::Clipper 1.15 qw(:cliptypes :polyfilltypes :jointypes is_counter_clockwise area);
use Slic3r::Geometry qw(scale);
our $clipper = Math::Clipper->new;
sub safety_offset {
my ($polygons, $factor) = @_;
return Math::Clipper::offset($polygons, $factor || (scale 1e-05), 100000, JT_MITER, 2);
return Math::Clipper::offset($polygons, $factor // (scale 1e-05), 100000, JT_MITER, 2);
}
sub offset {
my ($polygons, $distance, $scale, $joinType, $miterLimit) = @_;
$scale ||= 100000;
$joinType = JT_MITER if !defined $joinType;
$miterLimit ||= 2;
$joinType //= JT_MITER;
$miterLimit //= 10;
my $offsets = Math::Clipper::offset($polygons, $distance, $scale, $joinType, $miterLimit);
return @$offsets;

View File

@ -1,9 +1,10 @@
package Slic3r::Layer;
use Moo;
use List::Util qw(first);
use Slic3r::Geometry::Clipper qw(union_ex);
has 'id' => (is => 'rw', required => 1); # sequential number of layer, 0-based
has 'id' => (is => 'rw', required => 1, trigger => 1); # sequential number of layer, 0-based
has 'object' => (is => 'ro', weak_ref => 1, required => 1);
has 'regions' => (is => 'ro', default => sub { [] });
has 'slicing_errors' => (is => 'rw');
@ -11,16 +12,23 @@ has 'slicing_errors' => (is => 'rw');
has 'slice_z' => (is => 'lazy');
has 'print_z' => (is => 'lazy');
has 'height' => (is => 'lazy');
has 'flow' => (is => 'ro', default => sub { $Slic3r::flow });
has 'flow' => (is => 'lazy');
# collection of expolygons generated by slicing the original geometry;
# also known as 'islands' (all regions are merged here)
has 'slices' => (is => 'rw');
# ordered collection of extrusion paths to fill surfaces for support material
has 'support_fills' => (is => 'rw');
has 'support_islands' => (is => 'rw');
has 'support_fills' => (is => 'rw');
has 'support_interface_fills' => (is => 'rw');
# Z used for slicing
sub _trigger_id {
my $self = shift;
$_->_trigger_layer for @{$self->regions || []};
}
# Z used for slicing in scaled coordinates
sub _build_slice_z {
my $self = shift;
@ -31,27 +39,50 @@ sub _build_slice_z {
/ &Slic3r::SCALING_FACTOR; #/
}
# Z used for printing
# Z used for printing in scaled coordinates
sub _build_print_z {
my $self = shift;
return ($Slic3r::Config->get_value('first_layer_height') + ($self->id * $Slic3r::Config->layer_height)) / &Slic3r::SCALING_FACTOR;
}
# layer height in unscaled coordinates
sub _build_height {
my $self = shift;
return $self->id == 0 ? $Slic3r::Config->get_value('first_layer_height') : $Slic3r::Config->layer_height;
}
sub _build_flow { $Slic3r::flow }
# layer height of interface paths in unscaled coordinates
sub support_material_interface_height {
my $self = shift;
return $self->height if $self->id == 0;
# this is not very correct because:
# - we should sum our height with the actual upper layers height (which might be different)
# - we should use the actual flow of the upper layer bridges, not the default one
# ...but we're close enough for now
return 2*$self->height - $self->flow->nozzle_diameter;
}
# Z used for printing support material interface in scaled coordinates
sub support_material_interface_z {
my $self = shift;
return $self->print_z - ($self->height - $self->support_material_interface_height) / &Slic3r::SCALING_FACTOR;
}
sub region {
my $self = shift;
my ($region_id) = @_;
if (!defined $self->regions->[$region_id]) {
$self->regions->[$region_id] = Slic3r::Layer::Region->new(
for (my $i = @{$self->regions}; $i <= $region_id; $i++) {
$self->regions->[$i] //= Slic3r::Layer::Region->new(
layer => $self,
region => $self->object->print->regions->[$region_id],
region => $self->object->print->regions->[$i],
);
}
return $self->regions->[$region_id];
}
@ -75,4 +106,10 @@ sub make_perimeters {
$_->make_perimeters for @{$self->regions};
}
sub support_islands_enclose_line {
my $self = shift;
my ($line) = @_;
return (first { $_->encloses_line($line) } @{$self->support_islands}) ? 1 : 0;
}
1;

View File

@ -1,7 +1,6 @@
package Slic3r::Layer::Region;
use Moo;
use Math::Clipper ':all';
use Slic3r::ExtrusionPath ':roles';
use Slic3r::Geometry qw(scale shortest_path);
use Slic3r::Geometry::Clipper qw(safety_offset union_ex diff_ex intersection_ex);
@ -11,11 +10,12 @@ has 'layer' => (
is => 'ro',
weak_ref => 1,
required => 1,
trigger => 1,
handles => [qw(id slice_z print_z height flow)],
);
has 'region' => (is => 'ro', required => 1);
has 'perimeter_flow' => (is => 'lazy');
has 'infill_flow' => (is => 'lazy');
has 'perimeter_flow' => (is => 'rw');
has 'infill_flow' => (is => 'rw');
# collection of spare segments generated by slicing the original geometry;
# these need to be merged in continuos (closed) polylines
@ -32,13 +32,7 @@ has 'thin_walls' => (is => 'rw', default => sub { [] });
# need to be filled with a medial axis
has 'thin_fills' => (is => 'rw', default => sub { [] });
# collection of expolygons generated by offsetting the innermost perimeter(s)
# they represent boundaries of areas to fill, typed (top/bottom/internal)
has 'surfaces' => (is => 'rw', default => sub { [] });
# collection of surfaces for infill generation. the difference between surfaces
# fill_surfaces is that this one honors fill_density == 0 and turns small internal
# surfaces into solid ones
# collection of surfaces for infill generation
has 'fill_surfaces' => (is => 'rw', default => sub { [] });
# ordered collection of extrusion paths/loops to build all perimeters
@ -47,18 +41,27 @@ has 'perimeters' => (is => 'rw', default => sub { [] });
# ordered collection of extrusion paths to fill surfaces
has 'fills' => (is => 'rw', default => sub { [] });
sub _build_perimeter_flow {
sub BUILD {
my $self = shift;
return $self->id == 0
? $self->region->first_layer_flows->{perimeter}
: $self->region->flows->{perimeter}
$self->_update_flows;
}
sub _build_infill_flow {
sub _trigger_layer {
my $self = shift;
return $self->id == 0
$self->_update_flows;
}
sub _update_flows {
my $self = shift;
return if !$self->region;
$self->perimeter_flow($self->id == 0
? $self->region->first_layer_flows->{perimeter}
: $self->region->flows->{perimeter});
$self->infill_flow($self->id == 0
? $self->region->first_layer_flows->{infill}
: $self->region->flows->{infill}
: $self->region->flows->{infill});
}
# build polylines from lines
@ -66,20 +69,8 @@ sub make_surfaces {
my $self = shift;
my ($loops) = @_;
return if !@$loops;
{
my $safety_offset = scale 0.1;
# merge everything
my $expolygons = [ map $_->offset_ex(-$safety_offset), @{union_ex(safety_offset($loops, $safety_offset))} ];
Slic3r::debugf " %d surface(s) having %d holes detected from %d polylines\n",
scalar(@$expolygons), scalar(map $_->holes, @$expolygons), scalar(@$loops);
$self->slices([
map Slic3r::Surface->new(expolygon => $_, surface_type => S_TYPE_INTERNAL),
@$expolygons
]);
}
return if !@$loops;
$self->slices([ _merge_loops($loops) ]);
# the contours must be offsetted by half extrusion width inwards
{
@ -89,13 +80,17 @@ sub make_surfaces {
foreach my $surface (@surfaces) {
push @{$self->slices}, map Slic3r::Surface->new
(expolygon => $_, surface_type => S_TYPE_INTERNAL),
map $_->offset_ex(+$distance),
$surface->expolygon->offset_ex(-2*$distance);
@{union_ex([
Slic3r::Geometry::Clipper::offset(
[Slic3r::Geometry::Clipper::offset($surface->expolygon, -2*$distance)],
+$distance,
),
])};
}
# now detect thin walls by re-outgrowing offsetted surfaces and subtracting
# them from the original slices
my $outgrown = Math::Clipper::offset([ map $_->p, @{$self->slices} ], $distance);
my $outgrown = [ Slic3r::Geometry::Clipper::offset([ map $_->p, @{$self->slices} ], $distance) ];
my $diff = diff_ex(
[ map $_->p, @surfaces ],
$outgrown,
@ -115,13 +110,43 @@ sub make_surfaces {
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "surfaces.svg",
Slic3r::SVG::output("surfaces.svg",
polygons => [ map $_->contour, @{$self->slices} ],
red_polygons => [ map $_->p, map @{$_->holes}, @{$self->slices} ],
);
}
}
sub _merge_loops {
my ($loops, $safety_offset) = @_;
# Input loops are not suitable for evenodd nor nonzero fill types, as we might get
# two consecutive concentric loops having the same winding order - and we have to
# respect such order. In that case, evenodd would create wrong inversions, and nonzero
# would ignore holes inside two concentric contours.
# So we're ordering loops and collapse consecutive concentric loops having the same
# winding order.
# TODO: find a faster algorithm for this.
my @loops = sort { $a->encloses_point($b->[0]) ? 0 : 1 } @$loops; # outer first
$safety_offset //= scale 0.1;
@loops = @{ safety_offset(\@loops, $safety_offset) };
my $expolygons = [];
while (my $loop = shift @loops) {
bless $loop, 'Slic3r::Polygon';
if ($loop->is_counter_clockwise) {
$expolygons = union_ex([ $loop, map @$_, @$expolygons ]);
} else {
$expolygons = diff_ex([ map @$_, @$expolygons ], [$loop]);
}
}
$expolygons = [ map $_->offset_ex(-$safety_offset), @$expolygons ];
Slic3r::debugf " %d surface(s) having %d holes detected from %d polylines\n",
scalar(@$expolygons), scalar(map $_->holes, @$expolygons), scalar(@$loops);
return map Slic3r::Surface->new(expolygon => $_, surface_type => S_TYPE_INTERNAL), @$expolygons;
}
sub make_perimeters {
my $self = shift;
@ -147,7 +172,7 @@ sub make_perimeters {
])};
$self->perimeters([]);
$self->surfaces([]);
$self->fill_surfaces([]);
$self->thin_fills([]);
# for each island:
@ -182,39 +207,114 @@ sub make_perimeters {
# generate perimeters inwards (loop 0 is the external one)
my $loop_number = $Slic3r::Config->perimeters + ($surface->additional_inner_perimeters || 0);
push @perimeters, [[@last_offsets]];
for (my $loop = 1; $loop < $loop_number; $loop++) {
push @perimeters, [[@last_offsets]] if $loop_number > 0;
# do one more loop (<= instead of <) so that we can detect gaps even after the desired
# number of perimeters has been generated
for (my $loop = 1; $loop <= $loop_number; $loop++) {
# offsetting a polygon can result in one or many offset polygons
my @new_offsets = ();
foreach my $expolygon (@last_offsets) {
my @offsets = map $_->offset_ex(+0.5*$distance), $expolygon->offset_ex(-1.5*$distance);
my @offsets = @{union_ex([
Slic3r::Geometry::Clipper::offset(
[Slic3r::Geometry::Clipper::offset($expolygon, -1.5*$distance)],
+0.5*$distance,
),
])};
push @new_offsets, @offsets;
# where the above check collapses the expolygon, then there's no room for an inner loop
# and we can extract the gap for later processing
my $diff = diff_ex(
[ map @$_, $expolygon->offset_ex(-$distance) ],
[ map @$_, @offsets ],
[ map @$_, $expolygon->offset_ex(-0.5*$distance) ],
# +2 on the offset here makes sure that Clipper float truncation
# won't shrink the clip polygon to be smaller than intended.
[ Slic3r::Geometry::Clipper::offset([map @$_, @offsets], +0.5*$distance + 2) ],
);
push @gaps, grep $_->area >= $gap_area_threshold, @$diff;
}
@last_offsets = @new_offsets;
last if !@last_offsets;
last if !@new_offsets || $loop == $loop_number;
@last_offsets = @new_offsets;
push @{ $perimeters[-1] }, [@last_offsets];
}
# create one more offset to be used as boundary for fill
{
my @fill_boundaries = map $_->offset_ex(-$distance), @last_offsets;
$_->simplify(scale &Slic3r::RESOLUTION) for @fill_boundaries;
push @{ $self->surfaces }, @fill_boundaries;
my @fill_boundaries = @{union_ex([
Slic3r::Geometry::Clipper::offset(
[Slic3r::Geometry::Clipper::offset([ map @$_, @last_offsets ], -1.5*$distance)],
+0.5*$distance,
),
])};
$_->simplify(&Slic3r::SCALED_RESOLUTION) for @fill_boundaries;
push @{ $self->fill_surfaces }, @fill_boundaries;
}
# fill gaps
if ($Slic3r::Config->gap_fill_speed > 0) {
my $filler = Slic3r::Fill::Rectilinear->new(layer_id => $self->layer->id);
# detect the small gaps that we need to treat like thin polygons,
# thus generating the skeleton and using it to fill them
push @{ $self->thin_fills },
map $_->medial_axis($self->perimeter_flow->scaled_width),
@gaps;
Slic3r::debugf " %d gaps filled\n", scalar @{ $self->thin_fills }
if @{ $self->thin_fills };
my $w = $self->perimeter_flow->width;
my @widths = (1.5 * $w, $w, 0.5 * $w); # worth trying 0.2 too?
foreach my $width (@widths) {
my $flow = $self->perimeter_flow->clone(width => $width);
# extract the gaps having this width
my @this_width = map $_->offset_ex(+0.5*$flow->scaled_width),
map $_->noncollapsing_offset_ex(-0.5*$flow->scaled_width),
@gaps;
if (0) { # remember to re-enable t/dynamic.t
# fill gaps using dynamic extrusion width, by treating them like thin polygons,
# thus generating the skeleton and using it to fill them
my %path_args = (
role => EXTR_ROLE_SOLIDFILL,
flow_spacing => $flow->spacing,
);
push @{ $self->thin_fills }, map {
$_->isa('Slic3r::Polygon')
? (map $_->pack, Slic3r::ExtrusionLoop->new(polygon => $_, %path_args)->split_at_first_point) # we should keep these as loops
: Slic3r::ExtrusionPath->pack(polyline => $_, %path_args),
} map $_->medial_axis($flow->scaled_width), @this_width;
Slic3r::debugf " %d gaps filled with extrusion width = %s\n", scalar @this_width, $width
if @{ $self->thin_fills };
} else {
# fill gaps using zigzag infill
# since this is infill, we have to offset by half-extrusion width inwards
my @infill = map $_->offset_ex(-0.5*$flow->scaled_width), @this_width;
foreach my $expolygon (@infill) {
my @paths = $filler->fill_surface(
Slic3r::Surface->new(expolygon => $expolygon),
density => 1,
flow_spacing => $flow->spacing,
);
my $params = shift @paths;
push @{ $self->thin_fills },
map {
$_->polyline->simplify($flow->scaled_width / 3);
$_->pack;
}
map Slic3r::ExtrusionPath->new(
polyline => Slic3r::Polyline->new(@$_),
role => EXTR_ROLE_GAPFILL,
height => $self->height,
flow_spacing => $params->{flow_spacing},
), @paths;
}
}
# check what's left
@gaps = @{diff_ex(
[ map @$_, @gaps ],
[ map @$_, @this_width ],
)};
}
}
}
@ -276,21 +376,22 @@ sub make_perimeters {
}
}
# add thin walls as perimeters
{
my @thin_paths = ();
my %properties = (
role => EXTR_ROLE_EXTERNAL_PERIMETER,
flow_spacing => $self->perimeter_flow->spacing,
);
for (@{ $self->thin_walls }) {
push @thin_paths, $_->isa('Slic3r::Polygon')
? Slic3r::ExtrusionLoop->pack(polygon => $_, %properties)
: Slic3r::ExtrusionPath->pack(polyline => $_, %properties);
}
my $collection = Slic3r::ExtrusionPath::Collection->new(paths => \@thin_paths);
push @{ $self->perimeters }, $collection->shortest_path;
# if brim will be printed, reverse the order of perimeters so that
# we continue inwards after having finished the brim
if ($self->layer->id == 0 && $Slic3r::Config->brim_width > 0) {
@{$self->perimeters} = reverse @{$self->perimeters};
}
# add thin walls as perimeters
push @{ $self->perimeters }, Slic3r::ExtrusionPath::Collection->new(paths => [
map {
Slic3r::ExtrusionPath->pack(
polyline => ($_->isa('Slic3r::Polygon') ? $_->split_at_first_point : $_),
role => EXTR_ROLE_EXTERNAL_PERIMETER,
flow_spacing => $self->perimeter_flow->spacing,
);
} @{ $self->thin_walls }
])->shortest_path;
}
sub _add_perimeter {
@ -300,7 +401,7 @@ sub _add_perimeter {
return unless $polygon->is_printable($self->perimeter_flow->width);
push @{ $self->perimeters }, Slic3r::ExtrusionLoop->pack(
polygon => $polygon,
role => (abs($polygon->length) <= &Slic3r::SMALL_PERIMETER_LENGTH) ? EXTR_ROLE_SMALLPERIMETER : ($role // EXTR_ROLE_PERIMETER), #/
role => ($role // EXTR_ROLE_PERIMETER),
flow_spacing => $self->perimeter_flow->spacing,
);
}
@ -308,45 +409,21 @@ sub _add_perimeter {
sub prepare_fill_surfaces {
my $self = shift;
my @surfaces = @{$self->surfaces};
# if no solid layers are requested, turn top/bottom surfaces to internal
# note that this modifies $self->surfaces in place
if ($Slic3r::Config->solid_layers == 0) {
$_->surface_type(S_TYPE_INTERNAL) for grep $_->surface_type != S_TYPE_INTERNAL, @surfaces;
if ($Slic3r::Config->top_solid_layers == 0) {
$_->surface_type(S_TYPE_INTERNAL) for grep $_->surface_type == S_TYPE_TOP, @{$self->fill_surfaces};
}
# if hollow object is requested, remove internal surfaces
if ($Slic3r::Config->fill_density == 0) {
@surfaces = grep $_->surface_type != S_TYPE_INTERNAL, @surfaces;
}
# remove unprintable regions (they would slow down the infill process and also cause
# some weird failures during bridge neighbor detection)
{
my $distance = $self->infill_flow->scaled_spacing / 2;
@surfaces = map {
my $surface = $_;
# offset inwards
my @offsets = $surface->expolygon->offset_ex(-$distance);
@offsets = @{union_ex(Math::Clipper::offset([ map @$_, @offsets ], $distance, 100000, JT_MITER))};
map Slic3r::Surface->new(
expolygon => $_,
surface_type => $surface->surface_type,
), @offsets;
} @surfaces;
if ($Slic3r::Config->bottom_solid_layers == 0) {
$_->surface_type(S_TYPE_INTERNAL) for grep $_->surface_type == S_TYPE_BOTTOM, @{$self->fill_surfaces};
}
# turn too small internal regions into solid regions
{
my $min_area = scale scale $Slic3r::Config->solid_infill_below_area; # scaling an area requires two calls!
my @small = grep $_->surface_type == S_TYPE_INTERNAL && $_->expolygon->contour->area <= $min_area, @surfaces;
my @small = grep $_->surface_type == S_TYPE_INTERNAL && $_->expolygon->contour->area <= $min_area, @{$self->fill_surfaces};
$_->surface_type(S_TYPE_INTERNALSOLID) for @small;
Slic3r::debugf "identified %d small solid surfaces at layer %d\n", scalar(@small), $self->id if @small > 0;
}
$self->fill_surfaces([@surfaces]);
}
# make bridges printable
@ -385,7 +462,7 @@ sub process_bridges {
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "bridge_surfaces.svg",
Slic3r::SVG::output("bridge_surfaces.svg",
green_polygons => [ map $_->p, @supporting_surfaces ],
red_polygons => [ @$expolygon ],
);
@ -417,7 +494,7 @@ sub process_bridges {
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "bridge_edges.svg",
Slic3r::SVG::output("bridge_edges.svg",
polylines => [ map $_->p, @edges ],
);
}

View File

@ -14,14 +14,6 @@ sub new {
return $self;
}
sub a { $_[0][0] }
sub b { $_[0][1] }
sub coordinates {
my $self = shift;
return ($self->a->coordinates, $self->b->coordinates);
}
sub boost_linestring {
my $self = shift;
return Boost::Geometry::Utils::linestring($self);
@ -35,32 +27,16 @@ sub coincides_with {
|| ($self->a->coincides_with($line->b) && $self->b->coincides_with($line->a));
}
sub has_endpoint {
my $self = shift;
my ($point) = @_;
return $point->coincides_with($self->a) || $point->coincides_with($self->b);
}
sub has_segment {
my $self = shift;
my ($line) = @_;
# a segment belongs to another segment if its points belong to it
return Slic3r::Geometry::point_in_segment($line->[0], $self)
&& Slic3r::Geometry::point_in_segment($line->[1], $self);
}
sub parallel_to {
my $self = shift;
my ($line) = @_;
return Slic3r::Geometry::lines_parallel($self, $line);
}
sub length {
my $self = shift;
return Slic3r::Geometry::line_length($self);
}
sub vector {
my $self = shift;
return (ref $self)->new([0,0], [map $self->[B][$_] - $self->[A][$_], X,Y]);
}
sub atan {
my $self = shift;
return Slic3r::Geometry::line_atan($self);
@ -96,4 +72,12 @@ sub reverse {
@$self = reverse @$self;
}
sub translate {
my $self = shift;
my ($x, $y) = @_;
@$self = Slic3r::Geometry::move_points([$x, $y], @$self);
bless $_, 'Slic3r::Point' for @$self;
return $self;
}
1;

View File

@ -129,6 +129,13 @@ sub scale {
}
}
sub materials_count {
my $self = shift;
my %materials = map { $_->material_id // '_default' => 1 } @{$self->volumes};
return scalar keys %materials;
}
package Slic3r::Model::Volume;
use Moo;

View File

@ -23,20 +23,6 @@ sub clone {
return (ref $self)->new(@$self);
}
sub cast {
my $class = shift;
if (ref $_[0] eq 'Slic3r::Point') {
return $_[0];
} else {
return $class->new(@_);
}
}
sub coordinates {
my $self = shift;
return @$self;
}
sub coincides_with {
my $self = shift;
my ($point) = @_;

View File

@ -24,19 +24,32 @@ sub boost_linestring {
return Boost::Geometry::Utils::linestring([@$self, $self->[0]]);
}
sub wkt {
my $self = shift;
return sprintf "POLYGON((%s))", join ',', map "$_->[0] $_->[1]", @$self;
}
sub is_counter_clockwise {
my $self = shift;
return Math::Clipper::is_counter_clockwise($self);
return Slic3r::Geometry::Clipper::is_counter_clockwise($self);
}
sub make_counter_clockwise {
my $self = shift;
$self->reverse if !$self->is_counter_clockwise;
if (!$self->is_counter_clockwise) {
$self->reverse;
return 1;
}
return 0;
}
sub make_clockwise {
my $self = shift;
$self->reverse if $self->is_counter_clockwise;
if ($self->is_counter_clockwise) {
$self->reverse;
return 1;
}
return 0;
}
sub merge_continuous_lines {
@ -124,4 +137,36 @@ sub is_valid {
return @$self >= 3;
}
sub split_at_index {
my $self = shift;
my ($index) = @_;
return (ref $self)->new(
@$self[$index .. $#$self],
@$self[0 .. $index],
);
}
sub split_at {
my $self = shift;
my ($point) = @_;
# find index of point
my $i = -1;
for (my $n = 0; $n <= $#$self; $n++) {
if (Slic3r::Geometry::same_point($point, $self->[$n])) {
$i = $n;
last;
}
}
die "Point not found" if $i == -1;
return $self->split_at_index($i);
}
sub split_at_first_point {
my $self = shift;
return $self->split_at_index(0);
}
1;

View File

@ -2,7 +2,6 @@ package Slic3r::Polyline;
use strict;
use warnings;
use Math::Clipper qw();
use Scalar::Util qw(reftype);
use Slic3r::Geometry qw(A B X Y X1 X2 Y1 Y2 polyline_remove_parallel_continuous_edges polyline_remove_acute_vertices
polyline_lines move_points same_point);
@ -40,11 +39,6 @@ sub deserialize {
return $class->new(map [ $v[2*$_], $v[2*$_+1] ], 0 .. int($#v/2));
}
sub is_serialized {
my $self = shift;
return (reftype $self) eq 'SCALAR' ? 1 : 0;
}
sub lines {
my $self = shift;
return polyline_lines($self);
@ -55,6 +49,11 @@ sub boost_linestring {
return Boost::Geometry::Utils::linestring($self);
}
sub wkt {
my $self = shift;
return sprintf "LINESTRING((%s))", join ',', map "$_->[0] $_->[1]", @$self;
}
sub merge_continuous_lines {
my $self = shift;
polyline_remove_parallel_continuous_edges($self);
@ -105,16 +104,6 @@ sub nearest_point_index_to {
return Slic3r::Geometry::nearest_point_index($point, $self);
}
sub has_segment {
my $self = shift;
my ($line) = @_;
for ($self->lines) {
return 1 if $_->has_segment($line);
}
return 0;
}
sub clip_with_polygon {
my $self = shift;
my ($polygon) = @_;
@ -142,9 +131,7 @@ sub bounding_box {
sub size {
my $self = shift;
my @extents = $self->bounding_box;
return [$extents[X2] - $extents[X1], $extents[Y2] - $extents[Y1]];
return [ Slic3r::Geometry::size_2D($self) ];
}
sub align_to_origin {
@ -181,4 +168,60 @@ sub scale {
return $self;
}
sub clip_end {
my $self = shift;
my ($distance) = @_;
while ($distance > 0) {
my $last_point = pop @$self;
last if !@$self;
my $last_segment_length = $last_point->distance_to($self->[-1]);
if ($last_segment_length <= $distance) {
$distance -= $last_segment_length;
next;
}
my $new_point = Slic3r::Geometry::point_along_segment($last_point, $self->[-1], $distance);
push @$self, Slic3r::Point->new($new_point);
$distance = 0;
}
}
package Slic3r::Polyline::Collection;
use Moo;
has 'polylines' => (is => 'ro', default => sub { [] });
# If the second argument is provided, this method will return its items sorted
# instead of returning the actual sorted polylines.
# Note that our polylines will be reversed in place when necessary.
sub shortest_path {
my $self = shift;
my ($start_near, $items) = @_;
$items ||= $self->polylines;
my %items_map = map { $self->polylines->[$_] => $items->[$_] } 0 .. $#{$self->polylines};
my @my_paths = @{$self->polylines};
my @paths = ();
my $start_at;
my $endpoints = [ map { $_->[0], $_->[-1] } @my_paths ];
while (@my_paths) {
# find nearest point
my $start_index = $start_near
? Slic3r::Geometry::nearest_point_index($start_near, $endpoints)
: 0;
my $path_index = int($start_index/2);
if ($start_index%2) { # index is end so reverse to make it the start
$my_paths[$path_index]->reverse;
}
push @paths, splice @my_paths, $path_index, 1;
splice @$endpoints, $path_index*2, 2;
$start_near = $paths[-1][-1];
}
return map $items_map{"$_"}, @paths;
}
1;

View File

@ -3,7 +3,7 @@ use Moo;
use File::Basename qw(basename fileparse);
use File::Spec;
use List::Util qw(max);
use List::Util qw(max first);
use Math::ConvexHull::MonotoneChain qw(convex_hull);
use Slic3r::ExtrusionPath ':roles';
use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 MIN PI scale unscale move_points nearest_point);
@ -54,6 +54,8 @@ sub _trigger_config {
$self->config->set_ifndef('bridge_speed', $self->config->infill_speed);
$self->config->set_ifndef('solid_infill_speed', $self->config->infill_speed);
$self->config->set_ifndef('top_solid_infill_speed', $self->config->solid_infill_speed);
$self->config->set_ifndef('top_solid_layers', $self->config->solid_layers);
$self->config->set_ifndef('bottom_solid_layers', $self->config->solid_layers);
# G-code flavors
$self->config->set('extrusion_axis', 'A') if $self->config->gcode_flavor eq 'mach3';
@ -339,12 +341,12 @@ sub export_gcode {
# simplify slices (both layer and region slices),
# we only need the max resolution for perimeters
foreach my $layer (map @{$_->layers}, @{$self->objects}) {
$_->simplify(scale &Slic3r::RESOLUTION)
$_->simplify(&Slic3r::SCALED_RESOLUTION)
for @{$layer->slices}, (map $_->expolygon, map @{$_->slices}, @{$layer->regions});
}
# this will clip $layer->surfaces to the infill boundaries
# and split them in top/bottom/internal surfaces;
# this will transform $layer->fill_surfaces from expolygon
# to typed top/bottom/internal surfaces;
$status_cb->(30, "Detecting solid surfaces");
$_->detect_surfaces_type for @{$self->objects};
@ -362,9 +364,6 @@ sub export_gcode {
$status_cb->(60, "Generating horizontal shells");
$_->discover_horizontal_shells for @{$self->objects};
# free memory
$_->surfaces(undef) for map @{$_->regions}, map @{$_->layers}, @{$self->objects};
# combine fill surfaces to honor the "infill every N layers" option
$status_cb->(70, "Combining infill");
$_->combine_infill for @{$self->objects};
@ -433,8 +432,8 @@ sub export_gcode {
# output everything to a G-code file
my $output_file = $self->expanded_output_filepath($params{output_file});
$status_cb->(90, "Exporting G-code to $output_file");
$self->write_gcode($output_file);
$status_cb->(90, "Exporting G-code" . ($output_file ? " to $output_file" : ""));
$self->write_gcode($params{output_fh} || $output_file);
# run post-processing scripts
if (@{$Slic3r::Config->post_process}) {
@ -447,14 +446,16 @@ sub export_gcode {
}
# output some statistics
$self->processing_time(tv_interval($t0));
printf "Done. Process took %d minutes and %.3f seconds\n",
int($self->processing_time/60),
$self->processing_time - int($self->processing_time/60)*60;
# TODO: more statistics!
printf "Filament required: %.1fmm (%.1fcm3)\n",
$self->total_extrusion_length, $self->total_extrusion_volume;
unless ($params{quiet}) {
$self->processing_time(tv_interval($t0));
printf "Done. Process took %d minutes and %.3f seconds\n",
int($self->processing_time/60),
$self->processing_time - int($self->processing_time/60)*60;
# TODO: more statistics!
printf "Filament required: %.1fmm (%.1fcm3)\n",
$self->total_extrusion_length, $self->total_extrusion_volume;
}
}
sub export_svg {
@ -553,7 +554,7 @@ sub make_skirt {
$skirt_height = $self->layer_count if $skirt_height > $self->layer_count;
my @points = ();
foreach my $obj_idx (0 .. $#{$self->objects}) {
my @layers = map $self->objects->[$obj_idx]->layer($_), 0..($skirt_height-1);
my @layers = map $self->objects->[$obj_idx]->layers->[$_], 0..($skirt_height-1);
my @layer_points = (
(map @$_, map @$_, map @{$_->slices}, @layers),
(map @$_, map @{$_->thin_walls}, map @{$_->regions}, @layers),
@ -566,16 +567,40 @@ sub make_skirt {
# find out convex hull
my $convex_hull = convex_hull(\@points);
my @extruded_length = (); # for each extruder
my $spacing = $Slic3r::first_layer_flow->spacing;
my $first_layer_height = $Slic3r::Config->get_value('first_layer_height');
my @extruders_e_per_mm = ();
my $extruder_idx = 0;
# draw outlines from outside to inside
# loop while we have less skirts than required or any extruder hasn't reached the min length if any
my $distance = scale $Slic3r::Config->skirt_distance;
for (my $i = $Slic3r::Config->skirts; $i > 0; $i--) {
my $distance = scale ($Slic3r::Config->skirt_distance + ($Slic3r::first_layer_flow->spacing * $i));
my $outline = Math::Clipper::offset([$convex_hull], $distance, &Slic3r::SCALING_FACTOR * 100, JT_ROUND);
$distance += scale $spacing;
my ($loop) = Slic3r::Geometry::Clipper::offset([$convex_hull], $distance, 0.0001, JT_ROUND);
push @{$self->skirt}, Slic3r::ExtrusionLoop->pack(
polygon => Slic3r::Polygon->new(@{$outline->[0]}),
polygon => Slic3r::Polygon->new(@$loop),
role => EXTR_ROLE_SKIRT,
flow_spacing => $Slic3r::first_layer_flow->spacing,
flow_spacing => $spacing,
);
if ($Slic3r::Config->min_skirt_length > 0) {
bless $loop, 'Slic3r::Polygon';
$extruded_length[$extruder_idx] ||= 0;
$extruders_e_per_mm[$extruder_idx] ||= $self->extruders->[$extruder_idx]->e_per_mm($spacing, $first_layer_height);
$extruded_length[$extruder_idx] += unscale $loop->length * $extruders_e_per_mm[$extruder_idx];
$i++ if defined first { ($extruded_length[$_] // 0) < $Slic3r::Config->min_skirt_length } 0 .. $#{$self->extruders};
if ($extruded_length[$extruder_idx] >= $Slic3r::Config->min_skirt_length) {
if ($extruder_idx < $#{$self->extruders}) {
$extruder_idx++;
next;
}
}
}
}
@{$self->skirt} = reverse @{$self->skirt};
}
sub make_brim {
@ -608,7 +633,7 @@ sub make_brim {
polygon => Slic3r::Polygon->new($_),
role => EXTR_ROLE_SKIRT,
flow_spacing => $Slic3r::first_layer_flow->spacing,
) for @{Math::Clipper::offset(\@islands, $i * $Slic3r::first_layer_flow->scaled_spacing, 100, JT_SQUARE)};
) for Slic3r::Geometry::Clipper::offset(\@islands, $i * $Slic3r::first_layer_flow->scaled_spacing, undef, JT_SQUARE);
# TODO: we need the offset inwards/offset outwards logic to avoid overlapping extrusions
}
}
@ -617,9 +642,14 @@ sub write_gcode {
my $self = shift;
my ($file) = @_;
# open output gcode file
open my $fh, ">", $file
or die "Failed to open $file for writing\n";
# open output gcode file if we weren't supplied a file-handle
my $fh;
if (ref $file eq 'IO::Scalar') {
$fh = $file;
} else {
open $fh, ">", $file
or die "Failed to open $file for writing\n";
}
# write some information
my @lt = localtime;
@ -629,14 +659,17 @@ sub write_gcode {
print $fh "; $_\n" foreach split /\R/, $Slic3r::Config->notes;
print $fh "\n" if $Slic3r::Config->notes;
for (qw(layer_height perimeters solid_layers fill_density perimeter_speed infill_speed travel_speed scale)) {
for (qw(layer_height perimeters top_solid_layers bottom_solid_layers fill_density perimeter_speed infill_speed travel_speed scale)) {
printf $fh "; %s = %s\n", $_, $Slic3r::Config->$_;
}
for (qw(nozzle_diameter filament_diameter extrusion_multiplier)) {
printf $fh "; %s = %s\n", $_, $Slic3r::Config->$_->[0];
}
printf $fh "; single wall width = %.2fmm\n", $Slic3r::flow->width;
printf $fh "; first layer single wall width = %.2fmm\n", $Slic3r::first_layer_flow->width
printf $fh "; perimeters extrusion width = %.2fmm\n", $self->regions->[0]->flows->{perimeter}->width;
printf $fh "; infill extrusion width = %.2fmm\n", $self->regions->[0]->flows->{infill}->width;
printf $fh "; support material extrusion width = %.2fmm\n", $self->support_material_flow->width
if $self->support_material_flow;
printf $fh "; first layer extrusion width = %.2fmm\n", $Slic3r::first_layer_flow->width
if $Slic3r::first_layer_flow;
print $fh "\n";
@ -646,7 +679,6 @@ sub write_gcode {
);
my $min_print_speed = 60 * $Slic3r::Config->min_print_speed;
my $dec = $gcodegen->dec;
print $fh $gcodegen->set_extruder($self->extruders->[0]);
print $fh $gcodegen->set_fan(0, 1) if $Slic3r::Config->cooling && $Slic3r::Config->disable_fan_first_layers;
# write start commands to file
@ -717,20 +749,36 @@ sub write_gcode {
if $self->extruders->[$t]->temperature && $self->extruders->[$t]->temperature != $self->extruders->[$t]->first_layer_temperature;
}
$gcode .= $gcodegen->set_bed_temperature($Slic3r::Config->bed_temperature)
if $Slic3r::Config->first_layer_bed_temperature && $Slic3r::Config->bed_temperature != $Slic3r::Config->first_layer_bed_temperature;
if $Slic3r::Config->bed_temperature && $Slic3r::Config->bed_temperature != $Slic3r::Config->first_layer_bed_temperature;
}
# go to layer (just use the first one, we only need Z from it)
$gcode .= $gcodegen->change_layer($self->objects->[$object_copies->[0][0]]->layers->[$layer_id]);
# set new layer, but don't move Z as support material interfaces may need an intermediate one
$gcodegen->change_layer($self->objects->[$object_copies->[0][0]]->layers->[$layer_id]);
$gcodegen->elapsed_time(0);
# prepare callback to call as soon as a Z command is generated
$gcodegen->move_z_callback(sub {
$gcodegen->move_z_callback(undef); # circular ref or not?
return "" if !$Slic3r::Config->layer_gcode;
return $Slic3r::Config->replace_options($Slic3r::Config->layer_gcode) . "\n";
});
# extrude skirt
if ($skirt_done < $Slic3r::Config->skirt_height) {
$gcodegen->set_shift(@shift);
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->perimeter_acceleration);
$gcode .= $gcodegen->set_extruder($self->extruders->[0]); # move_z requires extruder
$gcode .= $gcodegen->move_z($gcodegen->layer->print_z);
# skip skirt if we have a large brim
if ($layer_id < $Slic3r::Config->skirt_height) {
$gcode .= $gcodegen->extrude_loop($_, 'skirt') for @{$self->skirt};
# distribute skirt loops across all extruders
for my $i (0 .. $#{$self->skirt}) {
# when printing layers > 0 ignore 'min_skirt_length' and
# just use the 'skirts' setting; also just use the current extruder
last if ($layer_id > 0) && ($i >= $Slic3r::Config->skirts);
$gcode .= $gcodegen->set_extruder($self->extruders->[ ($i/@{$self->extruders}) % @{$self->extruders} ])
if $layer_id == 0;
$gcode .= $gcodegen->extrude_loop($self->skirt->[$i], 'skirt');
}
}
$skirt_done++;
$gcodegen->straight_once(1);
@ -738,9 +786,9 @@ sub write_gcode {
# extrude brim
if ($layer_id == 0 && !$brim_done) {
$gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]);
$gcodegen->shift_x($shift[X]);
$gcodegen->shift_y($shift[Y]);
$gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]); # move_z requires extruder
$gcode .= $gcodegen->move_z($gcodegen->layer->print_z);
$gcodegen->set_shift(@shift);
$gcode .= $gcodegen->extrude_loop($_, 'brim') for @{$self->brim};
$brim_done = 1;
$gcodegen->straight_once(1);
@ -751,44 +799,58 @@ sub write_gcode {
$gcodegen->new_object(1) if $last_obj_copy && $last_obj_copy ne "${obj_idx}_${copy}";
my $layer = $self->objects->[$obj_idx]->layers->[$layer_id];
# retract explicitely because changing the shift_[xy] properties below
# won't always trigger the automatic retraction
$gcode .= $gcodegen->retract;
$gcodegen->set_shift(map $shift[$_] + unscale $copy->[$_], X,Y);
$gcodegen->set_shift(
$shift[X] + unscale $copy->[X],
$shift[Y] + unscale $copy->[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 ($Slic3r::Config->support_material) {
$gcode .= $gcodegen->move_z($layer->support_material_interface_z)
if ($layer->support_interface_fills && @{ $layer->support_interface_fills->paths });
$gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]);
if ($layer->support_interface_fills) {
$gcode .= $gcodegen->extrude_path($_, 'support material interface')
for $layer->support_interface_fills->shortest_path($gcodegen->last_pos);
}
$gcode .= $gcodegen->move_z($layer->print_z);
if ($layer->support_fills) {
$gcode .= $gcodegen->extrude_path($_, 'support material')
for $layer->support_fills->shortest_path($gcodegen->last_pos);
}
}
# set actual Z - this will force a retraction
$gcode .= $gcodegen->move_z($layer->print_z);
foreach my $region_id (0 .. ($self->regions_count-1)) {
my $layerm = $layer->regions->[$region_id];
my $region = $self->regions->[$region_id];
# extrude perimeters
$gcode .= $gcodegen->set_extruder($region->extruders->{perimeter});
$gcode .= $gcodegen->extrude($_, 'perimeter') for @{ $layerm->perimeters };
if (@{ $layerm->perimeters }) {
$gcode .= $gcodegen->set_extruder($region->extruders->{perimeter});
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->perimeter_acceleration);
$gcode .= $gcodegen->extrude($_, 'perimeter') for @{ $layerm->perimeters };
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->default_acceleration)
if $Slic3r::Config->perimeter_acceleration;
}
# extrude fills
$gcode .= $gcodegen->set_extruder($region->extruders->{infill});
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->infill_acceleration);
for my $fill (@{ $layerm->fills }) {
if ($fill->isa('Slic3r::ExtrusionPath::Collection')) {
$gcode .= $gcodegen->extrude($_, 'fill')
for $fill->shortest_path($gcodegen->last_pos);
} else {
$gcode .= $gcodegen->extrude($fill, 'fill') ;
if (@{ $layerm->fills }) {
$gcode .= $gcodegen->set_extruder($region->extruders->{infill});
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->infill_acceleration);
for my $fill (@{ $layerm->fills }) {
if ($fill->isa('Slic3r::ExtrusionPath::Collection')) {
$gcode .= $gcodegen->extrude($_, 'fill')
for $fill->shortest_path($gcodegen->last_pos);
} else {
$gcode .= $gcodegen->extrude($fill, 'fill') ;
}
}
$gcode .= $gcodegen->set_acceleration($Slic3r::Config->default_acceleration)
if $Slic3r::Config->infill_acceleration;
}
}
# extrude support material
if ($layer->support_fills) {
$gcode .= $gcodegen->set_extruder($self->extruders->[$Slic3r::Config->support_material_extruder-1]);
$gcode .= $gcodegen->extrude_path($_, 'support material')
for $layer->support_fills->shortest_path($gcodegen->last_pos);
}
$last_obj_copy = "${obj_idx}_${copy}";
}
return if !$gcode;
@ -808,7 +870,7 @@ sub write_gcode {
Slic3r::debugf " fan = %d%%, speed = %d%%\n", $fan_speed, $speed_factor * 100;
if ($speed_factor < 1) {
$gcode =~ s/^(?=.*? [XY])(?=.*? E)(G1 .*?F)(\d+(?:\.\d+)?)/
$gcode =~ s/^(?=.*? [XY])(?=.*? E)(?<!;_BRIDGE_FAN_START\n)(G1 .*?F)(\d+(?:\.\d+)?)/
my $new_speed = $2 * $speed_factor;
$1 . sprintf("%.${dec}f", $new_speed < $min_print_speed ? $min_print_speed : $new_speed)
/gexm;
@ -842,10 +904,7 @@ sub write_gcode {
# this happens before Z goes down to layer 0 again, so that
# no collision happens hopefully.
if ($finished_objects > 0) {
$gcodegen->set_shift(
$shift[X] + unscale $copy->[X],
$shift[Y] + unscale $copy->[Y],
);
$gcodegen->set_shift(map $shift[$_] + unscale $copy->[$_], X,Y);
print $fh $gcodegen->retract;
print $fh $gcodegen->G0(Slic3r::Point->new(0,0), undef, 0, 'move to origin position for next object');
}
@ -880,12 +939,21 @@ sub write_gcode {
# write end commands to file
print $fh $gcodegen->retract;
print $fh $gcodegen->set_fan(0);
print $fh "M501 ; reset acceleration\n" if $Slic3r::Config->acceleration;
printf $fh "%s\n", $Slic3r::Config->replace_options($Slic3r::Config->end_gcode);
printf $fh "; filament used = %.1fmm (%.1fcm3)\n",
$self->total_extrusion_length, $self->total_extrusion_volume;
if ($Slic3r::Config->gcode_comments) {
# append full config
print $fh "\n";
foreach my $opt_key (sort keys %{$Slic3r::Config}) {
next if $Slic3r::Config::Options->{$opt_key}{shortcut};
next if $Slic3r::Config::Options->{$opt_key}{gui_only};
printf $fh "; %s = %s\n", $opt_key, $Slic3r::Config->serialize($opt_key);
}
}
# close our gcode file
close $fh;
}
@ -903,6 +971,7 @@ sub expanded_output_filepath {
# if no input file was supplied, take the first one from our objects
$input_file ||= $self->objects->[0]->input_file;
return undef if !defined $input_file;
# if output path is an existing directory, we take that and append
# the specified filename format

View File

@ -2,7 +2,7 @@ package Slic3r::Print::Object;
use Moo;
use Slic3r::ExtrusionPath ':roles';
use Slic3r::Geometry qw(scale unscale deg2rad scaled_epsilon);
use Slic3r::Geometry qw(Z PI scale unscale deg2rad rad2deg scaled_epsilon);
use Slic3r::Geometry::Clipper qw(diff_ex intersection_ex union_ex);
use Slic3r::Surface ':types';
@ -13,21 +13,36 @@ has 'size' => (is => 'rw', required => 1);
has 'copies' => (is => 'rw', default => sub {[ [0,0] ]});
has 'layers' => (is => 'rw', default => sub { [] });
sub BUILD {
my $self = shift;
# make layers
while (!@{$self->layers} || $self->layers->[-1]->slice_z < $self->size->[Z]) {
push @{$self->layers}, Slic3r::Layer->new(
object => $self,
id => $#{$self->layers} + 1,
);
}
}
sub layer_count {
my $self = shift;
return scalar @{ $self->layers };
}
sub layer {
sub get_layer_range {
my $self = shift;
my ($layer_id) = @_;
my ($min_z, $max_z) = @_;
# extend our print by creating all necessary layers
for (my $i = $self->layer_count; $i <= $layer_id; $i++) {
push @{ $self->layers }, Slic3r::Layer->new(id => $i, object => $self);
my ($min_layer, $max_layer) = (0, undef);
for my $layer (@{$self->layers}) {
$min_layer = $layer->id if $layer->slice_z <= $min_z;
if ($layer->slice_z >= $max_z) {
$max_layer = $layer->id;
last;
}
}
return $self->layers->[$layer_id];
return ($min_layer, $max_layer);
}
sub slice {
@ -41,7 +56,7 @@ sub slice {
my $apply_lines = sub {
my $lines = shift;
foreach my $layer_id (keys %$lines) {
my $layerm = $self->layer($layer_id)->region($region_id);
my $layerm = $self->layers->[$layer_id]->region($region_id);
push @{$layerm->lines}, @{$lines->{$layer_id}};
}
};
@ -304,14 +319,14 @@ sub detect_surfaces_type {
# clip surfaces to the fill boundaries
foreach my $layer (@{$self->layers}) {
my $layerm = $layer->regions->[$region_id];
my $fill_boundaries = [ map @$_, @{$layerm->surfaces} ];
@{$layerm->surfaces} = ();
my $fill_boundaries = [ map @$_, @{$layerm->fill_surfaces} ];
@{$layerm->fill_surfaces} = ();
foreach my $surface (@{$layerm->slices}) {
my $intersection = intersection_ex(
[ $surface->p ],
$fill_boundaries,
);
push @{$layerm->surfaces}, map Slic3r::Surface->new
push @{$layerm->fill_surfaces}, map Slic3r::Surface->new
(expolygon => $_, surface_type => $surface->surface_type),
@$intersection;
}
@ -336,29 +351,29 @@ sub discover_horizontal_shells {
}
foreach my $type (S_TYPE_TOP, S_TYPE_BOTTOM) {
# find surfaces of current type for current layer
# and offset them to take perimeters into account
my @surfaces = map $_->offset($Slic3r::Config->perimeters * $layerm->perimeter_flow->scaled_width),
grep $_->surface_type == $type, @{$layerm->fill_surfaces} or next;
# find slices of current type for current layer
my @surfaces = grep $_->surface_type == $type, @{$layerm->slices} or next;
my $surfaces_p = [ map $_->p, @surfaces ];
Slic3r::debugf "Layer %d has %d surfaces of type '%s'\n",
$i, scalar(@surfaces), ($type == S_TYPE_TOP ? 'top' : 'bottom');
my $solid_layers = ($type == S_TYPE_TOP)
? $Slic3r::Config->top_solid_layers
: $Slic3r::Config->bottom_solid_layers;
for (my $n = $type == S_TYPE_TOP ? $i-1 : $i+1;
abs($n - $i) <= $Slic3r::Config->solid_layers-1;
abs($n - $i) <= $solid_layers-1;
$type == S_TYPE_TOP ? $n-- : $n++) {
next if $n < 0 || $n >= $self->layer_count;
Slic3r::debugf " looking for neighbors on layer %d...\n", $n;
my @neighbor_surfaces = @{$self->layers->[$n]->regions->[$region_id]->surfaces};
my @neighbor_fill_surfaces = @{$self->layers->[$n]->regions->[$region_id]->fill_surfaces};
# find intersection between neighbor and current layer's surfaces
# intersections have contours and holes
my $new_internal_solid = intersection_ex(
$surfaces_p,
[ map $_->p, grep { $_->surface_type == S_TYPE_INTERNAL || $_->surface_type == S_TYPE_INTERNALSOLID } @neighbor_surfaces ],
[ map $_->p, grep { $_->surface_type == S_TYPE_INTERNAL || $_->surface_type == S_TYPE_INTERNALSOLID } @neighbor_fill_surfaces ],
undef, 1,
);
next if !@$new_internal_solid;
@ -410,6 +425,15 @@ sub discover_horizontal_shells {
@{$layerm->fill_surfaces} = grep $_->expolygon->area > $area_threshold, @{$layerm->fill_surfaces};
}
for (my $i = 0; $i < $self->layer_count; $i++) {
my $layerm = $self->layers->[$i]->regions->[$region_id];
# if hollow object is requested, remove internal surfaces
if ($Slic3r::Config->fill_density == 0) {
@{$layerm->fill_surfaces} = grep $_->surface_type != S_TYPE_INTERNAL, @{$layerm->fill_surfaces};
}
}
}
}
@ -431,8 +455,8 @@ sub combine_infill {
# for each possible depth, look for intersections with the lower layer
# we do this from the greater depth to the smaller
for (my $d = $Slic3r::Config->infill_every_layers - 1; $d >= 1; $d--) {
next if ($i - $d) < 0;
my $lower_layerm = $self->layer($i - 1)->regions->[$region_id];
next if ($i - $d) <= 0; # do not combine infill for bottom layer
my $lower_layerm = $self->layers->[$i - 1]->regions->[$region_id];
# select surfaces of the lower layer having the depth we're looking for
my @lower_surfaces = grep $_->depth_layers == $d && $_->surface_type == S_TYPE_INTERNAL,
@ -507,14 +531,22 @@ sub combine_infill {
sub generate_support_material {
my $self = shift;
my $threshold_rad = $Slic3r::Config->support_material_threshold
? deg2rad($Slic3r::Config->support_material_threshold + 1) # +1 makes the threshold inclusive
: PI/2 - atan2($self->layers->[1]->regions->[0]->perimeter_flow->width/$Slic3r::Config->layer_height/2, 1);
Slic3r::debugf "Threshold angle = %d°\n", rad2deg($threshold_rad);
my $flow = $self->print->support_material_flow;
my $threshold_rad = deg2rad($Slic3r::Config->support_material_threshold + 1); # +1 makes the threshold inclusive
my $overhang_width = $threshold_rad == 0 ? undef : scale $Slic3r::Config->layer_height * ((cos $threshold_rad) / (sin $threshold_rad));
my $distance_from_object = 1.5 * $flow->scaled_width;
my $pattern_spacing = ($Slic3r::Config->support_material_spacing > $flow->spacing)
? $Slic3r::Config->support_material_spacing
: $flow->spacing;
# determine support regions in each layer (for upper layers)
Slic3r::debugf "Detecting regions\n";
my %layers = ();
my %layers = (); # this represents the areas of each layer having to support upper layers (excluding interfaces)
my %layers_interfaces = (); # this represents the areas of each layer having an overhang in the immediately upper layer
{
my @current_support_regions = (); # expolygons we've started to support (i.e. below the empty interface layers)
my @queue = (); # the number of items of this array determines the number of empty interface layers
@ -522,6 +554,10 @@ sub generate_support_material {
my $layer = $self->layers->[$i];
my $lower_layer = $i > 0 ? $self->layers->[$i-1] : undef;
# $queue[-1] contains the overhangs of the upper layer, regardless of any empty interface layers
# $queue[0] contains the overhangs of the first upper layer above the empty interface layers
$layers_interfaces{$i} = [@{ $queue[-1] || [] }];
# step 1: generate support material in current layer (for upper layers)
push @current_support_regions, @{ shift @queue } if @queue && $i < $#{$self->layers};
@ -532,7 +568,10 @@ sub generate_support_material {
$layers{$i} = diff_ex(
[ map @$_, @current_support_regions ],
[ map @$_, map $_->offset_ex($distance_from_object), @{$layer->slices} ],
[
(map @$_, map $_->offset_ex($distance_from_object), @{$layer->slices}),
(map @$_, @{ $layers_interfaces{$i} }),
],
);
$_->simplify($flow->scaled_spacing * 2) for @{$layers{$i}};
@ -559,15 +598,14 @@ sub generate_support_material {
my @support_material_areas = map $_->offset_ex(- 0.5 * $flow->scaled_width),
@{union_ex([ map $_->contour, map @$_, values %layers ])};
my $fill = Slic3r::Fill->new(print => $self->print);
my $filler = $fill->filler($Slic3r::Config->support_material_pattern);
my $filler = Slic3r::Fill->filler($Slic3r::Config->support_material_pattern);
$filler->angle($Slic3r::Config->support_material_angle);
{
my @patterns = ();
foreach my $expolygon (@support_material_areas) {
my @paths = $filler->fill_surface(
Slic3r::Surface->new(expolygon => $expolygon),
density => $flow->spacing / $Slic3r::Config->support_material_spacing,
density => $flow->spacing / $pattern_spacing,
flow_spacing => $flow->spacing,
);
my $params = shift @paths;
@ -576,7 +614,7 @@ sub generate_support_material {
map Slic3r::ExtrusionPath->new(
polyline => Slic3r::Polyline->new(@$_),
role => EXTR_ROLE_SUPPORTMATERIAL,
depth_layers => 1,
height => undef,
flow_spacing => $params->{flow_spacing},
), @paths;
}
@ -585,7 +623,7 @@ sub generate_support_material {
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "support_$_.svg",
Slic3r::SVG::output("support_$_.svg",
polylines => [ map $_->polyline, map @$_, $support_patterns->[$_] ],
polygons => [ map @$_, @support_material_areas ],
) for 0 .. $#$support_patterns;
@ -596,12 +634,13 @@ sub generate_support_material {
Slic3r::debugf "Applying patterns\n";
{
my $clip_pattern = sub {
my ($layer_id, $expolygons) = @_;
my ($layer_id, $expolygons, $height) = @_;
my @paths = ();
foreach my $expolygon (@$expolygons) {
push @paths,
map $_->pack,
map {
$_->height($height);
$_->flow_spacing($self->print->first_layer_support_material_flow->spacing)
if $layer_id == 0;
$_;
@ -612,30 +651,44 @@ sub generate_support_material {
};
return @paths;
};
my %layer_paths = ();
my %layer_paths = ();
my %layer_interface_paths = ();
my %layer_islands = ();
my $process_layer = sub {
my ($layer_id) = @_;
my $layer = $self->layers->[$layer_id];
my $paths = [ $clip_pattern->($layer_id, $layers{$layer_id}, $layer->height) ];
my $interface_paths = [ $clip_pattern->($layer_id, $layers_interfaces{$layer_id}, $layer->support_material_interface_height) ];
my $islands = union_ex([ map @$_, map @$_, $layers{$layer_id}, $layers_interfaces{$layer_id} ]);
return ($paths, $interface_paths, $islands);
};
Slic3r::parallelize(
items => [ keys %layers ],
thread_cb => sub {
my $q = shift;
my $paths = {};
my $result = {};
while (defined (my $layer_id = $q->dequeue)) {
$paths->{$layer_id} = [ $clip_pattern->($layer_id, $layers{$layer_id}) ];
$result->{$layer_id} = [ $process_layer->($layer_id) ];
}
return $paths;
return $result;
},
collect_cb => sub {
my $paths = shift;
$layer_paths{$_} = $paths->{$_} for keys %$paths;
my $result = shift;
($layer_paths{$_}, $layer_interface_paths{$_}, $layer_islands{$_}) = @{$result->{$_}} for keys %$result;
},
no_threads_cb => sub {
$layer_paths{$_} = [ $clip_pattern->($_, $layers{$_}) ] for keys %layers;
($layer_paths{$_}, $layer_interface_paths{$_}, $layer_islands{$_}) = $process_layer->($_) for keys %layers;
},
);
foreach my $layer_id (keys %layer_paths) {
my $layer = $self->layers->[$layer_id];
$layer->support_islands($layer_islands{$layer_id});
$layer->support_fills(Slic3r::ExtrusionPath::Collection->new);
$layer->support_interface_fills(Slic3r::ExtrusionPath::Collection->new);
push @{$layer->support_fills->paths}, @{$layer_paths{$layer_id}};
push @{$layer->support_interface_fills->paths}, @{$layer_interface_paths{$layer_id}};
}
}
}

View File

@ -12,9 +12,7 @@ sub factor {
}
sub svg {
my ($print) = @_;
my $svg = SVG->new(width => 200 * 10, height => 200 * 10);
my $marker_end = $svg->marker(
id => "endArrow",
viewBox => "0 0 10 10",
@ -34,9 +32,9 @@ sub svg {
}
sub output {
my ($print, $filename, %things) = @_;
my ($filename, %things) = @_;
my $svg = svg($print);
my $svg = svg();
foreach my $type (qw(polygons polylines white_polygons green_polygons red_polygons red_polylines)) {
if ($things{$type}) {

138
lib/Slic3r/Test.pm Normal file
View File

@ -0,0 +1,138 @@
package Slic3r::Test;
use strict;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(_eq);
use IO::Scalar;
use List::Util qw(first);
use Slic3r::Geometry qw(epsilon X Y Z);
my %cuboids = (
'20mm_cube' => [20,20,20],
'2x20x10' => [2, 20,10],
);
sub init_print {
my ($model_name, %params) = @_;
my $model = Slic3r::Model->new;
{
my ($vertices, $facets);
if ($cuboids{$model_name}) {
my ($x, $y, $z) = @{ $cuboids{$model_name} };
$vertices = [
[$x,$y,0], [$x,0,0], [0,0,0], [0,$y,0], [$x,$y,$z], [0,$y,$z], [0,0,$z], [$x,0,$z],
];
$facets = [
[0,1,2], [0,2,3], [4,5,6], [4,6,7], [0,4,7], [0,7,1], [1,7,6], [1,6,2], [2,6,5], [2,5,3], [4,0,3], [4,3,5],
],
}
$model->add_object(vertices => $vertices)->add_volume(facets => $facets);
}
my $config = Slic3r::Config->new_from_defaults;
$config->apply($params{config}) if $params{config};
$config->set('gcode_comments', 1) if $ENV{SLIC3R_TESTS_GCODE};
my $print = Slic3r::Print->new(config => $config);
$print->add_model($model);
$print->validate;
return $print;
}
sub gcode {
my ($print) = @_;
my $fh = IO::Scalar->new(\my $gcode);
$print->export_gcode(output_fh => $fh, quiet => 1);
$fh->close;
return $gcode;
}
sub _eq {
my ($a, $b) = @_;
return abs($a - $b) < epsilon;
}
sub add_facet {
my ($facet, $vertices, $facets) = @_;
push @$facets, [];
for my $i (0..2) {
my $v = first { $vertices->[$_][X] == $facet->[$i][X] && $vertices->[$_][Y] == $facet->[$i][Y] && $vertices->[$_][Z] == $facet->[$i][Z] } 0..$#$vertices;
if (!defined $v) {
push @$vertices, [ @{$facet->[$i]}[X,Y,Z] ];
$v = $#$vertices;
}
$facets->[-1][$i] = $v;
}
}
package Slic3r::Test::GCodeReader;
use Moo;
has 'gcode' => (is => 'ro', required => 1);
has 'X' => (is => 'rw', default => sub {0});
has 'Y' => (is => 'rw', default => sub {0});
has 'Z' => (is => 'rw', default => sub {0});
has 'E' => (is => 'rw', default => sub {0});
has 'F' => (is => 'rw', default => sub {0});
our $Verbose = 0;
my @AXES = qw(X Y Z E);
sub parse {
my $self = shift;
my ($cb) = @_;
foreach my $line (split /\n/, $self->gcode) {
print "$line\n" if $Verbose || $ENV{SLIC3R_TESTS_GCODE};
$line =~ s/\s*;(.*)//; # strip comment
next if $line eq '';
my $comment = $1;
# parse command
my ($command, @args) = split /\s+/, $line;
my %args = map { /([A-Z])(.*)/; ($1 => $2) } @args;
my %info = ();
# check retraction
if ($command =~ /^G[01]$/) {
if (!exists $args{E}) {
$info{travel} = 1;
}
foreach my $axis (@AXES) {
if (!exists $args{$axis}) {
$info{"dist_$axis"} = 0;
next;
}
$info{"dist_$axis"} = $args{$axis} - $self->$axis;
}
$info{dist_XY} = Slic3r::Line->new([0,0], [@info{qw(dist_X dist_Y)}])->length;
if (exists $args{E}) {
($info{dist_E} > 0)
? ($info{extruding} = 1)
: ($info{retracting} = 1);
}
}
# run callback
$cb->($self, $command, \%args, \%info);
# update coordinates
if ($command =~ /^(?:G[01]|G92)$/) {
for (@AXES, 'F') {
$self->$_($args{$_}) if exists $args{$_};
}
}
# TODO: update temperatures
}
}
1;

View File

@ -2,6 +2,7 @@ package Slic3r::TriangleMesh;
use Moo;
use Slic3r::Geometry qw(X Y Z A B unscale same_point);
use Slic3r::Geometry::Clipper qw(union_ex);
# public
has 'vertices' => (is => 'ro', required => 1); # id => [$x,$y,$z]
@ -154,7 +155,9 @@ sub check_manifoldness {
warn sprintf "Warning: The input file contains a hole near edge %f-%f (not manifold). "
. "You might want to repair it and retry, or to check the resulting G-code before printing anyway.\n",
@{$self->edges->[$first_bad_edge_id]};
return 0;
}
return 1;
}
sub unpack_line {
@ -238,7 +241,7 @@ sub make_loops {
Slic3r::debugf " this shouldn't happen and should be further investigated\n";
if (0) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output(undef, "same_point.svg",
Slic3r::SVG::output("same_point.svg",
lines => [ map $_->line, grep !defined $_->[I_FACET_EDGE], @lines ],
red_lines => [ map $_->line, grep defined $_->[I_FACET_EDGE], @lines ],
#points => [ $self->vertices->[$point_id] ],
@ -412,14 +415,12 @@ sub slice_facet {
}
# calculate the layer extents
my $min_layer = int((unscale($min_z) - ($Slic3r::Config->get_value('first_layer_height') + $Slic3r::Config->layer_height / 2)) / $Slic3r::Config->layer_height) - 2;
$min_layer = 0 if $min_layer < 0;
my $max_layer = int((unscale($max_z) - ($Slic3r::Config->get_value('first_layer_height') + $Slic3r::Config->layer_height / 2)) / $Slic3r::Config->layer_height) + 2;
my ($min_layer, $max_layer) = $print_object->get_layer_range($min_z, $max_z);
Slic3r::debugf "layers: min = %s, max = %s\n", $min_layer, $max_layer;
my $lines = {}; # layer_id => [ lines ]
for (my $layer_id = $min_layer; $layer_id <= $max_layer; $layer_id++) {
my $layer = $print_object->layer($layer_id);
my $layer = $print_object->layers->[$layer_id];
$lines->{$layer_id} ||= [];
push @{ $lines->{$layer_id} }, $self->intersect_facet($facet_id, $layer->slice_z);
}
@ -576,4 +577,19 @@ sub split_mesh {
return @meshes;
}
sub horizontal_projection {
my $self = shift;
my @f = ();
foreach my $facet (@{$self->facets}) {
push @f, Slic3r::Polygon->new([ map [ @{$self->vertices->[$_]}[X,Y] ], @$facet ]);
}
$_->make_counter_clockwise for @f;
my $scale_vector = Math::Clipper::integerize_coordinate_sets({ bits => 32 }, @f);
my $union = union_ex([ Slic3r::Geometry::Clipper::offset(\@f, 10000) ]);
Math::Clipper::unscale_coordinate_sets($scale_vector, $_) for @$union;
return $union;
}
1;

View File

@ -164,6 +164,8 @@ $j
--g0 Use G0 commands for retraction (experimental, not supported by all
firmwares)
--gcode-comments Make G-code verbose by adding comments (default: no)
--vibration-limit Limit the frequency of moves on X and Y axes (Hz, set zero to disable;
default: $config->{vibration_limit})
Filament options:
--filament-diameter Diameter in mm of your raw filament (default: $config->{filament_diameter}->[0])
@ -192,10 +194,24 @@ $j
(default: $config->{solid_infill_speed})
--top-solid-infill-speed Speed of print moves for top surfaces in mm/s or % over solid infill speed
(default: $config->{top_solid_infill_speed})
--support-material-speed
Speed of support material print moves in mm/s (default: $config->{support_material_speed})
--bridge-speed Speed of bridge print moves in mm/s (default: $config->{bridge_speed})
--gap-fill-speed Speed of gap fill print moves in mm/s (default: $config->{gap_fill_speed})
--first-layer-speed Speed of print moves for bottom layer, expressed either as an absolute
value or as a percentage over normal speeds (default: $config->{first_layer_speed})
Acceleration options:
--perimeter-acceleration
Overrides firmware's default acceleration for perimeters. (mm/s^2, set zero
to disable; default: $config->{perimeter_acceleration})
--infill-acceleration
Overrides firmware's default acceleration for infill. (mm/s^2, set zero
to disable; default: $config->{infill_acceleration})
--default-acceleration
Acceleration will be reset to this value after the specific settings above
have been applied. (mm/s^2, set zero to disable; default: $config->{travel_speed})
Accuracy options:
--layer-height Layer height in mm (default: $config->{layer_height})
--first-layer-height Layer height for first layer (mm or %, default: $config->{first_layer_height})
@ -206,8 +222,9 @@ $j
Print options:
--perimeters Number of perimeters/horizontal skins (range: 0+, default: $config->{perimeters})
--solid-layers Number of solid layers to do for top/bottom surfaces
(range: 1+, default: $config->{solid_layers})
--top-solid-layers Number of solid layers to do for top surfaces (range: 0+, default: $config->{top_solid_layers})
--bottom-solid-layers Number of solid layers to do for bottom surfaces (range: 0+, default: $config->{bottom_solid_layers})
--solid-layers Shortcut for setting the two options above at once
--fill-density Infill density (range: 0-1, 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})
@ -218,6 +235,7 @@ $j
the default commands (turn off temperature [M104 S0],
home X axis [G28 X], disable motors [M84]).
--layer-gcode Load layer-change G-code from the supplied file (default: nothing).
--toolchange-gcode Load tool-change G-code from the supplied file (default: nothing).
--extra-perimeters Add more perimeters when needed (default: yes)
--randomize-start Randomize starting point across layers (default: yes)
--avoid-crossing-perimeters Optimize travel moves so that no perimeters are crossed (default: no)
@ -231,7 +249,8 @@ $j
Support material options:
--support-material Generate support material for overhangs
--support-material-threshold
Overhang threshold angle (range: 0-90, default: $config->{support_material_threshold})
Overhang threshold angle (range: 0-90, set 0 for automatic detection,
default: $config->{support_material_threshold})
--support-material-pattern
Pattern to use for support material (default: $config->{support_material_pattern})
--support-material-spacing
@ -275,6 +294,8 @@ $j
--skirt-distance Distance in mm between innermost skirt and object
(default: $config->{skirt_distance})
--skirt-height Height of skirts to draw (expressed in layers, 0+, default: $config->{skirt_height})
--min-skirt-length Generate no less than the number of loops required to consume this length
of filament on the first layer, for each extruder (mm, 0+, default: $config->{min_skirt_length})
--brim-width Width of the brim that will get added to each object to help adhesion
(mm, default: $config->{brim_width})

View File

@ -2,7 +2,7 @@ use Test::More;
use strict;
use warnings;
plan tests => 24;
plan tests => 26;
BEGIN {
use FindBin;
@ -10,7 +10,7 @@ BEGIN {
}
use Slic3r;
use Slic3r::Geometry qw(line_atan line_direction rad2deg_dir PI);
use Slic3r::Geometry qw(line_atan line_direction rad2deg_dir angle3points PI);
#==========================================================
@ -54,3 +54,10 @@ use Slic3r::Geometry qw(line_atan line_direction rad2deg_dir PI);
}
#==========================================================
{
is angle3points([0,0], [10,0], [0,10]), PI/2, 'CW angle3points';
is angle3points([0,0], [0,10], [10,0]), PI/2*3, 'CCW angle3points';
}
#==========================================================

View File

@ -2,52 +2,72 @@ use Test::More;
use strict;
use warnings;
plan tests => 1;
plan tests => 3;
use Math::Clipper ':all';
my $clipper = Math::Clipper->new;
my $square = [ # ccw
[10, 10],
[20, 10],
[20, 20],
[10, 20],
];
my $hole_in_square = [ # cw
[14, 14],
[14, 16],
[16, 16],
[16, 14],
];
my $square2 = [ # ccw
[5, 12],
[25, 12],
[25, 18],
[5, 18],
];
$clipper->add_subject_polygons([ $square, $hole_in_square ]);
$clipper->add_clip_polygons([ $square2 ]);
my $intersection = $clipper->ex_execute(CT_INTERSECTION, PFT_NONZERO, PFT_NONZERO);
is_deeply $intersection, [
{
holes => [
[
[14, 16],
[16, 16],
[16, 14],
[14, 14],
{
my $square = [ # ccw
[10, 10],
[20, 10],
[20, 20],
[10, 20],
];
my $hole_in_square = [ # cw
[14, 14],
[14, 16],
[16, 16],
[16, 14],
];
my $square2 = [ # ccw
[5, 12],
[25, 12],
[25, 18],
[5, 18],
];
my $clipper = Math::Clipper->new;
$clipper->add_subject_polygons([ $square, $hole_in_square ]);
$clipper->add_clip_polygons([ $square2 ]);
my $intersection = $clipper->ex_execute(CT_INTERSECTION, PFT_NONZERO, PFT_NONZERO);
is_deeply $intersection, [
{
holes => [
[
[14, 16],
[16, 16],
[16, 14],
[14, 14],
],
],
],
outer => [
[10, 18],
[10, 12],
[20, 12],
[20, 18],
],
},
], 'hole is preserved after intersection';
outer => [
[10, 18],
[10, 12],
[20, 12],
[20, 18],
],
},
], 'hole is preserved after intersection';
}
#==========================================================
{
my $contour1 = [ [0,0], [40,0], [40,40], [0,40] ]; # ccw
my $contour2 = [ [10,10], [30,10], [30,30], [10,30] ]; # ccw
my $hole = [ [15,15], [15,25], [25,25], [25,15] ]; # cw
my $clipper = Math::Clipper->new;
$clipper->add_subject_polygons([ $contour1, $contour2, $hole ]);
my $union = $clipper->ex_execute(CT_UNION, PFT_NONZERO, PFT_NONZERO);
is_deeply $union, [{ holes => [], outer => [ [0,40], [0,0], [40,0], [40,40] ] }],
'union of two ccw and one cw is a contour with no holes';
$clipper->clear;
$clipper->add_subject_polygons([ $contour1, $contour2 ]);
$clipper->add_clip_polygons([ $hole ]);
my $diff = $clipper->ex_execute(CT_DIFFERENCE, PFT_NONZERO, PFT_NONZERO);
is_deeply $diff, [{ holes => [[ [15,25], [25,25], [25,15], [15,15] ]], outer => [ [0,40], [0,0], [40,0], [40,40] ] }],
'difference of a cw from two ccw is a contour with one hole';
}

52
t/custom_gcode.t Normal file
View File

@ -0,0 +1,52 @@
use Test::More tests => 2;
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use Slic3r;
use Slic3r::Test;
{
my $config = Slic3r::Config->new_from_defaults;
my $test = sub {
my ($conf) = @_;
$conf ||= $config;
my $print = Slic3r::Test::init_print('2x20x10', config => $conf);
my $last_move_was_z_change = 0;
Slic3r::Test::GCodeReader->new(gcode => Slic3r::Test::gcode($print))->parse(sub {
my ($self, $cmd, $args, $info) = @_;
if ($last_move_was_z_change && $cmd ne $config->layer_gcode) {
fail 'custom layer G-code was not applied after Z change';
}
if (!$last_move_was_z_change && $cmd eq $config->layer_gcode) {
fail 'custom layer G-code was not applied after Z change';
}
$last_move_was_z_change = (defined $info->{dist_Z} && $info->{dist_Z} > 0);
});
1;
};
$config->set('layer_gcode', '_MY_CUSTOM_GCODE_');
ok $test->(), "custom layer G-code is applied after Z move and before other moves";
}
#==========================================================
{
my $config = Slic3r::Config->new_from_defaults;
is $config->replace_options('[temperature_[foo]]', { foo => '0' }),
200,
"nested config options";
}
__END__

92
t/dynamic.t Normal file
View File

@ -0,0 +1,92 @@
use Test::More;
use strict;
use warnings;
plan skip_all => 'variable-width paths are currently disabled';
plan tests => 20;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use List::Util qw(first);
use Slic3r;
use Slic3r::Geometry qw(X Y scale epsilon);
use Slic3r::Surface ':types';
sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
{
my $square = Slic3r::ExPolygon->new([
scale_points [0,0], [10,0], [10,10], [0,10],
]);
my @offsets = $square->noncollapsing_offset_ex(- scale 5);
is scalar @offsets, 1, 'non-collapsing offset';
}
{
local $Slic3r::Config = Slic3r::Config->new(
perimeters => 3,
);
my $w = 0.7;
my $perimeter_flow = Slic3r::Flow->new(
nozzle_diameter => 0.5,
layer_height => 0.4,
width => $w,
);
my $print = Slic3r::Print->new;
my $region = Slic3r::Print::Region->new(
print => $print,
flows => { perimeter => $perimeter_flow },
);
push @{$print->regions}, $region;
my $object = Slic3r::Print::Object->new(
print => $print,
size => [1,1],
);
my $make_layer = sub {
my ($width) = @_;
my $layer = Slic3r::Layer->new(
object => $object,
id => 1,
slices => [
Slic3r::Surface->new(
surface_type => S_TYPE_INTERNAL,
expolygon => Slic3r::ExPolygon->new([ scale_points [0,0], [50,0], [50,$width], [0,$width] ]),
),
],
thin_walls => [],
);
my $layerm = $layer->region(0);
$layer->make_perimeters;
return $layerm;
};
my %widths = (
1 * $w => { perimeters => 1, gaps => 0 },
1.3 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 0.2 * $w)->spacing },
1.5 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 0.5 * $w)->spacing },
2 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->spacing },
2.5 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 1.5 * $w)->spacing },
3 * $w => { perimeters => 2, gaps => 0 },
4 * $w => { perimeters => 2, gaps => 1, gap_flow_spacing => $perimeter_flow->spacing },
);
foreach my $width (sort keys %widths) {
my $layerm = $make_layer->($width);
is scalar @{$layerm->perimeters}, $widths{$width}{perimeters}, 'right number of perimeters';
is scalar @{$layerm->thin_fills} ? 1 : 0, $widths{$width}{gaps},
($widths{$width}{gaps} ? 'gaps were filled' : 'no gaps detected'); # TODO: we should check the exact number of gaps, but we need a better medial axis algorithm
my @gaps = map $_->unpack, @{$layerm->thin_fills};
if (@gaps) {
ok +(!first { abs($_->flow_spacing - $widths{$width}{gap_flow_spacing}) > epsilon } @gaps),
'flow spacing was dynamically adjusted';
}
}
}
__END__

View File

@ -2,7 +2,7 @@ use Test::More;
use strict;
use warnings;
plan tests => 4;
plan tests => 9;
BEGIN {
use FindBin;
@ -12,6 +12,7 @@ BEGIN {
use Slic3r;
use Slic3r::Geometry qw(scale X Y);
use Slic3r::Surface qw(:types);
use Slic3r::Test;
sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
@ -29,10 +30,7 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
}
{
my $filler = Slic3r::Fill::Rectilinear->new(
print => Slic3r::Print->new,
max_print_dimension => scale 100,
);
my $filler = Slic3r::Fill::Rectilinear->new;
my $surface = Slic3r::Surface->new(
surface_type => S_TYPE_TOP,
expolygon => Slic3r::ExPolygon->new([ scale_points [0,0], [50,0], [50,50], [0,50] ]),
@ -44,4 +42,57 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
}
}
{
my $collection = Slic3r::Polyline::Collection->new(polylines => [
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
]);
is_deeply
[ map $_->[Y], map @$_, $collection->shortest_path(Slic3r::Point->new(0,30)) ],
[20, 18, 15, 10, 8, 5],
'shortest path';
}
{
my $collection = Slic3r::Polyline::Collection->new(polylines => [
Slic3r::Polyline->new([4,0], [10,0], [15,0]),
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
]);
is_deeply
[ map $_->[X], map @$_, $collection->shortest_path(Slic3r::Point->new(30,0)) ],
[reverse 4, 10, 15, 10, 15, 20],
'shortest path';
}
{
my $collection = Slic3r::ExtrusionPath::Collection->new(paths => [
map Slic3r::ExtrusionPath->pack(polyline => $_, role => 0),
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
]);
is_deeply
[ map $_->[Y], map @{$_->unpack->polyline}, $collection->shortest_path(Slic3r::Point->new(0,30)) ],
[20, 18, 15, 10, 8, 5],
'shortest path';
}
{
my $collection = Slic3r::ExtrusionPath::Collection->new(paths => [
map Slic3r::ExtrusionPath->pack(polyline => $_, role => 0),
Slic3r::Polyline->new([15,0], [10,0], [4,0]),
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
]);
is_deeply
[ map $_->[X], map @{$_->unpack->polyline}, $collection->shortest_path(Slic3r::Point->new(30,0)) ],
[reverse 4, 10, 15, 10, 15, 20],
'shortest path';
}
{
my $config = Slic3r::Config->new_from_defaults;
$config->set('fill_pattern', 'hilbertcurve');
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
ok Slic3r::Test::gcode($print), 'successful hilbertcurve infill generation';
}
__END__

58
t/layers.t Normal file
View File

@ -0,0 +1,58 @@
use Test::More tests => 4;
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use List::Util qw(first);
use Slic3r;
use Slic3r::Test qw(_eq);
my $config = Slic3r::Config->new_from_defaults;
my $test = sub {
my ($conf) = @_;
$conf ||= $config;
my $print = Slic3r::Test::init_print('20mm_cube', config => $conf);
my @z = ();
my @increments = ();
Slic3r::Test::GCodeReader->new(gcode => Slic3r::Test::gcode($print))->parse(sub {
my ($self, $cmd, $args, $info) = @_;
if ($info->{dist_Z}) {
push @z, 1*$args->{Z};
push @increments, $info->{dist_Z};
}
});
fail 'wrong first layer height'
if $z[0] ne $config->get_value('first_layer_height') + $config->z_offset;
fail 'wrong second layer height'
if $z[1] ne $config->get_value('first_layer_height') + $config->get_value('layer_height') + $config->z_offset;
fail 'wrong layer height'
if first { !_eq($_, $config->layer_height) } @increments[1..$#increments];
1;
};
$config->set('layer_height', 0.3);
$config->set('first_layer_height', 0.2);
ok $test->(), "absolute first layer height";
$config->set('first_layer_height', '60%');
ok $test->(), "relative first layer height";
$config->set('z_offset', 0.9);
ok $test->(), "positive Z offset";
$config->set('z_offset', -0.8);
ok $test->(), "negative Z offset";
__END__

53
t/loops.t Normal file
View File

@ -0,0 +1,53 @@
use Test::More;
use strict;
use warnings;
plan tests => 4;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use Slic3r;
use Slic3r::Test;
{
# We only need to slice at one height, so we'll build a non-manifold mesh
# that still produces complete loops at that height. Triangular walls are
# enough for this purpose.
# Basically we want to check what happens when three concentric loops happen
# to be at the same height, the two external ones being ccw and the other being
# a hole, thus cw.
my (@vertices, @facets) = ();
Slic3r::Test::add_facet($_, \@vertices, \@facets) for
# external surface below the slicing Z
[ [0,0,0], [20,0,10], [0,0,10] ],
[ [20,0,0], [20,20,10], [20,0,10] ],
[ [20,20,0], [0,20,10], [20,20,10] ],
[ [0,20,0], [0,0,10], [0,20,10] ],
# external insetted surface above the slicing Z
[ [2,2,10], [18,2,10], [2,2,20] ],
[ [18,2,10], [18,18,10], [18,2,20] ],
[ [18,18,10], [2,18,10], [18,18,20] ],
[ [2,18,10], [2,2,10], [2,18,20] ],
# insetted hole below the slicing Z
[ [15,5,0], [5,5,10], [15,5,10] ],
[ [15,15,0], [15,5,10], [15,15,10] ],
[ [5,15,0], [15,15,10], [5,15,10] ],
[ [5,5,0], [5,15,10], [5,5,10] ];
my $mesh = Slic3r::TriangleMesh->new(vertices => \@vertices, facets => \@facets);
my @lines = map $mesh->intersect_facet($_, 10), 0..$#facets;
my $loops = Slic3r::TriangleMesh::make_loops(\@lines);
is scalar(@$loops), 3, 'correct number of loops detected';
is scalar(grep $_->is_counter_clockwise, @$loops), 2, 'correct number of ccw loops detected';
my @surfaces = Slic3r::Layer::Region::_merge_loops($loops, 0);
is scalar(@surfaces), 1, 'one surface detected';
is scalar(@{$surfaces[0]->expolygon})-1, 1, 'surface has one hole';
}
__END__

119
t/retraction.t Normal file
View File

@ -0,0 +1,119 @@
use Test::More tests => 16;
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use Slic3r;
use Slic3r::Test qw(_eq);
my $config = Slic3r::Config->new_from_defaults;
my $test = sub {
my ($conf) = @_;
$conf ||= $config;
my $print = Slic3r::Test::init_print('20mm_cube', config => $conf);
my $tool = 0;
my @toolchange_count = (); # track first usages so that we don't expect retract_length_toolchange when extruders are used for the first time
my @retracted = (1); # ignore the first travel move from home to first point
my $lifted = 0;
my $changed_tool = 0;
my $wait_for_toolchange = 0;
Slic3r::Test::GCodeReader->new(gcode => Slic3r::Test::gcode($print))->parse(sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
$changed_tool = 1;
$wait_for_toolchange = 0;
$toolchange_count[$tool] //= 0;
$toolchange_count[$tool]++;
} elsif ($cmd =~ /^G[01]$/ && !$args->{Z}) { # ignore lift taking place after retraction
fail 'toolchange happens right after retraction' if $wait_for_toolchange;
}
if ($info->{dist_Z}) {
# lift move or lift + change layer
if (_eq($info->{dist_Z}, $print->extruders->[$tool]->retract_lift)
|| (_eq($info->{dist_Z}, $conf->layer_height + $print->extruders->[$tool]->retract_lift) && $print->extruders->[$tool]->retract_lift > 0)) {
fail 'only lifting while retracted' if !$retracted[$tool] && !($conf->g0 && $info->{retracting});
$lifted = 1;
}
if ($info->{dist_Z} < 0) {
fail 'going down only after lifting' if !$lifted;
fail 'going down by the same amount of the lift'
if !_eq($info->{dist_Z}, -$print->extruders->[$tool]->retract_lift);
$lifted = 0;
}
}
if ($info->{retracting}) {
if (_eq(-$info->{dist_E}, $print->extruders->[$tool]->retract_length)) {
# okay
} elsif (_eq(-$info->{dist_E}, $print->extruders->[$tool]->retract_length_toolchange)) {
$wait_for_toolchange = 1;
} else {
fail 'retracted by the correct amount';
}
fail 'combining retraction and travel with G0'
if $cmd ne 'G0' && $conf->g0 && ($info->{dist_Z} || $info->{dist_XY});
$retracted[$tool] = 1;
}
if ($info->{extruding}) {
fail 'only extruding while not lifted' if $lifted;
if ($retracted[$tool]) {
my $expected_amount = $print->extruders->[$tool]->retract_length + $print->extruders->[$tool]->retract_restart_extra;
if ($changed_tool && $toolchange_count[$tool] > 1) {
$expected_amount = $print->extruders->[$tool]->retract_length_toolchange + $print->extruders->[$tool]->retract_restart_extra_toolchange;
$changed_tool = 0;
}
fail 'unretracted by the correct amount'
if !_eq($info->{dist_E}, $expected_amount);
$retracted[$tool] = 0;
}
}
if ($info->{travel} && $info->{dist_XY} >= $print->extruders->[$tool]->retract_before_travel) {
fail 'retracted before long travel move' if !$retracted[$tool];
}
});
1;
};
$config->set('retract_length', [1.5]);
$config->set('retract_before_travel', [3]);
my $retract_tests = sub {
my ($descr) = @_;
ok $test->(), "retraction$descr";
my $conf = $config->clone;
$conf->set('retract_restart_extra', [1]);
ok $test->($conf), "restart extra length$descr";
$conf->set('retract_restart_extra', [-1]);
ok $test->($conf), "negative restart extra length$descr";
$conf->set('retract_lift', [1]);
ok $test->($conf), "lift$descr";
};
$retract_tests->('');
$config->set('duplicate', 2);
$retract_tests->(' (duplicate)');
$config->set('g0', 1);
$retract_tests->(' (G0 and duplicate)');
$config->set('duplicate', 1);
$config->set('g0', 0);
$config->set('infill_extruder', 2);
$retract_tests->(' (dual extruder)');
__END__

48
t/shells.t Normal file
View File

@ -0,0 +1,48 @@
use Test::More tests => 2;
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use List::Util qw(first);
use Slic3r;
use Slic3r::Test;
{
my $config = Slic3r::Config->new_from_defaults;
$config->set('skirts', 0);
$config->set('perimeters', 0);
my $test = sub {
my ($conf) = @_;
$conf ||= $config;
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %layers_with_shells = (); # Z => $count
Slic3r::Test::GCodeReader->new(gcode => Slic3r::Test::gcode($print))->parse(sub {
my ($self, $cmd, $args, $info) = @_;
if ($self->Z > 0) {
$layers_with_shells{$self->Z} //= 0;
$layers_with_shells{$self->Z} = 1 if $info->{extruding} && $info->{dist_XY} > 0;
}
});
my @shells = @layers_with_shells{sort { $a <=> $b } keys %layers_with_shells};
fail "wrong number of bottom solid layers"
unless !defined(first { !$_ } @shells[0..$config->bottom_solid_layers-1]);
fail "wrong number of top solid layers"
unless !defined(first { !$_ } @shells[-$config->top_solid_layers..-1]);
1;
};
ok $test->(), "proper number of shells is applied";
$config->set('fill_density', 0);
ok $test->(), "proper number of shells is applied even when fill density is none";
}
__END__

View File

95
t/vibrationlimit.t Normal file
View File

@ -0,0 +1,95 @@
use Test::More tests => 9;
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use Slic3r;
use Slic3r::Geometry qw(epsilon);
use Slic3r::Test;
my $config = Slic3r::Config->new_from_defaults;
# tolerance, in minutes
# (our time estimation differs from the internal one because of decimals truncation)
my $epsilon = 0.002;
my $test = sub {
my ($conf) = @_;
$conf ||= $config;
my $print = Slic3r::Test::init_print('2x20x10', config => $conf);
my $min_time = 1 / ($conf->vibration_limit * 60); # minimum time between direction changes in minutes
my %dir = (X => 0, Y => 0);
my %dir_time = (X => 0, Y => 0);
my %dir_sleep_time = (X => 0, Y => 0);
my $last_cmd_pause = 0;
Slic3r::Test::GCodeReader->new(gcode => Slic3r::Test::gcode($print))->parse(sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd !~ /^G[01]$/) {
if ($cmd eq 'G4') {
$last_cmd_pause = (($args->{P} // 0) / 1000 + ($args->{S} // 0)) / 60; # in minutes
$dir_sleep_time{$_} += $last_cmd_pause for qw(X Y);
$last_cmd_pause -= $epsilon; # error builds up
}
return;
}
# Z moves are slow enough that we can consider any vibration interrupted
if ($info->{dist_Z}) {
$dir_time{$_} += 99999 for qw(X Y);
$last_cmd_pause = 0;
return;
} elsif ($info->{dist_E} != 0 && $info->{dist_XY} == 0) {
my $time = abs($info->{dist_E}) / ($args->{F} // $self->F); # in minutes
$dir_time{$_} += $time for qw(X Y);
$last_cmd_pause = 0;
return;
}
# compute move time (this assumes that the speed is XY-bound, which happens very likely)
my $time = abs($info->{dist_XY}) / ($args->{F} // $self->F); # in minutes
my $one_axis_would_trigger_limit_without_pause = 0;
foreach my $axis (qw(X Y)) {
# are we changing direction on this axis?
my $dir = $info->{"dist_$axis"} <=> 0;
if ($dir != 0 && $dir{$axis} != $dir) {
# this move changes direction on this axis
if ($dir{$axis} != 0) {
if (($dir_time{$axis} + $dir_sleep_time{$axis}) < ($min_time - $epsilon)) {
fail 'vibration limit exceeded';
}
$one_axis_would_trigger_limit_without_pause = 1
if ($dir_time{$axis} - $last_cmd_pause) < $min_time;
}
$dir{$axis} = $dir;
$dir_time{$axis} = 0;
$dir_sleep_time{$axis} = 0;
}
$dir_time{$axis} += $time;
}
fail 'no unnecessary pauses are added'
if $last_cmd_pause > $epsilon && !$one_axis_would_trigger_limit_without_pause;
$last_cmd_pause = 0;
});
1;
};
$config->set('gcode_comments', 1);
$config->set('perimeters', 1);
foreach my $frequency (5, 10, 15) {
foreach my $gapfillspeed (20, 50, 100) {
$config->set('vibration_limit', $frequency);
ok $test->(), "vibrations limited to ${frequency}Hz (gap fill speed = ${gapfillspeed} mm/s)";
}
}
__END__

View File

@ -0,0 +1,39 @@
#!/usr/bin/perl -i
#
# Post-processing script for calculating flow rate for each move
use strict;
use warnings;
my $E = 0;
my ($X, $Y);
while (<>) {
if (/^G1 X([0-9.]+) Y([0-9.]+).*? E([0-9.]+)/) {
my ($x, $y, $e) = ($1, $2, $3);
my $e_length = $e - $E;
if ($e_length > 0 && defined $X && defined $Y) {
my $dist = sqrt( (($x-$X)**2) + (($y-$Y)**2) );
if ($dist > 0) {
my $flowrate = sprintf '%.2f', $e_length / $dist;
s/(\R+)/ ; XY dist = $dist ; E dist = $e_length ; E\/XY = $flowrate mm\/mm$1/;
}
}
$E = $e;
$X = $x;
$Y = $y;
}
if (/^G1 X([0-9.]+) Y([0-9.]+)/) {
$X = $1;
$Y = $2;
}
if (/^G1.*? E([0-9.]+)/) {
$E = $1;
}
if (/^G92 E0/) {
$E = 0;
}
print;
}
__END__