diff --git a/Build.PL b/Build.PL index 41907905df..69f0302063 100644 --- a/Build.PL +++ b/Build.PL @@ -27,16 +27,28 @@ my %recommends = qw( Class::XSAccessor 0 Growl::GNTP 0.15 XML::SAX::ExpatXS 0 - Wx 0.9901 +); + +# removed: +# Wx 0.9901 + +my @try = ( + $ENV{CPANM} // (), + File::Spec->catfile($Config{sitebin}, 'cpanm'), + File::Spec->catfile($Config{installscript}, 'cpanm'), ); my $cpanm; -if (defined $ENV{CPANM} && -x $ENV{CPANM}) { - $cpanm = $ENV{CPANM}; -} elsif (-x (my $c = File::Spec->catfile($Config{installscript}, 'cpanm'))) { - $cpanm = $c; -} elsif ($^O = /^(?:darwin|linux)$/ && system(qw(which cpanm)) == 0) { - $cpanm = 'cpanm'; +foreach my $path (@try) { + if (-e $path) { # don't use -x because it fails on Windows + $cpanm = $path; + last; + } +} +if (!$cpanm) { + if ($^O =~ /^(?:darwin|linux)$/ && system(qw(which cpanm)) == 0) { + $cpanm = 'cpanm'; + } } die <<'EOF' cpanm was not found. Please install it before running this script. diff --git a/lib/Slic3r/Fill/Concentric.pm b/lib/Slic3r/Fill/Concentric.pm index 20ef31c2a2..b18352be91 100644 --- a/lib/Slic3r/Fill/Concentric.pm +++ b/lib/Slic3r/Fill/Concentric.pm @@ -3,8 +3,8 @@ use Moo; extends 'Slic3r::Fill::Base'; -use Slic3r::Geometry qw(scale unscale X); -use Slic3r::Geometry::Clipper qw(offset2 union_pt traverse_pt PFT_EVENODD); +use Slic3r::Geometry qw(scale unscale X nearest_point_index); +use Slic3r::Geometry::Clipper qw(offset offset2 union_pt traverse_pt PFT_EVENODD); sub fill_surface { my $self = shift; @@ -27,16 +27,26 @@ sub fill_surface { $flow_spacing = unscale $distance; } - my @loops = my @last = @$expolygon; + # compensate the overlap which is good for rectilinear but harmful for concentric + # where the perimeter/infill spacing should be equal to any other loop spacing + my @loops = my @last = offset($expolygon, -&Slic3r::INFILL_OVERLAP_OVER_SPACING * $min_spacing / 2); while (@last) { push @loops, @last = offset2(\@last, -1.5*$distance, +0.5*$distance); } # generate paths from the outermost to the innermost, to avoid # adhesion problems of the first central tiny loops - my @paths = map Slic3r::Polygon->new(@$_)->split_at_first_point, + @loops = map Slic3r::Polygon->new(@$_), reverse traverse_pt( union_pt(\@loops, PFT_EVENODD) ); + # order paths using a nearest neighbor search + my @paths = (); + my $last_pos = [0,0]; + foreach my $loop (@loops) { + push @paths, $loop->split_at_index(nearest_point_index($last_pos, $loop)); + $last_pos = $paths[-1][-1]; + } + # clip the paths to avoid the extruder to get exactly on the first point of the loop my $clip_length = scale $flow_spacing * &Slic3r::LOOP_CLIPPING_LENGTH_OVER_SPACING; $_->clip_end($clip_length) for @paths; diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index a8000cba41..a067c9fadd 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -190,8 +190,6 @@ sub extrude_loop { $extrusion_path->intersect_expolygons($self->_layer_overhangs); # reapply the nearest point search for starting point - # (TODO: choose the nearest point not on an overhang - make sure wipe and - # inwards move consider the new actual starting point) @paths = Slic3r::ExtrusionPath::Collection ->new(paths => [@paths]) ->chained_path($last_pos, 1); @@ -443,9 +441,16 @@ sub retract { $self->speed('travel'); # subdivide the retraction + my $retracted = 0; for (1 .. $#$wipe_path) { my $segment_length = $wipe_path->[$_-1]->distance_to($wipe_path->[$_]); - $gcode .= $self->G1($wipe_path->[$_], undef, $retract->[2] * ($segment_length / $total_wipe_length), $retract->[3] . ";_WIPE"); + $retracted += my $e = $retract->[2] * ($segment_length / $total_wipe_length); + $gcode .= $self->G1($wipe_path->[$_], undef, $e, $retract->[3] . ";_WIPE"); + } + if ($retracted > $retract->[2]) { + # if we retracted less than we had to, retract the remainder + # TODO: add regression test + $gcode .= $self->G1(undef, undef, $retract->[2] - $retracted, $comment); } } else { $self->speed('retract'); diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index e0848aa86b..3c1476ffd0 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -12,7 +12,7 @@ our @EXPORT_OK = qw( point_is_on_left_of_segment polyline_lines polygon_lines nearest_point point_along_segment polygon_segment_having_point polygon_has_subsegment polygon_has_vertex polyline_length can_connect_points deg2rad rad2deg - rotate_points move_points clip_segment_polygon + rotate_points move_points clip_segment_polygon nearest_point_index sum_vectors multiply_vector subtract_vectors dot perp polygon_points_visibility line_intersection bounding_box bounding_box_intersect same_point same_line longest_segment angle3points three_points_aligned line_direction diff --git a/lib/Slic3r/Geometry/BoundingBox.pm b/lib/Slic3r/Geometry/BoundingBox.pm index 7b720e5319..1e0ed33c4c 100644 --- a/lib/Slic3r/Geometry/BoundingBox.pm +++ b/lib/Slic3r/Geometry/BoundingBox.pm @@ -62,6 +62,18 @@ sub scale { $self; } +sub translate { + my $self = shift; + my @shift = @_; + + for my $axis (X .. $#{$self->extents}) { + $self->extents->[$axis][MIN] += $shift[$axis]; + $self->extents->[$axis][MAX] += $shift[$axis]; + } + + $self; +} + sub size { my $self = shift; diff --git a/lib/Slic3r/Layer/Region.pm b/lib/Slic3r/Layer/Region.pm index 7a838d65ac..3f71bfba94 100644 --- a/lib/Slic3r/Layer/Region.pm +++ b/lib/Slic3r/Layer/Region.pm @@ -256,7 +256,11 @@ sub make_perimeters { foreach my $polynode (@nodes) { push @loops, $traverse->($polynode->{children}, $depth+1, $is_contour); + # return ccw contours and cw holes + # GCode.pm will convert all of them to ccw, but it needs to know + # what the holes are in order to compute the correct inwards move my $polygon = Slic3r::Polygon->new($polynode->{outer} // [ reverse @{$polynode->{hole}} ]); + $polygon->reverse if !$is_contour; my $role = EXTR_ROLE_PERIMETER; if ($is_contour ? $depth == 0 : !@{ $polynode->{children} }) { diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm index f0fe69a287..71937ef07a 100644 --- a/lib/Slic3r/Model.pm +++ b/lib/Slic3r/Model.pm @@ -6,6 +6,7 @@ use Slic3r::Geometry qw(X Y Z MIN move_points); has 'materials' => (is => 'ro', default => sub { {} }); has 'objects' => (is => 'ro', default => sub { [] }); +has '_bounding_box' => (is => 'rw'); sub read_from_file { my $class = shift; @@ -57,6 +58,7 @@ sub add_object { my $object = Slic3r::Model::Object->new(model => $self, @_); push @{$self->objects}, $object; + $self->_bounding_box(undef); return $object; } @@ -70,11 +72,6 @@ sub set_material { ); } -sub scale { - my $self = shift; - $_->scale(@_) for @{$self->objects}; -} - sub arrange_objects { my $self = shift; my ($config) = @_; @@ -151,8 +148,9 @@ sub _arrange { return ($config->duplicate_grid->[X] * $config->duplicate_grid->[Y]), @positions; } else { my $total_parts = $config->duplicate * @items; - my $partx = max(map $_->size->[X], @items); - my $party = max(map $_->size->[Y], @items); + my @sizes = map $_->size, @items; + my $partx = max(map $_->[X], @sizes); + my $party = max(map $_->[Y], @sizes); return $config->duplicate, Slic3r::Geometry::arrange ($total_parts, $partx, $party, (map $_, @{$config->bed_size}), @@ -172,12 +170,16 @@ sub used_vertices { sub size { my $self = shift; - return [ Slic3r::Geometry::size_3D($self->used_vertices) ]; + return $self->bounding_box->size; } sub bounding_box { my $self = shift; - return Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices); + + if (!defined $self->_bounding_box) { + $self->_bounding_box(Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices)); + } + return $self->_bounding_box; } sub align_to_origin { @@ -198,9 +200,18 @@ sub align_to_origin { } } +sub scale { + my $self = shift; + $_->scale(@_) for @{$self->objects}; + $self->_bounding_box->scale(@_) if defined $self->_bounding_box; +} + sub move { my $self = shift; - $_->move(@_) for @{$self->objects}; + my @shift = @_; + + $_->move(@shift) for @{$self->objects}; + $self->_bounding_box->translate(@shift) if defined $self->_bounding_box; } # flattens everything to a single mesh @@ -286,6 +297,7 @@ has 'vertices' => (is => 'ro', default => sub { [] }); has 'volumes' => (is => 'ro', default => sub { [] }); has 'instances' => (is => 'rw'); has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] +has '_bounding_box' => (is => 'rw'); sub add_volume { my $self = shift; @@ -304,6 +316,8 @@ sub add_volume { my $volume = Slic3r::Model::Volume->new(object => $self, %args); push @{$self->volumes}, $volume; + $self->_bounding_box(undef); + $self->model->_bounding_box(undef); return $volume; } @@ -312,6 +326,7 @@ sub add_instance { $self->instances([]) if !defined $self->instances; push @{$self->instances}, Slic3r::Model::Instance->new(object => $self, @_); + $self->model->_bounding_box(undef); return $self->instances->[-1]; } @@ -333,7 +348,7 @@ sub used_vertices { sub size { my $self = shift; - return [ Slic3r::Geometry::size_3D($self->used_vertices) ]; + return $self->bounding_box->size; } sub center { @@ -343,7 +358,11 @@ sub center { sub bounding_box { my $self = shift; - return Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices); + + if (!defined $self->_bounding_box) { + $self->_bounding_box(Slic3r::Geometry::BoundingBox->new_from_points_3D($self->used_vertices)); + } + return $self->_bounding_box; } sub align_to_origin { @@ -359,7 +378,10 @@ sub align_to_origin { sub move { my $self = shift; - @{$self->vertices} = move_points_3D([ @_ ], @{$self->vertices}); + my @shift = @_; + + @{$self->vertices} = move_points_3D([ @shift ], @{$self->vertices}); + $self->_bounding_box->translate(@shift) if defined $self->_bounding_box; } sub scale { @@ -371,6 +393,8 @@ sub scale { foreach my $vertex (@{$self->vertices}) { $vertex->[$_] *= $factor for X,Y,Z; } + + $self->_bounding_box->scale($factor) if defined $self->_bounding_box; } sub rotate { @@ -384,6 +408,8 @@ sub rotate { foreach my $vertex (@{$self->vertices}) { @$vertex = (@{ +(Slic3r::Geometry::rotate_points($rad, undef, [ $vertex->[X], $vertex->[Y] ]))[0] }, $vertex->[Z]); } + + $self->_bounding_box(undef); } sub materials_count { diff --git a/lib/Slic3r/Polygon.pm b/lib/Slic3r/Polygon.pm index aa57247bef..f743ac5435 100644 --- a/lib/Slic3r/Polygon.pm +++ b/lib/Slic3r/Polygon.pm @@ -7,7 +7,7 @@ use parent 'Slic3r::Polyline'; use Slic3r::Geometry qw(polygon_lines polygon_remove_parallel_continuous_edges polygon_remove_acute_vertices polygon_segment_having_point point_in_polygon - PI X1 X2 Y1 Y2); + PI X1 X2 Y1 Y2 epsilon); use Slic3r::Geometry::Clipper qw(JT_MITER); sub lines { @@ -159,7 +159,7 @@ sub concave_points { my $self = shift; return map $self->[$_], - grep Slic3r::Geometry::angle3points(@$self[$_, $_-1, $_+1]) < PI, + grep Slic3r::Geometry::angle3points(@$self[$_, $_-1, $_+1]) < PI - epsilon, -1 .. ($#$self-1); } diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm index 13a80b133c..0df58e872e 100644 --- a/lib/Slic3r/Print/Object.pm +++ b/lib/Slic3r/Print/Object.pm @@ -145,6 +145,12 @@ sub slice { my $self = shift; my %params = @_; + # make sure all layers contain layer region objects for all regions + my $regions_count = $self->print->regions_count; + foreach my $layer (@{ $self->layers }) { + $layer->region($_) for 0 .. ($regions_count-1); + } + # process facets for my $region_id (0 .. $#{$self->meshes}) { my $mesh = $self->meshes->[$region_id]; # ignore undef meshes @@ -152,8 +158,7 @@ sub slice { my $apply_lines = sub { my $lines = shift; foreach my $layer_id (keys %$lines) { - my $layerm = $self->layers->[$layer_id]->region($region_id); - push @{$layerm->lines}, @{$lines->{$layer_id}}; + push @{$self->layers->[$layer_id]->regions->[$region_id]->lines}, @{$lines->{$layer_id}}; } }; Slic3r::parallelize( @@ -192,9 +197,6 @@ sub slice { pop @{$self->layers} while @{$self->layers} && (!map @{$_->lines}, @{$self->layers->[-1]->regions}); foreach my $layer (@{ $self->layers }) { - # make sure all layers contain layer region objects for all regions - $layer->region($_) for 0 .. ($self->print->regions_count-1); - Slic3r::debugf "Making surfaces for layer %d (slice z = %f):\n", $layer->id, unscale $layer->slice_z if $Slic3r::debug; diff --git a/lib/Slic3r/Test.pm b/lib/Slic3r/Test.pm index fee73b0dc7..a8bcd2426a 100644 --- a/lib/Slic3r/Test.pm +++ b/lib/Slic3r/Test.pm @@ -27,6 +27,13 @@ sub model { $facets = [ [0,1,2], [0,2,3], [4,5,6], [4,6,7], [0,4,7], [0,7,1], [1,7,6], [1,6,2], [2,6,5], [2,5,3], [4,0,3], [4,3,5], ], + } elsif ($model_name eq 'cube_with_hole') { + $vertices = [ + [0,0,0],[0,0,10],[0,20,0],[0,20,10],[20,0,0],[20,0,10],[5,5,0],[15,5,0],[5,15,0],[20,20,0],[15,15,0],[20,20,10],[5,5,10],[5,15,10],[15,5,10],[15,15,10] + ]; + $facets = [ + [0,1,2],[2,1,3],[1,0,4],[5,1,4],[6,7,4],[8,2,9],[0,2,8],[10,8,9],[0,8,6],[0,6,4],[4,7,9],[7,10,9],[2,3,9],[9,3,11],[12,1,5],[13,3,12],[14,12,5],[3,1,12],[11,3,13],[11,15,5],[11,13,15],[15,14,5],[5,4,9],[11,5,9],[8,13,12],[6,8,12],[10,15,13],[8,10,13],[15,10,14],[14,10,7],[14,7,12],[12,7,6] + ], } elsif ($model_name eq 'V') { $vertices = [ [-14,0,20],[-14,15,20],[0,0,0],[0,15,0],[-4,0,20],[-4,15,20],[5,0,7.14286],[10,0,0],[24,0,20],[14,0,20],[10,15,0],[5,15,7.14286],[14,15,20],[24,15,20] diff --git a/lib/Slic3r/TriangleMesh.pm b/lib/Slic3r/TriangleMesh.pm index 7d06ef86c5..b4624250df 100644 --- a/lib/Slic3r/TriangleMesh.pm +++ b/lib/Slic3r/TriangleMesh.pm @@ -1,6 +1,7 @@ package Slic3r::TriangleMesh; use Moo; +use List::Util qw(reduce min max); use Slic3r::Geometry qw(X Y Z A B unscale same_point); use Slic3r::Geometry::Clipper qw(union_ex); use Storable; @@ -37,6 +38,7 @@ sub analyze { $self->facets_edges([]); $self->edges_facets([]); my %table = (); # edge_coordinates => edge_id + my $vertices = $self->vertices; # save method calls for (my $facet_id = 0; $facet_id <= $#{$self->facets}; $facet_id++) { my $facet = $self->facets->[$facet_id]; @@ -46,8 +48,10 @@ sub analyze { # this is needed to get all intersection lines in a consistent order # (external on the right of the line) { - my @z_order = sort { $self->vertices->[$facet->[$a]][Z] <=> $self->vertices->[$facet->[$b]][Z] } -3..-1; - @$facet[-3..-1] = (@$facet[$z_order[0]..-1], @$facet[-3..($z_order[0]-1)]); + my $lowest_vertex_idx = reduce { + $vertices->[ $facet->[$a] ][Z] < $vertices->[ $facet->[$b] ][Z] ? $a : $b + } -3 .. -1; + @$facet[-3..-1] = (@$facet[$lowest_vertex_idx..-1], @$facet[-3..($lowest_vertex_idx-1)]); } # ignore the normal if provided @@ -420,13 +424,11 @@ sub slice_facet { if $Slic3r::debug; # find the vertical extents of the facet - my ($min_z, $max_z) = (99999999999, -99999999999); - foreach my $vertex (@vertices) { - my $vertex_z = $self->vertices->[$vertex][Z]; - $min_z = $vertex_z if $vertex_z < $min_z; - $max_z = $vertex_z if $vertex_z > $max_z; - } - Slic3r::debugf "z: min = %.0f, max = %.0f\n", $min_z, $max_z; + my @z = map $_->[Z], @{$self->vertices}[@vertices]; + my $min_z = min(@z); + my $max_z = max(@z); + Slic3r::debugf "z: min = %.0f, max = %.0f\n", $min_z, $max_z + if $Slic3r::debug; if ($max_z == $min_z) { Slic3r::debugf "Facet is horizontal; ignoring\n"; @@ -435,10 +437,11 @@ sub slice_facet { # calculate the layer extents 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; + Slic3r::debugf "layers: min = %s, max = %s\n", $min_layer, $max_layer + if $Slic3r::debug; my $lines = {}; # layer_id => [ lines ] - for (my $layer_id = $min_layer; $layer_id <= $max_layer; $layer_id++) { + for my $layer_id ($min_layer .. $max_layer) { my $layer = $print_object->layers->[$layer_id]; $lines->{$layer_id} ||= []; push @{ $lines->{$layer_id} }, $self->intersect_facet($facet_id, $layer->slice_z); @@ -451,25 +454,27 @@ sub intersect_facet { my ($facet_id, $z) = @_; my @vertices_ids = @{$self->facets->[$facet_id]}[-3..-1]; + my %vertices = map { $_ => $self->vertices->[$_] } @vertices_ids; # cache vertices my @edge_ids = @{$self->facets_edges->[$facet_id]}; my @edge_vertices_ids = $self->_facet_edges($facet_id); - my (@lines, @points, @intersection_points, @points_on_layer) = (); + my (@points, @intersection_points, @points_on_layer) = (); for my $e (0..2) { - my $edge_id = $edge_ids[$e]; my ($a_id, $b_id) = @{$edge_vertices_ids[$e]}; - my ($a, $b) = map $self->vertices->[$_], ($a_id, $b_id); + my ($a, $b) = @vertices{$a_id, $b_id}; #printf "Az = %f, Bz = %f, z = %f\n", $a->[Z], $b->[Z], $z; if ($a->[Z] == $b->[Z] && $a->[Z] == $z) { # edge is horizontal and belongs to the current layer - my $edge_type = (grep $self->vertices->[$_][Z] < $z, @vertices_ids) ? FE_TOP : FE_BOTTOM; + my $edge_type = (grep $vertices{$_}[Z] < $z, @vertices_ids) ? FE_TOP : FE_BOTTOM; if ($edge_type == FE_TOP) { ($a, $b) = ($b, $a); ($a_id, $b_id) = ($b_id, $a_id); } - push @lines, pack I_FMT, ( + # We assume that this method is never being called for horizontal + # facets, so no other edge is going to be on this layer. + return pack I_FMT, ( $b->[X], $b->[Y], # I_B $a_id, # I_A_ID $b_id, # I_B_ID @@ -499,14 +504,13 @@ sub intersect_facet { $b->[X] + ($a->[X] - $b->[X]) * ($z - $b->[Z]) / ($a->[Z] - $b->[Z]), $b->[Y] + ($a->[Y] - $b->[Y]) * ($z - $b->[Z]) / ($a->[Z] - $b->[Z]), undef, - $edge_id, + $edge_ids[$e], ]; push @intersection_points, $#points; #print "Intersects at $z!\n"; } } - return @lines if @lines; if (@points_on_layer == 2 && @intersection_points == 1) { $points[ $points_on_layer[1] ] = undef; @points = grep $_, @points; diff --git a/t/perimeters.t b/t/perimeters.t index de53b6a14e..1ce91bd32e 100644 --- a/t/perimeters.t +++ b/t/perimeters.t @@ -1,4 +1,4 @@ -use Test::More tests => 3; +use Test::More tests => 5; use strict; use warnings; @@ -41,6 +41,37 @@ use Slic3r::Test; ok !$has_cw_loops, 'all perimeters extruded ccw'; } + { + $config->set('external_perimeter_speed', 68); + my $print = Slic3r::Test::init_print('cube_with_hole', config => $config); + my $has_cw_loops = my $has_outwards_move = 0; + my $cur_loop; + my %external_loops = (); # print_z => count of external loops + Slic3r::GCode::Reader->new(gcode => Slic3r::Test::gcode($print))->parse(sub { + my ($self, $cmd, $args, $info) = @_; + + if ($info->{extruding} && $info->{dist_XY} > 0) { + $cur_loop ||= [ [$self->X, $self->Y] ]; + push @$cur_loop, [ @$info{qw(new_X new_Y)} ]; + } else { + if ($cur_loop) { + $has_cw_loops = 1 if !Slic3r::Geometry::Clipper::is_counter_clockwise($cur_loop); + if ($self->F == $config->external_perimeter_speed*60) { + my $move_dest = [ @$info{qw(new_X new_Y)} ]; + $external_loops{$self->Z}++; + $has_outwards_move = 1 + if !Slic3r::Polygon->new(@$cur_loop)->encloses_point($move_dest) + ? ($external_loops{$self->Z} == 2) # contour should include destination + : ($external_loops{$self->Z} == 1); # hole should not + } + $cur_loop = undef; + } + } + }); + ok !$has_cw_loops, 'all perimeters extruded ccw'; + ok !$has_outwards_move, 'move inwards after completing external loop'; + } + { my $print = Slic3r::Test::init_print('L', config => $config); my $loop_starts_from_convex_point = 0; diff --git a/t/slice.t b/t/slice.t index e47929f0fc..8032924fb5 100644 --- a/t/slice.t +++ b/t/slice.t @@ -2,7 +2,7 @@ use Test::More; use strict; use warnings; -plan tests => 17; +plan tests => 16; BEGIN { use FindBin; @@ -20,11 +20,13 @@ my @points = ([3, 4], [8, 5], [1, 9]); # XY coordinates of the facet vertices # the first point of the intersection lines is replaced by -1 because TriangleMesh.pm # is saving memory and doesn't store point A anymore since it's not actually needed. -is_deeply lines(20, 20, 20), [ - [ -1, $points[1] ], # $points[0] - [ -1, $points[2] ], # $points[1] - [ -1, $points[0] ], # $points[2] -], 'horizontal'; +# We disable this test because intersect_facet() now assumes we never feed a horizontal +# facet to it. +# is_deeply lines(20, 20, 20), [ +# [ -1, $points[1] ], # $points[0] +# [ -1, $points[2] ], # $points[1] +# [ -1, $points[0] ], # $points[2] +# ], 'horizontal'; is_deeply lines(22, 20, 20), [ [ -1, $points[2] ] ], 'lower edge on layer'; # $points[1] is_deeply lines(20, 20, 22), [ [ -1, $points[1] ] ], 'lower edge on layer'; # $points[0]