WIP: Reconstruction of background processing.
This commit is contained in:
parent
f33713e060
commit
bded28f888
17 changed files with 273 additions and 229 deletions
|
@ -242,6 +242,14 @@ BoundingBoxf3 Model::bounding_box() const
|
|||
return bb;
|
||||
}
|
||||
|
||||
unsigned int Model::update_print_volume_state(const BoundingBoxf3 &print_volume)
|
||||
{
|
||||
unsigned int num_printable = 0;
|
||||
for (ModelObject *model_object : this->objects)
|
||||
num_printable += model_object->check_instances_print_volume_state(print_volume);
|
||||
return num_printable;
|
||||
}
|
||||
|
||||
void Model::center_instances_around_point(const Vec2d &point)
|
||||
{
|
||||
BoundingBoxf3 bb;
|
||||
|
@ -875,26 +883,32 @@ void ModelObject::repair()
|
|||
v->mesh.repair();
|
||||
}
|
||||
|
||||
// Called by Print::validate() from the UI thread.
|
||||
void ModelObject::check_instances_print_volume_state(const BoundingBoxf3& print_volume)
|
||||
unsigned int ModelObject::check_instances_print_volume_state(const BoundingBoxf3& print_volume)
|
||||
{
|
||||
for (const ModelVolume* vol : this->volumes)
|
||||
{
|
||||
if (vol->is_model_part())
|
||||
{
|
||||
for (ModelInstance* inst : this->instances)
|
||||
{
|
||||
BoundingBoxf3 bb = vol->get_convex_hull().transformed_bounding_box(inst->world_matrix());
|
||||
|
||||
unsigned int num_printable = 0;
|
||||
enum {
|
||||
INSIDE = 1,
|
||||
OUTSIDE = 2
|
||||
};
|
||||
for (ModelInstance *model_instance : this->instances) {
|
||||
unsigned int inside_outside = 0;
|
||||
for (const ModelVolume *vol : this->volumes)
|
||||
if (vol->is_model_part()) {
|
||||
BoundingBoxf3 bb = vol->get_convex_hull().transformed_bounding_box(model_instance->world_matrix());
|
||||
if (print_volume.contains(bb))
|
||||
inst->print_volume_state = ModelInstance::PVS_Inside;
|
||||
inside_outside |= INSIDE;
|
||||
else if (print_volume.intersects(bb))
|
||||
inst->print_volume_state = ModelInstance::PVS_Partly_Outside;
|
||||
inside_outside |= INSIDE | OUTSIDE;
|
||||
else
|
||||
inst->print_volume_state = ModelInstance::PVS_Fully_Outside;
|
||||
inside_outside |= OUTSIDE;
|
||||
}
|
||||
}
|
||||
model_instance->print_volume_state =
|
||||
(inside_outside == (INSIDE | OUTSIDE)) ? ModelInstance::PVS_Partly_Outside :
|
||||
(inside_outside == INSIDE) ? ModelInstance::PVS_Inside : ModelInstance::PVS_Fully_Outside;
|
||||
if (inside_outside == INSIDE)
|
||||
++ num_printable;
|
||||
}
|
||||
return num_printable;
|
||||
}
|
||||
|
||||
void ModelObject::print_info() const
|
||||
|
|
|
@ -156,7 +156,7 @@ public:
|
|||
void repair();
|
||||
|
||||
// Called by Print::validate() from the UI thread.
|
||||
void check_instances_print_volume_state(const BoundingBoxf3& print_volume);
|
||||
unsigned int check_instances_print_volume_state(const BoundingBoxf3& print_volume);
|
||||
|
||||
// Print object statistics to console.
|
||||
void print_info() const;
|
||||
|
@ -411,6 +411,9 @@ public:
|
|||
bool add_default_instances();
|
||||
// Returns approximate axis aligned bounding box of this model
|
||||
BoundingBoxf3 bounding_box() const;
|
||||
// Set the print_volume_state of PrintObject::instances,
|
||||
// return total number of printable objects.
|
||||
unsigned int update_print_volume_state(const BoundingBoxf3 &print_volume);
|
||||
void 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;
|
||||
|
|
|
@ -852,7 +852,11 @@ bool Print::apply(const Model &model, const DynamicPrintConfig &config_in)
|
|||
bool modifiers_differ = model_volume_list_changed(model_object, model_object_new, ModelVolume::PARAMETER_MODIFIER);
|
||||
bool support_blockers_differ = model_volume_list_changed(model_object, model_object_new, ModelVolume::SUPPORT_BLOCKER);
|
||||
bool support_enforcers_differ = model_volume_list_changed(model_object, model_object_new, ModelVolume::SUPPORT_ENFORCER);
|
||||
if (model_parts_differ || modifiers_differ) {
|
||||
if (model_parts_differ || modifiers_differ ||
|
||||
model_object.origin_translation != model_object_new.origin_translation ||
|
||||
model_object.layer_height_ranges != model_object_new.layer_height_ranges ||
|
||||
model_object.layer_height_profile != model_object_new.layer_height_profile ||
|
||||
model_object.layer_height_profile_valid != model_object_new.layer_height_profile_valid) {
|
||||
// The very first step (the slicing step) is invalidated. One may freely remove all associated PrintObjects.
|
||||
auto range = print_object_status.equal_range(PrintObjectStatus(model_object.id()));
|
||||
for (auto it = range.first; it != range.second; ++ it) {
|
||||
|
@ -889,6 +893,9 @@ bool Print::apply(const Model &model, const DynamicPrintConfig &config_in)
|
|||
it->print_object->config_apply_only(new_config, diff, true);
|
||||
}
|
||||
}
|
||||
model_object.name = model_object_new.name;
|
||||
model_object.input_file = model_object_new.input_file;
|
||||
model_object.instances = model_object_new.instances;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -936,11 +943,10 @@ bool Print::apply(const Model &model, const DynamicPrintConfig &config_in)
|
|||
print_objects_new.emplace_back(print_object);
|
||||
print_object_status.emplace(PrintObjectStatus(print_object, PrintObjectStatus::New));
|
||||
} else if ((*it_old)->print_object->copies() != new_instances.copies) {
|
||||
// The PrintObject already exists and the copies differ. The only step currently sensitive to the order is the G-code generator.
|
||||
// Stop it.
|
||||
this->invalidate_step(psGCodeExport);
|
||||
// The PrintObject already exists and the copies differ.
|
||||
(*it_old)->print_object->set_copies(new_instances.copies);
|
||||
}
|
||||
print_objects_new.emplace_back((*it_old)->print_object);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_objects != print_objects_new) {
|
||||
|
@ -958,7 +964,7 @@ bool Print::apply(const Model &model, const DynamicPrintConfig &config_in)
|
|||
int idx_region = 0;
|
||||
for (const auto &volumes : print_object->region_volumes) {
|
||||
if (! volumes.empty())
|
||||
++ m_regions[idx_region];
|
||||
++ m_regions[idx_region]->m_refcnt;
|
||||
++ idx_region;
|
||||
}
|
||||
}
|
||||
|
@ -1110,23 +1116,7 @@ bool Print::has_skirt() const
|
|||
|
||||
std::string Print::validate() const
|
||||
{
|
||||
BoundingBox bed_box_2D = get_extents(Polygon::new_scale(m_config.bed_shape.values));
|
||||
BoundingBoxf3 print_volume(unscale(bed_box_2D.min(0), bed_box_2D.min(1), 0.0), unscale(bed_box_2D.max(0), bed_box_2D.max(1), scale_(m_config.max_print_height)));
|
||||
// Allow the objects to protrude below the print bed, only the part of the object above the print bed will be sliced.
|
||||
print_volume.min(2) = -1e10;
|
||||
unsigned int printable_count = 0;
|
||||
{
|
||||
// Lock due to the po->reload_model_instances()
|
||||
tbb::mutex::scoped_lock lock(m_mutex);
|
||||
for (PrintObject *po : m_objects) {
|
||||
po->model_object()->check_instances_print_volume_state(print_volume);
|
||||
po->reload_model_instances();
|
||||
if (po->is_printable())
|
||||
++ printable_count;
|
||||
}
|
||||
}
|
||||
|
||||
if (printable_count == 0)
|
||||
if (m_objects.empty())
|
||||
return L("All objects are outside of the print volume.");
|
||||
|
||||
if (m_config.complete_objects) {
|
||||
|
|
|
@ -96,6 +96,7 @@ public:
|
|||
#endif
|
||||
mtx.unlock();
|
||||
cancel();
|
||||
m_state[step] = INVALID;
|
||||
mtx.lock();
|
||||
}
|
||||
return invalidated;
|
||||
|
|
|
@ -87,23 +87,25 @@ bool PrintObject::delete_last_copy()
|
|||
|
||||
bool PrintObject::set_copies(const Points &points)
|
||||
{
|
||||
bool copies_num_changed = m_copies.size() != points.size();
|
||||
|
||||
// order copies with a nearest neighbor search and translate them by _copies_shift
|
||||
m_copies.clear();
|
||||
m_copies.reserve(points.size());
|
||||
|
||||
// order copies with a nearest-neighbor search
|
||||
std::vector<Points::size_type> ordered_copies;
|
||||
Slic3r::Geometry::chained_path(points, ordered_copies);
|
||||
|
||||
for (size_t point_idx : ordered_copies)
|
||||
m_copies.push_back(points[point_idx] + m_copies_shift);
|
||||
|
||||
bool invalidated = m_print->invalidate_step(psSkirt);
|
||||
invalidated |= m_print->invalidate_step(psBrim);
|
||||
if (copies_num_changed)
|
||||
invalidated |= m_print->invalidate_step(psWipeTower);
|
||||
// Order copies with a nearest-neighbor search.
|
||||
std::vector<Point> copies;
|
||||
{
|
||||
std::vector<Points::size_type> ordered_copies;
|
||||
Slic3r::Geometry::chained_path(points, ordered_copies);
|
||||
copies.reserve(ordered_copies.size());
|
||||
for (size_t point_idx : ordered_copies)
|
||||
copies.emplace_back(points[point_idx] + m_copies_shift);
|
||||
}
|
||||
// Invalidate and set copies.
|
||||
bool invalidated = false;
|
||||
if (copies != m_copies) {
|
||||
invalidated = m_print->invalidate_step(psSkirt);
|
||||
invalidated |= m_print->invalidate_step(psBrim);
|
||||
if (copies.size() != m_copies.size())
|
||||
invalidated |= m_print->invalidate_step(psWipeTower);
|
||||
invalidated |= m_print->invalidate_step(psGCodeExport);
|
||||
m_copies = copies;
|
||||
}
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
|
|
|
@ -858,12 +858,12 @@ void TriangleMeshSlicer::_slice_do(size_t facet_idx, std::vector<IntersectionLin
|
|||
// find layer extents
|
||||
std::vector<float>::const_iterator min_layer, max_layer;
|
||||
min_layer = std::lower_bound(z.begin(), z.end(), min_z); // first layer whose slice_z is >= min_z
|
||||
max_layer = std::upper_bound(z.begin() + (min_layer - z.begin()), z.end(), max_z) - 1; // last layer whose slice_z is <= max_z
|
||||
max_layer = std::upper_bound(z.begin() + (min_layer - z.begin()), z.end(), max_z); // first layer, whose slice_z is > max_z
|
||||
#ifdef SLIC3R_TRIANGLEMESH_DEBUG
|
||||
printf("layers: min = %d, max = %d\n", (int)(min_layer - z.begin()), (int)(max_layer - z.begin()));
|
||||
printf("layers: min = %d, max = %d\n", (int)(min_layer - z.begin()), (int)(max_layer - z.begin()) - 1);
|
||||
#endif /* SLIC3R_TRIANGLEMESH_DEBUG */
|
||||
|
||||
for (std::vector<float>::const_iterator it = min_layer; it != max_layer + 1; ++it) {
|
||||
for (std::vector<float>::const_iterator it = min_layer; it != max_layer; ++it) {
|
||||
std::vector<float>::size_type layer_idx = it - z.begin();
|
||||
IntersectionLine il;
|
||||
if (this->slice_facet(*it / SCALING_FACTOR, facet, facet_idx, min_z, max_z, &il) == TriangleMeshSlicer::Slicing) {
|
||||
|
|
|
@ -57,12 +57,20 @@ void BackgroundSlicingProcess::thread_proc()
|
|||
if (! m_print->canceled()) {
|
||||
wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_sliced_id));
|
||||
m_print->export_gcode(m_temp_output_path, m_gcode_preview_data);
|
||||
if (! m_print->canceled() && ! m_output_path.empty()) {
|
||||
if (copy_file(m_temp_output_path, m_output_path) != 0)
|
||||
throw std::runtime_error("Copying of the temporary G-code to the output G-code failed");
|
||||
m_print->set_status(95, "Running post-processing scripts");
|
||||
run_post_process_scripts(m_output_path, m_print->config());
|
||||
}
|
||||
if (! m_print->canceled() && ! this->is_step_done(bspsGCodeFinalize)) {
|
||||
this->set_step_started(bspsGCodeFinalize);
|
||||
if (! m_export_path.empty()) {
|
||||
//FIXME localize the messages
|
||||
if (copy_file(m_temp_output_path, m_export_path) != 0)
|
||||
throw std::runtime_error("Copying of the temporary G-code to the output G-code failed");
|
||||
m_print->set_status(95, "Running post-processing scripts");
|
||||
run_post_process_scripts(m_export_path, m_print->config());
|
||||
m_print->set_status(100, "G-code file exported to " + m_export_path);
|
||||
} else {
|
||||
m_print->set_status(100, "Slicing complete");
|
||||
}
|
||||
this->set_step_done(bspsGCodeFinalize);
|
||||
}
|
||||
}
|
||||
} catch (CanceledException &ex) {
|
||||
// Canceled, this is all right.
|
||||
|
@ -133,7 +141,7 @@ bool BackgroundSlicingProcess::stop()
|
|||
{
|
||||
std::unique_lock<std::mutex> lck(m_mutex);
|
||||
if (m_state == STATE_INITIAL) {
|
||||
this->m_output_path.clear();
|
||||
// this->m_export_path.clear();
|
||||
return false;
|
||||
}
|
||||
// assert(this->running());
|
||||
|
@ -147,7 +155,7 @@ bool BackgroundSlicingProcess::stop()
|
|||
// In the "Finished" or "Canceled" state. Reset the state to "Idle".
|
||||
m_state = STATE_IDLE;
|
||||
}
|
||||
this->m_output_path.clear();
|
||||
// this->m_export_path.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -169,4 +177,53 @@ bool BackgroundSlicingProcess::apply(const Model &model, const DynamicPrintConfi
|
|||
return invalidated;
|
||||
}
|
||||
|
||||
// Set the output path of the G-code.
|
||||
void BackgroundSlicingProcess::schedule_export(const std::string &path)
|
||||
{
|
||||
assert(m_export_path.empty());
|
||||
if (! m_export_path.empty())
|
||||
return;
|
||||
|
||||
// Guard against entering the export step before changing the export path.
|
||||
tbb::mutex::scoped_lock lock(m_step_state_mutex);
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
m_export_path = path;
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::reset_export()
|
||||
{
|
||||
assert(! this->running());
|
||||
if (! this->running()) {
|
||||
m_export_path.clear();
|
||||
// invalidate_step expects the mutex to be locked.
|
||||
tbb::mutex::scoped_lock lock(m_step_state_mutex);
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
}
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::set_step_started(BackgroundSlicingProcessStep step)
|
||||
{
|
||||
m_step_state.set_started(step, m_step_state_mutex);
|
||||
if (m_print->canceled())
|
||||
throw CanceledException();
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::set_step_done(BackgroundSlicingProcessStep step)
|
||||
{
|
||||
m_step_state.set_done(step, m_step_state_mutex);
|
||||
if (m_print->canceled())
|
||||
throw CanceledException();
|
||||
}
|
||||
|
||||
bool BackgroundSlicingProcess::invalidate_step(BackgroundSlicingProcessStep step)
|
||||
{
|
||||
bool invalidated = m_step_state.invalidate(step, m_step_state_mutex, [this](){ this->stop(); });
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
bool BackgroundSlicingProcess::invalidate_all_steps()
|
||||
{
|
||||
return m_step_state.invalidate_all(m_step_state_mutex, [this](){ this->stop(); });
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "Print.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
@ -13,6 +15,11 @@ class GCodePreviewData;
|
|||
class Model;
|
||||
class Print;
|
||||
|
||||
// Print step IDs for keeping track of the print state.
|
||||
enum BackgroundSlicingProcessStep {
|
||||
bspsGCodeFinalize, bspsCount,
|
||||
};
|
||||
|
||||
// Support for the GUI background processing (Slicing and G-code generation).
|
||||
// As of now this class is not declared in Slic3r::GUI due to the Perl bindings limits.
|
||||
class BackgroundSlicingProcess
|
||||
|
@ -32,8 +39,6 @@ public:
|
|||
// The wxCommandEvent is sent to the UI thread asynchronously without waiting for the event to be processed.
|
||||
void set_finished_event(int event_id) { m_event_finished_id = event_id; }
|
||||
|
||||
// Set the output path of the G-code.
|
||||
void set_output_path(const std::string &path) { m_output_path = path; }
|
||||
// Start the background processing. Returns false if the background processing was already running.
|
||||
bool start();
|
||||
// Cancel the background processing. Returns false if the background processing was not running.
|
||||
|
@ -46,6 +51,13 @@ public:
|
|||
// Apply config over the print. Returns false, if the new config values caused any of the already
|
||||
// processed steps to be invalidated, therefore the task will need to be restarted.
|
||||
bool apply(const Model &model, const DynamicPrintConfig &config);
|
||||
// Set the export path of the G-code.
|
||||
// Once the path is set, the G-code
|
||||
void schedule_export(const std::string &path);
|
||||
// Clear m_export_path.
|
||||
void reset_export();
|
||||
// Once the G-code export is scheduled, the apply() methods will do nothing.
|
||||
bool is_export_scheduled() const { return ! m_export_path.empty(); }
|
||||
|
||||
enum State {
|
||||
// m_thread is not running yet, or it did not reach the STATE_IDLE yet (it does not wait on the condition yet).
|
||||
|
@ -74,8 +86,11 @@ private:
|
|||
Print *m_print = nullptr;
|
||||
// Data structure, to which the G-code export writes its annotations.
|
||||
GCodePreviewData *m_gcode_preview_data = nullptr;
|
||||
// Temporary G-code, there is one defined for the BackgroundSlicingProcess, differentiated from the other processes by a process ID.
|
||||
std::string m_temp_output_path;
|
||||
std::string m_output_path;
|
||||
// Output path provided by the user. The output path may be set even if the slicing is running,
|
||||
// but once set, it cannot be re-set.
|
||||
std::string m_export_path;
|
||||
// Thread, on which the background processing is executed. The thread will always be present
|
||||
// and ready to execute the slicing process.
|
||||
std::thread m_thread;
|
||||
|
@ -84,6 +99,14 @@ private:
|
|||
std::condition_variable m_condition;
|
||||
State m_state = STATE_INITIAL;
|
||||
|
||||
PrintState<BackgroundSlicingProcessStep, bspsCount> m_step_state;
|
||||
mutable tbb::mutex m_step_state_mutex;
|
||||
void set_step_started(BackgroundSlicingProcessStep step);
|
||||
void set_step_done(BackgroundSlicingProcessStep step);
|
||||
bool is_step_done(BackgroundSlicingProcessStep step) const { return m_step_state.is_done(step); }
|
||||
bool invalidate_step(BackgroundSlicingProcessStep step);
|
||||
bool invalidate_all_steps();
|
||||
|
||||
// wxWidgets command ID to be sent to the platter to inform that the slicing is finished, and the G-code export will continue.
|
||||
int m_event_sliced_id = 0;
|
||||
// wxWidgets command ID to be sent to the platter to inform that the task finished.
|
||||
|
|
|
@ -5145,7 +5145,7 @@ void GLCanvas3D::_render_layer_editing_overlay() const
|
|||
#else
|
||||
int object_idx = int(volume->select_group_id / 1000000);
|
||||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
if ((int)m_print->objects().size() < object_idx)
|
||||
if ((int)m_print->objects().size() <= object_idx)
|
||||
return;
|
||||
|
||||
const PrintObject* print_object = m_print->get_object(object_idx);
|
||||
|
|
|
@ -261,18 +261,6 @@ void warning_catcher(wxWindow* parent, const wxString& message){
|
|||
msg.ShowModal();
|
||||
}
|
||||
|
||||
// Assign a Lambda to the print object to emit a wxWidgets Command with the provided ID
|
||||
// to deliver a progress status message.
|
||||
void set_print_callback_event(Print *print, int id)
|
||||
{
|
||||
print->set_status_callback([id](int percent, const std::string &message){
|
||||
wxCommandEvent event(id);
|
||||
event.SetInt(percent);
|
||||
event.SetString(message);
|
||||
wxQueueEvent(wxGetApp().mainframe, event.Clone());
|
||||
});
|
||||
}
|
||||
|
||||
void create_combochecklist(wxComboCtrl* comboCtrl, std::string text, std::string items, bool initial_value)
|
||||
{
|
||||
if (comboCtrl == nullptr)
|
||||
|
|
|
@ -80,10 +80,6 @@ void show_error_id(int id, const std::string& message); // For Perl
|
|||
void show_info(wxWindow* parent, const wxString& message, const wxString& title);
|
||||
void warning_catcher(wxWindow* parent, const wxString& message);
|
||||
|
||||
// Assign a Lambda to the print object to emit a wxWidgets Command with the provided ID
|
||||
// to deliver a progress status message.
|
||||
void set_print_callback_event(Print *print, int id);
|
||||
|
||||
// Creates a wxCheckListBoxComboPopup inside the given wxComboCtrl, filled with the given text and items.
|
||||
// Items are all initialized to the given value.
|
||||
// Items must be separated by '|', for example "Item1|Item2|Item3", and so on.
|
||||
|
|
|
@ -63,10 +63,10 @@ namespace Slic3r {
|
|||
namespace GUI {
|
||||
|
||||
|
||||
wxDEFINE_EVENT(EVT_PROGRESS_BAR, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, wxCommandEvent);
|
||||
|
||||
|
||||
// Sidebar widgets
|
||||
|
||||
// struct InfoBox : public wxStaticBox
|
||||
|
@ -741,9 +741,6 @@ struct Plater::priv
|
|||
std::vector<PlaterObject> objects;
|
||||
#endif // !ENABLE_EXTENDED_SELECTION
|
||||
|
||||
fs::path export_gcode_output_file;
|
||||
fs::path send_gcode_file;
|
||||
|
||||
// GUI elements
|
||||
wxNotebook *notebook;
|
||||
Sidebar *sidebar;
|
||||
|
@ -809,7 +806,7 @@ struct Plater::priv
|
|||
|
||||
void on_notebook_changed(wxBookCtrlEvent&);
|
||||
void on_select_preset(wxCommandEvent&);
|
||||
void on_progress_event();
|
||||
void on_progress_event(wxCommandEvent&);
|
||||
void on_update_print_preview(wxCommandEvent&);
|
||||
void on_process_completed(wxCommandEvent&);
|
||||
void on_layer_editing_toggled(bool enable);
|
||||
|
@ -880,6 +877,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) :
|
|||
background_process.set_gcode_preview_data(&gcode_preview_data);
|
||||
background_process.set_sliced_event(EVT_SLICING_COMPLETED);
|
||||
background_process.set_finished_event(EVT_PROCESS_COMPLETED);
|
||||
// Register progress callback from the Print class to the Platter.
|
||||
print.set_status_callback([this](int percent, const std::string &message){
|
||||
wxCommandEvent event(EVT_PROGRESS_BAR);
|
||||
event.SetInt(percent);
|
||||
event.SetString(message);
|
||||
wxQueueEvent(this->q, event.Clone());
|
||||
});
|
||||
this->q->Bind(EVT_PROGRESS_BAR, &priv::on_progress_event, this);
|
||||
|
||||
_3DScene::add_canvas(canvas3D);
|
||||
_3DScene::allow_multisample(canvas3D, GLCanvas3DManager::can_multisample());
|
||||
|
@ -902,7 +907,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) :
|
|||
_3DScene::enable_shader(canvas3D, true);
|
||||
_3DScene::enable_force_zoom_to_bed(canvas3D, true);
|
||||
|
||||
background_process_timer.Bind(wxEVT_TIMER, [this](wxTimerEvent &evt){ this->async_apply_config(); }, 0);
|
||||
this->background_process_timer.SetOwner(this->q, 0);
|
||||
this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt){ this->async_apply_config(); });
|
||||
|
||||
auto *bed_shape = config->opt<ConfigOptionPoints>("bed_shape");
|
||||
_3DScene::set_bed_shape(canvas3D, bed_shape->values);
|
||||
|
@ -938,7 +944,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) :
|
|||
canvas3D->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { arrange(); });
|
||||
#if !ENABLE_EXTENDED_SELECTION
|
||||
canvas3D->Bind(EVT_GLCANVAS_ROTATE_OBJECT, [this](Event<int> &evt) { /*TODO: call rotate */ });
|
||||
canvas3D->Bind(EVT_GLCANVAS_SCALE_UNIFORMLY, [this](SimpleEvent&) { scale(); });
|
||||
canvas3D->Bind(EVT_GLCANVAS_SCALE_UNIFORMLY, [this](SimpleEvent&) { this->scale(); });
|
||||
#endif // !ENABLE_EXTENDED_SELECTION
|
||||
canvas3D->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [q](Event<int> &evt) { evt.data == 1 ? q->increase_instances() : q->decrease_instances(); });
|
||||
canvas3D->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
|
||||
|
@ -998,7 +1004,7 @@ void Plater::priv::update(bool force_autocenter)
|
|||
}
|
||||
|
||||
// stop_background_process(); // TODO
|
||||
print.reload_model_instances();
|
||||
// print.reload_model_instances();
|
||||
|
||||
#if !ENABLE_EXTENDED_SELECTION
|
||||
const auto selections = collect_selections();
|
||||
|
@ -1008,7 +1014,7 @@ void Plater::priv::update(bool force_autocenter)
|
|||
preview->reset_gcode_preview_data();
|
||||
preview->reload_print();
|
||||
|
||||
schedule_background_process();
|
||||
this->schedule_background_process();
|
||||
}
|
||||
|
||||
void Plater::priv::select_view(const std::string& direction)
|
||||
|
@ -1034,7 +1040,7 @@ void Plater::priv::update_ui_from_settings()
|
|||
// }
|
||||
}
|
||||
|
||||
ProgressStatusBar* Plater::priv::statusbar()
|
||||
ProgressStatusBar* Plater::priv::statusbar()
|
||||
{
|
||||
return main_frame->m_statusbar;
|
||||
}
|
||||
|
@ -1257,7 +1263,7 @@ std::unique_ptr<CheckboxFileDialog> Plater::priv::get_export_file(GUI::FileType
|
|||
case FT_GCODE:
|
||||
wildcard = file_wildcards[file_type];
|
||||
break;
|
||||
|
||||
// FT_GCODE
|
||||
default:
|
||||
wildcard = file_wildcards[FT_MODEL];
|
||||
break;
|
||||
|
@ -1290,7 +1296,6 @@ std::unique_ptr<CheckboxFileDialog> Plater::priv::get_export_file(GUI::FileType
|
|||
|
||||
fs::path path(dlg->GetPath());
|
||||
wxGetApp().app_config->update_last_output_dir(path.parent_path().string());
|
||||
export_gcode_output_file = path;
|
||||
|
||||
return dlg;
|
||||
}
|
||||
|
@ -1446,7 +1451,7 @@ void Plater::priv::object_list_changed()
|
|||
_3DScene::enable_toolbar_item(canvas3D, "arrange", have_objects);
|
||||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
|
||||
const bool export_in_progress = !(export_gcode_output_file.empty() && send_gcode_file.empty());
|
||||
const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty());
|
||||
// XXX: is this right?
|
||||
const bool model_fits = _3DScene::check_volumes_outside_state(canvas3D, config) == ModelInstance::PVS_Inside;
|
||||
|
||||
|
@ -1478,7 +1483,7 @@ void Plater::priv::remove(size_t obj_idx)
|
|||
objects.erase(objects.begin() + obj_idx);
|
||||
#endif // !ENABLE_EXTENDED_SELECTION
|
||||
model.delete_object(obj_idx);
|
||||
print.delete_object(obj_idx);
|
||||
// print.delete_object(obj_idx);
|
||||
// Delete object from Sidebar list
|
||||
sidebar->obj_list()->delete_object_from_list(obj_idx);
|
||||
|
||||
|
@ -1504,7 +1509,7 @@ void Plater::priv::reset()
|
|||
objects.clear();
|
||||
#endif // !ENABLE_EXTENDED_SELECTION
|
||||
model.clear_objects();
|
||||
print.clear_objects();
|
||||
// print.clear_objects();
|
||||
|
||||
// Delete all objects from list on c++ side
|
||||
sidebar->obj_list()->delete_all_objects_from_list();
|
||||
|
@ -1609,7 +1614,6 @@ void Plater::priv::split_object()
|
|||
if (new_objects.size() == 1)
|
||||
{
|
||||
Slic3r::GUI::warning_catcher(q, _(L("The selected object couldn't be split because it contains only one part.")));
|
||||
// $self->schedule_background_process;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1635,9 +1639,16 @@ void Plater::priv::schedule_background_process()
|
|||
|
||||
void Plater::priv::async_apply_config()
|
||||
{
|
||||
DynamicPrintConfig config = wxGetApp().preset_bundle->full_config();
|
||||
BoundingBox bed_box_2D = get_extents(Polygon::new_scale(config.opt<ConfigOptionPoints>("bed_shape")->values));
|
||||
BoundingBoxf3 print_volume(unscale(bed_box_2D.min(0), bed_box_2D.min(1), 0.0), unscale(bed_box_2D.max(0), bed_box_2D.max(1), scale_(config.opt_float("max_print_height"))));
|
||||
// Allow the objects to protrude below the print bed, only the part of the object above the print bed will be sliced.
|
||||
print_volume.min(2) = -1e10;
|
||||
this->q->model().update_print_volume_state(print_volume);
|
||||
|
||||
// Apply new config to the possibly running background task.
|
||||
bool was_running = this->background_process.running();
|
||||
bool invalidated = this->background_process.apply(this->q->model(), wxGetApp().preset_bundle->full_config());
|
||||
bool invalidated = this->background_process.apply(this->q->model(), std::move(config));
|
||||
// Just redraw the 3D canvas without reloading the scene to consume the update of the layer height profile.
|
||||
if (Slic3r::_3DScene::is_layers_editing_enabled(this->canvas3D))
|
||||
this->canvas3D->Refresh();
|
||||
|
@ -1669,19 +1680,19 @@ void Plater::priv::async_apply_config()
|
|||
|
||||
void Plater::priv::start_background_process()
|
||||
{
|
||||
if (this->background_process.running())
|
||||
return;
|
||||
// return if ! @{$self->{objects}} || $self->{background_slicing_process}->running;
|
||||
// Don't start process thread if config is not valid.
|
||||
std::string err = wxGetApp().preset_bundle->full_config().validate();
|
||||
if (err.empty())
|
||||
err = this->q->print().validate();
|
||||
// Don't start process thread if Print is not valid.
|
||||
std::string err = this->q->print().validate();
|
||||
if (! err.empty()) {
|
||||
// $self->statusbar->SetStatusText(err);
|
||||
return;
|
||||
}
|
||||
// Copy the names of active presets into the placeholder parser.
|
||||
wxGetApp().preset_bundle->export_selections(this->q->print().placeholder_parser());
|
||||
// Start the background process.
|
||||
this->background_process.start();
|
||||
this->statusbar()->set_status_text(err);
|
||||
} else {
|
||||
// Copy the names of active presets into the placeholder parser.
|
||||
wxGetApp().preset_bundle->export_selections(this->q->print().placeholder_parser());
|
||||
// Start the background process.
|
||||
this->background_process.start();
|
||||
}
|
||||
}
|
||||
|
||||
void Plater::priv::stop_background_process()
|
||||
|
@ -1795,9 +1806,10 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
|
|||
wxGetApp().plater()->on_config_change(wxGetApp().preset_bundle->full_config());
|
||||
}
|
||||
|
||||
void Plater::priv::on_progress_event()
|
||||
void Plater::priv::on_progress_event(wxCommandEvent &evt)
|
||||
{
|
||||
// TODO
|
||||
this->statusbar()->set_progress(evt.GetInt());
|
||||
this->statusbar()->set_status_text(evt.GetString() + wxString::FromUTF8("…"));
|
||||
}
|
||||
|
||||
void Plater::priv::on_update_print_preview(wxCommandEvent &)
|
||||
|
@ -1813,62 +1825,25 @@ void Plater::priv::on_update_print_preview(wxCommandEvent &)
|
|||
#endif // !ENABLE_EXTENDED_SELECTION
|
||||
}
|
||||
|
||||
void Plater::priv::on_process_completed(wxCommandEvent &)
|
||||
void Plater::priv::on_process_completed(wxCommandEvent &evt)
|
||||
{
|
||||
// Stop the background task, wait until the thread goes into the "Idle" state.
|
||||
// At this point of time the thread should be either finished or canceled,
|
||||
// so the following call just confirms, that the produced data were consumed.
|
||||
this->background_process.stop();
|
||||
//$self->statusbar->ResetCancelCallback();
|
||||
//$self->statusbar->StopBusy;
|
||||
//$self->statusbar->SetStatusText("");
|
||||
|
||||
/*
|
||||
my $message;
|
||||
my $send_gcode = 0;
|
||||
my $do_print = 0;
|
||||
# print "Process completed, message: ", $message, "\n";
|
||||
if (defined($result)) {
|
||||
$message = L("Export failed");
|
||||
} else {
|
||||
# G-code file exported successfully.
|
||||
if ($self->{print_file}) {
|
||||
$message = L("File added to print queue");
|
||||
$do_print = 1;
|
||||
} elsif ($self->{send_gcode_file}) {
|
||||
$message = L("Sending G-code file to the Printer Host ...");
|
||||
$send_gcode = 1;
|
||||
} elsif (defined $self->{export_gcode_output_file}) {
|
||||
$message = L("G-code file exported to ") . $self->{export_gcode_output_file};
|
||||
} else {
|
||||
$message = L("Slicing complete");
|
||||
}
|
||||
this->statusbar()->reset_cancel_callback();
|
||||
this->statusbar()->stop_busy();
|
||||
|
||||
bool success = evt.GetInt();
|
||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
||||
this->background_process.reset_export();
|
||||
if (! success) {
|
||||
wxString message = evt.GetString();
|
||||
if (message.IsEmpty())
|
||||
message = _(L("Export failed"));
|
||||
this->statusbar()->set_status_text(message);
|
||||
}
|
||||
$self->{export_gcode_output_file} = undef;
|
||||
wxTheApp->notify($message);
|
||||
|
||||
$self->do_print if $do_print;
|
||||
|
||||
# Send $self->{send_gcode_file} to OctoPrint.
|
||||
if ($send_gcode) {
|
||||
my $host = Slic3r::PrintHost::get_print_host($self->{config});
|
||||
if ($host->send_gcode($self->{send_gcode_file})) {
|
||||
$message = L("Upload to host finished.");
|
||||
} else {
|
||||
$message = "";
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// As of now, the BackgroundProcessing thread posts status bar update messages to a queue on the MainFrame.pm,
|
||||
// but the "Processing finished" message is posted to this window.
|
||||
// Delay the following status bar update, so it will be called later than what is received by MainFrame.pm.
|
||||
//wxTheApp->CallAfter(sub {
|
||||
// $self->statusbar->SetStatusText($message);
|
||||
// });
|
||||
|
||||
//$self->{print_file} = undef;
|
||||
//$self->{send_gcode_file} = undef;
|
||||
//$self->print_info_box_show(1);
|
||||
|
||||
// this updates buttons status
|
||||
|
@ -2209,9 +2184,9 @@ void Plater::increase_instances(size_t num)
|
|||
Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0);
|
||||
model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation());
|
||||
#if ENABLE_EXTENDED_SELECTION
|
||||
p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec));
|
||||
// p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec));
|
||||
#else
|
||||
p->print.get_object(*obj_idx)->add_copy(Slic3r::to_2d(offset_vec));
|
||||
// p->print.get_object(*obj_idx)->add_copy(Slic3r::to_2d(offset_vec));
|
||||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
}
|
||||
|
||||
|
@ -2254,9 +2229,9 @@ void Plater::decrease_instances(size_t num)
|
|||
for (size_t i = 0; i < num; i++) {
|
||||
model_object->delete_last_instance();
|
||||
#if ENABLE_EXTENDED_SELECTION
|
||||
p->print.get_object(obj_idx)->delete_last_copy();
|
||||
// p->print.get_object(obj_idx)->delete_last_copy();
|
||||
#else
|
||||
p->print.get_object(*obj_idx)->delete_last_copy();
|
||||
// p->print.get_object(*obj_idx)->delete_last_copy();
|
||||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
}
|
||||
#if ENABLE_EXTENDED_SELECTION
|
||||
|
@ -2281,8 +2256,7 @@ void Plater::decrease_instances(size_t num)
|
|||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
|
||||
p->selection_changed();
|
||||
|
||||
// $self->schedule_background_process;
|
||||
this->p->schedule_background_process();
|
||||
}
|
||||
|
||||
void Plater::set_number_of_copies(size_t num)
|
||||
|
@ -2307,38 +2281,35 @@ void Plater::set_number_of_copies(size_t num)
|
|||
decrease_instances(-diff);
|
||||
}
|
||||
|
||||
fs::path Plater::export_gcode(const fs::path &output_path)
|
||||
void Plater::export_gcode(fs::path output_path)
|
||||
{
|
||||
#if ENABLE_EXTENDED_SELECTION
|
||||
if (p->model.objects.empty()) { return ""; }
|
||||
if (p->model.objects.empty())
|
||||
return;
|
||||
#else
|
||||
if (p->objects.empty()) { return ""; }
|
||||
if (p->objects.empty())
|
||||
return;
|
||||
#endif // ENABLE_EXTENDED_SELECTION
|
||||
|
||||
if (! p->export_gcode_output_file.empty()) {
|
||||
if (this->p->background_process.is_export_scheduled()) {
|
||||
GUI::show_error(this, _(L("Another export job is currently running.")));
|
||||
return "";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string err = wxGetApp().preset_bundle->full_config().validate();
|
||||
if (err.empty()) {
|
||||
if (err.empty())
|
||||
err = p->print.validate();
|
||||
}
|
||||
if (! err.empty()) {
|
||||
// The config is not valid
|
||||
GUI::show_error(this, _(err));
|
||||
return fs::path();
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the names of active presets into the placeholder parser.
|
||||
wxGetApp().preset_bundle->export_selections(p->print.placeholder_parser());
|
||||
|
||||
// select output file
|
||||
if (! output_path.empty()) {
|
||||
p->export_gcode_output_file = fs::path(p->print.output_filepath(output_path.string()));
|
||||
// FIXME: ^ errors to handle?
|
||||
} else {
|
||||
|
||||
if (output_path.empty()) {
|
||||
// XXX: take output path from CLI opts? Ancient Slic3r versions used to do that...
|
||||
|
||||
// If possible, remove accents from accented latin characters.
|
||||
|
@ -2355,13 +2326,17 @@ fs::path Plater::export_gcode(const fs::path &output_path)
|
|||
wxFD_SAVE | wxFD_OVERWRITE_PROMPT
|
||||
);
|
||||
|
||||
if (dlg.ShowModal() != wxID_OK) { return ""; }
|
||||
fs::path path(dlg.GetPath());
|
||||
wxGetApp().app_config->update_last_output_dir(path.parent_path().string());
|
||||
p->export_gcode_output_file = path;
|
||||
if (dlg.ShowModal() == wxID_OK) {
|
||||
fs::path path(dlg.GetPath());
|
||||
wxGetApp().app_config->update_last_output_dir(path.parent_path().string());
|
||||
output_path = path;
|
||||
}
|
||||
}
|
||||
|
||||
return p->export_gcode_output_file;
|
||||
if (! output_path.empty()) {
|
||||
this->p->background_process.schedule_export(p->print.output_filepath(output_path.string()));
|
||||
this->p->background_process.start();
|
||||
}
|
||||
}
|
||||
|
||||
void Plater::export_stl()
|
||||
|
@ -2452,7 +2427,7 @@ void Plater::reslice()
|
|||
|
||||
void Plater::send_gcode()
|
||||
{
|
||||
p->send_gcode_file = export_gcode();
|
||||
// p->send_gcode_file = export_gcode();
|
||||
}
|
||||
|
||||
void Plater::on_extruders_change(int num_extruders)
|
||||
|
|
|
@ -118,7 +118,7 @@ public:
|
|||
void set_number_of_copies(size_t num);
|
||||
|
||||
// Note: empty path means "use the default"
|
||||
boost::filesystem::path export_gcode(const boost::filesystem::path &output_path = boost::filesystem::path());
|
||||
void export_gcode(boost::filesystem::path output_path = boost::filesystem::path());
|
||||
void export_stl();
|
||||
void export_amf();
|
||||
void export_3mf();
|
||||
|
|
|
@ -884,7 +884,8 @@ bool PresetCollection::update_dirty_ui(wxBitmapComboBox *ui)
|
|||
std::string old_label = ui->GetString(ui_id).utf8_str().data();
|
||||
std::string preset_name = Preset::remove_suffix_modified(old_label);
|
||||
const Preset *preset = this->find_preset(preset_name, false);
|
||||
assert(preset != nullptr);
|
||||
// The old_label could be the "----- system presets ------" or the "------- user presets --------" separator.
|
||||
// assert(preset != nullptr);
|
||||
if (preset != nullptr) {
|
||||
std::string new_label = preset->is_dirty ? preset->name + g_suffix_modified : preset->name;
|
||||
if (old_label != new_label)
|
||||
|
|
|
@ -53,8 +53,8 @@ ProgressStatusBar::ProgressStatusBar(wxWindow *parent, int id):
|
|||
});
|
||||
|
||||
m_cancelbutton->Bind(wxEVT_BUTTON, [this](const wxCommandEvent&) {
|
||||
if(m_cancel_cb) m_cancel_cb();
|
||||
m_perl_cancel_callback.call();
|
||||
if (m_cancel_cb)
|
||||
m_cancel_cb();
|
||||
m_cancelbutton->Hide();
|
||||
});
|
||||
}
|
||||
|
@ -136,7 +136,17 @@ void ProgressStatusBar::embed(wxFrame *frame)
|
|||
|
||||
void ProgressStatusBar::set_status_text(const wxString& txt)
|
||||
{
|
||||
self->SetStatusText(wxString::FromUTF8(txt.c_str()));
|
||||
self->SetStatusText(txt);
|
||||
}
|
||||
|
||||
void ProgressStatusBar::set_status_text(const std::string& txt)
|
||||
{
|
||||
this->set_status_text(txt.c_str());
|
||||
}
|
||||
|
||||
void ProgressStatusBar::set_status_text(const char *txt)
|
||||
{
|
||||
this->set_status_text(wxString::FromUTF8(txt));
|
||||
}
|
||||
|
||||
void ProgressStatusBar::show_cancel_button()
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
#include "callback.hpp"
|
||||
|
||||
class wxTimer;
|
||||
class wxGauge;
|
||||
class wxButton;
|
||||
|
@ -22,7 +20,8 @@ namespace Slic3r {
|
|||
* of the Slicer main window. It consists of a message area to the left and a
|
||||
* progress indication area to the right with an optional cancel button.
|
||||
*/
|
||||
class ProgressStatusBar {
|
||||
class ProgressStatusBar
|
||||
{
|
||||
wxStatusBar *self; // we cheat! It should be the base class but: perl!
|
||||
wxTimer *m_timer;
|
||||
wxGauge *m_prog;
|
||||
|
@ -35,25 +34,26 @@ public:
|
|||
ProgressStatusBar(wxWindow *parent = nullptr, int id = -1);
|
||||
~ProgressStatusBar();
|
||||
|
||||
int get_progress() const;
|
||||
void set_progress(int);
|
||||
int get_range() const;
|
||||
void set_range(int = 100);
|
||||
void show_progress(bool);
|
||||
void start_busy(int = 100);
|
||||
void stop_busy();
|
||||
int get_progress() const;
|
||||
void set_progress(int);
|
||||
int get_range() const;
|
||||
void set_range(int = 100);
|
||||
void show_progress(bool);
|
||||
void start_busy(int = 100);
|
||||
void stop_busy();
|
||||
inline bool is_busy() const { return m_busy; }
|
||||
void set_cancel_callback(CancelFn = CancelFn());
|
||||
inline void remove_cancel_callback() { set_cancel_callback(); }
|
||||
void run(int rate);
|
||||
void embed(wxFrame *frame = nullptr);
|
||||
void set_status_text(const wxString& txt);
|
||||
void set_cancel_callback(CancelFn = CancelFn());
|
||||
inline void reset_cancel_callback() { set_cancel_callback(); }
|
||||
void run(int rate);
|
||||
void embed(wxFrame *frame = nullptr);
|
||||
void set_status_text(const wxString& txt);
|
||||
void set_status_text(const std::string& txt);
|
||||
void set_status_text(const char *txt);
|
||||
|
||||
// Temporary methods to satisfy Perl side
|
||||
void show_cancel_button();
|
||||
void hide_cancel_button();
|
||||
void show_cancel_button();
|
||||
void hide_cancel_button();
|
||||
|
||||
PerlCallback m_perl_cancel_callback;
|
||||
private:
|
||||
bool m_busy = false;
|
||||
CancelFn m_cancel_cb;
|
||||
|
|
|
@ -185,22 +185,6 @@ _constant()
|
|||
}
|
||||
%};
|
||||
|
||||
void export_gcode_with_preview_data(char *path_template, GCodePreviewData *preview_data) %code%{
|
||||
try {
|
||||
THIS->export_gcode(path_template, preview_data);
|
||||
} catch (std::exception& e) {
|
||||
croak(e.what());
|
||||
}
|
||||
%};
|
||||
|
||||
void export_gcode(char *path_template) %code%{
|
||||
try {
|
||||
THIS->export_gcode(path_template, nullptr);
|
||||
} catch (std::exception& e) {
|
||||
croak(e.what());
|
||||
}
|
||||
%};
|
||||
|
||||
void export_png(char *path) %code%{
|
||||
try {
|
||||
THIS->export_png(path);
|
||||
|
|
Loading…
Reference in a new issue