WIP Refactoring of Layers: Sorting of infill extrusions into LayerIslands.
FIXME: Gap fill extrusions are currently not handled!
This commit is contained in:
parent
409fae6183
commit
386cfae546
@ -244,6 +244,39 @@ auto cast(const BoundingBox3Base<Tin> &b)
|
|||||||
b.max.template cast<Tout>()};
|
b.max.template cast<Tout>()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Distance of a point to a bounding box. Zero inside and on the boundary, positive outside.
|
||||||
|
inline double bbox_point_distance(const BoundingBox &bbox, const Point &pt)
|
||||||
|
{
|
||||||
|
if (pt.x() < bbox.min.x())
|
||||||
|
return pt.y() < bbox.min.y() ? (bbox.min - pt).cast<double>().norm() :
|
||||||
|
pt.y() > bbox.max.y() ? (Point(bbox.min.x(), bbox.max.y()) - pt).cast<double>().norm() :
|
||||||
|
double(bbox.min.x() - pt.x());
|
||||||
|
else if (pt.x() > bbox.max.x())
|
||||||
|
return pt.y() < bbox.min.y() ? (Point(bbox.max.x(), bbox.min.y()) - pt).cast<double>().norm() :
|
||||||
|
pt.y() > bbox.max.y() ? (bbox.max - pt).cast<double>().norm() :
|
||||||
|
double(pt.x() - bbox.max.x());
|
||||||
|
else
|
||||||
|
return pt.y() < bbox.min.y() ? bbox.min.y() - pt.y() :
|
||||||
|
pt.y() > bbox.max.y() ? pt.y() - bbox.max.y() :
|
||||||
|
coord_t(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double bbox_point_distance_squared(const BoundingBox &bbox, const Point &pt)
|
||||||
|
{
|
||||||
|
if (pt.x() < bbox.min.x())
|
||||||
|
return pt.y() < bbox.min.y() ? (bbox.min - pt).cast<double>().squaredNorm() :
|
||||||
|
pt.y() > bbox.max.y() ? (Point(bbox.min.x(), bbox.max.y()) - pt).cast<double>().squaredNorm() :
|
||||||
|
Slic3r::sqr(double(bbox.min.x() - pt.x()));
|
||||||
|
else if (pt.x() > bbox.max.x())
|
||||||
|
return pt.y() < bbox.min.y() ? (Point(bbox.max.x(), bbox.min.y()) - pt).cast<double>().squaredNorm() :
|
||||||
|
pt.y() > bbox.max.y() ? (bbox.max - pt).cast<double>().squaredNorm() :
|
||||||
|
Slic3r::sqr<double>(pt.x() - bbox.max.x());
|
||||||
|
else
|
||||||
|
return Slic3r::sqr<double>(pt.y() < bbox.min.y() ? bbox.min.y() - pt.y() :
|
||||||
|
pt.y() > bbox.max.y() ? pt.y() - bbox.max.y() :
|
||||||
|
coord_t(0));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|
||||||
// Serialization through the Cereal library
|
// Serialization through the Cereal library
|
||||||
|
@ -117,6 +117,26 @@ ExPolygon::has_boundary_point(const Point &point) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Projection of a point onto the polygon.
|
||||||
|
Point ExPolygon::point_projection(const Point &point) const
|
||||||
|
{
|
||||||
|
if (this->holes.empty()) {
|
||||||
|
return this->contour.point_projection(point);
|
||||||
|
} else {
|
||||||
|
double dist_min2 = std::numeric_limits<double>::max();
|
||||||
|
Point closest_pt_min;
|
||||||
|
for (size_t i = 0; i < this->num_contours(); ++ i) {
|
||||||
|
Point closest_pt = this->contour_or_hole(i).point_projection(point);
|
||||||
|
double d2 = (closest_pt - point).cast<double>().squaredNorm();
|
||||||
|
if (d2 < dist_min2) {
|
||||||
|
dist_min2 = d2;
|
||||||
|
closest_pt_min = closest_pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closest_pt_min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool ExPolygon::overlaps(const ExPolygon &other) const
|
bool ExPolygon::overlaps(const ExPolygon &other) const
|
||||||
{
|
{
|
||||||
#if 0
|
#if 0
|
||||||
|
@ -53,6 +53,8 @@ public:
|
|||||||
bool contains(const Point &point) const;
|
bool contains(const Point &point) const;
|
||||||
bool contains_b(const Point &point) const;
|
bool contains_b(const Point &point) const;
|
||||||
bool has_boundary_point(const Point &point) const;
|
bool has_boundary_point(const Point &point) const;
|
||||||
|
// Projection of a point onto the polygon.
|
||||||
|
Point point_projection(const Point &point) const;
|
||||||
|
|
||||||
// Does this expolygon overlap another expolygon?
|
// Does this expolygon overlap another expolygon?
|
||||||
// Either the ExPolygons intersect, or one is fully inside the other,
|
// Either the ExPolygons intersect, or one is fully inside the other,
|
||||||
|
@ -322,6 +322,92 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static void insert_fills_into_islands(Layer &layer, uint32_t fill_region_id, uint32_t fill_begin, uint32_t fill_end)
|
||||||
|
{
|
||||||
|
if (fill_begin < fill_end) {
|
||||||
|
// Sort the extrusion range into its LayerIsland.
|
||||||
|
// Traverse the slices in an increasing order of bounding box size, so that the islands inside another islands are tested first,
|
||||||
|
// so we can just test a point inside ExPolygon::contour and we may skip testing the holes.
|
||||||
|
auto point_inside_surface = [&layer](const size_t lslice_idx, const Point &point) {
|
||||||
|
const BoundingBox &bbox = layer.lslices_ex[lslice_idx].bbox;
|
||||||
|
return point.x() >= bbox.min.x() && point.x() < bbox.max.x() &&
|
||||||
|
point.y() >= bbox.min.y() && point.y() < bbox.max.y() &&
|
||||||
|
layer.lslices[lslice_idx].contour.contains(point);
|
||||||
|
};
|
||||||
|
Point point = layer.get_region(fill_region_id)->fills().entities[fill_begin]->first_point();
|
||||||
|
int lslice_idx = int(layer.lslices_ex.size()) - 1;
|
||||||
|
for (; lslice_idx >= 0; -- lslice_idx)
|
||||||
|
if (point_inside_surface(lslice_idx, point))
|
||||||
|
break;
|
||||||
|
assert(lslice_idx >= 0);
|
||||||
|
if (lslice_idx >= 0) {
|
||||||
|
LayerSlice &lslice = layer.lslices_ex[lslice_idx];
|
||||||
|
// Find an island.
|
||||||
|
LayerIsland *island = nullptr;
|
||||||
|
if (lslice.islands.size() == 1) {
|
||||||
|
// Cool, just save the extrusions in there.
|
||||||
|
island = &lslice.islands.front();
|
||||||
|
} else {
|
||||||
|
// The infill was created for one of the infills.
|
||||||
|
// In case of ironing, the infill may not fall into any of the infill expolygons either.
|
||||||
|
// In case of some numerical error, the infill may not fall into any of the infill expolygons either.
|
||||||
|
// 1) Try an exact test, it should be cheaper than a closest region test.
|
||||||
|
for (LayerIsland &li : lslice.islands) {
|
||||||
|
const BoundingBoxes &bboxes = li.fill_expolygons_composite() ?
|
||||||
|
layer.get_region(li.perimeters.region())->fill_expolygons_composite_bboxes() :
|
||||||
|
layer.get_region(li.fill_region_id)->fill_expolygons_bboxes();
|
||||||
|
const ExPolygons &expolygons = li.fill_expolygons_composite() ?
|
||||||
|
layer.get_region(li.perimeters.region())->fill_expolygons_composite() :
|
||||||
|
layer.get_region(li.fill_region_id)->fill_expolygons();
|
||||||
|
for (uint32_t fill_expolygon_id : li.fill_expolygons)
|
||||||
|
if (bboxes[fill_expolygon_id].contains(point) && expolygons[fill_expolygon_id].contains(point)) {
|
||||||
|
island = &li;
|
||||||
|
goto found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2) Find closest fill_expolygon, branch and bound by distance to bounding box.
|
||||||
|
{
|
||||||
|
struct Island {
|
||||||
|
uint32_t island_idx;
|
||||||
|
uint32_t expolygon_idx;
|
||||||
|
double distance2;
|
||||||
|
};
|
||||||
|
std::vector<Island> islands_sorted;
|
||||||
|
for (uint32_t island_idx = 0; island_idx < uint32_t(lslice.islands.size()); ++ island_idx) {
|
||||||
|
const LayerIsland &li = lslice.islands[island_idx];
|
||||||
|
const BoundingBoxes &bboxes = li.fill_expolygons_composite() ?
|
||||||
|
layer.get_region(li.perimeters.region())->fill_expolygons_composite_bboxes() :
|
||||||
|
layer.get_region(li.fill_region_id)->fill_expolygons_bboxes();
|
||||||
|
for (uint32_t fill_expolygon_id : li.fill_expolygons)
|
||||||
|
islands_sorted.push_back({ island_idx, fill_expolygon_id, bbox_point_distance_squared(bboxes[fill_expolygon_id], point) });
|
||||||
|
}
|
||||||
|
std::sort(islands_sorted.begin(), islands_sorted.end(), [](auto &l, auto &r){ return l.distance2 < r.distance2; });
|
||||||
|
auto dist_min2 = std::numeric_limits<double>::max();
|
||||||
|
for (uint32_t sorted_bbox_idx = 0; sorted_bbox_idx < uint32_t(islands_sorted.size()); ++ sorted_bbox_idx) {
|
||||||
|
const Island &isl = islands_sorted[sorted_bbox_idx];
|
||||||
|
if (isl.distance2 > dist_min2)
|
||||||
|
// Branch & bound condition.
|
||||||
|
break;
|
||||||
|
LayerIsland &li = lslice.islands[isl.island_idx];
|
||||||
|
const ExPolygons &expolygons = li.fill_expolygons_composite() ?
|
||||||
|
layer.get_region(li.perimeters.region())->fill_expolygons_composite() :
|
||||||
|
layer.get_region(li.fill_region_id)->fill_expolygons();
|
||||||
|
double d2 = (expolygons[isl.expolygon_idx].point_projection(point) - point).cast<double>().squaredNorm();
|
||||||
|
if (d2 < dist_min2) {
|
||||||
|
dist_min2 = d2;
|
||||||
|
island = &li;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found:;
|
||||||
|
}
|
||||||
|
assert(island);
|
||||||
|
if (island)
|
||||||
|
island->fills.push_back(LayerExtrusionRange{ fill_region_id, { fill_begin, fill_end }});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// friend to Layer
|
// friend to Layer
|
||||||
void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator)
|
void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator)
|
||||||
{
|
{
|
||||||
@ -382,6 +468,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
// Used by the concentric infill pattern to clip the loops to create extrusion paths.
|
// Used by the concentric infill pattern to clip the loops to create extrusion paths.
|
||||||
f->loop_clipping = coord_t(scale_(surface_fill.params.flow.nozzle_diameter()) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER);
|
f->loop_clipping = coord_t(scale_(surface_fill.params.flow.nozzle_diameter()) * LOOP_CLIPPING_LENGTH_OVER_NOZZLE_DIAMETER);
|
||||||
|
|
||||||
|
LayerRegion &layerm = *m_regions[surface_fill.region_id];
|
||||||
|
|
||||||
// apply half spacing using this flow's own spacing and generate infill
|
// apply half spacing using this flow's own spacing and generate infill
|
||||||
FillParams params;
|
FillParams params;
|
||||||
params.density = float(0.01 * surface_fill.params.density);
|
params.density = float(0.01 * surface_fill.params.density);
|
||||||
@ -390,7 +478,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
params.anchor_length_max = surface_fill.params.anchor_length_max;
|
params.anchor_length_max = surface_fill.params.anchor_length_max;
|
||||||
params.resolution = resolution;
|
params.resolution = resolution;
|
||||||
params.use_arachne = perimeter_generator == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric;
|
params.use_arachne = perimeter_generator == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric;
|
||||||
params.layer_height = m_regions[surface_fill.region_id]->layer()->height;
|
params.layer_height = layerm.layer()->height;
|
||||||
|
|
||||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||||
@ -421,7 +509,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
}
|
}
|
||||||
// Save into layer.
|
// Save into layer.
|
||||||
ExtrusionEntityCollection* eec = nullptr;
|
ExtrusionEntityCollection* eec = nullptr;
|
||||||
m_regions[surface_fill.region_id]->m_fills.entities.push_back(eec = new ExtrusionEntityCollection());
|
auto fill_begin = uint32_t(layerm.fills().size());
|
||||||
|
layerm.m_fills.entities.push_back(eec = new ExtrusionEntityCollection());
|
||||||
// Only concentric fills are not sorted.
|
// Only concentric fills are not sorted.
|
||||||
eec->no_sort = f->no_sort();
|
eec->no_sort = f->no_sort();
|
||||||
if (params.use_arachne) {
|
if (params.use_arachne) {
|
||||||
@ -445,10 +534,14 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
surface_fill.params.extrusion_role,
|
surface_fill.params.extrusion_role,
|
||||||
flow_mm3_per_mm, float(flow_width), surface_fill.params.flow.height());
|
flow_mm3_per_mm, float(flow_width), surface_fill.params.flow.height());
|
||||||
}
|
}
|
||||||
|
insert_fills_into_islands(*this, uint32_t(surface_fill.region_id), fill_begin, uint32_t(layerm.fills().size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME Don't copy thin fill extrusions into fills, just use these thin fill extrusions
|
||||||
|
// from the G-code export directly.
|
||||||
|
#if 0
|
||||||
// add thin fill regions
|
// add thin fill regions
|
||||||
// Unpacks the collection, creates multiple collections per path.
|
// Unpacks the collection, creates multiple collections per path.
|
||||||
// The path type could be ExtrusionPath, ExtrusionLoop or ExtrusionEntityCollection.
|
// The path type could be ExtrusionPath, ExtrusionLoop or ExtrusionEntityCollection.
|
||||||
@ -459,6 +552,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||||||
layerm->m_fills.entities.push_back(&collection);
|
layerm->m_fills.entities.push_back(&collection);
|
||||||
collection.entities.push_back(thin_fill->clone());
|
collection.entities.push_back(thin_fill->clone());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
for (LayerRegion *layerm : m_regions)
|
for (LayerRegion *layerm : m_regions)
|
||||||
@ -519,7 +613,8 @@ void Layer::make_ironing()
|
|||||||
this->angle == rhs.angle;
|
this->angle == rhs.angle;
|
||||||
}
|
}
|
||||||
|
|
||||||
LayerRegion *layerm = nullptr;
|
LayerRegion *layerm;
|
||||||
|
uint32_t region_id;
|
||||||
|
|
||||||
// IdeaMaker: ironing
|
// IdeaMaker: ironing
|
||||||
// ironing flowrate (5% percent)
|
// ironing flowrate (5% percent)
|
||||||
@ -539,8 +634,8 @@ void Layer::make_ironing()
|
|||||||
std::vector<IroningParams> by_extruder;
|
std::vector<IroningParams> by_extruder;
|
||||||
double default_layer_height = this->object()->config().layer_height;
|
double default_layer_height = this->object()->config().layer_height;
|
||||||
|
|
||||||
for (LayerRegion *layerm : m_regions)
|
for (uint32_t region_id = 0; region_id < uint32_t(this->regions().size()); ++region_id)
|
||||||
if (! layerm->slices().empty()) {
|
if (LayerRegion *layerm = this->get_region(region_id); ! layerm->slices().empty()) {
|
||||||
IroningParams ironing_params;
|
IroningParams ironing_params;
|
||||||
const PrintRegionConfig &config = layerm->region().config();
|
const PrintRegionConfig &config = layerm->region().config();
|
||||||
if (config.ironing &&
|
if (config.ironing &&
|
||||||
@ -564,6 +659,7 @@ void Layer::make_ironing()
|
|||||||
ironing_params.speed = config.ironing_speed;
|
ironing_params.speed = config.ironing_speed;
|
||||||
ironing_params.angle = config.fill_angle * M_PI / 180.;
|
ironing_params.angle = config.fill_angle * M_PI / 180.;
|
||||||
ironing_params.layerm = layerm;
|
ironing_params.layerm = layerm;
|
||||||
|
ironing_params.region_id = region_id;
|
||||||
by_extruder.emplace_back(ironing_params);
|
by_extruder.emplace_back(ironing_params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -659,6 +755,7 @@ void Layer::make_ironing()
|
|||||||
}
|
}
|
||||||
if (! polylines.empty()) {
|
if (! polylines.empty()) {
|
||||||
// Save into layer.
|
// Save into layer.
|
||||||
|
auto fill_begin = uint32_t(ironing_params.layerm->fills().size());
|
||||||
ExtrusionEntityCollection *eec = nullptr;
|
ExtrusionEntityCollection *eec = nullptr;
|
||||||
ironing_params.layerm->m_fills.entities.push_back(eec = new ExtrusionEntityCollection());
|
ironing_params.layerm->m_fills.entities.push_back(eec = new ExtrusionEntityCollection());
|
||||||
// Don't sort the ironing infill lines as they are monotonicly ordered.
|
// Don't sort the ironing infill lines as they are monotonicly ordered.
|
||||||
@ -667,6 +764,7 @@ void Layer::make_ironing()
|
|||||||
eec->entities, std::move(polylines),
|
eec->entities, std::move(polylines),
|
||||||
erIroning,
|
erIroning,
|
||||||
flow_mm3_per_mm, extrusion_width, float(extrusion_height));
|
flow_mm3_per_mm, extrusion_width, float(extrusion_height));
|
||||||
|
insert_fills_into_islands(*this, ironing_params.region_id, fill_begin, uint32_t(ironing_params.layerm->fills().size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,35 +32,33 @@ namespace FillLightning {
|
|||||||
template<typename T>
|
template<typename T>
|
||||||
class IndexRange
|
class IndexRange
|
||||||
{
|
{
|
||||||
private:
|
|
||||||
// Just a bare minimum functionality iterator required by range-for loop.
|
|
||||||
template<typename T>
|
|
||||||
class IteratorType {
|
|
||||||
public:
|
|
||||||
T operator*() const { return m_idx; }
|
|
||||||
bool operator!=(const IteratorType &rhs) const { return m_idx != rhs.m_idx; }
|
|
||||||
void operator++() { ++ m_idx; }
|
|
||||||
private:
|
|
||||||
friend class IndexRange<T>;
|
|
||||||
IteratorType(T idx) : m_idx(idx) {}
|
|
||||||
T m_idx;
|
|
||||||
};
|
|
||||||
// Index of the first extrusion in LayerRegion.
|
|
||||||
T m_begin { 0 };
|
|
||||||
// Index of the last extrusion in LayerRegion.
|
|
||||||
T m_end { 0 };
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
IndexRange(T ibegin, T iend) : m_begin(ibegin), m_end(iend) {}
|
IndexRange(T ibegin, T iend) : m_begin(ibegin), m_end(iend) {}
|
||||||
IndexRange() = default;
|
IndexRange() = default;
|
||||||
|
|
||||||
using Iterator = IteratorType<T>;
|
// Just a bare minimum functionality iterator required by range-for loop.
|
||||||
|
class Iterator {
|
||||||
|
public:
|
||||||
|
T operator*() const { return m_idx; }
|
||||||
|
bool operator!=(const Iterator &rhs) const { return m_idx != rhs.m_idx; }
|
||||||
|
void operator++() { ++ m_idx; }
|
||||||
|
private:
|
||||||
|
friend class IndexRange<T>;
|
||||||
|
Iterator(T idx) : m_idx(idx) {}
|
||||||
|
T m_idx;
|
||||||
|
};
|
||||||
|
|
||||||
Iterator begin() const { assert(m_begin <= m_end); return Iterator(m_begin); };
|
Iterator begin() const { assert(m_begin <= m_end); return Iterator(m_begin); };
|
||||||
Iterator end() const { assert(m_begin <= m_end); return Iterator(m_end); };
|
Iterator end() const { assert(m_begin <= m_end); return Iterator(m_end); };
|
||||||
|
|
||||||
bool empty() const { assert(m_begin <= m_end); return m_begin >= m_end; }
|
bool empty() const { assert(m_begin <= m_end); return m_begin >= m_end; }
|
||||||
T size() const { assert(m_begin <= m_end); return m_end - m_begin; }
|
T size() const { assert(m_begin <= m_end); return m_end - m_begin; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Index of the first extrusion in LayerRegion.
|
||||||
|
T m_begin { 0 };
|
||||||
|
// Index of the last extrusion in LayerRegion.
|
||||||
|
T m_end { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
using ExtrusionRange = IndexRange<uint32_t>;
|
using ExtrusionRange = IndexRange<uint32_t>;
|
||||||
@ -70,7 +68,6 @@ using ExPolygonRange = IndexRange<uint32_t>;
|
|||||||
class LayerExtrusionRange : public ExtrusionRange
|
class LayerExtrusionRange : public ExtrusionRange
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LayerExtrusionRange(uint32_t iregion, uint32_t ibegin, uint32_t iend) : m_region(iregion), ExtrusionRange(ibegin, iend) {}
|
|
||||||
LayerExtrusionRange(uint32_t iregion, ExtrusionRange extrusion_range) : m_region(iregion), ExtrusionRange(extrusion_range) {}
|
LayerExtrusionRange(uint32_t iregion, ExtrusionRange extrusion_range) : m_region(iregion), ExtrusionRange(extrusion_range) {}
|
||||||
LayerExtrusionRange() = default;
|
LayerExtrusionRange() = default;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user