Fixed conflicts after merge with master
This commit is contained in:
commit
e5c45405d4
@ -34,6 +34,7 @@
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
@ -41,6 +42,7 @@
|
||||
#include "libslic3r/Format/3mf.hpp"
|
||||
#include "libslic3r/Format/STL.hpp"
|
||||
#include "libslic3r/Format/OBJ.hpp"
|
||||
#include "libslic3r/Format/SL1.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "PrusaSlicer.hpp"
|
||||
@ -53,12 +55,6 @@
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
PrinterTechnology get_printer_technology(const DynamicConfig &config)
|
||||
{
|
||||
const ConfigOptionEnum<PrinterTechnology> *opt = config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology");
|
||||
return (opt == nullptr) ? ptUnknown : opt->value;
|
||||
}
|
||||
|
||||
int CLI::run(int argc, char **argv)
|
||||
{
|
||||
// Switch boost::filesystem to utf8.
|
||||
@ -87,12 +83,14 @@ int CLI::run(int argc, char **argv)
|
||||
m_extra_config.apply(m_config, true);
|
||||
m_extra_config.normalize();
|
||||
|
||||
PrinterTechnology printer_technology = Slic3r::printer_technology(m_config);
|
||||
|
||||
bool start_gui = m_actions.empty() &&
|
||||
// cutting transformations are setting an "export" action.
|
||||
std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() &&
|
||||
std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() &&
|
||||
std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end();
|
||||
PrinterTechnology printer_technology = get_printer_technology(m_extra_config);
|
||||
|
||||
const std::vector<std::string> &load_configs = m_config.option<ConfigOptionStrings>("load", true)->values;
|
||||
|
||||
// load config files supplied via --load
|
||||
@ -113,7 +111,7 @@ int CLI::run(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
config.normalize();
|
||||
PrinterTechnology other_printer_technology = get_printer_technology(config);
|
||||
PrinterTechnology other_printer_technology = Slic3r::printer_technology(config);
|
||||
if (printer_technology == ptUnknown) {
|
||||
printer_technology = other_printer_technology;
|
||||
} else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) {
|
||||
@ -134,7 +132,7 @@ int CLI::run(int argc, char **argv)
|
||||
// When loading an AMF or 3MF, config is imported as well, including the printer technology.
|
||||
DynamicPrintConfig config;
|
||||
model = Model::read_from_file(file, &config, true);
|
||||
PrinterTechnology other_printer_technology = get_printer_technology(config);
|
||||
PrinterTechnology other_printer_technology = Slic3r::printer_technology(config);
|
||||
if (printer_technology == ptUnknown) {
|
||||
printer_technology = other_printer_technology;
|
||||
} else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) {
|
||||
@ -161,9 +159,6 @@ int CLI::run(int argc, char **argv)
|
||||
// Normalizing after importing the 3MFs / AMFs
|
||||
m_print_config.normalize();
|
||||
|
||||
if (printer_technology == ptUnknown)
|
||||
printer_technology = std::find(m_actions.begin(), m_actions.end(), "export_sla") == m_actions.end() ? ptFFF : ptSLA;
|
||||
|
||||
// Initialize full print configs for both the FFF and SLA technologies.
|
||||
FullPrintConfig fff_print_config;
|
||||
SLAFullPrintConfig sla_print_config;
|
||||
@ -174,6 +169,7 @@ int CLI::run(int argc, char **argv)
|
||||
m_print_config.apply(fff_print_config, true);
|
||||
} else if (printer_technology == ptSLA) {
|
||||
// The default value has to be different from the one in fff mode.
|
||||
sla_print_config.printer_technology.value = ptSLA;
|
||||
sla_print_config.output_filename_format.value = "[input_filename_base].sl1";
|
||||
|
||||
// The default bed shape should reflect the default display parameters
|
||||
@ -186,8 +182,18 @@ int CLI::run(int argc, char **argv)
|
||||
m_print_config.apply(sla_print_config, true);
|
||||
}
|
||||
|
||||
std::string validity = m_print_config.validate();
|
||||
if (!validity.empty()) {
|
||||
boost::nowide::cerr << "error: " << validity << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Loop through transform options.
|
||||
bool user_center_specified = false;
|
||||
Points bed = get_bed_shape(m_print_config);
|
||||
ArrangeParams arrange_cfg;
|
||||
arrange_cfg.min_obj_distance = scaled(min_object_distance(m_print_config));
|
||||
|
||||
for (auto const &opt_key : m_transforms) {
|
||||
if (opt_key == "merge") {
|
||||
Model m;
|
||||
@ -197,29 +203,33 @@ int CLI::run(int argc, char **argv)
|
||||
// Rearrange instances unless --dont-arrange is supplied
|
||||
if (! m_config.opt_bool("dont_arrange")) {
|
||||
m.add_default_instances();
|
||||
const BoundingBoxf &bb = fff_print_config.bed_shape.values;
|
||||
m.arrange_objects(
|
||||
fff_print_config.min_object_distance(),
|
||||
// If we are going to use the merged model for printing, honor
|
||||
// the configured print bed for arranging, otherwise do it freely.
|
||||
this->has_print_action() ? &bb : nullptr
|
||||
);
|
||||
if (this->has_print_action())
|
||||
arrange_objects(m, bed, arrange_cfg);
|
||||
else
|
||||
arrange_objects(m, InfiniteBed{}, arrange_cfg);
|
||||
}
|
||||
m_models.clear();
|
||||
m_models.emplace_back(std::move(m));
|
||||
} else if (opt_key == "duplicate") {
|
||||
const BoundingBoxf &bb = fff_print_config.bed_shape.values;
|
||||
for (auto &model : m_models) {
|
||||
const bool all_objects_have_instances = std::none_of(
|
||||
model.objects.begin(), model.objects.end(),
|
||||
[](ModelObject* o){ return o->instances.empty(); }
|
||||
);
|
||||
if (all_objects_have_instances) {
|
||||
|
||||
int dups = m_config.opt_int("duplicate");
|
||||
if (!all_objects_have_instances) model.add_default_instances();
|
||||
|
||||
try {
|
||||
if (dups > 1) {
|
||||
// if all input objects have defined position(s) apply duplication to the whole model
|
||||
model.duplicate(m_config.opt_int("duplicate"), fff_print_config.min_object_distance(), &bb);
|
||||
duplicate(model, size_t(dups), bed, arrange_cfg);
|
||||
} else {
|
||||
model.add_default_instances();
|
||||
model.duplicate_objects(m_config.opt_int("duplicate"), fff_print_config.min_object_distance(), &bb);
|
||||
arrange_objects(model, bed, arrange_cfg);
|
||||
}
|
||||
} catch (std::exception &ex) {
|
||||
boost::nowide::cerr << "error: " << ex.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "duplicate_grid") {
|
||||
@ -413,7 +423,8 @@ int CLI::run(int argc, char **argv)
|
||||
std::string outfile = m_config.opt_string("output");
|
||||
Print fff_print;
|
||||
SLAPrint sla_print;
|
||||
|
||||
SL1Archive sla_archive(sla_print.printer_config());
|
||||
sla_print.set_printer(&sla_archive);
|
||||
sla_print.set_status_callback(
|
||||
[](const PrintBase::SlicingStatus& s)
|
||||
{
|
||||
@ -423,11 +434,11 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
PrintBase *print = (printer_technology == ptFFF) ? static_cast<PrintBase*>(&fff_print) : static_cast<PrintBase*>(&sla_print);
|
||||
if (! m_config.opt_bool("dont_arrange")) {
|
||||
//FIXME make the min_object_distance configurable.
|
||||
model.arrange_objects(fff_print.config().min_object_distance());
|
||||
model.center_instances_around_point((! user_center_specified && m_print_config.has("bed_shape")) ?
|
||||
BoundingBoxf(m_print_config.opt<ConfigOptionPoints>("bed_shape")->values).center() :
|
||||
m_config.option<ConfigOptionPoint>("center")->value);
|
||||
if (user_center_specified) {
|
||||
Vec2d c = m_config.option<ConfigOptionPoint>("center")->value;
|
||||
arrange_objects(model, InfiniteBed{scaled(c)}, arrange_cfg);
|
||||
} else
|
||||
arrange_objects(model, bed, arrange_cfg);
|
||||
}
|
||||
if (printer_technology == ptFFF) {
|
||||
for (auto* mo : model.objects)
|
||||
@ -457,7 +468,7 @@ int CLI::run(int argc, char **argv)
|
||||
outfile = sla_print.output_filepath(outfile);
|
||||
// We need to finalize the filename beforehand because the export function sets the filename inside the zip metadata
|
||||
outfile_final = sla_print.print_statistics().finalize_output_path(outfile);
|
||||
sla_print.export_raster(outfile_final);
|
||||
sla_archive.export_print(outfile_final, sla_print);
|
||||
}
|
||||
if (outfile != outfile_final && Slic3r::rename_file(outfile, outfile_final)) {
|
||||
boost::nowide::cerr << "Renaming file " << outfile << " to " << outfile_final << " failed" << std::endl;
|
||||
@ -614,6 +625,8 @@ bool CLI::setup(int argc, char **argv)
|
||||
set_logging_level(opt_loglevel->value);
|
||||
}
|
||||
|
||||
std::string validity = m_config.validate();
|
||||
|
||||
// Initialize with defaults.
|
||||
for (const t_optiondef_map *options : { &cli_actions_config_def.options, &cli_transform_config_def.options, &cli_misc_config_def.options })
|
||||
for (const std::pair<t_config_option_key, ConfigOptionDef> &optdef : *options)
|
||||
@ -621,6 +634,11 @@ bool CLI::setup(int argc, char **argv)
|
||||
|
||||
set_data_dir(m_config.opt_string("datadir"));
|
||||
|
||||
if (!validity.empty()) {
|
||||
boost::nowide::cerr << "error: " << validity << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -982,6 +982,9 @@ template<class S> inline double area(const S& poly, const PolygonTag& )
|
||||
});
|
||||
}
|
||||
|
||||
template<class RawShapes>
|
||||
inline double area(const RawShapes& shapes, const MultiPolygonTag&);
|
||||
|
||||
template<class S> // Dispatching function
|
||||
inline double area(const S& sh)
|
||||
{
|
||||
|
@ -27,6 +27,7 @@ using Coord = TCoord<PointImpl>;
|
||||
using Box = _Box<PointImpl>;
|
||||
using Segment = _Segment<PointImpl>;
|
||||
using Circle = _Circle<PointImpl>;
|
||||
using MultiPolygon = TMultiShape<PolygonImpl>;
|
||||
|
||||
using Item = _Item<PolygonImpl>;
|
||||
using Rectangle = _Rectangle<PolygonImpl>;
|
||||
|
@ -5,7 +5,7 @@
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <libnest2d/libnest2d.hpp>
|
||||
#include <libnest2d/nester.hpp>
|
||||
|
||||
namespace libnest2d { namespace svg {
|
||||
|
||||
@ -49,9 +49,8 @@ public:
|
||||
conf_.mm_in_coord_units;
|
||||
}
|
||||
|
||||
void writeItem(const Item& item) {
|
||||
void writeShape(RawShape tsh) {
|
||||
if(svg_layers_.empty()) addLayer();
|
||||
auto tsh = item.transformedShape();
|
||||
if(conf_.origo_location == BOTTOMLEFT) {
|
||||
auto d = static_cast<Coord>(
|
||||
std::round(conf_.height*conf_.mm_in_coord_units) );
|
||||
@ -63,8 +62,14 @@ public:
|
||||
for(auto& h : holes) for(auto& v : h) setY(v, -getY(v) + d);
|
||||
|
||||
}
|
||||
currentLayer() += shapelike::serialize<Formats::SVG>(tsh,
|
||||
1.0/conf_.mm_in_coord_units) + "\n";
|
||||
currentLayer() +=
|
||||
shapelike::serialize<Formats::SVG>(tsh,
|
||||
1.0 / conf_.mm_in_coord_units) +
|
||||
"\n";
|
||||
}
|
||||
|
||||
void writeItem(const Item& item) {
|
||||
writeShape(item.transformedShape());
|
||||
}
|
||||
|
||||
void writePackGroup(const PackGroup& result) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
#include "Arrange.hpp"
|
||||
#include "Geometry.hpp"
|
||||
//#include "Geometry.hpp"
|
||||
#include "SVG.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
|
||||
#include <libnest2d/backends/clipper/geometries.hpp>
|
||||
#include <libnest2d/optimizers/nlopt/subplex.hpp>
|
||||
@ -83,7 +82,7 @@ const double BIG_ITEM_TRESHOLD = 0.02;
|
||||
// Fill in the placer algorithm configuration with values carefully chosen for
|
||||
// Slic3r.
|
||||
template<class PConf>
|
||||
void fillConfig(PConf& pcfg) {
|
||||
void fill_config(PConf& pcfg) {
|
||||
|
||||
// Align the arranged pile into the center of the bin
|
||||
pcfg.alignment = PConf::Alignment::CENTER;
|
||||
@ -105,7 +104,7 @@ void fillConfig(PConf& pcfg) {
|
||||
|
||||
// Apply penalty to object function result. This is used only when alignment
|
||||
// after arrange is explicitly disabled (PConfig::Alignment::DONT_ALIGN)
|
||||
double fixed_overfit(const std::tuple<double, Box>& result, const Box &binbb)
|
||||
static double fixed_overfit(const std::tuple<double, Box>& result, const Box &binbb)
|
||||
{
|
||||
double score = std::get<0>(result);
|
||||
Box pilebb = std::get<1>(result);
|
||||
@ -312,7 +311,7 @@ public:
|
||||
, m_bin_area(sl::area(bin))
|
||||
, m_norm(std::sqrt(m_bin_area))
|
||||
{
|
||||
fillConfig(m_pconf);
|
||||
fill_config(m_pconf);
|
||||
|
||||
// Set up a callback that is called just before arranging starts
|
||||
// This functionality is provided by the Nester class (m_pack).
|
||||
@ -363,6 +362,9 @@ public:
|
||||
m_item_count = 0;
|
||||
}
|
||||
|
||||
PConfig& config() { return m_pconf; }
|
||||
const PConfig& config() const { return m_pconf; }
|
||||
|
||||
inline void preload(std::vector<Item>& fixeditems) {
|
||||
m_pconf.alignment = PConfig::Alignment::DONT_ALIGN;
|
||||
auto bb = sl::boundingBox(m_bin);
|
||||
@ -438,127 +440,6 @@ std::function<double(const Item &)> AutoArranger<clppr::Polygon>::get_objfn()
|
||||
};
|
||||
}
|
||||
|
||||
inline Circle to_lnCircle(const CircleBed& circ) {
|
||||
return Circle({circ.center()(0), circ.center()(1)}, circ.radius());
|
||||
}
|
||||
|
||||
// Get the type of bed geometry from a simple vector of points.
|
||||
void BedShapeHint::reset(BedShapes type)
|
||||
{
|
||||
if (m_type != type) {
|
||||
if (m_type == bsIrregular)
|
||||
m_bed.polygon.Slic3r::Polyline::~Polyline();
|
||||
else if (type == bsIrregular)
|
||||
::new (&m_bed.polygon) Polyline();
|
||||
}
|
||||
|
||||
m_type = type;
|
||||
}
|
||||
|
||||
BedShapeHint::BedShapeHint(const Polyline &bed) {
|
||||
auto x = [](const Point& p) { return p(X); };
|
||||
auto y = [](const Point& p) { return p(Y); };
|
||||
|
||||
auto width = [x](const BoundingBox& box) {
|
||||
return x(box.max) - x(box.min);
|
||||
};
|
||||
|
||||
auto height = [y](const BoundingBox& box) {
|
||||
return y(box.max) - y(box.min);
|
||||
};
|
||||
|
||||
auto area = [&width, &height](const BoundingBox& box) {
|
||||
double w = width(box);
|
||||
double h = height(box);
|
||||
return w * h;
|
||||
};
|
||||
|
||||
auto poly_area = [](Polyline p) {
|
||||
Polygon pp; pp.points.reserve(p.points.size() + 1);
|
||||
pp.points = std::move(p.points);
|
||||
pp.points.emplace_back(pp.points.front());
|
||||
return std::abs(pp.area());
|
||||
};
|
||||
|
||||
auto distance_to = [x, y](const Point& p1, const Point& p2) {
|
||||
double dx = x(p2) - x(p1);
|
||||
double dy = y(p2) - y(p1);
|
||||
return std::sqrt(dx*dx + dy*dy);
|
||||
};
|
||||
|
||||
auto bb = bed.bounding_box();
|
||||
|
||||
auto isCircle = [bb, distance_to](const Polyline& polygon) {
|
||||
auto center = bb.center();
|
||||
std::vector<double> vertex_distances;
|
||||
double avg_dist = 0;
|
||||
for (auto pt: polygon.points)
|
||||
{
|
||||
double distance = distance_to(center, pt);
|
||||
vertex_distances.push_back(distance);
|
||||
avg_dist += distance;
|
||||
}
|
||||
|
||||
avg_dist /= vertex_distances.size();
|
||||
|
||||
CircleBed ret(center, avg_dist);
|
||||
for(auto el : vertex_distances)
|
||||
{
|
||||
if (std::abs(el - avg_dist) > 10 * SCALED_EPSILON) {
|
||||
ret = CircleBed();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto parea = poly_area(bed);
|
||||
|
||||
if( (1.0 - parea/area(bb)) < 1e-3 ) {
|
||||
m_type = BedShapes::bsBox;
|
||||
m_bed.box = bb;
|
||||
}
|
||||
else if(auto c = isCircle(bed)) {
|
||||
m_type = BedShapes::bsCircle;
|
||||
m_bed.circ = c;
|
||||
} else {
|
||||
assert(m_type != BedShapes::bsIrregular);
|
||||
m_type = BedShapes::bsIrregular;
|
||||
::new (&m_bed.polygon) Polyline(bed);
|
||||
}
|
||||
}
|
||||
|
||||
BedShapeHint &BedShapeHint::operator=(BedShapeHint &&cpy)
|
||||
{
|
||||
reset(cpy.m_type);
|
||||
|
||||
switch(m_type) {
|
||||
case bsBox: m_bed.box = std::move(cpy.m_bed.box); break;
|
||||
case bsCircle: m_bed.circ = std::move(cpy.m_bed.circ); break;
|
||||
case bsIrregular: m_bed.polygon = std::move(cpy.m_bed.polygon); break;
|
||||
case bsInfinite: m_bed.infbed = std::move(cpy.m_bed.infbed); break;
|
||||
case bsUnknown: break;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
BedShapeHint &BedShapeHint::operator=(const BedShapeHint &cpy)
|
||||
{
|
||||
reset(cpy.m_type);
|
||||
|
||||
switch(m_type) {
|
||||
case bsBox: m_bed.box = cpy.m_bed.box; break;
|
||||
case bsCircle: m_bed.circ = cpy.m_bed.circ; break;
|
||||
case bsIrregular: m_bed.polygon = cpy.m_bed.polygon; break;
|
||||
case bsInfinite: m_bed.infbed = cpy.m_bed.infbed; break;
|
||||
case bsUnknown: break;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class Bin> void remove_large_items(std::vector<Item> &items, Bin &&bin)
|
||||
{
|
||||
auto it = items.begin();
|
||||
@ -572,12 +453,12 @@ void _arrange(
|
||||
std::vector<Item> & shapes,
|
||||
std::vector<Item> & excludes,
|
||||
const BinT & bin,
|
||||
coord_t minobjd,
|
||||
const ArrangeParams & params,
|
||||
std::function<void(unsigned)> progressfn,
|
||||
std::function<bool()> stopfn)
|
||||
{
|
||||
// Integer ceiling the min distance from the bed perimeters
|
||||
coord_t md = minobjd;
|
||||
coord_t md = params.min_obj_distance;
|
||||
md = (md % 2) ? md / 2 + 1 : md / 2;
|
||||
|
||||
auto corrected_bin = bin;
|
||||
@ -585,7 +466,10 @@ void _arrange(
|
||||
|
||||
AutoArranger<BinT> arranger{corrected_bin, progressfn, stopfn};
|
||||
|
||||
auto infl = coord_t(std::ceil(minobjd / 2.0));
|
||||
arranger.config().accuracy = params.accuracy;
|
||||
arranger.config().parallel = params.parallel;
|
||||
|
||||
auto infl = coord_t(std::ceil(params.min_obj_distance / 2.0));
|
||||
for (Item& itm : shapes) itm.inflate(infl);
|
||||
for (Item& itm : excludes) itm.inflate(infl);
|
||||
|
||||
@ -603,24 +487,51 @@ void _arrange(
|
||||
for (Item &itm : inp) itm.inflate(-infl);
|
||||
}
|
||||
|
||||
// The final client function for arrangement. A progress indicator and
|
||||
// a stop predicate can be also be passed to control the process.
|
||||
void arrange(ArrangePolygons & arrangables,
|
||||
const ArrangePolygons & excludes,
|
||||
coord_t min_obj_dist,
|
||||
const BedShapeHint & bedhint,
|
||||
std::function<void(unsigned)> progressind,
|
||||
std::function<bool()> stopcondition)
|
||||
inline Box to_nestbin(const BoundingBox &bb) { return Box{{bb.min(X), bb.min(Y)}, {bb.max(X), bb.max(Y)}};}
|
||||
inline Circle to_nestbin(const CircleBed &c) { return Circle({c.center()(0), c.center()(1)}, c.radius()); }
|
||||
inline clppr::Polygon to_nestbin(const Polygon &p) { return sl::create<clppr::Polygon>(Slic3rMultiPoint_to_ClipperPath(p)); }
|
||||
inline Box to_nestbin(const InfiniteBed &bed) { return Box::infinite({bed.center.x(), bed.center.y()}); }
|
||||
|
||||
inline coord_t width(const BoundingBox& box) { return box.max.x() - box.min.x(); }
|
||||
inline coord_t height(const BoundingBox& box) { return box.max.y() - box.min.y(); }
|
||||
inline double area(const BoundingBox& box) { return double(width(box)) * height(box); }
|
||||
inline double poly_area(const Points &pts) { return std::abs(Polygon::area(pts)); }
|
||||
inline double distance_to(const Point& p1, const Point& p2)
|
||||
{
|
||||
namespace clppr = ClipperLib;
|
||||
double dx = p2.x() - p1.x();
|
||||
double dy = p2.y() - p1.y();
|
||||
return std::sqrt(dx*dx + dy*dy);
|
||||
}
|
||||
|
||||
std::vector<Item> items, fixeditems;
|
||||
items.reserve(arrangables.size());
|
||||
static CircleBed to_circle(const Point ¢er, const Points& points) {
|
||||
std::vector<double> vertex_distances;
|
||||
double avg_dist = 0;
|
||||
|
||||
// Create Item from Arrangeable
|
||||
auto process_arrangeable = [](const ArrangePolygon &arrpoly,
|
||||
std::vector<Item> & outp)
|
||||
for (auto pt : points)
|
||||
{
|
||||
double distance = distance_to(center, pt);
|
||||
vertex_distances.push_back(distance);
|
||||
avg_dist += distance;
|
||||
}
|
||||
|
||||
avg_dist /= vertex_distances.size();
|
||||
|
||||
CircleBed ret(center, avg_dist);
|
||||
for(auto el : vertex_distances)
|
||||
{
|
||||
if (std::abs(el - avg_dist) > 10 * SCALED_EPSILON) {
|
||||
ret = {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Create Item from Arrangeable
|
||||
static void process_arrangeable(const ArrangePolygon &arrpoly,
|
||||
std::vector<Item> & outp)
|
||||
{
|
||||
Polygon p = arrpoly.poly.contour;
|
||||
const Vec2crd &offs = arrpoly.translation;
|
||||
double rotation = arrpoly.rotation;
|
||||
@ -639,7 +550,42 @@ void arrange(ArrangePolygons & arrangables,
|
||||
outp.back().translation({offs.x(), offs.y()});
|
||||
outp.back().binId(arrpoly.bed_idx);
|
||||
outp.back().priority(arrpoly.priority);
|
||||
};
|
||||
}
|
||||
|
||||
template<>
|
||||
void arrange(ArrangePolygons & items,
|
||||
const ArrangePolygons &excludes,
|
||||
const Points & bed,
|
||||
const ArrangeParams & params)
|
||||
{
|
||||
if (bed.empty())
|
||||
arrange(items, excludes, InfiniteBed{}, params);
|
||||
else if (bed.size() == 1)
|
||||
arrange(items, excludes, InfiniteBed{bed.front()}, params);
|
||||
else {
|
||||
auto bb = BoundingBox(bed);
|
||||
CircleBed circ = to_circle(bb.center(), bed);
|
||||
auto parea = poly_area(bed);
|
||||
|
||||
if ((1.0 - parea / area(bb)) < 1e-3)
|
||||
arrange(items, excludes, bb, params);
|
||||
else if (!std::isnan(circ.radius()))
|
||||
arrange(items, excludes, circ, params);
|
||||
else
|
||||
arrange(items, excludes, Polygon(bed), params);
|
||||
}
|
||||
}
|
||||
|
||||
template<class BedT>
|
||||
void arrange(ArrangePolygons & arrangables,
|
||||
const ArrangePolygons &excludes,
|
||||
const BedT & bed,
|
||||
const ArrangeParams & params)
|
||||
{
|
||||
namespace clppr = ClipperLib;
|
||||
|
||||
std::vector<Item> items, fixeditems;
|
||||
items.reserve(arrangables.size());
|
||||
|
||||
for (ArrangePolygon &arrangeable : arrangables)
|
||||
process_arrangeable(arrangeable, items);
|
||||
@ -649,45 +595,10 @@ void arrange(ArrangePolygons & arrangables,
|
||||
|
||||
for (Item &itm : fixeditems) itm.inflate(scaled(-2. * EPSILON));
|
||||
|
||||
auto &cfn = stopcondition;
|
||||
auto &pri = progressind;
|
||||
auto &cfn = params.stopcondition;
|
||||
auto &pri = params.progressind;
|
||||
|
||||
switch (bedhint.get_type()) {
|
||||
case bsBox: {
|
||||
// Create the arranger for the box shaped bed
|
||||
BoundingBox bbb = bedhint.get_box();
|
||||
Box binbb{{bbb.min(X), bbb.min(Y)}, {bbb.max(X), bbb.max(Y)}};
|
||||
|
||||
_arrange(items, fixeditems, binbb, min_obj_dist, pri, cfn);
|
||||
break;
|
||||
}
|
||||
case bsCircle: {
|
||||
auto cc = to_lnCircle(bedhint.get_circle());
|
||||
|
||||
_arrange(items, fixeditems, cc, min_obj_dist, pri, cfn);
|
||||
break;
|
||||
}
|
||||
case bsIrregular: {
|
||||
auto ctour = Slic3rMultiPoint_to_ClipperPath(bedhint.get_irregular());
|
||||
auto irrbed = sl::create<clppr::Polygon>(std::move(ctour));
|
||||
BoundingBox polybb(bedhint.get_irregular());
|
||||
|
||||
_arrange(items, fixeditems, irrbed, min_obj_dist, pri, cfn);
|
||||
break;
|
||||
}
|
||||
case bsInfinite: {
|
||||
const InfiniteBed& nobin = bedhint.get_infinite();
|
||||
auto infbb = Box::infinite({nobin.center.x(), nobin.center.y()});
|
||||
|
||||
_arrange(items, fixeditems, infbb, min_obj_dist, pri, cfn);
|
||||
break;
|
||||
}
|
||||
case bsUnknown: {
|
||||
// We know nothing about the bed, let it be infinite and zero centered
|
||||
_arrange(items, fixeditems, Box::infinite(), min_obj_dist, pri, cfn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_arrange(items, fixeditems, to_nestbin(bed), params, pri, cfn);
|
||||
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
clppr::IntPoint tr = items[i].translation();
|
||||
@ -697,15 +608,10 @@ void arrange(ArrangePolygons & arrangables,
|
||||
}
|
||||
}
|
||||
|
||||
// Arrange, without the fixed items (excludes)
|
||||
void arrange(ArrangePolygons & inp,
|
||||
coord_t min_d,
|
||||
const BedShapeHint & bedhint,
|
||||
std::function<void(unsigned)> prfn,
|
||||
std::function<bool()> stopfn)
|
||||
{
|
||||
arrange(inp, {}, min_d, bedhint, prfn, stopfn);
|
||||
}
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const BoundingBox &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams ¶ms);
|
||||
|
||||
} // namespace arr
|
||||
} // namespace Slic3r
|
||||
|
@ -1,12 +1,10 @@
|
||||
#ifndef MODELARRANGE_HPP
|
||||
#define MODELARRANGE_HPP
|
||||
#ifndef ARRANGE_HPP
|
||||
#define ARRANGE_HPP
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace arrangement {
|
||||
namespace Slic3r { namespace arrangement {
|
||||
|
||||
/// A geometry abstraction for a circular print bed. Similarly to BoundingBox.
|
||||
class CircleBed {
|
||||
@ -15,96 +13,16 @@ class CircleBed {
|
||||
public:
|
||||
|
||||
inline CircleBed(): center_(0, 0), radius_(std::nan("")) {}
|
||||
inline CircleBed(const Point& c, double r): center_(c), radius_(r) {}
|
||||
explicit inline CircleBed(const Point& c, double r): center_(c), radius_(r) {}
|
||||
|
||||
inline double radius() const { return radius_; }
|
||||
inline const Point& center() const { return center_; }
|
||||
inline operator bool() { return !std::isnan(radius_); }
|
||||
};
|
||||
|
||||
/// Representing an unbounded bed.
|
||||
struct InfiniteBed { Point center; };
|
||||
|
||||
/// Types of print bed shapes.
|
||||
enum BedShapes {
|
||||
bsBox,
|
||||
bsCircle,
|
||||
bsIrregular,
|
||||
bsInfinite,
|
||||
bsUnknown
|
||||
};
|
||||
|
||||
/// Info about the print bed for the arrange() function. This is a variant
|
||||
/// holding one of the four shapes a bed can be.
|
||||
class BedShapeHint {
|
||||
BedShapes m_type = BedShapes::bsInfinite;
|
||||
|
||||
// The union neither calls constructors nor destructors of its members.
|
||||
// The only member with non-trivial constructor / destructor is the polygon,
|
||||
// a placement new / delete needs to be called over it.
|
||||
union BedShape_u { // TODO: use variant from cpp17?
|
||||
CircleBed circ;
|
||||
BoundingBox box;
|
||||
Polyline polygon;
|
||||
InfiniteBed infbed{};
|
||||
~BedShape_u() {}
|
||||
BedShape_u() {}
|
||||
} m_bed;
|
||||
|
||||
// Reset the type, allocate m_bed properly
|
||||
void reset(BedShapes type);
|
||||
|
||||
public:
|
||||
|
||||
BedShapeHint(){}
|
||||
|
||||
/// Get a bed shape hint for arrange() from a naked Polyline.
|
||||
explicit BedShapeHint(const Polyline &polyl);
|
||||
explicit BedShapeHint(const BoundingBox &bb)
|
||||
{
|
||||
m_type = bsBox; m_bed.box = bb;
|
||||
}
|
||||
|
||||
explicit BedShapeHint(const CircleBed &c)
|
||||
{
|
||||
m_type = bsCircle; m_bed.circ = c;
|
||||
}
|
||||
|
||||
explicit BedShapeHint(const InfiniteBed &ibed)
|
||||
{
|
||||
m_type = bsInfinite; m_bed.infbed = ibed;
|
||||
}
|
||||
|
||||
~BedShapeHint()
|
||||
{
|
||||
if (m_type == BedShapes::bsIrregular)
|
||||
m_bed.polygon.Slic3r::Polyline::~Polyline();
|
||||
}
|
||||
|
||||
BedShapeHint(const BedShapeHint &cpy) { *this = cpy; }
|
||||
BedShapeHint(BedShapeHint &&cpy) { *this = std::move(cpy); }
|
||||
|
||||
BedShapeHint &operator=(const BedShapeHint &cpy);
|
||||
BedShapeHint& operator=(BedShapeHint &&cpy);
|
||||
|
||||
BedShapes get_type() const { return m_type; }
|
||||
|
||||
const BoundingBox &get_box() const
|
||||
{
|
||||
assert(m_type == bsBox); return m_bed.box;
|
||||
}
|
||||
const CircleBed &get_circle() const
|
||||
{
|
||||
assert(m_type == bsCircle); return m_bed.circ;
|
||||
}
|
||||
const Polyline &get_irregular() const
|
||||
{
|
||||
assert(m_type == bsIrregular); return m_bed.polygon;
|
||||
}
|
||||
const InfiniteBed &get_infinite() const
|
||||
{
|
||||
assert(m_type == bsInfinite); return m_bed.infbed;
|
||||
}
|
||||
struct InfiniteBed {
|
||||
Point center;
|
||||
explicit InfiniteBed(const Point &p = {0, 0}): center{p} {}
|
||||
};
|
||||
|
||||
/// A logical bed representing an object not being arranged. Either the arrange
|
||||
@ -125,9 +43,14 @@ struct ArrangePolygon {
|
||||
ExPolygon poly; /// The 2D silhouette to be arranged
|
||||
Vec2crd translation{0, 0}; /// The translation of the poly
|
||||
double rotation{0.0}; /// The rotation of the poly in radians
|
||||
coord_t inflation = 0; /// Arrange with inflated polygon
|
||||
int bed_idx{UNARRANGED}; /// To which logical bed does poly belong...
|
||||
int priority{0};
|
||||
|
||||
// If empty, any rotation is allowed (currently unsupported)
|
||||
// If only a zero is there, no rotation is allowed
|
||||
std::vector<double> allowed_rotations = {0.};
|
||||
|
||||
/// Optional setter function which can store arbitrary data in its closure
|
||||
std::function<void(const ArrangePolygon&)> setter = nullptr;
|
||||
|
||||
@ -140,6 +63,30 @@ struct ArrangePolygon {
|
||||
|
||||
using ArrangePolygons = std::vector<ArrangePolygon>;
|
||||
|
||||
struct ArrangeParams {
|
||||
|
||||
/// The minimum distance which is allowed for any
|
||||
/// pair of items on the print bed in any direction.
|
||||
coord_t min_obj_distance = 0.;
|
||||
|
||||
/// The accuracy of optimization.
|
||||
/// Goes from 0.0 to 1.0 and scales performance as well
|
||||
float accuracy = 0.65f;
|
||||
|
||||
/// Allow parallel execution.
|
||||
bool parallel = true;
|
||||
|
||||
/// Progress indicator callback called when an object gets packed.
|
||||
/// The unsigned argument is the number of items remaining to pack.
|
||||
std::function<void(unsigned)> progressind;
|
||||
|
||||
/// A predicate returning true if abort is needed.
|
||||
std::function<bool(void)> stopcondition;
|
||||
|
||||
ArrangeParams() = default;
|
||||
explicit ArrangeParams(coord_t md) : min_obj_distance(md) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Arranges the input polygons.
|
||||
*
|
||||
@ -150,33 +97,23 @@ using ArrangePolygons = std::vector<ArrangePolygon>;
|
||||
* \param items Input vector of ArrangePolygons. The transformation, rotation
|
||||
* and bin_idx fields will be changed after the call finished and can be used
|
||||
* to apply the result on the input polygon.
|
||||
*
|
||||
* \param min_obj_distance The minimum distance which is allowed for any
|
||||
* pair of items on the print bed in any direction.
|
||||
*
|
||||
* \param bedhint Info about the shape and type of the bed.
|
||||
*
|
||||
* \param progressind Progress indicator callback called when
|
||||
* an object gets packed. The unsigned argument is the number of items
|
||||
* remaining to pack.
|
||||
*
|
||||
* \param stopcondition A predicate returning true if abort is needed.
|
||||
*/
|
||||
void arrange(ArrangePolygons & items,
|
||||
coord_t min_obj_distance,
|
||||
const BedShapeHint & bedhint,
|
||||
std::function<void(unsigned)> progressind = nullptr,
|
||||
std::function<bool(void)> stopcondition = nullptr);
|
||||
template<class TBed> void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const TBed &bed, const ArrangeParams ¶ms = {});
|
||||
|
||||
/// Same as the previous, only that it takes unmovable items as an
|
||||
/// additional argument. Those will be considered as already arranged objects.
|
||||
void arrange(ArrangePolygons & items,
|
||||
const ArrangePolygons & excludes,
|
||||
coord_t min_obj_distance,
|
||||
const BedShapeHint & bedhint,
|
||||
std::function<void(unsigned)> progressind = nullptr,
|
||||
std::function<bool(void)> stopcondition = nullptr);
|
||||
// A dispatch function that determines the bed shape from a set of points.
|
||||
template<> void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Points &bed, const ArrangeParams ¶ms);
|
||||
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const BoundingBox &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams ¶ms);
|
||||
|
||||
inline void arrange(ArrangePolygons &items, const Points &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const BoundingBox &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const CircleBed &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const Polygon &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const InfiniteBed &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
|
||||
}} // namespace Slic3r::arrangement
|
||||
|
||||
} // arr
|
||||
} // Slic3r
|
||||
#endif // MODELARRANGE_HPP
|
||||
|
@ -186,6 +186,11 @@ inline bool empty(const BoundingBox3Base<VT> &bb)
|
||||
return ! bb.defined || bb.min(0) >= bb.max(0) || bb.min(1) >= bb.max(1) || bb.min(2) >= bb.max(2);
|
||||
}
|
||||
|
||||
inline BoundingBox scaled(const BoundingBoxf &bb) { return {scaled(bb.min), scaled(bb.max)}; }
|
||||
inline BoundingBox3 scaled(const BoundingBoxf3 &bb) { return {scaled(bb.min), scaled(bb.max)}; }
|
||||
inline BoundingBoxf unscaled(const BoundingBox &bb) { return {unscaled(bb.min), unscaled(bb.max)}; }
|
||||
inline BoundingBoxf3 unscaled(const BoundingBox3 &bb) { return {unscaled(bb.min), unscaled(bb.max)}; }
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
// Serialization through the Cereal library
|
||||
|
@ -77,6 +77,8 @@ add_library(libslic3r STATIC
|
||||
Format/PRUS.hpp
|
||||
Format/STL.cpp
|
||||
Format/STL.hpp
|
||||
Format/SL1.hpp
|
||||
Format/SL1.cpp
|
||||
GCode/Analyzer.cpp
|
||||
GCode/Analyzer.hpp
|
||||
GCode/ThumbnailData.cpp
|
||||
@ -122,6 +124,8 @@ add_library(libslic3r STATIC
|
||||
Line.hpp
|
||||
Model.cpp
|
||||
Model.hpp
|
||||
ModelArrange.hpp
|
||||
ModelArrange.cpp
|
||||
CustomGCode.cpp
|
||||
CustomGCode.hpp
|
||||
Arrange.hpp
|
||||
@ -162,6 +166,8 @@ add_library(libslic3r STATIC
|
||||
SLAPrint.hpp
|
||||
Slicing.cpp
|
||||
Slicing.hpp
|
||||
SlicesToTriangleMesh.hpp
|
||||
SlicesToTriangleMesh.cpp
|
||||
SlicingAdaptive.cpp
|
||||
SlicingAdaptive.hpp
|
||||
SupportMaterial.cpp
|
||||
@ -177,6 +183,8 @@ add_library(libslic3r STATIC
|
||||
Tesselate.hpp
|
||||
TriangleMesh.cpp
|
||||
TriangleMesh.hpp
|
||||
TriangulateWall.hpp
|
||||
TriangulateWall.cpp
|
||||
utils.cpp
|
||||
Utils.hpp
|
||||
Time.cpp
|
||||
@ -191,6 +199,7 @@ add_library(libslic3r STATIC
|
||||
SimplifyMesh.hpp
|
||||
SimplifyMeshImpl.hpp
|
||||
SimplifyMesh.cpp
|
||||
MarchingSquares.hpp
|
||||
${OpenVDBUtils_SOURCES}
|
||||
SLA/Common.hpp
|
||||
SLA/Common.cpp
|
||||
@ -208,10 +217,11 @@ add_library(libslic3r STATIC
|
||||
SLA/Rotfinder.cpp
|
||||
SLA/BoostAdapter.hpp
|
||||
SLA/SpatIndex.hpp
|
||||
SLA/Raster.hpp
|
||||
SLA/Raster.cpp
|
||||
SLA/RasterWriter.hpp
|
||||
SLA/RasterWriter.cpp
|
||||
SLA/RasterBase.hpp
|
||||
SLA/RasterBase.cpp
|
||||
SLA/AGGRaster.hpp
|
||||
SLA/RasterToPolygons.hpp
|
||||
SLA/RasterToPolygons.cpp
|
||||
SLA/ConcaveHull.hpp
|
||||
SLA/ConcaveHull.cpp
|
||||
SLA/Hollowing.hpp
|
||||
|
171
src/libslic3r/Format/SL1.cpp
Normal file
171
src/libslic3r/Format/SL1.cpp
Normal file
@ -0,0 +1,171 @@
|
||||
#include "SL1.hpp"
|
||||
#include "GCode/ThumbnailData.hpp"
|
||||
#include "libslic3r/Time.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "libslic3r/Zipper.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using ConfMap = std::map<std::string, std::string>;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string to_ini(const ConfMap &m)
|
||||
{
|
||||
std::string ret;
|
||||
for (auto ¶m : m) ret += param.first + " = " + param.second + "\n";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key)
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
if (cfg.has(key)) {
|
||||
auto opt = cfg.option(key);
|
||||
if (opt) ret = opt->serialize();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void fill_iniconf(ConfMap &m, const SLAPrint &print)
|
||||
{
|
||||
auto &cfg = print.full_print_config();
|
||||
m["layerHeight"] = get_cfg_value(cfg, "layer_height");
|
||||
m["expTime"] = get_cfg_value(cfg, "exposure_time");
|
||||
m["expTimeFirst"] = get_cfg_value(cfg, "initial_exposure_time");
|
||||
m["materialName"] = get_cfg_value(cfg, "sla_material_settings_id");
|
||||
m["printerModel"] = get_cfg_value(cfg, "printer_model");
|
||||
m["printerVariant"] = get_cfg_value(cfg, "printer_variant");
|
||||
m["printerProfile"] = get_cfg_value(cfg, "printer_settings_id");
|
||||
m["printProfile"] = get_cfg_value(cfg, "sla_print_settings_id");
|
||||
m["fileCreationTimestamp"] = Utils::utc_timestamp();
|
||||
m["prusaSlicerVersion"] = SLIC3R_BUILD_ID;
|
||||
|
||||
SLAPrintStatistics stats = print.print_statistics();
|
||||
// Set statistics values to the printer
|
||||
|
||||
double used_material = (stats.objects_used_material +
|
||||
stats.support_used_material) / 1000;
|
||||
|
||||
int num_fade = print.default_object_config().faded_layers.getInt();
|
||||
num_fade = num_fade >= 0 ? num_fade : 0;
|
||||
|
||||
m["usedMaterial"] = std::to_string(used_material);
|
||||
m["numFade"] = std::to_string(num_fade);
|
||||
m["numSlow"] = std::to_string(stats.slow_layers_count);
|
||||
m["numFast"] = std::to_string(stats.fast_layers_count);
|
||||
m["printTime"] = std::to_string(stats.estimated_print_time);
|
||||
|
||||
m["action"] = "print";
|
||||
}
|
||||
|
||||
void fill_slicerconf(ConfMap &m, const SLAPrint &print)
|
||||
{
|
||||
using namespace std::literals::string_view_literals;
|
||||
|
||||
// Sorted list of config keys, which shall not be stored into the ini.
|
||||
static constexpr auto banned_keys = {
|
||||
"compatible_printers"sv,
|
||||
"compatible_prints"sv,
|
||||
"print_host"sv,
|
||||
"printhost_apikey"sv,
|
||||
"printhost_cafile"sv
|
||||
};
|
||||
|
||||
assert(std::is_sorted(banned_keys.begin(), banned_keys.end()));
|
||||
auto is_banned = [](const std::string &key) {
|
||||
return std::binary_search(banned_keys.begin(), banned_keys.end(), key);
|
||||
};
|
||||
|
||||
auto &cfg = print.full_print_config();
|
||||
for (const std::string &key : cfg.keys())
|
||||
if (! is_banned(key) && ! cfg.option(key)->is_nil())
|
||||
m[key] = cfg.opt_serialize(key);
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uqptr<sla::RasterBase> SL1Archive::create_raster() const
|
||||
{
|
||||
sla::RasterBase::Resolution res;
|
||||
sla::RasterBase::PixelDim pxdim;
|
||||
std::array<bool, 2> mirror;
|
||||
|
||||
double w = m_cfg.display_width.getFloat();
|
||||
double h = m_cfg.display_height.getFloat();
|
||||
auto pw = size_t(m_cfg.display_pixels_x.getInt());
|
||||
auto ph = size_t(m_cfg.display_pixels_y.getInt());
|
||||
|
||||
mirror[X] = m_cfg.display_mirror_x.getBool();
|
||||
mirror[Y] = m_cfg.display_mirror_y.getBool();
|
||||
|
||||
auto ro = m_cfg.display_orientation.getInt();
|
||||
sla::RasterBase::Orientation orientation =
|
||||
ro == sla::RasterBase::roPortrait ? sla::RasterBase::roPortrait :
|
||||
sla::RasterBase::roLandscape;
|
||||
|
||||
if (orientation == sla::RasterBase::roPortrait) {
|
||||
std::swap(w, h);
|
||||
std::swap(pw, ph);
|
||||
}
|
||||
|
||||
res = sla::RasterBase::Resolution{pw, ph};
|
||||
pxdim = sla::RasterBase::PixelDim{w / pw, h / ph};
|
||||
sla::RasterBase::Trafo tr{orientation, mirror};
|
||||
|
||||
double gamma = m_cfg.gamma_correction.getFloat();
|
||||
|
||||
return sla::create_raster_grayscale_aa(res, pxdim, gamma, tr);
|
||||
}
|
||||
|
||||
sla::EncodedRaster SL1Archive::encode_raster(const sla::RasterBase &rst) const
|
||||
{
|
||||
return rst.encode(sla::PNGRasterEncoder());
|
||||
}
|
||||
|
||||
void SL1Archive::export_print(Zipper& zipper,
|
||||
const SLAPrint &print,
|
||||
const std::string &prjname)
|
||||
{
|
||||
std::string project =
|
||||
prjname.empty() ?
|
||||
boost::filesystem::path(zipper.get_filename()).stem().string() :
|
||||
prjname;
|
||||
|
||||
ConfMap iniconf, slicerconf;
|
||||
fill_iniconf(iniconf, print);
|
||||
|
||||
iniconf["jobDir"] = project;
|
||||
|
||||
fill_slicerconf(slicerconf, print);
|
||||
|
||||
try {
|
||||
zipper.add_entry("config.ini");
|
||||
zipper << to_ini(iniconf);
|
||||
zipper.add_entry("prusaslicer.ini");
|
||||
zipper << to_ini(slicerconf);
|
||||
|
||||
size_t i = 0;
|
||||
for (const sla::EncodedRaster &rst : m_layers) {
|
||||
|
||||
std::string imgname = project + string_printf("%.5d", i++) + "." +
|
||||
rst.extension();
|
||||
|
||||
zipper.add_entry(imgname.c_str(), rst.data(), rst.size());
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||
// Rethrow the exception
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
44
src/libslic3r/Format/SL1.hpp
Normal file
44
src/libslic3r/Format/SL1.hpp
Normal file
@ -0,0 +1,44 @@
|
||||
#ifndef ARCHIVETRAITS_HPP
|
||||
#define ARCHIVETRAITS_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "libslic3r/Zipper.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class SL1Archive: public SLAPrinter {
|
||||
SLAPrinterConfig m_cfg;
|
||||
|
||||
protected:
|
||||
uqptr<sla::RasterBase> create_raster() const override;
|
||||
sla::EncodedRaster encode_raster(const sla::RasterBase &rst) const override;
|
||||
|
||||
public:
|
||||
|
||||
SL1Archive() = default;
|
||||
explicit SL1Archive(const SLAPrinterConfig &cfg): m_cfg(cfg) {}
|
||||
explicit SL1Archive(SLAPrinterConfig &&cfg): m_cfg(std::move(cfg)) {}
|
||||
|
||||
void export_print(Zipper &zipper, const SLAPrint &print, const std::string &projectname = "");
|
||||
void export_print(const std::string &fname, const SLAPrint &print, const std::string &projectname = "")
|
||||
{
|
||||
Zipper zipper(fname);
|
||||
export_print(zipper, print, projectname);
|
||||
}
|
||||
|
||||
void apply(const SLAPrinterConfig &cfg) override
|
||||
{
|
||||
auto diff = m_cfg.diff(cfg);
|
||||
if (!diff.empty()) {
|
||||
m_cfg.apply_only(cfg, diff);
|
||||
m_layers = {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} // namespace Slic3r::sla
|
||||
|
||||
#endif // ARCHIVETRAITS_HPP
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@ -75,143 +76,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/// An std compatible random access iterator which uses indices to the
|
||||
/// source vector thus resistant to invalidation caused by relocations. It
|
||||
/// also "knows" its container. No comparison is neccesary to the container
|
||||
/// "end()" iterator. The template can be instantiated with a different
|
||||
/// value type than that of the container's but the types must be
|
||||
/// compatible. E.g. a base class of the contained objects is compatible.
|
||||
///
|
||||
/// For a constant iterator, one can instantiate this template with a value
|
||||
/// type preceded with 'const'.
|
||||
template<class Vector, // The container type, must be random access...
|
||||
class Value = typename Vector::value_type // The value type
|
||||
>
|
||||
class IndexBasedIterator
|
||||
{
|
||||
static const size_t NONE = size_t(-1);
|
||||
|
||||
std::reference_wrapper<Vector> m_index_ref;
|
||||
size_t m_idx = NONE;
|
||||
|
||||
public:
|
||||
using value_type = Value;
|
||||
using pointer = Value *;
|
||||
using reference = Value &;
|
||||
using difference_type = long;
|
||||
using iterator_category = std::random_access_iterator_tag;
|
||||
|
||||
inline explicit IndexBasedIterator(Vector &index, size_t idx)
|
||||
: m_index_ref(index), m_idx(idx)
|
||||
{}
|
||||
|
||||
// Post increment
|
||||
inline IndexBasedIterator operator++(int)
|
||||
{
|
||||
IndexBasedIterator cpy(*this);
|
||||
++m_idx;
|
||||
return cpy;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator operator--(int)
|
||||
{
|
||||
IndexBasedIterator cpy(*this);
|
||||
--m_idx;
|
||||
return cpy;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator &operator++()
|
||||
{
|
||||
++m_idx;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator &operator--()
|
||||
{
|
||||
--m_idx;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator &operator+=(difference_type l)
|
||||
{
|
||||
m_idx += size_t(l);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator operator+(difference_type l)
|
||||
{
|
||||
auto cpy = *this;
|
||||
cpy += l;
|
||||
return cpy;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator &operator-=(difference_type l)
|
||||
{
|
||||
m_idx -= size_t(l);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline IndexBasedIterator operator-(difference_type l)
|
||||
{
|
||||
auto cpy = *this;
|
||||
cpy -= l;
|
||||
return cpy;
|
||||
}
|
||||
|
||||
operator difference_type() { return difference_type(m_idx); }
|
||||
|
||||
/// Tesing the end of the container... this is not possible with std
|
||||
/// iterators.
|
||||
inline bool is_end() const
|
||||
{
|
||||
return m_idx >= m_index_ref.get().size();
|
||||
}
|
||||
|
||||
inline Value &operator*() const
|
||||
{
|
||||
assert(m_idx < m_index_ref.get().size());
|
||||
return m_index_ref.get().operator[](m_idx);
|
||||
}
|
||||
|
||||
inline Value *operator->() const
|
||||
{
|
||||
assert(m_idx < m_index_ref.get().size());
|
||||
return &m_index_ref.get().operator[](m_idx);
|
||||
}
|
||||
|
||||
/// If both iterators point past the container, they are equal...
|
||||
inline bool operator==(const IndexBasedIterator &other)
|
||||
{
|
||||
size_t e = m_index_ref.get().size();
|
||||
return m_idx == other.m_idx || (m_idx >= e && other.m_idx >= e);
|
||||
}
|
||||
|
||||
inline bool operator!=(const IndexBasedIterator &other)
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
inline bool operator<=(const IndexBasedIterator &other)
|
||||
{
|
||||
return (m_idx < other.m_idx) || (*this == other);
|
||||
}
|
||||
|
||||
inline bool operator<(const IndexBasedIterator &other)
|
||||
{
|
||||
return m_idx < other.m_idx && (*this != other);
|
||||
}
|
||||
|
||||
inline bool operator>=(const IndexBasedIterator &other)
|
||||
{
|
||||
return m_idx > other.m_idx || *this == other;
|
||||
}
|
||||
|
||||
inline bool operator>(const IndexBasedIterator &other)
|
||||
{
|
||||
return m_idx > other.m_idx && *this != other;
|
||||
}
|
||||
};
|
||||
|
||||
/// A very simple range concept implementation with iterator-like objects.
|
||||
template<class It> class Range
|
||||
{
|
||||
@ -252,97 +116,6 @@ template<class T> struct remove_cvref
|
||||
|
||||
template<class T> using remove_cvref_t = typename remove_cvref<T>::type;
|
||||
|
||||
// A shorter C++14 style form of the enable_if metafunction
|
||||
template<bool B, class T>
|
||||
using enable_if_t = typename std::enable_if<B, T>::type;
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Type safe conversions to and from scaled and unscaled coordinates
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// A meta-predicate which is true for integers wider than or equal to coord_t
|
||||
template<class I> struct is_scaled_coord
|
||||
{
|
||||
static const SLIC3R_CONSTEXPR bool value =
|
||||
std::is_integral<I>::value &&
|
||||
std::numeric_limits<I>::digits >=
|
||||
std::numeric_limits<coord_t>::digits;
|
||||
};
|
||||
|
||||
// Meta predicates for floating, 'scaled coord' and generic arithmetic types
|
||||
template<class T, class O = T>
|
||||
using FloatingOnly = enable_if_t<std::is_floating_point<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using ScaledCoordOnly = enable_if_t<is_scaled_coord<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using IntegerOnly = enable_if_t<std::is_integral<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using ArithmeticOnly = enable_if_t<std::is_arithmetic<T>::value, O>;
|
||||
|
||||
// Semantics are the following:
|
||||
// Upscaling (scaled()): only from floating point types (or Vec) to either
|
||||
// floating point or integer 'scaled coord' coordinates.
|
||||
// Downscaling (unscaled()): from arithmetic (or Vec) to floating point only
|
||||
|
||||
// Conversion definition from unscaled to floating point scaled
|
||||
template<class Tout,
|
||||
class Tin,
|
||||
class = FloatingOnly<Tin>>
|
||||
inline constexpr FloatingOnly<Tout> scaled(const Tin &v) noexcept
|
||||
{
|
||||
return Tout(v / Tin(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Conversion definition from unscaled to integer 'scaled coord'.
|
||||
// TODO: is the rounding necessary? Here it is commented out to show that
|
||||
// it can be different for integers but it does not have to be. Using
|
||||
// std::round means loosing noexcept and constexpr modifiers
|
||||
template<class Tout = coord_t, class Tin, class = FloatingOnly<Tin>>
|
||||
inline constexpr ScaledCoordOnly<Tout> scaled(const Tin &v) noexcept
|
||||
{
|
||||
//return static_cast<Tout>(std::round(v / SCALING_FACTOR));
|
||||
return Tout(v / Tin(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Conversion for Eigen vectors (N dimensional points)
|
||||
template<class Tout = coord_t,
|
||||
class Tin,
|
||||
int N,
|
||||
class = FloatingOnly<Tin>,
|
||||
int...EigenArgs>
|
||||
inline Eigen::Matrix<ArithmeticOnly<Tout>, N, EigenArgs...>
|
||||
scaled(const Eigen::Matrix<Tin, N, EigenArgs...> &v)
|
||||
{
|
||||
return (v / SCALING_FACTOR).template cast<Tout>();
|
||||
}
|
||||
|
||||
// Conversion from arithmetic scaled type to floating point unscaled
|
||||
template<class Tout = double,
|
||||
class Tin,
|
||||
class = ArithmeticOnly<Tin>,
|
||||
class = FloatingOnly<Tout>>
|
||||
inline constexpr Tout unscaled(const Tin &v) noexcept
|
||||
{
|
||||
return Tout(v * Tout(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Unscaling for Eigen vectors. Input base type can be arithmetic, output base
|
||||
// type can only be floating point.
|
||||
template<class Tout = double,
|
||||
class Tin,
|
||||
int N,
|
||||
class = ArithmeticOnly<Tin>,
|
||||
class = FloatingOnly<Tout>,
|
||||
int...EigenArgs>
|
||||
inline constexpr Eigen::Matrix<Tout, N, EigenArgs...>
|
||||
unscaled(const Eigen::Matrix<Tin, N, EigenArgs...> &v) noexcept
|
||||
{
|
||||
return v.template cast<Tout>() * SCALING_FACTOR;
|
||||
}
|
||||
|
||||
template<class T, class I, class... Args> // Arbitrary allocator can be used
|
||||
inline IntegerOnly<I, std::vector<T, Args...>> reserve_vector(I capacity)
|
||||
{
|
||||
@ -353,10 +126,10 @@ inline IntegerOnly<I, std::vector<T, Args...>> reserve_vector(I capacity)
|
||||
}
|
||||
|
||||
/// Exactly like Matlab https://www.mathworks.com/help/matlab/ref/linspace.html
|
||||
template<class T, class I>
|
||||
template<class T, class I, class = IntegerOnly<I>>
|
||||
inline std::vector<T> linspace_vector(const ArithmeticOnly<T> &start,
|
||||
const T &stop,
|
||||
const IntegerOnly<I> &n)
|
||||
const I &n)
|
||||
{
|
||||
std::vector<T> vals(n, T());
|
||||
|
||||
|
448
src/libslic3r/MarchingSquares.hpp
Normal file
448
src/libslic3r/MarchingSquares.hpp
Normal file
@ -0,0 +1,448 @@
|
||||
#ifndef MARCHINGSQUARES_HPP
|
||||
#define MARCHINGSQUARES_HPP
|
||||
|
||||
#include <type_traits>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace marchsq {
|
||||
|
||||
// Marks a square in the grid
|
||||
struct Coord {
|
||||
long r = 0, c = 0;
|
||||
|
||||
Coord() = default;
|
||||
explicit Coord(long s) : r(s), c(s) {}
|
||||
Coord(long _r, long _c): r(_r), c(_c) {}
|
||||
|
||||
size_t seq(const Coord &res) const { return r * res.c + c; }
|
||||
Coord& operator+=(const Coord& b) { r += b.r; c += b.c; return *this; }
|
||||
Coord operator+(const Coord& b) const { Coord a = *this; a += b; return a; }
|
||||
};
|
||||
|
||||
// Closed ring of cell coordinates
|
||||
using Ring = std::vector<Coord>;
|
||||
|
||||
// Specialize this struct to register a raster type for the Marching squares alg
|
||||
template<class T, class Enable = void> struct _RasterTraits {
|
||||
|
||||
// The type of pixel cell in the raster
|
||||
using ValueType = typename T::ValueType;
|
||||
|
||||
// Value at a given position
|
||||
static ValueType get(const T &raster, size_t row, size_t col);
|
||||
|
||||
// Number of rows and cols of the raster
|
||||
static size_t rows(const T &raster);
|
||||
static size_t cols(const T &raster);
|
||||
};
|
||||
|
||||
// Specialize this to use parellel loops within the algorithm
|
||||
template<class ExecutionPolicy, class Enable = void> struct _Loop {
|
||||
template<class It, class Fn> static void for_each(It from, It to, Fn &&fn)
|
||||
{
|
||||
for (auto it = from; it < to; ++it) fn(*it, size_t(it - from));
|
||||
}
|
||||
};
|
||||
|
||||
namespace __impl {
|
||||
|
||||
template<class T> using RasterTraits = _RasterTraits<std::decay_t<T>>;
|
||||
template<class T> using TRasterValue = typename RasterTraits<T>::ValueType;
|
||||
|
||||
template<class T> size_t rows(const T &raster)
|
||||
{
|
||||
return RasterTraits<T>::rows(raster);
|
||||
}
|
||||
|
||||
template<class T> size_t cols(const T &raster)
|
||||
{
|
||||
return RasterTraits<T>::cols(raster);
|
||||
}
|
||||
|
||||
template<class T> TRasterValue<T> isoval(const T &rst, const Coord &crd)
|
||||
{
|
||||
return RasterTraits<T>::get(rst, crd.r, crd.c);
|
||||
}
|
||||
|
||||
template<class ExecutionPolicy, class It, class Fn>
|
||||
void for_each(ExecutionPolicy&& policy, It from, It to, Fn &&fn)
|
||||
{
|
||||
_Loop<ExecutionPolicy>::for_each(from, to, fn);
|
||||
}
|
||||
|
||||
// Type of squares (tiles) depending on which vertices are inside an ROI
|
||||
// The vertices would be marked a, b, c, d in counter clockwise order from the
|
||||
// bottom left vertex of a square.
|
||||
// d --- c
|
||||
// | |
|
||||
// | |
|
||||
// a --- b
|
||||
enum class SquareTag : uint8_t {
|
||||
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
|
||||
none, a, b, ab, c, ac, bc, abc, d, ad, bd, abd, cd, acd, bcd, full
|
||||
};
|
||||
|
||||
template<class E> constexpr std::underlying_type_t<E> _t(E e) noexcept
|
||||
{
|
||||
return static_cast<std::underlying_type_t<E>>(e);
|
||||
}
|
||||
|
||||
enum class Dir: uint8_t { left, down, right, up, none};
|
||||
|
||||
static const constexpr Dir NEXT_CCW[] = {
|
||||
/* 00 */ Dir::none, // SquareTag::none (empty square, nowhere to go)
|
||||
/* 01 */ Dir::left, // SquareTag::a
|
||||
/* 02 */ Dir::down, // SquareTag::b
|
||||
/* 03 */ Dir::left, // SquareTag::ab
|
||||
/* 04 */ Dir::right, // SquareTag::c
|
||||
/* 05 */ Dir::none, // SquareTag::ac (ambiguous case)
|
||||
/* 06 */ Dir::down, // SquareTag::bc
|
||||
/* 07 */ Dir::left, // SquareTag::abc
|
||||
/* 08 */ Dir::up, // SquareTag::d
|
||||
/* 09 */ Dir::up, // SquareTag::ad
|
||||
/* 10 */ Dir::none, // SquareTag::bd (ambiguous case)
|
||||
/* 11 */ Dir::up, // SquareTag::abd
|
||||
/* 12 */ Dir::right, // SquareTag::cd
|
||||
/* 13 */ Dir::right, // SquareTag::acd
|
||||
/* 14 */ Dir::down, // SquareTag::bcd
|
||||
/* 15 */ Dir::none // SquareTag::full (full covered, nowhere to go)
|
||||
};
|
||||
|
||||
static const constexpr uint8_t PREV_CCW[] = {
|
||||
/* 00 */ 1 << _t(Dir::none),
|
||||
/* 01 */ 1 << _t(Dir::up),
|
||||
/* 02 */ 1 << _t(Dir::left),
|
||||
/* 03 */ 1 << _t(Dir::left),
|
||||
/* 04 */ 1 << _t(Dir::down),
|
||||
/* 05 */ 1 << _t(Dir::up) | 1 << _t(Dir::down),
|
||||
/* 06 */ 1 << _t(Dir::down),
|
||||
/* 07 */ 1 << _t(Dir::down),
|
||||
/* 08 */ 1 << _t(Dir::right),
|
||||
/* 09 */ 1 << _t(Dir::up),
|
||||
/* 10 */ 1 << _t(Dir::left) | 1 << _t(Dir::right),
|
||||
/* 11 */ 1 << _t(Dir::left),
|
||||
/* 12 */ 1 << _t(Dir::right),
|
||||
/* 13 */ 1 << _t(Dir::up),
|
||||
/* 14 */ 1 << _t(Dir::right),
|
||||
/* 15 */ 1 << _t(Dir::none)
|
||||
};
|
||||
|
||||
const constexpr uint8_t DIRMASKS[] = {
|
||||
/*left: */ 0x01, /*down*/ 0x12, /*right */0x21, /*up*/ 0x10, /*none*/ 0x00
|
||||
};
|
||||
|
||||
inline Coord step(const Coord &crd, Dir d)
|
||||
{
|
||||
uint8_t dd = DIRMASKS[uint8_t(d)];
|
||||
return {crd.r - 1 + (dd & 0x0f), crd.c - 1 + (dd >> 4)};
|
||||
}
|
||||
|
||||
template<class Rst> class Grid {
|
||||
const Rst * m_rst = nullptr;
|
||||
Coord m_cellsize, m_res_1, m_window, m_gridsize, m_grid_1;
|
||||
std::vector<uint8_t> m_tags; // Assign tags to each square
|
||||
|
||||
Coord rastercoord(const Coord &crd) const
|
||||
{
|
||||
return {(crd.r - 1) * m_window.r, (crd.c - 1) * m_window.c};
|
||||
}
|
||||
|
||||
Coord bl(const Coord &crd) const { return tl(crd) + Coord{m_res_1.r, 0}; }
|
||||
Coord br(const Coord &crd) const { return tl(crd) + Coord{m_res_1.r, m_res_1.c}; }
|
||||
Coord tr(const Coord &crd) const { return tl(crd) + Coord{0, m_res_1.c}; }
|
||||
Coord tl(const Coord &crd) const { return rastercoord(crd); }
|
||||
|
||||
bool is_within(const Coord &crd)
|
||||
{
|
||||
long R = rows(*m_rst), C = cols(*m_rst);
|
||||
return crd.r >= 0 && crd.r < R && crd.c >= 0 && crd.c < C;
|
||||
};
|
||||
|
||||
// Calculate the tag for a cell (or square). The cell coordinates mark the
|
||||
// top left vertex of a square in the raster. v is the isovalue
|
||||
uint8_t get_tag_for_cell(const Coord &cell, TRasterValue<Rst> v)
|
||||
{
|
||||
Coord sqr[] = {bl(cell), br(cell), tr(cell), tl(cell)};
|
||||
|
||||
uint8_t t = ((is_within(sqr[0]) && isoval(*m_rst, sqr[0]) >= v)) +
|
||||
((is_within(sqr[1]) && isoval(*m_rst, sqr[1]) >= v) << 1) +
|
||||
((is_within(sqr[2]) && isoval(*m_rst, sqr[2]) >= v) << 2) +
|
||||
((is_within(sqr[3]) && isoval(*m_rst, sqr[3]) >= v) << 3);
|
||||
|
||||
assert(t < 16);
|
||||
return t;
|
||||
}
|
||||
|
||||
// Get a cell coordinate from a sequential index
|
||||
Coord coord(size_t i) const
|
||||
{
|
||||
return {long(i) / m_gridsize.c, long(i) % m_gridsize.c};
|
||||
}
|
||||
|
||||
size_t seq(const Coord &crd) const { return crd.seq(m_gridsize); }
|
||||
|
||||
bool is_visited(size_t idx, Dir d = Dir::none) const
|
||||
{
|
||||
SquareTag t = get_tag(idx);
|
||||
uint8_t ref = d == Dir::none ? PREV_CCW[_t(t)] : uint8_t(1 << _t(d));
|
||||
return t == SquareTag::full || t == SquareTag::none ||
|
||||
((m_tags[idx] & 0xf0) >> 4) == ref;
|
||||
}
|
||||
|
||||
void set_visited(size_t idx, Dir d = Dir::none)
|
||||
{
|
||||
m_tags[idx] |= (1 << (_t(d)) << 4);
|
||||
}
|
||||
|
||||
bool is_ambiguous(size_t idx) const
|
||||
{
|
||||
SquareTag t = get_tag(idx);
|
||||
return t == SquareTag::ac || t == SquareTag::bd;
|
||||
}
|
||||
|
||||
// Search for a new starting square
|
||||
size_t search_start_cell(size_t i = 0) const
|
||||
{
|
||||
// Skip ambiguous tags as starting tags due to unknown previous
|
||||
// direction.
|
||||
while ((i < m_tags.size()) && (is_visited(i) || is_ambiguous(i))) ++i;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
SquareTag get_tag(size_t idx) const { return SquareTag(m_tags[idx] & 0x0f); }
|
||||
|
||||
Dir next_dir(Dir prev, SquareTag tag) const
|
||||
{
|
||||
// Treat ambiguous cases as two separate regions in one square.
|
||||
switch (tag) {
|
||||
case SquareTag::ac:
|
||||
switch (prev) {
|
||||
case Dir::down: return Dir::right;
|
||||
case Dir::up: return Dir::left;
|
||||
default: assert(false); return Dir::none;
|
||||
}
|
||||
case SquareTag::bd:
|
||||
switch (prev) {
|
||||
case Dir::right: return Dir::up;
|
||||
case Dir::left: return Dir::down;
|
||||
default: assert(false); return Dir::none;
|
||||
}
|
||||
default:
|
||||
return NEXT_CCW[uint8_t(tag)];
|
||||
}
|
||||
|
||||
return Dir::none;
|
||||
}
|
||||
|
||||
struct CellIt {
|
||||
Coord crd; Dir dir= Dir::none; const Rst *grid = nullptr;
|
||||
|
||||
TRasterValue<Rst> operator*() const { return isoval(*grid, crd); }
|
||||
CellIt& operator++() { crd = step(crd, dir); return *this; }
|
||||
CellIt operator++(int) { CellIt it = *this; ++(*this); return it; }
|
||||
bool operator!=(const CellIt &it) { return crd.r != it.crd.r || crd.c != it.crd.c; }
|
||||
|
||||
using value_type = TRasterValue<Rst>;
|
||||
using pointer = TRasterValue<Rst> *;
|
||||
using reference = TRasterValue<Rst> &;
|
||||
using difference_type = long;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
};
|
||||
|
||||
// Two cell iterators representing an edge of a square. This is then
|
||||
// used for binary search for the first active pixel on the edge.
|
||||
struct Edge { CellIt from, to; };
|
||||
|
||||
Edge _edge(const Coord &ringvertex) const
|
||||
{
|
||||
size_t idx = ringvertex.r;
|
||||
Coord cell = coord(idx);
|
||||
uint8_t tg = m_tags[ringvertex.r];
|
||||
SquareTag t = SquareTag(tg & 0x0f);
|
||||
|
||||
switch (t) {
|
||||
case SquareTag::a:
|
||||
case SquareTag::ab:
|
||||
case SquareTag::abc:
|
||||
return {{tl(cell), Dir::down, m_rst}, {bl(cell)}};
|
||||
case SquareTag::b:
|
||||
case SquareTag::bc:
|
||||
case SquareTag::bcd:
|
||||
return {{bl(cell), Dir::right, m_rst}, {br(cell)}};
|
||||
case SquareTag::c:
|
||||
return {{br(cell), Dir::up, m_rst}, {tr(cell)}};
|
||||
case SquareTag::ac:
|
||||
switch (Dir(ringvertex.c)) {
|
||||
case Dir::left: return {{tl(cell), Dir::down, m_rst}, {bl(cell)}};
|
||||
case Dir::right: return {{br(cell), Dir::up, m_rst}, {tr(cell)}};
|
||||
default: assert(false);
|
||||
}
|
||||
case SquareTag::d:
|
||||
case SquareTag::ad:
|
||||
case SquareTag::abd:
|
||||
return {{tr(cell), Dir::left, m_rst}, {tl(cell)}};
|
||||
case SquareTag::bd:
|
||||
switch (Dir(ringvertex.c)) {
|
||||
case Dir::down: return {{bl(cell), Dir::right, m_rst}, {br(cell)}};
|
||||
case Dir::up: return {{tr(cell), Dir::left, m_rst}, {tl(cell)}};
|
||||
default: assert(false);
|
||||
}
|
||||
case SquareTag::cd:
|
||||
case SquareTag::acd:
|
||||
return {{br(cell), Dir::up, m_rst}, {tr(cell)}};
|
||||
case SquareTag::full:
|
||||
case SquareTag::none: {
|
||||
Coord crd{tl(cell) + Coord{m_cellsize.r / 2, m_cellsize.c / 2}};
|
||||
return {{crd, Dir::none, m_rst}, crd};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Edge edge(const Coord &ringvertex) const
|
||||
{
|
||||
const long R = rows(*m_rst), C = cols(*m_rst);
|
||||
const long R_1 = R - 1, C_1 = C - 1;
|
||||
|
||||
Edge e = _edge(ringvertex);
|
||||
e.to.dir = e.from.dir;
|
||||
++e.to;
|
||||
|
||||
e.from.crd.r = std::min(e.from.crd.r, R_1);
|
||||
e.from.crd.r = std::max(e.from.crd.r, 0l);
|
||||
e.from.crd.c = std::min(e.from.crd.c, C_1);
|
||||
e.from.crd.c = std::max(e.from.crd.c, 0l);
|
||||
|
||||
e.to.crd.r = std::min(e.to.crd.r, R);
|
||||
e.to.crd.r = std::max(e.to.crd.r, 0l);
|
||||
e.to.crd.c = std::min(e.to.crd.c, C);
|
||||
e.to.crd.c = std::max(e.to.crd.c, 0l);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Grid(const Rst &rst, const Coord &cellsz, const Coord &overlap)
|
||||
: m_rst{&rst}
|
||||
, m_cellsize{cellsz}
|
||||
, m_res_1{m_cellsize.r - 1, m_cellsize.c - 1}
|
||||
, m_window{overlap.r < cellsz.r ? cellsz.r - overlap.r : cellsz.r,
|
||||
overlap.c < cellsz.c ? cellsz.c - overlap.c : cellsz.c}
|
||||
, m_gridsize{2 + (long(rows(rst)) - overlap.r) / m_window.r,
|
||||
2 + (long(cols(rst)) - overlap.c) / m_window.c}
|
||||
, m_tags(m_gridsize.r * m_gridsize.c, 0)
|
||||
{}
|
||||
|
||||
// Go through the cells and mark them with the appropriate tag.
|
||||
template<class ExecutionPolicy>
|
||||
void tag_grid(ExecutionPolicy &&policy, TRasterValue<Rst> isoval)
|
||||
{
|
||||
// parallel for r
|
||||
for_each (std::forward<ExecutionPolicy>(policy),
|
||||
m_tags.begin(), m_tags.end(),
|
||||
[this, isoval](uint8_t& tag, size_t idx) {
|
||||
tag = get_tag_for_cell(coord(idx), isoval);
|
||||
});
|
||||
}
|
||||
|
||||
// Scan for the rings on the tagged grid. Each ring vertex stores the
|
||||
// sequential index of the cell and the next direction (Dir).
|
||||
// This info can be used later to calculate the exact raster coordinate.
|
||||
std::vector<Ring> scan_rings()
|
||||
{
|
||||
std::vector<Ring> rings;
|
||||
size_t startidx = 0;
|
||||
while ((startidx = search_start_cell(startidx)) < m_tags.size()) {
|
||||
Ring ring;
|
||||
|
||||
size_t idx = startidx;
|
||||
Dir prev = Dir::none, next = next_dir(prev, get_tag(idx));
|
||||
|
||||
while (next != Dir::none && !is_visited(idx, prev)) {
|
||||
Coord ringvertex{long(idx), long(next)};
|
||||
ring.emplace_back(ringvertex);
|
||||
set_visited(idx, prev);
|
||||
|
||||
idx = seq(step(coord(idx), next));
|
||||
prev = next;
|
||||
next = next_dir(next, get_tag(idx));
|
||||
}
|
||||
|
||||
// To prevent infinite loops in case of degenerate input
|
||||
if (next == Dir::none) m_tags[startidx] = _t(SquareTag::none);
|
||||
|
||||
if (ring.size() > 1) {
|
||||
ring.pop_back();
|
||||
rings.emplace_back(ring);
|
||||
}
|
||||
}
|
||||
|
||||
return rings;
|
||||
}
|
||||
|
||||
// Calculate the exact raster position from the cells which store the
|
||||
// sequantial index of the square and the next direction
|
||||
template<class ExecutionPolicy>
|
||||
void interpolate_rings(ExecutionPolicy && policy,
|
||||
std::vector<Ring> &rings,
|
||||
TRasterValue<Rst> isov)
|
||||
{
|
||||
for_each(std::forward<ExecutionPolicy>(policy),
|
||||
rings.begin(), rings.end(), [this, isov] (Ring &ring, size_t)
|
||||
{
|
||||
for (Coord &ringvertex : ring) {
|
||||
Edge e = edge(ringvertex);
|
||||
|
||||
CellIt found = std::lower_bound(e.from, e.to, isov);
|
||||
ringvertex = found.crd;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
template<class Raster, class ExecutionPolicy>
|
||||
std::vector<marchsq::Ring> execute_with_policy(ExecutionPolicy && policy,
|
||||
const Raster & raster,
|
||||
TRasterValue<Raster> isoval,
|
||||
Coord windowsize = {})
|
||||
{
|
||||
if (!rows(raster) || !cols(raster)) return {};
|
||||
|
||||
size_t ratio = cols(raster) / rows(raster);
|
||||
|
||||
if (!windowsize.r) windowsize.r = 2;
|
||||
if (!windowsize.c)
|
||||
windowsize.c = std::max(2l, long(windowsize.r * ratio));
|
||||
|
||||
Coord overlap{1};
|
||||
|
||||
Grid<Raster> grid{raster, windowsize, overlap};
|
||||
|
||||
grid.tag_grid(std::forward<ExecutionPolicy>(policy), isoval);
|
||||
std::vector<marchsq::Ring> rings = grid.scan_rings();
|
||||
grid.interpolate_rings(std::forward<ExecutionPolicy>(policy), rings, isoval);
|
||||
|
||||
return rings;
|
||||
}
|
||||
|
||||
template<class Raster>
|
||||
std::vector<marchsq::Ring> execute(const Raster &raster,
|
||||
TRasterValue<Raster> isoval,
|
||||
Coord windowsize = {})
|
||||
{
|
||||
return execute_with_policy(nullptr, raster, isoval, windowsize);
|
||||
}
|
||||
|
||||
} // namespace __impl
|
||||
|
||||
using __impl::execute_with_policy;
|
||||
using __impl::execute;
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
#endif // MARCHINGSQUARES_HPP
|
@ -1,4 +1,5 @@
|
||||
#include "Model.hpp"
|
||||
#include "ModelArrange.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
|
||||
@ -355,116 +356,6 @@ TriangleMesh Model::mesh() const
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static bool _arrange(const Pointfs &sizes, coordf_t dist, const BoundingBoxf* bb, Pointfs &out)
|
||||
{
|
||||
if (sizes.empty())
|
||||
// return if the list is empty or the following call to BoundingBoxf constructor will lead to a crash
|
||||
return true;
|
||||
|
||||
// we supply unscaled data to arrange()
|
||||
bool result = Slic3r::Geometry::arrange(
|
||||
sizes.size(), // number of parts
|
||||
BoundingBoxf(sizes).max, // width and height of a single cell
|
||||
dist, // distance between cells
|
||||
bb, // bounding box of the area to fill
|
||||
out // output positions
|
||||
);
|
||||
|
||||
if (!result && bb != nullptr) {
|
||||
// Try to arrange again ignoring bb
|
||||
result = Slic3r::Geometry::arrange(
|
||||
sizes.size(), // number of parts
|
||||
BoundingBoxf(sizes).max, // width and height of a single cell
|
||||
dist, // distance between cells
|
||||
nullptr, // bounding box of the area to fill
|
||||
out // output positions
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* arrange objects preserving their instance count
|
||||
but altering their instance positions */
|
||||
bool Model::arrange_objects(coordf_t dist, const BoundingBoxf* bb)
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto obj : objects) count += obj->instances.size();
|
||||
|
||||
arrangement::ArrangePolygons input;
|
||||
ModelInstancePtrs instances;
|
||||
input.reserve(count);
|
||||
instances.reserve(count);
|
||||
for (ModelObject *mo : objects)
|
||||
for (ModelInstance *minst : mo->instances) {
|
||||
input.emplace_back(minst->get_arrange_polygon());
|
||||
instances.emplace_back(minst);
|
||||
}
|
||||
|
||||
arrangement::BedShapeHint bedhint;
|
||||
coord_t bedwidth = 0;
|
||||
|
||||
if (bb) {
|
||||
bedwidth = scaled(bb->size().x());
|
||||
bedhint = arrangement::BedShapeHint(
|
||||
BoundingBox(scaled(bb->min), scaled(bb->max)));
|
||||
}
|
||||
|
||||
arrangement::arrange(input, scaled(dist), bedhint);
|
||||
|
||||
bool ret = true;
|
||||
coord_t stride = bedwidth + bedwidth / 5;
|
||||
|
||||
for(size_t i = 0; i < input.size(); ++i) {
|
||||
if (input[i].bed_idx != 0) ret = false;
|
||||
if (input[i].bed_idx >= 0) {
|
||||
input[i].translation += Vec2crd{input[i].bed_idx * stride, 0};
|
||||
instances[i]->apply_arrange_result(input[i].translation.cast<double>(),
|
||||
input[i].rotation);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Duplicate the entire model preserving instance relative positions.
|
||||
void Model::duplicate(size_t copies_num, coordf_t dist, const BoundingBoxf* bb)
|
||||
{
|
||||
Pointfs model_sizes(copies_num-1, to_2d(this->bounding_box().size()));
|
||||
Pointfs positions;
|
||||
if (! _arrange(model_sizes, dist, bb, positions))
|
||||
throw std::invalid_argument("Cannot duplicate part as the resulting objects would not fit on the print bed.\n");
|
||||
|
||||
// note that this will leave the object count unaltered
|
||||
|
||||
for (ModelObject *o : this->objects) {
|
||||
// make a copy of the pointers in order to avoid recursion when appending their copies
|
||||
ModelInstancePtrs instances = o->instances;
|
||||
for (const ModelInstance *i : instances) {
|
||||
for (const Vec2d &pos : positions) {
|
||||
ModelInstance *instance = o->add_instance(*i);
|
||||
instance->set_offset(instance->get_offset() + Vec3d(pos(0), pos(1), 0.0));
|
||||
}
|
||||
}
|
||||
o->invalidate_bounding_box();
|
||||
}
|
||||
}
|
||||
|
||||
/* this will append more instances to each object
|
||||
and then automatically rearrange everything */
|
||||
void Model::duplicate_objects(size_t copies_num, coordf_t dist, const BoundingBoxf* bb)
|
||||
{
|
||||
for (ModelObject *o : this->objects) {
|
||||
// make a copy of the pointers in order to avoid recursion when appending their copies
|
||||
ModelInstancePtrs instances = o->instances;
|
||||
for (const ModelInstance *i : instances)
|
||||
for (size_t k = 2; k <= copies_num; ++ k)
|
||||
o->add_instance(*i);
|
||||
}
|
||||
|
||||
this->arrange_objects(dist, bb);
|
||||
}
|
||||
|
||||
void Model::duplicate_objects_grid(size_t x, size_t y, coordf_t dist)
|
||||
{
|
||||
if (this->objects.size() > 1) throw "Grid duplication is not supported with multiple objects";
|
||||
@ -1991,6 +1882,7 @@ void check_model_ids_equal(const Model &model1, const Model &model2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* NDEBUG */
|
||||
|
||||
}
|
||||
|
@ -802,10 +802,8 @@ public:
|
||||
bool center_instances_around_point(const Vec2d &point);
|
||||
void translate(coordf_t x, coordf_t y, coordf_t z) { for (ModelObject *o : this->objects) o->translate(x, y, z); }
|
||||
TriangleMesh mesh() const;
|
||||
bool arrange_objects(coordf_t dist, const BoundingBoxf* bb = NULL);
|
||||
|
||||
// Croaks if the duplicated objects do not fit the print bed.
|
||||
void duplicate(size_t copies_num, coordf_t dist, const BoundingBoxf* bb = NULL);
|
||||
void duplicate_objects(size_t copies_num, coordf_t dist, const BoundingBoxf* bb = NULL);
|
||||
void duplicate_objects_grid(size_t x, size_t y, coordf_t dist);
|
||||
|
||||
bool looks_like_multipart_object() const;
|
||||
@ -822,6 +820,7 @@ public:
|
||||
std::string propose_export_file_name_and_path(const std::string &new_extension) const;
|
||||
|
||||
private:
|
||||
|
||||
explicit Model(int) : ObjectBase(-1) { assert(this->id().invalid()); };
|
||||
void assign_new_unique_ids_recursive();
|
||||
void update_links_bottom_up_recursive();
|
||||
|
83
src/libslic3r/ModelArrange.cpp
Normal file
83
src/libslic3r/ModelArrange.cpp
Normal file
@ -0,0 +1,83 @@
|
||||
#include "ModelArrange.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
arrangement::ArrangePolygons get_arrange_polys(const Model &model, ModelInstancePtrs &instances)
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto obj : model.objects) count += obj->instances.size();
|
||||
|
||||
ArrangePolygons input;
|
||||
input.reserve(count);
|
||||
instances.clear(); instances.reserve(count);
|
||||
for (ModelObject *mo : model.objects)
|
||||
for (ModelInstance *minst : mo->instances) {
|
||||
input.emplace_back(minst->get_arrange_polygon());
|
||||
instances.emplace_back(minst);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
bool apply_arrange_polys(ArrangePolygons &input, ModelInstancePtrs &instances, VirtualBedFn vfn)
|
||||
{
|
||||
bool ret = true;
|
||||
|
||||
for(size_t i = 0; i < input.size(); ++i) {
|
||||
if (input[i].bed_idx != 0) { ret = false; if (vfn) vfn(input[i]); }
|
||||
if (input[i].bed_idx >= 0)
|
||||
instances[i]->apply_arrange_result(input[i].translation.cast<double>(),
|
||||
input[i].rotation);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Slic3r::arrangement::ArrangePolygon get_arrange_poly(const Model &model)
|
||||
{
|
||||
ArrangePolygon ap;
|
||||
Points &apts = ap.poly.contour.points;
|
||||
for (const ModelObject *mo : model.objects)
|
||||
for (const ModelInstance *minst : mo->instances) {
|
||||
ArrangePolygon obj_ap = minst->get_arrange_polygon();
|
||||
ap.poly.contour.rotate(obj_ap.rotation);
|
||||
ap.poly.contour.translate(obj_ap.translation.x(), obj_ap.translation.y());
|
||||
const Points &pts = obj_ap.poly.contour.points;
|
||||
std::copy(pts.begin(), pts.end(), std::back_inserter(apts));
|
||||
}
|
||||
|
||||
apts = Geometry::convex_hull(apts);
|
||||
return ap;
|
||||
}
|
||||
|
||||
void duplicate(Model &model, Slic3r::arrangement::ArrangePolygons &copies, VirtualBedFn vfn)
|
||||
{
|
||||
for (ModelObject *o : model.objects) {
|
||||
// make a copy of the pointers in order to avoid recursion when appending their copies
|
||||
ModelInstancePtrs instances = o->instances;
|
||||
o->instances.clear();
|
||||
for (const ModelInstance *i : instances) {
|
||||
for (arrangement::ArrangePolygon &ap : copies) {
|
||||
if (ap.bed_idx != 0) vfn(ap);
|
||||
ModelInstance *instance = o->add_instance(*i);
|
||||
Vec2d pos = unscale(ap.translation);
|
||||
instance->set_offset(instance->get_offset() + to_3d(pos, 0.));
|
||||
}
|
||||
}
|
||||
o->invalidate_bounding_box();
|
||||
}
|
||||
}
|
||||
|
||||
void duplicate_objects(Model &model, size_t copies_num)
|
||||
{
|
||||
for (ModelObject *o : model.objects) {
|
||||
// make a copy of the pointers in order to avoid recursion when appending their copies
|
||||
ModelInstancePtrs instances = o->instances;
|
||||
for (const ModelInstance *i : instances)
|
||||
for (size_t k = 2; k <= copies_num; ++ k)
|
||||
o->add_instance(*i);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
68
src/libslic3r/ModelArrange.hpp
Normal file
68
src/libslic3r/ModelArrange.hpp
Normal file
@ -0,0 +1,68 @@
|
||||
#ifndef MODELARRANGE_HPP
|
||||
#define MODELARRANGE_HPP
|
||||
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/Arrange.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using arrangement::ArrangePolygon;
|
||||
using arrangement::ArrangePolygons;
|
||||
using arrangement::ArrangeParams;
|
||||
using arrangement::InfiniteBed;
|
||||
using arrangement::CircleBed;
|
||||
|
||||
// Do something with ArrangePolygons in virtual beds
|
||||
using VirtualBedFn = std::function<void(arrangement::ArrangePolygon&)>;
|
||||
|
||||
[[noreturn]] inline void throw_if_out_of_bed(arrangement::ArrangePolygon&)
|
||||
{
|
||||
throw std::runtime_error("Objects could not fit on the bed");
|
||||
}
|
||||
|
||||
ArrangePolygons get_arrange_polys(const Model &model, ModelInstancePtrs &instances);
|
||||
ArrangePolygon get_arrange_poly(const Model &model);
|
||||
bool apply_arrange_polys(ArrangePolygons &polys, ModelInstancePtrs &instances, VirtualBedFn);
|
||||
|
||||
void duplicate(Model &model, ArrangePolygons &copies, VirtualBedFn);
|
||||
void duplicate_objects(Model &model, size_t copies_num);
|
||||
|
||||
template<class TBed>
|
||||
bool arrange_objects(Model & model,
|
||||
const TBed & bed,
|
||||
const ArrangeParams ¶ms,
|
||||
VirtualBedFn vfn = throw_if_out_of_bed)
|
||||
{
|
||||
ModelInstancePtrs instances;
|
||||
auto&& input = get_arrange_polys(model, instances);
|
||||
arrangement::arrange(input, bed, params);
|
||||
|
||||
return apply_arrange_polys(input, instances, vfn);
|
||||
}
|
||||
|
||||
template<class TBed>
|
||||
void duplicate(Model & model,
|
||||
size_t copies_num,
|
||||
const TBed & bed,
|
||||
const ArrangeParams ¶ms,
|
||||
VirtualBedFn vfn = throw_if_out_of_bed)
|
||||
{
|
||||
ArrangePolygons copies(copies_num, get_arrange_poly(model));
|
||||
arrangement::arrange(copies, bed, params);
|
||||
duplicate(model, copies, vfn);
|
||||
}
|
||||
|
||||
template<class TBed>
|
||||
void duplicate_objects(Model & model,
|
||||
size_t copies_num,
|
||||
const TBed & bed,
|
||||
const ArrangeParams ¶ms,
|
||||
VirtualBedFn vfn = throw_if_out_of_bed)
|
||||
{
|
||||
duplicate_objects(model, copies_num);
|
||||
arrange_objects(model, bed, params, vfn);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // MODELARRANGE_HPP
|
@ -288,6 +288,72 @@ private:
|
||||
|
||||
std::ostream& operator<<(std::ostream &stm, const Vec2d &pointf);
|
||||
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Type safe conversions to and from scaled and unscaled coordinates
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Semantics are the following:
|
||||
// Upscaling (scaled()): only from floating point types (or Vec) to either
|
||||
// floating point or integer 'scaled coord' coordinates.
|
||||
// Downscaling (unscaled()): from arithmetic (or Vec) to floating point only
|
||||
|
||||
// Conversion definition from unscaled to floating point scaled
|
||||
template<class Tout,
|
||||
class Tin,
|
||||
class = FloatingOnly<Tin>>
|
||||
inline constexpr FloatingOnly<Tout> scaled(const Tin &v) noexcept
|
||||
{
|
||||
return Tout(v / Tin(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Conversion definition from unscaled to integer 'scaled coord'.
|
||||
// TODO: is the rounding necessary? Here it is commented out to show that
|
||||
// it can be different for integers but it does not have to be. Using
|
||||
// std::round means loosing noexcept and constexpr modifiers
|
||||
template<class Tout = coord_t, class Tin, class = FloatingOnly<Tin>>
|
||||
inline constexpr ScaledCoordOnly<Tout> scaled(const Tin &v) noexcept
|
||||
{
|
||||
//return static_cast<Tout>(std::round(v / SCALING_FACTOR));
|
||||
return Tout(v / Tin(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Conversion for Eigen vectors (N dimensional points)
|
||||
template<class Tout = coord_t,
|
||||
class Tin,
|
||||
int N,
|
||||
class = FloatingOnly<Tin>,
|
||||
int...EigenArgs>
|
||||
inline Eigen::Matrix<ArithmeticOnly<Tout>, N, EigenArgs...>
|
||||
scaled(const Eigen::Matrix<Tin, N, EigenArgs...> &v)
|
||||
{
|
||||
return (v / SCALING_FACTOR).template cast<Tout>();
|
||||
}
|
||||
|
||||
// Conversion from arithmetic scaled type to floating point unscaled
|
||||
template<class Tout = double,
|
||||
class Tin,
|
||||
class = ArithmeticOnly<Tin>,
|
||||
class = FloatingOnly<Tout>>
|
||||
inline constexpr Tout unscaled(const Tin &v) noexcept
|
||||
{
|
||||
return Tout(v * Tout(SCALING_FACTOR));
|
||||
}
|
||||
|
||||
// Unscaling for Eigen vectors. Input base type can be arithmetic, output base
|
||||
// type can only be floating point.
|
||||
template<class Tout = double,
|
||||
class Tin,
|
||||
int N,
|
||||
class = ArithmeticOnly<Tin>,
|
||||
class = FloatingOnly<Tout>,
|
||||
int...EigenArgs>
|
||||
inline constexpr Eigen::Matrix<Tout, N, EigenArgs...>
|
||||
unscaled(const Eigen::Matrix<Tin, N, EigenArgs...> &v) noexcept
|
||||
{
|
||||
return v.template cast<Tout>() * SCALING_FACTOR;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
// start Boost
|
||||
|
@ -48,7 +48,7 @@ int64_t Polygon::area2x() const
|
||||
}
|
||||
*/
|
||||
|
||||
double Polygon::area() const
|
||||
double Polygon::area(const Points &points)
|
||||
{
|
||||
size_t n = points.size();
|
||||
if (n < 3)
|
||||
@ -62,6 +62,11 @@ double Polygon::area() const
|
||||
return 0.5 * a;
|
||||
}
|
||||
|
||||
double Polygon::area() const
|
||||
{
|
||||
return Polygon::area(points);
|
||||
}
|
||||
|
||||
bool Polygon::is_counter_clockwise() const
|
||||
{
|
||||
return ClipperLib::Orientation(Slic3rMultiPoint_to_ClipperPath(*this));
|
||||
|
@ -22,6 +22,7 @@ public:
|
||||
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
|
||||
|
||||
Polygon() {}
|
||||
virtual ~Polygon() = default;
|
||||
explicit Polygon(const Points &points) : MultiPoint(points) {}
|
||||
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
|
||||
Polygon(const Polygon &other) : MultiPoint(other.points) {}
|
||||
@ -47,6 +48,7 @@ public:
|
||||
Polyline split_at_first_point() const { return this->split_at_index(0); }
|
||||
Points equally_spaced_points(double distance) const { return this->split_at_first_point().equally_spaced_points(distance); }
|
||||
|
||||
static double area(const Points &pts);
|
||||
double area() const;
|
||||
bool is_counter_clockwise() const;
|
||||
bool is_clockwise() const;
|
||||
|
@ -3060,6 +3060,42 @@ DynamicPrintConfig* DynamicPrintConfig::new_from_defaults_keys(const std::vector
|
||||
return out;
|
||||
}
|
||||
|
||||
double min_object_distance(const ConfigBase &cfg)
|
||||
{
|
||||
double ret = 0.;
|
||||
|
||||
if (printer_technology(cfg) == ptSLA) ret = 6.;
|
||||
else {
|
||||
auto ecr_opt = cfg.option<ConfigOptionFloat>("extruder_clearance_radius");
|
||||
auto dd_opt = cfg.option<ConfigOptionFloat>("duplicate_distance");
|
||||
auto co_opt = cfg.option<ConfigOptionBool>("complete_objects");
|
||||
|
||||
if (!ecr_opt || !dd_opt || !co_opt) ret = 0.;
|
||||
else {
|
||||
// min object distance is max(duplicate_distance, clearance_radius)
|
||||
ret = (co_opt->value && ecr_opt->value > dd_opt->value) ?
|
||||
ecr_opt->value : dd_opt->value;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
PrinterTechnology printer_technology(const ConfigBase &cfg)
|
||||
{
|
||||
const ConfigOptionEnum<PrinterTechnology> *opt = cfg.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology");
|
||||
|
||||
if (opt) return opt->value;
|
||||
|
||||
const ConfigOptionBool *export_opt = cfg.option<ConfigOptionBool>("export_sla");
|
||||
if (export_opt && export_opt->getBool()) return ptSLA;
|
||||
|
||||
export_opt = cfg.option<ConfigOptionBool>("export_gcode");
|
||||
if (export_opt && export_opt->getBool()) return ptFFF;
|
||||
|
||||
return ptUnknown;
|
||||
}
|
||||
|
||||
void DynamicPrintConfig::normalize()
|
||||
{
|
||||
if (this->has("extruder")) {
|
||||
@ -3130,22 +3166,6 @@ std::string DynamicPrintConfig::validate()
|
||||
}
|
||||
}
|
||||
|
||||
double PrintConfig::min_object_distance() const
|
||||
{
|
||||
return PrintConfig::min_object_distance(static_cast<const ConfigBase*>(this));
|
||||
}
|
||||
|
||||
double PrintConfig::min_object_distance(const ConfigBase *config)
|
||||
{
|
||||
double extruder_clearance_radius = config->option("extruder_clearance_radius")->getFloat();
|
||||
double duplicate_distance = config->option("duplicate_distance")->getFloat();
|
||||
|
||||
// min object distance is max(duplicate_distance, clearance_radius)
|
||||
return (config->option("complete_objects")->getBool() && extruder_clearance_radius > duplicate_distance)
|
||||
? extruder_clearance_radius
|
||||
: duplicate_distance;
|
||||
}
|
||||
|
||||
//FIXME localize this function.
|
||||
std::string FullPrintConfig::validate()
|
||||
{
|
||||
@ -3555,8 +3575,39 @@ void DynamicPrintAndCLIConfig::handle_legacy(t_config_option_key &opt_key, std::
|
||||
}
|
||||
}
|
||||
|
||||
static Points to_points(const std::vector<Vec2d> &dpts)
|
||||
{
|
||||
Points pts; pts.reserve(dpts.size());
|
||||
for (auto &v : dpts)
|
||||
pts.emplace_back( coord_t(scale_(v.x())), coord_t(scale_(v.y())) );
|
||||
return pts;
|
||||
}
|
||||
|
||||
Points get_bed_shape(const DynamicPrintConfig &config)
|
||||
{
|
||||
const auto *bed_shape_opt = config.opt<ConfigOptionPoints>("bed_shape");
|
||||
if (!bed_shape_opt) {
|
||||
|
||||
// Here, it is certain that the bed shape is missing, so an infinite one
|
||||
// has to be used, but still, the center of bed can be queried
|
||||
if (auto center_opt = config.opt<ConfigOptionPoint>("center"))
|
||||
return { scaled(center_opt->value) };
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return to_points(bed_shape_opt->values);
|
||||
}
|
||||
|
||||
Points get_bed_shape(const PrintConfig &cfg)
|
||||
{
|
||||
return to_points(cfg.bed_shape.values);
|
||||
}
|
||||
|
||||
Points get_bed_shape(const SLAPrinterConfig &cfg) { return to_points(cfg.bed_shape.values); }
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#include <cereal/types/polymorphic.hpp>
|
||||
CEREAL_REGISTER_TYPE(Slic3r::DynamicPrintConfig)
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(Slic3r::DynamicConfig, Slic3r::DynamicPrintConfig)
|
||||
|
@ -194,6 +194,9 @@ extern const PrintConfigDef print_config_def;
|
||||
|
||||
class StaticPrintConfig;
|
||||
|
||||
PrinterTechnology printer_technology(const ConfigBase &cfg);
|
||||
double min_object_distance(const ConfigBase &cfg);
|
||||
|
||||
// Slic3r dynamic configuration, used to override the configuration
|
||||
// per object, per modification volume or per printing material.
|
||||
// The dynamic configuration is also used to store user modifications of the print global parameters,
|
||||
@ -749,8 +752,6 @@ class PrintConfig : public MachineEnvelopeConfig, public GCodeConfig
|
||||
STATIC_PRINT_CONFIG_CACHE_DERIVED(PrintConfig)
|
||||
PrintConfig() : MachineEnvelopeConfig(0), GCodeConfig(0) { initialize_cache(); *this = s_cache_PrintConfig.defaults(); }
|
||||
public:
|
||||
double min_object_distance() const;
|
||||
static double min_object_distance(const ConfigBase *config);
|
||||
|
||||
ConfigOptionBool avoid_crossing_perimeters;
|
||||
ConfigOptionPoints bed_shape;
|
||||
@ -1305,6 +1306,10 @@ private:
|
||||
static PrintAndCLIConfigDef s_def;
|
||||
};
|
||||
|
||||
Points get_bed_shape(const DynamicPrintConfig &cfg);
|
||||
Points get_bed_shape(const PrintConfig &cfg);
|
||||
Points get_bed_shape(const SLAPrinterConfig &cfg);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
// Serialization through the Cereal library
|
||||
|
222
src/libslic3r/SLA/AGGRaster.hpp
Normal file
222
src/libslic3r/SLA/AGGRaster.hpp
Normal file
@ -0,0 +1,222 @@
|
||||
#ifndef AGGRASTER_HPP
|
||||
#define AGGRASTER_HPP
|
||||
|
||||
#include <libslic3r/SLA/RasterBase.hpp>
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include <libnest2d/backends/clipper/clipper_polygon.hpp>
|
||||
|
||||
// For rasterizing
|
||||
#include <agg/agg_basics.h>
|
||||
#include <agg/agg_rendering_buffer.h>
|
||||
#include <agg/agg_pixfmt_gray.h>
|
||||
#include <agg/agg_pixfmt_rgb.h>
|
||||
#include <agg/agg_renderer_base.h>
|
||||
#include <agg/agg_renderer_scanline.h>
|
||||
|
||||
#include <agg/agg_scanline_p.h>
|
||||
#include <agg/agg_rasterizer_scanline_aa.h>
|
||||
#include <agg/agg_path_storage.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
inline const Polygon& contour(const ExPolygon& p) { return p.contour; }
|
||||
inline const ClipperLib::Path& contour(const ClipperLib::Polygon& p) { return p.Contour; }
|
||||
|
||||
inline const Polygons& holes(const ExPolygon& p) { return p.holes; }
|
||||
inline const ClipperLib::Paths& holes(const ClipperLib::Polygon& p) { return p.Holes; }
|
||||
|
||||
namespace sla {
|
||||
|
||||
template<class Color> struct Colors {
|
||||
static const Color White;
|
||||
static const Color Black;
|
||||
};
|
||||
|
||||
template<class Color> const Color Colors<Color>::White = Color{255};
|
||||
template<class Color> const Color Colors<Color>::Black = Color{0};
|
||||
|
||||
template<class PixelRenderer,
|
||||
template<class /*agg::renderer_base<PixelRenderer>*/> class Renderer,
|
||||
class Rasterizer = agg::rasterizer_scanline_aa<>,
|
||||
class Scanline = agg::scanline_p8>
|
||||
class AGGRaster: public RasterBase {
|
||||
public:
|
||||
using TColor = typename PixelRenderer::color_type;
|
||||
using TValue = typename TColor::value_type;
|
||||
using TPixel = typename PixelRenderer::pixel_type;
|
||||
using TRawBuffer = agg::rendering_buffer;
|
||||
|
||||
protected:
|
||||
|
||||
Resolution m_resolution;
|
||||
PixelDim m_pxdim_scaled; // used for scaled coordinate polygons
|
||||
|
||||
std::vector<TPixel> m_buf;
|
||||
agg::rendering_buffer m_rbuf;
|
||||
|
||||
PixelRenderer m_pixrenderer;
|
||||
|
||||
agg::renderer_base<PixelRenderer> m_raw_renderer;
|
||||
Renderer<agg::renderer_base<PixelRenderer>> m_renderer;
|
||||
|
||||
Trafo m_trafo;
|
||||
Scanline m_scanlines;
|
||||
Rasterizer m_rasterizer;
|
||||
|
||||
void flipy(agg::path_storage &path) const
|
||||
{
|
||||
path.flip_y(0, double(m_resolution.height_px));
|
||||
}
|
||||
|
||||
void flipx(agg::path_storage &path) const
|
||||
{
|
||||
path.flip_x(0, double(m_resolution.width_px));
|
||||
}
|
||||
|
||||
double getPx(const Point &p) { return p(0) * m_pxdim_scaled.w_mm; }
|
||||
double getPy(const Point &p) { return p(1) * m_pxdim_scaled.h_mm; }
|
||||
agg::path_storage to_path(const Polygon &poly) { return to_path(poly.points); }
|
||||
double getPx(const ClipperLib::IntPoint &p) { return p.X * m_pxdim_scaled.w_mm; }
|
||||
double getPy(const ClipperLib::IntPoint& p) { return p.Y * m_pxdim_scaled.h_mm; }
|
||||
|
||||
template<class PointVec> agg::path_storage _to_path(const PointVec& v)
|
||||
{
|
||||
agg::path_storage path;
|
||||
|
||||
auto it = v.begin();
|
||||
path.move_to(getPx(*it), getPy(*it));
|
||||
while(++it != v.end()) path.line_to(getPx(*it), getPy(*it));
|
||||
path.line_to(getPx(v.front()), getPy(v.front()));
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
template<class PointVec> agg::path_storage _to_path_flpxy(const PointVec& v)
|
||||
{
|
||||
agg::path_storage path;
|
||||
|
||||
auto it = v.begin();
|
||||
path.move_to(getPy(*it), getPx(*it));
|
||||
while(++it != v.end()) path.line_to(getPy(*it), getPx(*it));
|
||||
path.line_to(getPy(v.front()), getPx(v.front()));
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
template<class PointVec> agg::path_storage to_path(const PointVec &v)
|
||||
{
|
||||
auto path = m_trafo.flipXY ? _to_path_flpxy(v) : _to_path(v);
|
||||
|
||||
path.translate_all_paths(m_trafo.center_x * m_pxdim_scaled.w_mm,
|
||||
m_trafo.center_y * m_pxdim_scaled.h_mm);
|
||||
|
||||
if(m_trafo.mirror_x) flipx(path);
|
||||
if(m_trafo.mirror_y) flipy(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
template<class P> void _draw(const P &poly)
|
||||
{
|
||||
m_rasterizer.reset();
|
||||
|
||||
m_rasterizer.add_path(to_path(contour(poly)));
|
||||
for(auto& h : holes(poly)) m_rasterizer.add_path(to_path(h));
|
||||
|
||||
agg::render_scanlines(m_rasterizer, m_scanlines, m_renderer);
|
||||
}
|
||||
|
||||
public:
|
||||
template<class GammaFn> AGGRaster(const Resolution &res,
|
||||
const PixelDim & pd,
|
||||
const Trafo & trafo,
|
||||
const TColor & foreground,
|
||||
const TColor & background,
|
||||
GammaFn && gammafn)
|
||||
: m_resolution(res)
|
||||
, m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm)
|
||||
, m_buf(res.pixels())
|
||||
, m_rbuf(reinterpret_cast<TValue *>(m_buf.data()),
|
||||
unsigned(res.width_px),
|
||||
unsigned(res.height_px),
|
||||
int(res.width_px *PixelRenderer::num_components))
|
||||
, m_pixrenderer(m_rbuf)
|
||||
, m_raw_renderer(m_pixrenderer)
|
||||
, m_renderer(m_raw_renderer)
|
||||
, m_trafo(trafo)
|
||||
{
|
||||
m_renderer.color(foreground);
|
||||
clear(background);
|
||||
|
||||
m_rasterizer.gamma(gammafn);
|
||||
}
|
||||
|
||||
Trafo trafo() const override { return m_trafo; }
|
||||
Resolution resolution() const override { return m_resolution; }
|
||||
PixelDim pixel_dimensions() const override
|
||||
{
|
||||
return {SCALING_FACTOR / m_pxdim_scaled.w_mm,
|
||||
SCALING_FACTOR / m_pxdim_scaled.h_mm};
|
||||
}
|
||||
|
||||
void draw(const ExPolygon &poly) override { _draw(poly); }
|
||||
void draw(const ClipperLib::Polygon &poly) override { _draw(poly); }
|
||||
|
||||
EncodedRaster encode(RasterEncoder encoder) const override
|
||||
{
|
||||
return encoder(m_buf.data(), m_resolution.width_px, m_resolution.height_px, 1);
|
||||
}
|
||||
|
||||
void clear(const TColor color) { m_raw_renderer.clear(color); }
|
||||
};
|
||||
|
||||
/*
|
||||
* Captures an anti-aliased monochrome canvas where vectorial
|
||||
* polygons can be rasterized. Fill color is always white and the background is
|
||||
* black. Contours are anti-aliased.
|
||||
*
|
||||
* A gamma function can be specified at compile time to make it more flexible.
|
||||
*/
|
||||
using _RasterGrayscaleAA =
|
||||
AGGRaster<agg::pixfmt_gray8, agg::renderer_scanline_aa_solid>;
|
||||
|
||||
class RasterGrayscaleAA : public _RasterGrayscaleAA {
|
||||
using Base = _RasterGrayscaleAA;
|
||||
using typename Base::TColor;
|
||||
using typename Base::TValue;
|
||||
public:
|
||||
template<class GammaFn>
|
||||
RasterGrayscaleAA(const RasterBase::Resolution &res,
|
||||
const RasterBase::PixelDim & pd,
|
||||
const RasterBase::Trafo & trafo,
|
||||
GammaFn && fn)
|
||||
: Base(res, pd, trafo, Colors<TColor>::White, Colors<TColor>::Black,
|
||||
std::forward<GammaFn>(fn))
|
||||
{}
|
||||
|
||||
uint8_t read_pixel(size_t col, size_t row) const
|
||||
{
|
||||
static_assert(std::is_same<TValue, uint8_t>::value, "Not grayscale pix");
|
||||
|
||||
uint8_t px;
|
||||
Base::m_buf[row * Base::resolution().width_px + col].get(px);
|
||||
return px;
|
||||
}
|
||||
|
||||
void clear() { Base::clear(Colors<TColor>::Black); }
|
||||
};
|
||||
|
||||
class RasterGrayscaleAAGammaPower: public RasterGrayscaleAA {
|
||||
public:
|
||||
RasterGrayscaleAAGammaPower(const RasterBase::Resolution &res,
|
||||
const RasterBase::PixelDim & pd,
|
||||
const RasterBase::Trafo & trafo,
|
||||
double gamma = 1.)
|
||||
: RasterGrayscaleAA(res, pd, trafo, agg::gamma_power(gamma))
|
||||
{}
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::sla
|
||||
|
||||
#endif // AGGRASTER_HPP
|
@ -11,6 +11,8 @@
|
||||
#include "Tesselate.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
|
||||
#include "TriangulateWall.hpp"
|
||||
|
||||
// For debugging:
|
||||
// #include <fstream>
|
||||
// #include <libnest2d/tools/benchmark.h>
|
||||
@ -27,175 +29,17 @@ namespace Slic3r { namespace sla {
|
||||
|
||||
namespace {
|
||||
|
||||
/// This function will return a triangulation of a sheet connecting an upper
|
||||
/// and a lower plate given as input polygons. It will not triangulate the
|
||||
/// plates themselves only the sheet. The caller has to specify the lower and
|
||||
/// upper z levels in world coordinates as well as the offset difference
|
||||
/// between the sheets. If the lower_z_mm is higher than upper_z_mm or the
|
||||
/// offset difference is negative, the resulting triangle orientation will be
|
||||
/// reversed.
|
||||
///
|
||||
/// IMPORTANT: This is not a universal triangulation algorithm. It assumes
|
||||
/// that the lower and upper polygons are offsetted versions of the same
|
||||
/// original polygon. In general, it assumes that one of the polygons is
|
||||
/// completely inside the other. The offset difference is the reference
|
||||
/// distance from the inner polygon's perimeter to the outer polygon's
|
||||
/// perimeter. The real distance will be variable as the clipper offset has
|
||||
/// different strategies (rounding, etc...). This algorithm should have
|
||||
/// O(2n + 3m) complexity where n is the number of upper vertices and m is the
|
||||
/// number of lower vertices.
|
||||
Contour3D walls(
|
||||
const Polygon &lower,
|
||||
const Polygon &upper,
|
||||
double lower_z_mm,
|
||||
double upper_z_mm,
|
||||
double offset_difference_mm,
|
||||
ThrowOnCancel thr = [] {})
|
||||
double upper_z_mm)
|
||||
{
|
||||
Wall w = triangulate_wall(lower, upper, lower_z_mm, upper_z_mm);
|
||||
|
||||
Contour3D ret;
|
||||
|
||||
if(upper.points.size() < 3 || lower.size() < 3) return ret;
|
||||
|
||||
// The concept of the algorithm is relatively simple. It will try to find
|
||||
// the closest vertices from the upper and the lower polygon and use those
|
||||
// as starting points. Then it will create the triangles sequentially using
|
||||
// an edge from the upper polygon and a vertex from the lower or vice versa,
|
||||
// depending on the resulting triangle's quality.
|
||||
// The quality is measured by a scalar value. So far it looks like it is
|
||||
// enough to derive it from the slope of the triangle's two edges connecting
|
||||
// the upper and the lower part. A reference slope is calculated from the
|
||||
// height and the offset difference.
|
||||
|
||||
// Offset in the index array for the ceiling
|
||||
const auto offs = upper.points.size();
|
||||
|
||||
// Shorthand for the vertex arrays
|
||||
auto& upts = upper.points, &lpts = lower.points;
|
||||
auto& rpts = ret.points; auto& ind = ret.faces3;
|
||||
|
||||
// If the Z levels are flipped, or the offset difference is negative, we
|
||||
// will interpret that as the triangles normals should be inverted.
|
||||
bool inverted = upper_z_mm < lower_z_mm || offset_difference_mm < 0;
|
||||
|
||||
// Copy the points into the mesh, convert them from 2D to 3D
|
||||
rpts.reserve(upts.size() + lpts.size());
|
||||
ind.reserve(2 * upts.size() + 2 * lpts.size());
|
||||
for (auto &p : upts)
|
||||
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm);
|
||||
for (auto &p : lpts)
|
||||
rpts.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm);
|
||||
|
||||
// Create pointing indices into vertex arrays. u-upper, l-lower
|
||||
size_t uidx = 0, lidx = offs, unextidx = 1, lnextidx = offs + 1;
|
||||
|
||||
// Simple squared distance calculation.
|
||||
auto distfn = [](const Vec3d& p1, const Vec3d& p2) {
|
||||
auto p = p1 - p2; return p.transpose() * p;
|
||||
};
|
||||
|
||||
// We need to find the closest point on lower polygon to the first point on
|
||||
// the upper polygon. These will be our starting points.
|
||||
double distmin = std::numeric_limits<double>::max();
|
||||
for(size_t l = lidx; l < rpts.size(); ++l) {
|
||||
thr();
|
||||
double d = distfn(rpts[l], rpts[uidx]);
|
||||
if(d < distmin) { lidx = l; distmin = d; }
|
||||
}
|
||||
|
||||
// Set up lnextidx to be ahead of lidx in cyclic mode
|
||||
lnextidx = lidx + 1;
|
||||
if(lnextidx == rpts.size()) lnextidx = offs;
|
||||
|
||||
// This will be the flip switch to toggle between upper and lower triangle
|
||||
// creation mode
|
||||
enum class Proceed {
|
||||
UPPER, // A segment from the upper polygon and one vertex from the lower
|
||||
LOWER // A segment from the lower polygon and one vertex from the upper
|
||||
} proceed = Proceed::UPPER;
|
||||
|
||||
// Flags to help evaluating loop termination.
|
||||
bool ustarted = false, lstarted = false;
|
||||
|
||||
// The variables for the fitness values, one for the actual and one for the
|
||||
// previous.
|
||||
double current_fit = 0, prev_fit = 0;
|
||||
|
||||
// Every triangle of the wall has two edges connecting the upper plate with
|
||||
// the lower plate. From the length of these two edges and the zdiff we
|
||||
// can calculate the momentary squared offset distance at a particular
|
||||
// position on the wall. The average of the differences from the reference
|
||||
// (squared) offset distance will give us the driving fitness value.
|
||||
const double offsdiff2 = std::pow(offset_difference_mm, 2);
|
||||
const double zdiff2 = std::pow(upper_z_mm - lower_z_mm, 2);
|
||||
|
||||
// Mark the current vertex iterator positions. If the iterators return to
|
||||
// the same position, the loop can be terminated.
|
||||
size_t uendidx = uidx, lendidx = lidx;
|
||||
|
||||
do { thr(); // check throw if canceled
|
||||
|
||||
prev_fit = current_fit;
|
||||
|
||||
switch(proceed) { // proceed depending on the current state
|
||||
case Proceed::UPPER:
|
||||
if(!ustarted || uidx != uendidx) { // there are vertices remaining
|
||||
// Get the 3D vertices in order
|
||||
const Vec3d& p_up1 = rpts[uidx];
|
||||
const Vec3d& p_low = rpts[lidx];
|
||||
const Vec3d& p_up2 = rpts[unextidx];
|
||||
|
||||
// Calculate fitness: the average of the two connecting edges
|
||||
double a = offsdiff2 - (distfn(p_up1, p_low) - zdiff2);
|
||||
double b = offsdiff2 - (distfn(p_up2, p_low) - zdiff2);
|
||||
current_fit = (std::abs(a) + std::abs(b)) / 2;
|
||||
|
||||
if(current_fit > prev_fit) { // fit is worse than previously
|
||||
proceed = Proceed::LOWER;
|
||||
} else { // good to go, create the triangle
|
||||
inverted
|
||||
? ind.emplace_back(int(unextidx), int(lidx), int(uidx))
|
||||
: ind.emplace_back(int(uidx), int(lidx), int(unextidx));
|
||||
|
||||
// Increment the iterators, rotate if necessary
|
||||
++uidx; ++unextidx;
|
||||
if(unextidx == offs) unextidx = 0;
|
||||
if(uidx == offs) uidx = 0;
|
||||
|
||||
ustarted = true; // mark the movement of the iterators
|
||||
// so that the comparison to uendidx can be made correctly
|
||||
}
|
||||
} else proceed = Proceed::LOWER;
|
||||
|
||||
break;
|
||||
case Proceed::LOWER:
|
||||
// Mode with lower segment, upper vertex. Same structure:
|
||||
if(!lstarted || lidx != lendidx) {
|
||||
const Vec3d& p_low1 = rpts[lidx];
|
||||
const Vec3d& p_low2 = rpts[lnextidx];
|
||||
const Vec3d& p_up = rpts[uidx];
|
||||
|
||||
double a = offsdiff2 - (distfn(p_up, p_low1) - zdiff2);
|
||||
double b = offsdiff2 - (distfn(p_up, p_low2) - zdiff2);
|
||||
current_fit = (std::abs(a) + std::abs(b)) / 2;
|
||||
|
||||
if(current_fit > prev_fit) {
|
||||
proceed = Proceed::UPPER;
|
||||
} else {
|
||||
inverted
|
||||
? ind.emplace_back(int(uidx), int(lnextidx), int(lidx))
|
||||
: ind.emplace_back(int(lidx), int(lnextidx), int(uidx));
|
||||
|
||||
++lidx; ++lnextidx;
|
||||
if(lnextidx == rpts.size()) lnextidx = offs;
|
||||
if(lidx == rpts.size()) lidx = offs;
|
||||
|
||||
lstarted = true;
|
||||
}
|
||||
} else proceed = Proceed::UPPER;
|
||||
|
||||
break;
|
||||
} // end of switch
|
||||
} while(!ustarted || !lstarted || uidx != uendidx || lidx != lendidx);
|
||||
ret.points = std::move(w.first);
|
||||
ret.faces3 = std::move(w.second);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@ -203,10 +47,9 @@ Contour3D walls(
|
||||
// Same as walls() but with identical higher and lower polygons.
|
||||
Contour3D inline straight_walls(const Polygon &plate,
|
||||
double lo_z,
|
||||
double hi_z,
|
||||
ThrowOnCancel thr)
|
||||
double hi_z)
|
||||
{
|
||||
return walls(plate, plate, lo_z, hi_z, .0 /*offset_diff*/, thr);
|
||||
return walls(plate, plate, lo_z, hi_z);
|
||||
}
|
||||
|
||||
// Function to cut tiny connector cavities for a given polygon. The input poly
|
||||
@ -534,10 +377,8 @@ bool add_cavity(Contour3D &pad, ExPolygon &top_poly, const PadConfig3D &cfg,
|
||||
top_poly = pdiff.front();
|
||||
|
||||
double z_min = -cfg.wing_height, z_max = 0;
|
||||
double offset_difference = -wing_distance;
|
||||
pad.merge(walls(inner_base.contour, middle_base.contour, z_min, z_max,
|
||||
offset_difference, thr));
|
||||
|
||||
pad.merge(walls(inner_base.contour, middle_base.contour, z_min, z_max));
|
||||
thr();
|
||||
pad.merge(triangulate_expolygon_3d(inner_base, z_min, NORMALS_UP));
|
||||
|
||||
return true;
|
||||
@ -555,16 +396,16 @@ Contour3D create_outer_pad_geometry(const ExPolygons & skeleton,
|
||||
offset_contour_only(pad_part, -scaled(cfg.bottom_offset()));
|
||||
|
||||
if (bottom_poly.empty()) continue;
|
||||
thr();
|
||||
|
||||
double z_min = -cfg.height, z_max = 0;
|
||||
ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min,
|
||||
cfg.bottom_offset(), thr));
|
||||
ret.merge(walls(top_poly.contour, bottom_poly.contour, z_max, z_min));
|
||||
|
||||
if (cfg.wing_height > 0. && add_cavity(ret, top_poly, cfg, thr))
|
||||
z_max = -cfg.wing_height;
|
||||
|
||||
for (auto &h : bottom_poly.holes)
|
||||
ret.merge(straight_walls(h, z_max, z_min, thr));
|
||||
ret.merge(straight_walls(h, z_max, z_min));
|
||||
|
||||
ret.merge(triangulate_expolygon_3d(bottom_poly, z_min, NORMALS_DOWN));
|
||||
ret.merge(triangulate_expolygon_3d(top_poly, NORMALS_UP));
|
||||
@ -581,10 +422,11 @@ Contour3D create_inner_pad_geometry(const ExPolygons & skeleton,
|
||||
|
||||
double z_max = 0., z_min = -cfg.height;
|
||||
for (const ExPolygon &pad_part : skeleton) {
|
||||
ret.merge(straight_walls(pad_part.contour, z_max, z_min,thr));
|
||||
thr();
|
||||
ret.merge(straight_walls(pad_part.contour, z_max, z_min));
|
||||
|
||||
for (auto &h : pad_part.holes)
|
||||
ret.merge(straight_walls(h, z_max, z_min, thr));
|
||||
ret.merge(straight_walls(h, z_max, z_min));
|
||||
|
||||
ret.merge(triangulate_expolygon_3d(pad_part, z_min, NORMALS_DOWN));
|
||||
ret.merge(triangulate_expolygon_3d(pad_part, z_max, NORMALS_UP));
|
||||
|
@ -1,320 +0,0 @@
|
||||
#ifndef SLARASTER_CPP
|
||||
#define SLARASTER_CPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <libslic3r/SLA/Raster.hpp>
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include <libnest2d/backends/clipper/clipper_polygon.hpp>
|
||||
|
||||
// For rasterizing
|
||||
#include <agg/agg_basics.h>
|
||||
#include <agg/agg_rendering_buffer.h>
|
||||
#include <agg/agg_pixfmt_gray.h>
|
||||
#include <agg/agg_pixfmt_rgb.h>
|
||||
#include <agg/agg_renderer_base.h>
|
||||
#include <agg/agg_renderer_scanline.h>
|
||||
|
||||
#include <agg/agg_scanline_p.h>
|
||||
#include <agg/agg_rasterizer_scanline_aa.h>
|
||||
#include <agg/agg_path_storage.h>
|
||||
|
||||
// Experimental minz image write:
|
||||
#include <miniz.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
inline const Polygon& contour(const ExPolygon& p) { return p.contour; }
|
||||
inline const ClipperLib::Path& contour(const ClipperLib::Polygon& p) { return p.Contour; }
|
||||
|
||||
inline const Polygons& holes(const ExPolygon& p) { return p.holes; }
|
||||
inline const ClipperLib::Paths& holes(const ClipperLib::Polygon& p) { return p.Holes; }
|
||||
|
||||
namespace sla {
|
||||
|
||||
const Raster::TMirroring Raster::NoMirror = {false, false};
|
||||
const Raster::TMirroring Raster::MirrorX = {true, false};
|
||||
const Raster::TMirroring Raster::MirrorY = {false, true};
|
||||
const Raster::TMirroring Raster::MirrorXY = {true, true};
|
||||
|
||||
|
||||
using TPixelRenderer = agg::pixfmt_gray8; // agg::pixfmt_rgb24;
|
||||
using TRawRenderer = agg::renderer_base<TPixelRenderer>;
|
||||
using TPixel = TPixelRenderer::color_type;
|
||||
using TRawBuffer = agg::rendering_buffer;
|
||||
using TBuffer = std::vector<TPixelRenderer::pixel_type>;
|
||||
|
||||
using TRendererAA = agg::renderer_scanline_aa_solid<TRawRenderer>;
|
||||
|
||||
class Raster::Impl {
|
||||
public:
|
||||
|
||||
static const TPixel ColorWhite;
|
||||
static const TPixel ColorBlack;
|
||||
|
||||
using Format = Raster::RawData;
|
||||
|
||||
private:
|
||||
Raster::Resolution m_resolution;
|
||||
Raster::PixelDim m_pxdim_scaled; // used for scaled coordinate polygons
|
||||
TBuffer m_buf;
|
||||
TRawBuffer m_rbuf;
|
||||
TPixelRenderer m_pixfmt;
|
||||
TRawRenderer m_raw_renderer;
|
||||
TRendererAA m_renderer;
|
||||
|
||||
std::function<double(double)> m_gammafn;
|
||||
Trafo m_trafo;
|
||||
|
||||
inline void flipy(agg::path_storage& path) const {
|
||||
path.flip_y(0, double(m_resolution.height_px));
|
||||
}
|
||||
|
||||
inline void flipx(agg::path_storage& path) const {
|
||||
path.flip_x(0, double(m_resolution.width_px));
|
||||
}
|
||||
|
||||
public:
|
||||
inline Impl(const Raster::Resolution & res,
|
||||
const Raster::PixelDim & pd,
|
||||
const Trafo &trafo)
|
||||
: m_resolution(res)
|
||||
, m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm)
|
||||
, m_buf(res.pixels())
|
||||
, m_rbuf(reinterpret_cast<TPixelRenderer::value_type *>(m_buf.data()),
|
||||
unsigned(res.width_px),
|
||||
unsigned(res.height_px),
|
||||
int(res.width_px * TPixelRenderer::num_components))
|
||||
, m_pixfmt(m_rbuf)
|
||||
, m_raw_renderer(m_pixfmt)
|
||||
, m_renderer(m_raw_renderer)
|
||||
, m_trafo(trafo)
|
||||
{
|
||||
m_renderer.color(ColorWhite);
|
||||
|
||||
if (trafo.gamma > 0) m_gammafn = agg::gamma_power(trafo.gamma);
|
||||
else m_gammafn = agg::gamma_threshold(0.5);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
template<class P> void draw(const P &poly) {
|
||||
agg::rasterizer_scanline_aa<> ras;
|
||||
agg::scanline_p8 scanlines;
|
||||
|
||||
ras.gamma(m_gammafn);
|
||||
|
||||
ras.add_path(to_path(contour(poly)));
|
||||
for(auto& h : holes(poly)) ras.add_path(to_path(h));
|
||||
|
||||
agg::render_scanlines(ras, scanlines, m_renderer);
|
||||
}
|
||||
|
||||
inline void clear() {
|
||||
m_raw_renderer.clear(ColorBlack);
|
||||
}
|
||||
|
||||
inline TBuffer& buffer() { return m_buf; }
|
||||
inline const TBuffer& buffer() const { return m_buf; }
|
||||
|
||||
|
||||
inline const Raster::Resolution resolution() { return m_resolution; }
|
||||
inline const Raster::PixelDim pixdim()
|
||||
{
|
||||
return {SCALING_FACTOR / m_pxdim_scaled.w_mm,
|
||||
SCALING_FACTOR / m_pxdim_scaled.h_mm};
|
||||
}
|
||||
|
||||
private:
|
||||
inline double getPx(const Point& p) {
|
||||
return p(0) * m_pxdim_scaled.w_mm;
|
||||
}
|
||||
|
||||
inline double getPy(const Point& p) {
|
||||
return p(1) * m_pxdim_scaled.h_mm;
|
||||
}
|
||||
|
||||
inline agg::path_storage to_path(const Polygon& poly)
|
||||
{
|
||||
return to_path(poly.points);
|
||||
}
|
||||
|
||||
inline double getPx(const ClipperLib::IntPoint& p) {
|
||||
return p.X * m_pxdim_scaled.w_mm;
|
||||
}
|
||||
|
||||
inline double getPy(const ClipperLib::IntPoint& p) {
|
||||
return p.Y * m_pxdim_scaled.h_mm;
|
||||
}
|
||||
|
||||
template<class PointVec> agg::path_storage _to_path(const PointVec& v)
|
||||
{
|
||||
agg::path_storage path;
|
||||
|
||||
auto it = v.begin();
|
||||
path.move_to(getPx(*it), getPy(*it));
|
||||
while(++it != v.end()) path.line_to(getPx(*it), getPy(*it));
|
||||
path.line_to(getPx(v.front()), getPy(v.front()));
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
template<class PointVec> agg::path_storage _to_path_flpxy(const PointVec& v)
|
||||
{
|
||||
agg::path_storage path;
|
||||
|
||||
auto it = v.begin();
|
||||
path.move_to(getPy(*it), getPx(*it));
|
||||
while(++it != v.end()) path.line_to(getPy(*it), getPx(*it));
|
||||
path.line_to(getPy(v.front()), getPx(v.front()));
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
template<class PointVec> agg::path_storage to_path(const PointVec &v)
|
||||
{
|
||||
auto path = m_trafo.flipXY ? _to_path_flpxy(v) : _to_path(v);
|
||||
|
||||
path.translate_all_paths(m_trafo.origin_x * m_pxdim_scaled.w_mm,
|
||||
m_trafo.origin_y * m_pxdim_scaled.h_mm);
|
||||
|
||||
if(m_trafo.mirror_x) flipx(path);
|
||||
if(m_trafo.mirror_y) flipy(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const TPixel Raster::Impl::ColorWhite = TPixel(255);
|
||||
const TPixel Raster::Impl::ColorBlack = TPixel(0);
|
||||
|
||||
Raster::Raster() { reset(); }
|
||||
|
||||
Raster::Raster(const Raster::Resolution &r,
|
||||
const Raster::PixelDim & pd,
|
||||
const Raster::Trafo & tr)
|
||||
{
|
||||
reset(r, pd, tr);
|
||||
}
|
||||
|
||||
Raster::~Raster() = default;
|
||||
|
||||
Raster::Raster(Raster &&m) = default;
|
||||
Raster &Raster::operator=(Raster &&) = default;
|
||||
|
||||
void Raster::reset(const Raster::Resolution &r, const Raster::PixelDim &pd,
|
||||
const Trafo &trafo)
|
||||
{
|
||||
m_impl.reset();
|
||||
m_impl.reset(new Impl(r, pd, trafo));
|
||||
}
|
||||
|
||||
void Raster::reset()
|
||||
{
|
||||
m_impl.reset();
|
||||
}
|
||||
|
||||
Raster::Resolution Raster::resolution() const
|
||||
{
|
||||
if (m_impl) return m_impl->resolution();
|
||||
|
||||
return Resolution{0, 0};
|
||||
}
|
||||
|
||||
Raster::PixelDim Raster::pixel_dimensions() const
|
||||
{
|
||||
if (m_impl) return m_impl->pixdim();
|
||||
|
||||
return PixelDim{0., 0.};
|
||||
}
|
||||
|
||||
void Raster::clear()
|
||||
{
|
||||
assert(m_impl);
|
||||
m_impl->clear();
|
||||
}
|
||||
|
||||
void Raster::draw(const ExPolygon &expoly)
|
||||
{
|
||||
assert(m_impl);
|
||||
m_impl->draw(expoly);
|
||||
}
|
||||
|
||||
void Raster::draw(const ClipperLib::Polygon &poly)
|
||||
{
|
||||
assert(m_impl);
|
||||
m_impl->draw(poly);
|
||||
}
|
||||
|
||||
uint8_t Raster::read_pixel(size_t x, size_t y) const
|
||||
{
|
||||
assert (m_impl);
|
||||
TPixel::value_type px;
|
||||
m_impl->buffer()[y * resolution().width_px + x].get(px);
|
||||
return px;
|
||||
}
|
||||
|
||||
PNGImage & PNGImage::serialize(const Raster &raster)
|
||||
{
|
||||
size_t s = 0;
|
||||
m_buffer.clear();
|
||||
|
||||
void *rawdata = tdefl_write_image_to_png_file_in_memory(
|
||||
get_internals(raster).buffer().data(),
|
||||
int(raster.resolution().width_px),
|
||||
int(raster.resolution().height_px), 1, &s);
|
||||
|
||||
// On error, data() will return an empty vector. No other info can be
|
||||
// retrieved from miniz anyway...
|
||||
if (rawdata == nullptr) return *this;
|
||||
|
||||
auto ptr = static_cast<std::uint8_t*>(rawdata);
|
||||
|
||||
m_buffer.reserve(s);
|
||||
std::copy(ptr, ptr + s, std::back_inserter(m_buffer));
|
||||
|
||||
MZ_FREE(rawdata);
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &stream, const Raster::RawData &bytes)
|
||||
{
|
||||
stream.write(reinterpret_cast<const char *>(bytes.data()),
|
||||
std::streamsize(bytes.size()));
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
Raster::RawData::~RawData() = default;
|
||||
|
||||
PPMImage & PPMImage::serialize(const Raster &raster)
|
||||
{
|
||||
auto header = std::string("P5 ") +
|
||||
std::to_string(raster.resolution().width_px) + " " +
|
||||
std::to_string(raster.resolution().height_px) + " " + "255 ";
|
||||
|
||||
const auto &impl = get_internals(raster);
|
||||
auto sz = impl.buffer().size() * sizeof(TBuffer::value_type);
|
||||
size_t s = sz + header.size();
|
||||
|
||||
m_buffer.clear();
|
||||
m_buffer.reserve(s);
|
||||
|
||||
auto buff = reinterpret_cast<const std::uint8_t*>(impl.buffer().data());
|
||||
std::copy(header.begin(), header.end(), std::back_inserter(m_buffer));
|
||||
std::copy(buff, buff+sz, std::back_inserter(m_buffer));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
const Raster::Impl &Raster::RawData::get_internals(const Raster &raster)
|
||||
{
|
||||
return *raster.m_impl;
|
||||
}
|
||||
|
||||
} // namespace sla
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SLARASTER_CPP
|
@ -1,157 +0,0 @@
|
||||
#ifndef SLA_RASTER_HPP
|
||||
#define SLA_RASTER_HPP
|
||||
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
namespace ClipperLib { struct Polygon; }
|
||||
|
||||
namespace Slic3r {
|
||||
namespace sla {
|
||||
|
||||
/**
|
||||
* @brief Raster captures an anti-aliased monochrome canvas where vectorial
|
||||
* polygons can be rasterized. Fill color is always white and the background is
|
||||
* black. Contours are anti-aliased.
|
||||
*
|
||||
* It also supports saving the raster data into a standard output stream in raw
|
||||
* or PNG format.
|
||||
*/
|
||||
class Raster {
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
public:
|
||||
|
||||
// Raw byte buffer paired with its size. Suitable for compressed image data.
|
||||
class RawData
|
||||
{
|
||||
protected:
|
||||
std::vector<std::uint8_t> m_buffer;
|
||||
const Impl& get_internals(const Raster& raster);
|
||||
public:
|
||||
RawData() = default;
|
||||
RawData(std::vector<std::uint8_t>&& data): m_buffer(std::move(data)) {}
|
||||
virtual ~RawData();
|
||||
|
||||
RawData(const RawData &) = delete;
|
||||
RawData &operator=(const RawData &) = delete;
|
||||
|
||||
RawData(RawData &&) = default;
|
||||
RawData &operator=(RawData &&) = default;
|
||||
|
||||
size_t size() const { return m_buffer.size(); }
|
||||
const uint8_t * data() const { return m_buffer.data(); }
|
||||
|
||||
virtual RawData& serialize(const Raster &/*raster*/) { return *this; }
|
||||
virtual std::string get_file_extension() const = 0;
|
||||
};
|
||||
|
||||
/// Type that represents a resolution in pixels.
|
||||
struct Resolution {
|
||||
size_t width_px;
|
||||
size_t height_px;
|
||||
|
||||
inline Resolution(size_t w = 0, size_t h = 0)
|
||||
: width_px(w), height_px(h)
|
||||
{}
|
||||
|
||||
inline size_t pixels() const { return width_px * height_px; }
|
||||
};
|
||||
|
||||
/// Types that represents the dimension of a pixel in millimeters.
|
||||
struct PixelDim {
|
||||
double w_mm;
|
||||
double h_mm;
|
||||
inline PixelDim(double px_width_mm = 0.0, double px_height_mm = 0.0):
|
||||
w_mm(px_width_mm), h_mm(px_height_mm) {}
|
||||
};
|
||||
|
||||
enum Orientation { roLandscape, roPortrait };
|
||||
|
||||
using TMirroring = std::array<bool, 2>;
|
||||
static const TMirroring NoMirror;
|
||||
static const TMirroring MirrorX;
|
||||
static const TMirroring MirrorY;
|
||||
static const TMirroring MirrorXY;
|
||||
|
||||
struct Trafo {
|
||||
bool mirror_x = false, mirror_y = false, flipXY = false;
|
||||
coord_t origin_x = 0, origin_y = 0;
|
||||
|
||||
// If gamma is zero, thresholding will be performed which disables AA.
|
||||
double gamma = 1.;
|
||||
|
||||
// Portrait orientation will make sure the drawed polygons are rotated
|
||||
// by 90 degrees.
|
||||
Trafo(Orientation o = roLandscape, const TMirroring &mirror = NoMirror)
|
||||
// XY flipping implicitly does an X mirror
|
||||
: mirror_x(o == roPortrait ? !mirror[0] : mirror[0])
|
||||
, mirror_y(!mirror[1]) // Makes raster origin to be top left corner
|
||||
, flipXY(o == roPortrait)
|
||||
{}
|
||||
};
|
||||
|
||||
Raster();
|
||||
Raster(const Resolution &r,
|
||||
const PixelDim & pd,
|
||||
const Trafo & tr = {});
|
||||
|
||||
Raster(const Raster& cpy) = delete;
|
||||
Raster& operator=(const Raster& cpy) = delete;
|
||||
Raster(Raster&& m);
|
||||
Raster& operator=(Raster&&);
|
||||
~Raster();
|
||||
|
||||
/// Reallocated everything for the given resolution and pixel dimension.
|
||||
void reset(const Resolution& r,
|
||||
const PixelDim& pd,
|
||||
const Trafo &tr = {});
|
||||
|
||||
/**
|
||||
* Release the allocated resources. Drawing in this state ends in
|
||||
* unspecified behavior.
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/// Get the resolution of the raster.
|
||||
Resolution resolution() const;
|
||||
PixelDim pixel_dimensions() const;
|
||||
|
||||
/// Clear the raster with black color.
|
||||
void clear();
|
||||
|
||||
/// Draw a polygon with holes.
|
||||
void draw(const ExPolygon& poly);
|
||||
void draw(const ClipperLib::Polygon& poly);
|
||||
|
||||
uint8_t read_pixel(size_t w, size_t h) const;
|
||||
|
||||
inline bool empty() const { return ! bool(m_impl); }
|
||||
|
||||
};
|
||||
|
||||
class PNGImage: public Raster::RawData {
|
||||
public:
|
||||
PNGImage& serialize(const Raster &raster) override;
|
||||
std::string get_file_extension() const override { return "png"; }
|
||||
};
|
||||
|
||||
class PPMImage: public Raster::RawData {
|
||||
public:
|
||||
PPMImage& serialize(const Raster &raster) override;
|
||||
std::string get_file_extension() const override { return "ppm"; }
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream &stream, const Raster::RawData &bytes);
|
||||
|
||||
} // sla
|
||||
} // Slic3r
|
||||
|
||||
|
||||
#endif // SLARASTER_HPP
|
89
src/libslic3r/SLA/RasterBase.cpp
Normal file
89
src/libslic3r/SLA/RasterBase.cpp
Normal file
@ -0,0 +1,89 @@
|
||||
#ifndef SLARASTER_CPP
|
||||
#define SLARASTER_CPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <libslic3r/SLA/RasterBase.hpp>
|
||||
#include <libslic3r/SLA/AGGRaster.hpp>
|
||||
|
||||
// minz image write:
|
||||
#include <miniz.h>
|
||||
|
||||
namespace Slic3r { namespace sla {
|
||||
|
||||
const RasterBase::TMirroring RasterBase::NoMirror = {false, false};
|
||||
const RasterBase::TMirroring RasterBase::MirrorX = {true, false};
|
||||
const RasterBase::TMirroring RasterBase::MirrorY = {false, true};
|
||||
const RasterBase::TMirroring RasterBase::MirrorXY = {true, true};
|
||||
|
||||
EncodedRaster PNGRasterEncoder::operator()(const void *ptr, size_t w, size_t h,
|
||||
size_t num_components)
|
||||
{
|
||||
std::vector<uint8_t> buf;
|
||||
size_t s = 0;
|
||||
|
||||
void *rawdata = tdefl_write_image_to_png_file_in_memory(
|
||||
ptr, int(w), int(h), int(num_components), &s);
|
||||
|
||||
// On error, data() will return an empty vector. No other info can be
|
||||
// retrieved from miniz anyway...
|
||||
if (rawdata == nullptr) return EncodedRaster({}, "png");
|
||||
|
||||
auto pptr = static_cast<std::uint8_t*>(rawdata);
|
||||
|
||||
buf.reserve(s);
|
||||
std::copy(pptr, pptr + s, std::back_inserter(buf));
|
||||
|
||||
MZ_FREE(rawdata);
|
||||
return EncodedRaster(std::move(buf), "png");
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &stream, const EncodedRaster &bytes)
|
||||
{
|
||||
stream.write(reinterpret_cast<const char *>(bytes.data()),
|
||||
std::streamsize(bytes.size()));
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
EncodedRaster PPMRasterEncoder::operator()(const void *ptr, size_t w, size_t h,
|
||||
size_t num_components)
|
||||
{
|
||||
std::vector<uint8_t> buf;
|
||||
|
||||
auto header = std::string("P5 ") +
|
||||
std::to_string(w) + " " +
|
||||
std::to_string(h) + " " + "255 ";
|
||||
|
||||
auto sz = w * h * num_components;
|
||||
size_t s = sz + header.size();
|
||||
|
||||
buf.reserve(s);
|
||||
|
||||
auto buff = reinterpret_cast<const std::uint8_t*>(ptr);
|
||||
std::copy(header.begin(), header.end(), std::back_inserter(buf));
|
||||
std::copy(buff, buff+sz, std::back_inserter(buf));
|
||||
|
||||
return EncodedRaster(std::move(buf), "ppm");
|
||||
}
|
||||
|
||||
std::unique_ptr<RasterBase> create_raster_grayscale_aa(
|
||||
const RasterBase::Resolution &res,
|
||||
const RasterBase::PixelDim & pxdim,
|
||||
double gamma,
|
||||
const RasterBase::Trafo & tr)
|
||||
{
|
||||
std::unique_ptr<RasterBase> rst;
|
||||
|
||||
if (gamma > 0)
|
||||
rst = std::make_unique<RasterGrayscaleAAGammaPower>(res, pxdim, tr, gamma);
|
||||
else
|
||||
rst = std::make_unique<RasterGrayscaleAA>(res, pxdim, tr, agg::gamma_threshold(.5));
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
} // namespace sla
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SLARASTER_CPP
|
124
src/libslic3r/SLA/RasterBase.hpp
Normal file
124
src/libslic3r/SLA/RasterBase.hpp
Normal file
@ -0,0 +1,124 @@
|
||||
#ifndef SLA_RASTERBASE_HPP
|
||||
#define SLA_RASTERBASE_HPP
|
||||
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <libslic3r/SLA/Concurrency.hpp>
|
||||
|
||||
namespace ClipperLib { struct Polygon; }
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
template<class T> using uqptr = std::unique_ptr<T>;
|
||||
template<class T> using shptr = std::shared_ptr<T>;
|
||||
template<class T> using wkptr = std::weak_ptr<T>;
|
||||
|
||||
namespace sla {
|
||||
|
||||
// Raw byte buffer paired with its size. Suitable for compressed image data.
|
||||
class EncodedRaster {
|
||||
protected:
|
||||
std::vector<uint8_t> m_buffer;
|
||||
std::string m_ext;
|
||||
public:
|
||||
EncodedRaster() = default;
|
||||
explicit EncodedRaster(std::vector<uint8_t> &&buf, std::string ext)
|
||||
: m_buffer(std::move(buf)), m_ext(std::move(ext))
|
||||
{}
|
||||
|
||||
size_t size() const { return m_buffer.size(); }
|
||||
const void * data() const { return m_buffer.data(); }
|
||||
const char * extension() const { return m_ext.c_str(); }
|
||||
};
|
||||
|
||||
using RasterEncoder =
|
||||
std::function<EncodedRaster(const void *ptr, size_t w, size_t h, size_t num_components)>;
|
||||
|
||||
class RasterBase {
|
||||
public:
|
||||
|
||||
enum Orientation { roLandscape, roPortrait };
|
||||
|
||||
using TMirroring = std::array<bool, 2>;
|
||||
static const TMirroring NoMirror;
|
||||
static const TMirroring MirrorX;
|
||||
static const TMirroring MirrorY;
|
||||
static const TMirroring MirrorXY;
|
||||
|
||||
struct Trafo {
|
||||
bool mirror_x = false, mirror_y = false, flipXY = false;
|
||||
coord_t center_x = 0, center_y = 0;
|
||||
|
||||
// Portrait orientation will make sure the drawed polygons are rotated
|
||||
// by 90 degrees.
|
||||
Trafo(Orientation o = roLandscape, const TMirroring &mirror = NoMirror)
|
||||
// XY flipping implicitly does an X mirror
|
||||
: mirror_x(o == roPortrait ? !mirror[0] : mirror[0])
|
||||
, mirror_y(!mirror[1]) // Makes raster origin to be top left corner
|
||||
, flipXY(o == roPortrait)
|
||||
{}
|
||||
|
||||
TMirroring get_mirror() const { return { (roPortrait ? !mirror_x : mirror_x), mirror_y}; }
|
||||
Orientation get_orientation() const { return flipXY ? roPortrait : roLandscape; }
|
||||
Point get_center() const { return {center_x, center_y}; }
|
||||
};
|
||||
|
||||
/// Type that represents a resolution in pixels.
|
||||
struct Resolution {
|
||||
size_t width_px = 0;
|
||||
size_t height_px = 0;
|
||||
|
||||
Resolution(size_t w = 0, size_t h = 0) : width_px(w), height_px(h) {}
|
||||
size_t pixels() const { return width_px * height_px; }
|
||||
};
|
||||
|
||||
/// Types that represents the dimension of a pixel in millimeters.
|
||||
struct PixelDim {
|
||||
double w_mm = 0.;
|
||||
double h_mm = 0.;
|
||||
|
||||
PixelDim(double px_width_mm = 0.0, double px_height_mm = 0.0)
|
||||
: w_mm(px_width_mm), h_mm(px_height_mm)
|
||||
{}
|
||||
};
|
||||
|
||||
virtual ~RasterBase() = default;
|
||||
|
||||
/// Draw a polygon with holes.
|
||||
virtual void draw(const ExPolygon& poly) = 0;
|
||||
virtual void draw(const ClipperLib::Polygon& poly) = 0;
|
||||
|
||||
/// Get the resolution of the raster.
|
||||
virtual Resolution resolution() const = 0;
|
||||
virtual PixelDim pixel_dimensions() const = 0;
|
||||
virtual Trafo trafo() const = 0;
|
||||
|
||||
virtual EncodedRaster encode(RasterEncoder encoder) const = 0;
|
||||
};
|
||||
|
||||
struct PNGRasterEncoder {
|
||||
EncodedRaster operator()(const void *ptr, size_t w, size_t h, size_t num_components);
|
||||
};
|
||||
|
||||
struct PPMRasterEncoder {
|
||||
EncodedRaster operator()(const void *ptr, size_t w, size_t h, size_t num_components);
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream &stream, const EncodedRaster &bytes);
|
||||
|
||||
// If gamma is zero, thresholding will be performed which disables AA.
|
||||
uqptr<RasterBase> create_raster_grayscale_aa(
|
||||
const RasterBase::Resolution &res,
|
||||
const RasterBase::PixelDim & pxdim,
|
||||
double gamma = 1.0,
|
||||
const RasterBase::Trafo & tr = {});
|
||||
|
||||
}} // namespace Slic3r::sla
|
||||
|
||||
#endif // SLARASTERBASE_HPP
|
91
src/libslic3r/SLA/RasterToPolygons.cpp
Normal file
91
src/libslic3r/SLA/RasterToPolygons.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
#include "RasterToPolygons.hpp"
|
||||
|
||||
#include "AGGRaster.hpp"
|
||||
#include "libslic3r/MarchingSquares.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
|
||||
namespace marchsq {
|
||||
|
||||
// Specialize this struct to register a raster type for the Marching squares alg
|
||||
template<> struct _RasterTraits<Slic3r::sla::RasterGrayscaleAA> {
|
||||
using Rst = Slic3r::sla::RasterGrayscaleAA;
|
||||
|
||||
// The type of pixel cell in the raster
|
||||
using ValueType = uint8_t;
|
||||
|
||||
// Value at a given position
|
||||
static uint8_t get(const Rst &rst, size_t row, size_t col) { return rst.read_pixel(col, row); }
|
||||
|
||||
// Number of rows and cols of the raster
|
||||
static size_t rows(const Rst &rst) { return rst.resolution().height_px; }
|
||||
static size_t cols(const Rst &rst) { return rst.resolution().width_px; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r::marchsq
|
||||
|
||||
namespace Slic3r { namespace sla {
|
||||
|
||||
template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn)
|
||||
{
|
||||
for (auto &p : poly.contour.points) fn(p);
|
||||
for (auto &h : poly.holes)
|
||||
for (auto &p : h.points) fn(p);
|
||||
}
|
||||
|
||||
ExPolygons raster_to_polygons(const RasterGrayscaleAA &rst, Vec2i windowsize)
|
||||
{
|
||||
size_t rows = rst.resolution().height_px, cols = rst.resolution().width_px;
|
||||
|
||||
if (rows < 2 || cols < 2) return {};
|
||||
|
||||
Polygons polys;
|
||||
long w_rows = std::max(2l, long(windowsize.y()));
|
||||
long w_cols = std::max(2l, long(windowsize.x()));
|
||||
|
||||
std::vector<marchsq::Ring> rings =
|
||||
marchsq::execute(rst, 128, {w_rows, w_cols});
|
||||
|
||||
polys.reserve(rings.size());
|
||||
|
||||
auto pxd = rst.pixel_dimensions();
|
||||
pxd.w_mm = (rst.resolution().width_px * pxd.w_mm) / (rst.resolution().width_px - 1);
|
||||
pxd.h_mm = (rst.resolution().height_px * pxd.h_mm) / (rst.resolution().height_px - 1);
|
||||
|
||||
for (const marchsq::Ring &ring : rings) {
|
||||
Polygon poly; Points &pts = poly.points;
|
||||
pts.reserve(ring.size());
|
||||
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
pts.emplace_back(scaled(crd.c * pxd.w_mm), scaled(crd.r * pxd.h_mm));
|
||||
|
||||
polys.emplace_back(poly);
|
||||
}
|
||||
|
||||
// reverse the raster transformations
|
||||
ExPolygons unioned = union_ex(polys);
|
||||
coord_t width = scaled(cols * pxd.h_mm), height = scaled(rows * pxd.w_mm);
|
||||
|
||||
auto tr = rst.trafo();
|
||||
for (ExPolygon &expoly : unioned) {
|
||||
if (tr.mirror_y)
|
||||
foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); });
|
||||
|
||||
if (tr.mirror_x)
|
||||
foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); });
|
||||
|
||||
expoly.translate(-tr.center_x, -tr.center_y);
|
||||
|
||||
if (tr.flipXY)
|
||||
foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); });
|
||||
|
||||
if ((tr.mirror_x + tr.mirror_y + tr.flipXY) % 2) {
|
||||
expoly.contour.reverse();
|
||||
for (auto &h : expoly.holes) h.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
return unioned;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r
|
15
src/libslic3r/SLA/RasterToPolygons.hpp
Normal file
15
src/libslic3r/SLA/RasterToPolygons.hpp
Normal file
@ -0,0 +1,15 @@
|
||||
#ifndef RASTERTOPOLYGONS_HPP
|
||||
#define RASTERTOPOLYGONS_HPP
|
||||
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace sla {
|
||||
|
||||
class RasterGrayscaleAA;
|
||||
|
||||
ExPolygons raster_to_polygons(const RasterGrayscaleAA &rst, Vec2i windowsize = {2, 2});
|
||||
|
||||
}} // namespace Slic3r::sla
|
||||
|
||||
#endif // RASTERTOPOLYGONS_HPP
|
@ -1,151 +0,0 @@
|
||||
#include <string_view>
|
||||
|
||||
#include <libslic3r/SLA/RasterWriter.hpp>
|
||||
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <libslic3r/Zipper.hpp>
|
||||
#include <libslic3r/Time.hpp>
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include <libnest2d/backends/clipper/clipper_polygon.hpp>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
namespace Slic3r { namespace sla {
|
||||
|
||||
void RasterWriter::write_ini(const std::map<std::string, std::string> &m, std::string &ini)
|
||||
{
|
||||
for (auto ¶m : m) ini += param.first + " = " + param.second + "\n";
|
||||
}
|
||||
|
||||
std::string RasterWriter::create_ini_content(const std::string& projectname) const
|
||||
{
|
||||
std::string out("action = print\njobDir = ");
|
||||
out += projectname + "\n";
|
||||
write_ini(m_config, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
RasterWriter::RasterWriter(const Raster::Resolution &res,
|
||||
const Raster::PixelDim & pixdim,
|
||||
const Raster::Trafo & trafo,
|
||||
double gamma)
|
||||
: m_res(res), m_pxdim(pixdim), m_trafo(trafo), m_gamma(gamma)
|
||||
{}
|
||||
|
||||
void RasterWriter::save(const std::string &fpath, const std::string &prjname)
|
||||
{
|
||||
try {
|
||||
Zipper zipper(fpath); // zipper with no compression
|
||||
save(zipper, prjname);
|
||||
zipper.finalize();
|
||||
} catch(std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||
// Rethrow the exception
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void RasterWriter::save(Zipper &zipper, const std::string &prjname)
|
||||
{
|
||||
try {
|
||||
std::string project =
|
||||
prjname.empty() ?
|
||||
boost::filesystem::path(zipper.get_filename()).stem().string() :
|
||||
prjname;
|
||||
|
||||
zipper.add_entry("config.ini");
|
||||
|
||||
zipper << create_ini_content(project);
|
||||
|
||||
zipper.add_entry("prusaslicer.ini");
|
||||
std::string prusaslicer_ini;
|
||||
write_ini(m_slicer_config, prusaslicer_ini);
|
||||
zipper << prusaslicer_ini;
|
||||
|
||||
for(unsigned i = 0; i < m_layers_rst.size(); i++)
|
||||
{
|
||||
if(m_layers_rst[i].rawbytes.size() > 0) {
|
||||
char lyrnum[6];
|
||||
std::sprintf(lyrnum, "%.5d", i);
|
||||
auto zfilename = project + lyrnum + ".png";
|
||||
|
||||
// Add binary entry to the zipper
|
||||
zipper.add_entry(zfilename,
|
||||
m_layers_rst[i].rawbytes.data(),
|
||||
m_layers_rst[i].rawbytes.size());
|
||||
}
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << e.what();
|
||||
// Rethrow the exception
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key)
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
if (cfg.has(key)) {
|
||||
auto opt = cfg.option(key);
|
||||
if (opt) ret = opt->serialize();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void append_full_config(const DynamicPrintConfig &cfg, std::map<std::string, std::string> &keys)
|
||||
{
|
||||
using namespace std::literals::string_view_literals;
|
||||
|
||||
// Sorted list of config keys, which shall not be stored into the ini.
|
||||
static constexpr auto banned_keys = {
|
||||
"compatible_printers"sv,
|
||||
"compatible_prints"sv,
|
||||
"print_host"sv,
|
||||
"printhost_apikey"sv,
|
||||
"printhost_cafile"sv
|
||||
};
|
||||
|
||||
assert(std::is_sorted(banned_keys.begin(), banned_keys.end()));
|
||||
auto is_banned = [](const std::string &key) {
|
||||
return std::binary_search(banned_keys.begin(), banned_keys.end(), key);
|
||||
};
|
||||
|
||||
for (const std::string &key : cfg.keys())
|
||||
if (! is_banned(key) && ! cfg.option(key)->is_nil())
|
||||
keys[key] = cfg.opt_serialize(key);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RasterWriter::set_config(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
m_config["layerHeight"] = get_cfg_value(cfg, "layer_height");
|
||||
m_config["expTime"] = get_cfg_value(cfg, "exposure_time");
|
||||
m_config["expTimeFirst"] = get_cfg_value(cfg, "initial_exposure_time");
|
||||
m_config["materialName"] = get_cfg_value(cfg, "sla_material_settings_id");
|
||||
m_config["printerModel"] = get_cfg_value(cfg, "printer_model");
|
||||
m_config["printerVariant"] = get_cfg_value(cfg, "printer_variant");
|
||||
m_config["printerProfile"] = get_cfg_value(cfg, "printer_settings_id");
|
||||
m_config["printProfile"] = get_cfg_value(cfg, "sla_print_settings_id");
|
||||
m_config["fileCreationTimestamp"] = Utils::utc_timestamp();
|
||||
m_config["prusaSlicerVersion"] = SLIC3R_BUILD_ID;
|
||||
append_full_config(cfg, m_slicer_config);
|
||||
}
|
||||
|
||||
void RasterWriter::set_statistics(const PrintStatistics &stats)
|
||||
{
|
||||
m_config["usedMaterial"] = std::to_string(stats.used_material);
|
||||
m_config["numFade"] = std::to_string(stats.num_fade);
|
||||
m_config["numSlow"] = std::to_string(stats.num_slow);
|
||||
m_config["numFast"] = std::to_string(stats.num_fast);
|
||||
m_config["printTime"] = std::to_string(stats.estimated_print_time_s);
|
||||
}
|
||||
|
||||
} // namespace sla
|
||||
} // namespace Slic3r
|
@ -1,130 +0,0 @@
|
||||
#ifndef SLA_RASTERWRITER_HPP
|
||||
#define SLA_RASTERWRITER_HPP
|
||||
|
||||
// For png export of the sliced model
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <array>
|
||||
|
||||
#include <libslic3r/SLA/Raster.hpp>
|
||||
#include <libslic3r/Zipper.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
namespace sla {
|
||||
|
||||
// API to write the zipped sla output layers and metadata.
|
||||
// Implementation uses PNG raster output.
|
||||
// Be aware that if a large number of layers are allocated, it can very well
|
||||
// exhaust the available memory especially on 32 bit platform.
|
||||
// This class is designed to be used in parallel mode. Layers have an ID and
|
||||
// each layer can be written and compressed independently (in parallel).
|
||||
// At the end when all layers where written, the save method can be used to
|
||||
// write out the result into a zipped archive.
|
||||
class RasterWriter
|
||||
{
|
||||
public:
|
||||
|
||||
// Used for addressing parameters of set_statistics()
|
||||
struct PrintStatistics
|
||||
{
|
||||
double used_material = 0.;
|
||||
double estimated_print_time_s = 0.;
|
||||
size_t num_fade = 0;
|
||||
size_t num_slow = 0;
|
||||
size_t num_fast = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
// A struct to bind the raster image data and its compressed bytes together.
|
||||
struct Layer {
|
||||
Raster raster;
|
||||
PNGImage rawbytes;
|
||||
|
||||
Layer() = default;
|
||||
|
||||
// The image is big, do not copy by accident
|
||||
Layer(const Layer&) = delete;
|
||||
Layer& operator=(const Layer&) = delete;
|
||||
|
||||
Layer(Layer &&m) = default;
|
||||
Layer &operator=(Layer &&) = default;
|
||||
};
|
||||
|
||||
// We will save the compressed PNG data into RawBytes type buffers in
|
||||
// parallel. Later we can write every layer to the disk sequentially.
|
||||
std::vector<Layer> m_layers_rst;
|
||||
Raster::Resolution m_res;
|
||||
Raster::PixelDim m_pxdim;
|
||||
Raster::Trafo m_trafo;
|
||||
double m_gamma;
|
||||
|
||||
std::map<std::string, std::string> m_config;
|
||||
std::map<std::string, std::string> m_slicer_config;
|
||||
|
||||
static void write_ini(const std::map<std::string, std::string> &m, std::string &ini);
|
||||
std::string create_ini_content(const std::string& projectname) const;
|
||||
|
||||
public:
|
||||
|
||||
// SLARasterWriter is using Raster in custom mirroring mode
|
||||
RasterWriter(const Raster::Resolution &res,
|
||||
const Raster::PixelDim & pixdim,
|
||||
const Raster::Trafo & trafo,
|
||||
double gamma = 1.);
|
||||
|
||||
RasterWriter(const RasterWriter& ) = delete;
|
||||
RasterWriter& operator=(const RasterWriter&) = delete;
|
||||
RasterWriter(RasterWriter&& m) = default;
|
||||
RasterWriter& operator=(RasterWriter&&) = default;
|
||||
|
||||
inline void layers(unsigned cnt) { if(cnt > 0) m_layers_rst.resize(cnt); }
|
||||
inline unsigned layers() const { return unsigned(m_layers_rst.size()); }
|
||||
|
||||
template<class Poly> void draw_polygon(const Poly& p, unsigned lyr)
|
||||
{
|
||||
assert(lyr < m_layers_rst.size());
|
||||
m_layers_rst[lyr].raster.draw(p);
|
||||
}
|
||||
|
||||
inline void begin_layer(unsigned lyr) {
|
||||
if(m_layers_rst.size() <= lyr) m_layers_rst.resize(lyr+1);
|
||||
m_layers_rst[lyr].raster.reset(m_res, m_pxdim, m_trafo);
|
||||
}
|
||||
|
||||
inline void begin_layer() {
|
||||
m_layers_rst.emplace_back();
|
||||
m_layers_rst.front().raster.reset(m_res, m_pxdim, m_trafo);
|
||||
}
|
||||
|
||||
inline void finish_layer(unsigned lyr_id) {
|
||||
assert(lyr_id < m_layers_rst.size());
|
||||
m_layers_rst[lyr_id].rawbytes.serialize(m_layers_rst[lyr_id].raster);
|
||||
m_layers_rst[lyr_id].raster.reset();
|
||||
}
|
||||
|
||||
inline void finish_layer() {
|
||||
if(!m_layers_rst.empty()) {
|
||||
m_layers_rst.back().rawbytes.serialize(m_layers_rst.back().raster);
|
||||
m_layers_rst.back().raster.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void save(const std::string &fpath, const std::string &prjname = "");
|
||||
void save(Zipper &zipper, const std::string &prjname = "");
|
||||
|
||||
void set_statistics(const PrintStatistics &statistics);
|
||||
|
||||
void set_config(const DynamicPrintConfig &cfg);
|
||||
};
|
||||
|
||||
} // namespace sla
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SLARASTERWRITER_HPP
|
@ -228,6 +228,8 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, DynamicPrintConfig con
|
||||
// Handle changes to object config defaults
|
||||
m_default_object_config.apply_only(config, object_diff, true);
|
||||
|
||||
if (m_printer) m_printer->apply(m_printer_config);
|
||||
|
||||
struct ModelObjectStatus {
|
||||
enum Status {
|
||||
Unknown,
|
||||
@ -482,7 +484,6 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, DynamicPrintConfig con
|
||||
}
|
||||
|
||||
if(m_objects.empty()) {
|
||||
m_printer.reset();
|
||||
m_printer_input = {};
|
||||
m_print_statistics = {};
|
||||
}
|
||||
@ -657,6 +658,12 @@ std::string SLAPrint::validate() const
|
||||
return "";
|
||||
}
|
||||
|
||||
void SLAPrint::set_printer(SLAPrinter *arch)
|
||||
{
|
||||
invalidate_step(slapsRasterize);
|
||||
m_printer = arch;
|
||||
}
|
||||
|
||||
bool SLAPrint::invalidate_step(SLAPrintStep step)
|
||||
{
|
||||
bool invalidated = Inherited::invalidate_step(step);
|
||||
@ -676,7 +683,7 @@ void SLAPrint::process()
|
||||
// Assumption: at this point the print objects should be populated only with
|
||||
// the model objects we have to process and the instances are also filtered
|
||||
|
||||
Steps printsteps{this};
|
||||
Steps printsteps(this);
|
||||
|
||||
// We want to first process all objects...
|
||||
std::vector<SLAPrintObjectStep> level1_obj_steps = {
|
||||
@ -855,36 +862,6 @@ bool SLAPrint::invalidate_state_by_config_options(const std::vector<t_config_opt
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
sla::RasterWriter & SLAPrint::init_printer()
|
||||
{
|
||||
sla::Raster::Resolution res;
|
||||
sla::Raster::PixelDim pxdim;
|
||||
std::array<bool, 2> mirror;
|
||||
|
||||
double w = m_printer_config.display_width.getFloat();
|
||||
double h = m_printer_config.display_height.getFloat();
|
||||
auto pw = size_t(m_printer_config.display_pixels_x.getInt());
|
||||
auto ph = size_t(m_printer_config.display_pixels_y.getInt());
|
||||
|
||||
mirror[X] = m_printer_config.display_mirror_x.getBool();
|
||||
mirror[Y] = m_printer_config.display_mirror_y.getBool();
|
||||
|
||||
auto orientation = get_printer_orientation();
|
||||
if (orientation == sla::Raster::roPortrait) {
|
||||
std::swap(w, h);
|
||||
std::swap(pw, ph);
|
||||
}
|
||||
|
||||
res = sla::Raster::Resolution{pw, ph};
|
||||
pxdim = sla::Raster::PixelDim{w / pw, h / ph};
|
||||
sla::Raster::Trafo tr{orientation, mirror};
|
||||
tr.gamma = m_printer_config.gamma_correction.getFloat();
|
||||
|
||||
m_printer.reset(new sla::RasterWriter(res, pxdim, tr));
|
||||
m_printer->set_config(m_full_print_config);
|
||||
return *m_printer;
|
||||
}
|
||||
|
||||
// Returns true if an object step is done on all objects and there's at least one object.
|
||||
bool SLAPrint::is_step_done(SLAPrintObjectStep step) const
|
||||
{
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
#include <mutex>
|
||||
#include "PrintBase.hpp"
|
||||
#include "SLA/RasterWriter.hpp"
|
||||
#include "SLA/RasterBase.hpp"
|
||||
#include "SLA/SupportTree.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
@ -369,6 +369,31 @@ struct SLAPrintStatistics
|
||||
}
|
||||
};
|
||||
|
||||
class SLAPrinter {
|
||||
protected:
|
||||
std::vector<sla::EncodedRaster> m_layers;
|
||||
|
||||
virtual uqptr<sla::RasterBase> create_raster() const = 0;
|
||||
virtual sla::EncodedRaster encode_raster(const sla::RasterBase &rst) const = 0;
|
||||
|
||||
public:
|
||||
virtual ~SLAPrinter() = default;
|
||||
|
||||
virtual void apply(const SLAPrinterConfig &cfg) = 0;
|
||||
|
||||
// Fn have to be thread safe: void(sla::RasterBase& raster, size_t lyrid);
|
||||
template<class Fn> void draw_layers(size_t layer_num, Fn &&drawfn)
|
||||
{
|
||||
m_layers.resize(layer_num);
|
||||
sla::ccr::enumerate(m_layers.begin(), m_layers.end(),
|
||||
[this, &drawfn](sla::EncodedRaster& enc, size_t idx) {
|
||||
auto rst = create_raster();
|
||||
drawfn(*rst, idx);
|
||||
enc = encode_raster(*rst);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This class is the high level FSM for the SLA printing process.
|
||||
*
|
||||
@ -403,18 +428,6 @@ public:
|
||||
// Returns true if the last step was finished with success.
|
||||
bool finished() const override { return this->is_step_done(slaposSliceSupports) && this->Inherited::is_step_done(slapsRasterize); }
|
||||
|
||||
inline void export_raster(const std::string& fpath,
|
||||
const std::string& projectname = "")
|
||||
{
|
||||
if(m_printer) m_printer->save(fpath, projectname);
|
||||
}
|
||||
|
||||
inline void export_raster(Zipper &zipper,
|
||||
const std::string& projectname = "")
|
||||
{
|
||||
if(m_printer) m_printer->save(zipper, projectname);
|
||||
}
|
||||
|
||||
const PrintObjects& objects() const { return m_objects; }
|
||||
|
||||
const SLAPrintConfig& print_config() const { return m_print_config; }
|
||||
@ -445,7 +458,8 @@ public:
|
||||
|
||||
std::vector<ClipperLib::Polygon> m_transformed_slices;
|
||||
|
||||
template<class Container> void transformed_slices(Container&& c) {
|
||||
template<class Container> void transformed_slices(Container&& c)
|
||||
{
|
||||
m_transformed_slices = std::forward<Container>(c);
|
||||
}
|
||||
|
||||
@ -475,7 +489,10 @@ public:
|
||||
// TODO: use this structure for the preview in the future.
|
||||
const std::vector<PrintLayer>& print_layers() const { return m_printer_input; }
|
||||
|
||||
void set_printer(SLAPrinter *archiver);
|
||||
|
||||
private:
|
||||
|
||||
// Implement same logic as in SLAPrintObject
|
||||
bool invalidate_step(SLAPrintStep st);
|
||||
|
||||
@ -493,8 +510,8 @@ private:
|
||||
// Ready-made data for rasterization.
|
||||
std::vector<PrintLayer> m_printer_input;
|
||||
|
||||
// The printer itself
|
||||
std::unique_ptr<sla::RasterWriter> m_printer;
|
||||
// The archive object which collects the raster images after slicing
|
||||
SLAPrinter *m_printer = nullptr;
|
||||
|
||||
// Estimated print time, material consumed.
|
||||
SLAPrintStatistics m_print_statistics;
|
||||
@ -513,15 +530,6 @@ private:
|
||||
double status() const { return m_st; }
|
||||
} m_report_status;
|
||||
|
||||
sla::RasterWriter &init_printer();
|
||||
|
||||
inline sla::Raster::Orientation get_printer_orientation() const
|
||||
{
|
||||
auto ro = m_printer_config.display_orientation.getInt();
|
||||
return ro == sla::Raster::roPortrait ? sla::Raster::roPortrait :
|
||||
sla::Raster::roLandscape;
|
||||
}
|
||||
|
||||
friend SLAPrintObject;
|
||||
};
|
||||
|
||||
|
@ -816,16 +816,7 @@ void SLAPrint::Steps::merge_slices_and_eval_stats() {
|
||||
// Rasterizing the model objects, and their supports
|
||||
void SLAPrint::Steps::rasterize()
|
||||
{
|
||||
if(canceled()) return;
|
||||
|
||||
auto &print_statistics = m_print->m_print_statistics;
|
||||
auto &printer_input = m_print->m_printer_input;
|
||||
|
||||
// Set up the printer, allocate space for all the layers
|
||||
sla::RasterWriter &printer = m_print->init_printer();
|
||||
|
||||
auto lvlcnt = unsigned(printer_input.size());
|
||||
printer.layers(lvlcnt);
|
||||
if(canceled() || !m_print->m_printer) return;
|
||||
|
||||
// coefficient to map the rasterization state (0-99) to the allocated
|
||||
// portion (slot) of the process state
|
||||
@ -837,7 +828,7 @@ void SLAPrint::Steps::rasterize()
|
||||
// pst: previous state
|
||||
double pst = current_status();
|
||||
|
||||
double increment = (slot * sd) / printer_input.size();
|
||||
double increment = (slot * sd) / m_print->m_printer_input.size();
|
||||
double dstatus = current_status();
|
||||
|
||||
sla::ccr::SpinningMutex slck;
|
||||
@ -845,20 +836,14 @@ void SLAPrint::Steps::rasterize()
|
||||
|
||||
// procedure to process one height level. This will run in parallel
|
||||
auto lvlfn =
|
||||
[this, &slck, &printer, increment, &dstatus, &pst]
|
||||
(PrintLayer& printlayer, size_t idx)
|
||||
[this, &slck, increment, &dstatus, &pst]
|
||||
(sla::RasterBase& raster, size_t idx)
|
||||
{
|
||||
PrintLayer& printlayer = m_print->m_printer_input[idx];
|
||||
if(canceled()) return;
|
||||
auto level_id = unsigned(idx);
|
||||
|
||||
// Switch to the appropriate layer in the printer
|
||||
printer.begin_layer(level_id);
|
||||
|
||||
for(const ClipperLib::Polygon& poly : printlayer.transformed_slices())
|
||||
printer.draw_polygon(poly, level_id);
|
||||
|
||||
// Finish the layer for later saving it.
|
||||
printer.finish_layer(level_id);
|
||||
for (const ClipperLib::Polygon& poly : printlayer.transformed_slices())
|
||||
raster.draw(poly);
|
||||
|
||||
// Status indication guarded with the spinlock
|
||||
{
|
||||
@ -875,24 +860,8 @@ void SLAPrint::Steps::rasterize()
|
||||
// last minute escape
|
||||
if(canceled()) return;
|
||||
|
||||
// Sequential version (for testing)
|
||||
// for(unsigned l = 0; l < lvlcnt; ++l) lvlfn(l);
|
||||
|
||||
// Print all the layers in parallel
|
||||
sla::ccr::enumerate(printer_input.begin(), printer_input.end(), lvlfn);
|
||||
|
||||
// Set statistics values to the printer
|
||||
sla::RasterWriter::PrintStatistics stats;
|
||||
stats.used_material = (print_statistics.objects_used_material +
|
||||
print_statistics.support_used_material) / 1000;
|
||||
|
||||
int num_fade = m_print->m_default_object_config.faded_layers.getInt();
|
||||
stats.num_fade = num_fade >= 0 ? size_t(num_fade) : size_t(0);
|
||||
stats.num_fast = print_statistics.fast_layers_count;
|
||||
stats.num_slow = print_statistics.slow_layers_count;
|
||||
stats.estimated_print_time_s = print_statistics.estimated_print_time;
|
||||
|
||||
printer.set_statistics(stats);
|
||||
m_print->m_printer->draw_layers(m_print->m_printer_input.size(), lvlfn);
|
||||
}
|
||||
|
||||
std::string SLAPrint::Steps::label(SLAPrintObjectStep step)
|
||||
|
@ -46,7 +46,7 @@ private:
|
||||
void apply_printer_corrections(SLAPrintObject &po, SliceOrigin o);
|
||||
|
||||
public:
|
||||
Steps(SLAPrint *print);
|
||||
explicit Steps(SLAPrint *print);
|
||||
|
||||
void hollow_model(SLAPrintObject &po);
|
||||
void drill_holes (SLAPrintObject &po);
|
||||
|
128
src/libslic3r/SlicesToTriangleMesh.cpp
Normal file
128
src/libslic3r/SlicesToTriangleMesh.cpp
Normal file
@ -0,0 +1,128 @@
|
||||
|
||||
#include "SlicesToTriangleMesh.hpp"
|
||||
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/SLA/Contour3D.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Tesselate.hpp"
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/parallel_reduce.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
inline sla::Contour3D wall_strip(const Polygon &poly,
|
||||
double lower_z_mm,
|
||||
double upper_z_mm)
|
||||
{
|
||||
sla::Contour3D ret;
|
||||
|
||||
size_t startidx = ret.points.size();
|
||||
size_t offs = poly.points.size();
|
||||
|
||||
ret.points.reserve(ret.points.size() + 2 *offs);
|
||||
|
||||
for (const Point &p : poly.points)
|
||||
ret.points.emplace_back(to_3d(unscaled(p), lower_z_mm));
|
||||
|
||||
for (const Point &p : poly.points)
|
||||
ret.points.emplace_back(to_3d(unscaled(p), upper_z_mm));
|
||||
|
||||
for (size_t i = startidx + 1; i < startidx + offs; ++i) {
|
||||
ret.faces3.emplace_back(i - 1, i, i + offs - 1);
|
||||
ret.faces3.emplace_back(i, i + offs, i + offs - 1);
|
||||
}
|
||||
|
||||
ret.faces3.emplace_back(startidx + offs - 1, startidx, startidx + 2 * offs - 1);
|
||||
ret.faces3.emplace_back(startidx, startidx + offs, startidx + 2 * offs - 1);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Same as walls() but with identical higher and lower polygons.
|
||||
sla::Contour3D inline straight_walls(const Polygon &plate,
|
||||
double lo_z,
|
||||
double hi_z)
|
||||
{
|
||||
return wall_strip(plate, lo_z, hi_z);
|
||||
}
|
||||
|
||||
sla::Contour3D inline straight_walls(const ExPolygon &plate,
|
||||
double lo_z,
|
||||
double hi_z)
|
||||
{
|
||||
sla::Contour3D ret;
|
||||
ret.merge(straight_walls(plate.contour, lo_z, hi_z));
|
||||
for (auto &h : plate.holes) ret.merge(straight_walls(h, lo_z, hi_z));
|
||||
return ret;
|
||||
}
|
||||
|
||||
sla::Contour3D inline straight_walls(const ExPolygons &slice,
|
||||
double lo_z,
|
||||
double hi_z)
|
||||
{
|
||||
sla::Contour3D ret;
|
||||
for (const ExPolygon &poly : slice)
|
||||
ret.merge(straight_walls(poly, lo_z, hi_z));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
sla::Contour3D slices_to_triangle_mesh(const std::vector<ExPolygons> &slices,
|
||||
double zmin,
|
||||
const std::vector<float> & grid)
|
||||
{
|
||||
assert(slices.size() == grid.size());
|
||||
|
||||
using Layers = std::vector<sla::Contour3D>;
|
||||
std::vector<sla::Contour3D> layers(slices.size());
|
||||
size_t len = slices.size() - 1;
|
||||
|
||||
tbb::parallel_for(size_t(0), len, [&slices, &layers, &grid](size_t i) {
|
||||
const ExPolygons &upper = slices[i + 1];
|
||||
const ExPolygons &lower = slices[i];
|
||||
|
||||
ExPolygons dff1 = diff_ex(lower, upper);
|
||||
ExPolygons dff2 = diff_ex(upper, lower);
|
||||
layers[i].merge(triangulate_expolygons_3d(dff1, grid[i], NORMALS_UP));
|
||||
layers[i].merge(triangulate_expolygons_3d(dff2, grid[i], NORMALS_DOWN));
|
||||
layers[i].merge(straight_walls(upper, grid[i], grid[i + 1]));
|
||||
|
||||
});
|
||||
|
||||
sla::Contour3D ret = tbb::parallel_reduce(
|
||||
tbb::blocked_range(layers.begin(), layers.end()),
|
||||
sla::Contour3D{},
|
||||
[](const tbb::blocked_range<Layers::iterator>& r, sla::Contour3D init) {
|
||||
for(auto it = r.begin(); it != r.end(); ++it ) init.merge(*it);
|
||||
return init;
|
||||
},
|
||||
[]( const sla::Contour3D &a, const sla::Contour3D &b ) {
|
||||
sla::Contour3D res{a}; res.merge(b); return res;
|
||||
});
|
||||
|
||||
ret.merge(triangulate_expolygons_3d(slices.front(), zmin, NORMALS_DOWN));
|
||||
ret.merge(straight_walls(slices.front(), zmin, grid.front()));
|
||||
ret.merge(triangulate_expolygons_3d(slices.back(), grid.back(), NORMALS_UP));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void slices_to_triangle_mesh(TriangleMesh & mesh,
|
||||
const std::vector<ExPolygons> &slices,
|
||||
double zmin,
|
||||
double lh,
|
||||
double ilh)
|
||||
{
|
||||
std::vector<sla::Contour3D> wall_meshes(slices.size());
|
||||
std::vector<float> grid(slices.size(), zmin + ilh);
|
||||
|
||||
for (size_t i = 1; i < grid.size(); ++i) grid[i] = grid[i - 1] + lh;
|
||||
|
||||
sla::Contour3D cntr = slices_to_triangle_mesh(slices, zmin, grid);
|
||||
mesh.merge(sla::to_triangle_mesh(cntr));
|
||||
mesh.repaired = true;
|
||||
mesh.require_shared_vertices();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
24
src/libslic3r/SlicesToTriangleMesh.hpp
Normal file
24
src/libslic3r/SlicesToTriangleMesh.hpp
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef SLICESTOTRIANGLEMESH_HPP
|
||||
#define SLICESTOTRIANGLEMESH_HPP
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void slices_to_triangle_mesh(TriangleMesh & mesh,
|
||||
const std::vector<ExPolygons> &slices,
|
||||
double zmin,
|
||||
double lh,
|
||||
double ilh);
|
||||
|
||||
inline TriangleMesh slices_to_triangle_mesh(
|
||||
const std::vector<ExPolygons> &slices, double zmin, double lh, double ilh)
|
||||
{
|
||||
TriangleMesh out; slices_to_triangle_mesh(out, slices, zmin, lh, ilh);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SLICESTOTRIANGLEMESH_HPP
|
133
src/libslic3r/TriangulateWall.cpp
Normal file
133
src/libslic3r/TriangulateWall.cpp
Normal file
@ -0,0 +1,133 @@
|
||||
#include "TriangulateWall.hpp"
|
||||
#include "MTUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Ring {
|
||||
size_t idx = 0, nextidx = 1, startidx = 0, begin = 0, end = 0;
|
||||
|
||||
public:
|
||||
explicit Ring(size_t from, size_t to) : begin(from), end(to) { init(begin); }
|
||||
|
||||
size_t size() const { return end - begin; }
|
||||
std::pair<size_t, size_t> pos() const { return {idx, nextidx}; }
|
||||
bool is_lower() const { return idx < size(); }
|
||||
|
||||
void inc()
|
||||
{
|
||||
if (nextidx != startidx) nextidx++;
|
||||
if (nextidx == end) nextidx = begin;
|
||||
idx ++;
|
||||
if (idx == end) idx = begin;
|
||||
}
|
||||
|
||||
void init(size_t pos)
|
||||
{
|
||||
startidx = begin + (pos - begin) % size();
|
||||
idx = startidx;
|
||||
nextidx = begin + (idx + 1 - begin) % size();
|
||||
}
|
||||
|
||||
bool is_finished() const { return nextidx == idx; }
|
||||
};
|
||||
|
||||
static double sq_dst(const Vec3d &v1, const Vec3d& v2)
|
||||
{
|
||||
Vec3d v = v1 - v2;
|
||||
return v.x() * v.x() + v.y() * v.y() /*+ v.z() * v.z()*/;
|
||||
}
|
||||
|
||||
static double score(const Ring& onring, const Ring &offring,
|
||||
const std::vector<Vec3d> &pts)
|
||||
{
|
||||
double a = sq_dst(pts[onring.pos().first], pts[offring.pos().first]);
|
||||
double b = sq_dst(pts[onring.pos().second], pts[offring.pos().first]);
|
||||
return (std::abs(a) + std::abs(b)) / 2.;
|
||||
}
|
||||
|
||||
class Triangulator {
|
||||
const std::vector<Vec3d> *pts;
|
||||
Ring *onring, *offring;
|
||||
|
||||
double calc_score() const
|
||||
{
|
||||
return Slic3r::score(*onring, *offring, *pts);
|
||||
}
|
||||
|
||||
void synchronize_rings()
|
||||
{
|
||||
Ring lring = *offring;
|
||||
auto minsc = Slic3r::score(*onring, lring, *pts);
|
||||
size_t imin = lring.pos().first;
|
||||
|
||||
lring.inc();
|
||||
|
||||
while(!lring.is_finished()) {
|
||||
double score = Slic3r::score(*onring, lring, *pts);
|
||||
if (score < minsc) { minsc = score; imin = lring.pos().first; }
|
||||
lring.inc();
|
||||
}
|
||||
|
||||
offring->init(imin);
|
||||
}
|
||||
|
||||
void emplace_indices(std::vector<Vec3i> &indices)
|
||||
{
|
||||
Vec3i tr{int(onring->pos().first), int(onring->pos().second),
|
||||
int(offring->pos().first)};
|
||||
if (onring->is_lower()) std::swap(tr(0), tr(1));
|
||||
indices.emplace_back(tr);
|
||||
}
|
||||
|
||||
public:
|
||||
void run(std::vector<Vec3i> &indices)
|
||||
{
|
||||
synchronize_rings();
|
||||
|
||||
double score = 0, prev_score = 0;
|
||||
while (!onring->is_finished() || !offring->is_finished()) {
|
||||
prev_score = score;
|
||||
if (onring->is_finished() || (score = calc_score()) > prev_score) {
|
||||
std::swap(onring, offring);
|
||||
} else {
|
||||
emplace_indices(indices);
|
||||
onring->inc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
explicit Triangulator(const std::vector<Vec3d> *points,
|
||||
Ring & lower,
|
||||
Ring & upper)
|
||||
: pts{points}, onring{&upper}, offring{&lower}
|
||||
{}
|
||||
};
|
||||
|
||||
Wall triangulate_wall(
|
||||
const Polygon & lower,
|
||||
const Polygon & upper,
|
||||
double lower_z_mm,
|
||||
double upper_z_mm)
|
||||
{
|
||||
if (upper.points.size() < 3 || lower.points.size() < 3) return {};
|
||||
|
||||
Wall wall;
|
||||
auto &pts = wall.first;
|
||||
auto &ind = wall.second;
|
||||
|
||||
pts.reserve(lower.points.size() + upper.points.size());
|
||||
for (auto &p : lower.points)
|
||||
wall.first.emplace_back(unscaled(p.x()), unscaled(p.y()), lower_z_mm);
|
||||
for (auto &p : upper.points)
|
||||
wall.first.emplace_back(unscaled(p.x()), unscaled(p.y()), upper_z_mm);
|
||||
|
||||
ind.reserve(2 * (lower.size() + upper.size()));
|
||||
|
||||
Ring lring{0, lower.points.size()}, uring{lower.points.size(), pts.size()};
|
||||
Triangulator t{&pts, lring, uring};
|
||||
t.run(ind);
|
||||
|
||||
return wall;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
17
src/libslic3r/TriangulateWall.hpp
Normal file
17
src/libslic3r/TriangulateWall.hpp
Normal file
@ -0,0 +1,17 @@
|
||||
#ifndef TRIANGULATEWALL_HPP
|
||||
#define TRIANGULATEWALL_HPP
|
||||
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using Wall = std::pair<std::vector<Vec3d>, std::vector<Vec3i>>;
|
||||
|
||||
Wall triangulate_wall(
|
||||
const Polygon & lower,
|
||||
const Polygon & upper,
|
||||
double lower_z_mm,
|
||||
double upper_z_mm);
|
||||
}
|
||||
|
||||
#endif // TRIANGULATEWALL_HPP
|
@ -17,90 +17,14 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Zipper::Impl {
|
||||
class Zipper::Impl: public MZ_Archive {
|
||||
public:
|
||||
mz_zip_archive arch;
|
||||
std::string m_zipname;
|
||||
|
||||
static std::string get_errorstr(mz_zip_error mz_err)
|
||||
{
|
||||
switch (mz_err)
|
||||
{
|
||||
case MZ_ZIP_NO_ERROR:
|
||||
return "no error";
|
||||
case MZ_ZIP_UNDEFINED_ERROR:
|
||||
return L("undefined error");
|
||||
case MZ_ZIP_TOO_MANY_FILES:
|
||||
return L("too many files");
|
||||
case MZ_ZIP_FILE_TOO_LARGE:
|
||||
return L("file too large");
|
||||
case MZ_ZIP_UNSUPPORTED_METHOD:
|
||||
return L("unsupported method");
|
||||
case MZ_ZIP_UNSUPPORTED_ENCRYPTION:
|
||||
return L("unsupported encryption");
|
||||
case MZ_ZIP_UNSUPPORTED_FEATURE:
|
||||
return L("unsupported feature");
|
||||
case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR:
|
||||
return L("failed finding central directory");
|
||||
case MZ_ZIP_NOT_AN_ARCHIVE:
|
||||
return L("not a ZIP archive");
|
||||
case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED:
|
||||
return L("invalid header or archive is corrupted");
|
||||
case MZ_ZIP_UNSUPPORTED_MULTIDISK:
|
||||
return L("unsupported multidisk archive");
|
||||
case MZ_ZIP_DECOMPRESSION_FAILED:
|
||||
return L("decompression failed or archive is corrupted");
|
||||
case MZ_ZIP_COMPRESSION_FAILED:
|
||||
return L("compression failed");
|
||||
case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE:
|
||||
return L("unexpected decompressed size");
|
||||
case MZ_ZIP_CRC_CHECK_FAILED:
|
||||
return L("CRC-32 check failed");
|
||||
case MZ_ZIP_UNSUPPORTED_CDIR_SIZE:
|
||||
return L("unsupported central directory size");
|
||||
case MZ_ZIP_ALLOC_FAILED:
|
||||
return L("allocation failed");
|
||||
case MZ_ZIP_FILE_OPEN_FAILED:
|
||||
return L("file open failed");
|
||||
case MZ_ZIP_FILE_CREATE_FAILED:
|
||||
return L("file create failed");
|
||||
case MZ_ZIP_FILE_WRITE_FAILED:
|
||||
return L("file write failed");
|
||||
case MZ_ZIP_FILE_READ_FAILED:
|
||||
return L("file read failed");
|
||||
case MZ_ZIP_FILE_CLOSE_FAILED:
|
||||
return L("file close failed");
|
||||
case MZ_ZIP_FILE_SEEK_FAILED:
|
||||
return L("file seek failed");
|
||||
case MZ_ZIP_FILE_STAT_FAILED:
|
||||
return L("file stat failed");
|
||||
case MZ_ZIP_INVALID_PARAMETER:
|
||||
return L("invalid parameter");
|
||||
case MZ_ZIP_INVALID_FILENAME:
|
||||
return L("invalid filename");
|
||||
case MZ_ZIP_BUF_TOO_SMALL:
|
||||
return L("buffer too small");
|
||||
case MZ_ZIP_INTERNAL_ERROR:
|
||||
return L("internal error");
|
||||
case MZ_ZIP_FILE_NOT_FOUND:
|
||||
return L("file not found");
|
||||
case MZ_ZIP_ARCHIVE_TOO_LARGE:
|
||||
return L("archive is too large");
|
||||
case MZ_ZIP_VALIDATION_FAILED:
|
||||
return L("validation failed");
|
||||
case MZ_ZIP_WRITE_CALLBACK_FAILED:
|
||||
return L("write calledback failed");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return "unknown error";
|
||||
}
|
||||
|
||||
std::string formatted_errorstr() const
|
||||
{
|
||||
return L("Error with zip archive") + " " + m_zipname + ": " +
|
||||
get_errorstr(arch.m_last_error) + "!";
|
||||
get_errorstr() + "!";
|
||||
}
|
||||
|
||||
SLIC3R_NORETURN void blow_up() const
|
||||
@ -167,7 +91,7 @@ void Zipper::add_entry(const std::string &name)
|
||||
m_entry = name;
|
||||
}
|
||||
|
||||
void Zipper::add_entry(const std::string &name, const uint8_t *data, size_t l)
|
||||
void Zipper::add_entry(const std::string &name, const void *data, size_t l)
|
||||
{
|
||||
if(!m_impl->is_alive()) return;
|
||||
|
||||
|
@ -28,7 +28,7 @@ public:
|
||||
|
||||
// Will blow up in a runtime exception if the file cannot be created.
|
||||
explicit Zipper(const std::string& zipfname,
|
||||
e_compression level = NO_COMPRESSION);
|
||||
e_compression level = FAST_COMPRESSION);
|
||||
~Zipper();
|
||||
|
||||
// No copies allwed, this is a file resource...
|
||||
@ -49,7 +49,7 @@ public:
|
||||
|
||||
/// Add a new binary file entry with an instantly given byte buffer.
|
||||
/// This method throws exactly like finish_entry() does.
|
||||
void add_entry(const std::string& name, const std::uint8_t* data, size_t l);
|
||||
void add_entry(const std::string& name, const void* data, size_t bytes);
|
||||
|
||||
// Writing data to the archive works like with standard streams. The target
|
||||
// within the zip file is the entry created with the add_entry method.
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Technologies.hpp"
|
||||
#include "Semver.hpp"
|
||||
@ -247,6 +248,37 @@ static inline bool is_approx(Number value, Number test_value)
|
||||
return std::fabs(double(value) - double(test_value)) < double(EPSILON);
|
||||
}
|
||||
|
||||
// A meta-predicate which is true for integers wider than or equal to coord_t
|
||||
template<class I> struct is_scaled_coord
|
||||
{
|
||||
static const constexpr bool value =
|
||||
std::is_integral<I>::value &&
|
||||
std::numeric_limits<I>::digits >=
|
||||
std::numeric_limits<coord_t>::digits;
|
||||
};
|
||||
|
||||
// Meta predicates for floating, 'scaled coord' and generic arithmetic types
|
||||
// Can be used to restrict templates to work for only the specified set of types.
|
||||
// parameter T is the type we want to restrict
|
||||
// parameter O (Optional defaults to T) is the type that the whole expression
|
||||
// will be evaluated to.
|
||||
// e.g. template<class T> FloatingOnly<T, bool> is_nan(T val);
|
||||
// The whole template will be defined only for floating point types and the
|
||||
// return type will be bool.
|
||||
// For more info how to use, see docs for std::enable_if
|
||||
//
|
||||
template<class T, class O = T>
|
||||
using FloatingOnly = std::enable_if_t<std::is_floating_point<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using ScaledCoordOnly = std::enable_if_t<is_scaled_coord<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using IntegerOnly = std::enable_if_t<std::is_integral<T>::value, O>;
|
||||
|
||||
template<class T, class O = T>
|
||||
using ArithmeticOnly = std::enable_if_t<std::is_arithmetic<T>::value, O>;
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
|
@ -1,9 +1,17 @@
|
||||
#include <exception>
|
||||
|
||||
#include "miniz_extension.hpp"
|
||||
|
||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||
#include "boost/nowide/cstdio.hpp"
|
||||
#endif
|
||||
|
||||
#include "I18N.hpp"
|
||||
|
||||
//! macro used to mark string used at localization,
|
||||
//! return same string
|
||||
#define L(s) Slic3r::I18N::translate(s)
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
@ -68,4 +76,84 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
||||
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
|
||||
|
||||
MZ_Archive::MZ_Archive()
|
||||
{
|
||||
mz_zip_zero_struct(&arch);
|
||||
}
|
||||
|
||||
std::string MZ_Archive::get_errorstr(mz_zip_error mz_err)
|
||||
{
|
||||
switch (mz_err)
|
||||
{
|
||||
case MZ_ZIP_NO_ERROR:
|
||||
return "no error";
|
||||
case MZ_ZIP_UNDEFINED_ERROR:
|
||||
return L("undefined error");
|
||||
case MZ_ZIP_TOO_MANY_FILES:
|
||||
return L("too many files");
|
||||
case MZ_ZIP_FILE_TOO_LARGE:
|
||||
return L("file too large");
|
||||
case MZ_ZIP_UNSUPPORTED_METHOD:
|
||||
return L("unsupported method");
|
||||
case MZ_ZIP_UNSUPPORTED_ENCRYPTION:
|
||||
return L("unsupported encryption");
|
||||
case MZ_ZIP_UNSUPPORTED_FEATURE:
|
||||
return L("unsupported feature");
|
||||
case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR:
|
||||
return L("failed finding central directory");
|
||||
case MZ_ZIP_NOT_AN_ARCHIVE:
|
||||
return L("not a ZIP archive");
|
||||
case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED:
|
||||
return L("invalid header or archive is corrupted");
|
||||
case MZ_ZIP_UNSUPPORTED_MULTIDISK:
|
||||
return L("unsupported multidisk archive");
|
||||
case MZ_ZIP_DECOMPRESSION_FAILED:
|
||||
return L("decompression failed or archive is corrupted");
|
||||
case MZ_ZIP_COMPRESSION_FAILED:
|
||||
return L("compression failed");
|
||||
case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE:
|
||||
return L("unexpected decompressed size");
|
||||
case MZ_ZIP_CRC_CHECK_FAILED:
|
||||
return L("CRC-32 check failed");
|
||||
case MZ_ZIP_UNSUPPORTED_CDIR_SIZE:
|
||||
return L("unsupported central directory size");
|
||||
case MZ_ZIP_ALLOC_FAILED:
|
||||
return L("allocation failed");
|
||||
case MZ_ZIP_FILE_OPEN_FAILED:
|
||||
return L("file open failed");
|
||||
case MZ_ZIP_FILE_CREATE_FAILED:
|
||||
return L("file create failed");
|
||||
case MZ_ZIP_FILE_WRITE_FAILED:
|
||||
return L("file write failed");
|
||||
case MZ_ZIP_FILE_READ_FAILED:
|
||||
return L("file read failed");
|
||||
case MZ_ZIP_FILE_CLOSE_FAILED:
|
||||
return L("file close failed");
|
||||
case MZ_ZIP_FILE_SEEK_FAILED:
|
||||
return L("file seek failed");
|
||||
case MZ_ZIP_FILE_STAT_FAILED:
|
||||
return L("file stat failed");
|
||||
case MZ_ZIP_INVALID_PARAMETER:
|
||||
return L("invalid parameter");
|
||||
case MZ_ZIP_INVALID_FILENAME:
|
||||
return L("invalid filename");
|
||||
case MZ_ZIP_BUF_TOO_SMALL:
|
||||
return L("buffer too small");
|
||||
case MZ_ZIP_INTERNAL_ERROR:
|
||||
return L("internal error");
|
||||
case MZ_ZIP_FILE_NOT_FOUND:
|
||||
return L("file not found");
|
||||
case MZ_ZIP_ARCHIVE_TOO_LARGE:
|
||||
return L("archive is too large");
|
||||
case MZ_ZIP_VALIDATION_FAILED:
|
||||
return L("validation failed");
|
||||
case MZ_ZIP_WRITE_CALLBACK_FAILED:
|
||||
return L("write calledback failed");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return "unknown error";
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
@ -11,6 +11,25 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
|
||||
bool close_zip_reader(mz_zip_archive *zip);
|
||||
bool close_zip_writer(mz_zip_archive *zip);
|
||||
|
||||
}
|
||||
class MZ_Archive {
|
||||
public:
|
||||
mz_zip_archive arch;
|
||||
|
||||
MZ_Archive();
|
||||
|
||||
static std::string get_errorstr(mz_zip_error mz_err);
|
||||
|
||||
std::string get_errorstr() const
|
||||
{
|
||||
return get_errorstr(arch.m_last_error) + "!";
|
||||
}
|
||||
|
||||
bool is_alive() const
|
||||
{
|
||||
return arch.m_zip_mode != MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // MINIZ_EXTENSION_HPP
|
||||
|
@ -144,12 +144,19 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/UpdateDialogs.hpp
|
||||
GUI/FirmwareDialog.cpp
|
||||
GUI/FirmwareDialog.hpp
|
||||
GUI/ProgressIndicator.hpp
|
||||
GUI/ProgressStatusBar.hpp
|
||||
GUI/ProgressStatusBar.cpp
|
||||
GUI/PrintHostDialogs.cpp
|
||||
GUI/PrintHostDialogs.hpp
|
||||
GUI/Job.hpp
|
||||
GUI/Jobs/Job.hpp
|
||||
GUI/Jobs/Job.cpp
|
||||
GUI/Jobs/ArrangeJob.hpp
|
||||
GUI/Jobs/ArrangeJob.cpp
|
||||
GUI/Jobs/RotoptimizeJob.hpp
|
||||
GUI/Jobs/RotoptimizeJob.cpp
|
||||
GUI/Jobs/SLAImportJob.hpp
|
||||
GUI/Jobs/SLAImportJob.cpp
|
||||
GUI/Jobs/ProgressIndicator.hpp
|
||||
GUI/ProgressStatusBar.hpp
|
||||
GUI/ProgressStatusBar.cpp
|
||||
GUI/Mouse3DController.cpp
|
||||
GUI/Mouse3DController.hpp
|
||||
GUI/DoubleSlider.cpp
|
||||
@ -179,6 +186,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/HexFile.cpp
|
||||
Utils/HexFile.hpp
|
||||
Utils/Thread.hpp
|
||||
Utils/SLAImport.hpp
|
||||
Utils/SLAImport.cpp
|
||||
)
|
||||
|
||||
if (APPLE)
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/GCode/PostProcessor.hpp"
|
||||
#include "libslic3r/GCode/PreviewData.hpp"
|
||||
#include "libslic3r/Format/SL1.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <cassert>
|
||||
@ -153,7 +154,7 @@ void BackgroundSlicingProcess::process_sla()
|
||||
const std::string export_path = m_sla_print->print_statistics().finalize_output_path(m_export_path);
|
||||
|
||||
Zipper zipper(export_path);
|
||||
m_sla_print->export_raster(zipper);
|
||||
m_sla_archive.export_print(zipper, *m_sla_print);
|
||||
|
||||
if (m_thumbnail_cb != nullptr)
|
||||
{
|
||||
@ -493,7 +494,7 @@ void BackgroundSlicingProcess::prepare_upload()
|
||||
m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string());
|
||||
|
||||
Zipper zipper{source_path.string()};
|
||||
m_sla_print->export_raster(zipper, m_upload_job.upload_data.upload_path.string());
|
||||
m_sla_archive.export_print(zipper, *m_sla_print, m_upload_job.upload_data.upload_path.string());
|
||||
if (m_thumbnail_cb != nullptr)
|
||||
{
|
||||
ThumbnailsList thumbnails;
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include <wx/event.h>
|
||||
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Format/SL1.hpp"
|
||||
#include "slic3r/Utils/PrintHost.hpp"
|
||||
|
||||
|
||||
@ -19,6 +20,7 @@ class DynamicPrintConfig;
|
||||
class GCodePreviewData;
|
||||
class Model;
|
||||
class SLAPrint;
|
||||
class SL1Archive;
|
||||
|
||||
class SlicingStatusEvent : public wxEvent
|
||||
{
|
||||
@ -47,7 +49,7 @@ public:
|
||||
~BackgroundSlicingProcess();
|
||||
|
||||
void set_fff_print(Print *print) { m_fff_print = print; }
|
||||
void set_sla_print(SLAPrint *print) { m_sla_print = print; }
|
||||
void set_sla_print(SLAPrint *print) { m_sla_print = print; m_sla_print->set_printer(&m_sla_archive); }
|
||||
void set_gcode_preview_data(GCodePreviewData *gpd) { m_gcode_preview_data = gpd; }
|
||||
void set_thumbnail_cb(ThumbnailsGeneratorCallback cb) { m_thumbnail_cb = cb; }
|
||||
#if ENABLE_GCODE_VIEWER
|
||||
@ -158,6 +160,7 @@ private:
|
||||
GCodePreviewData *m_gcode_preview_data = nullptr;
|
||||
// Callback function, used to write thumbnails into gcode.
|
||||
ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr;
|
||||
SL1Archive m_sla_archive;
|
||||
#if ENABLE_GCODE_VIEWER
|
||||
GCodeProcessor::Result* m_gcode_result = nullptr;
|
||||
#endif // ENABLE_GCODE_VIEWER
|
||||
|
@ -624,12 +624,6 @@ void GCodeViewer::render_shells() const
|
||||
}
|
||||
|
||||
void GCodeViewer::render_overlay() const
|
||||
{
|
||||
render_legend();
|
||||
render_toolbar();
|
||||
}
|
||||
|
||||
void GCodeViewer::render_legend() const
|
||||
{
|
||||
static const ImVec4 ORANGE(1.0f, 0.49f, 0.22f, 1.0f);
|
||||
static const float ICON_BORDER_SIZE = 25.0f;
|
||||
@ -803,10 +797,6 @@ void GCodeViewer::render_legend() const
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
void GCodeViewer::render_toolbar() const
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
|
@ -160,7 +160,7 @@ private:
|
||||
std::vector<unsigned char> m_extruder_ids;
|
||||
Extrusions m_extrusions;
|
||||
Shells m_shells;
|
||||
mutable EViewType m_view_type{ EViewType::FeatureType };
|
||||
EViewType m_view_type{ EViewType::FeatureType };
|
||||
bool m_legend_enabled{ true };
|
||||
|
||||
public:
|
||||
@ -208,8 +208,6 @@ private:
|
||||
void render_toolpaths() const;
|
||||
void render_shells() const;
|
||||
void render_overlay() const;
|
||||
void render_legend() const;
|
||||
void render_toolbar() const;
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include <wx/filefn.h>
|
||||
#include <wx/sysopt.h>
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/richmsgdlg.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/intl.h>
|
||||
|
||||
@ -321,11 +322,6 @@ bool GUI_App::on_init_inner()
|
||||
set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data());
|
||||
|
||||
app_config = new AppConfig();
|
||||
preset_bundle = new PresetBundle();
|
||||
|
||||
// just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory
|
||||
// supplied as argument to --datadir; in that case we should still run the wizard
|
||||
preset_bundle->setup_directories();
|
||||
|
||||
// load settings
|
||||
app_conf_exists = app_config->exists();
|
||||
@ -333,9 +329,35 @@ bool GUI_App::on_init_inner()
|
||||
app_config->load();
|
||||
}
|
||||
|
||||
std::string msg = Http::tls_global_init();
|
||||
wxRichMessageDialog
|
||||
dlg(nullptr,
|
||||
wxString::Format(_(L("%s\nDo you want to continue?")), _(msg)),
|
||||
"PrusaSlicer", wxICON_QUESTION | wxYES_NO);
|
||||
|
||||
bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes";
|
||||
std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location");
|
||||
ssl_accept = ssl_accept && ssl_cert_store == Http::tls_system_cert_store();
|
||||
|
||||
dlg.ShowCheckBox(_(L("Remember my choice")));
|
||||
if (!msg.empty() && !ssl_accept) {
|
||||
if (dlg.ShowModal() != wxID_YES) return false;
|
||||
|
||||
app_config->set("tls_cert_store_accepted",
|
||||
dlg.IsCheckBoxChecked() ? "yes" : "no");
|
||||
app_config->set("tls_accepted_cert_store_location",
|
||||
dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : "");
|
||||
}
|
||||
|
||||
app_config->set("version", SLIC3R_VERSION);
|
||||
app_config->save();
|
||||
|
||||
preset_bundle = new PresetBundle();
|
||||
|
||||
// just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory
|
||||
// supplied as argument to --datadir; in that case we should still run the wizard
|
||||
preset_bundle->setup_directories();
|
||||
|
||||
#ifdef __WXMSW__
|
||||
associate_3mf_files();
|
||||
#endif // __WXMSW__
|
||||
|
@ -2071,10 +2071,13 @@ void ObjectList::load_shape_object(const std::string& type_name)
|
||||
// Create mesh
|
||||
BoundingBoxf3 bb;
|
||||
TriangleMesh mesh = create_mesh(type_name, bb);
|
||||
load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name));
|
||||
}
|
||||
|
||||
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name)
|
||||
{
|
||||
// Add mesh to model as a new object
|
||||
Model& model = wxGetApp().plater()->model();
|
||||
const wxString name = _(L("Shape")) + "-" + _(type_name);
|
||||
|
||||
#ifdef _DEBUG
|
||||
check_model_ids_validity(model);
|
||||
|
@ -24,6 +24,7 @@ class ConfigOptionsGroup;
|
||||
class DynamicPrintConfig;
|
||||
class ModelObject;
|
||||
class ModelVolume;
|
||||
class TriangleMesh;
|
||||
enum class ModelVolumeType : int;
|
||||
|
||||
// FIXME: broken build on mac os because of this is missing:
|
||||
@ -265,6 +266,7 @@ public:
|
||||
void load_part(ModelObject* model_object, std::vector<std::pair<wxString, bool>> &volumes_info, ModelVolumeType type);
|
||||
void load_generic_subobject(const std::string& type_name, const ModelVolumeType type);
|
||||
void load_shape_object(const std::string &type_name);
|
||||
void load_mesh_object(const TriangleMesh &mesh, const wxString &name);
|
||||
void del_object(const int obj_idx);
|
||||
void del_subobject_item(wxDataViewItem& item);
|
||||
void del_settings_from_config(const wxDataViewItem& parent_item);
|
||||
|
@ -1,155 +0,0 @@
|
||||
#ifndef JOB_HPP
|
||||
#define JOB_HPP
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <slic3r/Utils/Thread.hpp>
|
||||
#include <slic3r/GUI/I18N.hpp>
|
||||
#include <slic3r/GUI/ProgressIndicator.hpp>
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A class to handle UI jobs like arranging and optimizing rotation.
|
||||
// These are not instant jobs, the user has to be informed about their
|
||||
// state in the status progress indicator. On the other hand they are
|
||||
// separated from the background slicing process. Ideally, these jobs should
|
||||
// run when the background process is not running.
|
||||
//
|
||||
// TODO: A mechanism would be useful for blocking the plater interactions:
|
||||
// objects would be frozen for the user. In case of arrange, an animation
|
||||
// could be shown, or with the optimize orientations, partial results
|
||||
// could be displayed.
|
||||
class Job : public wxEvtHandler
|
||||
{
|
||||
int m_range = 100;
|
||||
boost::thread m_thread;
|
||||
std::atomic<bool> m_running{false}, m_canceled{false};
|
||||
bool m_finalized = false;
|
||||
std::shared_ptr<ProgressIndicator> m_progress;
|
||||
|
||||
void run()
|
||||
{
|
||||
m_running.store(true);
|
||||
process();
|
||||
m_running.store(false);
|
||||
|
||||
// ensure to call the last status to finalize the job
|
||||
update_status(status_range(), "");
|
||||
}
|
||||
|
||||
protected:
|
||||
// status range for a particular job
|
||||
virtual int status_range() const { return 100; }
|
||||
|
||||
// status update, to be used from the work thread (process() method)
|
||||
void update_status(int st, const wxString &msg = "")
|
||||
{
|
||||
auto evt = new wxThreadEvent();
|
||||
evt->SetInt(st);
|
||||
evt->SetString(msg);
|
||||
wxQueueEvent(this, evt);
|
||||
}
|
||||
|
||||
bool was_canceled() const { return m_canceled.load(); }
|
||||
|
||||
// Launched just before start(), a job can use it to prepare internals
|
||||
virtual void prepare() {}
|
||||
|
||||
// Launched when the job is finished. It refreshes the 3Dscene by def.
|
||||
virtual void finalize() { m_finalized = true; }
|
||||
|
||||
|
||||
public:
|
||||
Job(std::shared_ptr<ProgressIndicator> pri) : m_progress(pri)
|
||||
{
|
||||
Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) {
|
||||
auto msg = evt.GetString();
|
||||
if (!msg.empty())
|
||||
m_progress->set_status_text(msg.ToUTF8().data());
|
||||
|
||||
if (m_finalized) return;
|
||||
|
||||
m_progress->set_progress(evt.GetInt());
|
||||
if (evt.GetInt() == status_range()) {
|
||||
// set back the original range and cancel callback
|
||||
m_progress->set_range(m_range);
|
||||
m_progress->set_cancel_callback();
|
||||
wxEndBusyCursor();
|
||||
|
||||
finalize();
|
||||
|
||||
// dont do finalization again for the same process
|
||||
m_finalized = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool is_finalized() const { return m_finalized; }
|
||||
|
||||
Job(const Job &) = delete;
|
||||
Job(Job &&) = delete;
|
||||
Job &operator=(const Job &) = delete;
|
||||
Job &operator=(Job &&) = delete;
|
||||
|
||||
virtual void process() = 0;
|
||||
|
||||
void start()
|
||||
{ // Start the job. No effect if the job is already running
|
||||
if (!m_running.load()) {
|
||||
prepare();
|
||||
|
||||
// Save the current status indicatior range and push the new one
|
||||
m_range = m_progress->get_range();
|
||||
m_progress->set_range(status_range());
|
||||
|
||||
// init cancellation flag and set the cancel callback
|
||||
m_canceled.store(false);
|
||||
m_progress->set_cancel_callback(
|
||||
[this]() { m_canceled.store(true); });
|
||||
|
||||
m_finalized = false;
|
||||
|
||||
// Changing cursor to busy
|
||||
wxBeginBusyCursor();
|
||||
|
||||
try { // Execute the job
|
||||
m_thread = create_thread([this] { this->run(); });
|
||||
} catch (std::exception &) {
|
||||
update_status(status_range(),
|
||||
_(L("ERROR: not enough resources to "
|
||||
"execute a new job.")));
|
||||
}
|
||||
|
||||
// The state changes will be undone when the process hits the
|
||||
// last status value, in the status update handler (see ctor)
|
||||
}
|
||||
}
|
||||
|
||||
// To wait for the running job and join the threads. False is
|
||||
// returned if the timeout has been reached and the job is still
|
||||
// running. Call cancel() before this fn if you want to explicitly
|
||||
// end the job.
|
||||
bool join(int timeout_ms = 0)
|
||||
{
|
||||
if (!m_thread.joinable()) return true;
|
||||
|
||||
if (timeout_ms <= 0)
|
||||
m_thread.join();
|
||||
else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_running() const { return m_running.load(); }
|
||||
void cancel() { m_canceled.store(true); }
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif // JOB_HPP
|
223
src/slic3r/GUI/Jobs/ArrangeJob.cpp
Normal file
223
src/slic3r/GUI/Jobs/ArrangeJob.cpp
Normal file
@ -0,0 +1,223 @@
|
||||
#include "ArrangeJob.hpp"
|
||||
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Cache the wti info
|
||||
class WipeTower: public GLCanvas3D::WipeTowerInfo {
|
||||
using ArrangePolygon = arrangement::ArrangePolygon;
|
||||
public:
|
||||
explicit WipeTower(const GLCanvas3D::WipeTowerInfo &wti)
|
||||
: GLCanvas3D::WipeTowerInfo(wti)
|
||||
{}
|
||||
|
||||
explicit WipeTower(GLCanvas3D::WipeTowerInfo &&wti)
|
||||
: GLCanvas3D::WipeTowerInfo(std::move(wti))
|
||||
{}
|
||||
|
||||
void apply_arrange_result(const Vec2d& tr, double rotation)
|
||||
{
|
||||
m_pos = unscaled(tr); m_rotation = rotation;
|
||||
apply_wipe_tower();
|
||||
}
|
||||
|
||||
ArrangePolygon get_arrange_polygon() const
|
||||
{
|
||||
Polygon ap({
|
||||
{coord_t(0), coord_t(0)},
|
||||
{scaled(m_bb_size(X)), coord_t(0)},
|
||||
{scaled(m_bb_size)},
|
||||
{coord_t(0), scaled(m_bb_size(Y))},
|
||||
{coord_t(0), coord_t(0)},
|
||||
});
|
||||
|
||||
ArrangePolygon ret;
|
||||
ret.poly.contour = std::move(ap);
|
||||
ret.translation = scaled(m_pos);
|
||||
ret.rotation = m_rotation;
|
||||
ret.priority++;
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
static WipeTower get_wipe_tower(Plater &plater)
|
||||
{
|
||||
return WipeTower{plater.canvas3D()->get_wipe_tower_info()};
|
||||
}
|
||||
|
||||
void ArrangeJob::clear_input()
|
||||
{
|
||||
const Model &model = m_plater->model();
|
||||
|
||||
size_t count = 0, cunprint = 0; // To know how much space to reserve
|
||||
for (auto obj : model.objects)
|
||||
for (auto mi : obj->instances)
|
||||
mi->printable ? count++ : cunprint++;
|
||||
|
||||
m_selected.clear();
|
||||
m_unselected.clear();
|
||||
m_unprintable.clear();
|
||||
m_selected.reserve(count + 1 /* for optional wti */);
|
||||
m_unselected.reserve(count + 1 /* for optional wti */);
|
||||
m_unprintable.reserve(cunprint /* for optional wti */);
|
||||
}
|
||||
|
||||
double ArrangeJob::bed_stride() const {
|
||||
double bedwidth = m_plater->bed_shape_bb().size().x();
|
||||
return scaled<double>((1. + LOGICAL_BED_GAP) * bedwidth);
|
||||
}
|
||||
|
||||
void ArrangeJob::prepare_all() {
|
||||
clear_input();
|
||||
|
||||
for (ModelObject *obj: m_plater->model().objects)
|
||||
for (ModelInstance *mi : obj->instances) {
|
||||
ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable;
|
||||
cont.emplace_back(get_arrange_poly(mi));
|
||||
}
|
||||
|
||||
if (auto wti = get_wipe_tower(*m_plater))
|
||||
m_selected.emplace_back(wti.get_arrange_polygon());
|
||||
}
|
||||
|
||||
void ArrangeJob::prepare_selected() {
|
||||
clear_input();
|
||||
|
||||
Model &model = m_plater->model();
|
||||
double stride = bed_stride();
|
||||
|
||||
std::vector<const Selection::InstanceIdxsList *>
|
||||
obj_sel(model.objects.size(), nullptr);
|
||||
|
||||
for (auto &s : m_plater->get_selection().get_content())
|
||||
if (s.first < int(obj_sel.size()))
|
||||
obj_sel[size_t(s.first)] = &s.second;
|
||||
|
||||
// Go through the objects and check if inside the selection
|
||||
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
|
||||
const Selection::InstanceIdxsList * instlist = obj_sel[oidx];
|
||||
ModelObject *mo = model.objects[oidx];
|
||||
|
||||
std::vector<bool> inst_sel(mo->instances.size(), false);
|
||||
|
||||
if (instlist)
|
||||
for (auto inst_id : *instlist)
|
||||
inst_sel[size_t(inst_id)] = true;
|
||||
|
||||
for (size_t i = 0; i < inst_sel.size(); ++i) {
|
||||
ArrangePolygon &&ap = get_arrange_poly(mo->instances[i]);
|
||||
|
||||
ArrangePolygons &cont = mo->instances[i]->printable ?
|
||||
(inst_sel[i] ? m_selected :
|
||||
m_unselected) :
|
||||
m_unprintable;
|
||||
|
||||
cont.emplace_back(std::move(ap));
|
||||
}
|
||||
}
|
||||
|
||||
if (auto wti = get_wipe_tower(*m_plater)) {
|
||||
ArrangePolygon &&ap = get_arrange_poly(&wti);
|
||||
|
||||
m_plater->get_selection().is_wipe_tower() ?
|
||||
m_selected.emplace_back(std::move(ap)) :
|
||||
m_unselected.emplace_back(std::move(ap));
|
||||
}
|
||||
|
||||
// If the selection was empty arrange everything
|
||||
if (m_selected.empty()) m_selected.swap(m_unselected);
|
||||
|
||||
// The strides have to be removed from the fixed items. For the
|
||||
// arrangeable (selected) items bed_idx is ignored and the
|
||||
// translation is irrelevant.
|
||||
for (auto &p : m_unselected) p.translation(X) -= p.bed_idx * stride;
|
||||
}
|
||||
|
||||
void ArrangeJob::prepare()
|
||||
{
|
||||
wxGetKeyState(WXK_SHIFT) ? prepare_selected() : prepare_all();
|
||||
}
|
||||
|
||||
void ArrangeJob::process()
|
||||
{
|
||||
static const auto arrangestr = _(L("Arranging"));
|
||||
|
||||
double dist = min_object_distance(*m_plater->config());
|
||||
|
||||
arrangement::ArrangeParams params;
|
||||
params.min_obj_distance = scaled(dist);
|
||||
|
||||
auto count = unsigned(m_selected.size() + m_unprintable.size());
|
||||
Points bedpts = get_bed_shape(*m_plater->config());
|
||||
|
||||
params.stopcondition = [this]() { return was_canceled(); };
|
||||
|
||||
try {
|
||||
params.progressind = [this, count](unsigned st) {
|
||||
st += m_unprintable.size();
|
||||
if (st > 0) update_status(int(count - st), arrangestr);
|
||||
};
|
||||
|
||||
arrangement::arrange(m_selected, m_unselected, bedpts, params);
|
||||
|
||||
params.progressind = [this, count](unsigned st) {
|
||||
if (st > 0) update_status(int(count - st), arrangestr);
|
||||
};
|
||||
|
||||
arrangement::arrange(m_unprintable, {}, bedpts, params);
|
||||
} catch (std::exception & /*e*/) {
|
||||
GUI::show_error(m_plater,
|
||||
_(L("Could not arrange model objects! "
|
||||
"Some geometries may be invalid.")));
|
||||
}
|
||||
|
||||
// finalize just here.
|
||||
update_status(int(count),
|
||||
was_canceled() ? _(L("Arranging canceled."))
|
||||
: _(L("Arranging done.")));
|
||||
}
|
||||
|
||||
void ArrangeJob::finalize() {
|
||||
// Ignore the arrange result if aborted.
|
||||
if (was_canceled()) return;
|
||||
|
||||
// Unprintable items go to the last virtual bed
|
||||
int beds = 0;
|
||||
|
||||
// Apply the arrange result to all selected objects
|
||||
for (ArrangePolygon &ap : m_selected) {
|
||||
beds = std::max(ap.bed_idx, beds);
|
||||
ap.apply();
|
||||
}
|
||||
|
||||
// Get the virtual beds from the unselected items
|
||||
for (ArrangePolygon &ap : m_unselected)
|
||||
beds = std::max(ap.bed_idx, beds);
|
||||
|
||||
// Move the unprintable items to the last virtual bed.
|
||||
for (ArrangePolygon &ap : m_unprintable) {
|
||||
ap.bed_idx += beds + 1;
|
||||
ap.apply();
|
||||
}
|
||||
|
||||
m_plater->update();
|
||||
|
||||
Job::finalize();
|
||||
}
|
||||
|
||||
arrangement::ArrangePolygon get_wipe_tower_arrangepoly(Plater &plater)
|
||||
{
|
||||
return WipeTower{plater.canvas3D()->get_wipe_tower_info()}.get_arrange_polygon();
|
||||
}
|
||||
|
||||
void apply_wipe_tower_arrangepoly(Plater &plater, const arrangement::ArrangePolygon &ap)
|
||||
{
|
||||
WipeTower{plater.canvas3D()->get_wipe_tower_info()}.apply_arrange_result(ap.translation.cast<double>(), ap.rotation);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
77
src/slic3r/GUI/Jobs/ArrangeJob.hpp
Normal file
77
src/slic3r/GUI/Jobs/ArrangeJob.hpp
Normal file
@ -0,0 +1,77 @@
|
||||
#ifndef ARRANGEJOB_HPP
|
||||
#define ARRANGEJOB_HPP
|
||||
|
||||
#include "Job.hpp"
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class Plater;
|
||||
|
||||
class ArrangeJob : public Job
|
||||
{
|
||||
Plater *m_plater;
|
||||
|
||||
using ArrangePolygon = arrangement::ArrangePolygon;
|
||||
using ArrangePolygons = arrangement::ArrangePolygons;
|
||||
|
||||
// The gap between logical beds in the x axis expressed in ratio of
|
||||
// the current bed width.
|
||||
static const constexpr double LOGICAL_BED_GAP = 1. / 5.;
|
||||
|
||||
ArrangePolygons m_selected, m_unselected, m_unprintable;
|
||||
|
||||
// clear m_selected and m_unselected, reserve space for next usage
|
||||
void clear_input();
|
||||
|
||||
// Stride between logical beds
|
||||
double bed_stride() const;
|
||||
|
||||
// Set up arrange polygon for a ModelInstance and Wipe tower
|
||||
template<class T> ArrangePolygon get_arrange_poly(T *obj) const
|
||||
{
|
||||
ArrangePolygon ap = obj->get_arrange_polygon();
|
||||
ap.priority = 0;
|
||||
ap.bed_idx = ap.translation.x() / bed_stride();
|
||||
ap.setter = [obj, this](const ArrangePolygon &p) {
|
||||
if (p.is_arranged()) {
|
||||
Vec2d t = p.translation.cast<double>();
|
||||
t.x() += p.bed_idx * bed_stride();
|
||||
obj->apply_arrange_result(t, p.rotation);
|
||||
}
|
||||
};
|
||||
return ap;
|
||||
}
|
||||
|
||||
// Prepare all objects on the bed regardless of the selection
|
||||
void prepare_all();
|
||||
|
||||
// Prepare the selected and unselected items separately. If nothing is
|
||||
// selected, behaves as if everything would be selected.
|
||||
void prepare_selected();
|
||||
|
||||
protected:
|
||||
|
||||
void prepare() override;
|
||||
|
||||
public:
|
||||
ArrangeJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater)
|
||||
: Job{std::move(pri)}, m_plater{plater}
|
||||
{}
|
||||
|
||||
int status_range() const override
|
||||
{
|
||||
return int(m_selected.size() + m_unprintable.size());
|
||||
}
|
||||
|
||||
void process() override;
|
||||
|
||||
void finalize() override;
|
||||
};
|
||||
|
||||
arrangement::ArrangePolygon get_wipe_tower_arrangepoly(Plater &);
|
||||
void apply_wipe_tower_arrangepoly(Plater &plater, const arrangement::ArrangePolygon &ap);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // ARRANGEJOB_HPP
|
121
src/slic3r/GUI/Jobs/Job.cpp
Normal file
121
src/slic3r/GUI/Jobs/Job.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "Job.hpp"
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void GUI::Job::run()
|
||||
{
|
||||
m_running.store(true);
|
||||
process();
|
||||
m_running.store(false);
|
||||
|
||||
// ensure to call the last status to finalize the job
|
||||
update_status(status_range(), "");
|
||||
}
|
||||
|
||||
void GUI::Job::update_status(int st, const wxString &msg)
|
||||
{
|
||||
auto evt = new wxThreadEvent();
|
||||
evt->SetInt(st);
|
||||
evt->SetString(msg);
|
||||
wxQueueEvent(this, evt);
|
||||
}
|
||||
|
||||
GUI::Job::Job(std::shared_ptr<ProgressIndicator> pri)
|
||||
: m_progress(std::move(pri))
|
||||
{
|
||||
Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) {
|
||||
auto msg = evt.GetString();
|
||||
if (!msg.empty())
|
||||
m_progress->set_status_text(msg.ToUTF8().data());
|
||||
|
||||
if (m_finalized) return;
|
||||
|
||||
m_progress->set_progress(evt.GetInt());
|
||||
if (evt.GetInt() == status_range()) {
|
||||
// set back the original range and cancel callback
|
||||
m_progress->set_range(m_range);
|
||||
m_progress->set_cancel_callback();
|
||||
wxEndBusyCursor();
|
||||
|
||||
finalize();
|
||||
|
||||
// dont do finalization again for the same process
|
||||
m_finalized = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void GUI::Job::start()
|
||||
{ // Start the job. No effect if the job is already running
|
||||
if (!m_running.load()) {
|
||||
prepare();
|
||||
|
||||
// Save the current status indicatior range and push the new one
|
||||
m_range = m_progress->get_range();
|
||||
m_progress->set_range(status_range());
|
||||
|
||||
// init cancellation flag and set the cancel callback
|
||||
m_canceled.store(false);
|
||||
m_progress->set_cancel_callback(
|
||||
[this]() { m_canceled.store(true); });
|
||||
|
||||
m_finalized = false;
|
||||
|
||||
// Changing cursor to busy
|
||||
wxBeginBusyCursor();
|
||||
|
||||
try { // Execute the job
|
||||
m_thread = create_thread([this] { this->run(); });
|
||||
} catch (std::exception &) {
|
||||
update_status(status_range(),
|
||||
_(L("ERROR: not enough resources to "
|
||||
"execute a new job.")));
|
||||
}
|
||||
|
||||
// The state changes will be undone when the process hits the
|
||||
// last status value, in the status update handler (see ctor)
|
||||
}
|
||||
}
|
||||
|
||||
bool GUI::Job::join(int timeout_ms)
|
||||
{
|
||||
if (!m_thread.joinable()) return true;
|
||||
|
||||
if (timeout_ms <= 0)
|
||||
m_thread.join();
|
||||
else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GUI::ExclusiveJobGroup::start(size_t jid) {
|
||||
assert(jid < m_jobs.size());
|
||||
stop_all();
|
||||
m_jobs[jid]->start();
|
||||
}
|
||||
|
||||
void GUI::ExclusiveJobGroup::join_all(int wait_ms)
|
||||
{
|
||||
std::vector<bool> aborted(m_jobs.size(), false);
|
||||
|
||||
for (size_t jid = 0; jid < m_jobs.size(); ++jid)
|
||||
aborted[jid] = m_jobs[jid]->join(wait_ms);
|
||||
|
||||
if (!std::all_of(aborted.begin(), aborted.end(), [](bool t) { return t; }))
|
||||
BOOST_LOG_TRIVIAL(error) << "Could not abort a job!";
|
||||
}
|
||||
|
||||
bool GUI::ExclusiveJobGroup::is_any_running() const
|
||||
{
|
||||
return std::any_of(m_jobs.begin(), m_jobs.end(),
|
||||
[](const std::unique_ptr<GUI::Job> &j) {
|
||||
return j->is_running();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
110
src/slic3r/GUI/Jobs/Job.hpp
Normal file
110
src/slic3r/GUI/Jobs/Job.hpp
Normal file
@ -0,0 +1,110 @@
|
||||
#ifndef JOB_HPP
|
||||
#define JOB_HPP
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <slic3r/Utils/Thread.hpp>
|
||||
#include <slic3r/GUI/I18N.hpp>
|
||||
|
||||
#include "ProgressIndicator.hpp"
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// A class to handle UI jobs like arranging and optimizing rotation.
|
||||
// These are not instant jobs, the user has to be informed about their
|
||||
// state in the status progress indicator. On the other hand they are
|
||||
// separated from the background slicing process. Ideally, these jobs should
|
||||
// run when the background process is not running.
|
||||
//
|
||||
// TODO: A mechanism would be useful for blocking the plater interactions:
|
||||
// objects would be frozen for the user. In case of arrange, an animation
|
||||
// could be shown, or with the optimize orientations, partial results
|
||||
// could be displayed.
|
||||
class Job : public wxEvtHandler
|
||||
{
|
||||
int m_range = 100;
|
||||
boost::thread m_thread;
|
||||
std::atomic<bool> m_running{false}, m_canceled{false};
|
||||
bool m_finalized = false;
|
||||
std::shared_ptr<ProgressIndicator> m_progress;
|
||||
|
||||
void run();
|
||||
|
||||
protected:
|
||||
// status range for a particular job
|
||||
virtual int status_range() const { return 100; }
|
||||
|
||||
// status update, to be used from the work thread (process() method)
|
||||
void update_status(int st, const wxString &msg = "");
|
||||
|
||||
bool was_canceled() const { return m_canceled.load(); }
|
||||
|
||||
// Launched just before start(), a job can use it to prepare internals
|
||||
virtual void prepare() {}
|
||||
|
||||
// Launched when the job is finished. It refreshes the 3Dscene by def.
|
||||
virtual void finalize() { m_finalized = true; }
|
||||
|
||||
public:
|
||||
Job(std::shared_ptr<ProgressIndicator> pri);
|
||||
|
||||
bool is_finalized() const { return m_finalized; }
|
||||
|
||||
Job(const Job &) = delete;
|
||||
Job(Job &&) = delete;
|
||||
Job &operator=(const Job &) = delete;
|
||||
Job &operator=(Job &&) = delete;
|
||||
|
||||
virtual void process() = 0;
|
||||
|
||||
void start();
|
||||
|
||||
// To wait for the running job and join the threads. False is
|
||||
// returned if the timeout has been reached and the job is still
|
||||
// running. Call cancel() before this fn if you want to explicitly
|
||||
// end the job.
|
||||
bool join(int timeout_ms = 0);
|
||||
|
||||
bool is_running() const { return m_running.load(); }
|
||||
void cancel() { m_canceled.store(true); }
|
||||
};
|
||||
|
||||
// Jobs defined inside the group class will be managed so that only one can
|
||||
// run at a time. Also, the background process will be stopped if a job is
|
||||
// started.
|
||||
class ExclusiveJobGroup
|
||||
{
|
||||
static const int ABORT_WAIT_MAX_MS = 10000;
|
||||
|
||||
std::vector<std::unique_ptr<GUI::Job>> m_jobs;
|
||||
|
||||
protected:
|
||||
virtual void before_start() {}
|
||||
|
||||
public:
|
||||
virtual ~ExclusiveJobGroup() = default;
|
||||
|
||||
size_t add_job(std::unique_ptr<GUI::Job> &&job)
|
||||
{
|
||||
m_jobs.emplace_back(std::move(job));
|
||||
return m_jobs.size() - 1;
|
||||
}
|
||||
|
||||
void start(size_t jid);
|
||||
|
||||
void cancel_all() { for (auto& j : m_jobs) j->cancel(); }
|
||||
|
||||
void join_all(int wait_ms = 0);
|
||||
|
||||
void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); }
|
||||
|
||||
bool is_any_running() const;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // JOB_HPP
|
68
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
Normal file
68
src/slic3r/GUI/Jobs/RotoptimizeJob.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
#include "RotoptimizeJob.hpp"
|
||||
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/SLA/Rotfinder.hpp"
|
||||
#include "libslic3r/MinAreaBoundingBox.hpp"
|
||||
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
void RotoptimizeJob::process()
|
||||
{
|
||||
int obj_idx = m_plater->get_selected_object_idx();
|
||||
if (obj_idx < 0) { return; }
|
||||
|
||||
ModelObject *o = m_plater->model().objects[size_t(obj_idx)];
|
||||
|
||||
auto r = sla::find_best_rotation(
|
||||
*o,
|
||||
.005f,
|
||||
[this](unsigned s) {
|
||||
if (s < 100)
|
||||
update_status(int(s),
|
||||
_(L("Searching for optimal orientation")));
|
||||
},
|
||||
[this]() { return was_canceled(); });
|
||||
|
||||
|
||||
double mindist = 6.0; // FIXME
|
||||
|
||||
if (!was_canceled()) {
|
||||
for(ModelInstance * oi : o->instances) {
|
||||
oi->set_rotation({r[X], r[Y], r[Z]});
|
||||
|
||||
auto trmatrix = oi->get_transformation().get_matrix();
|
||||
Polygon trchull = o->convex_hull_2d(trmatrix);
|
||||
|
||||
MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex);
|
||||
double phi = rotbb.angle_to_X();
|
||||
|
||||
// The box should be landscape
|
||||
if(rotbb.width() < rotbb.height()) phi += PI / 2;
|
||||
|
||||
Vec3d rt = oi->get_rotation(); rt(Z) += phi;
|
||||
|
||||
oi->set_rotation(rt);
|
||||
}
|
||||
|
||||
m_plater->find_new_position(o->instances, scaled(mindist));
|
||||
|
||||
// Correct the z offset of the object which was corrupted be
|
||||
// the rotation
|
||||
o->ensure_on_bed();
|
||||
}
|
||||
|
||||
update_status(100, was_canceled() ? _(L("Orientation search canceled.")) :
|
||||
_(L("Orientation found.")));
|
||||
}
|
||||
|
||||
void RotoptimizeJob::finalize()
|
||||
{
|
||||
if (!was_canceled())
|
||||
m_plater->update();
|
||||
|
||||
Job::finalize();
|
||||
}
|
||||
|
||||
}}
|
24
src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
Normal file
24
src/slic3r/GUI/Jobs/RotoptimizeJob.hpp
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef ROTOPTIMIZEJOB_HPP
|
||||
#define ROTOPTIMIZEJOB_HPP
|
||||
|
||||
#include "Job.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class Plater;
|
||||
|
||||
class RotoptimizeJob : public Job
|
||||
{
|
||||
Plater *m_plater;
|
||||
public:
|
||||
RotoptimizeJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater)
|
||||
: Job{std::move(pri)}, m_plater{plater}
|
||||
{}
|
||||
|
||||
void process() override;
|
||||
void finalize() override;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // ROTOPTIMIZEJOB_HPP
|
226
src/slic3r/GUI/Jobs/SLAImportJob.cpp
Normal file
226
src/slic3r/GUI/Jobs/SLAImportJob.cpp
Normal file
@ -0,0 +1,226 @@
|
||||
#include "SLAImportJob.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/AppConfig.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/Utils/SLAImport.hpp"
|
||||
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/combobox.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/filepicker.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
enum class Sel { modelAndProfile, profileOnly, modelOnly};
|
||||
|
||||
class ImportDlg: public wxDialog {
|
||||
wxFilePickerCtrl *m_filepicker;
|
||||
wxComboBox *m_import_dropdown, *m_quality_dropdown;
|
||||
|
||||
public:
|
||||
ImportDlg(Plater *plater)
|
||||
: wxDialog{plater, wxID_ANY, "Import SLA archive"}
|
||||
{
|
||||
auto szvert = new wxBoxSizer{wxVERTICAL};
|
||||
auto szfilepck = new wxBoxSizer{wxHORIZONTAL};
|
||||
|
||||
m_filepicker = new wxFilePickerCtrl(this, wxID_ANY,
|
||||
from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")),
|
||||
"SL1 archive files (*.sl1, *.zip)|*.sl1;*.SL1;*.zip;*.ZIP",
|
||||
wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
|
||||
szfilepck->Add(new wxStaticText(this, wxID_ANY, _(L("Import file: "))), 0, wxALIGN_CENTER);
|
||||
szfilepck->Add(m_filepicker, 1);
|
||||
szvert->Add(szfilepck, 0, wxALL | wxEXPAND, 5);
|
||||
|
||||
auto szchoices = new wxBoxSizer{wxHORIZONTAL};
|
||||
|
||||
static const std::vector<wxString> inp_choices = {
|
||||
_(L("Import model and profile")),
|
||||
_(L("Import profile only")),
|
||||
_(L("Import model only"))
|
||||
};
|
||||
|
||||
m_import_dropdown = new wxComboBox(
|
||||
this, wxID_ANY, inp_choices[0], wxDefaultPosition, wxDefaultSize,
|
||||
inp_choices.size(), inp_choices.data(), wxCB_READONLY | wxCB_DROPDOWN);
|
||||
|
||||
szchoices->Add(m_import_dropdown);
|
||||
szchoices->Add(new wxStaticText(this, wxID_ANY, _(L("Quality: "))), 0, wxALIGN_CENTER | wxALL, 5);
|
||||
|
||||
static const std::vector<wxString> qual_choices = {
|
||||
_(L("Accurate")),
|
||||
_(L("Balanced")),
|
||||
_(L("Quick"))
|
||||
};
|
||||
|
||||
m_quality_dropdown = new wxComboBox(
|
||||
this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize,
|
||||
qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN);
|
||||
szchoices->Add(m_quality_dropdown);
|
||||
|
||||
m_import_dropdown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) {
|
||||
if (get_selection() == Sel::profileOnly)
|
||||
m_quality_dropdown->Disable();
|
||||
else m_quality_dropdown->Enable();
|
||||
});
|
||||
|
||||
szvert->Add(szchoices, 0, wxALL, 5);
|
||||
szvert->AddStretchSpacer(1);
|
||||
auto szbtn = new wxBoxSizer(wxHORIZONTAL);
|
||||
szbtn->Add(new wxButton{this, wxID_CANCEL});
|
||||
szbtn->Add(new wxButton{this, wxID_OK});
|
||||
szvert->Add(szbtn, 0, wxALIGN_RIGHT | wxALL, 5);
|
||||
|
||||
SetSizerAndFit(szvert);
|
||||
}
|
||||
|
||||
Sel get_selection() const
|
||||
{
|
||||
int sel = m_import_dropdown->GetSelection();
|
||||
return Sel(std::min(int(Sel::modelOnly), std::max(0, sel)));
|
||||
}
|
||||
|
||||
Vec2i get_marchsq_windowsize() const
|
||||
{
|
||||
enum { Accurate, Balanced, Fast};
|
||||
|
||||
switch(m_quality_dropdown->GetSelection())
|
||||
{
|
||||
case Fast: return {8, 8};
|
||||
case Balanced: return {4, 4};
|
||||
default:
|
||||
case Accurate:
|
||||
return {2, 2};
|
||||
}
|
||||
}
|
||||
|
||||
wxString get_path() const
|
||||
{
|
||||
return m_filepicker->GetPath();
|
||||
}
|
||||
};
|
||||
|
||||
class SLAImportJob::priv {
|
||||
public:
|
||||
Plater *plater;
|
||||
|
||||
Sel sel = Sel::modelAndProfile;
|
||||
|
||||
TriangleMesh mesh;
|
||||
DynamicPrintConfig profile;
|
||||
wxString path;
|
||||
Vec2i win = {2, 2};
|
||||
std::string err;
|
||||
|
||||
priv(Plater *plt): plater{plt} {}
|
||||
};
|
||||
|
||||
SLAImportJob::SLAImportJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater)
|
||||
: Job{std::move(pri)}, p{std::make_unique<priv>(plater)}
|
||||
{}
|
||||
|
||||
SLAImportJob::~SLAImportJob() = default;
|
||||
|
||||
void SLAImportJob::process()
|
||||
{
|
||||
auto progr = [this](int s) {
|
||||
if (s < 100) update_status(int(s), _(L("Importing SLA archive")));
|
||||
return !was_canceled();
|
||||
};
|
||||
|
||||
if (p->path.empty()) return;
|
||||
|
||||
std::string path = p->path.ToUTF8().data();
|
||||
try {
|
||||
switch (p->sel) {
|
||||
case Sel::modelAndProfile:
|
||||
import_sla_archive(path, p->win, p->mesh, p->profile, progr);
|
||||
break;
|
||||
case Sel::modelOnly:
|
||||
import_sla_archive(path, p->win, p->mesh, progr);
|
||||
break;
|
||||
case Sel::profileOnly:
|
||||
import_sla_archive(path, p->profile);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (std::exception &ex) {
|
||||
p->err = ex.what();
|
||||
}
|
||||
|
||||
update_status(100, was_canceled() ? _(L("Importing canceled.")) :
|
||||
_(L("Importing done.")));
|
||||
}
|
||||
|
||||
void SLAImportJob::reset()
|
||||
{
|
||||
p->sel = Sel::modelAndProfile;
|
||||
p->mesh = {};
|
||||
p->profile = {};
|
||||
p->win = {2, 2};
|
||||
p->path.Clear();
|
||||
}
|
||||
|
||||
void SLAImportJob::prepare()
|
||||
{
|
||||
reset();
|
||||
|
||||
ImportDlg dlg{p->plater};
|
||||
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
auto path = dlg.get_path();
|
||||
auto nm = wxFileName(path);
|
||||
p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : path.ToUTF8();
|
||||
p->sel = dlg.get_selection();
|
||||
p->win = dlg.get_marchsq_windowsize();
|
||||
} else {
|
||||
p->path = "";
|
||||
}
|
||||
}
|
||||
|
||||
void SLAImportJob::finalize()
|
||||
{
|
||||
// Ignore the arrange result if aborted.
|
||||
if (was_canceled()) return;
|
||||
|
||||
if (!p->err.empty()) {
|
||||
show_error(p->plater, p->err);
|
||||
p->err = "";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name = wxFileName(p->path).GetName().ToUTF8().data();
|
||||
|
||||
if (!p->profile.empty()) {
|
||||
const ModelObjectPtrs& objects = p->plater->model().objects;
|
||||
for (auto object : objects)
|
||||
if (object->volumes.size() > 1)
|
||||
{
|
||||
Slic3r::GUI::show_info(nullptr,
|
||||
_(L("You cannot load SLA project with a multi-part object on the bed")) + "\n\n" +
|
||||
_(L("Please check your object list before preset changing.")),
|
||||
_(L("Attention!")) );
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicPrintConfig config = {};
|
||||
config.apply(SLAFullPrintConfig::defaults());
|
||||
config += std::move(p->profile);
|
||||
|
||||
wxGetApp().preset_bundle->load_config_model(name, std::move(config));
|
||||
wxGetApp().load_current_presets();
|
||||
}
|
||||
|
||||
if (!p->mesh.empty())
|
||||
p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
}}
|
31
src/slic3r/GUI/Jobs/SLAImportJob.hpp
Normal file
31
src/slic3r/GUI/Jobs/SLAImportJob.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#ifndef SLAIMPORTJOB_HPP
|
||||
#define SLAIMPORTJOB_HPP
|
||||
|
||||
#include "Job.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class Plater;
|
||||
|
||||
class SLAImportJob : public Job {
|
||||
class priv;
|
||||
|
||||
std::unique_ptr<priv> p;
|
||||
|
||||
public:
|
||||
SLAImportJob(std::shared_ptr<ProgressIndicator> pri, Plater *plater);
|
||||
~SLAImportJob();
|
||||
|
||||
void process() override;
|
||||
|
||||
void reset();
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
void finalize() override;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // SLAIMPORTJOB_HPP
|
@ -589,6 +589,11 @@ void MainFrame::init_menubar()
|
||||
append_menu_item(import_menu, wxID_ANY, _(L("Import STL/OBJ/AM&F/3MF")) + dots + "\tCtrl+I", _(L("Load a model")),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->add_model(); }, "import_plater", nullptr,
|
||||
[this](){return m_plater != nullptr; }, this);
|
||||
|
||||
append_menu_item(import_menu, wxID_ANY, _(L("Import SL1 archive")) + dots, _(L("Load an SL1 output archive")),
|
||||
[this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr,
|
||||
[this](){return m_plater != nullptr; }, this);
|
||||
|
||||
import_menu->AppendSeparator();
|
||||
append_menu_item(import_menu, wxID_ANY, _(L("Import &Config")) + dots + "\tCtrl+L", _(L("Load exported configuration file")),
|
||||
[this](wxCommandEvent&) { load_config_file(); }, "import_config", nullptr,
|
||||
|
@ -36,7 +36,6 @@
|
||||
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/SLA/Hollowing.hpp"
|
||||
#include "libslic3r/SLA/Rotfinder.hpp"
|
||||
#include "libslic3r/SLA/SupportPoint.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
@ -44,13 +43,6 @@
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
//#include "libslic3r/ClipperUtils.hpp"
|
||||
|
||||
// #include "libnest2d/optimizers/nlopt/genetic.hpp"
|
||||
// #include "libnest2d/backends/clipper/geometries.hpp"
|
||||
// #include "libnest2d/utils/rotcalipers.hpp"
|
||||
#include "libslic3r/MinAreaBoundingBox.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
@ -69,7 +61,9 @@
|
||||
#include "Camera.hpp"
|
||||
#include "Mouse3DController.hpp"
|
||||
#include "Tab.hpp"
|
||||
#include "Job.hpp"
|
||||
#include "Jobs/ArrangeJob.hpp"
|
||||
#include "Jobs/RotoptimizeJob.hpp"
|
||||
#include "Jobs/SLAImportJob.hpp"
|
||||
#include "PresetBundle.hpp"
|
||||
#include "BackgroundSlicingProcess.hpp"
|
||||
#include "ProgressStatusBar.hpp"
|
||||
@ -1488,311 +1482,44 @@ struct Plater::priv
|
||||
BackgroundSlicingProcess background_process;
|
||||
bool suppressed_backround_processing_update { false };
|
||||
|
||||
// Cache the wti info
|
||||
class WipeTower: public GLCanvas3D::WipeTowerInfo {
|
||||
using ArrangePolygon = arrangement::ArrangePolygon;
|
||||
friend priv;
|
||||
public:
|
||||
|
||||
void apply_arrange_result(const Vec2d& tr, double rotation)
|
||||
{
|
||||
m_pos = unscaled(tr); m_rotation = rotation;
|
||||
apply_wipe_tower();
|
||||
}
|
||||
|
||||
ArrangePolygon get_arrange_polygon() const
|
||||
{
|
||||
Polygon p({
|
||||
{coord_t(0), coord_t(0)},
|
||||
{scaled(m_bb_size(X)), coord_t(0)},
|
||||
{scaled(m_bb_size)},
|
||||
{coord_t(0), scaled(m_bb_size(Y))},
|
||||
{coord_t(0), coord_t(0)},
|
||||
});
|
||||
|
||||
ArrangePolygon ret;
|
||||
ret.poly.contour = std::move(p);
|
||||
ret.translation = scaled(m_pos);
|
||||
ret.rotation = m_rotation;
|
||||
ret.priority++;
|
||||
return ret;
|
||||
}
|
||||
} wipetower;
|
||||
|
||||
WipeTower& updated_wipe_tower() {
|
||||
auto wti = view3D->get_canvas3d()->get_wipe_tower_info();
|
||||
wipetower.m_pos = wti.pos();
|
||||
wipetower.m_rotation = wti.rotation();
|
||||
wipetower.m_bb_size = wti.bb_size();
|
||||
return wipetower;
|
||||
}
|
||||
|
||||
// A class to handle UI jobs like arranging and optimizing rotation.
|
||||
// These are not instant jobs, the user has to be informed about their
|
||||
// state in the status progress indicator. On the other hand they are
|
||||
// separated from the background slicing process. Ideally, these jobs should
|
||||
// run when the background process is not running.
|
||||
//
|
||||
// TODO: A mechanism would be useful for blocking the plater interactions:
|
||||
// objects would be frozen for the user. In case of arrange, an animation
|
||||
// could be shown, or with the optimize orientations, partial results
|
||||
// could be displayed.
|
||||
class PlaterJob: public Job
|
||||
{
|
||||
priv *m_plater;
|
||||
protected:
|
||||
|
||||
priv & plater() { return *m_plater; }
|
||||
const priv &plater() const { return *m_plater; }
|
||||
|
||||
// Launched when the job is finished. It refreshes the 3Dscene by def.
|
||||
void finalize() override
|
||||
{
|
||||
// Do a full refresh of scene tree, including regenerating
|
||||
// all the GLVolumes. FIXME The update function shall just
|
||||
// reload the modified matrices.
|
||||
if (!Job::was_canceled())
|
||||
plater().update(unsigned(UpdateParams::FORCE_FULL_SCREEN_REFRESH));
|
||||
|
||||
Job::finalize();
|
||||
}
|
||||
|
||||
public:
|
||||
PlaterJob(priv *_plater)
|
||||
: Job(_plater->statusbar()), m_plater(_plater)
|
||||
{}
|
||||
};
|
||||
|
||||
enum class Jobs : size_t {
|
||||
Arrange,
|
||||
Rotoptimize
|
||||
};
|
||||
|
||||
class ArrangeJob : public PlaterJob
|
||||
{
|
||||
using ArrangePolygon = arrangement::ArrangePolygon;
|
||||
using ArrangePolygons = arrangement::ArrangePolygons;
|
||||
|
||||
// The gap between logical beds in the x axis expressed in ratio of
|
||||
// the current bed width.
|
||||
static const constexpr double LOGICAL_BED_GAP = 1. / 5.;
|
||||
|
||||
ArrangePolygons m_selected, m_unselected, m_unprintable;
|
||||
|
||||
// clear m_selected and m_unselected, reserve space for next usage
|
||||
void clear_input() {
|
||||
const Model &model = plater().model;
|
||||
|
||||
size_t count = 0, cunprint = 0; // To know how much space to reserve
|
||||
for (auto obj : model.objects)
|
||||
for (auto mi : obj->instances)
|
||||
mi->printable ? count++ : cunprint++;
|
||||
|
||||
m_selected.clear();
|
||||
m_unselected.clear();
|
||||
m_unprintable.clear();
|
||||
m_selected.reserve(count + 1 /* for optional wti */);
|
||||
m_unselected.reserve(count + 1 /* for optional wti */);
|
||||
m_unprintable.reserve(cunprint /* for optional wti */);
|
||||
}
|
||||
|
||||
// Stride between logical beds
|
||||
double bed_stride() const {
|
||||
double bedwidth = plater().bed_shape_bb().size().x();
|
||||
return scaled<double>((1. + LOGICAL_BED_GAP) * bedwidth);
|
||||
}
|
||||
|
||||
// Set up arrange polygon for a ModelInstance and Wipe tower
|
||||
template<class T> ArrangePolygon get_arrange_poly(T *obj) const {
|
||||
ArrangePolygon ap = obj->get_arrange_polygon();
|
||||
ap.priority = 0;
|
||||
ap.bed_idx = ap.translation.x() / bed_stride();
|
||||
ap.setter = [obj, this](const ArrangePolygon &p) {
|
||||
if (p.is_arranged()) {
|
||||
Vec2d t = p.translation.cast<double>();
|
||||
t.x() += p.bed_idx * bed_stride();
|
||||
obj->apply_arrange_result(t, p.rotation);
|
||||
}
|
||||
};
|
||||
return ap;
|
||||
}
|
||||
|
||||
// Prepare all objects on the bed regardless of the selection
|
||||
void prepare_all() {
|
||||
clear_input();
|
||||
|
||||
for (ModelObject *obj: plater().model.objects)
|
||||
for (ModelInstance *mi : obj->instances) {
|
||||
ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable;
|
||||
cont.emplace_back(get_arrange_poly(mi));
|
||||
}
|
||||
|
||||
auto& wti = plater().updated_wipe_tower();
|
||||
if (wti) m_selected.emplace_back(get_arrange_poly(&wti));
|
||||
}
|
||||
|
||||
// Prepare the selected and unselected items separately. If nothing is
|
||||
// selected, behaves as if everything would be selected.
|
||||
void prepare_selected() {
|
||||
clear_input();
|
||||
|
||||
Model &model = plater().model;
|
||||
coord_t stride = bed_stride();
|
||||
|
||||
std::vector<const Selection::InstanceIdxsList *>
|
||||
obj_sel(model.objects.size(), nullptr);
|
||||
|
||||
for (auto &s : plater().get_selection().get_content())
|
||||
if (s.first < int(obj_sel.size()))
|
||||
obj_sel[size_t(s.first)] = &s.second;
|
||||
|
||||
// Go through the objects and check if inside the selection
|
||||
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
|
||||
const Selection::InstanceIdxsList * instlist = obj_sel[oidx];
|
||||
ModelObject *mo = model.objects[oidx];
|
||||
|
||||
std::vector<bool> inst_sel(mo->instances.size(), false);
|
||||
|
||||
if (instlist)
|
||||
for (auto inst_id : *instlist)
|
||||
inst_sel[size_t(inst_id)] = true;
|
||||
|
||||
for (size_t i = 0; i < inst_sel.size(); ++i) {
|
||||
ArrangePolygon &&ap = get_arrange_poly(mo->instances[i]);
|
||||
|
||||
ArrangePolygons &cont = mo->instances[i]->printable ?
|
||||
(inst_sel[i] ? m_selected :
|
||||
m_unselected) :
|
||||
m_unprintable;
|
||||
|
||||
cont.emplace_back(std::move(ap));
|
||||
}
|
||||
}
|
||||
|
||||
auto& wti = plater().updated_wipe_tower();
|
||||
if (wti) {
|
||||
ArrangePolygon &&ap = get_arrange_poly(&wti);
|
||||
|
||||
plater().get_selection().is_wipe_tower() ?
|
||||
m_selected.emplace_back(std::move(ap)) :
|
||||
m_unselected.emplace_back(std::move(ap));
|
||||
}
|
||||
|
||||
// If the selection was empty arrange everything
|
||||
if (m_selected.empty()) m_selected.swap(m_unselected);
|
||||
|
||||
// The strides have to be removed from the fixed items. For the
|
||||
// arrangeable (selected) items bed_idx is ignored and the
|
||||
// translation is irrelevant.
|
||||
for (auto &p : m_unselected) p.translation(X) -= p.bed_idx * stride;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
void prepare() override
|
||||
{
|
||||
wxGetKeyState(WXK_SHIFT) ? prepare_selected() : prepare_all();
|
||||
}
|
||||
|
||||
public:
|
||||
using PlaterJob::PlaterJob;
|
||||
|
||||
int status_range() const override
|
||||
{
|
||||
return int(m_selected.size() + m_unprintable.size());
|
||||
}
|
||||
|
||||
void process() override;
|
||||
|
||||
void finalize() override {
|
||||
// Ignore the arrange result if aborted.
|
||||
if (was_canceled()) return;
|
||||
|
||||
// Unprintable items go to the last virtual bed
|
||||
int beds = 0;
|
||||
|
||||
// Apply the arrange result to all selected objects
|
||||
for (ArrangePolygon &ap : m_selected) {
|
||||
beds = std::max(ap.bed_idx, beds);
|
||||
ap.apply();
|
||||
}
|
||||
|
||||
// Get the virtual beds from the unselected items
|
||||
for (ArrangePolygon &ap : m_unselected)
|
||||
beds = std::max(ap.bed_idx, beds);
|
||||
|
||||
// Move the unprintable items to the last virtual bed.
|
||||
for (ArrangePolygon &ap : m_unprintable) {
|
||||
ap.bed_idx += beds + 1;
|
||||
ap.apply();
|
||||
}
|
||||
|
||||
plater().update();
|
||||
}
|
||||
};
|
||||
|
||||
class RotoptimizeJob : public PlaterJob
|
||||
{
|
||||
public:
|
||||
using PlaterJob::PlaterJob;
|
||||
void process() override;
|
||||
};
|
||||
|
||||
|
||||
// Jobs defined inside the group class will be managed so that only one can
|
||||
// run at a time. Also, the background process will be stopped if a job is
|
||||
// started.
|
||||
class ExclusiveJobGroup {
|
||||
// started. It is up the the plater to ensure that the background slicing
|
||||
// can't be restarted while a ui job is still running.
|
||||
class Jobs: public ExclusiveJobGroup
|
||||
{
|
||||
priv *m;
|
||||
size_t m_arrange_id, m_rotoptimize_id, m_sla_import_id;
|
||||
|
||||
static const int ABORT_WAIT_MAX_MS = 10000;
|
||||
|
||||
priv * m_plater;
|
||||
|
||||
ArrangeJob arrange_job{m_plater};
|
||||
RotoptimizeJob rotoptimize_job{m_plater};
|
||||
|
||||
// To create a new job, just define a new subclass of Job, implement
|
||||
// the process and the optional prepare() and finalize() methods
|
||||
// Register the instance of the class in the m_jobs container
|
||||
// if it cannot run concurrently with other jobs in this group
|
||||
|
||||
std::vector<std::reference_wrapper<Job>> m_jobs{arrange_job,
|
||||
rotoptimize_job};
|
||||
void before_start() override { m->background_process.stop(); }
|
||||
|
||||
public:
|
||||
ExclusiveJobGroup(priv *_plater) : m_plater(_plater) {}
|
||||
|
||||
void start(Jobs jid) {
|
||||
m_plater->background_process.stop();
|
||||
stop_all();
|
||||
m_jobs[size_t(jid)].get().start();
|
||||
}
|
||||
|
||||
void cancel_all() { for (Job& j : m_jobs) j.cancel(); }
|
||||
|
||||
void join_all(int wait_ms = 0)
|
||||
Jobs(priv *_m) : m(_m)
|
||||
{
|
||||
std::vector<bool> aborted(m_jobs.size(), false);
|
||||
|
||||
for (size_t jid = 0; jid < m_jobs.size(); ++jid)
|
||||
aborted[jid] = m_jobs[jid].get().join(wait_ms);
|
||||
|
||||
if (!all_of(aborted))
|
||||
BOOST_LOG_TRIVIAL(error) << "Could not abort a job!";
|
||||
m_arrange_id = add_job(std::make_unique<ArrangeJob>(m->statusbar(), m->q));
|
||||
m_rotoptimize_id = add_job(std::make_unique<RotoptimizeJob>(m->statusbar(), m->q));
|
||||
m_sla_import_id = add_job(std::make_unique<SLAImportJob>(m->statusbar(), m->q));
|
||||
}
|
||||
|
||||
void stop_all() { cancel_all(); join_all(ABORT_WAIT_MAX_MS); }
|
||||
|
||||
const Job& get(Jobs jobid) const { return m_jobs[size_t(jobid)]; }
|
||||
|
||||
bool is_any_running() const
|
||||
void arrange()
|
||||
{
|
||||
return std::any_of(m_jobs.begin(),
|
||||
m_jobs.end(),
|
||||
[](const Job &j) { return j.is_running(); });
|
||||
m->take_snapshot(_(L("Arrange")));
|
||||
start(m_arrange_id);
|
||||
}
|
||||
|
||||
} m_ui_jobs{this};
|
||||
void optimize_rotation()
|
||||
{
|
||||
m->take_snapshot(_(L("Optimize Rotation")));
|
||||
start(m_rotoptimize_id);
|
||||
}
|
||||
|
||||
void import_sla_arch()
|
||||
{
|
||||
m->take_snapshot(_(L("Import SLA archive")));
|
||||
start(m_sla_import_id);
|
||||
}
|
||||
|
||||
} m_ui_jobs;
|
||||
|
||||
bool delayed_scene_refresh;
|
||||
std::string delayed_error_message;
|
||||
@ -1850,9 +1577,7 @@ struct Plater::priv
|
||||
std::string get_config(const std::string &key) const;
|
||||
BoundingBoxf bed_shape_bb() const;
|
||||
BoundingBox scaled_bed_shape_bb() const;
|
||||
arrangement::BedShapeHint get_bed_shape_hint() const;
|
||||
|
||||
void find_new_position(const ModelInstancePtrs &instances, coord_t min_d);
|
||||
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, bool load_model, bool load_config);
|
||||
std::vector<size_t> load_model_objects(const ModelObjectPtrs &model_objects);
|
||||
wxString get_export_file(GUI::FileType file_type);
|
||||
@ -1870,8 +1595,6 @@ struct Plater::priv
|
||||
void delete_object_from_model(size_t obj_idx);
|
||||
void reset();
|
||||
void mirror(Axis axis);
|
||||
void arrange();
|
||||
void sla_optimize_rotation();
|
||||
void split_object();
|
||||
void split_volume();
|
||||
void scale_selection_to_fit_print_volume();
|
||||
@ -2038,6 +1761,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
"support_material", "support_material_extruder", "support_material_interface_extruder", "support_material_contact_distance", "raft_layers"
|
||||
}))
|
||||
, sidebar(new Sidebar(q))
|
||||
, m_ui_jobs(this)
|
||||
, delayed_scene_refresh(false)
|
||||
, view_toolbar(GLToolbar::Radio, "View")
|
||||
, m_project_filename(wxEmptyString)
|
||||
@ -2124,14 +1848,15 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
sidebar->Bind(EVT_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); });
|
||||
|
||||
wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas();
|
||||
|
||||
// 3DScene events:
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { this->schedule_background_process(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this);
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this);
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { arrange(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { this->q->arrange(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [this](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int> &evt)
|
||||
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
|
||||
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
||||
@ -2156,7 +1881,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this);
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { arrange(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { this->q->arrange(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); });
|
||||
view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); });
|
||||
@ -2824,40 +2549,12 @@ void Plater::priv::mirror(Axis axis)
|
||||
view3D->mirror_selection(axis);
|
||||
}
|
||||
|
||||
void Plater::priv::arrange()
|
||||
{
|
||||
this->take_snapshot(_L("Arrange"));
|
||||
m_ui_jobs.start(Jobs::Arrange);
|
||||
}
|
||||
|
||||
|
||||
// This method will find an optimal orientation for the currently selected item
|
||||
// Very similar in nature to the arrange method above...
|
||||
void Plater::priv::sla_optimize_rotation() {
|
||||
this->take_snapshot(_L("Optimize Rotation"));
|
||||
m_ui_jobs.start(Jobs::Rotoptimize);
|
||||
}
|
||||
|
||||
arrangement::BedShapeHint Plater::priv::get_bed_shape_hint() const {
|
||||
|
||||
const auto *bed_shape_opt = config->opt<ConfigOptionPoints>("bed_shape");
|
||||
assert(bed_shape_opt);
|
||||
|
||||
if (!bed_shape_opt) return {};
|
||||
|
||||
auto &bedpoints = bed_shape_opt->values;
|
||||
Polyline bedpoly; bedpoly.points.reserve(bedpoints.size());
|
||||
for (auto &v : bedpoints) bedpoly.append(scaled(v));
|
||||
|
||||
return arrangement::BedShapeHint(bedpoly);
|
||||
}
|
||||
|
||||
void Plater::priv::find_new_position(const ModelInstancePtrs &instances,
|
||||
void Plater::find_new_position(const ModelInstancePtrs &instances,
|
||||
coord_t min_d)
|
||||
{
|
||||
arrangement::ArrangePolygons movable, fixed;
|
||||
|
||||
for (const ModelObject *mo : model.objects)
|
||||
for (const ModelObject *mo : p->model.objects)
|
||||
for (const ModelInstance *inst : mo->instances) {
|
||||
auto it = std::find(instances.begin(), instances.end(), inst);
|
||||
auto arrpoly = inst->get_arrange_polygon();
|
||||
@ -2868,10 +2565,11 @@ void Plater::priv::find_new_position(const ModelInstancePtrs &instances,
|
||||
movable.emplace_back(std::move(arrpoly));
|
||||
}
|
||||
|
||||
if (updated_wipe_tower())
|
||||
fixed.emplace_back(wipetower.get_arrange_polygon());
|
||||
if (p->view3D->get_canvas3d()->get_wipe_tower_info())
|
||||
fixed.emplace_back(get_wipe_tower_arrangepoly(*this));
|
||||
|
||||
arrangement::arrange(movable, fixed, min_d, get_bed_shape_hint());
|
||||
arrangement::arrange(movable, fixed, get_bed_shape(*config()),
|
||||
arrangement::ArrangeParams{min_d});
|
||||
|
||||
for (size_t i = 0; i < instances.size(); ++i)
|
||||
if (movable[i].bed_idx == 0)
|
||||
@ -2879,95 +2577,6 @@ void Plater::priv::find_new_position(const ModelInstancePtrs &instances,
|
||||
movable[i].rotation);
|
||||
}
|
||||
|
||||
void Plater::priv::ArrangeJob::process() {
|
||||
static const auto arrangestr = _L("Arranging");
|
||||
|
||||
// FIXME: I don't know how to obtain the minimum distance, it depends
|
||||
// on printer technology. I guess the following should work but it crashes.
|
||||
double dist = 6; // PrintConfig::min_object_distance(config);
|
||||
if (plater().printer_technology == ptFFF) {
|
||||
dist = PrintConfig::min_object_distance(plater().config);
|
||||
}
|
||||
|
||||
coord_t min_d = scaled(dist);
|
||||
auto count = unsigned(m_selected.size() + m_unprintable.size());
|
||||
arrangement::BedShapeHint bedshape = plater().get_bed_shape_hint();
|
||||
|
||||
auto stopfn = [this]() { return was_canceled(); };
|
||||
|
||||
try {
|
||||
arrangement::arrange(m_selected, m_unselected, min_d, bedshape,
|
||||
[this, count](unsigned st) {
|
||||
st += m_unprintable.size();
|
||||
if (st > 0) update_status(int(count - st), arrangestr);
|
||||
}, stopfn);
|
||||
arrangement::arrange(m_unprintable, {}, min_d, bedshape,
|
||||
[this, count](unsigned st) {
|
||||
if (st > 0) update_status(int(count - st), arrangestr);
|
||||
}, stopfn);
|
||||
} catch (std::exception & /*e*/) {
|
||||
GUI::show_error(plater().q,
|
||||
_L("Could not arrange model objects! "
|
||||
"Some geometries may be invalid."));
|
||||
}
|
||||
|
||||
// finalize just here.
|
||||
update_status(int(count),
|
||||
was_canceled() ? _L("Arranging canceled.")
|
||||
: _L("Arranging done."));
|
||||
}
|
||||
|
||||
void Plater::priv::RotoptimizeJob::process()
|
||||
{
|
||||
int obj_idx = plater().get_selected_object_idx();
|
||||
if (obj_idx < 0) { return; }
|
||||
|
||||
ModelObject *o = plater().model.objects[size_t(obj_idx)];
|
||||
|
||||
auto r = sla::find_best_rotation(
|
||||
*o,
|
||||
.005f,
|
||||
[this](unsigned s) {
|
||||
if (s < 100)
|
||||
update_status(int(s),
|
||||
_L("Searching for optimal orientation"));
|
||||
},
|
||||
[this]() { return was_canceled(); });
|
||||
|
||||
|
||||
double mindist = 6.0; // FIXME
|
||||
|
||||
if (!was_canceled()) {
|
||||
for(ModelInstance * oi : o->instances) {
|
||||
oi->set_rotation({r[X], r[Y], r[Z]});
|
||||
|
||||
auto trmatrix = oi->get_transformation().get_matrix();
|
||||
Polygon trchull = o->convex_hull_2d(trmatrix);
|
||||
|
||||
MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex);
|
||||
double r = rotbb.angle_to_X();
|
||||
|
||||
// The box should be landscape
|
||||
if(rotbb.width() < rotbb.height()) r += PI / 2;
|
||||
|
||||
Vec3d rt = oi->get_rotation(); rt(Z) += r;
|
||||
|
||||
oi->set_rotation(rt);
|
||||
}
|
||||
|
||||
plater().find_new_position(o->instances, scaled(mindist));
|
||||
|
||||
// Correct the z offset of the object which was corrupted be
|
||||
// the rotation
|
||||
o->ensure_on_bed();
|
||||
}
|
||||
|
||||
update_status(100,
|
||||
was_canceled() ? _L("Orientation search canceled.")
|
||||
: _L("Orientation found."));
|
||||
}
|
||||
|
||||
|
||||
void Plater::priv::split_object()
|
||||
{
|
||||
int obj_idx = get_selected_object_idx();
|
||||
@ -3608,7 +3217,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
|
||||
}
|
||||
|
||||
// update plater with new config
|
||||
wxGetApp().plater()->on_config_change(wxGetApp().preset_bundle->full_config());
|
||||
q->on_config_change(wxGetApp().preset_bundle->full_config());
|
||||
/* Settings list can be changed after printer preset changing, so
|
||||
* update all settings items for all item had it.
|
||||
* Furthermore, Layers editing is implemented only for FFF printers
|
||||
@ -4055,8 +3664,12 @@ bool Plater::priv::complit_init_sla_object_menu()
|
||||
sla_object_menu.AppendSeparator();
|
||||
|
||||
// Add the automatic rotation sub-menu
|
||||
append_menu_item(&sla_object_menu, wxID_ANY, _L("Optimize orientation"), _L("Optimize the rotation of the object for better print results."),
|
||||
[this](wxCommandEvent&) { sla_optimize_rotation(); });
|
||||
append_menu_item(
|
||||
&sla_object_menu, wxID_ANY, _(L("Optimize orientation")),
|
||||
_(L("Optimize the rotation of the object for better print results.")),
|
||||
[this](wxCommandEvent &) {
|
||||
m_ui_jobs.optimize_rotation();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -4660,6 +4273,11 @@ void Plater::add_model()
|
||||
load_files(paths, true, false);
|
||||
}
|
||||
|
||||
void Plater::import_sl1_archive()
|
||||
{
|
||||
p->m_ui_jobs.import_sla_arch();
|
||||
}
|
||||
|
||||
void Plater::extract_config_from_project()
|
||||
{
|
||||
wxString input_file;
|
||||
@ -4752,7 +4370,7 @@ void Plater::increase_instances(size_t num)
|
||||
sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num);
|
||||
|
||||
if (p->get_config("autocenter") == "1")
|
||||
p->arrange();
|
||||
arrange();
|
||||
|
||||
p->update();
|
||||
|
||||
@ -5486,6 +5104,11 @@ bool Plater::is_export_gcode_scheduled() const
|
||||
return p->background_process.is_export_scheduled();
|
||||
}
|
||||
|
||||
const Selection &Plater::get_selection() const
|
||||
{
|
||||
return p->get_selection();
|
||||
}
|
||||
|
||||
int Plater::get_selected_object_idx()
|
||||
{
|
||||
return p->get_selected_object_idx();
|
||||
@ -5511,6 +5134,11 @@ BoundingBoxf Plater::bed_shape_bb() const
|
||||
return p->bed_shape_bb();
|
||||
}
|
||||
|
||||
void Plater::arrange()
|
||||
{
|
||||
p->m_ui_jobs.arrange();
|
||||
}
|
||||
|
||||
void Plater::set_current_canvas_as_dirty()
|
||||
{
|
||||
p->set_current_canvas_as_dirty();
|
||||
@ -5533,6 +5161,8 @@ PrinterTechnology Plater::printer_technology() const
|
||||
return p->printer_technology;
|
||||
}
|
||||
|
||||
const DynamicPrintConfig * Plater::config() const { return p->config; }
|
||||
|
||||
void Plater::set_printer_technology(PrinterTechnology printer_technology)
|
||||
{
|
||||
p->printer_technology = printer_technology;
|
||||
|
@ -9,8 +9,10 @@
|
||||
#include <wx/bmpcbox.h>
|
||||
|
||||
#include "Preset.hpp"
|
||||
#include "Selection.hpp"
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "Jobs/Job.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
class wxButton;
|
||||
@ -157,6 +159,7 @@ public:
|
||||
void load_project();
|
||||
void load_project(const wxString& filename);
|
||||
void add_model();
|
||||
void import_sl1_archive();
|
||||
void extract_config_from_project();
|
||||
|
||||
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, bool load_model = true, bool load_config = true);
|
||||
@ -253,12 +256,16 @@ public:
|
||||
|
||||
bool is_export_gcode_scheduled() const;
|
||||
|
||||
const Selection& get_selection() const;
|
||||
int get_selected_object_idx();
|
||||
bool is_single_full_object_selection() const;
|
||||
GLCanvas3D* canvas3D();
|
||||
GLCanvas3D* get_current_canvas3D();
|
||||
BoundingBoxf bed_shape_bb() const;
|
||||
|
||||
void arrange();
|
||||
void find_new_position(const ModelInstancePtrs &instances, coord_t min_d);
|
||||
|
||||
void set_current_canvas_as_dirty();
|
||||
#if ENABLE_NON_STATIC_CANVAS_MANAGER
|
||||
void unbind_canvas_event_handlers();
|
||||
@ -266,6 +273,7 @@ public:
|
||||
#endif // ENABLE_NON_STATIC_CANVAS_MANAGER
|
||||
|
||||
PrinterTechnology printer_technology() const;
|
||||
const DynamicPrintConfig * config() const;
|
||||
void set_printer_technology(PrinterTechnology printer_technology);
|
||||
|
||||
void copy_selection_to_clipboard();
|
||||
@ -371,6 +379,7 @@ private:
|
||||
bool m_was_scheduled;
|
||||
};
|
||||
|
||||
}}
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
|
@ -6,7 +6,7 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "ProgressIndicator.hpp"
|
||||
#include "Jobs/ProgressIndicator.hpp"
|
||||
|
||||
class wxTimer;
|
||||
class wxGauge;
|
||||
|
@ -18,6 +18,8 @@
|
||||
#include <openssl/x509.h>
|
||||
#endif
|
||||
|
||||
#define L(s) s
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
@ -32,6 +34,7 @@ namespace Slic3r {
|
||||
struct CurlGlobalInit
|
||||
{
|
||||
static std::unique_ptr<CurlGlobalInit> instance;
|
||||
std::string message;
|
||||
|
||||
CurlGlobalInit()
|
||||
{
|
||||
@ -57,21 +60,39 @@ struct CurlGlobalInit
|
||||
ssl_cafile = X509_get_default_cert_file();
|
||||
|
||||
int replace = true;
|
||||
|
||||
if (!ssl_cafile || !fs::exists(fs::path(ssl_cafile)))
|
||||
for (const char * bundle : CA_BUNDLES) {
|
||||
if (fs::exists(fs::path(bundle))) {
|
||||
::setenv(SSL_CA_FILE, bundle, replace);
|
||||
if (!ssl_cafile || !fs::exists(fs::path(ssl_cafile))) {
|
||||
const char * bundle = nullptr;
|
||||
for (const char * b : CA_BUNDLES) {
|
||||
if (fs::exists(fs::path(b))) {
|
||||
::setenv(SSL_CA_FILE, bundle = b, replace);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "Detected OpenSSL root CA store: " << ::getenv(SSL_CA_FILE);
|
||||
if (!bundle)
|
||||
message = L("Could not detect system SSL certificate store. "
|
||||
"PrusaSlicer will be unable to establish secure "
|
||||
"network connections.");
|
||||
else
|
||||
message = string_printf(
|
||||
L("PrusaSlicer detected system SSL certificate store in: %s"),
|
||||
bundle);
|
||||
|
||||
#endif
|
||||
message += string_printf(
|
||||
L("\nTo specify the system certificate store manually, please "
|
||||
"set the %s environment variable to the correct CA bundle "
|
||||
"and restart the application."),
|
||||
SSL_CA_FILE);
|
||||
}
|
||||
|
||||
::curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
#endif // OPENSSL_CERT_OVERRIDE
|
||||
|
||||
if (CURLcode ec = ::curl_global_init(CURL_GLOBAL_DEFAULT)) {
|
||||
message = L("CURL init has failed. PrusaSlicer will be unable to establish "
|
||||
"network connections. See logs for additional details.");
|
||||
|
||||
BOOST_LOG_TRIVIAL(error) << ::curl_easy_strerror(ec);
|
||||
}
|
||||
}
|
||||
|
||||
~CurlGlobalInit() { ::curl_global_cleanup(); }
|
||||
@ -132,8 +153,7 @@ Http::priv::priv(const std::string &url)
|
||||
, limit(0)
|
||||
, cancel(false)
|
||||
{
|
||||
if (!CurlGlobalInit::instance)
|
||||
CurlGlobalInit::instance = std::make_unique<CurlGlobalInit>();
|
||||
Http::tls_global_init();
|
||||
|
||||
if (curl == nullptr) {
|
||||
throw std::runtime_error(std::string("Could not construct Curl object"));
|
||||
@ -497,6 +517,25 @@ bool Http::ca_file_supported()
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string Http::tls_global_init()
|
||||
{
|
||||
if (!CurlGlobalInit::instance)
|
||||
CurlGlobalInit::instance = std::make_unique<CurlGlobalInit>();
|
||||
|
||||
return CurlGlobalInit::instance->message;
|
||||
}
|
||||
|
||||
std::string Http::tls_system_cert_store()
|
||||
{
|
||||
std::string ret;
|
||||
|
||||
#ifdef OPENSSL_CERT_OVERRIDE
|
||||
ret = ::getenv(X509_get_default_cert_file_env());
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string Http::url_encode(const std::string &str)
|
||||
{
|
||||
::CURL *curl = ::curl_easy_init();
|
||||
|
@ -101,6 +101,10 @@ public:
|
||||
// Tells whether current backend supports seting up a CA file using ca_file()
|
||||
static bool ca_file_supported();
|
||||
|
||||
// Return empty string on success or error message on fail.
|
||||
static std::string tls_global_init();
|
||||
static std::string tls_system_cert_store();
|
||||
|
||||
// converts the given string to an url_encoded_string
|
||||
static std::string url_encode(const std::string &str);
|
||||
private:
|
||||
|
314
src/slic3r/Utils/SLAImport.cpp
Normal file
314
src/slic3r/Utils/SLAImport.cpp
Normal file
@ -0,0 +1,314 @@
|
||||
#include "SLAImport.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "libslic3r/SlicesToTriangleMesh.hpp"
|
||||
#include "libslic3r/MarchingSquares.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/SLA/RasterBase.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
namespace marchsq {
|
||||
|
||||
// Specialize this struct to register a raster type for the Marching squares alg
|
||||
template<> struct _RasterTraits<wxImage> {
|
||||
using Rst = wxImage;
|
||||
|
||||
// The type of pixel cell in the raster
|
||||
using ValueType = uint8_t;
|
||||
|
||||
// Value at a given position
|
||||
static uint8_t get(const Rst &rst, size_t row, size_t col)
|
||||
{
|
||||
return rst.GetRed(col, row);
|
||||
}
|
||||
|
||||
// Number of rows and cols of the raster
|
||||
static size_t rows(const Rst &rst) { return rst.GetHeight(); }
|
||||
static size_t cols(const Rst &rst) { return rst.GetWidth(); }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ArchiveData {
|
||||
boost::property_tree::ptree profile, config;
|
||||
std::vector<sla::EncodedRaster> images;
|
||||
};
|
||||
|
||||
static const constexpr char *CONFIG_FNAME = "config.ini";
|
||||
static const constexpr char *PROFILE_FNAME = "prusaslicer.ini";
|
||||
|
||||
boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip)
|
||||
{
|
||||
std::string buf(size_t(entry.m_uncomp_size), '\0');
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
boost::property_tree::ptree tree;
|
||||
std::stringstream ss(buf);
|
||||
boost::property_tree::read_ini(ss, tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
sla::EncodedRaster read_png(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip,
|
||||
const std::string & name)
|
||||
{
|
||||
std::vector<uint8_t> buf(entry.m_uncomp_size);
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
return sla::EncodedRaster(std::move(buf),
|
||||
name.empty() ? entry.m_filename : name);
|
||||
}
|
||||
|
||||
ArchiveData extract_sla_archive(const std::string &zipfname,
|
||||
const std::string &exclude)
|
||||
{
|
||||
ArchiveData arch;
|
||||
|
||||
// Little RAII
|
||||
struct Arch: public MZ_Archive {
|
||||
Arch(const std::string &fname) {
|
||||
if (!open_zip_reader(&arch, fname))
|
||||
throw std::runtime_error(get_errorstr());
|
||||
}
|
||||
|
||||
~Arch() { close_zip_reader(&arch); }
|
||||
} zip (zipfname);
|
||||
|
||||
mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch);
|
||||
|
||||
for (mz_uint i = 0; i < num_entries; ++i)
|
||||
{
|
||||
mz_zip_archive_file_stat entry;
|
||||
|
||||
if (mz_zip_reader_file_stat(&zip.arch, i, &entry))
|
||||
{
|
||||
std::string name = entry.m_filename;
|
||||
boost::algorithm::to_lower(name);
|
||||
|
||||
if (boost::algorithm::contains(name, exclude)) continue;
|
||||
|
||||
if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip);
|
||||
if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip);
|
||||
|
||||
if (boost::filesystem::path(name).extension().string() == ".png") {
|
||||
auto it = std::lower_bound(
|
||||
arch.images.begin(), arch.images.end(), sla::EncodedRaster({}, name),
|
||||
[](const sla::EncodedRaster &r1, const sla::EncodedRaster &r2) {
|
||||
return std::less<std::string>()(r1.extension(), r2.extension());
|
||||
});
|
||||
|
||||
arch.images.insert(it, read_png(entry, zip, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
|
||||
ExPolygons rings_to_expolygons(const std::vector<marchsq::Ring> &rings,
|
||||
double px_w, double px_h)
|
||||
{
|
||||
ExPolygons polys; polys.reserve(rings.size());
|
||||
|
||||
for (const marchsq::Ring &ring : rings) {
|
||||
Polygon poly; Points &pts = poly.points;
|
||||
pts.reserve(ring.size());
|
||||
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h));
|
||||
|
||||
polys.emplace_back(poly);
|
||||
}
|
||||
|
||||
// reverse the raster transformations
|
||||
return union_ex(polys);
|
||||
}
|
||||
|
||||
template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn)
|
||||
{
|
||||
for (auto &p : poly.contour.points) fn(p);
|
||||
for (auto &h : poly.holes)
|
||||
for (auto &p : h.points) fn(p);
|
||||
}
|
||||
|
||||
void invert_raster_trafo(ExPolygons & expolys,
|
||||
const sla::RasterBase::Trafo &trafo,
|
||||
coord_t width,
|
||||
coord_t height)
|
||||
{
|
||||
for (auto &expoly : expolys) {
|
||||
if (trafo.mirror_y)
|
||||
foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); });
|
||||
|
||||
if (trafo.mirror_x)
|
||||
foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); });
|
||||
|
||||
expoly.translate(-trafo.center_x, -trafo.center_y);
|
||||
|
||||
if (trafo.flipXY)
|
||||
foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); });
|
||||
|
||||
if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) {
|
||||
expoly.contour.reverse();
|
||||
for (auto &h : expoly.holes) h.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RasterParams {
|
||||
sla::RasterBase::Trafo trafo; // Raster transformations
|
||||
coord_t width, height; // scaled raster dimensions (not resolution)
|
||||
double px_h, px_w; // pixel dimesions
|
||||
marchsq::Coord win; // marching squares window size
|
||||
};
|
||||
|
||||
RasterParams get_raster_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_disp_cols = cfg.option<ConfigOptionInt>("display_pixels_x");
|
||||
auto *opt_disp_rows = cfg.option<ConfigOptionInt>("display_pixels_y");
|
||||
auto *opt_disp_w = cfg.option<ConfigOptionFloat>("display_width");
|
||||
auto *opt_disp_h = cfg.option<ConfigOptionFloat>("display_height");
|
||||
auto *opt_mirror_x = cfg.option<ConfigOptionBool>("display_mirror_x");
|
||||
auto *opt_mirror_y = cfg.option<ConfigOptionBool>("display_mirror_y");
|
||||
auto *opt_orient = cfg.option<ConfigOptionEnum<SLADisplayOrientation>>("display_orientation");
|
||||
|
||||
if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h ||
|
||||
!opt_mirror_x || !opt_mirror_y || !opt_orient)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
RasterParams rstp;
|
||||
|
||||
rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1);
|
||||
rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1);
|
||||
|
||||
sla::RasterBase::Trafo trafo{opt_orient->value == sladoLandscape ?
|
||||
sla::RasterBase::roLandscape :
|
||||
sla::RasterBase::roPortrait,
|
||||
{opt_mirror_x->value, opt_mirror_y->value}};
|
||||
|
||||
rstp.height = scaled(opt_disp_h->value);
|
||||
rstp.width = scaled(opt_disp_w->value);
|
||||
|
||||
return rstp;
|
||||
}
|
||||
|
||||
struct SliceParams { double layerh = 0., initial_layerh = 0.; };
|
||||
|
||||
SliceParams get_slice_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_layerh = cfg.option<ConfigOptionFloat>("layer_height");
|
||||
auto *opt_init_layerh = cfg.option<ConfigOptionFloat>("initial_layer_height");
|
||||
|
||||
if (!opt_layerh || !opt_init_layerh)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()};
|
||||
}
|
||||
|
||||
std::vector<ExPolygons> extract_slices_from_sla_archive(
|
||||
ArchiveData & arch,
|
||||
const RasterParams & rstp,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
auto jobdir = arch.config.get<std::string>("jobDir");
|
||||
for (auto &c : jobdir) c = std::tolower(c);
|
||||
|
||||
std::vector<ExPolygons> slices(arch.images.size());
|
||||
|
||||
struct Status
|
||||
{
|
||||
double incr, val, prev;
|
||||
bool stop = false;
|
||||
tbb::spin_mutex mutex;
|
||||
} st {100. / slices.size(), 0., 0.};
|
||||
|
||||
tbb::parallel_for(size_t(0), arch.images.size(),
|
||||
[&arch, &slices, &st, &rstp, progr](size_t i) {
|
||||
// Status indication guarded with the spinlock
|
||||
{
|
||||
std::lock_guard<tbb::spin_mutex> lck(st.mutex);
|
||||
if (st.stop) return;
|
||||
|
||||
st.val += st.incr;
|
||||
double curr = std::round(st.val);
|
||||
if (curr > st.prev) {
|
||||
st.prev = curr;
|
||||
st.stop = !progr(int(curr));
|
||||
}
|
||||
}
|
||||
|
||||
auto &buf = arch.images[i];
|
||||
wxMemoryInputStream stream{buf.data(), buf.size()};
|
||||
wxImage img{stream};
|
||||
|
||||
auto rings = marchsq::execute(img, 128, rstp.win);
|
||||
ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h);
|
||||
|
||||
// Invert the raster transformations indicated in
|
||||
// the profile metadata
|
||||
invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height);
|
||||
|
||||
slices[i] = std::move(expolys);
|
||||
});
|
||||
|
||||
if (st.stop) slices = {};
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out)
|
||||
{
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "png");
|
||||
out.load(arch.profile);
|
||||
}
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
// Ensure minimum window size for marching squares
|
||||
windowsize.x() = std::max(2, windowsize.x());
|
||||
windowsize.y() = std::max(2, windowsize.y());
|
||||
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "thumbnail");
|
||||
profile.load(arch.profile);
|
||||
|
||||
RasterParams rstp = get_raster_params(profile);
|
||||
rstp.win = {windowsize.y(), windowsize.x()};
|
||||
|
||||
SliceParams slicp = get_slice_params(profile);
|
||||
|
||||
std::vector<ExPolygons> slices =
|
||||
extract_slices_from_sla_archive(arch, rstp, progr);
|
||||
|
||||
if (!slices.empty())
|
||||
out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
36
src/slic3r/Utils/SLAImport.hpp
Normal file
36
src/slic3r/Utils/SLAImport.hpp
Normal file
@ -0,0 +1,36 @@
|
||||
#ifndef SLAIMPORT_HPP
|
||||
#define SLAIMPORT_HPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <libslic3r/PrintConfig.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
class DynamicPrintConfig;
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out);
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr = [](int) { return true; });
|
||||
|
||||
inline void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
std::function<bool(int)> progr = [](int) { return true; })
|
||||
{
|
||||
DynamicPrintConfig profile;
|
||||
import_sla_archive(zipfname, windowsize, out, profile, progr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // SLAIMPORT_HPP
|
@ -12,6 +12,7 @@
|
||||
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <libslic3r/ModelArrange.hpp>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@ -167,8 +168,7 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
|
||||
object->add_volume(std::move(t));
|
||||
object->add_instance();
|
||||
}
|
||||
model.arrange_objects(PrintConfig::min_object_distance(&config));
|
||||
model.center_instances_around_point(Slic3r::Vec2d(100, 100));
|
||||
arrange_objects(model, InfiniteBed{}, ArrangeParams{ scaled(min_object_distance(config))});
|
||||
for (ModelObject *mo : model.objects) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@ -41,8 +42,7 @@ SCENARIO("Model construction", "[Model]") {
|
||||
}
|
||||
}
|
||||
model_object->add_instance();
|
||||
model.arrange_objects(PrintConfig::min_object_distance(&config));
|
||||
model.center_instances_around_point(Slic3r::Vec2d(100, 100));
|
||||
arrange_objects(model, InfiniteBed{scaled(Vec2d(100, 100))}, ArrangeParams{scaled(min_object_distance(config))});
|
||||
model_object->ensure_on_bed();
|
||||
print.auto_assign_extruders(model_object);
|
||||
THEN("Print works?") {
|
||||
|
@ -472,32 +472,30 @@ TEST_CASE("ArrangeRectanglesLoose", "[Nesting]")
|
||||
namespace {
|
||||
using namespace libnest2d;
|
||||
|
||||
template<long long SCALE = 1, class Bin>
|
||||
void exportSVG(std::vector<std::reference_wrapper<Item>>& result, const Bin& bin, int idx = 0) {
|
||||
std::string loc = "out";
|
||||
template<long long SCALE = 1, class It>
|
||||
void exportSVG(const char *loc, It from, It to) {
|
||||
|
||||
static std::string svg_header =
|
||||
R"raw(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
static const char* svg_header =
|
||||
R"raw(<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg height="500" width="500" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
)raw";
|
||||
|
||||
int i = idx;
|
||||
auto r = result;
|
||||
// for(auto r : result) {
|
||||
std::fstream out(loc + std::to_string(i) + ".svg", std::fstream::out);
|
||||
std::fstream out(loc, std::fstream::out);
|
||||
if(out.is_open()) {
|
||||
out << svg_header;
|
||||
Item rbin( RectangleItem(bin.width(), bin.height()) );
|
||||
for(unsigned j = 0; j < rbin.vertexCount(); j++) {
|
||||
auto v = rbin.vertex(j);
|
||||
setY(v, -getY(v)/SCALE + 500 );
|
||||
setX(v, getX(v)/SCALE);
|
||||
rbin.setVertex(j, v);
|
||||
}
|
||||
out << shapelike::serialize<Formats::SVG>(rbin.rawShape()) << std::endl;
|
||||
for(Item& sh : r) {
|
||||
Item tsh(sh.transformedShape());
|
||||
// Item rbin( RectangleItem(bin.width(), bin.height()) );
|
||||
// for(unsigned j = 0; j < rbin.vertexCount(); j++) {
|
||||
// auto v = rbin.vertex(j);
|
||||
// setY(v, -getY(v)/SCALE + 500 );
|
||||
// setX(v, getX(v)/SCALE);
|
||||
// rbin.setVertex(j, v);
|
||||
// }
|
||||
// out << shapelike::serialize<Formats::SVG>(rbin.rawShape()) << std::endl;
|
||||
for(auto it = from; it != to; ++it) {
|
||||
const Item &itm = *it;
|
||||
Item tsh(itm.transformedShape());
|
||||
for(unsigned j = 0; j < tsh.vertexCount(); j++) {
|
||||
auto v = tsh.vertex(j);
|
||||
setY(v, -getY(v)/SCALE + 500);
|
||||
@ -513,6 +511,12 @@ void exportSVG(std::vector<std::reference_wrapper<Item>>& result, const Bin& bin
|
||||
// i++;
|
||||
// }
|
||||
}
|
||||
|
||||
template<long long SCALE = 1>
|
||||
void exportSVG(std::vector<std::reference_wrapper<Item>>& result, int idx = 0) {
|
||||
exportSVG((std::string("out") + std::to_string(idx) + ".svg").c_str(),
|
||||
result.begin(), result.end());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("BottomLeftStressTest", "[Geometry]") {
|
||||
@ -541,7 +545,7 @@ TEST_CASE("BottomLeftStressTest", "[Geometry]") {
|
||||
valid = (valid && !r1.isInside(r2) && !r2.isInside(r1));
|
||||
if(!valid) {
|
||||
std::cout << "error index: " << i << std::endl;
|
||||
exportSVG(result, bin, i);
|
||||
exportSVG<SCALE>(result, i);
|
||||
}
|
||||
REQUIRE(valid);
|
||||
} else {
|
||||
@ -894,7 +898,7 @@ void testNfp(const std::vector<ItemPair>& testdata) {
|
||||
|
||||
int TEST_CASEcase = 0;
|
||||
|
||||
auto& exportfun = exportSVG<SCALE, Box>;
|
||||
auto& exportfun = exportSVG<SCALE>;
|
||||
|
||||
auto onetest = [&](Item& orbiter, Item& stationary, unsigned /*testidx*/){
|
||||
TEST_CASEcase++;
|
||||
@ -941,7 +945,7 @@ void testNfp(const std::vector<ItemPair>& testdata) {
|
||||
std::ref(stationary), std::ref(tmp), std::ref(infp)
|
||||
};
|
||||
|
||||
exportfun(inp, bin, TEST_CASEcase*i++);
|
||||
exportfun(inp, TEST_CASEcase*i++);
|
||||
}
|
||||
|
||||
REQUIRE(touching);
|
||||
@ -1096,3 +1100,91 @@ TEST_CASE("MinAreaBBWithRotatingCalipers", "[Geometry]") {
|
||||
REQUIRE(succ);
|
||||
}
|
||||
}
|
||||
|
||||
template<class It> MultiPolygon merged_pile(It from, It to, int bin_id)
|
||||
{
|
||||
MultiPolygon pile;
|
||||
pile.reserve(size_t(to - from));
|
||||
|
||||
for (auto it = from; it != to; ++it) {
|
||||
if (it->binId() == bin_id) pile.emplace_back(it->transformedShape());
|
||||
}
|
||||
|
||||
return nfp::merge(pile);
|
||||
}
|
||||
|
||||
TEST_CASE("Test for bed center distance optimization", "[Nesting], [NestKernels]")
|
||||
{
|
||||
static const constexpr ClipperLib::cInt W = 10000000;
|
||||
|
||||
// Get the input items and define the bin.
|
||||
std::vector<RectangleItem> input(9, {W, W});
|
||||
|
||||
auto bin = Box::infinite();
|
||||
|
||||
NfpPlacer::Config pconfig;
|
||||
|
||||
pconfig.object_function = [](const Item &item) -> double {
|
||||
return pl::magnsq<PointImpl, double>(item.boundingBox().center());
|
||||
};
|
||||
|
||||
size_t bins = nest(input, bin, 0, NestConfig{pconfig});
|
||||
|
||||
REQUIRE(bins == 1);
|
||||
|
||||
// Gather the items into piles of arranged polygons...
|
||||
MultiPolygon pile;
|
||||
pile.reserve(input.size());
|
||||
|
||||
for (auto &itm : input) {
|
||||
REQUIRE(itm.binId() == 0);
|
||||
pile.emplace_back(itm.transformedShape());
|
||||
}
|
||||
|
||||
MultiPolygon m = merged_pile(input.begin(), input.end(), 0);
|
||||
|
||||
REQUIRE(m.size() == 1);
|
||||
|
||||
REQUIRE(sl::area(m) == Approx(9. * W * W));
|
||||
}
|
||||
|
||||
TEST_CASE("Test for biggest bounding box area", "[Nesting], [NestKernels]")
|
||||
{
|
||||
static const constexpr ClipperLib::cInt W = 10000000;
|
||||
static const constexpr size_t N = 100;
|
||||
|
||||
// Get the input items and define the bin.
|
||||
std::vector<RectangleItem> input(N, {W, W});
|
||||
|
||||
auto bin = Box::infinite();
|
||||
|
||||
NfpPlacer::Config pconfig;
|
||||
pconfig.rotations = {0.};
|
||||
Box pile_box;
|
||||
pconfig.before_packing =
|
||||
[&pile_box](const MultiPolygon &pile,
|
||||
const _ItemGroup<PolygonImpl> &/*packed_items*/,
|
||||
const _ItemGroup<PolygonImpl> &/*remaining_items*/) {
|
||||
pile_box = sl::boundingBox(pile);
|
||||
};
|
||||
|
||||
pconfig.object_function = [&pile_box](const Item &item) -> double {
|
||||
Box b = sl::boundingBox(item.boundingBox(), pile_box);
|
||||
double area = b.area<double>() / (W * W);
|
||||
return -area;
|
||||
};
|
||||
|
||||
size_t bins = nest(input, bin, 0, NestConfig{pconfig});
|
||||
|
||||
// To debug:
|
||||
exportSVG<1000000>("out", input.begin(), input.end());
|
||||
|
||||
REQUIRE(bins == 1);
|
||||
|
||||
MultiPolygon pile = merged_pile(input.begin(), input.end(), 0);
|
||||
Box bb = sl::boundingBox(pile);
|
||||
|
||||
// Here the result shall be a stairway of boxes
|
||||
REQUIRE(pile.size() == N);
|
||||
REQUIRE(bb.area() == N * N * W * W);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_stl.cpp
|
||||
test_meshsimplify.cpp
|
||||
test_meshboolean.cpp
|
||||
test_marchingsquares.cpp
|
||||
test_timeutils.cpp
|
||||
)
|
||||
|
||||
|
371
tests/libslic3r/test_marchingsquares.cpp
Normal file
371
tests/libslic3r/test_marchingsquares.cpp
Normal file
@ -0,0 +1,371 @@
|
||||
#define NOMINMAX
|
||||
|
||||
#include <catch2/catch.hpp>
|
||||
#include <test_utils.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <libslic3r/MarchingSquares.hpp>
|
||||
#include <libslic3r/SLA/RasterToPolygons.hpp>
|
||||
|
||||
#include <libslic3r/SLA/AGGRaster.hpp>
|
||||
#include <libslic3r/MTUtils.hpp>
|
||||
#include <libslic3r/SVG.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
||||
#include <libslic3r/TriangulateWall.hpp>
|
||||
#include <libslic3r/Tesselate.hpp>
|
||||
#include <libslic3r/SlicesToTriangleMesh.hpp>
|
||||
#include <libslic3r/SLA/Contour3D.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
static double area(const sla::RasterBase::PixelDim &pxd)
|
||||
{
|
||||
return pxd.w_mm * pxd.h_mm;
|
||||
}
|
||||
|
||||
static Slic3r::sla::RasterGrayscaleAA create_raster(
|
||||
const sla::RasterBase::Resolution &res,
|
||||
double disp_w = 100.,
|
||||
double disp_h = 100.)
|
||||
{
|
||||
sla::RasterBase::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
|
||||
|
||||
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
|
||||
sla::RasterBase::Trafo trafo;
|
||||
trafo.center_x = bb.center().x();
|
||||
trafo.center_y = bb.center().y();
|
||||
|
||||
return sla::RasterGrayscaleAA{res, pixdim, trafo, agg::gamma_threshold(.5)};
|
||||
}
|
||||
|
||||
static ExPolygon square(double a, Point center = {0, 0})
|
||||
{
|
||||
ExPolygon poly;
|
||||
coord_t V = scaled(a / 2.);
|
||||
|
||||
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
|
||||
poly.translate(center.x(), center.y());
|
||||
|
||||
return poly;
|
||||
}
|
||||
|
||||
static ExPolygon square_with_hole(double a, Point center = {0, 0})
|
||||
{
|
||||
ExPolygon poly = square(a);
|
||||
|
||||
poly.holes.emplace_back();
|
||||
coord_t V = scaled(a / 4.);
|
||||
poly.holes.front().points = {{-V, V}, {V, V}, {V, -V}, {-V, -V}};
|
||||
|
||||
poly.translate(center.x(), center.y());
|
||||
|
||||
return poly;
|
||||
}
|
||||
|
||||
static ExPolygons circle_with_hole(double r, Point center = {0, 0}) {
|
||||
|
||||
ExPolygon poly;
|
||||
|
||||
std::vector<double> pis = linspace_vector(0., 2 * PI, 100);
|
||||
|
||||
coord_t rs = scaled(r);
|
||||
for (double phi : pis) {
|
||||
poly.contour.points.emplace_back(rs * std::cos(phi), rs * std::sin(phi));
|
||||
}
|
||||
|
||||
poly.holes.emplace_back(poly.contour);
|
||||
poly.holes.front().reverse();
|
||||
for (auto &p : poly.holes.front().points) p /= 2;
|
||||
|
||||
poly.translate(center.x(), center.y());
|
||||
|
||||
return {poly};
|
||||
}
|
||||
|
||||
static const Vec2i W4x4 = {4, 4};
|
||||
static const Vec2i W2x2 = {2, 2};
|
||||
|
||||
template<class Rst>
|
||||
static void test_expolys(Rst && rst,
|
||||
const ExPolygons & ref,
|
||||
Vec2i window,
|
||||
const std::string &name = "test")
|
||||
{
|
||||
for (const ExPolygon &expoly : ref) rst.draw(expoly);
|
||||
|
||||
std::fstream out(name + ".png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
|
||||
ExPolygons extracted = sla::raster_to_polygons(rst, window);
|
||||
|
||||
SVG svg(name + ".svg");
|
||||
svg.draw(extracted);
|
||||
svg.draw(ref, "green");
|
||||
svg.Close();
|
||||
|
||||
double max_rel_err = 0.1;
|
||||
sla::RasterBase::PixelDim pxd = rst.pixel_dimensions();
|
||||
double max_abs_err = area(pxd) * scaled(1.) * scaled(1.);
|
||||
|
||||
BoundingBox ref_bb;
|
||||
for (auto &expoly : ref) ref_bb.merge(expoly.contour.bounding_box());
|
||||
|
||||
double max_displacement = 4. * (std::pow(pxd.h_mm, 2) + std::pow(pxd.w_mm, 2));
|
||||
max_displacement *= scaled<double>(1.) * scaled(1.);
|
||||
|
||||
REQUIRE(extracted.size() == ref.size());
|
||||
for (size_t i = 0; i < ref.size(); ++i) {
|
||||
REQUIRE(extracted[i].contour.is_counter_clockwise());
|
||||
REQUIRE(extracted[i].holes.size() == ref[i].holes.size());
|
||||
|
||||
for (auto &h : extracted[i].holes) REQUIRE(h.is_clockwise());
|
||||
|
||||
double refa = ref[i].area();
|
||||
double abs_err = std::abs(extracted[i].area() - refa);
|
||||
double rel_err = abs_err / refa;
|
||||
|
||||
REQUIRE((rel_err <= max_rel_err || abs_err <= max_abs_err));
|
||||
|
||||
BoundingBox bb;
|
||||
for (auto &expoly : extracted) bb.merge(expoly.contour.bounding_box());
|
||||
|
||||
Point d = bb.center() - ref_bb.center();
|
||||
REQUIRE(double(d.transpose() * d) <= max_displacement);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Empty raster should result in empty polygons", "[MarchingSquares]") {
|
||||
sla::RasterGrayscaleAAGammaPower rst{{}, {}, {}};
|
||||
ExPolygons extracted = sla::raster_to_polygons(rst);
|
||||
REQUIRE(extracted.size() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Marching squares directions", "[MarchingSquares]") {
|
||||
marchsq::Coord crd{1, 1};
|
||||
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::left).r == 1);
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::left).c == 0);
|
||||
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::down).r == 2);
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::down).c == 1);
|
||||
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::right).r == 1);
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::right).c == 2);
|
||||
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::up).r == 0);
|
||||
REQUIRE(step(crd, marchsq::__impl::Dir::up).c == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Fully covered raster should result in a rectangle", "[MarchingSquares]") {
|
||||
auto rst = create_raster({4, 4}, 4., 4.);
|
||||
|
||||
ExPolygon rect = square(4);
|
||||
|
||||
SECTION("Full accuracy") {
|
||||
test_expolys(rst, {rect}, W2x2, "fully_covered_full_acc");
|
||||
}
|
||||
|
||||
SECTION("Half accuracy") {
|
||||
test_expolys(rst, {rect}, W4x4, "fully_covered_half_acc");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("4x4 raster with one ring", "[MarchingSquares]") {
|
||||
|
||||
sla::RasterBase::PixelDim pixdim{1, 1};
|
||||
|
||||
// We need one additional row and column to detect edges
|
||||
sla::RasterGrayscaleAA rst{{4, 4}, pixdim, {}, agg::gamma_threshold(.5)};
|
||||
|
||||
// Draw a triangle from individual pixels
|
||||
rst.draw(square(1., {0500000, 0500000}));
|
||||
rst.draw(square(1., {1500000, 0500000}));
|
||||
rst.draw(square(1., {2500000, 0500000}));
|
||||
|
||||
rst.draw(square(1., {1500000, 1500000}));
|
||||
rst.draw(square(1., {2500000, 1500000}));
|
||||
|
||||
rst.draw(square(1., {2500000, 2500000}));
|
||||
|
||||
std::fstream out("4x4.png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
|
||||
ExPolygons extracted = sla::raster_to_polygons(rst);
|
||||
|
||||
SVG svg("4x4.svg");
|
||||
svg.draw(extracted);
|
||||
svg.Close();
|
||||
|
||||
REQUIRE(extracted.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("4x4 raster with two rings", "[MarchingSquares]") {
|
||||
|
||||
sla::RasterBase::PixelDim pixdim{1, 1};
|
||||
|
||||
// We need one additional row and column to detect edges
|
||||
sla::RasterGrayscaleAA rst{{5, 5}, pixdim, {}, agg::gamma_threshold(.5)};
|
||||
|
||||
SECTION("Ambiguous case with 'ac' square") {
|
||||
|
||||
// Draw a triangle from individual pixels
|
||||
rst.draw(square(1., {3500000, 2500000}));
|
||||
rst.draw(square(1., {3500000, 3500000}));
|
||||
rst.draw(square(1., {2500000, 3500000}));
|
||||
|
||||
rst.draw(square(1., {2500000, 1500000}));
|
||||
rst.draw(square(1., {1500000, 1500000}));
|
||||
rst.draw(square(1., {1500000, 2500000}));
|
||||
|
||||
std::fstream out("4x4_ac.png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
|
||||
ExPolygons extracted = sla::raster_to_polygons(rst);
|
||||
|
||||
SVG svg("4x4_ac.svg");
|
||||
svg.draw(extracted);
|
||||
svg.Close();
|
||||
|
||||
REQUIRE(extracted.size() == 2);
|
||||
}
|
||||
|
||||
SECTION("Ambiguous case with 'bd' square") {
|
||||
|
||||
// Draw a triangle from individual pixels
|
||||
rst.draw(square(1., {3500000, 1500000}));
|
||||
rst.draw(square(1., {3500000, 2500000}));
|
||||
rst.draw(square(1., {2500000, 1500000}));
|
||||
|
||||
rst.draw(square(1., {1500000, 2500000}));
|
||||
rst.draw(square(1., {1500000, 3500000}));
|
||||
rst.draw(square(1., {2500000, 3500000}));
|
||||
|
||||
std::fstream out("4x4_bd.png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
|
||||
ExPolygons extracted = sla::raster_to_polygons(rst);
|
||||
|
||||
SVG svg("4x4_bd.svg");
|
||||
svg.draw(extracted);
|
||||
svg.Close();
|
||||
|
||||
REQUIRE(extracted.size() == 2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Square with hole in the middle", "[MarchingSquares]") {
|
||||
using namespace Slic3r;
|
||||
|
||||
ExPolygons inp = {square_with_hole(50.)};
|
||||
|
||||
SECTION("Proportional raster, 1x1 mm pixel size, full accuracy") {
|
||||
test_expolys(create_raster({100, 100}, 100., 100.), inp, W2x2, "square_with_hole_proportional_1x1_mm_px_full");
|
||||
}
|
||||
|
||||
SECTION("Proportional raster, 1x1 mm pixel size, half accuracy") {
|
||||
test_expolys(create_raster({100, 100}, 100., 100.), inp, W4x4, "square_with_hole_proportional_1x1_mm_px_half");
|
||||
}
|
||||
|
||||
SECTION("Landscape raster, 1x1 mm pixel size, full accuracy") {
|
||||
test_expolys(create_raster({150, 100}, 150., 100.), inp, W2x2, "square_with_hole_landsc_1x1_mm_px_full");
|
||||
}
|
||||
|
||||
SECTION("Landscape raster, 1x1 mm pixel size, half accuracy") {
|
||||
test_expolys(create_raster({150, 100}, 150., 100.), inp, W4x4, "square_with_hole_landsc_1x1_mm_px_half");
|
||||
}
|
||||
|
||||
SECTION("Portrait raster, 1x1 mm pixel size, full accuracy") {
|
||||
test_expolys(create_raster({100, 150}, 100., 150.), inp, W2x2, "square_with_hole_portrait_1x1_mm_px_full");
|
||||
}
|
||||
|
||||
SECTION("Portrait raster, 1x1 mm pixel size, half accuracy") {
|
||||
test_expolys(create_raster({100, 150}, 100., 150.), inp, W4x4, "square_with_hole_portrait_1x1_mm_px_half");
|
||||
}
|
||||
|
||||
SECTION("Proportional raster, 2x2 mm pixel size, full accuracy") {
|
||||
test_expolys(create_raster({200, 200}, 100., 100.), inp, W2x2, "square_with_hole_proportional_2x2_mm_px_full");
|
||||
}
|
||||
|
||||
SECTION("Proportional raster, 2x2 mm pixel size, half accuracy") {
|
||||
test_expolys(create_raster({200, 200}, 100., 100.), inp, W4x4, "square_with_hole_proportional_2x2_mm_px_half");
|
||||
}
|
||||
|
||||
SECTION("Proportional raster, 0.5x0.5 mm pixel size, full accuracy") {
|
||||
test_expolys(create_raster({50, 50}, 100., 100.), inp, W2x2, "square_with_hole_proportional_0.5x0.5_mm_px_full");
|
||||
}
|
||||
|
||||
SECTION("Proportional raster, 0.5x0.5 mm pixel size, half accuracy") {
|
||||
test_expolys(create_raster({50, 50}, 100., 100.), inp, W4x4, "square_with_hole_proportional_0.5x0.5_mm_px_half");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Circle with hole in the middle", "[MarchingSquares]") {
|
||||
using namespace Slic3r;
|
||||
|
||||
test_expolys(create_raster({1000, 1000}), circle_with_hole(25.), W2x2, "circle_with_hole");
|
||||
}
|
||||
|
||||
static void recreate_object_from_rasters(const std::string &objname, float lh) {
|
||||
TriangleMesh mesh = load_model(objname);
|
||||
|
||||
auto bb = mesh.bounding_box();
|
||||
Vec3f tr = -bb.center().cast<float>();
|
||||
mesh.translate(tr.x(), tr.y(), tr.z());
|
||||
bb = mesh.bounding_box();
|
||||
|
||||
std::vector<ExPolygons> layers;
|
||||
slice_mesh(mesh, grid(float(bb.min.z()) + lh, float(bb.max.z()), lh), layers, 0.f, []{});
|
||||
|
||||
sla::RasterBase::Resolution res{2560, 1440};
|
||||
double disp_w = 120.96;
|
||||
double disp_h = 68.04;
|
||||
|
||||
size_t cntr = 0;
|
||||
for (ExPolygons &layer : layers) {
|
||||
auto rst = create_raster(res, disp_w, disp_h);
|
||||
|
||||
for (ExPolygon &island : layer) {
|
||||
rst.draw(island);
|
||||
}
|
||||
|
||||
std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out);
|
||||
out << rst.encode(sla::PNGRasterEncoder{});
|
||||
out.close();
|
||||
|
||||
ExPolygons layer_ = sla::raster_to_polygons(rst);
|
||||
// float delta = scaled(std::min(rst.pixel_dimensions().h_mm,
|
||||
// rst.pixel_dimensions().w_mm)) / 2;
|
||||
|
||||
// layer_ = expolygons_simplify(layer_, delta);
|
||||
|
||||
SVG svg(objname + std::to_string(cntr) + ".svg", BoundingBox(Point{0, 0}, Point{scaled(disp_w), scaled(disp_h)}));
|
||||
svg.draw(layer_);
|
||||
svg.draw(layer, "green");
|
||||
svg.Close();
|
||||
|
||||
double layera = 0., layera_ = 0.;
|
||||
for (auto &p : layer) layera += p.area();
|
||||
for (auto &p : layer_) layera_ += p.area();
|
||||
|
||||
std::cout << cntr++ << std::endl;
|
||||
double diff = std::abs(layera_ - layera);
|
||||
REQUIRE((diff <= 0.1 * layera || diff < scaled<double>(1.) * scaled<double>(1.)));
|
||||
|
||||
layer = std::move(layer_);
|
||||
}
|
||||
|
||||
TriangleMesh out = slices_to_triangle_mesh(layers, bb.min.z(), double(lh), double(lh));
|
||||
|
||||
out.require_shared_vertices();
|
||||
out.WriteOBJFile("out_from_rasters.obj");
|
||||
}
|
||||
|
||||
TEST_CASE("Recreate object from rasters", "[SL1Import]") {
|
||||
recreate_object_from_rasters("frog_legs.obj", 0.05f);
|
||||
}
|
@ -154,19 +154,12 @@ TEST_CASE("FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") {
|
||||
test_support_model_collision(fname, supportcfg);
|
||||
}
|
||||
|
||||
TEST_CASE("DefaultRasterShouldBeEmpty", "[SLARasterOutput]") {
|
||||
sla::Raster raster;
|
||||
REQUIRE(raster.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") {
|
||||
// Default Prusa SL1 display parameters
|
||||
sla::Raster::Resolution res{2560, 1440};
|
||||
sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px};
|
||||
sla::RasterBase::Resolution res{2560, 1440};
|
||||
sla::RasterBase::PixelDim pixdim{120. / res.width_px, 68. / res.height_px};
|
||||
|
||||
sla::Raster raster;
|
||||
raster.reset(res, pixdim);
|
||||
REQUIRE_FALSE(raster.empty());
|
||||
sla::RasterGrayscaleAAGammaPower raster(res, pixdim, {}, 1.);
|
||||
REQUIRE(raster.resolution().width_px == res.width_px);
|
||||
REQUIRE(raster.resolution().height_px == res.height_px);
|
||||
REQUIRE(raster.pixel_dimensions().w_mm == Approx(pixdim.w_mm));
|
||||
@ -174,13 +167,14 @@ TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") {
|
||||
}
|
||||
|
||||
TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") {
|
||||
sla::Raster::TMirroring mirrorings[] = {sla::Raster::NoMirror,
|
||||
sla::Raster::MirrorX,
|
||||
sla::Raster::MirrorY,
|
||||
sla::Raster::MirrorXY};
|
||||
sla::RasterBase::TMirroring mirrorings[] = {sla::RasterBase::NoMirror,
|
||||
sla::RasterBase::MirrorX,
|
||||
sla::RasterBase::MirrorY,
|
||||
sla::RasterBase::MirrorXY};
|
||||
|
||||
sla::RasterBase::Orientation orientations[] =
|
||||
{sla::RasterBase::roLandscape, sla::RasterBase::roPortrait};
|
||||
|
||||
sla::Raster::Orientation orientations[] = {sla::Raster::roLandscape,
|
||||
sla::Raster::roPortrait};
|
||||
for (auto orientation : orientations)
|
||||
for (auto &mirror : mirrorings)
|
||||
check_raster_transformations(orientation, mirror);
|
||||
@ -189,10 +183,11 @@ TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") {
|
||||
|
||||
TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") {
|
||||
double disp_w = 120., disp_h = 68.;
|
||||
sla::Raster::Resolution res{2560, 1440};
|
||||
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
|
||||
sla::RasterBase::Resolution res{2560, 1440};
|
||||
sla::RasterBase::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
|
||||
|
||||
sla::Raster raster{res, pixdim};
|
||||
double gamma = 1.;
|
||||
sla::RasterGrayscaleAAGammaPower raster(res, pixdim, {}, gamma);
|
||||
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
|
||||
|
||||
ExPolygon poly = square_with_hole(10.);
|
||||
@ -215,6 +210,13 @@ TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") {
|
||||
diff = std::abs(a - ra);
|
||||
|
||||
REQUIRE(diff <= predict_error(poly, pixdim));
|
||||
|
||||
sla::RasterGrayscaleAA raster0(res, pixdim, {}, [](double) { return 0.; });
|
||||
REQUIRE(raster_pxsum(raster0) == 0);
|
||||
|
||||
raster0.draw(poly);
|
||||
ra = raster_white_area(raster);
|
||||
REQUIRE(raster_pxsum(raster0) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Triangle mesh conversions should be correct", "[SLAConversions]")
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include "sla_test_utils.hpp"
|
||||
#include "libslic3r/SLA/AGGRaster.hpp"
|
||||
|
||||
void test_support_model_collision(const std::string &obj_filename,
|
||||
const sla::SupportConfig &input_supportcfg,
|
||||
@ -293,18 +294,19 @@ void check_validity(const TriangleMesh &input_mesh, int flags)
|
||||
}
|
||||
}
|
||||
|
||||
void check_raster_transformations(sla::Raster::Orientation o, sla::Raster::TMirroring mirroring)
|
||||
void check_raster_transformations(sla::RasterBase::Orientation o, sla::RasterBase::TMirroring mirroring)
|
||||
{
|
||||
double disp_w = 120., disp_h = 68.;
|
||||
sla::Raster::Resolution res{2560, 1440};
|
||||
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
|
||||
sla::RasterBase::Resolution res{2560, 1440};
|
||||
sla::RasterBase::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
|
||||
|
||||
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
|
||||
sla::Raster::Trafo trafo{o, mirroring};
|
||||
trafo.origin_x = bb.center().x();
|
||||
trafo.origin_y = bb.center().y();
|
||||
sla::RasterBase::Trafo trafo{o, mirroring};
|
||||
trafo.center_x = bb.center().x();
|
||||
trafo.center_y = bb.center().y();
|
||||
double gamma = 1.;
|
||||
|
||||
sla::Raster raster{res, pixdim, trafo};
|
||||
sla::RasterGrayscaleAAGammaPower raster{res, pixdim, trafo, gamma};
|
||||
|
||||
// create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors)
|
||||
coord_t pw = 32 * coord_t(std::ceil(scaled<double>(pixdim.w_mm)));
|
||||
@ -319,7 +321,7 @@ void check_raster_transformations(sla::Raster::Orientation o, sla::Raster::TMirr
|
||||
|
||||
// Now calculate the position of the translated box according to output
|
||||
// trafo.
|
||||
if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.);
|
||||
if (o == sla::RasterBase::Orientation::roPortrait) expected_box.rotate(PI / 2.);
|
||||
|
||||
if (mirroring[X])
|
||||
for (auto &p : expected_box.contour.points) p.x() = -p.x();
|
||||
@ -340,10 +342,9 @@ void check_raster_transformations(sla::Raster::Orientation o, sla::Raster::TMirr
|
||||
auto px = raster.read_pixel(w, h);
|
||||
|
||||
if (px != FullWhite) {
|
||||
sla::PNGImage img;
|
||||
std::fstream outf("out.png", std::ios::out);
|
||||
|
||||
outf << img.serialize(raster);
|
||||
outf << raster.encode(sla::PNGRasterEncoder());
|
||||
}
|
||||
|
||||
REQUIRE(px == FullWhite);
|
||||
@ -361,9 +362,21 @@ ExPolygon square_with_hole(double v)
|
||||
return poly;
|
||||
}
|
||||
|
||||
double raster_white_area(const sla::Raster &raster)
|
||||
long raster_pxsum(const sla::RasterGrayscaleAA &raster)
|
||||
{
|
||||
if (raster.empty()) return std::nan("");
|
||||
auto res = raster.resolution();
|
||||
long a = 0;
|
||||
|
||||
for (size_t x = 0; x < res.width_px; ++x)
|
||||
for (size_t y = 0; y < res.height_px; ++y)
|
||||
a += raster.read_pixel(x, y);
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
double raster_white_area(const sla::RasterGrayscaleAA &raster)
|
||||
{
|
||||
if (raster.resolution().pixels() == 0) return std::nan("");
|
||||
|
||||
auto res = raster.resolution();
|
||||
double a = 0;
|
||||
@ -377,7 +390,7 @@ double raster_white_area(const sla::Raster &raster)
|
||||
return a;
|
||||
}
|
||||
|
||||
double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd)
|
||||
double predict_error(const ExPolygon &p, const sla::RasterBase::PixelDim &pd)
|
||||
{
|
||||
auto lines = p.lines();
|
||||
double pix_err = pixel_area(FullWhite, pd) / 2.;
|
||||
|
@ -16,7 +16,7 @@
|
||||
#include "libslic3r/SLA/SupportTreeBuilder.hpp"
|
||||
#include "libslic3r/SLA/SupportTreeBuildsteps.hpp"
|
||||
#include "libslic3r/SLA/SupportPointGenerator.hpp"
|
||||
#include "libslic3r/SLA/Raster.hpp"
|
||||
#include "libslic3r/SLA/AGGRaster.hpp"
|
||||
#include "libslic3r/SLA/ConcaveHull.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
|
||||
@ -170,18 +170,19 @@ static constexpr const TPixel FullBlack = 0;
|
||||
|
||||
template <class A, int N> constexpr int arraysize(const A (&)[N]) { return N; }
|
||||
|
||||
void check_raster_transformations(sla::Raster::Orientation o,
|
||||
sla::Raster::TMirroring mirroring);
|
||||
void check_raster_transformations(sla::RasterBase::Orientation o,
|
||||
sla::RasterBase::TMirroring mirroring);
|
||||
|
||||
ExPolygon square_with_hole(double v);
|
||||
|
||||
inline double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim)
|
||||
inline double pixel_area(TPixel px, const sla::RasterBase::PixelDim &pxdim)
|
||||
{
|
||||
return (pxdim.h_mm * pxdim.w_mm) * px * 1. / (FullWhite - FullBlack);
|
||||
}
|
||||
|
||||
double raster_white_area(const sla::Raster &raster);
|
||||
double raster_white_area(const sla::RasterGrayscaleAA &raster);
|
||||
long raster_pxsum(const sla::RasterGrayscaleAA &raster);
|
||||
|
||||
double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd);
|
||||
double predict_error(const ExPolygon &p, const sla::RasterBase::PixelDim &pd);
|
||||
|
||||
#endif // SLA_TEST_UTILS_HPP
|
||||
|
@ -50,7 +50,7 @@
|
||||
void erase(t_config_option_key opt_key);
|
||||
void normalize();
|
||||
%name{setenv} void setenv_();
|
||||
double min_object_distance() %code{% PrintConfig cfg; cfg.apply(*THIS, true); RETVAL = cfg.min_object_distance(); %};
|
||||
double min_object_distance() %code{% RETVAL = Slic3r::min_object_distance(*THIS); %};
|
||||
static DynamicPrintConfig* load(char *path)
|
||||
%code%{
|
||||
auto config = new DynamicPrintConfig();
|
||||
@ -114,7 +114,7 @@
|
||||
}
|
||||
%};
|
||||
%name{setenv} void setenv_();
|
||||
double min_object_distance() %code{% RETVAL = PrintConfig::min_object_distance(THIS); %};
|
||||
double min_object_distance() %code{% RETVAL = Slic3r::min_object_distance(*THIS); %};
|
||||
static StaticPrintConfig* load(char *path)
|
||||
%code%{
|
||||
auto config = new FullPrintConfig();
|
||||
|
@ -3,6 +3,7 @@
|
||||
%{
|
||||
#include <xsinit.h>
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Slicing.hpp"
|
||||
@ -80,9 +81,9 @@
|
||||
ModelObjectPtrs* objects()
|
||||
%code%{ RETVAL = &THIS->objects; %};
|
||||
|
||||
bool arrange_objects(double dist, BoundingBoxf* bb = NULL);
|
||||
void duplicate(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL);
|
||||
void duplicate_objects(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL);
|
||||
bool arrange_objects(double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) arrange_objects(*THIS, scaled(*bb), ap); else arrange_objects(*THIS, InfiniteBed{}, ap); %};
|
||||
void duplicate(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) duplicate(*THIS, copies_num, scaled(*bb), ap); else duplicate(*THIS, copies_num, InfiniteBed{}, ap); %};
|
||||
void duplicate_objects(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) duplicate_objects(*THIS, copies_num, scaled(*bb), ap); else duplicate_objects(*THIS, copies_num, InfiniteBed{}, ap); %};
|
||||
void duplicate_objects_grid(unsigned int x, unsigned int y, double dist);
|
||||
|
||||
bool looks_like_multipart_object() const;
|
||||
|
Loading…
Reference in New Issue
Block a user