Bugfixes and improvements in surface detection
This commit is contained in:
parent
f1a36502e1
commit
2da5ee7448
@ -17,6 +17,7 @@ use Slic3r::Fill;
|
|||||||
use Slic3r::Geometry;
|
use Slic3r::Geometry;
|
||||||
use Slic3r::Layer;
|
use Slic3r::Layer;
|
||||||
use Slic3r::Line;
|
use Slic3r::Line;
|
||||||
|
use Slic3r::Line::FacetEdge;
|
||||||
use Slic3r::Perimeter;
|
use Slic3r::Perimeter;
|
||||||
use Slic3r::Point;
|
use Slic3r::Point;
|
||||||
use Slic3r::Polyline;
|
use Slic3r::Polyline;
|
||||||
|
@ -11,7 +11,6 @@ use constant B => 1;
|
|||||||
use constant X => 0;
|
use constant X => 0;
|
||||||
use constant Y => 1;
|
use constant Y => 1;
|
||||||
use constant epsilon => 1E-6;
|
use constant epsilon => 1E-6;
|
||||||
use constant epsilon2 => epsilon**2;
|
|
||||||
our $parallel_degrees_limit = abs(deg2rad(3));
|
our $parallel_degrees_limit = abs(deg2rad(3));
|
||||||
|
|
||||||
sub slope {
|
sub slope {
|
||||||
@ -92,20 +91,35 @@ sub point_in_polygon {
|
|||||||
# if point is not in polygon, let's check whether it belongs to the contour
|
# if point is not in polygon, let's check whether it belongs to the contour
|
||||||
if (!$side && 0) {
|
if (!$side && 0) {
|
||||||
foreach my $line (polygon_lines($polygon)) {
|
foreach my $line (polygon_lines($polygon)) {
|
||||||
# calculate the Y in line at X of the point
|
return 1 if point_in_segment($point, $line);
|
||||||
if ($line->[A][X] == $line->[B][X]) {
|
|
||||||
return 1 if abs($x - $line->[A][X]) < epsilon;
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
my $y3 = $line->[A][Y] + ($line->[B][Y] - $line->[A][Y])
|
|
||||||
* ($x - $line->[A][X]) / ($line->[B][X] - $line->[A][X]);
|
|
||||||
return 1 if abs($y3 - $y) < epsilon2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $side;
|
return $side;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub point_in_segment {
|
||||||
|
my ($point, $line) = @_;
|
||||||
|
|
||||||
|
my ($x, $y) = @$point;
|
||||||
|
my @line_x = sort { $a <=> $b } $line->[A][X], $line->[B][X];
|
||||||
|
my @line_y = sort { $a <=> $b } $line->[A][Y], $line->[B][Y];
|
||||||
|
|
||||||
|
# check whether the point is in the segment bounding box
|
||||||
|
return 0 unless $x >= ($line_x[0] - epsilon) && $x <= ($line_x[1] + epsilon)
|
||||||
|
&& $y >= ($line_y[0] - epsilon) && $y <= ($line_y[1] + epsilon);
|
||||||
|
|
||||||
|
# if line is vertical, check whether point's X is the same as the line
|
||||||
|
if ($line->[A][X] == $line->[B][X]) {
|
||||||
|
return 1 if abs($x - $line->[A][X]) < epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate the Y in line at X of the point
|
||||||
|
my $y3 = $line->[A][Y] + ($line->[B][Y] - $line->[A][Y])
|
||||||
|
* ($x - $line->[A][X]) / ($line->[B][X] - $line->[A][X]);
|
||||||
|
return abs($y3 - $y) < epsilon ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
sub polygon_lines {
|
sub polygon_lines {
|
||||||
my ($polygon) = @_;
|
my ($polygon) = @_;
|
||||||
|
|
||||||
|
@ -112,6 +112,24 @@ sub make_polylines {
|
|||||||
@{ $self->lines } = grep $lines_map{"$_"}, @{ $self->lines };
|
@{ $self->lines } = grep $lines_map{"$_"}, @{ $self->lines };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# now remove lines that are already part of a surface
|
||||||
|
{
|
||||||
|
my @lines = @{ $self->lines };
|
||||||
|
@{ $self->lines } = ();
|
||||||
|
LINE: foreach my $line (@lines) {
|
||||||
|
if (!$line->isa('Slic3r::Line::FacetEdge')) {
|
||||||
|
push @{ $self->lines }, $line;
|
||||||
|
next LINE;
|
||||||
|
}
|
||||||
|
foreach my $surface (@{$self->surfaces}) {
|
||||||
|
if ($surface->surface_type eq $line->edge_type && $surface->contour->has_segment($line)) {
|
||||||
|
next LINE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push @{ $self->lines }, $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# make a cache of line endpoints
|
# make a cache of line endpoints
|
||||||
my %pointmap = ();
|
my %pointmap = ();
|
||||||
foreach my $line (@{ $self->lines }) {
|
foreach my $line (@{ $self->lines }) {
|
||||||
|
@ -10,10 +10,10 @@ has 'points' => (
|
|||||||
|
|
||||||
sub cast {
|
sub cast {
|
||||||
my $class = shift;
|
my $class = shift;
|
||||||
my ($line) = @_;
|
my ($line, %args) = @_;
|
||||||
if (ref $line eq 'ARRAY') {
|
if (ref $line eq 'ARRAY') {
|
||||||
@$line == 2 or die "Line needs two points!";
|
@$line == 2 or die "Line needs two points!";
|
||||||
return Slic3r::Line->new(points => [ map Slic3r::Point->cast($_), @$line ]);
|
return $class->new(points => [ map Slic3r::Point->cast($_), @$line ], %args);
|
||||||
} else {
|
} else {
|
||||||
return $line;
|
return $line;
|
||||||
}
|
}
|
||||||
@ -51,6 +51,17 @@ sub has_endpoint {
|
|||||||
return $point->coincides_with($self->a) || $point->coincides_with($self->b);
|
return $point->coincides_with($self->a) || $point->coincides_with($self->b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub has_segment {
|
||||||
|
my $self = shift;
|
||||||
|
my ($line) = @_;
|
||||||
|
|
||||||
|
$line = $line->p if $line->isa('Slic3r::Line');
|
||||||
|
|
||||||
|
# a segment belongs to another segment if its points belong to it
|
||||||
|
return Slic3r::Geometry::point_in_segment($line->[0], $self->p)
|
||||||
|
&& Slic3r::Geometry::point_in_segment($line->[1], $self->p);
|
||||||
|
}
|
||||||
|
|
||||||
sub parallel_to {
|
sub parallel_to {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my ($line) = @_;
|
my ($line) = @_;
|
||||||
|
8
lib/Slic3r/Line/FacetEdge.pm
Normal file
8
lib/Slic3r/Line/FacetEdge.pm
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package Slic3r::Line::FacetEdge;
|
||||||
|
use Moo;
|
||||||
|
|
||||||
|
extends 'Slic3r::Line';
|
||||||
|
|
||||||
|
has 'edge_type' => (is => 'ro'); # top/bottom
|
||||||
|
|
||||||
|
1;
|
@ -113,12 +113,11 @@ sub offset_polygon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# apply all holes to all contours;
|
# apply holes to the right contours
|
||||||
# this is improper, but Math::Clipper handles it
|
my $clipper = Math::Clipper->new;
|
||||||
return map {{
|
$clipper->add_subject_polygons($offsets);
|
||||||
outer => $_,
|
my $results = $clipper->ex_execute(CT_UNION, PFT_NONZERO, PFT_NONZERO);
|
||||||
holes => [ @hole_offsets ],
|
return @$results;
|
||||||
}} @contour_offsets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub _mgp_from_points_ref {
|
sub _mgp_from_points_ref {
|
||||||
|
@ -97,4 +97,14 @@ sub nearest_point_to {
|
|||||||
return Slic3r::Point->cast($point);
|
return Slic3r::Point->cast($point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub has_segment {
|
||||||
|
my $self = shift;
|
||||||
|
my ($line) = @_;
|
||||||
|
|
||||||
|
for ($self->lines) {
|
||||||
|
return 1 if $_->has_segment($line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -46,9 +46,6 @@ sub new_from_stl {
|
|||||||
$layer->merge_contiguous_surfaces;
|
$layer->merge_contiguous_surfaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
# detect which surfaces are near external layers
|
|
||||||
$print->discover_horizontal_shells;
|
|
||||||
|
|
||||||
return $print;
|
return $print;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +85,7 @@ sub discover_horizontal_shells {
|
|||||||
my $layer = $self->layers->[$i];
|
my $layer = $self->layers->[$i];
|
||||||
foreach my $type (qw(top bottom)) {
|
foreach my $type (qw(top bottom)) {
|
||||||
# find surfaces of current type for current layer
|
# find surfaces of current type for current layer
|
||||||
my @surfaces = grep $_->surface_type eq $type, @{$layer->surfaces} or next;
|
my @surfaces = grep $_->surface_type eq $type, map @{$_->surfaces}, @{$layer->fill_surfaces} or next;
|
||||||
Slic3r::debugf "Layer %d has %d surfaces of type '%s'\n",
|
Slic3r::debugf "Layer %d has %d surfaces of type '%s'\n",
|
||||||
$i, scalar(@surfaces), $type;
|
$i, scalar(@surfaces), $type;
|
||||||
|
|
||||||
@ -99,47 +96,51 @@ sub discover_horizontal_shells {
|
|||||||
next if $n < 0 || $n >= $self->layer_count;
|
next if $n < 0 || $n >= $self->layer_count;
|
||||||
Slic3r::debugf " looking for neighbors on layer %d...\n", $n;
|
Slic3r::debugf " looking for neighbors on layer %d...\n", $n;
|
||||||
|
|
||||||
my $neighbor_polygons = [ map $_->p, grep $_->surface_type eq 'internal', @{$self->layers->[$n]->surfaces} ];
|
foreach my $surf_coll (@{$self->layers->[$n]->fill_surfaces}) {
|
||||||
# find intersection between @surfaces and current layer's surfaces
|
my $neighbor_polygons = [ map $_->p, grep $_->surface_type eq 'internal', @{$surf_coll->surfaces} ];
|
||||||
$clipper->add_subject_polygons([ map $_->p, @surfaces ]);
|
|
||||||
$clipper->add_clip_polygons($neighbor_polygons);
|
# find intersection between @surfaces and current layer's surfaces
|
||||||
|
$clipper->add_subject_polygons([ map $_->p, @surfaces ]);
|
||||||
# intersections have contours and holes
|
$clipper->add_clip_polygons($neighbor_polygons);
|
||||||
my $intersections = $clipper->ex_execute(CT_INTERSECTION, PFT_NONZERO, PFT_NONZERO);
|
|
||||||
$clipper->clear;
|
# intersections have contours and holes
|
||||||
next if @$intersections == 0;
|
my $intersections = $clipper->ex_execute(CT_INTERSECTION, PFT_NONZERO, PFT_NONZERO);
|
||||||
Slic3r::debugf " %d intersections found\n", scalar @$intersections;
|
$clipper->clear;
|
||||||
|
|
||||||
# subtract intersections from layer surfaces to get resulting inner surfaces
|
next if @$intersections == 0;
|
||||||
$clipper->add_subject_polygons($neighbor_polygons);
|
Slic3r::debugf " %d intersections found\n", scalar @$intersections;
|
||||||
$clipper->add_clip_polygons([ map { $_->{outer}, @{$_->{holes}} } @$intersections ]);
|
|
||||||
my $internal_polygons = $clipper->ex_execute(CT_DIFFERENCE, PFT_NONZERO, PFT_NONZERO);
|
# subtract intersections from layer surfaces to get resulting inner surfaces
|
||||||
$clipper->clear;
|
$clipper->add_subject_polygons($neighbor_polygons);
|
||||||
|
$clipper->add_clip_polygons([ map { $_->{outer}, @{$_->{holes}} } @$intersections ]);
|
||||||
# Note: due to floating point math we're going to get some very small
|
my $internal_polygons = $clipper->ex_execute(CT_DIFFERENCE, PFT_NONZERO, PFT_NONZERO);
|
||||||
# polygons as $internal_polygons; they will be removed by removed_small_features()
|
$clipper->clear;
|
||||||
|
|
||||||
# assign resulting inner surfaces to layer
|
# Note: due to floating point math we're going to get some very small
|
||||||
$self->layers->[$n]->surfaces([]);
|
# polygons as $internal_polygons; they will be removed by removed_small_features()
|
||||||
foreach my $p (@$internal_polygons) {
|
|
||||||
push @{$self->layers->[$n]->surfaces}, Slic3r::Surface->new(
|
# assign resulting inner surfaces to layer
|
||||||
surface_type => 'internal',
|
$surf_coll->surfaces([]);
|
||||||
contour => Slic3r::Polyline::Closed->cast($p->{outer}),
|
foreach my $p (@$internal_polygons) {
|
||||||
holes => [
|
push @{$surf_coll->surfaces}, Slic3r::Surface->new(
|
||||||
map Slic3r::Polyline::Closed->cast($_), @{$p->{holes}}
|
surface_type => 'internal',
|
||||||
],
|
contour => Slic3r::Polyline::Closed->cast($p->{outer}),
|
||||||
);
|
holes => [
|
||||||
}
|
map Slic3r::Polyline::Closed->cast($_), @{$p->{holes}}
|
||||||
|
],
|
||||||
# assign new internal-solid surfaces to layer
|
);
|
||||||
foreach my $p (@$intersections) {
|
}
|
||||||
push @{$self->layers->[$n]->surfaces}, Slic3r::Surface->new(
|
|
||||||
surface_type => 'internal-solid',
|
# assign new internal-solid surfaces to layer
|
||||||
contour => Slic3r::Polyline::Closed->cast($p->{outer}),
|
foreach my $p (@$intersections) {
|
||||||
holes => [
|
push @{$surf_coll->surfaces}, Slic3r::Surface->new(
|
||||||
map Slic3r::Polyline::Closed->cast($_), @{$p->{holes}}
|
surface_type => 'internal-solid',
|
||||||
],
|
contour => Slic3r::Polyline::Closed->cast($p->{outer}),
|
||||||
);
|
holes => [
|
||||||
|
map Slic3r::Polyline::Closed->cast($_), @{$p->{holes}}
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,10 @@ sub intersect_facet {
|
|||||||
|
|
||||||
if ($a->[Z] == $b->[Z] && $a->[Z] == $z) {
|
if ($a->[Z] == $b->[Z] && $a->[Z] == $z) {
|
||||||
# edge is horizontal and belongs to the current layer
|
# edge is horizontal and belongs to the current layer
|
||||||
push @lines, [ [$a->[X], $a->[Y]], [$b->[X], $b->[Y]] ];
|
push @lines, Slic3r::Line::FacetEdge->cast(
|
||||||
|
[ [$a->[X], $a->[Y]], [$b->[X], $b->[Y]] ],
|
||||||
|
edge_type => (grep $_->[Z] > $z, @$vertices) ? 'bottom' : 'top',
|
||||||
|
);
|
||||||
#print "Horizontal!\n";
|
#print "Horizontal!\n";
|
||||||
|
|
||||||
} elsif (($a->[Z] < $z && $b->[Z] > $z) || ($b->[Z] < $z && $a->[Z] > $z)) {
|
} elsif (($a->[Z] < $z && $b->[Z] > $z) || ($b->[Z] < $z && $a->[Z] > $z)) {
|
||||||
@ -213,7 +216,7 @@ sub intersect_facet {
|
|||||||
#}
|
#}
|
||||||
|
|
||||||
# connect points:
|
# connect points:
|
||||||
push @lines, [ @intersection_points ];
|
push @lines, Slic3r::Line->cast([ @intersection_points ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return @lines;
|
return @lines;
|
||||||
|
@ -2,6 +2,7 @@ package Slic3r::Skein;
|
|||||||
use Moo;
|
use Moo;
|
||||||
|
|
||||||
use Time::HiRes qw(gettimeofday tv_interval);
|
use Time::HiRes qw(gettimeofday tv_interval);
|
||||||
|
use XXX;
|
||||||
|
|
||||||
has 'input_file' => (is => 'ro', required => 1);
|
has 'input_file' => (is => 'ro', required => 1);
|
||||||
has 'output_file' => (is => 'rw', required => 0);
|
has 'output_file' => (is => 'rw', required => 0);
|
||||||
@ -16,6 +17,10 @@ sub go {
|
|||||||
my $print = Slic3r::Print->new_from_stl($self->input_file);
|
my $print = Slic3r::Print->new_from_stl($self->input_file);
|
||||||
$print->extrude_perimeters;
|
$print->extrude_perimeters;
|
||||||
$print->remove_small_features;
|
$print->remove_small_features;
|
||||||
|
|
||||||
|
# detect which surfaces are near external layers
|
||||||
|
$print->discover_horizontal_shells;
|
||||||
|
|
||||||
$print->extrude_fills;
|
$print->extrude_fills;
|
||||||
|
|
||||||
|
|
||||||
|
51
t/clipper.t
Normal file
51
t/clipper.t
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use Test::More;
|
||||||
|
|
||||||
|
plan tests => 1;
|
||||||
|
|
||||||
|
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 $square = [ # ccw
|
||||||
|
[5, 12],
|
||||||
|
[25, 12],
|
||||||
|
[25, 18],
|
||||||
|
[5, 18],
|
||||||
|
];
|
||||||
|
|
||||||
|
$clipper->add_subject_polygons([ $square, $hole_in_square ]);
|
||||||
|
$clipper->add_clip_polygons([ $square ]);
|
||||||
|
my $intersection = $clipper->ex_execute(CT_INTERSECTION, PFT_NONZERO, PFT_NONZERO);
|
||||||
|
|
||||||
|
is_deeply $intersection, [
|
||||||
|
{
|
||||||
|
holes => [
|
||||||
|
[
|
||||||
|
[14, 16],
|
||||||
|
[16, 16],
|
||||||
|
[16, 14],
|
||||||
|
[14, 14],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
outer => [
|
||||||
|
[5, 18],
|
||||||
|
[5, 12],
|
||||||
|
[25, 12],
|
||||||
|
[25, 18],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
], 'hole is preserved after intersection';
|
@ -1,6 +1,6 @@
|
|||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
plan tests => 4;
|
plan tests => 9;
|
||||||
|
|
||||||
BEGIN {
|
BEGIN {
|
||||||
use FindBin;
|
use FindBin;
|
||||||
@ -29,3 +29,10 @@ is $intersection, undef, 'external lines are ignored 2';
|
|||||||
|
|
||||||
$intersection = Slic3r::Geometry::clip_segment_polygon([ [12, 12], [18, 16] ], $square);
|
$intersection = Slic3r::Geometry::clip_segment_polygon([ [12, 12], [18, 16] ], $square);
|
||||||
is_deeply $intersection, [ [12, 12], [18, 16] ], 'internal lines are preserved';
|
is_deeply $intersection, [ [12, 12], [18, 16] ], 'internal lines are preserved';
|
||||||
|
|
||||||
|
is Slic3r::Geometry::point_in_segment([10, 10], [ [5, 10], [20, 10] ]), 1, 'point in horizontal segment';
|
||||||
|
is Slic3r::Geometry::point_in_segment([30, 10], [ [5, 10], [20, 10] ]), 0, 'point not in horizontal segment';
|
||||||
|
is Slic3r::Geometry::point_in_segment([10, 10], [ [10, 5], [10, 20] ]), 1, 'point in vertical segment';
|
||||||
|
is Slic3r::Geometry::point_in_segment([10, 30], [ [10, 5], [10, 20] ]), 0, 'point not in vertical segment';
|
||||||
|
is Slic3r::Geometry::point_in_segment([15, 15], [ [10, 10], [20, 20] ]), 1, 'point in diagonal segment';
|
||||||
|
is Slic3r::Geometry::point_in_segment([20, 15], [ [10, 10], [20, 20] ]), 0, 'point not in diagonal segment';
|
||||||
|
11
t/stl.t
11
t/stl.t
@ -1,6 +1,6 @@
|
|||||||
use Test::More;
|
use Test::More;
|
||||||
|
|
||||||
plan tests => 7;
|
plan tests => 11;
|
||||||
|
|
||||||
BEGIN {
|
BEGIN {
|
||||||
use FindBin;
|
use FindBin;
|
||||||
@ -28,10 +28,17 @@ is_deeply lines(28, 20, 30), [ ], 'lower vertex on la
|
|||||||
is_deeply lines(24, 10, 16), [ [ [4, 4], [2, 6] ] ], 'two edges intersect';
|
is_deeply lines(24, 10, 16), [ [ [4, 4], [2, 6] ] ], 'two edges intersect';
|
||||||
is_deeply lines(24, 10, 20), [ [ [4, 4], [1, 9] ] ], 'one vertex on plane and one edge intersects';
|
is_deeply lines(24, 10, 20), [ [ [4, 4], [1, 9] ] ], 'one vertex on plane and one edge intersects';
|
||||||
|
|
||||||
|
my @lower = $stl->intersect_facet(vertices(22, 20, 20), $z, $dz);
|
||||||
|
my @upper = $stl->intersect_facet(vertices(20, 20, 10), $z, $dz);
|
||||||
|
isa_ok $lower[0], 'Slic3r::Line::FacetEdge', 'bottom edge on layer';
|
||||||
|
isa_ok $upper[0], 'Slic3r::Line::FacetEdge', 'upper edge on layer';
|
||||||
|
is $lower[0]->edge_type, 'bottom', 'lower edge is detected as bottom';
|
||||||
|
is $upper[0]->edge_type, 'top', 'upper edge is detected as top';
|
||||||
|
|
||||||
sub vertices {
|
sub vertices {
|
||||||
[ map [ @{$points[$_]}, $_[$_] ], 0..2 ]
|
[ map [ @{$points[$_]}, $_[$_] ], 0..2 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
sub lines {
|
sub lines {
|
||||||
[ map [ map ref $_ eq 'Slic3r::Point' ? $_->p : [ map sprintf('%.0f', $_), @$_ ], @$_ ], $stl->intersect_facet(vertices(@_), $z, $dz) ];
|
[ map [ map ref $_ eq 'Slic3r::Point' ? $_->p : [ map sprintf('%.0f', $_), @$_ ], @$_ ], map $_->p, $stl->intersect_facet(vertices(@_), $z, $dz) ];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user