Drain holes are now saved in ModelObject

Internal changes in GLGizmoHollow.cpp
This commit is contained in:
Lukas Matena 2019-11-08 14:05:56 +01:00
parent 9836533cb3
commit 645f13a0ae
6 changed files with 289 additions and 349 deletions

View file

@ -606,6 +606,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
assert(this->config.id() == rhs.config.id()); assert(this->config.id() == rhs.config.id());
this->sla_support_points = rhs.sla_support_points; this->sla_support_points = rhs.sla_support_points;
this->sla_points_status = rhs.sla_points_status; this->sla_points_status = rhs.sla_points_status;
this->sla_drain_holes = rhs.sla_drain_holes;
this->layer_config_ranges = rhs.layer_config_ranges; // #ys_FIXME_experiment this->layer_config_ranges = rhs.layer_config_ranges; // #ys_FIXME_experiment
this->layer_height_profile = rhs.layer_height_profile; this->layer_height_profile = rhs.layer_height_profile;
this->printable = rhs.printable; this->printable = rhs.printable;
@ -646,6 +647,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
assert(this->config.id() == rhs.config.id()); assert(this->config.id() == rhs.config.id());
this->sla_support_points = std::move(rhs.sla_support_points); this->sla_support_points = std::move(rhs.sla_support_points);
this->sla_points_status = std::move(rhs.sla_points_status); this->sla_points_status = std::move(rhs.sla_points_status);
this->sla_drain_holes = std::move(rhs.sla_drain_holes);
this->layer_config_ranges = std::move(rhs.layer_config_ranges); // #ys_FIXME_experiment this->layer_config_ranges = std::move(rhs.layer_config_ranges); // #ys_FIXME_experiment
this->layer_height_profile = std::move(rhs.layer_height_profile); this->layer_height_profile = std::move(rhs.layer_height_profile);
this->origin_translation = std::move(rhs.origin_translation); this->origin_translation = std::move(rhs.origin_translation);
@ -1099,6 +1101,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b
if (keep_upper) { if (keep_upper) {
upper->set_model(nullptr); upper->set_model(nullptr);
upper->sla_support_points.clear(); upper->sla_support_points.clear();
lower->sla_drain_holes.clear();
upper->sla_points_status = sla::PointsStatus::NoPoints; upper->sla_points_status = sla::PointsStatus::NoPoints;
upper->clear_volumes(); upper->clear_volumes();
upper->input_file = ""; upper->input_file = "";
@ -1107,6 +1110,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b
if (keep_lower) { if (keep_lower) {
lower->set_model(nullptr); lower->set_model(nullptr);
lower->sla_support_points.clear(); lower->sla_support_points.clear();
lower->sla_drain_holes.clear();
lower->sla_points_status = sla::PointsStatus::NoPoints; lower->sla_points_status = sla::PointsStatus::NoPoints;
lower->clear_volumes(); lower->clear_volumes();
lower->input_file = ""; lower->input_file = "";

View file

@ -203,6 +203,9 @@ public:
// the SLA gizmo and the backend). // the SLA gizmo and the backend).
sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints;
// Holes to be drilled into the object so resin can flow out
std::vector<sla::DrainHole> sla_drain_holes;
/* This vector accumulates the total translation applied to the object by the /* This vector accumulates the total translation applied to the object by the
center_around_origin() method. Callers might want to apply the same translation center_around_origin() method. Callers might want to apply the same translation
to new volumes before adding them to this object in order to preserve alignment to new volumes before adding them to this object in order to preserve alignment
@ -372,7 +375,7 @@ private:
template<class Archive> void serialize(Archive &ar) { template<class Archive> void serialize(Archive &ar) {
ar(cereal::base_class<ObjectBase>(this)); ar(cereal::base_class<ObjectBase>(this));
Internal::StaticSerializationWrapper<ModelConfig> config_wrapper(config); Internal::StaticSerializationWrapper<ModelConfig> config_wrapper(config);
ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_height_profile, sla_support_points, sla_points_status, printable, origin_translation, ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_height_profile, sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation,
m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid); m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid);
} }
}; };

View file

@ -80,6 +80,39 @@ struct SupportPoint
using SupportPoints = std::vector<SupportPoint>; using SupportPoints = std::vector<SupportPoint>;
struct DrainHole
{
Vec3f m_pos;
Vec3f m_normal;
float m_radius;
float m_height;
DrainHole()
: m_pos(Vec3f::Zero()), m_normal(Vec3f::UnitZ()), m_radius(5.f),
m_height(10.f)
{}
DrainHole(Vec3f position, Vec3f normal, float radius, float height)
: m_pos(position)
, m_normal(normal)
, m_radius(radius)
, m_height(height)
{}
bool operator==(const DrainHole &sp) const
{
return (m_pos == sp.m_pos) && (m_normal == sp.m_normal)
&& is_approx(m_radius, sp.m_radius) && is_approx(m_height, sp.m_height);
}
bool operator!=(const DrainHole &sp) const { return !(sp == (*this)); }
template<class Archive> void serialize(Archive &ar)
{
ar(m_pos, m_normal, m_radius, m_height);
}
};
struct Contour3D; struct Contour3D;
/// An index-triangle structure for libIGL functions. Also serves as an /// An index-triangle structure for libIGL functions. Also serves as an

View file

@ -52,6 +52,7 @@ bool GLGizmoHollow::on_init()
m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": "; m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": ";
m_desc["reset_direction"] = _(L("Reset direction")); m_desc["reset_direction"] = _(L("Reset direction"));
m_desc["hollow"] = _(L("Hollow")); m_desc["hollow"] = _(L("Hollow"));
m_desc["show_supports"] = _(L("Show supports"));
return true; return true;
} }
@ -243,13 +244,13 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) cons
glsafe(::glMultMatrixd(instance_matrix.data())); glsafe(::glMultMatrixd(instance_matrix.data()));
float render_color[4]; float render_color[4];
size_t cache_size = m_editing_mode ? m_editing_cache.size() : m_normal_cache.size(); size_t cache_size = m_model_object->sla_drain_holes.size();
for (size_t i = 0; i < cache_size; ++i) for (size_t i = 0; i < cache_size; ++i)
{ {
const sla::SupportPoint& support_point = m_editing_mode ? m_editing_cache[i].support_point : m_normal_cache[i]; const sla::DrainHole& drain_hole = m_model_object->sla_drain_holes[i];
const bool& point_selected = m_editing_mode ? m_editing_cache[i].selected : false; const bool& point_selected = m_selected[i];
if (is_mesh_point_clipped(support_point.pos.cast<double>())) if (is_mesh_point_clipped(drain_hole.m_pos.cast<double>()))
continue; continue;
// First decide about the color of the point. // First decide about the color of the point.
@ -262,7 +263,7 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) cons
} }
else { else {
render_color[3] = 1.f; render_color[3] = 1.f;
if ((size_t(m_hover_id) == i && m_editing_mode)) { // ignore hover state unless editing mode is active if (size_t(m_hover_id) == i) {
render_color[0] = 0.f; render_color[0] = 0.f;
render_color[1] = 1.0f; render_color[1] = 1.0f;
render_color[2] = 1.0f; render_color[2] = 1.0f;
@ -280,43 +281,34 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) cons
// Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object.
glsafe(::glPushMatrix()); glsafe(::glPushMatrix());
glsafe(::glTranslatef(support_point.pos(0), support_point.pos(1), support_point.pos(2))); glsafe(::glTranslatef(drain_hole.m_pos(0), drain_hole.m_pos(1), drain_hole.m_pos(2)));
glsafe(::glMultMatrixd(instance_scaling_matrix_inverse.data())); glsafe(::glMultMatrixd(instance_scaling_matrix_inverse.data()));
if (vol->is_left_handed()) if (vol->is_left_handed())
glFrontFace(GL_CW); glFrontFace(GL_CW);
// Matrices set, we can render the point mark now. // Matrices set, we can render the point mark now.
// If in editing mode, we'll also render a cone pointing to the sphere.
if (m_editing_mode) {
// in case the normal is not yet cached, find and cache it
if (m_editing_cache[i].normal == Vec3f::Zero())
m_mesh_raycaster->get_closest_point(m_editing_cache[i].support_point.pos, &m_editing_cache[i].normal);
Eigen::Quaterniond q; Eigen::Quaterniond q;
q.setFromTwoVectors(Vec3d{0., 0., 1.}, instance_scaling_matrix_inverse * m_editing_cache[i].normal.cast<double>()); q.setFromTwoVectors(Vec3d{0., 0., 1.}, instance_scaling_matrix_inverse * drain_hole.m_normal.cast<double>());
Eigen::AngleAxisd aa(q); Eigen::AngleAxisd aa(q);
glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2))); glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2)));
const double cylinder_radius = double(drain_hole.m_radius) * RenderPointScale; //0.25; // mm
const double stick_out_length = 1.;
const double cone_height = m_new_hole_height + stick_out_length;
glsafe(::glPushMatrix());
glsafe(::glTranslated(0., 0., -cone_height+stick_out_length));
::gluCylinder(m_quadric, cylinder_radius, cylinder_radius, cone_height, 24, 1);
glsafe(::glTranslated(0., 0., cone_height));
::gluDisk(m_quadric, 0.0, cylinder_radius, 24, 1);
glsafe(::glTranslated(0., 0., -cone_height));
glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f));
::gluDisk(m_quadric, 0.0, cylinder_radius, 24, 1);
glsafe(::glPopMatrix());
const double cone_radius = double(support_point.head_front_radius) * RenderPointScale; //0.25; // mm
const double cone_height = m_new_cone_height;
//const double cone_rad_diff = m_new_cone_angle*(cone_radius/(cone_height/2.))*(cone_height/2.);
const double cone_rad_diff = m_new_cone_angle*cone_radius;
glsafe(::glPushMatrix());
glsafe(::glTranslated(0., 0., -cone_height/2.));
//::gluCylinder(m_quadric, cone_radius, cone_radius, cone_height, 24, 1);
::gluCylinder(m_quadric, cone_radius+cone_rad_diff, cone_radius-cone_rad_diff, cone_height, 24, 1);
glsafe(::glTranslated(0., 0., cone_height));
::gluDisk(m_quadric, 0.0, cone_radius-cone_rad_diff, 24, 1);
glsafe(::glTranslated(0., 0., -cone_height));
glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f));
::gluDisk(m_quadric, 0.0, cone_radius+cone_rad_diff, 24, 1);
glsafe(::glPopMatrix());
}
//::gluSphere(m_quadric, (double)support_point.head_front_radius * RenderPointScale, 24, 12);
if (vol->is_left_handed()) if (vol->is_left_handed())
glFrontFace(GL_CCW); glFrontFace(GL_CCW);
glsafe(::glPopMatrix()); glsafe(::glPopMatrix());
} }
@ -412,122 +404,121 @@ bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, V
// concludes that the event was not intended for it, it should return false. // concludes that the event was not intended for it, it should return false.
bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down)
{ {
if (m_editing_mode) { // left down with shift - show the selection rectangle:
if (action == SLAGizmoEventType::LeftDown && (shift_down || alt_down || control_down)) {
// left down with shift - show the selection rectangle: if (m_hover_id == -1) {
if (action == SLAGizmoEventType::LeftDown && (shift_down || alt_down || control_down)) { if (shift_down || alt_down) {
if (m_hover_id == -1) { m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect);
if (shift_down || alt_down) {
m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect);
}
} }
}
else {
if (m_selected[m_hover_id])
unselect_point(m_hover_id);
else { else {
if (m_editing_cache[m_hover_id].selected) if (!alt_down)
unselect_point(m_hover_id); select_point(m_hover_id);
else {
if (!alt_down)
select_point(m_hover_id);
}
} }
return true;
} }
// left down without selection rectangle - place point on the mesh: return true;
if (action == SLAGizmoEventType::LeftDown && !m_selection_rectangle.is_dragging() && !shift_down) { }
// If any point is in hover state, this should initiate its move - return control back to GLCanvas:
if (m_hover_id != -1)
return false;
// If there is some selection, don't add new point and deselect everything instead. // left down without selection rectangle - place point on the mesh:
if (m_selection_empty) { if (action == SLAGizmoEventType::LeftDown && !m_selection_rectangle.is_dragging() && !shift_down) {
std::pair<Vec3f, Vec3f> pos_and_normal; // If any point is in hover state, this should initiate its move - return control back to GLCanvas:
if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection if (m_hover_id != -1)
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add drainage hole"))); return false;
m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second);
m_parent.set_as_dirty(); // If there is some selection, don't add new point and deselect everything instead.
m_wait_for_up_event = true; if (m_selection_empty) {
} std::pair<Vec3f, Vec3f> pos_and_normal;
else if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection
return false; Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add drainage hole")));
m_model_object->sla_drain_holes.emplace_back(pos_and_normal.first, pos_and_normal.second, m_new_hole_radius, m_new_hole_height);
m_selected.push_back(false);
assert(m_selected.size == m_model_object->sla_drain_holes.size());
m_parent.set_as_dirty();
m_wait_for_up_event = true;
} }
else else
select_point(NoPoints); return false;
}
else
select_point(NoPoints);
return true;
}
// left up with selection rectangle - select points inside the rectangle:
if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) {
// Is this a selection or deselection rectangle?
GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state();
// First collect positions of all the points in world coordinates.
Geometry::Transformation trafo = m_model_object->instances[m_active_instance]->get_transformation();
trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift));
std::vector<Vec3d> points;
for (unsigned int i=0; i<m_model_object->sla_drain_holes.size(); ++i)
points.push_back(trafo.get_matrix() * m_model_object->sla_drain_holes[i].m_pos.cast<double>());
// Now ask the rectangle which of the points are inside.
std::vector<Vec3f> points_inside;
std::vector<unsigned int> points_idxs = m_selection_rectangle.stop_dragging(m_parent, points);
for (size_t idx : points_idxs)
points_inside.push_back(points[idx].cast<float>());
// Only select/deselect points that are actually visible
for (size_t idx : m_mesh_raycaster->get_unobscured_idxs(trafo, m_parent.get_camera(), points_inside, m_clipping_plane.get()))
{
if (rectangle_status == GLSelectionRectangle::Deselect)
unselect_point(points_idxs[idx]);
else
select_point(points_idxs[idx]);
}
return true;
}
// left up with no selection rectangle
if (action == SLAGizmoEventType::LeftUp) {
if (m_wait_for_up_event) {
m_wait_for_up_event = false;
return true;
}
}
// dragging the selection rectangle:
if (action == SLAGizmoEventType::Dragging) {
if (m_wait_for_up_event)
return true; // point has been placed and the button not released yet
// this prevents GLCanvas from starting scene rotation
if (m_selection_rectangle.is_dragging()) {
m_selection_rectangle.dragging(mouse_position);
return true; return true;
} }
// left up with selection rectangle - select points inside the rectangle: return false;
if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) { }
// Is this a selection or deselection rectangle?
GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state();
// First collect positions of all the points in world coordinates. if (action == SLAGizmoEventType::Delete) {
Geometry::Transformation trafo = m_model_object->instances[m_active_instance]->get_transformation(); // delete key pressed
trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift)); delete_selected_points();
std::vector<Vec3d> points; return true;
for (unsigned int i=0; i<m_editing_cache.size(); ++i) }
points.push_back(trafo.get_matrix() * m_editing_cache[i].support_point.pos.cast<double>());
// Now ask the rectangle which of the points are inside. if (action == SLAGizmoEventType::RightDown) {
std::vector<Vec3f> points_inside; if (m_hover_id != -1) {
std::vector<unsigned int> points_idxs = m_selection_rectangle.stop_dragging(m_parent, points); select_point(NoPoints);
for (size_t idx : points_idxs) select_point(m_hover_id);
points_inside.push_back(points[idx].cast<float>());
// Only select/deselect points that are actually visible
for (size_t idx : m_mesh_raycaster->get_unobscured_idxs(trafo, m_parent.get_camera(), points_inside, m_clipping_plane.get()))
{
if (rectangle_status == GLSelectionRectangle::Deselect)
unselect_point(points_idxs[idx]);
else
select_point(points_idxs[idx]);
}
return true;
}
// left up with no selection rectangle
if (action == SLAGizmoEventType::LeftUp) {
if (m_wait_for_up_event) {
m_wait_for_up_event = false;
return true;
}
}
// dragging the selection rectangle:
if (action == SLAGizmoEventType::Dragging) {
if (m_wait_for_up_event)
return true; // point has been placed and the button not released yet
// this prevents GLCanvas from starting scene rotation
if (m_selection_rectangle.is_dragging()) {
m_selection_rectangle.dragging(mouse_position);
return true;
}
return false;
}
if (action == SLAGizmoEventType::Delete) {
// delete key pressed
delete_selected_points(); delete_selected_points();
return true; return true;
} }
return false;
}
if (action == SLAGizmoEventType::RightDown) { if (action == SLAGizmoEventType::SelectAll) {
if (m_hover_id != -1) { select_point(AllPoints);
select_point(NoPoints); return true;
select_point(m_hover_id);
delete_selected_points();
return true;
}
return false;
}
if (action == SLAGizmoEventType::SelectAll) {
select_point(AllPoints);
return true;
}
} }
if (action == SLAGizmoEventType::MouseWheelUp && control_down) { if (action == SLAGizmoEventType::MouseWheelUp && control_down) {
@ -552,39 +543,26 @@ bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_pos
void GLGizmoHollow::delete_selected_points(bool force) void GLGizmoHollow::delete_selected_points(bool force)
{ {
if (! m_editing_mode) {
std::cout << "DEBUGGING: delete_selected_points called out of editing mode!" << std::endl;
std::abort();
}
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete drainage hole"))); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete drainage hole")));
for (unsigned int idx=0; idx<m_editing_cache.size(); ++idx) { for (unsigned int idx=0; idx<m_model_object->sla_drain_holes.size(); ++idx) {
if (m_editing_cache[idx].selected) { if (m_selected[idx]) {
m_editing_cache.erase(m_editing_cache.begin() + (idx--)); m_selected.erase(m_selected.begin()+idx);
m_model_object->sla_drain_holes.erase(m_model_object->sla_drain_holes.begin() + (idx--));
} }
} }
select_point(NoPoints); select_point(NoPoints);
//m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
} }
void GLGizmoHollow::on_update(const UpdateData& data) void GLGizmoHollow::on_update(const UpdateData& data)
{ {
if (! m_editing_mode) if (m_hover_id != -1) {
return; std::pair<Vec3f, Vec3f> pos_and_normal;
else { if (! unproject_on_mesh(data.mouse_pos.cast<double>(), pos_and_normal))
if (m_hover_id != -1) { return;
std::pair<Vec3f, Vec3f> pos_and_normal; m_model_object->sla_drain_holes[m_hover_id].m_pos = pos_and_normal.first;
if (! unproject_on_mesh(data.mouse_pos.cast<double>(), pos_and_normal)) m_model_object->sla_drain_holes[m_hover_id].m_normal = pos_and_normal.second;
return;
m_editing_cache[m_hover_id].support_point.pos = pos_and_normal.first;
m_editing_cache[m_hover_id].support_point.is_new_island = false;
m_editing_cache[m_hover_id].normal = pos_and_normal.second;
// Do not update immediately, wait until the mouse is released.
// m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
}
} }
} }
@ -661,17 +639,12 @@ ClippingPlane GLGizmoHollow::get_sla_clipping_plane() const
void GLGizmoHollow::on_render_input_window(float x, float y, float bottom_limit) void GLGizmoHollow::on_render_input_window(float x, float y, float bottom_limit)
{ {
if (!m_model_object) if (! m_model_object)
return; return;
bool first_run = true; // This is a hack to redraw the button when all points are removed, bool first_run = true; // This is a hack to redraw the button when all points are removed,
// so it is not delayed until the background process finishes. // so it is not delayed until the background process finishes.
RENDER_AGAIN: RENDER_AGAIN:
//m_imgui->set_next_window_pos(x, y, ImGuiCond_Always);
//const ImVec2 window_size(m_imgui->scaled(18.f, 16.f));
//ImGui::SetNextWindowPos(ImVec2(x, y - std::max(0.f, y+window_size.y-bottom_limit) ));
//ImGui::SetNextWindowSize(ImVec2(window_size));
const float approx_height = m_imgui->scaled(18.0f); const float approx_height = m_imgui->scaled(18.0f);
y = std::min(y, bottom_limit - approx_height); y = std::min(y, bottom_limit - approx_height);
m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always);
@ -679,7 +652,6 @@ RENDER_AGAIN:
m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse);
// First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that:
const float settings_sliders_left = std::max(m_imgui->calc_text_size(m_desc.at("minimal_distance")).x, m_imgui->calc_text_size(m_desc.at("points_density")).x) + m_imgui->scaled(1.f); const float settings_sliders_left = std::max(m_imgui->calc_text_size(m_desc.at("minimal_distance")).x, m_imgui->calc_text_size(m_desc.at("points_density")).x) + m_imgui->scaled(1.f);
const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f);
const float diameter_slider_left = m_imgui->calc_text_size(m_desc.at("head_diameter")).x + m_imgui->scaled(1.f); const float diameter_slider_left = m_imgui->calc_text_size(m_desc.at("head_diameter")).x + m_imgui->scaled(1.f);
@ -690,125 +662,72 @@ RENDER_AGAIN:
float window_width = minimal_slider_width + std::max(std::max(settings_sliders_left, clipping_slider_left), diameter_slider_left); float window_width = minimal_slider_width + std::max(std::max(settings_sliders_left, clipping_slider_left), diameter_slider_left);
window_width = std::max(std::max(window_width, buttons_width_approx), lock_supports_width_approx); window_width = std::max(std::max(window_width, buttons_width_approx), lock_supports_width_approx);
bool force_refresh = false; bool force_refresh = false;
bool remove_selected = false; bool remove_selected = false;
bool remove_all = false; bool remove_all = false;
if (m_editing_mode) { if (m_imgui->button(m_desc.at("hollow")))
hollow_mesh();
if (m_imgui->button(m_desc.at("hollow"))) float diameter_upper_cap = 20.f; //static_cast<ConfigOptionFloat*>(wxGetApp().preset_bundle->sla_prints.get_edited_preset().config.option("support_pillar_diameter"))->value;
hollow_mesh(); if (m_new_hole_radius > diameter_upper_cap)
m_new_hole_radius = diameter_upper_cap;
m_imgui->text(m_desc.at("head_diameter"));
ImGui::SameLine(diameter_slider_left);
ImGui::PushItemWidth(window_width - diameter_slider_left);
float diameter_upper_cap = static_cast<ConfigOptionFloat*>(wxGetApp().preset_bundle->sla_prints.get_edited_preset().config.option("support_pillar_diameter"))->value; // Following is a nasty way to:
if (m_new_point_head_diameter > diameter_upper_cap) // - save the initial value of the slider before one starts messing with it
m_new_point_head_diameter = diameter_upper_cap; // - keep updating the head radius during sliding so it is continuosly refreshed in 3D scene
m_imgui->text(m_desc.at("head_diameter")); // - take correct undo/redo snapshot after the user is done with moving the slider
ImGui::SameLine(diameter_slider_left); float initial_value = m_new_hole_radius;
ImGui::PushItemWidth(window_width - diameter_slider_left); ImGui::SliderFloat("", &m_new_hole_radius, 0.1f, diameter_upper_cap, "%.1f");
if (ImGui::IsItemClicked()) {
// Following is a nasty way to: if (m_old_hole_radius == 0.f)
// - save the initial value of the slider before one starts messing with it m_old_hole_radius = initial_value;
// - keep updating the head radius during sliding so it is continuosly refreshed in 3D scene
// - take correct undo/redo snapshot after the user is done with moving the slider
float initial_value = m_new_point_head_diameter;
ImGui::SliderFloat("", &m_new_point_head_diameter, 0.1f, diameter_upper_cap, "%.1f");
if (ImGui::IsItemClicked()) {
if (m_old_point_head_diameter == 0.f)
m_old_point_head_diameter = initial_value;
}
if (ImGui::IsItemEdited()) {
for (auto& cache_entry : m_editing_cache)
if (cache_entry.selected)
cache_entry.support_point.head_front_radius = m_new_point_head_diameter / 2.f;
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
// momentarily restore the old value to take snapshot
for (auto& cache_entry : m_editing_cache)
if (cache_entry.selected)
cache_entry.support_point.head_front_radius = m_old_point_head_diameter / 2.f;
float backup = m_new_point_head_diameter;
m_new_point_head_diameter = m_old_point_head_diameter;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Change drainage hole diameter")));
m_new_point_head_diameter = backup;
for (auto& cache_entry : m_editing_cache)
if (cache_entry.selected)
cache_entry.support_point.head_front_radius = m_new_point_head_diameter / 2.f;
m_old_point_head_diameter = 0.f;
}
// !!!! Something as above should be done for the cone angle
m_imgui->text("Hole taper: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_new_cone_angle, -1.f, 1.f, "%.1f");
m_imgui->text("Hole height: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_new_cone_height, 0.1f, 10.f, "%.1f");
m_imgui->disabled_begin(m_selection_empty);
remove_selected = m_imgui->button(m_desc.at("remove_selected"));
m_imgui->disabled_end();
m_imgui->disabled_begin(m_editing_cache.empty());
remove_all = m_imgui->button(m_desc.at("remove_all"));
m_imgui->disabled_end();
m_imgui->text(" "); // vertical gap
m_imgui->text("Offset: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_offset, 0.f, 5.f, "%.1f");
m_imgui->text("Adaptibility: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_adaptability, 0.f, 1.f, "%.1f");
} }
else { // not in editing mode: if (ImGui::IsItemEdited()) {
m_imgui->text(m_desc.at("minimal_distance")); for (size_t idx=0; idx<m_selected.size(); ++idx)
ImGui::SameLine(settings_sliders_left); if (m_selected[idx])
ImGui::PushItemWidth(window_width - settings_sliders_left); m_model_object->sla_drain_holes[idx].m_radius = m_new_hole_radius;
}
std::vector<const ConfigOption*> opts = get_config_options({"support_points_density_relative", "support_points_minimal_distance"}); if (ImGui::IsItemDeactivatedAfterEdit()) {
float density = static_cast<const ConfigOptionInt*>(opts[0])->value; // momentarily restore the old value to take snapshot
float minimal_point_distance = static_cast<const ConfigOptionFloat*>(opts[1])->value; for (size_t idx=0; idx<m_selected.size(); ++idx)
if (m_selected[idx])
ImGui::SliderFloat("", &minimal_point_distance, 0.f, 20.f, "%.f mm"); m_model_object->sla_drain_holes[idx].m_radius = m_old_hole_radius;
bool slider_clicked = ImGui::IsItemClicked(); // someone clicked the slider float backup = m_new_hole_radius;
bool slider_edited = ImGui::IsItemEdited(); // someone is dragging the slider m_new_hole_radius = m_old_hole_radius;
bool slider_released = ImGui::IsItemDeactivatedAfterEdit(); // someone has just released the slider Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Change drainage hole diameter")));
m_new_hole_radius = backup;
m_imgui->text(m_desc.at("points_density")); for (size_t idx=0; idx<m_selected.size(); ++idx)
ImGui::SameLine(settings_sliders_left); if (m_selected[idx])
m_model_object->sla_drain_holes[idx].m_radius = m_new_hole_radius;
ImGui::SliderFloat(" ", &density, 0.f, 200.f, "%.f %%"); m_old_hole_radius = 0.f;
slider_clicked |= ImGui::IsItemClicked();
slider_edited |= ImGui::IsItemEdited();
slider_released |= ImGui::IsItemDeactivatedAfterEdit();
if (slider_clicked) { // stash the values of the settings so we know what to revert to after undo
m_minimal_point_distance_stash = minimal_point_distance;
m_density_stash = density;
}
if (slider_edited) {
m_model_object->config.opt<ConfigOptionFloat>("support_points_minimal_distance", true)->value = minimal_point_distance;
m_model_object->config.opt<ConfigOptionInt>("support_points_density_relative", true)->value = (int)density;
}
if (slider_released) {
m_model_object->config.opt<ConfigOptionFloat>("support_points_minimal_distance", true)->value = m_minimal_point_distance_stash;
m_model_object->config.opt<ConfigOptionInt>("support_points_density_relative", true)->value = (int)m_density_stash;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support parameter change")));
m_model_object->config.opt<ConfigOptionFloat>("support_points_minimal_distance", true)->value = minimal_point_distance;
m_model_object->config.opt<ConfigOptionInt>("support_points_density_relative", true)->value = (int)density;
wxGetApp().obj_list()->update_and_show_object_settings_item();
}
m_imgui->disabled_begin(m_normal_cache.empty());
remove_all = m_imgui->button(m_desc.at("remove_all"));
m_imgui->disabled_end();
} }
// !!!! Something as above should be done for the undo/redo
m_imgui->text("Hole height: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_new_hole_height, 0.1f, 10.f, "%.1f");
m_imgui->disabled_begin(m_selection_empty);
remove_selected = m_imgui->button(m_desc.at("remove_selected"));
m_imgui->disabled_end();
m_imgui->disabled_begin(m_model_object->sla_drain_holes.empty());
remove_all = m_imgui->button(m_desc.at("remove_all"));
m_imgui->disabled_end();
m_imgui->text(" "); // vertical gap
m_imgui->text("Offset: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_offset, 0.f, 5.f, "%.1f");
m_imgui->text("Adaptibility: ");
ImGui::SameLine();
ImGui::SliderFloat(" ", &m_adaptability, 0.f, 1.f, "%.1f");
// Following is rendered in both editing and non-editing mode: // Following is rendered in both editing and non-editing mode:
m_imgui->text(""); m_imgui->text("");
@ -827,7 +746,6 @@ RENDER_AGAIN:
if (ImGui::SliderFloat(" ", &m_clipping_plane_distance, 0.f, 1.f, "%.2f")) if (ImGui::SliderFloat(" ", &m_clipping_plane_distance, 0.f, 1.f, "%.2f"))
update_clipping_plane(true); update_clipping_plane(true);
if (m_imgui->button("?")) { if (m_imgui->button("?")) {
wxGetApp().CallAfter([]() { wxGetApp().CallAfter([]() {
SlaGizmoHelpDialog help_dlg; SlaGizmoHelpDialog help_dlg;
@ -835,18 +753,18 @@ RENDER_AGAIN:
}); });
} }
m_imgui->end(); if (m_imgui->checkbox(m_desc["show_supports"], m_show_supports)) {
m_parent.toggle_sla_auxiliaries_visibility(m_show_supports, m_model_object, m_active_instance);
if (m_editing_mode != m_old_editing_state) { // user toggled between editing/non-editing mode
m_parent.toggle_sla_auxiliaries_visibility(!m_editing_mode, m_model_object, m_active_instance);
force_refresh = true; force_refresh = true;
} }
m_old_editing_state = m_editing_mode;
m_imgui->end();
if (remove_selected || remove_all) { if (remove_selected || remove_all) {
force_refresh = false; force_refresh = false;
m_parent.set_as_dirty(); m_parent.set_as_dirty();
bool was_in_editing = m_editing_mode;
if (remove_all) { if (remove_all) {
select_point(AllPoints); select_point(AllPoints);
delete_selected_points(true); // true - delete regardless of locked status delete_selected_points(true); // true - delete regardless of locked status
@ -927,12 +845,11 @@ void GLGizmoHollow::on_set_state()
// Set default head diameter from config. // Set default head diameter from config.
const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config;
m_new_point_head_diameter = static_cast<const ConfigOptionFloat*>(cfg.option("support_head_front_diameter"))->value; m_new_hole_radius = static_cast<const ConfigOptionFloat*>(cfg.option("support_head_front_diameter"))->value;
} }
if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off
//Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned off"))); //Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned off")));
m_parent.toggle_model_objects_visibility(true); m_parent.toggle_model_objects_visibility(true);
m_normal_cache.clear();
m_clipping_plane_distance = 0.f; m_clipping_plane_distance = 0.f;
// Release clippers and the AABB raycaster. // Release clippers and the AABB raycaster.
m_object_clipper.reset(); m_object_clipper.reset();
@ -951,27 +868,27 @@ void GLGizmoHollow::on_start_dragging()
if (m_hover_id != -1) { if (m_hover_id != -1) {
select_point(NoPoints); select_point(NoPoints);
select_point(m_hover_id); select_point(m_hover_id);
m_point_before_drag = m_editing_cache[m_hover_id]; m_hole_before_drag = m_model_object->sla_drain_holes[m_hover_id].m_pos;
} }
else else
m_point_before_drag = CacheEntry(); m_hole_before_drag = Vec3f::Zero();
} }
void GLGizmoHollow::on_stop_dragging() void GLGizmoHollow::on_stop_dragging()
{ {
if (m_hover_id != -1) { if (m_hover_id != -1) {
CacheEntry backup = m_editing_cache[m_hover_id]; Vec3f backup = m_model_object->sla_drain_holes[m_hover_id].m_pos;
if (m_point_before_drag.support_point.pos != Vec3f::Zero() // some point was touched if (m_hole_before_drag != Vec3f::Zero() // some point was touched
&& backup.support_point.pos != m_point_before_drag.support_point.pos) // and it was moved, not just selected && backup != m_hole_before_drag) // and it was moved, not just selected
{ {
m_editing_cache[m_hover_id] = m_point_before_drag; m_model_object->sla_drain_holes[m_hover_id].m_pos = m_hole_before_drag;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Move drainage hole"))); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Move drainage hole")));
m_editing_cache[m_hover_id] = backup; m_model_object->sla_drain_holes[m_hover_id].m_pos = backup;
} }
} }
m_point_before_drag = CacheEntry(); m_hole_before_drag = Vec3f::Zero();
} }
@ -981,9 +898,8 @@ void GLGizmoHollow::on_load(cereal::BinaryInputArchive& ar)
ar(m_clipping_plane_distance, ar(m_clipping_plane_distance,
*m_clipping_plane, *m_clipping_plane,
m_model_object_id, m_model_object_id,
m_new_point_head_diameter, m_new_hole_radius,
m_normal_cache, m_selected,
m_editing_cache,
m_selection_empty m_selection_empty
); );
} }
@ -995,9 +911,8 @@ void GLGizmoHollow::on_save(cereal::BinaryOutputArchive& ar) const
ar(m_clipping_plane_distance, ar(m_clipping_plane_distance,
*m_clipping_plane, *m_clipping_plane,
m_model_object_id, m_model_object_id,
m_new_point_head_diameter, m_new_hole_radius,
m_normal_cache, m_selected,
m_editing_cache,
m_selection_empty m_selection_empty
); );
} }
@ -1006,63 +921,41 @@ void GLGizmoHollow::on_save(cereal::BinaryOutputArchive& ar) const
void GLGizmoHollow::select_point(int i) void GLGizmoHollow::select_point(int i)
{ {
if (! m_editing_mode) {
std::cout << "DEBUGGING: select_point called when out of editing mode!" << std::endl;
std::abort();
}
if (i == AllPoints || i == NoPoints) { if (i == AllPoints || i == NoPoints) {
for (auto& point_and_selection : m_editing_cache) m_selected.assign(m_selected.size(), i == AllPoints);
point_and_selection.selected = ( i == AllPoints );
m_selection_empty = (i == NoPoints); m_selection_empty = (i == NoPoints);
if (i == AllPoints) if (i == AllPoints)
m_new_point_head_diameter = m_editing_cache[0].support_point.head_front_radius * 2.f; m_new_hole_radius = m_model_object->sla_drain_holes[0].m_radius;
} }
else { else {
m_editing_cache[i].selected = true; while (size_t(i) >= m_selected.size())
m_selected.push_back(false);
m_selected[i] = true;
m_selection_empty = false; m_selection_empty = false;
m_new_point_head_diameter = m_editing_cache[i].support_point.head_front_radius * 2.f; m_new_hole_radius = m_model_object->sla_drain_holes[i].m_radius;
} }
} }
void GLGizmoHollow::unselect_point(int i) void GLGizmoHollow::unselect_point(int i)
{ {
if (! m_editing_mode) { m_selected[i] = false;
std::cout << "DEBUGGING: unselect_point called when out of editing mode!" << std::endl;
std::abort();
}
m_editing_cache[i].selected = false;
m_selection_empty = true; m_selection_empty = true;
for (const CacheEntry& ce : m_editing_cache) { for (const bool sel : m_selected) {
if (ce.selected) { if (sel) {
m_selection_empty = false; m_selection_empty = false;
break; break;
} }
} }
} }
bool GLGizmoHollow::unsaved_changes() const
{
if (m_editing_cache.size() != m_normal_cache.size())
return true;
for (size_t i=0; i<m_editing_cache.size(); ++i)
if (m_editing_cache[i].support_point != m_normal_cache[i])
return true;
return false;
}
void GLGizmoHollow::reload_cache() void GLGizmoHollow::reload_cache()
{ {
m_selected.clear();
m_selected.assign(m_model_object->sla_drain_holes.size(), false);
} }
void GLGizmoHollow::update_clipping_plane(bool keep_normal) const void GLGizmoHollow::update_clipping_plane(bool keep_normal) const
{ {
Vec3d normal = (keep_normal && m_clipping_plane->get_normal() != Vec3d::Zero() ? Vec3d normal = (keep_normal && m_clipping_plane->get_normal() != Vec3d::Zero() ?

View file

@ -25,7 +25,7 @@ private:
ObjectID m_model_object_id = 0; ObjectID m_model_object_id = 0;
int m_active_instance = -1; int m_active_instance = -1;
float m_active_instance_bb_radius; // to cache the bb float m_active_instance_bb_radius; // to cache the bb
mutable double m_z_shift = 0.f; mutable double m_z_shift = 0.;
bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, Vec3f>& pos_and_normal); bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, Vec3f>& pos_and_normal);
const float RenderPointScale = 1.f; const float RenderPointScale = 1.f;
@ -46,27 +46,26 @@ private:
class CacheEntry { class CacheEntry {
public: public:
CacheEntry() : CacheEntry() :
support_point(sla::SupportPoint()), selected(false), normal(Vec3f::Zero()) {} drain_hole(sla::DrainHole()), selected(false) {}
CacheEntry(const sla::SupportPoint& point, bool sel = false, const Vec3f& norm = Vec3f::Zero()) : CacheEntry(const sla::DrainHole& point, bool sel = false) :
support_point(point), selected(sel), normal(norm) {} drain_hole(point), selected(sel) {}
bool operator==(const CacheEntry& rhs) const { bool operator==(const CacheEntry& rhs) const {
return (support_point == rhs.support_point); return (drain_hole == rhs.drain_hole);
} }
bool operator!=(const CacheEntry& rhs) const { bool operator!=(const CacheEntry& rhs) const {
return ! ((*this) == rhs); return ! ((*this) == rhs);
} }
sla::SupportPoint support_point; sla::DrainHole drain_hole;
bool selected; // whether the point is selected bool selected; // whether the point is selected
Vec3f normal;
template<class Archive> template<class Archive>
void serialize(Archive & ar) void serialize(Archive & ar)
{ {
ar(support_point, selected, normal); ar(drain_hole, selected);
} }
}; };
@ -97,17 +96,14 @@ private:
bool unsaved_changes() const; bool unsaved_changes() const;
const TriangleMesh* mesh() const; const TriangleMesh* mesh() const;
bool m_editing_mode = true; // Is editing mode active? bool m_show_supports = true;
bool m_old_editing_state = false; // To keep track of whether the user toggled between the modes (needed for imgui refreshes). float m_new_hole_radius; // Size of a new hole.
float m_new_point_head_diameter; // Size of a new point. float m_new_hole_height = 5.f;
float m_new_cone_angle = 0.f; float m_old_hole_radius = 0.; // undo/redo - so we know what state was edited
float m_new_cone_height = 5.f; Vec3f m_hole_before_drag = Vec3f::Zero();
CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited
float m_old_point_head_diameter = 0.; // the same
float m_minimal_point_distance_stash = 0.f; // and again float m_minimal_point_distance_stash = 0.f; // and again
float m_density_stash = 0.f; // and again float m_density_stash = 0.f; // and again
mutable std::vector<CacheEntry> m_editing_cache; // a support point and whether it is currently selected mutable std::vector<bool> m_selected; // which holes are currently selected
std::vector<sla::SupportPoint> m_normal_cache; // to restore after discarding changes or undo/redo
float m_offset = 2.f; float m_offset = 2.f;
float m_adaptability = 1.f; float m_adaptability = 1.f;
@ -148,7 +144,7 @@ protected:
void on_set_hover_id() override void on_set_hover_id() override
{ {
if (! m_editing_mode || (int)m_editing_cache.size() <= m_hover_id) if (int(m_model_object->sla_drain_holes.size()) <= m_hover_id)
m_hover_id = -1; m_hover_id = -1;
} }
void on_start_dragging() override; void on_start_dragging() override;

View file

@ -750,20 +750,31 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
if (evt.GetEventType() == wxEVT_KEY_UP) if (evt.GetEventType() == wxEVT_KEY_UP)
{ {
if (m_current == SlaSupports) if (m_current == SlaSupports || m_current == Hollow)
{ {
GLGizmoSlaSupports* gizmo = dynamic_cast<GLGizmoSlaSupports*>(get_current()); bool is_editing = true;
bool is_rectangle_dragging = false;
if (m_current == SlaSupports) {
GLGizmoSlaSupports* gizmo = dynamic_cast<GLGizmoSlaSupports*>(get_current());
is_editing = gizmo->is_in_editing_mode();
is_rectangle_dragging = gizmo->is_selection_rectangle_dragging();
}
else {
GLGizmoHollow* gizmo = dynamic_cast<GLGizmoHollow*>(get_current());
is_rectangle_dragging = gizmo->is_selection_rectangle_dragging();
}
if (keyCode == WXK_SHIFT) if (keyCode == WXK_SHIFT)
{ {
// shift has been just released - SLA gizmo might want to close rectangular selection. // shift has been just released - SLA gizmo might want to close rectangular selection.
if (gizmo_event(SLAGizmoEventType::ShiftUp) || (gizmo->is_in_editing_mode() && gizmo->is_selection_rectangle_dragging())) if (gizmo_event(SLAGizmoEventType::ShiftUp) || (is_editing && is_rectangle_dragging))
processed = true; processed = true;
} }
else if (keyCode == WXK_ALT) else if (keyCode == WXK_ALT)
{ {
// alt has been just released - SLA gizmo might want to close rectangular selection. // alt has been just released - SLA gizmo might want to close rectangular selection.
if (gizmo_event(SLAGizmoEventType::AltUp) || (gizmo->is_in_editing_mode() && gizmo->is_selection_rectangle_dragging())) if (gizmo_event(SLAGizmoEventType::AltUp) || (is_editing && is_rectangle_dragging))
processed = true; processed = true;
} }
} }