b101a8e266
The offset curve extractor is already quite usable, though singular cases are still not covered yet when the offset curve intersects or nearly intersects a Voronoi vertex. Removal of the PRINTF_ZU "%zu" Visual Studio printf compatibility macro. Fixes of a contours self intersection test for collinear segments. SVG exporter now exports white background, so that the GNOME Eye viewer is usable.
478 lines
21 KiB
C++
478 lines
21 KiB
C++
#include "Layer.hpp"
|
|
#include "BridgeDetector.hpp"
|
|
#include "ClipperUtils.hpp"
|
|
#include "Geometry.hpp"
|
|
#include "PerimeterGenerator.hpp"
|
|
#include "Print.hpp"
|
|
#include "Surface.hpp"
|
|
#include "BoundingBox.hpp"
|
|
#include "SVG.hpp"
|
|
|
|
#include <string>
|
|
#include <map>
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
namespace Slic3r {
|
|
|
|
Flow LayerRegion::flow(FlowRole role, bool bridge, double width) const
|
|
{
|
|
return m_region->flow(
|
|
role,
|
|
m_layer->height,
|
|
bridge,
|
|
m_layer->id() == 0,
|
|
width,
|
|
*m_layer->object()
|
|
);
|
|
}
|
|
|
|
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
|
|
void LayerRegion::slices_to_fill_surfaces_clipped()
|
|
{
|
|
// Note: this method should be idempotent, but fill_surfaces gets modified
|
|
// in place. However we're now only using its boundaries (which are invariant)
|
|
// so we're safe. This guarantees idempotence of prepare_infill() also in case
|
|
// that combine_infill() turns some fill_surface into VOID surfaces.
|
|
// Polygons fill_boundaries = to_polygons(std::move(this->fill_surfaces));
|
|
Polygons fill_boundaries = to_polygons(this->fill_expolygons);
|
|
// Collect polygons per surface type.
|
|
std::vector<Polygons> polygons_by_surface;
|
|
polygons_by_surface.assign(size_t(stCount), Polygons());
|
|
for (Surface &surface : this->slices.surfaces)
|
|
polygons_append(polygons_by_surface[(size_t)surface.surface_type], surface.expolygon);
|
|
// Trim surfaces by the fill_boundaries.
|
|
this->fill_surfaces.surfaces.clear();
|
|
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
|
|
const Polygons &polygons = polygons_by_surface[surface_type];
|
|
if (! polygons.empty())
|
|
this->fill_surfaces.append(intersection_ex(polygons, fill_boundaries), SurfaceType(surface_type));
|
|
}
|
|
}
|
|
|
|
void LayerRegion::make_perimeters(const SurfaceCollection &slices, SurfaceCollection* fill_surfaces)
|
|
{
|
|
this->perimeters.clear();
|
|
this->thin_fills.clear();
|
|
|
|
PerimeterGenerator g(
|
|
// input:
|
|
&slices,
|
|
this->layer()->height,
|
|
this->flow(frPerimeter),
|
|
&this->region()->config(),
|
|
&this->layer()->object()->config(),
|
|
&this->layer()->object()->print()->config(),
|
|
|
|
// output:
|
|
&this->perimeters,
|
|
&this->thin_fills,
|
|
fill_surfaces
|
|
);
|
|
|
|
if (this->layer()->lower_layer != nullptr)
|
|
// Cummulative sum of polygons over all the regions.
|
|
g.lower_slices = &this->layer()->lower_layer->lslices;
|
|
|
|
g.layer_id = (int)this->layer()->id();
|
|
g.ext_perimeter_flow = this->flow(frExternalPerimeter);
|
|
g.overhang_flow = this->region()->flow(frPerimeter, -1, true, false, -1, *this->layer()->object());
|
|
g.solid_infill_flow = this->flow(frSolidInfill);
|
|
|
|
g.process();
|
|
}
|
|
|
|
//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3.
|
|
//#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5
|
|
#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0.
|
|
|
|
void LayerRegion::process_external_surfaces(const Layer *lower_layer, const Polygons *lower_layer_covered)
|
|
{
|
|
const bool has_infill = this->region()->config().fill_density.value > 0.;
|
|
const float margin = float(scale_(EXTERNAL_INFILL_MARGIN));
|
|
|
|
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
|
export_region_fill_surfaces_to_svg_debug("3_process_external_surfaces-initial");
|
|
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
|
|
|
// 1) Collect bottom and bridge surfaces, each of them grown by a fixed 3mm offset
|
|
// for better anchoring.
|
|
// Bottom surfaces, grown.
|
|
Surfaces bottom;
|
|
// Bridge surfaces, initialy not grown.
|
|
Surfaces bridges;
|
|
// Top surfaces, grown.
|
|
Surfaces top;
|
|
// Internal surfaces, not grown.
|
|
Surfaces internal;
|
|
// Areas, where an infill of various types (top, bottom, bottom bride, sparse, void) could be placed.
|
|
Polygons fill_boundaries = to_polygons(this->fill_expolygons);
|
|
Polygons lower_layer_covered_tmp;
|
|
|
|
// Collect top surfaces and internal surfaces.
|
|
// Collect fill_boundaries: If we're slicing with no infill, we can't extend external surfaces over non-existent infill.
|
|
// This loop destroys the surfaces (aliasing this->fill_surfaces.surfaces) by moving into top/internal/fill_boundaries!
|
|
|
|
{
|
|
// Voids are sparse infills if infill rate is zero.
|
|
Polygons voids;
|
|
for (const Surface &surface : this->fill_surfaces.surfaces) {
|
|
if (surface.is_top()) {
|
|
// Collect the top surfaces, inflate them and trim them by the bottom surfaces.
|
|
// This gives the priority to bottom surfaces.
|
|
surfaces_append(top, offset_ex(surface.expolygon, margin, EXTERNAL_SURFACES_OFFSET_PARAMETERS), surface);
|
|
} else if (surface.surface_type == stBottom || (surface.surface_type == stBottomBridge && lower_layer == nullptr)) {
|
|
// Grown by 3mm.
|
|
surfaces_append(bottom, offset_ex(surface.expolygon, margin, EXTERNAL_SURFACES_OFFSET_PARAMETERS), surface);
|
|
} else if (surface.surface_type == stBottomBridge) {
|
|
if (! surface.empty())
|
|
bridges.emplace_back(surface);
|
|
}
|
|
if (surface.is_internal()) {
|
|
assert(surface.surface_type == stInternal || surface.surface_type == stInternalSolid);
|
|
if (! has_infill && lower_layer != nullptr)
|
|
polygons_append(voids, surface.expolygon);
|
|
internal.emplace_back(std::move(surface));
|
|
}
|
|
}
|
|
if (! has_infill && lower_layer != nullptr && ! voids.empty()) {
|
|
// Remove voids from fill_boundaries, that are not supported by the layer below.
|
|
if (lower_layer_covered == nullptr) {
|
|
lower_layer_covered = &lower_layer_covered_tmp;
|
|
lower_layer_covered_tmp = to_polygons(lower_layer->lslices);
|
|
}
|
|
if (! lower_layer_covered->empty())
|
|
voids = diff(voids, *lower_layer_covered);
|
|
fill_boundaries = diff(fill_boundaries, voids);
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
{
|
|
static int iRun = 0;
|
|
bridges.export_to_svg(debug_out_path("bridges-before-grouping-%d.svg", iRun ++), true);
|
|
}
|
|
#endif
|
|
|
|
if (bridges.empty())
|
|
{
|
|
fill_boundaries = union_(fill_boundaries, true);
|
|
} else
|
|
{
|
|
// 1) Calculate the inflated bridge regions, each constrained to its island.
|
|
ExPolygons fill_boundaries_ex = union_ex(fill_boundaries, true);
|
|
std::vector<Polygons> bridges_grown;
|
|
std::vector<BoundingBox> bridge_bboxes;
|
|
|
|
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
|
{
|
|
static int iRun = 0;
|
|
SVG svg(debug_out_path("3_process_external_surfaces-fill_regions-%d.svg", iRun ++).c_str(), get_extents(fill_boundaries_ex));
|
|
svg.draw(fill_boundaries_ex);
|
|
svg.draw_outline(fill_boundaries_ex, "black", "blue", scale_(0.05));
|
|
svg.Close();
|
|
}
|
|
|
|
// export_region_fill_surfaces_to_svg_debug("3_process_external_surfaces-initial");
|
|
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
|
|
|
{
|
|
// Bridge expolygons, grown, to be tested for intersection with other bridge regions.
|
|
std::vector<BoundingBox> fill_boundaries_ex_bboxes = get_extents_vector(fill_boundaries_ex);
|
|
bridges_grown.reserve(bridges.size());
|
|
bridge_bboxes.reserve(bridges.size());
|
|
for (size_t i = 0; i < bridges.size(); ++ i) {
|
|
// Find the island of this bridge.
|
|
const Point pt = bridges[i].expolygon.contour.points.front();
|
|
int idx_island = -1;
|
|
for (int j = 0; j < int(fill_boundaries_ex.size()); ++ j)
|
|
if (fill_boundaries_ex_bboxes[j].contains(pt) &&
|
|
fill_boundaries_ex[j].contains(pt)) {
|
|
idx_island = j;
|
|
break;
|
|
}
|
|
// Grown by 3mm.
|
|
Polygons polys = offset(to_polygons(bridges[i].expolygon), margin, EXTERNAL_SURFACES_OFFSET_PARAMETERS);
|
|
if (idx_island == -1) {
|
|
BOOST_LOG_TRIVIAL(trace) << "Bridge did not fall into the source region!";
|
|
} else {
|
|
// Found an island, to which this bridge region belongs. Trim it,
|
|
polys = intersection(polys, to_polygons(fill_boundaries_ex[idx_island]));
|
|
}
|
|
bridge_bboxes.push_back(get_extents(polys));
|
|
bridges_grown.push_back(std::move(polys));
|
|
}
|
|
}
|
|
|
|
// 2) Group the bridge surfaces by overlaps.
|
|
std::vector<size_t> bridge_group(bridges.size(), (size_t)-1);
|
|
size_t n_groups = 0;
|
|
for (size_t i = 0; i < bridges.size(); ++ i) {
|
|
// A grup id for this bridge.
|
|
size_t group_id = (bridge_group[i] == size_t(-1)) ? (n_groups ++) : bridge_group[i];
|
|
bridge_group[i] = group_id;
|
|
// For all possibly overlaping bridges:
|
|
for (size_t j = i + 1; j < bridges.size(); ++ j) {
|
|
if (! bridge_bboxes[i].overlap(bridge_bboxes[j]))
|
|
continue;
|
|
if (intersection(bridges_grown[i], bridges_grown[j], false).empty())
|
|
continue;
|
|
// The two bridge regions intersect. Give them the same group id.
|
|
if (bridge_group[j] != size_t(-1)) {
|
|
// The j'th bridge has been merged with some other bridge before.
|
|
size_t group_id_new = bridge_group[j];
|
|
for (size_t k = 0; k < j; ++ k)
|
|
if (bridge_group[k] == group_id)
|
|
bridge_group[k] = group_id_new;
|
|
group_id = group_id_new;
|
|
}
|
|
bridge_group[j] = group_id;
|
|
}
|
|
}
|
|
|
|
// 3) Merge the groups with the same group id, detect bridges.
|
|
{
|
|
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges. layer" << this->layer()->print_z << ", bridge groups: " << n_groups;
|
|
for (size_t group_id = 0; group_id < n_groups; ++ group_id) {
|
|
size_t n_bridges_merged = 0;
|
|
size_t idx_last = (size_t)-1;
|
|
for (size_t i = 0; i < bridges.size(); ++ i) {
|
|
if (bridge_group[i] == group_id) {
|
|
++ n_bridges_merged;
|
|
idx_last = i;
|
|
}
|
|
}
|
|
if (n_bridges_merged == 0)
|
|
// This group has no regions assigned as these were moved into another group.
|
|
continue;
|
|
// Collect the initial ungrown regions and the grown polygons.
|
|
ExPolygons initial;
|
|
Polygons grown;
|
|
for (size_t i = 0; i < bridges.size(); ++ i) {
|
|
if (bridge_group[i] != group_id)
|
|
continue;
|
|
initial.push_back(std::move(bridges[i].expolygon));
|
|
polygons_append(grown, bridges_grown[i]);
|
|
}
|
|
// detect bridge direction before merging grown surfaces otherwise adjacent bridges
|
|
// would get merged into a single one while they need different directions
|
|
// also, supply the original expolygon instead of the grown one, because in case
|
|
// of very thin (but still working) anchors, the grown expolygon would go beyond them
|
|
BridgeDetector bd(
|
|
initial,
|
|
lower_layer->lslices,
|
|
this->flow(frInfill, true).scaled_width()
|
|
);
|
|
#ifdef SLIC3R_DEBUG
|
|
printf("Processing bridge at layer %zu:\n", this->layer()->id());
|
|
#endif
|
|
double custom_angle = Geometry::deg2rad(this->region()->config().bridge_angle.value);
|
|
if (bd.detect_angle(custom_angle)) {
|
|
bridges[idx_last].bridge_angle = bd.angle;
|
|
if (this->layer()->object()->config().support_material) {
|
|
polygons_append(this->bridged, bd.coverage());
|
|
append(this->unsupported_bridge_edges, bd.unsupported_edges());
|
|
}
|
|
} else if (custom_angle > 0) {
|
|
// Bridge was not detected (likely it is only supported at one side). Still it is a surface filled in
|
|
// using a bridging flow, therefore it makes sense to respect the custom bridging direction.
|
|
bridges[idx_last].bridge_angle = custom_angle;
|
|
}
|
|
// without safety offset, artifacts are generated (GH #2494)
|
|
surfaces_append(bottom, union_ex(grown, true), bridges[idx_last]);
|
|
}
|
|
|
|
fill_boundaries = std::move(to_polygons(fill_boundaries_ex));
|
|
BOOST_LOG_TRIVIAL(trace) << "Processing external surface, detecting bridges - done";
|
|
}
|
|
|
|
#if 0
|
|
{
|
|
static int iRun = 0;
|
|
bridges.export_to_svg(debug_out_path("bridges-after-grouping-%d.svg", iRun ++), true);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
Surfaces new_surfaces;
|
|
{
|
|
// Merge top and bottom in a single collection.
|
|
surfaces_append(top, std::move(bottom));
|
|
// Intersect the grown surfaces with the actual fill boundaries.
|
|
Polygons bottom_polygons = to_polygons(bottom);
|
|
for (size_t i = 0; i < top.size(); ++ i) {
|
|
Surface &s1 = top[i];
|
|
if (s1.empty())
|
|
continue;
|
|
Polygons polys;
|
|
polygons_append(polys, std::move(s1));
|
|
for (size_t j = i + 1; j < top.size(); ++ j) {
|
|
Surface &s2 = top[j];
|
|
if (! s2.empty() && surfaces_could_merge(s1, s2)) {
|
|
polygons_append(polys, std::move(s2));
|
|
s2.clear();
|
|
}
|
|
}
|
|
if (s1.is_top())
|
|
// Trim the top surfaces by the bottom surfaces. This gives the priority to the bottom surfaces.
|
|
polys = diff(polys, bottom_polygons);
|
|
surfaces_append(
|
|
new_surfaces,
|
|
// Don't use a safety offset as fill_boundaries were already united using the safety offset.
|
|
std::move(intersection_ex(polys, fill_boundaries, false)),
|
|
s1);
|
|
}
|
|
}
|
|
|
|
// Subtract the new top surfaces from the other non-top surfaces and re-add them.
|
|
Polygons new_polygons = to_polygons(new_surfaces);
|
|
for (size_t i = 0; i < internal.size(); ++ i) {
|
|
Surface &s1 = internal[i];
|
|
if (s1.empty())
|
|
continue;
|
|
Polygons polys;
|
|
polygons_append(polys, std::move(s1));
|
|
for (size_t j = i + 1; j < internal.size(); ++ j) {
|
|
Surface &s2 = internal[j];
|
|
if (! s2.empty() && surfaces_could_merge(s1, s2)) {
|
|
polygons_append(polys, std::move(s2));
|
|
s2.clear();
|
|
}
|
|
}
|
|
ExPolygons new_expolys = diff_ex(polys, new_polygons);
|
|
polygons_append(new_polygons, to_polygons(new_expolys));
|
|
surfaces_append(new_surfaces, std::move(new_expolys), s1);
|
|
}
|
|
|
|
this->fill_surfaces.surfaces = std::move(new_surfaces);
|
|
|
|
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
|
export_region_fill_surfaces_to_svg_debug("3_process_external_surfaces-final");
|
|
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
|
}
|
|
|
|
void LayerRegion::prepare_fill_surfaces()
|
|
{
|
|
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
|
export_region_slices_to_svg_debug("2_prepare_fill_surfaces-initial");
|
|
export_region_fill_surfaces_to_svg_debug("2_prepare_fill_surfaces-initial");
|
|
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
|
|
|
/* Note: in order to make the psPrepareInfill step idempotent, we should never
|
|
alter fill_surfaces boundaries on which our idempotency relies since that's
|
|
the only meaningful information returned by psPerimeters. */
|
|
|
|
bool spiral_vase = this->layer()->object()->print()->config().spiral_vase;
|
|
|
|
// if no solid layers are requested, turn top/bottom surfaces to internal
|
|
if (! spiral_vase && this->region()->config().top_solid_layers == 0) {
|
|
for (Surface &surface : this->fill_surfaces.surfaces)
|
|
if (surface.is_top())
|
|
surface.surface_type = this->layer()->object()->config().infill_only_where_needed ? stInternalVoid : stInternal;
|
|
}
|
|
if (this->region()->config().bottom_solid_layers == 0) {
|
|
for (Surface &surface : this->fill_surfaces.surfaces)
|
|
if (surface.is_bottom()) // (surface.surface_type == stBottom)
|
|
surface.surface_type = stInternal;
|
|
}
|
|
|
|
// turn too small internal regions into solid regions according to the user setting
|
|
if (! spiral_vase && this->region()->config().fill_density.value > 0) {
|
|
// scaling an area requires two calls!
|
|
double min_area = scale_(scale_(this->region()->config().solid_infill_below_area.value));
|
|
for (Surface &surface : this->fill_surfaces.surfaces)
|
|
if (surface.surface_type == stInternal && surface.area() <= min_area)
|
|
surface.surface_type = stInternalSolid;
|
|
}
|
|
|
|
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
|
export_region_slices_to_svg_debug("2_prepare_fill_surfaces-final");
|
|
export_region_fill_surfaces_to_svg_debug("2_prepare_fill_surfaces-final");
|
|
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
|
}
|
|
|
|
double LayerRegion::infill_area_threshold() const
|
|
{
|
|
double ss = this->flow(frSolidInfill).scaled_spacing();
|
|
return ss*ss;
|
|
}
|
|
|
|
void LayerRegion::trim_surfaces(const Polygons &trimming_polygons)
|
|
{
|
|
#ifndef NDEBUG
|
|
for (const Surface &surface : this->slices.surfaces)
|
|
assert(surface.surface_type == stInternal);
|
|
#endif /* NDEBUG */
|
|
this->slices.set(intersection_ex(to_polygons(std::move(this->slices.surfaces)), trimming_polygons, false), stInternal);
|
|
}
|
|
|
|
void LayerRegion::elephant_foot_compensation_step(const float elephant_foot_compensation_perimeter_step, const Polygons &trimming_polygons)
|
|
{
|
|
#ifndef NDEBUG
|
|
for (const Surface &surface : this->slices.surfaces)
|
|
assert(surface.surface_type == stInternal);
|
|
#endif /* NDEBUG */
|
|
ExPolygons slices_expolygons = to_expolygons(std::move(this->slices.surfaces));
|
|
Polygons slices_polygons = to_polygons(slices_expolygons);
|
|
Polygons tmp = intersection(slices_polygons, trimming_polygons, false);
|
|
append(tmp, diff(slices_polygons, offset(offset_ex(slices_expolygons, -elephant_foot_compensation_perimeter_step), elephant_foot_compensation_perimeter_step)));
|
|
this->slices.set(std::move(union_ex(tmp)), stInternal);
|
|
}
|
|
|
|
void LayerRegion::export_region_slices_to_svg(const char *path) const
|
|
{
|
|
BoundingBox bbox;
|
|
for (Surfaces::const_iterator surface = this->slices.surfaces.begin(); surface != this->slices.surfaces.end(); ++surface)
|
|
bbox.merge(get_extents(surface->expolygon));
|
|
Point legend_size = export_surface_type_legend_to_svg_box_size();
|
|
Point legend_pos(bbox.min(0), bbox.max(1));
|
|
bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1)));
|
|
|
|
SVG svg(path, bbox);
|
|
const float transparency = 0.5f;
|
|
for (Surfaces::const_iterator surface = this->slices.surfaces.begin(); surface != this->slices.surfaces.end(); ++surface)
|
|
svg.draw(surface->expolygon, surface_type_to_color_name(surface->surface_type), transparency);
|
|
for (Surfaces::const_iterator surface = this->fill_surfaces.surfaces.begin(); surface != this->fill_surfaces.surfaces.end(); ++surface)
|
|
svg.draw(surface->expolygon.lines(), surface_type_to_color_name(surface->surface_type));
|
|
export_surface_type_legend_to_svg(svg, legend_pos);
|
|
svg.Close();
|
|
}
|
|
|
|
// Export to "out/LayerRegion-name-%d.svg" with an increasing index with every export.
|
|
void LayerRegion::export_region_slices_to_svg_debug(const char *name) const
|
|
{
|
|
static std::map<std::string, size_t> idx_map;
|
|
size_t &idx = idx_map[name];
|
|
this->export_region_slices_to_svg(debug_out_path("LayerRegion-slices-%s-%d.svg", name, idx ++).c_str());
|
|
}
|
|
|
|
void LayerRegion::export_region_fill_surfaces_to_svg(const char *path) const
|
|
{
|
|
BoundingBox bbox;
|
|
for (Surfaces::const_iterator surface = this->fill_surfaces.surfaces.begin(); surface != this->fill_surfaces.surfaces.end(); ++surface)
|
|
bbox.merge(get_extents(surface->expolygon));
|
|
Point legend_size = export_surface_type_legend_to_svg_box_size();
|
|
Point legend_pos(bbox.min(0), bbox.max(1));
|
|
bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1)));
|
|
|
|
SVG svg(path, bbox);
|
|
const float transparency = 0.5f;
|
|
for (const Surface &surface : this->fill_surfaces.surfaces) {
|
|
svg.draw(surface.expolygon, surface_type_to_color_name(surface.surface_type), transparency);
|
|
svg.draw_outline(surface.expolygon, "black", "blue", scale_(0.05));
|
|
}
|
|
export_surface_type_legend_to_svg(svg, legend_pos);
|
|
svg.Close();
|
|
}
|
|
|
|
// Export to "out/LayerRegion-name-%d.svg" with an increasing index with every export.
|
|
void LayerRegion::export_region_fill_surfaces_to_svg_debug(const char *name) const
|
|
{
|
|
static std::map<std::string, size_t> idx_map;
|
|
size_t &idx = idx_map[name];
|
|
this->export_region_fill_surfaces_to_svg(debug_out_path("LayerRegion-fill_surfaces-%s-%d.svg", name, idx ++).c_str());
|
|
}
|
|
|
|
}
|
|
|