mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-08 07:27:41 -06:00
ENH: enable arachne for concentric pattern
Enable arachne for concentric pattern by referring to PrusaSlicer Also remove useless pattern we added. Signed-off-by: salt.wei <salt.wei@bambulab.com> Change-Id: Ie2574f7fc4751ebdf1caab4de52013f3101e104f
This commit is contained in:
parent
df321f8cd9
commit
db9ade2257
10 changed files with 153 additions and 167 deletions
|
@ -67,8 +67,6 @@ set(lisbslic3r_sources
|
|||
Fill/FillBase.hpp
|
||||
Fill/FillConcentric.cpp
|
||||
Fill/FillConcentric.hpp
|
||||
Fill/FillConcentricWGapFill.cpp
|
||||
Fill/FillConcentricWGapFill.hpp
|
||||
Fill/FillConcentricInternal.cpp
|
||||
Fill/FillConcentricInternal.hpp
|
||||
Fill/FillHoneycomb.cpp
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "FillBase.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
#include "FillConcentric.hpp"
|
||||
|
||||
#define NARROW_INFILL_AREA_THRESHOLD 3
|
||||
|
||||
|
@ -407,6 +408,11 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||
assert(fill_concentric != nullptr);
|
||||
fill_concentric->print_config = &this->object()->print()->config();
|
||||
fill_concentric->print_object_config = &this->object()->config();
|
||||
} else if (surface_fill.params.pattern == ipConcentric) {
|
||||
FillConcentric *fill_concentric = dynamic_cast<FillConcentric *>(f.get());
|
||||
assert(fill_concentric != nullptr);
|
||||
fill_concentric->print_config = &this->object()->print()->config();
|
||||
fill_concentric->print_object_config = &this->object()->config();
|
||||
}
|
||||
|
||||
// calculate flow spacing for infill pattern generation
|
||||
|
@ -434,6 +440,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
|||
params.anchor_length = surface_fill.params.anchor_length;
|
||||
params.anchor_length_max = surface_fill.params.anchor_length_max;
|
||||
params.resolution = resolution;
|
||||
params.use_arachne = surface_fill.params.pattern == ipConcentric;
|
||||
|
||||
// BBS
|
||||
params.flow = surface_fill.params.flow;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "../PrintConfig.hpp"
|
||||
#include "../Surface.hpp"
|
||||
#include "../libslic3r.h"
|
||||
#include "../VariableWidth.hpp"
|
||||
|
||||
#include "FillBase.hpp"
|
||||
#include "FillConcentric.hpp"
|
||||
|
@ -21,7 +22,6 @@
|
|||
#include "FillAdaptive.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
// BBS: new infill pattern header
|
||||
#include "FillConcentricWGapFill.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
|
||||
// #define INFILL_DEBUG_OUTPUT
|
||||
|
@ -58,7 +58,6 @@ Fill* Fill::new_from_type(const InfillPattern type)
|
|||
case ipLightning: return new FillLightning::Filler();
|
||||
#endif // HAS_LIGHTNING_INFILL
|
||||
// BBS: for internal solid infill only
|
||||
case ipConcentricGapFill: return new FillConcentricWGapFill();
|
||||
case ipConcentricInternal: return new FillConcentricInternal();
|
||||
// BBS: for bottom and top surface only
|
||||
case ipMonotonicLine: return new FillMonotonicLineWGapFill();
|
||||
|
@ -107,16 +106,31 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
|
|||
return polylines_out;
|
||||
}
|
||||
|
||||
ThickPolylines Fill::fill_surface_arachne(const Surface* surface, const FillParams& params)
|
||||
{
|
||||
// Perform offset.
|
||||
Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing)));
|
||||
// Create the infills for each of the regions.
|
||||
ThickPolylines thick_polylines_out;
|
||||
for (ExPolygon& expoly : expp)
|
||||
_fill_surface_single(params, surface->thickness_layers, _infill_direction(surface), std::move(expoly), thick_polylines_out);
|
||||
return thick_polylines_out;
|
||||
}
|
||||
|
||||
// BBS: this method is used to fill the ExtrusionEntityCollection. It call fill_surface by default
|
||||
void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& params, ExtrusionEntitiesPtr& out)
|
||||
{
|
||||
Polylines polylines;
|
||||
ThickPolylines thick_polylines;
|
||||
try {
|
||||
polylines = this->fill_surface(surface, params);
|
||||
if (params.use_arachne)
|
||||
thick_polylines = this->fill_surface_arachne(surface, params);
|
||||
else
|
||||
polylines = this->fill_surface(surface, params);
|
||||
}
|
||||
catch (InfillFailedException&) {}
|
||||
|
||||
if (!polylines.empty()) {
|
||||
if (!polylines.empty() || !thick_polylines.empty()) {
|
||||
// calculate actual flow from spacing (which might have been adjusted by the infill
|
||||
// pattern generator)
|
||||
double flow_mm3_per_mm = params.flow.mm3_per_mm();
|
||||
|
@ -136,10 +150,17 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
|
|||
out.push_back(eec = new ExtrusionEntityCollection());
|
||||
// Only concentric fills are not sorted.
|
||||
eec->no_sort = this->no_sort();
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
flow_mm3_per_mm, float(flow_width), params.flow.height());
|
||||
if (params.use_arachne) {
|
||||
Flow new_flow = params.flow.with_spacing(float(this->spacing));
|
||||
variable_width(thick_polylines, params.extrusion_role, new_flow, eec->entities);
|
||||
thick_polylines.clear();
|
||||
}
|
||||
else {
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
flow_mm3_per_mm, float(flow_width), params.flow.height());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,9 @@ struct FillParams
|
|||
// in this case we don't try to make more continuous paths
|
||||
bool complete { false };
|
||||
|
||||
// For Concentric infill, to switch between Classic and Arachne.
|
||||
bool use_arachne{ false };
|
||||
|
||||
// BBS
|
||||
Flow flow;
|
||||
ExtrusionRole extrusion_role{ ExtrusionRole(0) };
|
||||
|
@ -121,6 +124,7 @@ public:
|
|||
|
||||
// Perform the fill.
|
||||
virtual Polylines fill_surface(const Surface *surface, const FillParams ¶ms);
|
||||
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
|
||||
|
||||
// BBS: this method is used to fill the ExtrusionEntityCollection.
|
||||
// It call fill_surface by default
|
||||
|
@ -149,6 +153,13 @@ protected:
|
|||
ExPolygon /* expolygon */,
|
||||
Polylines & /* polylines_out */) {};
|
||||
|
||||
// Used for concentric infill to generate ThickPolylines using Arachne.
|
||||
virtual void _fill_surface_single(const FillParams& params,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point>& direction,
|
||||
ExPolygon expolygon,
|
||||
ThickPolylines& thick_polylines_out) {}
|
||||
|
||||
virtual float _layer_angle(size_t idx) const { return (idx & 1) ? float(M_PI/2.) : 0; }
|
||||
|
||||
virtual std::pair<float, Point> _infill_direction(const Surface *surface) const;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#include "../ClipperUtils.hpp"
|
||||
#include "../ExPolygon.hpp"
|
||||
#include "../Surface.hpp"
|
||||
#include "../VariableWidth.hpp"
|
||||
#include "Arachne/WallToolPaths.hpp"
|
||||
|
||||
#include "FillConcentric.hpp"
|
||||
|
||||
|
@ -61,4 +63,84 @@ void FillConcentric::_fill_surface_single(
|
|||
// We want the loops to be split inside the G-code generator to get optimum path planning.
|
||||
}
|
||||
|
||||
void FillConcentric::_fill_surface_single(const FillParams& params,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point>& direction,
|
||||
ExPolygon expolygon,
|
||||
ThickPolylines& thick_polylines_out)
|
||||
{
|
||||
assert(params.use_arachne);
|
||||
assert(this->print_config != nullptr && this->print_object_config != nullptr);
|
||||
|
||||
// no rotation is supported for this infill pattern
|
||||
Point bbox_size = expolygon.contour.bounding_box().size();
|
||||
coord_t min_spacing = scaled<coord_t>(this->spacing);
|
||||
|
||||
if (params.density > 0.9999f && !params.dont_adjust) {
|
||||
coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
|
||||
Polygons polygons = offset(expolygon, float(min_spacing) / 2.f);
|
||||
|
||||
double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end());
|
||||
Arachne::WallToolPathsParams input_params;
|
||||
input_params.min_bead_width = 0.85 * min_nozzle_diameter;
|
||||
input_params.min_feature_size = 0.1;
|
||||
input_params.wall_transition_length = 1.0 * min_nozzle_diameter;
|
||||
input_params.wall_transition_angle = 10;
|
||||
input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter;
|
||||
input_params.wall_distribution_count = 1;
|
||||
input_params.wall_add_middle_threshold = 0.75;
|
||||
input_params.wall_split_middle_threshold = 0.5;
|
||||
|
||||
Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, input_params);
|
||||
|
||||
std::vector<Arachne::VariableWidthLines> loops = wallToolPaths.getToolPaths();
|
||||
std::vector<const Arachne::ExtrusionLine*> all_extrusions;
|
||||
for (Arachne::VariableWidthLines& loop : loops) {
|
||||
if (loop.empty())
|
||||
continue;
|
||||
for (const Arachne::ExtrusionLine& wall : loop)
|
||||
all_extrusions.emplace_back(&wall);
|
||||
}
|
||||
|
||||
// Split paths using a nearest neighbor search.
|
||||
size_t firts_poly_idx = thick_polylines_out.size();
|
||||
Point last_pos(0, 0);
|
||||
for (const Arachne::ExtrusionLine* extrusion : all_extrusions) {
|
||||
if (extrusion->empty())
|
||||
continue;
|
||||
|
||||
ThickPolyline thick_polyline = Arachne::to_thick_polyline(*extrusion);
|
||||
if (extrusion->is_closed && thick_polyline.points.front() == thick_polyline.points.back() && thick_polyline.width.front() == thick_polyline.width.back()) {
|
||||
thick_polyline.points.pop_back();
|
||||
assert(thick_polyline.points.size() * 2 == thick_polyline.width.size());
|
||||
int nearest_idx = last_pos.nearest_point_index(thick_polyline.points);
|
||||
std::rotate(thick_polyline.points.begin(), thick_polyline.points.begin() + nearest_idx, thick_polyline.points.end());
|
||||
std::rotate(thick_polyline.width.begin(), thick_polyline.width.begin() + 2 * nearest_idx, thick_polyline.width.end());
|
||||
thick_polyline.points.emplace_back(thick_polyline.points.front());
|
||||
}
|
||||
thick_polylines_out.emplace_back(std::move(thick_polyline));
|
||||
last_pos = thick_polylines_out.back().last_point();
|
||||
}
|
||||
|
||||
// clip the paths to prevent the extruder from getting exactly on the first point of the loop
|
||||
// Keep valid paths only.
|
||||
size_t j = firts_poly_idx;
|
||||
for (size_t i = firts_poly_idx; i < thick_polylines_out.size(); ++i) {
|
||||
thick_polylines_out[i].clip_end(this->loop_clipping);
|
||||
if (thick_polylines_out[i].is_valid()) {
|
||||
if (j < i)
|
||||
thick_polylines_out[j] = std::move(thick_polylines_out[i]);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
if (j < thick_polylines_out.size())
|
||||
thick_polylines_out.erase(thick_polylines_out.begin() + int(j), thick_polylines_out.end());
|
||||
}
|
||||
else {
|
||||
Polylines polylines;
|
||||
this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
|
||||
append(thick_polylines_out, to_thick_polylines(std::move(polylines), min_spacing));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
|
|
@ -19,7 +19,18 @@ protected:
|
|||
ExPolygon expolygon,
|
||||
Polylines &polylines_out) override;
|
||||
|
||||
void _fill_surface_single(const FillParams& params,
|
||||
unsigned int thickness_layers,
|
||||
const std::pair<float, Point>& direction,
|
||||
ExPolygon expolygon,
|
||||
ThickPolylines& thick_polylines_out) override;
|
||||
|
||||
bool no_sort() const override { return true; }
|
||||
|
||||
const PrintConfig* print_config = nullptr;
|
||||
const PrintObjectConfig* print_object_config = nullptr;
|
||||
|
||||
friend class Layer;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
#include "../ClipperUtils.hpp"
|
||||
#include "../ExPolygon.hpp"
|
||||
#include "../Surface.hpp"
|
||||
#include "../VariableWidth.hpp"
|
||||
#include "../ShortestPath.hpp"
|
||||
|
||||
#include "FillConcentricWGapFill.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
const float concentric_overlap_threshold = 0.02;
|
||||
|
||||
void FillConcentricWGapFill::fill_surface_extrusion(const Surface* surface, const FillParams& params, ExtrusionEntitiesPtr& out)
|
||||
{
|
||||
//BBS: FillConcentricWGapFill.cpp is absolutely newly add by BBL for narrow internal solid infill area to reduce vibration
|
||||
// Because the area is narrow, we should not use the surface->expolygon which has overlap with perimeter, but
|
||||
// use no_overlap_expolygons instead to avoid overflow in narrow area.
|
||||
//Slic3r::ExPolygons expp = offset_ex(surface->expolygon, double(scale_(0 - 0.5 * this->spacing)));
|
||||
float min_spacing = this->spacing * (1 - concentric_overlap_threshold);
|
||||
Slic3r::ExPolygons expp = offset2_ex(this->no_overlap_expolygons, -double(scale_(0.5 * this->spacing + 0.5 * min_spacing) - 1),
|
||||
+double(scale_(0.5 * min_spacing) - 1));
|
||||
// Create the infills for each of the regions.
|
||||
Polylines polylines_out;
|
||||
for (size_t i = 0; i < expp.size(); ++i) {
|
||||
ExPolygon expolygon = expp[i];
|
||||
|
||||
coord_t distance = scale_(this->spacing / params.density);
|
||||
if (params.density > 0.9999f && !params.dont_adjust) {
|
||||
distance = scale_(this->spacing);
|
||||
}
|
||||
|
||||
ExPolygons gaps;
|
||||
Polygons loops = (Polygons)expolygon;
|
||||
ExPolygons last = { expolygon };
|
||||
bool first = true;
|
||||
while (!last.empty()) {
|
||||
ExPolygons next_onion = offset2_ex(last, -double(distance + scale_(this->spacing) / 2), +double(scale_(this->spacing) / 2));
|
||||
for (auto it = next_onion.begin(); it != next_onion.end(); it++) {
|
||||
Polygons temp_loops = (Polygons)(*it);
|
||||
loops.insert(loops.end(), temp_loops.begin(), temp_loops.end());
|
||||
}
|
||||
append(gaps, diff_ex(
|
||||
offset(last, -0.5f * distance),
|
||||
offset(next_onion, 0.5f * distance + 10))); // 10 is safty offset
|
||||
last = next_onion;
|
||||
if (first && !this->no_overlap_expolygons.empty()) {
|
||||
gaps = intersection_ex(gaps, this->no_overlap_expolygons);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
ExtrusionRole good_role = params.extrusion_role;
|
||||
ExtrusionEntityCollection *coll_nosort = new ExtrusionEntityCollection();
|
||||
coll_nosort->no_sort = this->no_sort(); //can be sorted inside the pass
|
||||
extrusion_entities_append_loops(
|
||||
coll_nosort->entities, std::move(loops),
|
||||
good_role,
|
||||
params.flow.mm3_per_mm(),
|
||||
params.flow.width(),
|
||||
params.flow.height());
|
||||
|
||||
//BBS: add internal gapfills between infill loops
|
||||
if (!gaps.empty() && params.density >= 1) {
|
||||
double min = 0.2 * distance * (1 - INSET_OVERLAP_TOLERANCE);
|
||||
double max = 2. * distance;
|
||||
ExPolygons gaps_ex = diff_ex(
|
||||
offset2_ex(gaps, -float(min / 2), float(min / 2)),
|
||||
offset2_ex(gaps, -float(max / 2), float(max / 2)),
|
||||
ApplySafetyOffset::Yes);
|
||||
//BBS: sort the gap_ex to avoid mess travel
|
||||
Points ordering_points;
|
||||
ordering_points.reserve(gaps_ex.size());
|
||||
ExPolygons gaps_ex_sorted;
|
||||
gaps_ex_sorted.reserve(gaps_ex.size());
|
||||
for (const ExPolygon &ex : gaps_ex)
|
||||
ordering_points.push_back(ex.contour.first_point());
|
||||
std::vector<Points::size_type> order = chain_points(ordering_points);
|
||||
for (size_t i : order)
|
||||
gaps_ex_sorted.emplace_back(std::move(gaps_ex[i]));
|
||||
|
||||
ThickPolylines polylines;
|
||||
for (ExPolygon& ex : gaps_ex_sorted) {
|
||||
//BBS: Use DP simplify to avoid duplicated points and accelerate medial-axis calculation as well.
|
||||
ex.douglas_peucker(SCALED_RESOLUTION * 0.1);
|
||||
ex.medial_axis(max, min, &polylines);
|
||||
}
|
||||
|
||||
if (!polylines.empty() && !is_bridge(good_role)) {
|
||||
ExtrusionEntityCollection gap_fill;
|
||||
variable_width(polylines, erGapFill, params.flow, gap_fill.entities);
|
||||
coll_nosort->append(std::move(gap_fill.entities));
|
||||
}
|
||||
}
|
||||
|
||||
if (!coll_nosort->entities.empty())
|
||||
out.push_back(coll_nosort);
|
||||
else
|
||||
delete coll_nosort;
|
||||
}
|
||||
|
||||
//BBS: add external gapfill between perimeter and infill
|
||||
ExPolygons external_gaps = diff_ex(this->no_overlap_expolygons, offset_ex(expp, double(scale_(0.5 * this->spacing))), ApplySafetyOffset::Yes);
|
||||
external_gaps = union_ex(external_gaps);
|
||||
if (!this->no_overlap_expolygons.empty())
|
||||
external_gaps = intersection_ex(external_gaps, this->no_overlap_expolygons);
|
||||
|
||||
if (!external_gaps.empty()) {
|
||||
double min = 0.4 * scale_(params.flow.nozzle_diameter()) * (1 - INSET_OVERLAP_TOLERANCE);
|
||||
double max = 2. * params.flow.scaled_width();
|
||||
//BBS: collapse, be sure we don't gapfill where the perimeters are already touching each other (negative spacing).
|
||||
min = std::max(min, (double)Flow::rounded_rectangle_extrusion_width_from_spacing((float)EPSILON, (float)params.flow.height()));
|
||||
ExPolygons external_gaps_collapsed = offset2_ex(external_gaps, double(-min / 2), double(+min / 2));
|
||||
|
||||
ThickPolylines polylines;
|
||||
for (ExPolygon& ex : external_gaps_collapsed) {
|
||||
//BBS: Use DP simplify to avoid duplicated points and accelerate medial-axis calculation as well.
|
||||
ex.douglas_peucker(SCALED_RESOLUTION * 0.1);
|
||||
ex.medial_axis(max, min, &polylines);
|
||||
}
|
||||
|
||||
ExtrusionEntityCollection* coll_external_gapfill = new ExtrusionEntityCollection();
|
||||
coll_external_gapfill->no_sort = this->no_sort();
|
||||
if (!polylines.empty() && !is_bridge(params.extrusion_role)) {
|
||||
ExtrusionEntityCollection gap_fill;
|
||||
variable_width(polylines, erGapFill, params.flow, gap_fill.entities);
|
||||
coll_external_gapfill->append(std::move(gap_fill.entities));
|
||||
}
|
||||
if (!coll_external_gapfill->entities.empty())
|
||||
out.push_back(coll_external_gapfill);
|
||||
else
|
||||
delete coll_external_gapfill;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
#ifndef slic3r_FillConcentricWGapFil_hpp_
|
||||
#define slic3r_FillConcentricWGapFil_hpp_
|
||||
|
||||
#include "FillBase.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class FillConcentricWGapFill : public Fill
|
||||
{
|
||||
public:
|
||||
~FillConcentricWGapFill() override = default;
|
||||
void fill_surface_extrusion(const Surface *surface, const FillParams ¶ms, ExtrusionEntitiesPtr &out) override;
|
||||
|
||||
protected:
|
||||
Fill* clone() const override { return new FillConcentricWGapFill(*this); };
|
||||
bool no_sort() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillConcentricWGapFil_hpp_
|
|
@ -239,6 +239,18 @@ public:
|
|||
std::pair<bool,bool> endpoints;
|
||||
};
|
||||
|
||||
inline ThickPolylines to_thick_polylines(Polylines&& polylines, const coordf_t width)
|
||||
{
|
||||
ThickPolylines out;
|
||||
out.reserve(polylines.size());
|
||||
for (Polyline& polyline : polylines) {
|
||||
out.emplace_back();
|
||||
out.back().width.assign((polyline.points.size() - 1) * 2, width);
|
||||
out.back().points = std::move(polyline.points);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class Polyline3 : public MultiPoint3
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -46,7 +46,7 @@ enum class FuzzySkinType {
|
|||
|
||||
enum InfillPattern : int {
|
||||
ipConcentric, ipRectilinear, ipGrid, ipLine, ipCubic, ipTriangles, ipStars, ipGyroid, ipHoneycomb, ipAdaptiveCubic, ipMonotonic, ipMonotonicLine, ipAlignedRectilinear, ip3DHoneycomb,
|
||||
ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipSupportCubic, ipSupportBase, ipConcentricGapFill, ipConcentricInternal,
|
||||
ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipSupportCubic, ipSupportBase, ipConcentricInternal,
|
||||
#if HAS_LIGHTNING_INFILL
|
||||
ipLightning,
|
||||
#endif // HAS_LIGHTNING_INFILL
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue