Merge branch 'main' into feat/texdisp-tooltip-media

This commit is contained in:
SoftFever
2026-10-15 00:02:11 +08:00
410 changed files with 124028 additions and 23317 deletions
+1 -1
View File
@@ -513,7 +513,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent)
m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
Label* time_unit_label = new Label(parent, "H");
Label* time_unit_label = new Label(parent, _L_CONTEXT("H", "Hours unit"));
time_unit_label->SetForegroundColour(*wxBLACK);
time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1));
time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL);
+4 -4
View File
@@ -974,12 +974,12 @@ void AuxiliaryPanel::on_import_file(wxCommandEvent &event)
if (file_model == s_default_folders[MODEL_PICTURE]) {
//wildcard = wxT("JPEG files (*.jpeg)|*.jpeg|BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg");
wildcard = wxT("files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp");
wildcard = _L("Image files") + " (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
}
if (file_model == s_default_folders[OTHERS]) { wildcard = wxT("TXT files (*.txt)|*.txt"); }
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = wxT("EXCEL files (*.xls)|*.xls|EXCEL files (*.xlsx)|*.xlsx|PDF files (*.pdf)|*.pdf"); }
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = wxT("PDF files (*.pdf)|*.pdf"); }
if (file_model == s_default_folders[OTHERS]) { wildcard = _L("TXT files") + " (*.txt)|*.txt"; }
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = _L("EXCEL files") + " (*.xls)|*.xls|" + _L("EXCEL files") + " (*.xlsx)|*.xlsx|" + _L("PDF files") + " (*.pdf)|*.pdf"; }
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = _L("PDF files") + " (*.pdf)|*.pdf"; }
wxFileDialog dialog(this, _L("Choose files"), wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
if (dialog.ShowModal() == wxID_OK) {
+1 -1
View File
@@ -122,7 +122,7 @@ std::pair<std::string, std::vector<size_t>> SlicingProcessCompletedEvent::format
} catch (std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown C++ exception.";
error = _u8L("Unknown C++ exception.");
}
return std::make_pair(std::move(error), std::vector<size_t>{monospace});
}
+67 -50
View File
@@ -931,7 +931,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
});
feat_dropdown("plane", "design_plane", _L("Datum / Curve (plane / axis / coord sys / helix / project)"), {
{"design_plane", _L("Plane"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
{"design_plane", _L_CONTEXT("Plane", "Design"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
[this] {
populate_plane_choices(m_plane_base);
reset_plane_refs();
@@ -1011,7 +1011,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
open_tool(Tool::Transform);
}, SHIFT('Y')},
{"design_mirror", _L("Mirror"), _L("Reflect a body about a plane"),
{"design_mirror", _L_CONTEXT("Mirror", "Design"), _L("Reflect a body about a plane"),
[this] {
if (m_doc.bodies.empty()) {
set_status(StatusKind::Error, _L("Mirror needs a body — add or import one first"));
@@ -1143,7 +1143,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Hole / Thread — drilling into a solid (both face-aware)
feat_dropdown("hole", "design_hole", _L("Hole / thread"), {
{"design_hole", _L("Hole"), _L("Drill a hole, centered on a picked face or placed on a plane"),
{"design_hole", _L_CONTEXT("Hole", "Design"), _L("Drill a hole, centered on a picked face or placed on a plane"),
[this] {
// #2: drill on the picked solid face, centred on it (origin = face centroid,
// normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then
@@ -1344,7 +1344,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
_L("Click 2 points or a line / circle / arc to place a dimension"));
// (separator dropped: the group it divided is now reached from the offer)
dropdown("design_line", _L("Line / polyline"), {
{"design_line", DesignSketchTool::Mode::Line, _L("Line"), _L("Click start, then end — then set the exact length")},
{"design_line", DesignSketchTool::Mode::Line, _L_CONTEXT("Line", "Design"), _L("Click start, then end — then set the exact length")},
{"design_polyline", DesignSketchTool::Mode::Polyline, _L("Polyline"), _L("Click points; click the first point to close the loop, right-click or Enter to end the chain")} });
dropdown("design_rect", _L("Rectangle"), {
{"design_rect", DesignSketchTool::Mode::CornerRect, _L("Corner rectangle"), _L("Click two opposite corners")},
@@ -1407,7 +1407,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
{"design_chamfer", DesignSketchTool::Mode::Chamfer, _L("Chamfer"), _L("Pick two lines, then drag the arrow or click the distance to set it")} });
skbtn("design_offset", DesignSketchTool::Mode::Offset, _L("Offset"),
_L("Pick an entity, then drag the arrow or click the distance; click empty to apply"));
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L("Mirror"),
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L_CONTEXT("Mirror", "Design"),
_L("Pick a mirror-axis line, then the entities to mirror; click empty to apply"));
// Trim / Extend scissors — standalone sketch tools (NOT inside Constrain): click a
// segment to cut it back to / out to its nearest intersection. One cut per click.
@@ -1438,9 +1438,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
auto arm_polygon = [this, select_tool] {
push_polygon_params();
select_tool(DesignSketchTool::Mode::Polygon,
wxString::Format(_L("Click center then a vertex — %d sides, %s"),
m_poly_sides,
m_poly_circumscribed ? _L("circumscribed") : _L("inscribed")));
wxString::Format(m_poly_circumscribed
? _L_PLURAL("Click center then a vertex — %d side, circumscribed",
"Click center then a vertex — %d sides, circumscribed", m_poly_sides)
: _L_PLURAL("Click center then a vertex — %d side, inscribed",
"Click center then a vertex — %d sides, inscribed", m_poly_sides),
m_poly_sides));
};
for (int n : {3, 4, 5, 6, 8, 12})
m_verb_actions["btn:poly#" + std::to_string(n)] =
@@ -1998,7 +2001,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
hform->Add(m_hole_target_label, 0, wxALIGN_CENTER_VERTICAL);
m_box_hole = new wxBoxSizer(wxVERTICAL);
m_box_hole->Add(card_header(m_cards, "design_hole", _L("Hole"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_hole->Add(card_header(m_cards, "design_hole", _L_CONTEXT("Hole", "Design"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_hole->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
m_box_hole->Add(hform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
cards->Add(m_box_hole, 0, wxEXPAND);
@@ -2022,7 +2025,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_thread_std->Append(s.name);
m_thread_std->SetSelection(0);
m_thread_std->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { apply_thread_standard(); });
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Standard", "Thread standard")), 0, wxALIGN_CENTER_VERTICAL);
tform->Add(m_thread_std, 0, wxEXPAND);
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
@@ -2233,7 +2236,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_cut_plane = make_combo(m_cards);
m_cut_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
cform->Add(m_cut_plane, 0, wxEXPAND);
m_cut_offset = make_spin(m_cards, 0.0, -10000.0, 10000.0);
@@ -2258,7 +2261,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// --- Plane (datum/reference plane: offset + tilt from a base plane; no solid) ---
m_box_plane = new wxBoxSizer(wxVERTICAL);
m_box_plane->Add(card_header(m_cards, "design_plane", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_plane->Add(card_header(m_cards, "design_plane", _L_CONTEXT("Plane", "Design"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_plane->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
{
// Plane type chooses which inputs matter (Onshape/Fusion parity):
@@ -2570,7 +2573,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// --- Mirror (reflect a body about a plane) ---
m_box_mirror = new wxBoxSizer(wxVERTICAL);
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L("Mirror"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L_CONTEXT("Mirror", "Design"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_mirror->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
{
auto* mform = two_col_form();
@@ -2748,7 +2751,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_helix_plane = make_combo(m_cards);
populate_plane_choices(m_helix_plane);
m_helix_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
hform->Add(m_helix_plane, 0, wxEXPAND);
m_helix_radius = make_spin(m_cards, 10.0, 0.01, 10000.0);
m_helix_radius->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); });
@@ -3556,7 +3559,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_doc.recompute();
set_status(StatusKind::Info, cons.empty()
? _L("Sketch created — select it, then right-click to Extrude")
: wxString::Format(_L("Sketch created (%zu driving dims) — select it, then right-click to Extrude"),
: wxString::Format(_L_PLURAL("Sketch created (%zu driving dim) — select it, then right-click to Extrude",
"Sketch created (%zu driving dims) — select it, then right-click to Extrude",
unsigned(cons.size())),
cons.size()));
refresh_tree();
sync_sketch_display(); // keep the just-committed sketch visible as a face
@@ -4585,8 +4590,8 @@ void DesignPanel::on_shape_changed()
void DesignPanel::set_status_ok()
{
set_status(wxString::Format(_L("OK — %zu triangles"),
m_doc.display_mesh.its.indices.size()));
const size_t n_tris = m_doc.display_mesh.its.indices.size();
set_status(wxString::Format(_L_PLURAL("OK — %zu triangle", "OK — %zu triangles", unsigned(n_tris)), n_tris));
if (m_viewport != nullptr) {
// A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
if (body_move_pending()) end_body_move(true);
@@ -5035,11 +5040,15 @@ void DesignPanel::on_import_mesh()
// A dense organic scan has few coplanar neighbours to merge away and stays heavy afterwards;
// warn rather than silently freezing the CAD kernel on every subsequent recompute.
if (mesh.its.indices.size() > MESH_IMPORT_TRIANGLE_WARN) {
const int n_tris = int(mesh.its.indices.size());
const wxString q = wxString::Format(
_L("This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?"),
int(mesh.its.indices.size()));
_L_PLURAL("This mesh has %d triangle. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?",
"This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?", n_tris),
n_tris);
MessageDialog dlg(this, q, _L("Large mesh"), wxYES_NO | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return;
@@ -5080,7 +5089,7 @@ void DesignPanel::on_import_mesh()
// source mesh the user needs to know about before they start cutting features into it.
if (stats.is_solid) {
set_status(StatusKind::Info, wxString::Format(
_L("Imported solid — %d triangles → %d faces, volume %.2f mm³. Pick a face or edge, "
_L("Imported solid — triangles: %d → faces: %d, volume %.2f mm³. Pick a face or edge, "
"then Fillet / Cut / Shell to modify"),
stats.kept_tris, stats.faces_final, stats.volume));
} else {
@@ -5171,7 +5180,8 @@ void DesignPanel::open_insert_card(const wxString& base_name)
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // surface the unified ✓/✗
set_status(StatusKind::Info, base_name + _L(" — drag to place/size, then Confirm"));
// TRN Design tab status; %s is the inserted item, such as "Text" or "SVG".
set_status(StatusKind::Info, wxString::Format(_L("%s — drag to place/size, then Confirm"), base_name));
}
// Confirm: keep the placed art and leave the placement gizmo. The feature is already in
@@ -5255,7 +5265,7 @@ void DesignPanel::on_add_extrude()
{
BooleanMode mode = static_cast<BooleanMode>(m_mode->GetSelection()); // New/Add/Cut/Intersect
m_feature_counter++;
const std::string name = feature_name(_L("Extrude"));
const std::string name = feature_name(_L_CONTEXT("Extrude", "Design"));
int idx = -1;
if (m_extrude_face_src >= 0) {
// Onshape face-extrude: the picked solid face is the profile (no sketch wire).
@@ -5383,7 +5393,7 @@ void DesignPanel::on_add_hole()
m_feature_counter++;
const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane,
feature_name(_L("Hole")));
feature_name(_L_CONTEXT("Hole", "Design")));
// On-face holes drill the body the face belongs to (even after the pick was cleared).
if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size()))
m_doc.features[hidx].target_body = m_hole_face_body;
@@ -5770,7 +5780,7 @@ void DesignPanel::on_add_mirror()
const BooleanMode mode = m_mirror_keep->GetValue() ? BooleanMode::New : BooleanMode::Add;
m_feature_counter++;
m_doc.add_mirror(plane_from_choice(m_mirror_plane->GetSelection()), target, mode,
feature_name(_L("Mirror")));
feature_name(_L_CONTEXT("Mirror", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -5817,7 +5827,7 @@ void DesignPanel::on_add_rib()
}
m_feature_counter++;
m_doc.add_rib(sketch_ref, m_rib_entity->GetValue(), m_rib_thickness->GetValue(),
m_rib_depth->GetValue(), target, feature_name(_L("Rib")));
m_rib_depth->GetValue(), target, feature_name(_L_CONTEXT("Rib", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -5837,7 +5847,7 @@ void DesignPanel::on_add_project()
m_feature_counter++;
m_doc.add_project_edges(src_body, {}, face,
plane_from_choice(m_proj_plane->GetSelection()),
feature_name(_L("Project")));
feature_name(_L_CONTEXT("Project", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -6024,7 +6034,7 @@ void DesignPanel::on_add_pattern()
m_doc.add_pattern(circular, int(m_pattern_count->GetValue()),
m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(),
m_pattern_angle->GetValue(), target,
feature_name(_L("Pattern")));
feature_name(_L_CONTEXT("Pattern", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
@@ -6126,7 +6136,7 @@ void DesignPanel::on_add_cut()
m_feature_counter++;
m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(),
/*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true,
m_cut_target->GetSelection(), feature_name(_L("Cut")));
m_cut_target->GetSelection(), feature_name(_L_CONTEXT("Cut", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -6429,7 +6439,8 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
using Mode = DesignSketchTool::Mode;
auto pick_more = [](const wxString& lead, int n) {
return n <= 0 ? lead
: lead + wxString::Format(_L(" · %d picked"), n);
// TRN Appended to a sketch tool prompt; %d is the number of picked entities.
: lead + wxString::Format(_L_PLURAL(" · %d picked", " · %d picked", n), n);
};
switch (m) {
case Mode::Select:
@@ -6442,8 +6453,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
"Shift-click adds · Del removes");
case Mode::Constrain:
return picks > 0
? wxString::Format(_L("%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)"), picks)
? wxString::Format(_L_PLURAL("%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)",
"%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)", picks), picks)
: _L("Constrain — click one or two entities, then choose a constraint");
case Mode::Dimension:
return step == 0 ? _L("Dimension — click an entity, or the first of two points")
@@ -6544,8 +6557,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
? _L("Mirror — first click the LINE to mirror about (a construction line works)")
: picks == 0
? _L("Mirror — axis set · now click the entities to mirror · Esc cancels")
: wxString::Format(_L("Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies"), picks);
: wxString::Format(_L_PLURAL("Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies",
"Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies", picks), picks);
case Mode::Move:
return step == 0 ? _L("Move — click the entities to move")
: pick_more(_L("Move — drag the handle, or click the number to type the "
@@ -6653,7 +6668,7 @@ wxString DesignPanel::offer_header(int kind) const
case OfferSel::SkPoint: return _L("Sketch point");
case OfferSel::Sk2Ent: {
const int n = m_viewport ? m_viewport->sketch_selection_count() : 0;
return wxString::Format(_L("%d sketch entities"), n);
return wxString::Format(_L_PLURAL("%d sketch entity", "%d sketch entities", n), n);
}
default: return wxString();
}
@@ -6753,7 +6768,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
} else {
offer_trace("row=%d %s DISABLED (%s)", row, kOfferRowNames[row],
v->refusal ? v->refusal : "blocked");
wxString s = tr(v->name);
wxString s = I18N::translate(v->name, "Design");
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
append_offer_item(&menu, id, s, *v)->Enable(false);
bound.push_back(nullptr);
@@ -6828,7 +6843,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
}
if (!ok) {
offer_trace("row=%d %s ~ %s BLOCKED", row, kOfferRowNames[row], v->id);
wxString s = tr(v->name);
wxString s = I18N::translate(v->name, "Design");
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
append_offer_item(target, base + int(bound.size()), s, *v)->Enable(false);
bound.push_back(nullptr);
@@ -6935,8 +6950,8 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
drop_ghost(); // the real bodies are about to become the ghost's pose
m_doc.checkpoint(); // undo boundary: committing a mate from the offer
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false,
"Mate" + std::to_string(++m_feature_counter));
++m_feature_counter;
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, feature_name(_L("Mate")));
if (idx < 0) {
set_status(StatusKind::Error, _L("Mate rejected"));
return;
@@ -7165,7 +7180,7 @@ bool DesignPanel::on_add_plane()
m_feature_counter++;
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
feature_name(_L("Plane")));
feature_name(_L_CONTEXT("Plane", "Design")));
if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]);
m_doc.recompute(); // datum-only docs yield no body; that is expected/benign
set_status(StatusKind::Info, _L("Plane added — pick it as a sketch plane"));
@@ -7206,7 +7221,7 @@ void DesignPanel::on_add_shell()
m_feature_counter++;
m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body,
feature_name(_L("Shell")));
feature_name(_L_CONTEXT("Shell", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
@@ -9500,7 +9515,8 @@ void DesignPanel::request_value(const wxString& label, double def, double mn, do
m_form->FitInside();
m_value_input->SetFocus();
m_value_input->SetSelection(-1, -1); // select all so typing replaces the value
set_status(StatusKind::Info, label + _L(" — type a value, press Enter (or Confirm)"));
// TRN Design tab status; %s is the value being asked for, such as "Fillet radius".
set_status(StatusKind::Info, wxString::Format(_L("%s — type a value, press Enter (or Confirm)"), label));
}
void DesignPanel::confirm_value()
@@ -10228,19 +10244,19 @@ void DesignPanel::on_commit()
for (size_t b = 0; b < m_disp_body_meshes.size(); ++b) {
if (b < m_body_visible.size() && !m_body_visible[b]) continue; // skip hidden
if (m_disp_body_meshes[b].its.indices.empty()) continue;
parts.emplace_back(&m_disp_body_meshes[b], wxString::FromUTF8("Design Body " + std::to_string(b + 1)));
parts.emplace_back(&m_disp_body_meshes[b], wxString::Format(_L("Design Body %d"), int(b + 1)));
}
if (parts.empty()) { // every body hidden — nothing to ship
set_status(_L("All bodies hidden — show one before committing"));
return;
}
if (m_commit_mode == CommitMode::Assembly && parts.size() > 1)
obj_list->load_mesh_object(parts, "Design Assembly");
obj_list->load_mesh_object(parts, _L("Design Assembly"));
else
for (const auto& [mesh, name] : parts)
obj_list->load_mesh_object(*mesh, name);
} else {
obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body");
obj_list->load_mesh_object(m_disp_pick_mesh, _L("Design Body"));
}
// Persist the editable parametric recipe alongside the committed meshes so the
@@ -11478,7 +11494,8 @@ void DesignPanel::refresh_preview()
if (ok) {
m_viewport->set_preview_mesh(mesh);
set_status(StatusKind::Ok, wxString::Format(_L("Preview — %zu triangles"), mesh.its.indices.size()));
const size_t n_tris = mesh.its.indices.size();
set_status(StatusKind::Ok, wxString::Format(_L_PLURAL("Preview — %zu triangle", "Preview — %zu triangles", unsigned(n_tris)), n_tris));
} else {
m_viewport->clear_preview();
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
@@ -11789,13 +11806,13 @@ void DesignPanel::open_tool(Tool t)
case Tool::Extrude: m_hdr_extrude->SetLabel(title(_L_CONTEXT("Extrude", "Design"))); break;
case Tool::Dressup: m_hdr_dressup->SetLabel(title(
m_dressup_type->GetSelection() == 0 ? _L("Fillet") : _L("Chamfer"))); break;
case Tool::Hole: m_hdr_hole->SetLabel(title(_L("Hole"))); break;
case Tool::Hole: m_hdr_hole->SetLabel(title(_L_CONTEXT("Hole", "Design"))); break;
case Tool::Thread: m_hdr_thread->SetLabel(title(_L("Thread"))); break;
case Tool::Shell: m_hdr_shell->SetLabel(title(_L_CONTEXT("Shell", "Design"))); break;
case Tool::Revolve: m_hdr_revolve->SetLabel(title(_L("Revolve"))); break;
case Tool::Sweep: m_hdr_sweep->SetLabel(title(_L("Sweep"))); break;
case Tool::Pattern: m_hdr_pattern->SetLabel(title(_L_CONTEXT("Pattern", "Design"))); break;
case Tool::Plane: m_hdr_plane->SetLabel(title(_L("Plane"))); break;
case Tool::Plane: m_hdr_plane->SetLabel(title(_L_CONTEXT("Plane", "Design"))); break;
case Tool::Loft: m_hdr_loft->SetLabel(title(_L("Loft"))); break;
case Tool::Draft: m_hdr_draft->SetLabel(title(_L("Draft"))); break;
case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break;
@@ -11809,7 +11826,7 @@ void DesignPanel::open_tool(Tool t)
case Tool::SurfaceOffset: m_hdr_surf_offset->SetLabel(title(_L("Surface Offset"))); break;
case Tool::ThickenSurface: m_hdr_surf_thicken->SetLabel(title(_L("Thicken Surface"))); break;
case Tool::Transform: m_hdr_transform->SetLabel(title(_L("Transform"))); break;
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L("Mirror"))); break;
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L_CONTEXT("Mirror", "Design"))); break;
case Tool::Thicken: m_hdr_thicken->SetLabel(title(_L("Thicken"))); break;
case Tool::Rib: m_hdr_rib->SetLabel(title(_L_CONTEXT("Rib", "Design"))); break;
case Tool::Project: m_hdr_project->SetLabel(title(_L_CONTEXT("Project", "Design"))); break;
+10 -4
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/Utils.hpp"
@@ -902,8 +903,11 @@ void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged
if (int(r.skipped.size()) != m_skipped_last) {
m_skipped_last = int(r.skipped.size());
if (m_skipped_last > 0)
notify(format(_u8L("%1% constraint(s) cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and are ignored"), m_skipped_last), false);
notify(format(into_u8(_L_PLURAL("%1% constraint cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and is ignored",
"%1% constraints cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and are ignored", m_skipped_last)),
m_skipped_last), false);
}
// Flag every entity referenced by a conflicting constraint so render() can
// tint it red (Onshape/SolveSpace over-constrained feedback), and remember which
@@ -8978,7 +8982,8 @@ void DesignSketchTool::confirm_op()
|| self_len(d, a) || self_len(d, b);
});
if (released > 0)
notify(format(_u8L("%1% constraint(s) the fillet/chamfer invalidated were removed"), released), false);
notify(format(into_u8(_L_PLURAL("%1% constraint the fillet/chamfer invalidated was removed",
"%1% constraints the fillet/chamfer invalidated were removed", released)), released), false);
auto coin = [&](R xr, int ln, R lr) {
SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; };
if (fillet) {
@@ -9427,7 +9432,8 @@ void DesignSketchTool::confirm_transform()
}
});
if (released > 0)
notify(format(_u8L("%1% constraint(s) released by the transform"), released), false);
notify(format(into_u8(_L_PLURAL("%1% constraint released by the transform",
"%1% constraints released by the transform", released)), released), false);
} else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
// ADDITIVE: append copies of each subject, then bind each copy to its source. Lines
// get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks
+1 -1
View File
@@ -559,7 +559,7 @@ int HistoryWindow::get_extruder_id()
void HistoryWindow::on_click_new_button(wxCommandEvent& event)
{
if (curr_obj && curr_obj->get_printer_series() == PrinterSeries::SERIES_P1P && m_calib_results_history.size() >= 16) {
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle."), 16), wxEmptyString, wxICON_WARNING | wxOK);
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle.", "This machine type can only hold %d historical results per nozzle.", 16), 16), wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return;
}
+1 -1
View File
@@ -973,7 +973,7 @@ bool PressureAdvanceWizard::can_save_cali_result(const std::vector<PACalibResult
}
if (curr_obj->get_printer_series() != PrinterSeries::SERIES_X1 && curr_obj->pa_calib_tab.size() >= MAX_PA_HISTORY_RESULTS_NUMS) {
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle. This result will not be saved."), MAX_PA_HISTORY_RESULTS_NUMS),
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle. This result will not be saved.", "This machine type can only hold %d historical results per nozzle. This result will not be saved.", MAX_PA_HISTORY_RESULTS_NUMS), MAX_PA_HISTORY_RESULTS_NUMS),
wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return false;
+3 -1
View File
@@ -352,7 +352,9 @@ void CalibrationCaliPage::update_subtask(MachineObject* obj)
prepare_text = wxString::Format(_L("Cloud Slicing..."));
}
else {
prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %s tasks ahead of you."), std::to_string(obj->queue_number));
prepare_text = wxString::Format(_L_PLURAL("In Cloud Slicing Queue, there is %d task ahead of you.",
"In Cloud Slicing Queue, there are %d tasks ahead of you.", obj->queue_number),
obj->queue_number);
show_percent = false;
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ CaliPageButton::CaliPageButton(wxWindow* parent, CaliPageActionType type, wxStri
this->SetLabel(_L("Finish"));
break;
default:
this->SetLabel("Unknown");
this->SetLabel(_L("Unknown"));
break;
}
@@ -644,9 +644,9 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
for (const auto& extruder : curr_obj->GetExtderSystem()->GetExtruders()) {
if (extruder.GetNozzleType() == NozzleType::ntUndefine) {
wxString name = _L("left");
if (extruder.GetExtId() == 0) { name = _L("right"); }
wxString msg = wxString::Format(_L("Printer %s nozzle information has not been set. Please configure it before proceeding with the calibration."), name);
wxString msg = extruder.GetExtId() == 0 ?
_L("Printer right nozzle information has not been set. Please configure it before proceeding with the calibration.") :
_L("Printer left nozzle information has not been set. Please configure it before proceeding with the calibration.");
MessageDialog msg_dlg(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
break;
@@ -2815,7 +2815,7 @@ MaxVolumetricSpeedPresetPage::MaxVolumetricSpeedPresetPage(
wxArrayString titles;
titles.push_back(_L("From Volumetric Speed"));
titles.push_back(_L("To Volumetric Speed"));
titles.push_back(_L("Step"));
titles.push_back(_L_CONTEXT("Step", "Increment"));
m_custom_range_panel->set_titles(titles);
m_custom_range_panel->set_unit(_L("mm³/s"));
+1 -4
View File
@@ -1,5 +1,4 @@
#ifndef slic3r_GUI_SingleChoice_hpp_
#define slic3r_GUI_SingleChoice_hpp_
#pragma once
#include "GUI_Utils.hpp"
#include "Plater.hpp"
@@ -33,5 +32,3 @@ private:
};
}} // namespace Slic3r::GUI
#endif
+9 -10
View File
@@ -680,16 +680,15 @@ void ColorDecomposeDialog::update_filament_limit_warning()
return;
}
wxString mode_name;
switch (m_selected_mode) {
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
}
const wxString warning_text = format_wxstr(
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
m_max_filament_count, mode_name);
const unsigned int max_count = static_cast<unsigned int>(m_max_filament_count);
const wxString warning_text = m_selected_mode == DecomposeMode::MaterialList ?
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
"The material list supports at most %1% colors. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
max_count), max_count) :
// TRN %2% is the decomposition mode name, "CMYW" or "RYBW"
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
"The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
max_count), max_count, m_selected_mode == DecomposeMode::CMYW ? "CMYW" : "RYBW");
// Show first so the panel is laid out and the text control gets its real
// width, then wrap to that width so the paragraph fills the content area.
+9 -4
View File
@@ -4,6 +4,7 @@
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Utils.hpp"
@@ -340,8 +341,9 @@ bool prepare_decompose_mixed_result(
missing_comp.component_idx = out_result.components.size();
missing_comp.official_component = official_component;
missing_comp.preset_name = preset_name;
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
// TRN %1% is a color name such as "Cyan", %2% is a filament type such as "PLA Basic"
missing_comp.display_name = format_wxstr(_L_CONTEXT("%1% %2%", "Color and filament type"),
decompose_base_color_display(comp.base_color), wxString::FromUTF8(basic_type));
missing.push_back(std::move(missing_comp));
out_result.components.push_back(0);
}
@@ -407,8 +409,11 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
missing_text += missing[i].display_name;
}
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition."),
wxString message = wxString::Format(_L_PLURAL("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition.",
"The current filament list does not contain %s. Project filaments required by "
"the mixed filament will be created automatically after decomposition.",
missing.size()),
missing_text);
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
-19
View File
@@ -1,19 +0,0 @@
#include <exception>
#include <stdexcept>
#include "libslic3r/Exception.hpp"
namespace Slic3r {
class ConfigError : public Slic3r::RuntimeError {
using Slic3r::RuntimeError::RuntimeError;
};
namespace GUI {
class ConfigGUITypeError : public ConfigError {
using ConfigError::ConfigError;
};
} // namespace GUI
} // namespace Slic3r
+4
View File
@@ -1105,6 +1105,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_field("support_filament", have_support_material || have_skirt);
toggle_line("raft_contact_distance", have_raft && !have_support_soluble);
// Orca:Hide raft expansion when raft is not enabled
toggle_line("raft_expansion", have_raft);
// Orca: Slim, Strong and Hybrid tree supports size their raft by first layer expansion only.
toggle_field("raft_expansion", !(is_tree(support_type) && (support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid)));
// Orca: First-layer density is available for supports broadly.
toggle_field("raft_first_layer_density", have_support_material);
+2 -1
View File
@@ -1,5 +1,6 @@
#include "ConnectPrinter.hpp"
#include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
@@ -169,7 +170,7 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
{
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
if (code.empty())
if (code.empty() && (!m_obj || m_obj->printer_agent_id != MOONRAKER_PRINTER_AGENT_ID))
code = "88888888";
for (char c : code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
+3 -2
View File
@@ -35,7 +35,7 @@ CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL);
m_list->SetMinSize(wxSize(50 * em, 18 * em));
m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em);
m_list->AppendColumn(_L_CONTEXT("Model", "Printer model"), wxLIST_FORMAT_LEFT, 8 * em);
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
@@ -109,7 +109,8 @@ void CrealityDiscoveryDialog::run_discovery()
"network and not blocked by Wi-Fi client isolation, then click Scan again."));
} else {
m_status->SetLabel(wxString::Format(
_L("Found %zu Creality printer(s). Select one and click Use Selected."),
_L_PLURAL("Found %zu Creality printer. Select it and click Use Selected.",
"Found %zu Creality printers. Select one and click Use Selected.", m_rows.size()),
m_rows.size()));
m_list->Select(0);
}
+1 -1
View File
@@ -4690,7 +4690,7 @@ void EditFilamentPresetDialog::delete_preset()
if (count > 0) {
msg = _L("Presets inherited by other presets cannot be deleted");
msg += "\n";
msg += _L_PLURAL("The following presets inherits this preset.", "The following preset inherits this preset.", count);
msg += _L_PLURAL("The following preset inherits this preset.", "The following presets inherit this preset.", count);
wxString title = _L("Delete Preset");
MessageDialog(this, msg + presets, title, wxOK | wxICON_ERROR).ShowModal();
m_selected_printer.clear();
+12
View File
@@ -276,6 +276,18 @@ namespace Slic3r
std::string connect_type = j["connect_type"].get<std::string>();
std::string bind_state = j["bind_state"].get<std::string>();
// Passive discovery must not replace another agent's saved binding.
if (get_current_printer_agent_id() == MOONRAKER_PRINTER_AGENT_ID) {
if (AppConfig* config = get_app_config()) {
const auto& machines = config->get_local_machines();
auto it = machines.find(dev_id);
if (it != machines.end() && it->second.printer_agent_id != MOONRAKER_PRINTER_AGENT_ID) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring Moonraker discovery for a device owned by another agent: " << dev_id;
return;
}
}
}
if (connect_type == "farm") {
connect_type ="lan";
bind_state = "free";
+28 -20
View File
@@ -4,6 +4,7 @@
#include "HMS.hpp"
#include "I18N.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
@@ -66,6 +67,7 @@
#include <vector>
#include <wx/colour.h>
#include <tuple>
#include <wx/app.h>
#include <wx/dir.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -527,6 +529,18 @@ bool MachineObject::HasRecentLanMessage()
return diff.count() < 5000;
}
bool MachineObject::has_access_right() const
{
return !m_access_revoked && (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID || !get_access_code().empty());
}
void MachineObject::revoke_access()
{
// UI panels retain this object; revoke its binding without invalidating their pointers.
access_code.clear();
m_access_revoked = true;
}
std::string MachineObject::get_access_code() const
{
return access_code;
@@ -535,6 +549,7 @@ std::string MachineObject::get_access_code() const
void MachineObject::set_access_code(std::string code, bool only_refresh)
{
this->access_code = code;
m_access_revoked = false;
if (only_refresh) {
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
if (config) {
@@ -548,25 +563,10 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
// session boundary, since parse_user_print_info() always overwrites their code
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
if (has_access_right()) {
DeviceManager::update_local_machine(*this, config);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
const auto& machines = config->get_local_machines();
auto it = machines.find(get_dev_id());
if (it != machines.end()) {
BBLocalMachine local_machine = it->second;
local_machine.access_code = "";
config->update_local_machine(local_machine);
}
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
// old BBL-era code can't silently "re-bind" this device again via
// get_access_code_with_legacy_fallback()'s legacy fallback.
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
config->erase("access_code", get_dev_id());
config->erase("user_access_code", get_dev_id());
}
config->clear_local_machine_access_code(get_dev_id(), printer_agent_id);
}
} else {
if (!code.empty())
@@ -2678,7 +2678,7 @@ void MachineObject::update_print_progress(const json& value)
int MachineObject::connect()
{
if (get_dev_ip().empty()) return -1;
if (m_access_revoked || get_dev_ip().empty()) return -1;
std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code();
@@ -2691,17 +2691,25 @@ int MachineObject::connect()
std::string host = Http::get_host_from_url(input, &port);
std::string ca_file;
if (GUI::wxGetApp().preset_bundle) {
if (wxTheApp && GUI::wxGetApp().preset_bundle) {
const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (port.empty())
port = config.opt_string("printhost_port");
ca_file = config.opt_string("printhost_cafile");
// Refresh credentials from the matching preset for connections that bypass GUI_App::select_machine().
const std::string print_host = config.opt_string("print_host");
if (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID && config.opt_string("printer_agent") == MOONRAKER_PRINTER_AGENT_ID &&
!print_host.empty() && dev_id_from_address(print_host, config.opt_string("printhost_port")) == get_dev_id()) {
password = config.opt_string("printhost_apikey");
if (password != get_access_code())
set_access_code(password);
}
}
if (host.empty())
host = get_dev_ip();
if (m_agent) {
try {
PrinterConnectionParams params{
+3 -1
View File
@@ -119,6 +119,7 @@ private:
std::string dev_name;
std::string dev_ip;
std::string access_code;
bool m_access_revoked = false;
// type, time stamp, delay
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
@@ -229,7 +230,8 @@ public:
std::string dev_connection_name; /* lan | eth */
/*access code*/
bool has_access_right() const { return !get_access_code().empty(); }
bool has_access_right() const;
void revoke_access();
std::string get_access_code() const;
void set_access_code(std::string code, bool only_refresh = true);
@@ -389,8 +389,8 @@ static void s_update_title(const std::shared_ptr<DevNozzleRack> rack, wgtDeviceN
wxString title = _L("Induction Hotend Rack");
if (rack && (rack->GetReadingCount() > 0))
{
wxString pending = ": " + _L("Reading") + wxString::Format(" %d/%d", rack->GetReadingIdx(), rack->GetReadingCount());
title += pending;
// TRN %d/%d is the hotend being read and the number of hotends to read.
title = wxString::Format(_L("Induction Hotend Rack: Reading %d/%d"), rack->GetReadingIdx(), rack->GetReadingCount());
}
title_label->SetLabel(title);
+2 -1
View File
@@ -385,7 +385,8 @@ void EditGCodeDialog::selection_changed(wxDataViewEvent& evt)
// Orca: move below checking for def in custom defined G-code placeholders
// This allows custom placeholders to override the default ones for this dialog
// Override custom def if selection is within the preset category
if (!def || m_params_list->GetSelectedTopLevelCategory() == "Presets") {
// The group label is translated, so compare against the translated label.
if (!def || m_params_list->GetSelectedTopLevelCategory() == _L("Presets").ToStdString()) {
const auto& full_config = wxGetApp().preset_bundle->full_config();
if (const ConfigDef* config_def = full_config.def(); config_def && config_def->has(opt_key)) {
def = config_def->get(opt_key);
+7 -3
View File
@@ -124,9 +124,13 @@ void FilamentMapManualPanel::OnTimer(wxTimerEvent &)
m_errors->Hide();
m_suggestion_panel->Hide();
} else {
m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."),
invalid_eid == 0 ? _L_CONTEXT("Left", "Nozzle position") : _L_CONTEXT("Right", "Nozzle position"),
invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow")));
// [extruder][nozzle volume type]
static const char* const error_msgs[2][2] = {
{L("Error: Left extruder has no available Standard nozzle, current group result is invalid."),
L("Error: Left extruder has no available High Flow nozzle, current group result is invalid.")},
{L("Error: Right extruder has no available Standard nozzle, current group result is invalid."),
L("Error: Right extruder has no available High Flow nozzle, current group result is invalid.")}};
m_errors->SetLabel(_(error_msgs[invalid_eid == 0 ? 0 : 1][invalid_nozzle == NozzleVolumeType::nvtStandard ? 0 : 1]));
// Re-wrap: wrapping only applies to the label text present when Wrap is called,
// and the label was empty at construction time.
m_errors->Wrap(FromDIP(520));
+21 -19
View File
@@ -1797,7 +1797,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
}
else {*/
//BBS: add only gcode mode
if (m_only_gcode_in_preview) {
// When the file's own settings were loaded into the presets, the bed already is the file's, with the
// model and texture of a user or custom printer too, which the lookup below cannot resolve.
if (m_only_gcode_in_preview && !wxGetApp().plater()->gcode_settings_loaded()) {
Pointfs printable_area;
//BBS: add bed exclude area
Pointfs bed_exclude_area = Pointfs();
@@ -3976,17 +3978,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
imgui.text(_u8L("Current grouping of slice result is not optimal."));
wxString tip;
if (delta_weight_to_best >= 0 && delta_change_to_best >= 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and %2% changes compared to optimal grouping."))
% number_format(delta_weight_to_best)
% delta_change_to_best).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and %2% change compared to optimal grouping.",
"Increase %1%g filament and %2% changes compared to optimal grouping.", delta_change_to_best),
number_format(delta_weight_to_best), delta_change_to_best);
else if (delta_weight_to_best >= 0 && delta_change_to_best < 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to optimal grouping."))
% number_format(delta_weight_to_best)
% std::abs(delta_change_to_best)).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to optimal grouping.",
"Increase %1%g filament and save %2% changes compared to optimal grouping.", std::abs(delta_change_to_best)),
number_format(delta_weight_to_best), std::abs(delta_change_to_best));
else if (delta_weight_to_best < 0 && delta_change_to_best >= 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to optimal grouping."))
% number_format(std::abs(delta_weight_to_best))
% delta_change_to_best).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to optimal grouping.",
"Save %1%g filament and increase %2% changes compared to optimal grouping.", delta_change_to_best),
number_format(std::abs(delta_weight_to_best)), delta_change_to_best);
imgui.text_wrapped(tip, parent_width);
ImGui::PopStyleColor(1);
@@ -3995,17 +3997,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
ImGui::Dummy({window_padding, window_padding});
wxString tip;
if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext >= 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and %2% changes compared to a printer with one nozzle."))
% number_format(delta_weight_to_single_ext)
% delta_change_to_single_ext).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and %2% change compared to a printer with one nozzle.",
"Save %1%g filament and %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
number_format(delta_weight_to_single_ext), delta_change_to_single_ext);
else if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext < 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to a printer with one nozzle."))
% number_format(delta_weight_to_single_ext)
% std::abs(delta_change_to_single_ext)).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to a printer with one nozzle.",
"Save %1%g filament and increase %2% changes compared to a printer with one nozzle.", std::abs(delta_change_to_single_ext)),
number_format(delta_weight_to_single_ext), std::abs(delta_change_to_single_ext));
else if (delta_weight_to_single_ext < 0 && delta_change_to_single_ext >= 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to a printer with one nozzle."))
% number_format(std::abs(delta_weight_to_single_ext))
% delta_change_to_single_ext).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to a printer with one nozzle.",
"Increase %1%g filament and save %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
number_format(std::abs(delta_weight_to_single_ext)), delta_change_to_single_ext);
imgui.text_wrapped(tip, parent_width);
}
+65 -43
View File
@@ -1514,6 +1514,8 @@ wxDEFINE_EVENT(EVT_GLCANVAS_PRINTABLE, SimpleEvent);
const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25;
const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00;
const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25;
// Main-thread only: _render_frame never runs off it, so no atomic is needed.
int GLCanvas3D::s_frames_in_progress = 0;
void GLCanvas3D::load_arrange_settings()
{
@@ -1637,7 +1639,7 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
m_shortcuts_assembly_view = {
{_L("Left mouse button"), _L("Object selection")},
{alt + _L("Left mouse button"), _L("Part selection")},
{"1~16 " + _L("number keys"), _L("Number keys can quickly change the color of objects")},
{_L("1~16 number keys"), _L("Number keys can quickly change the color of objects")},
};
}
@@ -2468,7 +2470,8 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
}
m_in_render = true;
Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; });
++s_frames_in_progress;
Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; --s_frames_in_progress; });
(void)in_render_guard;
if (m_canvas == nullptr)
@@ -2713,15 +2716,31 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
// On Wayland, eglSwapBuffers blocks when the canvas is hidden or
// occluded. Skip the swap to avoid stalling the render loop.
if (m_canvas->IsShownOnScreen()) {
// Code run from inside this frame - a modal dialog a gizmo opened from its ImGui panel, a
// synchronous paint of another canvas - may have made a different surface current. Bind this one
// again before presenting, so the swap is not issued against a window the context no longer
// belongs to: on Mesa/X11 that is a fatal BadMatch, not the no-op it is on other platforms. Draws
// already issued before this point cannot be redirected - only this swap is protected.
if (m_canvas->IsShownOnScreen() && _set_current()) {
m_canvas->SwapBuffers();
m_frame_profiler.mark("swap");
if (!overlay_tick)
m_render_stats.increment_fps_counter();
m_presented_signature = signature;
}
else
else {
if (m_canvas->IsShownOnScreen()) {
// Logged once: if the rebind fails it does so every frame, and a warning per frame would
// bury everything else.
static bool rebind_failure_logged = false;
if (!rebind_failure_logged) {
rebind_failure_logged = true;
BOOST_LOG_TRIVIAL(warning) << "GLCanvas3D: could not rebind the GL context to this canvas before "
"SwapBuffers(); the frame is not presented";
}
}
m_presented_signature.reset();
}
}
// Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the
@@ -3034,10 +3053,9 @@ void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
InstancesToZMap instances_min_z;
for (GLVolume* volume : m_volumes.volumes) {
ModelObject* mo = m_model->objects[volume->object_idx()];
ModelInstance* mi = mo->instances[volume->instance_idx()];
ModelObject* mo = m_model->objects[volume->object_idx()];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -4960,7 +4978,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
// Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
// during the scene manipulation.
if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
if (m_picking_enabled && (m_gizmos.is_allow_multi_select_parts_or_objects() || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
int volume_idx = get_first_hover_volume_idx();
bool already_selected = m_selection.contains_volume(volume_idx);
@@ -4970,7 +4988,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (already_selected && ctrl_down)
m_selection.remove(volume_idx);
else if (alt_down && m_gizmos.is_allow_multi_select_parts_or_objects() && m_selection.get_mode() == Selection::Volume &&
m_selection.get_object_idx() == m_volumes.volumes[volume_idx]->object_idx() &&
m_selection.get_instance_idx() == m_volumes.volumes[volume_idx]->instance_idx()) {
// Alt+click on another part of the selected parts' instance adds it, and on a selected
// part removes it, unless it is the only one.
if (!already_selected)
m_selection.add_volumes(Selection::Volume, {(unsigned int) volume_idx}, false);
else if (curr_idxs.size() > 1)
m_selection.remove_volumes(Selection::Volume, {(unsigned int) volume_idx});
}
else if (alt_down) {
// Select just the clicked part, also when an object is selected.
Selection::EMode mode = Selection::Volume;
if (already_selected) {
std::vector<unsigned int> volume_idxs;
@@ -5502,7 +5531,7 @@ void GLCanvas3D::set_tooltip(const std::string& tooltip)
m_tooltip.set_text(tooltip);
}
void GLCanvas3D::do_move(const std::string& snapshot_type)
void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_move)
{
if (m_model == nullptr)
return;
@@ -5534,10 +5563,18 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// Move instances/volumes
ModelObject* model_object = m_model->objects[object_idx];
if (model_object == nullptr)
if (model_object == nullptr)
continue;
if (selection_mode == Selection::Instance) {
if (force_volume_move || selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
else if (selection_mode == Selection::Instance) {
if (m_canvas_type == GLCanvas3D::ECanvasType::CanvasAssembleView) {
if ((model_object->instances[instance_idx]->get_assemble_offset() - v->get_instance_offset()).norm() > 1e-2) {
model_object->instances[instance_idx]->set_assemble_transformation(v->get_instance_transformation());
@@ -5546,14 +5583,6 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation());
}
}
else if (selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
object_moved = true;
model_object->invalidate_bounding_box();
@@ -5570,13 +5599,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// parts moved. Notifying a plate about an instance that stayed put invalidates its slice
// result, and notifying instance 0 alone left a moved copy unregistered on its new plate.
{
const bool parts_moved = force_volume_move || selection_mode == Selection::Volume;
std::set<std::pair<int, int>> notified;
for (unsigned int i : m_selection.get_volume_idxs()) {
const GLVolume* v = m_volumes.volumes[i];
const int object_idx = v->object_idx();
if (object_idx < 0 || object_idx >= static_cast<int>(m_model->objects.size()))
continue;
const std::pair<int, int> key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx());
const std::pair<int, int> key(object_idx, parts_moved ? -1 : v->instance_idx());
if (notified.insert(key).second)
m_selection.notify_instance_update(key.first, key.second);
}
@@ -5585,9 +5615,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5716,9 +5745,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int> &i : done) {
ModelObject *mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5819,9 +5847,8 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5940,9 +5967,8 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -7948,8 +7974,8 @@ void GLCanvas3D::_picking_pass()
if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) {
const GLVolume* volume = m_volumes.volumes[hit.raycaster_id];
if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
// do not add the volume id if any gizmo is active and CTRL is pressed
if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
// do not add the volume id if CTRL is pressed while a gizmo that does not allow Ctrl+click selection is active
if (m_gizmos.is_allow_multi_select_parts_or_objects() || !wxGetKeyState(WXK_CONTROL))
m_hover_volume_idxs.emplace_back(hit.raycaster_id);
m_gizmos.set_hover_id(-1);
}
@@ -11176,7 +11202,7 @@ void GLCanvas3D::_render_paint_toolbar() const
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f * f_scale);
imgui.tooltip(_L("Shortcut Key ") + std::to_string(i + 1), ImGui::GetFontSize() * 20.0f);
imgui.tooltip(wxString::Format(_L("Shortcut Key %d"), i + 1), ImGui::GetFontSize() * 20.0f);
ImGui::PopStyleVar(2);
}
}
@@ -12067,12 +12093,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
extruder_name += (boost::format(_u8L("Tool %d"))%extruder_id).str();
}
if (error_iter->second.size() == 1) {
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
}
else {
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
}
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
error_iter->second.size()))) %filaments %extruder_name %extruder_name).str();
}
error = ErrorType::SLICING_LIMIT_ERROR;
break;
@@ -12120,11 +12143,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
}
}
std::string extruder_name = extruder_name_list[extruder_id-1];
if (error_iter->second.size() == 1) {
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
} else {
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
}
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
filament_ids.size()))) % filaments % extruder_name % extruder_name).str();
}
if (!text.empty()) {
text += "\n";
@@ -12151,7 +12172,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
warning += std::to_string(filament + 1);
warning += " ";
}
text = (boost::format(_u8L("Filaments %s cannot be printed directly on the surface of this plate.")) % warning).str();
text = (boost::format(into_u8(_L_PLURAL("Filament %s cannot be printed directly on the surface of this plate.",
"Filaments %s cannot be printed directly on the surface of this plate.", conflict_filament.size()))) % warning).str();
error = ErrorType::SLICING_ERROR;
break;
}
+8 -1
View File
@@ -643,6 +643,8 @@ private:
#endif
unsigned int m_last_w, m_last_h;
bool m_in_render;
// How many canvases are inside _render_frame() right now - see is_frame_in_progress().
static int s_frames_in_progress;
wxTimer m_timer;
wxTimer m_timer_set_color;
// Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame.
@@ -974,6 +976,11 @@ public:
void on_change_color_mode(bool is_dark, bool reinit = true);
const bool get_dark_mode_status() { return m_is_dark; }
void set_as_dirty() { m_dirty = true; }
// True while any GLCanvas3D is inside _render_frame(). The other surfaces that share the app's one
// GL context (the UV editor canvas) check this before making themselves current: a frame interrupted
// by another surface finishes drawing into that surface and then presents a window the context no
// longer belongs to, which on Mesa/X11 is a fatal X error (BadMatch) rather than a no-op.
static bool is_frame_in_progress() { return s_frames_in_progress > 0; }
void requires_check_outside_state() { m_requires_check_outside_state = true; }
unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); }
@@ -1298,7 +1305,7 @@ public:
void set_tooltip(const std::string& tooltip);
// the following methods add a snapshot to the undo/redo stack, unless the given string is empty
void do_move(const std::string& snapshot_type);
void do_move(const std::string& snapshot_type, bool force_volume_move = false);
void do_rotate(const std::string& snapshot_type);
void do_scale(const std::string& snapshot_type);
void do_center();
-4
View File
@@ -719,9 +719,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
// does not treat anti-aliased icon edges as window transparency.
glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glEnable(GL_TEXTURE_2D));
glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE));
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id));
GLModel::Geometry init_data;
@@ -753,7 +750,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glDisable(GL_TEXTURE_2D));
glsafe(::glDisable(GL_BLEND));
}
+156 -99
View File
@@ -1109,6 +1109,8 @@ void GUI_App::post_init()
}
this->check_new_version_sf();
if (!cw_showed)
this->check_legacy_user_filament_presets(false);
const auto cloud_provider = get_printer_cloud_provider();
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
// this->check_privacy_version(0);
@@ -1147,14 +1149,8 @@ void GUI_App::post_init()
if(!m_networking_need_update && m_agent) {
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -1812,14 +1808,8 @@ void GUI_App::restart_networking()
init_networking_callbacks();
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -2616,7 +2606,10 @@ void GUI_App::init_download_path()
}
else {
fs::path dp(down_path);
if (!fs::exists(dp)) {
// ORCA: non-throwing probe; ec is deliberately unread, a download_path that cannot be
// probed falls back to the default like a missing one instead of failing startup.
boost::system::error_code ec;
if (!fs::exists(dp, ec)) {
std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data();
app_config->set("download_path", user_down_path);
@@ -3549,6 +3542,10 @@ bool GUI_App::on_init_inner()
}
// BBS if load user preset failed
// Let the libslic3r know the callback, which will translate messages on demand.
// Installed before the presets load so that their load errors are translated too.
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
//if (loaded_preset_result != 0) {
try {
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
@@ -3566,9 +3563,6 @@ bool GUI_App::on_init_inner()
register_win32_device_notification_event();
#endif // WIN32
// Let the libslic3r know the callback, which will translate messages on demand.
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
#if defined(__WXGTK__) && wxHAS_EGL
// Configure GL backend before any wxGLCanvas is created.
// On X11, prefer GLX for maximum driver compatibility.
@@ -3922,81 +3916,74 @@ bool GUI_App::on_init_network(bool try_backup)
if (config_version.empty()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download";
m_networking_need_update = true;
} else {
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
if (!m_device_manager)
m_device_manager = new Slic3r::DeviceManager();
if (!m_user_manager)
m_user_manager = new Slic3r::UserManager();
return false;
}
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
}
}
}
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
}
@@ -4024,6 +4011,11 @@ bool GUI_App::on_init_network(bool try_backup)
else
m_user_manager->set_agent(m_agent);
// A hot reload deletes the agent the task manager was built with. It lives until restart, so repoint it
// even if multi-device has since been turned off.
if (m_task_manager)
m_task_manager->set_agent(m_agent);
if (this->is_enable_multi_machine()) {
if (!m_task_manager) {
m_task_manager = new Slic3r::TaskManager(m_agent);
@@ -4228,8 +4220,10 @@ void GUI_App::switch_printer_agent()
const std::string print_host = config.opt_string("print_host");
if (!print_host.empty()) {
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
if (!sel || sel->get_dev_id() != dev_id)
MachineObject* sel = m_device_manager->get_selected_machine();
const bool access_code_changed = effective_agent_id == MOONRAKER_PRINTER_AGENT_ID && sel &&
sel->get_access_code() != config.opt_string("printhost_apikey");
if (!sel || sel->get_dev_id() != dev_id || access_code_changed)
select_machine(effective_agent_id);
}
}
@@ -4271,6 +4265,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
std::string port = host_cfg->opt_string("printhost_port");
std::string access_code = host_cfg->opt_string("printhost_apikey");
if (access_code.empty() && agent_id != MOONRAKER_PRINTER_AGENT_ID)
access_code = "88888888";
// Generate dev_id from host and port
std::string dev_id = MachineObject::dev_id_from_address(print_host, port);
@@ -4297,11 +4294,6 @@ void GUI_App::select_machine(const std::string& agent_id)
machine.dev_ip = dev_id;
machine.dev_name = dev_id;
machine.printer_type = preset.config.opt_string("printer_model");
auto access_code = preset.config.opt_string("printhost_apikey");
// Orca expect non empty access code
if (access_code.empty()) {
access_code = "88888888";
}
existing = m_device_manager->insert_local_device(
machine, "lan", "free", "", access_code);
@@ -4311,6 +4303,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id;
} else if (agent_id == MOONRAKER_PRINTER_AGENT_ID && existing->printer_agent_id == agent_id &&
(!existing->has_access_right() || existing->get_access_code() != access_code)) {
existing->set_access_code(access_code);
}
existing->local_use_ssl = boost::istarts_with(print_host, "https://");
@@ -4862,12 +4857,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
}
void GUI_App::system_info()
{
//SysInfoDialog dlg;
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
@@ -6822,6 +6811,71 @@ void GUI_App::reload_settings()
}
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
return;
}
wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare "
"for future features. As a result, the presets below no longer show on some printers they were made "
"for. Sorry for the trouble; this is a one-time fix.\n\n"
"Add those printers back to your presets now? Your presets are backed up first. You can also do this "
"later from Help > Migrate Filament Presets.") + "\n";
for (const LegacyUserFilamentPreset &legacy : legacy_presets) {
message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent);
for (const std::string &printer : legacy.printers)
message += "\n " + from_u8(printer);
}
RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION);
if (!by_user)
dlg.ShowCheckBox(_L("Don't remind me again"));
const bool migrate = dlg.ShowModal() == wxID_YES;
if (dlg.IsCheckBoxChecked())
app_config->set_bool("legacy_user_filament_presets_dont_remind", true);
if (!migrate)
return;
const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets);
const std::string &backup = migration.backup;
const std::vector<std::string> &failed = migration.failed;
if (backup.empty()) {
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
return;
}
if (!migration.locked) {
// The lock is also refused for a while after a miss, and when the lock file itself cannot be opened.
show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer "
"instance may be saving presets; try again later from Help > Migrate Filament Presets."));
return;
}
if (failed.size() == legacy_presets.size()) {
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
"A backup of your presets was saved to:\n%1%"), backup);
if (!failed.empty()) {
message += "\n\n" + _L("These presets could not be migrated:");
for (const std::string &name : failed)
message += "\n" + from_u8(name);
}
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
}
//BBS reload when logout
void GUI_App::remove_user_presets()
{
@@ -8588,22 +8642,23 @@ void GUI_App::refresh_plugins()
// and report completion through the notification manager -- the speed dial needs no dialog.
std::thread([]() {
wxString error;
bool unknown_error = false;
try {
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
} catch (const std::exception& ex) {
error = from_u8(ex.what());
} catch (...) {
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
unknown_error = true; // translated in CallAfter: wx translation isn't safe off the UI thread
}
if (!wxTheApp)
return;
wxTheApp->CallAfter([error]() {
wxTheApp->CallAfter([error, unknown_error]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
if (error.IsEmpty())
if (error.IsEmpty() && !unknown_error)
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
@@ -8612,7 +8667,7 @@ void GUI_App::refresh_plugins()
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), unknown_error ? _L("Unknown error") : error)));
});
}).detach();
}
@@ -10078,7 +10133,9 @@ void GUI_App::start_download(std::string url)
}
//lets always init so if the download dest folder was changed, new dest is used
boost::filesystem::path dest_folder(app_config->get("download_path"));
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) {
// ORCA: non-throwing probe, as in init_download_path(); ec is deliberately unread.
boost::system::error_code ec;
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder, ec)) {
std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard.");
BOOST_LOG_TRIVIAL(error) << msg;
show_error(nullptr, msg);
+4 -1
View File
@@ -519,7 +519,6 @@ public:
void check_printer_presets();
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
@@ -568,6 +567,10 @@ public:
void check_update(bool show_tips, int by_user);
void check_new_version(bool show_tips = false, int by_user = 0);
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; by_user also reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
void enter_force_upgrade();
+78 -3
View File
@@ -16,6 +16,7 @@
#include "Selection.hpp"
//BBS: add partplate related logic
#include "PartPlate.hpp"
#include "Gizmos/GLGizmoAlignment.hpp"
#include "Gizmos/GLGizmoEmboss.hpp"
#include "Gizmos/GLGizmoSVG.hpp"
@@ -1672,6 +1673,8 @@ void MenuFactory::create_extra_object_menu()
append_menu_item_merge_parts_to_single_part(&m_object_menu);
// Object Center
append_menu_item_center(&m_object_menu);
// Object Align/Distribute
append_menu_item_align_distribute(&m_object_menu);
// Object Drop
append_menu_item_drop(&m_object_menu);
// Object Split
@@ -1779,6 +1782,7 @@ void MenuFactory::create_text_part_menu()
append_menu_item_fix_through_cgal(menu);
append_menu_item_simplify(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_items_mirror(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
@@ -1811,6 +1815,7 @@ void MenuFactory::create_bbl_part_menu()
append_menu_item_simplify(menu);
append_menu_item_smooth_mesh(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_items_mirror(menu);
wxMenu* split_menu = new wxMenu();
@@ -1952,6 +1957,16 @@ void MenuFactory::create_plate_menu()
},
"", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent);
// fix objects on current plate
append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr,
[]() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent);
// fix objects on all plates
append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr,
[]() { return !plater()->model().objects.empty(); }, m_parent);
// orient objects on current plate
append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"),
[](wxCommandEvent&) {
@@ -2126,6 +2141,7 @@ wxMenu* MenuFactory::multi_selection_menu()
index++;
}
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2134,13 +2150,13 @@ wxMenu* MenuFactory::multi_selection_menu()
append_menu_item_set_printable(menu);
menu->AppendSeparator();
append_menu_item_set_auto_drop(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
menu->AppendSeparator();
append_menu_items_convert_unit(menu);
append_menu_item_replace_all_with_stl(menu);
//BBS
@@ -2151,6 +2167,7 @@ wxMenu* MenuFactory::multi_selection_menu()
}
else {
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2315,6 +2332,64 @@ void MenuFactory::append_menu_item_center(wxMenu* menu)
}, m_parent);
}
void MenuFactory::append_menu_item_align_distribute(wxMenu *menu)
{
using AlignType = GLGizmoAlignment::AlignType;
auto can = [](AlignType type) {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
return type >= AlignType::DISTRIBUTE_X ? alignment_helper.can_distribute(type) : alignment_helper.can_align(type);
};
auto run = [](AlignType type) {
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
if (type >= AlignType::DISTRIBUTE_X)
alignment_helper.distribute_objects(type);
else
alignment_helper.align_objects(type);
};
struct AlignMenuItem
{
wxString label;
AlignType type;
std::string icon;
};
const AlignMenuItem items[] = {
// icons shared with caanvas. BitmapCache converts colors dark to light. so dark variations used for proper colors
{_L("Distribute left-right") + " (X)" , AlignType::DISTRIBUTE_X, "distribute_x_dark"},
{_L("Distribute front-back") + " (Y)" , AlignType::DISTRIBUTE_Y, "distribute_y_dark"},
{_L("Distribute top-bottom") + " (Z)" , AlignType::DISTRIBUTE_Z, "distribute_z_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align left") + " (-X)", AlignType::X_MIN, "align_x_min_dark"},
{_L("Align left-right center") + " (X)" , AlignType::CENTER_X, "align_x_center_dark"},
{_L("Align right") + " (+X)", AlignType::X_MAX, "align_x_max_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align front") + " (-Y)", AlignType::Y_MIN, "align_y_min_dark"},
{_L("Align front-back center") + " (Y)" , AlignType::CENTER_Y, "align_y_center_dark"},
{_L("Align back") + " (+Y)", AlignType::Y_MAX, "align_y_max_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align bottom") + " (-Z)", AlignType::Z_MIN, "align_z_min_dark"},
{_L("Align top-bottom center") + " (Z)" , AlignType::CENTER_Z, "align_z_center_dark"},
{_L("Align top") + " (+Z)", AlignType::Z_MAX, "align_z_max_dark"},
};
wxMenu* align_distribute_menu = new wxMenu();
for (const AlignMenuItem &item : items) {
const AlignType type = item.type;
if (type == AlignType::NONE)
align_distribute_menu->AppendSeparator();
else
append_menu_item(
align_distribute_menu, wxID_ANY, item.label, "", [run, type](wxCommandEvent &) { run(type); }, item.icon, nullptr,
[can, type]() { return can(type); }, m_parent);
}
// Anything that can be distributed can also be aligned, and aligning in X has no extra condition.
append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "",
[can]() { return can(AlignType::X_MIN); }, m_parent);
}
void MenuFactory::append_menu_item_drop(wxMenu* menu)
{
append_menu_item(menu, wxID_ANY, _L("Drop") , "",
+1
View File
@@ -175,6 +175,7 @@ private:
void append_menu_item_merge_to_single_object(wxMenu* menu);
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
void append_menu_items_mirror(wxMenu *menu);
void append_menu_item_align_distribute(wxMenu *menu);
void append_menu_item_precise_seam_submenu(wxMenu* menu);
void append_menu_item_invalidate_cut_info(wxMenu *menu);
void append_menu_item_edit_text(wxMenu *menu);
+70 -68
View File
@@ -3234,8 +3234,8 @@ void ObjectList::merge(bool to_multipart_object)
new_object->printable = false;
new_object->instances[0]->printable = false;
}
if (object->instances[0]->auto_drop == false) {
new_object->instances[0]->auto_drop = false;
if (object->auto_drop == false) {
new_object->auto_drop = false;
}
// merge layers
@@ -6297,30 +6297,44 @@ void ObjectList::rename_item()
}
void ObjectList::fix_through_cgal()
{
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
fix_through_cgal(obj_idxs, vol_idxs);
}
void ObjectList::fix_all_through_cgal(bool all_plates)
{
std::vector<int> obj_idxs;
if (all_plates) {
for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx)
obj_idxs.push_back(obj_idx);
} else {
for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate())
obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin()));
std::sort(obj_idxs.begin(), obj_idxs.end());
obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end());
}
fix_through_cgal(obj_idxs, {});
}
void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs)
{
// Do not fix anything when a gizmo is open. There might be issues with updates
// and what is worse, the snapshot time would refer to the internal stack.
if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
return;
// model_name
std::vector<std::string> succes_models;
// model_name failing reason
std::vector<std::pair<std::string, std::string>> failed_models;
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
size_t unchanged_models = 0;
bool painting_removed = false;
std::vector<std::string> model_names;
// clear selections from the non-broken models if any exists
// and than fill names of models to repairing
if (vol_idxs.empty()) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(obj_idxs.size())-1; i >= 0; --i)
if (object(obj_idxs[i])->get_repaired_errors_count() == 0)
obj_idxs.erase(obj_idxs.begin()+i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int obj_idx : obj_idxs)
if (object(obj_idx))
model_names.push_back(object(obj_idx)->name);
@@ -6328,31 +6342,26 @@ void ObjectList::fix_through_cgal()
else {
ModelObject* obj = object(obj_idxs.front());
if (obj) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(vol_idxs.size()) - 1; i >= 0; --i)
if (obj->get_repaired_errors_count(vol_idxs[i]) == 0)
vol_idxs.erase(vol_idxs.begin() + i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int vol_idx : vol_idxs)
model_names.push_back(obj->volumes[vol_idx]->name);
}
}
auto plater = wxGetApp().plater();
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx,
int model_idx,
ProgressDialog& progress_dlg,
std::vector<std::string>& succes_models,
std::vector<std::pair<std::string, std::string>>& failed_models)
auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg)
{
if (!object(obj_idx))
ModelObject *mo = object(obj_idx);
if (!mo)
return false;
const std::string& model_name = model_names[model_idx];
wxString msg = _L("Repairing model object");
if (model_names.size() == 1)
msg += ": " + from_u8(model_name) + "\n";
else if (model_names.size() > 10)
msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n";
else {
msg += ":\n";
for (int i = 0; i < int(model_names.size()); ++i)
@@ -6360,33 +6369,29 @@ void ObjectList::fix_through_cgal()
msg += "\n";
}
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
if (!keep_painting) {
plater->clear_before_change_mesh(obj_idx);
}
const size_t volumes_before = object(obj_idx)->volumes.size();
auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); };
const auto painted_before = count_painted();
std::string res;
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
bool modified = false;
if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified))
return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed();
plater->changed_mesh(obj_idx);
const size_t volumes_after = object(obj_idx)->volumes.size();
if (volumes_after != volumes_before)
add_volumes_to_object_in_list(obj_idx);
if (modified) {
painting_removed = painting_removed || count_painted() < painted_before;
mo->ensure_on_bed();
plater->changed_mesh(obj_idx);
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
}
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
if (res.empty())
if (!res.empty())
failed_models.push_back({ model_name, res });
else if (modified)
succes_models.push_back(model_name);
else
failed_models.push_back({ model_name, res });
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
++unchanged_models;
return true;
};
@@ -6399,11 +6404,7 @@ void ObjectList::fix_through_cgal()
if (vol_idxs.empty()) {
int vol_idx{ -1 };
for (int obj_idx : obj_idxs) {
#if !FIX_THROUGH_CGAL_ALWAYS
if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0)
continue;
#endif // FIX_THROUGH_CGAL_ALWAYS
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
break;
model_idx++;
}
@@ -6411,7 +6412,7 @@ void ObjectList::fix_through_cgal()
else {
int obj_idx{ obj_idxs.front() };
for (int vol_idx : vol_idxs) {
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
break;
model_idx++;
}
@@ -6419,20 +6420,29 @@ void ObjectList::fix_through_cgal()
// Close the progress dialog
progress_dlg.Update(100, "");
if (painting_removed)
plater->notify_painting_removed();
// Show info notification
wxString msg;
std::vector<wxString> blocks;
wxString bullet_suf = "\n - ";
if (!succes_models.empty()) {
msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
for (auto& model : succes_models)
msg += bullet_suf + from_u8(model);
msg += "\n\n";
block += bullet_suf + from_u8(model);
blocks.push_back(block);
}
if (!failed_models.empty()) {
msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n";
wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":";
for (auto& model : failed_models)
msg += bullet_suf + from_u8(model.first) + ": " + _(model.second);
block += bullet_suf + from_u8(model.first) + ": " + _(model.second);
blocks.push_back(block);
}
if (unchanged_models > 0)
blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models));
wxString msg;
for (const wxString &block : blocks)
msg += (msg.IsEmpty() ? "" : "\n\n") + block;
if (msg.IsEmpty())
msg = _L("Repairing was canceled");
plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg));
@@ -7088,18 +7098,10 @@ void ObjectList::toggle_auto_drop()
obj_idxs.emplace_back(static_cast<size_t>(obj_idx));
ItemType type = m_objects_model->GetItemType(item);
// set auto_drop value for selected instance/instances in object
if (type == itInstance) {
int inst_idx = m_objects_model->GetInstanceIdByItem(item);
obj->instances[inst_idx]->auto_drop = !current_auto_drop;
} else {
for (auto inst : obj->instances)
inst->auto_drop = !current_auto_drop;
if (current_auto_drop == false)
// auto_drop is shared by all instances of an object.
obj->auto_drop = !current_auto_drop;
if (current_auto_drop == false)
obj->ensure_on_bed();
}
}
sort(obj_idxs.begin(), obj_idxs.end());
+3 -3
View File
@@ -47,9 +47,6 @@ typedef double coordf_t;
typedef std::pair<coordf_t, coordf_t> t_layer_height_range;
typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh.
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI {
enum class Shortcut : uint8_t;
@@ -463,6 +460,9 @@ public:
void split_instances();
void rename_item();
void fix_through_cgal();
// Fixes every object on the selected plate, or on all plates.
void fix_all_through_cgal(bool all_plates);
void fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs);
void simplify();
void smooth_mesh();
void update_item_error_icon(const int obj_idx, int vol_idx) const ;
+1 -1
View File
@@ -386,7 +386,7 @@ void GridCellFilamentsRenderer::Draw(wxGrid &grid, wxGridCellAttr &attr, wxDC &d
dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y));
}
else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){
dc.DrawText("Default", wxPoint(rect.x + offset_x, rect.y + offset_y));
dc.DrawText(_L("Default"), wxPoint(rect.x + offset_x, rect.y + offset_y));
}
text_rect.x += bitmap_width + grid_cell_border_width * 2;
+717
View File
@@ -0,0 +1,717 @@
#include "GLGizmoAlignment.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Selection.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas)
{
}
bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent)
{
if (!can_align(type, align_parent)) {
return false;
}
switch (type) {
case AlignType::CENTER_X: return align_to_center(type, align_parent);
case AlignType::CENTER_Y: return align_to_center(type, align_parent);
case AlignType::CENTER_Z: return align_to_center(type, align_parent);
case AlignType::Y_MAX: return align_to_y_max(align_parent);
case AlignType::Y_MIN: return align_to_y_min(align_parent);
case AlignType::X_MAX: return align_to_x_max(align_parent);
case AlignType::X_MIN: return align_to_x_min(align_parent);
case AlignType::Z_MAX: return align_to_z_max(align_parent);
case AlignType::Z_MIN: return align_to_z_min(align_parent);
default:
return false;
}
}
bool GLGizmoAlignment::distribute_objects(AlignType type)
{
if (!can_distribute(type)) {
return false;
}
switch (type) {
case AlignType::DISTRIBUTE_X:
return distribute_x();
case AlignType::DISTRIBUTE_Y:
return distribute_y();
case AlignType::DISTRIBUTE_Z:
return distribute_z();
default:
return false;
}
}
bool GLGizmoAlignment::align_to_y_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.y(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.y() = target - current_max;
}, "Align Y Max", align_parent
);
}
bool GLGizmoAlignment::align_to_y_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.y(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.y() = target - current_min;
}, "Align Y Min", align_parent
);
}
bool GLGizmoAlignment::align_to_x_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.x(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.x() = target - current_max;
}, "Align X Max", align_parent
);
}
bool GLGizmoAlignment::align_to_x_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.x(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.x() = target - current_min;
}, "Align X Min", align_parent
);
}
bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
{
std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z");
std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center");
double selection_center = 0.f;
if (align_parent) {
BoundingBoxf3 parent_box = get_parent_box();
selection_center = get_current_coord(operation_name, parent_box);
// Handle multiple volume selection
if (is_parts_selection(true)) {
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
BoundingBoxf3 selection_bbox = get_items_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Move the selected volumes together so that their collective bounding box is centered in the parent
for (const GLVolume *volume : get_selected_part_volumes()) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple objects or parts selection
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Multiple objects: use selection bounding box for alignment
for (const auto &obj : objects) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation();
return true;
}
// Handle the parts of a single instance
if (is_parts_selection(false)) {
if (!take_snapshot("Align Parts Center " + type_str)){
return false;
}
get_selection().setup_cache();
BoundingBoxf3 selection_bbox = get_items_box();
selection_center = get_current_coord(operation_name, selection_bbox);
// Move each part to align to the center of their collective bounding box
for (const GLVolume *volume : get_selected_part_volumes()) {
auto temp_box = volume->transformed_convex_hull_bounding_box();
double current = get_current_coord(operation_name, temp_box);
double offset = selection_center - current;
Vec3d displacement = generate_displacement(operation_name, offset);
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple instances
selection_center = get_current_coord(operation_name, big_bb);
if (!take_snapshot("Align Objects Center" + type_str)) {
return false;
}
get_selection().setup_cache();
// Multiple objects: use selection bounding box for alignment
for (const auto& obj : objects) {
BoundingBoxf3 temp_bbox = obj.bbox;
double current_coord = get_current_coord(operation_name, temp_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation();
return true;
}
bool GLGizmoAlignment::align_to_z_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.z(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.z() = target - current_max;
}, "Align Z Max", align_parent
);
}
bool GLGizmoAlignment::align_to_z_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.z(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.z() = target - current_min;
}, "Align Z Min", align_parent
);
}
bool GLGizmoAlignment::distribute_y()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.y(); },
1,
"Distribute Y"
);
}
bool GLGizmoAlignment::distribute_x()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.x(); },
0,
"Distribute X"
);
}
bool GLGizmoAlignment::distribute_z()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.z(); },
2,
"Distribute Z"
);
}
template<typename GetCoordFunc, typename SetCoordFunc>
bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent)
{
if (align_parent) {
// Find reference coordinate
BoundingBoxf3 parent_box = get_parent_box();
double reference_coord = get_current_coord(operation_name, parent_box);
// Handle parts selection differently
if (is_parts_selection(true)) {
if (!take_snapshot("align to parent node " + operation_name)) {
return false;
}
get_selection().setup_cache();
const std::vector<const GLVolume *> volumes = get_selected_part_volumes();
if (volumes.empty())
return false;
BoundingBoxf3 selection_bbox = get_items_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const GLVolume *volume : volumes) {
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot("align to parent node " + operation_name)) { return false; }
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const auto &obj : objects) {
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation();
return true;
}
// Handle parts selection differently
if (is_parts_selection(false)) {
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
if (volumes.empty()) return false;
// Find reference coordinate
double reference_coord = 0.0;
bool first = true;
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
coord = bbox.min.z();
}
if (first) {
reference_coord = coord;
first = false;
} else {
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
}
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
}
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = reference_coord - current_coord;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = reference_coord - current_coord;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = reference_coord - current_coord;
}
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
double reference_coord = get_coord(objects[0]);
for (const auto& obj : objects) {
double coord = get_coord(obj);
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
for (const auto& obj : objects) {
double current_coord = get_coord(obj);
Vec3d displacement = Vec3d::Zero();
set_coord(displacement, reference_coord, current_coord);
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation();
return true;
}
template<typename GetCoordFunc>
bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name)
{
if (is_parts_selection(false)) {
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
if (volumes.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
struct VolEntry { const GLVolume* v; double coord; };
std::vector<VolEntry> entries;
entries.reserve(volumes.size());
for (const GLVolume* v : volumes) {
BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
Vec3d c = bbox.center();
double coord = (axis == 0 ? c.x() : axis == 1 ? c.y() : c.z());
entries.push_back({v, coord});
}
std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; });
BoundingBoxf3 selection_bbox = get_items_box();
BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box();
BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box();
auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); };
double left_width = axis_extent(left_bbox);
double right_width = axis_extent(right_bbox);
double bbox_min = (axis == 0 ? selection_bbox.min.x() : axis == 1 ? selection_bbox.min.y() : selection_bbox.min.z());
double bbox_max = (axis == 0 ? selection_bbox.max.x() : axis == 1 ? selection_bbox.max.y() : selection_bbox.max.z());
double min_center = bbox_min + left_width * 0.5;
double max_center = bbox_max - right_width * 0.5;
double total_distance = max_center - min_center;
double interval = (entries.size() > 1) ? total_distance / (entries.size() - 1) : 0;
for (size_t i = 1; i < entries.size() - 1; ++i) {
double target_coord = min_center + i * interval;
double current_coord = entries[i].coord;
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
const GLVolume *v = entries[i].v;
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
std::sort(objects.begin(), objects.end(),
[&get_coord](const ObjectInfo& a, const ObjectInfo& b) {
return get_coord(a) < get_coord(b);
});
double min_coord = get_coord(objects.front());
double max_coord = get_coord(objects.back());
double total_distance = max_coord - min_coord;
double interval = (objects.size() > 1) ? total_distance / (objects.size() - 1) : 0;
for (size_t i = 1; i < objects.size() - 1; ++i) {
double target_coord = min_coord + i * interval;
double current_coord = get_coord(objects[i]);
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
apply_transformation(objects[i].object_idx, objects[i].instance_idx, displacement);
}
}
finish_operation();
return true;
}
bool GLGizmoAlignment::can_align(AlignType type, bool align_parent) const
{
return get_items_count(type, align_parent) >= (align_parent ? 1 : 2);
}
bool GLGizmoAlignment::can_distribute(AlignType type) const
{
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z) &&
get_items_count(type, false) >= 3;
}
double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox)
{
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
} else if (operation_name.find("X Center") != std::string::npos) {
current_coord = bbox.center().x();
} else if (operation_name.find("Y Center") != std::string::npos) {
current_coord = bbox.center().y();
} else if (operation_name.find("Z Center") != std::string::npos) {
current_coord = bbox.center().z();
}
return current_coord;
}
Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double offset) {
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = offset;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = offset;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = offset;
}
return displacement;
}
std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
{
std::vector<ObjectInfo> objects;
Selection& selection = get_selection();
if (selection.is_empty()) return objects;
const auto& content = selection.get_content();
for (const auto& obj_it : content) {
for (const auto& inst_idx : obj_it.second) {
int obj_idx = obj_it.first;
if (obj_idx >= 0 && obj_idx < selection.get_model()->objects.size()) {
ModelObject* object = selection.get_model()->objects[obj_idx];
if (inst_idx >= 0 && inst_idx < object->instances.size()) {
BoundingBoxf3 bbox = object->instance_bounding_box(inst_idx);
big_bb.merge(bbox);
objects.emplace_back(obj_idx, inst_idx, bbox);
}
}
}
}
return objects;
}
bool GLGizmoAlignment::is_parts_selection(bool align_parent) const
{
const Selection &selection = get_selection();
const auto &content = selection.get_content();
if (content.size() != 1 || content.begin()->first >= 1000 || content.begin()->second.size() != 1)
return false;
return !align_parent || selection.get_mode() == Selection::Volume;
}
std::vector<const GLVolume *> GLGizmoAlignment::get_selected_part_volumes() const
{
std::vector<const GLVolume *> volumes;
if (!is_parts_selection(false))
return volumes;
const Selection &selection = get_selection();
const ModelObject *object = selection.get_model()->objects[selection.get_object_idx()];
if (object->is_cut())
return volumes;
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume *volume = selection.get_volume(idx);
if (volume->volume_idx() < 0)
continue;
// Moving a whole instance's parts leaves its modifiers, negative volumes and support blockers in place,
// as dragging a part does.
if (selection.get_mode() == Selection::Volume || object->volumes[volume->volume_idx()]->is_model_part())
volumes.push_back(volume);
}
return volumes;
}
BoundingBoxf3 GLGizmoAlignment::get_items_box() const
{
BoundingBoxf3 box;
if (is_parts_selection(false)) {
for (const GLVolume *volume : get_selected_part_volumes())
box.merge(volume->transformed_convex_hull_bounding_box());
} else
get_selected_objects_info(box);
return box;
}
BoundingBoxf3 GLGizmoAlignment::get_parent_box() const
{
const Selection &selection = get_selection();
if (is_parts_selection(true))
return selection.get_model()->objects[selection.get_object_idx()]->instance_convex_hull_bounding_box(selection.get_instance_idx());
// can_align() requires every selected instance on one plate, so the first one gives the plate.
BoundingBoxf3 box;
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
if (obj_idx >= 1000 || inst_idxs.empty())
continue;
PartPlate *plate = plate_list.get_plate(plate_list.find_instance(obj_idx, *inst_idxs.begin()));
if (plate == nullptr)
break;
BoundingBoxf3 instances_box;
get_selected_objects_info(instances_box);
box = plate->get_plate_box();
box.max.z() = instances_box.max.z();
// Keep the aligned objects off the plate edge.
box.min += Vec3d(0.1, 0.1, 0.1);
box.max -= Vec3d(0.1, 0.1, 0.1);
break;
}
return box;
}
Selection& GLGizmoAlignment::get_selection() const
{
return m_canvas.get_selection();
}
bool GLGizmoAlignment::take_snapshot(const std::string& name)
{
wxGetApp().plater()->take_snapshot(name);
return true;
}
void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement)
{
get_selection().translate(obj_idx, inst_idx, displacement);
}
void GLGizmoAlignment::finish_operation(bool force_volume_move)
{
// A part move changes the ModelVolume shared by every copy of the object, so the other copies' volumes must show
// it too, or do_move() would write their stale transformation back to the model.
if (force_volume_move)
get_selection().synchronize_unselected_volumes();
// The operation already took its snapshot.
m_canvas.do_move("", force_volume_move);
}
size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const
{
auto cannot_move = [why_not](const std::string &reason) {
if (why_not != nullptr)
*why_not = reason;
return size_t(0);
};
if (is_parts_selection(align_parent)) {
const size_t count = get_selected_part_volumes().size();
return count > 0 ? count : cannot_move(_u8L("Parts of a cut object cannot be moved"));
}
// Instances are moved through their instance offsets, which do_move() commits only from an Instance selection.
const Selection &selection = get_selection();
if (selection.get_mode() != Selection::Instance)
return 0;
const bool is_z = type == AlignType::Z_MIN || type == AlignType::CENTER_Z || type == AlignType::Z_MAX ||
type == AlignType::DISTRIBUTE_Z;
// The plate gives no Z target, and aligning to its bottom would duplicate Drop.
if (is_z && align_parent)
return cannot_move(_u8L("Aligning to the plate is available in X and Y only"));
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
int plate_idx = -1;
size_t count = 0;
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
if (obj_idx >= 1000)
continue;
// Auto-drop would put a raised object back on the bed and leave a lowered one sunk.
if (is_z && selection.get_model()->objects[obj_idx]->auto_drop)
return cannot_move(_u8L("Turn off Auto-Drop for the selected objects to move them in Z"));
if (align_parent) {
// The plate holding the selection is the target, so it must hold all of it.
for (int inst_idx : inst_idxs) {
const int idx = plate_list.find_instance(obj_idx, inst_idx);
if (idx == -1 || (plate_idx != -1 && idx != plate_idx))
return cannot_move(_u8L("All selected objects must be on the same plate"));
plate_idx = idx;
}
}
count += inst_idxs.size();
}
return count;
}
} // namespace GUI
} // namespace Slic3r
+102
View File
@@ -0,0 +1,102 @@
#ifndef slic3r_GLGizmoAlignment_hpp_
#define slic3r_GLGizmoAlignment_hpp_
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "slic3r/GUI/Selection.hpp"
#include <cstddef>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
class GLCanvas3D;
class GLGizmoAlignment
{
public:
enum class AlignType {
NONE = -1,
CENTER_X,
CENTER_Y,
CENTER_Z,
Y_MAX,
Y_MIN,
X_MAX,
X_MIN,
Z_MAX,
Z_MIN,
DISTRIBUTE_X,
DISTRIBUTE_Y,
DISTRIBUTE_Z
};
struct ObjectInfo {
int object_idx;
int instance_idx;
BoundingBoxf3 bbox;
Vec3d center;
ObjectInfo(int obj_idx, int inst_idx, const BoundingBoxf3& bb)
: object_idx(obj_idx), instance_idx(inst_idx), bbox(bb), center(bb.center()) {}
};
explicit GLGizmoAlignment(GLCanvas3D& canvas);
~GLGizmoAlignment() = default;
bool align_objects(AlignType type,bool align_parent = false);
bool distribute_objects(AlignType type);
bool align_to_center(AlignType type,bool align_parent = false);
bool align_to_y_max(bool align_parent = false);
bool align_to_y_min(bool align_parent = false);
bool align_to_x_max(bool align_parent = false);
bool align_to_x_min(bool align_parent = false);
bool align_to_z_max(bool align_parent = false);
bool align_to_z_min(bool align_parent = false);
bool distribute_x();
bool distribute_y();
bool distribute_z();
bool can_align(AlignType type, bool align_parent = false) const;
bool can_distribute(AlignType type) const;
std::vector<ObjectInfo> get_selected_objects_info(BoundingBoxf3 &big_bb) const;
double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox);
Vec3d generate_displacement(std::string operation_name, double offset);
// A parts selection (one instance of one object) aligns the parts inside it; anything else aligns whole instances.
// With align_parent, only a Volume-mode selection counts as parts: a whole instance aligns to its plate instead.
bool is_parts_selection(bool align_parent) const;
// The volumes a parts operation moves: the selected volumes, or only the model parts when a whole instance is
// selected. Empty for a cut object, whose parts must keep their place.
std::vector<const GLVolume *> get_selected_part_volumes() const;
// World bounding box of the items an operation moves, the reference of every non-parent operation.
BoundingBoxf3 get_items_box() const;
// World bounding box aligned to with align_parent: the instance of a parts selection, or the plate holding the
// selected instances, up to their top.
BoundingBoxf3 get_parent_box() const;
// Number of items the operation would move, or 0 when it cannot move them; why_not then gets the reason, if any.
size_t get_items_count(AlignType type, bool align_parent, std::string *why_not = nullptr) const;
private:
GLCanvas3D& m_canvas;
template<typename GetCoordFunc, typename SetCoordFunc>
bool align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name, bool align_parent = false);
template<typename GetCoordFunc>
bool distribute_objects_generic(GetCoordFunc get_coord, int axis,
const std::string& operation_name);
Selection& get_selection() const;
bool take_snapshot(const std::string& name);
void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement);
void finish_operation(bool force_volume_move = false);
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoAlignment_hpp_
+2 -1
View File
@@ -818,7 +818,8 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi
}
if (has_invalid_ears) {
wxString out = _L("Warning") + ": " + std::to_string(m_single_brim.size()) + " " + _L("invalid brim ears");
const int n_invalid = int(m_single_brim.size());
wxString out = _L("Warning") + ": " + wxString::Format(_L_PLURAL("%d invalid brim ear", "%d invalid brim ears", n_invalid), n_invalid);
m_imgui->text_wrapped(out, parent_width);
}
+7 -4
View File
@@ -281,7 +281,6 @@ GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename,
{"Flap Angle" , _u8L("Flap Angle")},
{"Groove Angle" , _u8L("Groove Angle")},
{"Cut position" , _u8L("Cut position")}, // ORCA
{"Build Volume" , _u8L("Build Volume")}, // ORCA
{"Multiple" , _u8L("Multiple")}, // ORCA
{"Count" , _u8L("Count")}, // ORCA
{"Gap" , _u8L("Gap")}, // ORCA
@@ -2531,7 +2530,7 @@ void GLGizmoCut3D::render_build_size()
Vec3d tbb_sz = m_transformed_bounding_box.size() * koef; // ORCA
ImGui::AlignTextToFramePadding();
m_imgui->text(_L("Build Volume"));
m_imgui->text(_L("Size"));
ImGui::SameLine(m_label_width);
ImGui::Text("%.2f x %.2f x %.2f %s", tbb_sz.x(), tbb_sz.y(), tbb_sz.z(), unit_str.ToUTF8().data()); // ORCA use regular text color and simplify format
}
@@ -3555,10 +3554,14 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects)
wxString names = from_u8(err_objects_names[0]);
for (size_t i = 1; i < err_objects_names.size(); i++)
names += ", " + from_u8(err_objects_names[i]);
WarningDialog(wxGetApp().plater(), format_wxstr(_L("Objects(%1%) have duplicated connectors. "
WarningDialog(wxGetApp().plater(), format_wxstr(_L_PLURAL("Object (%1%) has duplicated connectors. "
"Some connectors may be missing in slicing result.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you."), names)).ShowModal();
"Thank you.",
"Objects (%1%) have duplicated connectors. "
"Some connectors may be missing in slicing result.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you.", unsigned(err_objects_names.size())), names)).ShowModal();
}
void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id)
+2 -2
View File
@@ -898,9 +898,9 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit)
// Not known situation when could happend this is only for sure
if (!m_is_unknown_font && !m_style_manager.is_active_font())
create_notification_not_valid_font("No active font in style. Select correct one.");
create_notification_not_valid_font(_u8L("No active font in style. Select correct one."));
else if (!m_is_unknown_font && !m_style_manager.get_wx_font().IsOk())
create_notification_not_valid_font("WxFont is not loaded properly.");
create_notification_not_valid_font(_u8L("WxFont is not loaded properly."));
double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor();
+2 -1
View File
@@ -644,7 +644,8 @@ void GLGizmoFdmSupports::update_from_model_object(bool first_update)
const TriangleMesh* mesh = &mv->mesh();
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false);
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
m_triangle_selectors.back()->request_update_render_data();
//BBS: add timestamp logic
+2 -1
View File
@@ -397,7 +397,8 @@ void GLGizmoFuzzySkin::update_from_model_object(bool first_update)
const TriangleMesh* mesh = &mv->mesh();
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false);
// Fuzzy skin paints a single state (FUZZY_SKIN): a higher state in the file is reset to NONE.
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false, EnforcerBlockerType::FUZZY_SKIN);
m_triangle_selectors.back()->request_update_render_data();
}
}
+9 -9
View File
@@ -1912,13 +1912,13 @@ void GLGizmoMeasure::show_selection_ui()
float selection_cap_length = 0;
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1" + _u8L(" (Moving)")).c_str()).x * 1.2;
selection_cap_length = std::max(ImGui::CalcTextSize(_u8L("Face 1 (Fixed)").c_str()).x, ImGui::CalcTextSize(_u8L("Face 2 (Moving)").c_str()).x) * 1.2;
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1" + _u8L(" (Moving)")).c_str()).x * 1.2;
selection_cap_length = std::max(ImGui::CalcTextSize(_u8L("Point 1 (Fixed)").c_str()).x, ImGui::CalcTextSize(_u8L("Point 2 (Moving)").c_str()).x) * 1.2;
}
}
else {
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1").c_str()).x * 1.2;
selection_cap_length = ImGui::CalcTextSize(_u8L("Selection 1").c_str()).x * 1.2;
}
auto feature_first_text = format_item_text(m_selected_features.first);
const float feature_first_text_length = ImGui::CalcTextSize((_u8L(feature_first_text)).c_str()).x;
@@ -1934,13 +1934,13 @@ void GLGizmoMeasure::show_selection_ui()
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR));
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
m_imgui->text(_u8L("Face") + " 1" + _u8L(" (Fixed)"));
m_imgui->text(_u8L("Face 1 (Fixed)"));
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
m_imgui->text(_u8L("Point") + " 1" + _u8L(" (Fixed)"));
m_imgui->text(_u8L("Point 1 (Fixed)"));
}
}
else {
m_imgui->text(_u8L("Selection") + " 1");
m_imgui->text(_u8L("Selection 1"));
}
ImGui::SameLine(selection_cap_length + space_size);
ImGui::PushItemWidth(feature_first_text_length);
@@ -1960,12 +1960,12 @@ void GLGizmoMeasure::show_selection_ui()
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR));
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
m_imgui->text(_u8L("Face") + " 2"+ _u8L(" (Moving)"));
m_imgui->text(_u8L("Face 2 (Moving)"));
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
m_imgui->text(_u8L("Point") + " 2"+ _u8L(" (Moving)"));
m_imgui->text(_u8L("Point 2 (Moving)"));
}
} else {
m_imgui->text(_u8L("Selection") + " 2");
m_imgui->text(_u8L("Selection 2"));
}
ImGui::SameLine(selection_cap_length + space_size);
ImGui::PushItemWidth(feature_second_text_length);
+4 -3
View File
@@ -248,7 +248,7 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
ImGui::CalcTextSize(_u8L("Subtract from").c_str()).x,
ImGui::CalcTextSize(_u8L("Subtract with").c_str()).x});
const int select_btn_length = 2 * ImGui::GetStyle().FramePadding.x + std::max(ImGui::CalcTextSize(("1 " + _u8L("selected")).c_str()).x, ImGui::CalcTextSize(_u8L("Select").c_str()).x);
const int select_btn_length = 2 * ImGui::GetStyle().FramePadding.x + std::max(ImGui::CalcTextSize(_u8L("1 selected").c_str()).x, ImGui::CalcTextSize(_u8L("Select").c_str()).x);
auto selectable = [](const std::string& label, bool selected, const ImVec2& size_arg) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0,0 });
@@ -323,7 +323,8 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
std::string cap_str1 = m_operation_mode != MeshBooleanOperation::Difference ? _u8L("Part 1") : _u8L("Subtract from");
m_imgui->text(cap_str1);
ImGui::SameLine(max_cap_length);
std::string select_src_str = m_src.mv ? "1 " + _u8L("selected") : _u8L("Select");
// TRN Mesh Boolean button label once one volume is picked.
std::string select_src_str = m_src.mv ? _u8L("1 selected") : _u8L("Select");
select_src_str += "##select_source_volume";
ImGui::PushItemWidth(select_btn_length);
if (selectable(select_src_str, m_selecting_state == MeshBooleanSelectingState::SelectSource, ImVec2(select_btn_length, 0)))
@@ -351,7 +352,7 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
std::string cap_str2 = m_operation_mode != MeshBooleanOperation::Difference ? _u8L("Part 2") : _u8L("Subtract with");
m_imgui->text(cap_str2);
ImGui::SameLine(max_cap_length);
std::string select_tool_str = m_tool.mv ? "1 " + _u8L("selected") : _u8L("Select");
std::string select_tool_str = m_tool.mv ? _u8L("1 selected") : _u8L("Select");
select_tool_str += "##select_tool_volume";
ImGui::PushItemWidth(select_btn_length);
if (selectable(select_tool_str, m_selecting_state == MeshBooleanSelectingState::SelectTool, ImVec2(select_btn_length, 0)))
@@ -56,8 +56,12 @@ static inline void show_notification_extruders_limit_exceeded()
.plater()
->get_notification_manager()
->push_notification(NotificationType::MmSegmentationExceededExtrudersLimit, NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
GUI::format(_L("Filament count exceeds the maximum number that painting tool supports. Only the "
"first %1% filaments will be available in painting tool."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT));
GUI::format(_L_PLURAL("Filament count exceeds the maximum number that painting tool supports. Only the "
"first %1% filament will be available in painting tool.",
"Filament count exceeds the maximum number that painting tool supports. Only the "
"first %1% filaments will be available in painting tool.",
unsigned(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)),
GLGizmoMmuSegmentation::EXTRUDERS_LIMIT));
}
void GLGizmoMmuSegmentation::on_opening()
@@ -459,7 +463,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
m_selected_extruder_idx = extruder_idx;
}
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered()) m_imgui->tooltip(wxString::Format(_L("Shortcut Key %d"), extruder_idx + 1), max_tooltip_width);
}
// ORCA: Remap filaments section (Border only, Title in border).
// Styled as a panel for visual grouping.
+96
View File
@@ -31,6 +31,11 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoAlignment.hpp"
#include "libslic3r/BoundingBox.hpp"
#include <algorithm>
#include <cstddef>
#include <vector>
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r {
@@ -109,6 +114,8 @@ void GLGizmoMove3D::on_set_state() {
m_last_selected_volume_idx = -1;
change_cs_by_selection();
}
if (m_object_manipulation)
m_object_manipulation->reset_align_state();
}
void GLGizmoMove3D::on_start_dragging()
@@ -157,6 +164,8 @@ void GLGizmoMove3D::on_render()
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
glsafe(::glEnable(GL_DEPTH_TEST));
render_align_preview();
const auto &[box, box_trafo] = selection.get_bounding_box_in_current_reference_system();
m_bounding_box = box;
m_center = box_trafo.translation();
@@ -340,6 +349,93 @@ double GLGizmoMove3D::calc_projection(const UpdateData& data) const
return projection;
}
void GLGizmoMove3D::render_align_preview()
{
using AlignType = GLGizmoAlignment::AlignType;
if (m_object_manipulation == nullptr || m_object_manipulation->get_hovered_align_type() == AlignType::NONE)
return;
// The box the hovered operation aligns to, in world coordinates like the operation itself.
const AlignType type = m_object_manipulation->get_hovered_align_type();
const GLGizmoAlignment &helper = m_object_manipulation->get_alignment_helper();
const BoundingBoxf3 box = m_object_manipulation->get_align_to_parent() ? helper.get_parent_box() : helper.get_items_box();
if (!box.defined)
return;
m_parent.get_selection().render_bounding_box(box, {0.0f, 150.0f / 255.0f, 136.0f / 255.0f}, m_parent.get_scale());
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
if (!m_align_plane.is_initialized()) {
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
init_data.reserve_vertices(4);
init_data.reserve_indices(6);
init_data.add_vertex(Vec3f(-0.5f, -0.5f, 0.0f));
init_data.add_vertex(Vec3f(0.5f, -0.5f, 0.0f));
init_data.add_vertex(Vec3f(0.5f, 0.5f, 0.0f));
init_data.add_vertex(Vec3f(-0.5f, 0.5f, 0.0f));
init_data.add_triangle(0, 1, 2);
init_data.add_triangle(0, 2, 3);
m_align_plane.init_from(std::move(init_data));
m_align_plane.set_color({ 1.0f, 1.0f, 1.0f, 0.5f });
}
// A plane across the box at its min, center or max on the hovered axis. Distributing shows up to six evenly
// spaced planes.
int axis = Z;
double t = 0.5;
switch (type) {
case AlignType::X_MIN: axis = X; t = 0.0; break;
case AlignType::X_MAX: axis = X; t = 1.0; break;
case AlignType::CENTER_X:
case AlignType::DISTRIBUTE_X: axis = X; break;
case AlignType::Y_MIN: axis = Y; t = 0.0; break;
case AlignType::Y_MAX: axis = Y; t = 1.0; break;
case AlignType::CENTER_Y:
case AlignType::DISTRIBUTE_Y: axis = Y; break;
case AlignType::Z_MIN: t = 0.0; break;
case AlignType::Z_MAX: t = 1.0; break;
default: break;
}
const Vec3d size = box.size();
std::vector<double> positions;
if (type >= AlignType::DISTRIBUTE_X) {
const size_t count = std::min<size_t>(helper.get_items_count(type, false), 6);
for (size_t i = 1; i <= count; ++i)
positions.push_back(box.min[axis] + i * size[axis] / (count + 1));
} else
positions.push_back(box.min[axis] + t * size[axis]);
const int u = (axis + 1) % 3;
const int v = (axis + 2) % 3;
Transform3d plane_matrix = Transform3d::Identity();
plane_matrix.linear().col(0) = size[u] * Vec3d::Unit(u);
plane_matrix.linear().col(1) = size[v] * Vec3d::Unit(v);
plane_matrix.linear().col(2) = Vec3d::Unit(axis);
Vec3d plane_center = box.center();
glsafe(::glDisable(GL_CULL_FACE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
// Translucent planes must hide neither each other nor the grabbers drawn after them.
glsafe(::glDepthMask(GL_FALSE));
shader->start_using();
const Camera& camera = wxGetApp().plater()->get_camera();
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
for (double position : positions) {
plane_center[axis] = position;
plane_matrix.translation() = plane_center;
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * plane_matrix);
m_align_plane.render();
}
shader->stop_using();
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glEnable(GL_CULL_FACE));
glsafe(::glDisable(GL_BLEND));
}
void GLGizmoMove3D::change_cs_by_selection() {
int obejct_idx, volume_idx;
ModelVolume *model_volume = m_parent.get_selection().get_selected_single_volume(obejct_idx, volume_idx);
+3
View File
@@ -34,6 +34,8 @@ class GLGizmoMove3D : public GLGizmoBase
Vec3d old_center{ Vec3d::Zero() };
};
std::array<GrabberConnection, 3> m_grabber_connections;
// Unit square previewing the target plane of the hovered align or distribute icon.
GLModel m_align_plane;
//BBS: add size adjust related
GizmoObjectManipulation* m_object_manipulation;
@@ -76,6 +78,7 @@ protected:
private:
double calc_projection(const UpdateData& data) const;
void render_align_preview();
void change_cs_by_selection(); //cs mean Coordinate System
private:
int m_last_selected_obejct_idx, m_last_selected_volume_idx;
@@ -1036,6 +1036,11 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event)
Vec2d mouse_pos = mouse_coord.cast<double>();
if (mouse_event.Moving()) {
// No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not
// capture the mouse. End the stroke as that release would have.
if (m_button_down != Button::None)
gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos,
mouse_event.ShiftDown(), mouse_event.AltDown(), false);
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
return false;
}
@@ -1205,6 +1210,10 @@ void GLGizmoPainterBase::on_set_state()
m_old_mo_id = -1;
//m_iva.release_geometry();
m_triangle_selectors.clear();
// A stroke still open goes with the selectors. Its state must not carry into the next session, where the
// first move would end it as if released and the first press would paint a line from its last position.
m_button_down = Button::None;
m_last_mouse_click = Vec2d::Zero();
//Camera& camera = wxGetApp().plater()->get_camera();
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
+3 -1
View File
@@ -1,6 +1,7 @@
#include "GLGizmoSeam.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
//#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
@@ -327,7 +328,8 @@ void GLGizmoSeam::update_from_model_object(bool first_update)
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false);
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
m_triangle_selectors.back()->request_update_render_data();
}
}
+1 -1
View File
@@ -355,7 +355,7 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
ImGui::NewLine();
ImGui::SameLine(bottom_left_width + space_size);
ImGui::Text(_u8L("%d triangles").c_str(), m_configuration.wanted_count);
ImGui::TextUnformatted(GUI::format(_L_PLURAL("%1% triangle", "%1% triangles", m_configuration.wanted_count), m_configuration.wanted_count).c_str());
m_imgui->disabled_end(); // use_count
m_imgui->bbl_checkbox(_L("Show wireframe").c_str(), m_show_wireframe);
File diff suppressed because it is too large Load Diff
@@ -24,6 +24,7 @@
#include "libslic3r/Point.hpp"
#include <imgui.h>
#include <map>
#include <future>
#include <memory>
#include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
@@ -62,6 +63,12 @@ public:
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
// `base`'s own UV channel, when it has one
// (ModelVolume::texture_uv). Resampled onto
// the result and reported back in it, and fed
// to the samplers so a ModelUV layer drives
// the refinement through its own layout.
const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings,
const DisplacementProgressFn &progress,
@@ -198,6 +205,27 @@ public:
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
// while islands are selected and comes back once none are. A view the user clicks in the View row is
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
struct IslandViewFollow
{
bool shown = false; // the highlight, as of the last step()
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
int step(bool shown_now, int view, bool debug_open);
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
// so `shown` is this frame's.
void picked(int view)
{
switched = false;
keep_normal = shown && view == 0;
}
};
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -263,6 +291,9 @@ private:
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
// can carry it. Cheap and idempotent - called wherever either of the two changes.
void store_panel_state();
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
// The order matters and is the whole reason this is one button - a height map can only move
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
@@ -405,6 +436,77 @@ private:
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// ---- The image-adjustment panel ----
//
// Brightness/contrast/sharpness/blur/hue over one layer's image, shown on the image itself and
// updating as the sliders move. What it produces is not a set of layer parameters: it replaces the
// layer's *image*, so the adjusted pixels feed the colour and the displacement together and a
// brightness change moves the relief too (see TextureAdjustments).
//
// An ordinary floating window rather than a popup, and deliberately so. A popup dims the canvas
// behind it and closes on any click outside itself, which between them make it impossible to watch the
// relief change on the model - which is the whole point of adjusting the image here rather than in an
// image editor. A window leaves the 3D view live, clickable and orbitable while the panel is open.
void render_image_adjust_window(const ImVec2 &panel_min, const ImVec2 &panel_max);
// Opens the panel for `layer`, decoding its image into m_adjust_source. Sliders never re-decode
// the PNG.
void open_image_adjust(const TextureDisplacementLayer &layer);
// Reads `layer`'s stored image into the panel's source buffers and records which image it came from.
// Called when the panel opens and again whenever the layer's image is swapped under it, which is why
// it leaves the slider values alone. False when the image cannot be decoded.
bool load_image_adjust_source(const TextureDisplacementLayer &layer);
// Resolves the panel: writes the adjusted image into the user's texture library as a new file and
// points `layer` at it, which is the only way an adjustment becomes permanent. Closes the panel.
void commit_image_adjust(TextureDisplacementLayer &layer);
// Closes the panel and reverts the layer's live preview_adjust to identity, so every exit that is not
// a commit - Cancel, the window's close button, the gizmo closing - leaves the layer as it was found.
void close_image_adjust();
// Collects a finished preview and starts one for the current slider values. One at a time, so a drag
// coalesces into "whatever the sliders last said" instead of a queue of superseded frames.
void update_image_adjust_preview();
int m_adjust_slot = -1; // the layer the panel is open for; -1 when closed
TextureAdjustments m_adjust; // what the sliders currently say
// What the layer's live preview_adjust was last set from, so a slider move can be told from a frame in
// which nothing happened and only the former asks the displacement to be recomputed.
TextureAdjustments m_adjust_applied;
// The layer's image as it was when the panel opened, and a small copy of it. The sliders are applied
// to the small copy for the live preview and to the full-size one exactly once, on commit. Measured on
// a 1024x1024 with every knob engaged, a full-size pass costs 13-19 ms grayscale and 40-60 ms in
// colour (130-190 ms at 2048 in colour) - far too much to repeat per slider frame. The same pass on a
// 384 px copy is 3-4 ms / 9-12 ms, and that is all the preview box can show anyway.
DecodedHeightTexture m_adjust_source;
DecodedHeightTexture m_adjust_preview_source;
// Which image_data the two buffers above were decoded from, so the panel can notice the layer's image
// being swapped from the texture picker while it is open and re-read it. Compared, never dereferenced.
const void *m_adjust_source_image = nullptr;
// And which volume the panel was opened on. Slot numbers are per volume, so without this a change of
// selection would silently rebind the open panel to whichever layer happens to hold the same slot on
// the newly selected object - a different image, under a panel the user opened for this one.
size_t m_adjust_volume_id = 0;
// The preview image on screen, re-uploaded whenever a background pass finishes.
std::unique_ptr<GLTexture> m_adjust_preview_tex;
// The live preview runs on its own thread rather than on the UI thread or on the plater's job
// worker. Not the UI thread because even the small pass is milliseconds and this runs while a slider
// is being dragged; not the job worker because that is a single FIFO shared with Bake and the
// displacement preview (see queue_preview_job()), where a per-frame image filter would sit behind -
// and delay - multi-second mesh work.
std::future<DecodedHeightTexture> m_adjust_preview_job;
TextureAdjustments m_adjust_preview_running; // what the in-flight pass is computing
TextureAdjustments m_adjust_preview_shown; // what m_adjust_preview_tex holds
bool m_adjust_preview_valid = false;
// Drops the GPU copies of the layer's image so the next frame rebuilds them from the decode, which
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
void invalidate_adjust_textures();
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
@@ -424,8 +526,8 @@ private:
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// on-canvas maps consistently onto offset whichever face the anchor is on.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
@@ -505,6 +607,11 @@ private:
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way.
void apply_view_mode(int mode);
// The view on screen, numbered as apply_view_mode() takes it.
int view_mode() const;
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
void follow_island_highlight();
IslandViewFollow m_island_view;
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr;
@@ -736,6 +843,8 @@ private:
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
GLModel m_island_overlay_glmodel;
std::vector<int> m_island_overlay_selection;
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
const std::vector<int> *island_highlight_selection();
void rebuild_island_overlay(const std::vector<int> &selection);
void render_island_overlay();
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
@@ -819,18 +928,31 @@ private:
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
// the shader projects on its own. Shared by the shaded preview and the UV-check overlay.
// unwrap), ModelUV (the model's own channel) and ViewProjected (a projector plane the shader
// doesn't know). One entry per patch/base vertex, already through apply_uv_transform(). Empty for
// Triplanar/Cylindrical/Spherical, which the shader projects on its own. Shared by the shaded
// preview and the UV-check overlay.
//
// `patch_source` maps each patch triangle back to its base-mesh triangle
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
// single-valued per point, so there a corner's UV is just its vertex's.
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
// by the two functions above so the per-vertex and per-corner forms can never disagree.
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const;
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
@@ -879,6 +1001,7 @@ private:
enum class UVBackground { None, Height, Checker };
UVBackground m_uv_editor_bg = UVBackground::None;
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
TextureAdjustments m_uv_editor_bg_adjust; // and the live image adjustment it was uploaded at
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
@@ -916,25 +1039,26 @@ private:
bool m_preview_job_pending = false;
void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from
// (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is:
// an image freed and replaced by another at the same address must not pass for it.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<std::weak_ptr<const std::vector<unsigned char>>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source;
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the preview shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f;
std::unique_ptr<GLTexture> m_height_tex;
std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source;
float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
std::unique_ptr<GLTexture> m_color_tex;
std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source;
float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
@@ -972,9 +1096,12 @@ private:
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
// config rather than in the project: it describes the user's window, not the model, so opening
// someone else's project must not move their panel.
bool m_undocked = false;
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
@@ -1017,6 +1144,9 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
+39 -5
View File
@@ -373,6 +373,25 @@ bool GLGizmosManager::init_icon_textures()
icon_list.insert(std::make_pair((int) icon, texture_id));
}
for (const auto& [icon, name] : std::initializer_list<std::pair<MENU_ICON_NAME, const char*>>{
{ IC_ALIGN_X_MIN , "align_x_min" }, { IC_ALIGN_X_MIN_DARK , "align_x_min_dark" },
{ IC_ALIGN_X_CENTER, "align_x_center" }, { IC_ALIGN_X_CENTER_DARK, "align_x_center_dark" },
{ IC_ALIGN_X_MAX , "align_x_max" }, { IC_ALIGN_X_MAX_DARK , "align_x_max_dark" },
{ IC_ALIGN_Y_MIN , "align_y_min" }, { IC_ALIGN_Y_MIN_DARK , "align_y_min_dark" },
{ IC_ALIGN_Y_CENTER, "align_y_center" }, { IC_ALIGN_Y_CENTER_DARK, "align_y_center_dark" },
{ IC_ALIGN_Y_MAX , "align_y_max" }, { IC_ALIGN_Y_MAX_DARK , "align_y_max_dark" },
{ IC_ALIGN_Z_MIN , "align_z_min" }, { IC_ALIGN_Z_MIN_DARK , "align_z_min_dark" },
{ IC_ALIGN_Z_CENTER, "align_z_center" }, { IC_ALIGN_Z_CENTER_DARK, "align_z_center_dark" },
{ IC_ALIGN_Z_MAX , "align_z_max" }, { IC_ALIGN_Z_MAX_DARK , "align_z_max_dark" },
{ IC_DISTRIBUTE_X , "distribute_x" }, { IC_DISTRIBUTE_X_DARK , "distribute_x_dark" },
{ IC_DISTRIBUTE_Y , "distribute_y" }, { IC_DISTRIBUTE_Y_DARK , "distribute_y_dark" },
{ IC_DISTRIBUTE_Z , "distribute_z" }, { IC_DISTRIBUTE_Z_DARK , "distribute_z_dark" }
}) {
if (!IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/" + name + ".svg", 16, 16, texture_id))
return false;
icon_list.insert(std::make_pair((int) icon, texture_id));
}
return true;
}
@@ -420,7 +439,10 @@ void GLGizmosManager::refresh_on_off_state()
void GLGizmosManager::reset_all_states()
{
if (! m_enabled || m_serializing)
// The restore/new-project event can run before deferred OpenGL resource
// loading has populated the gizmo list. There is nothing to reset until
// initialization completes.
if (! m_enabled || m_serializing || m_gizmos.empty())
return;
const EType current = get_current_type();
@@ -435,6 +457,9 @@ void GLGizmosManager::reset_all_states()
bool GLGizmosManager::open_gizmo(EType type)
{
if (m_gizmos.empty())
return false;
int idx = static_cast<int>(type);
// re-open same type cause closing
@@ -522,7 +547,7 @@ bool GLGizmosManager::is_running() const
bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut)
{
if (!m_enabled)
if (!m_enabled || m_gizmos.empty())
return false;
// The text tool opens without a selection because it creates its own object.
@@ -611,6 +636,13 @@ bool GLGizmosManager::is_allow_select_all() {
return false;
}
// Ctrl+click and Alt+click grow the selection only where that serves the gizmo: the Move gizmo aligns and distributes
// several objects or parts, while Scale uses Ctrl for single sided scaling.
bool GLGizmosManager::is_allow_multi_select_parts_or_objects() const
{
return m_current == Undefined || m_current == EType::Move;
}
std::optional<ClippingPlane> GLGizmosManager::get_clipping_plane() const
{
if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper())
@@ -1427,10 +1459,12 @@ void GLGizmosManager::update_hover_state(const EType &type)
bool GLGizmosManager::activate_gizmo(EType type)
{
assert(!m_gizmos.empty());
// already activated
if (m_current == type) return true;
if (m_current == type)
return true;
if (m_gizmos.empty())
return false;
if (m_current != Undefined) {
// clean up previous gizmo
+25
View File
@@ -201,6 +201,30 @@ public:
IC_CANVAS_SECTION_ACTIVE_HOVER,
IC_CANVAS_SECTION_ACTIVE_DARK,
IC_CANVAS_SECTION_ACTIVE_DARK_HOVER,
IC_ALIGN_X_MIN,
IC_ALIGN_X_CENTER,
IC_ALIGN_X_MAX,
IC_ALIGN_Y_MIN,
IC_ALIGN_Y_CENTER,
IC_ALIGN_Y_MAX,
IC_ALIGN_Z_MIN,
IC_ALIGN_Z_CENTER,
IC_ALIGN_Z_MAX,
IC_DISTRIBUTE_X,
IC_DISTRIBUTE_Y,
IC_DISTRIBUTE_Z,
IC_ALIGN_X_MIN_DARK,
IC_ALIGN_X_CENTER_DARK,
IC_ALIGN_X_MAX_DARK,
IC_ALIGN_Y_MIN_DARK,
IC_ALIGN_Y_CENTER_DARK,
IC_ALIGN_Y_MAX_DARK,
IC_ALIGN_Z_MIN_DARK,
IC_ALIGN_Z_CENTER_DARK,
IC_ALIGN_Z_MAX_DARK,
IC_DISTRIBUTE_X_DARK,
IC_DISTRIBUTE_Y_DARK,
IC_DISTRIBUTE_Z_DARK,
};
explicit GLGizmosManager(GLCanvas3D& parent);
@@ -301,6 +325,7 @@ public:
bool is_paint_gizmo();
bool is_allow_select_all();
bool is_allow_multi_select_parts_or_objects() const;
// Empty when the open gizmo has no object clipper.
std::optional<ClippingPlane> get_clipping_plane() const;
bool wants_reslice_supports_on_undo() const;
@@ -11,6 +11,7 @@
#include "slic3r/GUI/GUI_ObjectList.hpp"
//#include "I18N.hpp"
#include "GLGizmosManager.hpp"
#include "GLGizmoAlignment.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
@@ -75,7 +76,7 @@ static double get_volume_min_z(const GLVolume* volume)
}
GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
: m_glcanvas(glcanvas)
: m_glcanvas(glcanvas), m_alignment_helper(glcanvas)
{
m_imperial_units = wxGetApp().app_config->get("use_inches") == "1";
m_new_unit_string = m_imperial_units ? L_CONTEXT("in", "inches") : L("mm");
@@ -86,6 +87,7 @@ GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
m_shortcuts_move = {
{alt + _L("Left mouse button"), _L("Part selection")},
{ctrl + _L("Left mouse button"), _L("Select multiple objects")},
{shift + _L("Left mouse button"), _L("Fixed step drag")},
{_L("Context Menu"), _L("Toggle Auto-Drop")}
};
@@ -761,6 +763,12 @@ void GizmoObjectManipulation::set_init_rotation(const Geometry::Transformation &
m_init_rotation = value.get_rotation();
}
void GizmoObjectManipulation::reset_align_state()
{
m_hovered_align_type = GLGizmoAlignment::AlignType::NONE;
m_align_to_parent = !m_alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN);
}
void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit)
{
// BBS: GUI refactor: move gizmo to the right
@@ -782,6 +790,8 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
ImGuiWrapper::push_toolbar_style(m_glcanvas.get_scale());
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0, 6.0));
const ImGuiStyle &style = ImGui::GetStyle();
std::string name = this->m_new_title_string + "##" + window_name;
imgui_wrapper->begin(_L(name), ImGuiWrapper::TOOLBAR_WINDOW_FLAGS);
@@ -798,16 +808,26 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
};
float space_size = imgui_wrapper->get_style_scaling() * 8;
// The align section shares the combo width, the caption column and the input columns of the position row.
const bool show_align = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D &&
!m_glcanvas.get_selection().is_wipe_tower();
//ORCA
float coord_combo_width = std::max({
imgui_wrapper->calc_text_size(_L("World")).x,
imgui_wrapper->calc_text_size(_L("Object")).x,
imgui_wrapper->calc_text_size(_L("Part")).x
}) + imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
});
float label_max = std::max({
imgui_wrapper->calc_text_size(_L("Position")).x,
imgui_wrapper->calc_text_size(_L("Relative")).x
});
if (show_align) {
coord_combo_width = std::max({coord_combo_width, imgui_wrapper->calc_text_size(_L("Selection")).x,
imgui_wrapper->calc_text_size(_L("Object")).x,
imgui_wrapper->calc_text_size(_L("Plate")).x});
label_max = std::max(label_max, imgui_wrapper->calc_text_size(_L("Distribute")).x);
}
coord_combo_width += imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
float caption_max = std::max(label_max, coord_combo_width - 3 * space_size);
float end_text_size = imgui_wrapper->calc_text_size(this->m_new_unit_string).x;
@@ -821,6 +841,13 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
// Rotation
float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size;
// Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them.
const float align_icon_size = 16.f * m_glcanvas.get_scale(); // ORCA use 16px icons
const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA
if (show_align) {
unit_size = std::max(unit_size, 3.f * align_button_size);
unit_size = std::round(unit_size); // ORCA keep columns on whole pixels
}
int index = 1;
int index_unit = 1;
@@ -909,6 +936,134 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
}
if (!focued_on_text) m_glcanvas.handle_sidebar_focus_event("", false);
// Align and distribute along the world axes, within the selection or to the object or plate holding it.
using AlignType = GLGizmoAlignment::AlignType;
const bool old_align_to_parent = m_align_to_parent;
AlignType hovered_align_type = AlignType::NONE;
if (show_align) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Each row is row_height tall, separated by ItemSpacing.y. Every control is centered vertically in its row.
const float row_y = ImGui::GetCursorPosY();
const float button_height = align_icon_size + 2.f * style.FramePadding.y;
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
const float combo_height = ImGui::GetFrameHeight();
ImGuiWrapper::pop_combo_style();
const float row_height = std::max({ImGui::GetTextLineHeight(), button_height, combo_height});
const float row_gap = style.ItemSpacing.y;
int row = 0;
auto row_top = [&](int r) { return row_y + r * (row_height + row_gap); };
// Snap to whole pixels to avoid blurry icons
auto center_in_row = [&](int r, float item_height) { return std::floor(row_top(r) + (row_height - item_height) * 0.5f); };
// Row 0: align mode combo in the caption column.
ImGui::SetCursorPosY(center_in_row(row, combo_height));
// The last mode aligns to the parent. Aligning within the selection needs two items.
std::vector<std::string> align_modes = {
_u8L("Selection"),
m_alignment_helper.is_parts_selection(true) ? _u8L("Object") : _u8L("Plate")
};
if (!m_alignment_helper.can_align(AlignType::X_MIN)) {
align_modes.erase(align_modes.begin());
m_align_to_parent = true;
}
size_t align_mode_idx = m_align_to_parent ? align_modes.size() - 1 : 0;
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
// render_combo() takes its ImGui id from the empty label, which the coordinate combo uses too.
ImGui::PushID("align_mode");
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
m_align_to_parent = align_mode_idx + 1 == align_modes.size();
ImGui::PopID();
if (ImGui::IsItemHovered()) {
wxString tooltip_str = _L("Reference for alignment");
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
}
ImGuiWrapper::pop_combo_style();
const bool dark_mode = wxGetApp().dark_mode();
int current_axis = -1;
auto align_icon = [&](int axis, AlignType type, GLGizmosManager::MENU_ICON_NAME icon,
GLGizmosManager::MENU_ICON_NAME dark_icon, const wxString &tip) {
const bool distribute = type >= AlignType::DISTRIBUTE_X;
const bool enabled = m_align_to_parent ? m_alignment_helper.can_align(type, true) :
distribute ? m_alignment_helper.can_distribute(type) :
m_alignment_helper.can_align(type);
if (axis != current_axis) {
ImGui::SameLine(int(caption_max + (axis + 1) * space_size + axis * unit_size)); // ORCA ensure placement snaps to pixel to fix blur
if (current_axis < 0)
ImGui::SetCursorPosY(center_in_row(row, button_height));
current_axis = axis;
} else
ImGui::SameLine(0, 0);
if (!enabled) {
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3f);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_Button]);
}
ImTextureID texture = m_glcanvas.get_gizmos_manager().get_icon_texture_id(dark_mode ? dark_icon : icon);
if (ImGui::ImageButton3(texture, texture, ImVec2(align_icon_size, align_icon_size)) && enabled) {
if (distribute)
m_alignment_helper.distribute_objects(type);
else
m_alignment_helper.align_objects(type, m_align_to_parent);
}
if (!enabled) {
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
if (ImGui::IsItemHovered()) {
wxString text = tip;
if (enabled)
hovered_align_type = type;
else {
// Too few items is the reason when there are items to move (only distribute needs three);
// otherwise the helper names it.
std::string why_not;
if (m_alignment_helper.get_items_count(type, m_align_to_parent, &why_not) > 0)
why_not = _u8L("Select at least 3 parts or objects");
if (!why_not.empty())
text += "\n" + from_u8(why_not);
}
imgui_wrapper->tooltip(text, imgui_wrapper->calc_text_size(text).x + 3 * space_size);
}
};
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
align_icon(0, AlignType::X_MIN, GLGizmosManager::IC_ALIGN_X_MIN, GLGizmosManager::IC_ALIGN_X_MIN_DARK, _L("Align left") + " (-X)");
align_icon(0, AlignType::CENTER_X, GLGizmosManager::IC_ALIGN_X_CENTER, GLGizmosManager::IC_ALIGN_X_CENTER_DARK, _L("Align left-right center") + " (X)");
align_icon(0, AlignType::X_MAX, GLGizmosManager::IC_ALIGN_X_MAX, GLGizmosManager::IC_ALIGN_X_MAX_DARK, _L("Align right") + " (+X)");
align_icon(1, AlignType::Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN_DARK, _L("Align front") + " (-Y)");
align_icon(1, AlignType::CENTER_Y, GLGizmosManager::IC_ALIGN_Y_CENTER, GLGizmosManager::IC_ALIGN_Y_CENTER_DARK, _L("Align front-back center") + " (Y)");
align_icon(1, AlignType::Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX_DARK, _L("Align back") + " (+Y)");
align_icon(2, AlignType::Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN_DARK, _L("Align bottom") + " (-Z)");
align_icon(2, AlignType::CENTER_Z, GLGizmosManager::IC_ALIGN_Z_CENTER, GLGizmosManager::IC_ALIGN_Z_CENTER_DARK, _L("Align top-bottom center") + " (Z)");
align_icon(2, AlignType::Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX_DARK, _L("Align top") + " (+Z)");
if (!m_align_to_parent) {
row = 1;
current_axis = -1;
ImGui::SetCursorPosY(center_in_row(row, ImGui::GetTextLineHeight()));
imgui_wrapper->text(_L("Distribute"));
align_icon(0, AlignType::DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X_DARK, _L("Distribute left-right") + " (X)");
align_icon(1, AlignType::DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y_DARK, _L("Distribute front-back") + " (Y)");
align_icon(2, AlignType::DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z_DARK, _L("Distribute top-bottom") + " (Z)");
}
ImGui::PopStyleVar();
ImGui::SetCursorPosY(row_top(row + 1));
}
// The Move gizmo draws the preview with the scene, which an ImGui-only frame does not redraw.
if (hovered_align_type != m_hovered_align_type || m_align_to_parent != old_align_to_parent) {
m_hovered_align_type = hovered_align_type;
m_glcanvas.set_as_dirty();
m_glcanvas.request_extra_frame();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
@@ -925,8 +1080,9 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
m_last_active_item = current_active_id;
last_move_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar();
ImGuiWrapper::pop_toolbar_style();
}
@@ -1131,10 +1287,11 @@ void GizmoObjectManipulation::do_render_rotate_window(ImGuiWrapper *imgui_wrappe
m_last_active_item = current_active_id;
last_rotate_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end();
// BBS
ImGui::PopStyleVar(2);
ImGui::PopStyleVar();
ImGuiWrapper::pop_toolbar_style();
}
@@ -13,6 +13,7 @@
#include <wx/string.h>
#include "slic3r/GUI/GUI_Geometry.hpp"
#include "GLGizmoAlignment.hpp"
//#include "slic3r/GUI/GLCanvas3D.hpp"
@@ -151,6 +152,13 @@ public:
void set_init_rotation(const Geometry::Transformation &value);
// Align/distribute state of the Move panel, which the Move gizmo previews.
const GLGizmoAlignment &get_alignment_helper() const { return m_alignment_helper; }
GLGizmoAlignment::AlignType get_hovered_align_type() const { return m_hovered_align_type; }
bool get_align_to_parent() const { return m_align_to_parent; }
// When the Move gizmo opens or closes: no preview, and "Align selected" whenever the selection allows it.
void reset_align_state();
private:
void reset_settings_value();
void update_settings_value(const Selection& selection);
@@ -175,6 +183,12 @@ private:
GLCanvas3D& m_glcanvas;
unsigned int m_last_active_item { 0 };
GLGizmoAlignment m_alignment_helper;
// Operation of the enabled align icon under the mouse.
GLGizmoAlignment::AlignType m_hovered_align_type{GLGizmoAlignment::AlignType::NONE};
// Align to the object or plate holding the selection rather than within the selection.
bool m_align_to_parent{false};
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_move;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
+2 -1
View File
@@ -20,6 +20,7 @@
#include "slic3r/GUI/BitmapComboBox.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
namespace Slic3r { namespace GUI {
@@ -95,7 +96,7 @@ void IMEXFilamentPickerPopover::build_row()
};
for (int L : lane_logicals) {
const wxString label = wxString::Format("filament %d", L + 1);
const wxString label = wxString::Format(_L("Filament %d"), L + 1);
if (L >= 0 && L < (int)bmps.size() && bmps[L] && bmps[L]->IsOk())
choice->Append(label, pad_left(*bmps[L]));
else
+12 -6
View File
@@ -321,15 +321,21 @@ bool IMEXModesCtrl::fix_row_name(Row& r) {
IMEXModesCtrl::RoleStyle IMEXModesCtrl::role_style(std::optional<ImexRole> role) {
// Gray / "Inactive" is the no-role answer; every role gets an explicit case so a new
// one is a compile-time -Wswitch prompt rather than a tile that silently renders gray.
// TRN IDEX/IQEX role of a tool head that takes no part in the mode.
const wxString inactive = _L_CONTEXT("Inactive", "IDEX/IQEX role");
if (!role)
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" };
return { wxColour(90, 90, 90), *wxWHITE, inactive };
switch (*role) {
case ImexRole::Primary: return { wxColour(50, 160, 50), *wxWHITE, "Primary" }; // green
case ImexRole::Copy: return { wxColour(60, 120, 210), *wxWHITE, "Copy" }; // blue
case ImexRole::Mirror: return { wxColour(210, 130, 20), *wxWHITE, "Mirror" }; // amber
case ImexRole::Span: return { wxColour(180, 180, 40), *wxWHITE, "Span" }; // yellow
// TRN IDEX/IQEX role: the tool head the slicer generates paths for.
case ImexRole::Primary: return { wxColour(50, 160, 50), *wxWHITE, _L_CONTEXT("Primary", "IDEX/IQEX role") }; // green
// TRN IDEX/IQEX role: the tool head prints a copy of the Primary's paths.
case ImexRole::Copy: return { wxColour(60, 120, 210), *wxWHITE, _L_CONTEXT("Copy", "IDEX/IQEX role") }; // blue
// TRN IDEX/IQEX role: the tool head prints a mirror image of the Primary's paths.
case ImexRole::Mirror: return { wxColour(210, 130, 20), *wxWHITE, _L_CONTEXT("Mirror", "IDEX/IQEX role") }; // amber
// TRN IDEX/IQEX role: multicolor partner of the Primary on the same gantry.
case ImexRole::Span: return { wxColour(180, 180, 40), *wxWHITE, _L_CONTEXT("Span", "IDEX/IQEX role") }; // yellow
}
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" }; // keeps every compiler quiet
return { wxColour(90, 90, 90), *wxWHITE, inactive }; // keeps every compiler quiet
}
wxString IMEXModesCtrl::role_hint(ImexRole role) {
+2 -3
View File
@@ -147,9 +147,8 @@ private:
// Everything the editor draws for one role, in ONE place: a new role needs a color and
// a legend name here and nowhere else in this file. nullopt is Inactive (gray).
// `label` is deliberately untranslated — it names a role, and the legend sits next to
// the same letters the preset stores.
struct RoleStyle { wxColour bg; wxColour fg; const char* label; };
// `label` is display-only and translated; the preset stores the role letters, never it.
struct RoleStyle { wxColour bg; wxColour fg; wxString label; };
static RoleStyle role_style(std::optional<ImexRole> role);
// True when this printer offers `role` at all, regardless of which tile is clicked.
+2 -2
View File
@@ -1564,7 +1564,7 @@ void IMSlider::render_add_menu()
ImU32 icon_clr = ImGuiWrapper::to_ImU32(rgba);
if (rgba.a() == 0)
icon_clr = 0;
if (menu_item_with_icon((_u8L("Filament ") + std::to_string(i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr, false, true, &hovered)) add_code_as_tick(ToolChange, i + 1);
if (menu_item_with_icon(into_u8(wxString::Format(_L("Filament %d"), i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr, false, true, &hovered)) add_code_as_tick(ToolChange, i + 1);
if (hovered) { show_tooltip(_u8L("Change filament at the beginning of this layer.")); }
}
end_menu();
@@ -1614,7 +1614,7 @@ void IMSlider::render_edit_menu(const TickCode& tick)
for (int i = 0; i < extruder_num; i++) {
ColorRGBA rgba = decode_color_to_float_array(m_extruder_colors[i]);
ImU32 icon_clr = ImGuiWrapper::to_ImU32(rgba);
if (menu_item_with_icon((_u8L("Filament ") + std::to_string(i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr)) add_code_as_tick(ToolChange, i + 1);
if (menu_item_with_icon(into_u8(wxString::Format(_L("Filament %d"), i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr)) add_code_as_tick(ToolChange, i + 1);
}
end_menu();
}
+3 -2
View File
@@ -647,7 +647,7 @@ void ArrangeJob::check_unprintable()
if (it->poly.area() < 0.001) {
auto msg = (boost::format(
_u8L("Object %s has zero size and can't be arranged."))
% _utf8(it->name)).str();
% it->name).str();
m_plater->get_notification_manager()->push_notification(NotificationType::BBLPlateInfo,
NotificationManager::NotificationLevel::WarningNotificationLevel, msg);
}
@@ -693,7 +693,8 @@ void ArrangeJob::process(Ctl &ctl)
params.stopcondition = [&ctl]() { return ctl.was_canceled(); };
params.progressind = [this, &ctl](unsigned num_finished, std::string str = "") {
ctl.update_status(num_finished * 100 / status_range(), _u8L("Arranging ") + str);
// TRN Progress status. %1% is the name of the object being arranged.
ctl.update_status(num_finished * 100 / status_range(), GUI::format(_u8L("Arranging %1%"), str));
};
{
+4 -2
View File
@@ -30,6 +30,7 @@
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/Selection.hpp"
@@ -265,9 +266,10 @@ void FillBedJob::process(Ctl &ctl)
return ctl.was_canceled() || do_stop;
};
params.progressind = [this, &ctl, &statustxt](unsigned st,std::string str="") {
params.progressind = [this, &ctl](unsigned st,std::string str="") {
if (st > 0)
ctl.update_status(st * 100 / status_range(), statustxt + " " + str);
// TRN Progress status of Fill bed. %1% is the name of the object being copied.
ctl.update_status(st * 100 / status_range(), GUI::format(_u8L("Filling %1%"), str));
};
params.on_packed = [&do_stop] (const ArrangePolygon &ap) {
+3 -1
View File
@@ -26,6 +26,7 @@
#include "libslic3r/TriangleMesh.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
#include "slic3r/GUI/Selection.hpp"
@@ -235,7 +236,8 @@ void OrientJob::process(Ctl &ctl)
params.progressind = [this, count, &ctl](unsigned st, std::string orientstr) {
st += m_unprintable.size();
if (st > 0) ctl.update_status(int(st / float(count) * 100), _u8L("Orienting") + " " + orientstr);
// TRN Progress status. %1% is the name of the object being oriented.
if (st > 0) ctl.update_status(int(st / float(count) * 100), GUI::format(_u8L("Orienting %1%"), orientstr));
};
orientation::orient(m_selected, m_unselected, params);
+1 -1
View File
@@ -104,7 +104,7 @@ class PlaterWorker: public Worker {
if (eptr) try {
std::rethrow_exception(eptr);
} catch (std::exception &e) {
show_error(m_plater, _L("An unexpected error occurred") + ": " + e.what());
show_error(m_plater, _L("An unexpected error occurred") + ": " + from_u8(e.what()));
eptr = nullptr;
}
}
+2 -2
View File
@@ -34,13 +34,13 @@ class SLAImportDialog : public wxDialog, public SLAImportJobView
wxComboBox *m_import_dropdown, *m_quality_dropdown;
public:
SLAImportDialog(Plater *plater) : wxDialog{plater, wxID_ANY, "Import SLA archive"}
SLAImportDialog(Plater *plater) : wxDialog{plater, wxID_ANY, _L("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 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP",
_L("SL1 / SL1S archive files") + " (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.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);
+3 -3
View File
@@ -76,7 +76,7 @@ void SLAImportJob::process(Ctl &ctl)
break;
}
} catch (MissingProfileError &) {
p->err = _L("The SLA archive doesn\'t contain any presets. Please activate some SLA printer presets first before importing that SLA archive.").ToStdString();
p->err = _u8L("The SLA archive doesn\'t contain any presets. Please activate some SLA printer presets first before importing that SLA archive.");
} catch (std::exception &ex) {
p->err = ex.what();
}
@@ -124,8 +124,8 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr)
p->plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::WarningNotificationLevel,
_L("The imported SLA archive did not contain any presets. "
"The current SLA presets were used as fallback.").ToStdString());
_u8L("The imported SLA archive did not contain any presets. "
"The current SLA presets were used as fallback."));
}
if (p->sel != Sel::modelOnly) {
@@ -8,6 +8,7 @@
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
@@ -26,6 +27,7 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI {
@@ -74,7 +76,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
}
return true;
},
color, m_input.volume_to_world, nullptr, &m_stats));
color, m_input.volume_to_world, nullptr, &m_stats,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
@@ -126,16 +129,71 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() });
});
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
// the triangles matter - the annotations are per facet and reference no vertex index - so a
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
// reason to remap anything.
const bool topology_changed = m_input.options.pipeline_v2 ||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
// no longer existed, which is what scrambled the colours of an imported coloured model even
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
//
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
// could only lose detail.
std::array<TriangleSelector::TriangleSplittingData, 4> before;
if (topology_changed) {
before[0] = volume->supported_facets.get_data();
before[1] = volume->seam_facets.get_data();
before[2] = volume->mmu_segmentation_facets.get_data();
before[3] = volume->fuzzy_skin_facets.get_data();
}
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
if (topology_changed) {
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
&volume->mmu_segmentation_facets,
&volume->fuzzy_skin_facets };
for (size_t ch = 0; ch < target.size(); ++ch) {
const std::vector<uint8_t> state =
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
// Nothing was painted in this channel, or there is nothing to paint it onto: either
// way the annotation must not keep describing the mesh that is gone.
if (state.empty()) {
target[ch]->reset();
continue;
}
TriangleSelector selector(volume->mesh());
bool any = false;
for (size_t i = 0; i < state.size(); ++i)
if (state[i] != 0) {
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
any = true;
}
if (any)
target[ch]->set(selector);
else
target[ch]->reset();
}
}
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
// and one the user never painted at all stays at NONE, which already means "the volume's own
// filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// topology-preserving, so triangle i is still triangle i.
// colour for everything outside it. What was already painted is whatever the transfer above
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
@@ -159,7 +217,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
@@ -167,6 +224,14 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
volume->texture_displacement_facet(slot).reset();
}
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
// from: a displaced vertex came from its foot point on that surface, which is the position its
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
if (!m_input.model_uv.empty())
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
volume->mesh().its) :
m_input.model_uv;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
@@ -219,6 +284,8 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
input.take_snapshot = take_snapshot;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume);
@@ -34,6 +34,9 @@ struct TextureDisplacementBakeInput
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
// layer maps through, and what the commit puts back on the baked mesh.
std::vector<Vec2f> model_uv;
// Whether this job pushes its own undo step when it commits. False when the caller has already
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
// and subdivides first and has to undo as a single action.
@@ -17,6 +17,7 @@
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r::GUI {
@@ -52,13 +53,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
indexed_triangle_set mesh = m_input.base_mesh;
TextureDisplacementFacetsData masks = m_input.facets_data;
std::vector<Vec2f> uv = m_input.model_uv;
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
// preparation - which is where "first it remeshes, then it refines" actually happens. The
// experimental pipeline refines as part of the bake and has nothing to prepare.
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
m_input.color,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
@@ -77,12 +79,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (!prepared.mesh.indices.empty()) {
mesh = prepared.mesh;
masks = prepared.masks;
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
}
}
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
[&report](int pct) { return report(50 + pct / 2); },
/* color */ nullptr, m_input.volume_to_world, &recorder);
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
uv.empty() ? nullptr : &uv);
m_stages = recorder.take();
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
@@ -115,6 +119,8 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
input.check_topology = check_topology;
input.color = color;
input.volume_to_world = texture_displacement_volume_to_world(volume);
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
@@ -47,6 +47,9 @@ struct TextureDisplacementDebugInput
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
// produces no colour of its own.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
// ModelUV layer actually bakes through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
// multi-million triangle stage costs more than the stage did. On by default because a stage that
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color,
m_input.model_uv, m_input.params, m_input.color,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
@@ -106,6 +106,10 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
volume->restore_painting(saved_painting);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
// restore_painting(), which resets it along with the extra facets for the same reason: it is
// indexed by triangle and the triangles have changed.
volume->texture_uv = std::move(m_result.uv);
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
@@ -9,6 +9,7 @@
#include "Job.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
namespace Slic3r {
@@ -47,6 +48,10 @@ struct TextureDisplacementPrepareInput
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
// perceived colour, never the interleaving that realises a mix.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
// indexed by triangle and so cannot survive the stages on its own.
std::vector<Vec2f> model_uv;
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
// displacement job that follows commits into this same step rather than pushing its own.
std::string snapshot_name;
@@ -48,7 +48,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
(!m_current_generation ||
m_current_generation->load() == m_generation);
},
color, m_input.volume_to_world);
color, m_input.volume_to_world, nullptr, nullptr,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
@@ -29,6 +29,9 @@ struct TextureDisplacementPreviewInput
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
// same layout it will bake through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
+45 -25
View File
@@ -200,6 +200,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode)
}
}
// libvgcode finds where a path starts from a phantom vertex placed at the previous move's
// position. Every move that is not an option marker (extrusions, travels, wipes, and Noop or
// unmapped moves) starts a path when it is the first move, or when it differs from the previous
// move in type, role or a value the preview colors by. convert() sizes its vertex array by
// counting these, so the count and the fill must both use this test.
static bool starts_path(const Slic3r::GCodeProcessorResult::MoveVertex& prev,
const Slic3r::GCodeProcessorResult::MoveVertex& curr, bool first_move)
{
const EOptionType option_type = move_type_to_option(convert(curr.type));
if (option_type != EOptionType::COUNT && option_type != EOptionType::Travels && option_type != EOptionType::Wipes)
return false;
return first_move || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
// ORCA: Split the path when a preview value changes.
|| prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk;
}
GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector<std::string>& str_tool_colors,
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer,
const Slic3r::Transform3d* belt_xform)
@@ -231,7 +247,14 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
}
const std::vector<Slic3r::GCodeProcessorResult::MoveVertex>& moves = result.moves;
ret.vertices.reserve(2 * moves.size());
// One vertex per move after the first, plus a phantom in front of every path start. Reserving
// the exact count leaves nothing to shrink afterwards, and shrinking would copy the whole array
// while both copies are alive.
size_t vertices_count = moves.empty() ? 0 : moves.size() - 1;
for (size_t i = 1; i < moves.size(); ++i)
if (starts_path(moves[i - 1], moves[i], i == 1))
++vertices_count;
ret.vertices.reserve(vertices_count);
// libvgcode numbers its layers from the vertices it is given and expects them to
// arrive one layer after the other with consecutive ids: a layer without any move
// (a belt file can carry layer changes that print nothing) would leave a gap, and
@@ -248,33 +271,28 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
++ layer_id;
src_layer_id = curr.layer_id;
}
const EOptionType option_type = move_type_to_option(curr_type);
if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) {
if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
// ORCA: Split the path when a preview value changes.
|| prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk) {
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
// equal to the current one with the exception of the position, which should match the previous move position,
// and the times, which are set to zero
if (starts_path(prev, curr, i == 1)) {
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
// equal to the current one with the exception of the position, which should match the previous move position,
// and the times, which are set to zero
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
#else
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
/* ORCA: Add Jerk visualization support */ curr.jerk };
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
ret.vertices.emplace_back(vertex);
}
ret.vertices.emplace_back(vertex);
}
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
@@ -297,6 +315,8 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
ret.vertices.emplace_back(vertex);
}
assert(ret.vertices.size() == vertices_count);
// A no-op when the count above is exact; restores the exact fit if the count ever drifts.
ret.vertices.shrink_to_fit();
// Note: the belt designed-view anchoring (recovering the per-object placement/
+6 -1
View File
@@ -1746,7 +1746,8 @@ bool MainFrame::preview_only_hint()
confirm_dlg.update_btn_label(_L("Yes"), _L("No"));
auto filename = m_plater->get_preview_only_filename();
confirm_dlg.update_text(filename + " " + _L("will be closed before creating a new model. Do you want to continue?"));
// TRN %s is a file name.
confirm_dlg.update_text(wxString::Format(_L("%s will be closed before creating a new model. Do you want to continue?"), from_u8(filename)));
confirm_dlg.on_show();
if (preview_only_to_editor) {
m_plater->new_project();
@@ -2827,6 +2828,10 @@ wxMenu* MainFrame::generate_help_menu()
append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"),
[](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); });
append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"),
_L("Bring back filament presets that a profile update removed from printers they were made for"),
[](wxCommandEvent&) { wxGetApp().check_legacy_user_filament_presets(true); });
helpMenu->AppendSeparator();
// Troubleshoot center
+1 -1
View File
@@ -144,7 +144,7 @@ wxString DeviceItem::get_state_device()
str_state_device.push_back(_L("Printing Failed"));
str_state_device.push_back(_L("Printing"));
str_state_device.push_back(_L("Printing Pause"));
str_state_device.push_back(_L("Prepare"));
str_state_device.push_back(_L_CONTEXT("Prepare", "Device status"));
str_state_device.push_back(_L("Slicing"));
str_state_device.push_back(_L("Syncing"));
+2 -1
View File
@@ -1,5 +1,6 @@
#include "MultiMachineManagerPage.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "MainFrame.hpp"
#include "DeviceCore/DevManager.h"
@@ -253,7 +254,7 @@ void MultiMachineItem::doRender(wxDC& dc)
dc.DrawRoundedRectangle(left, (size.y - FromDIP(38)) / 2, FromDIP(90), FromDIP(38), 6);
dc.SetFont(Label::Body_14);
dc.SetTextForeground(*wxBLACK);
dc.DrawText(_L("View"),left + FromDIP(90) / 2 - dc.GetTextExtent(_L("View")).x / 2, (size.y -dc.GetTextExtent(_L("View")).y) / 2);
dc.DrawText(_L_CONTEXT("View", "Verb"),left + FromDIP(90) / 2 - dc.GetTextExtent(_L_CONTEXT("View", "Verb")).x / 2, (size.y -dc.GetTextExtent(_L_CONTEXT("View", "Verb")).y) / 2);
}
+2 -2
View File
@@ -101,7 +101,7 @@ MultiTaskItem::MultiTaskItem(wxWindow* parent, MachineObject* obj, int type)
m_button_cancel->SetCornerRadius(6);
m_button_cancel->SetMinSize(wxSize(FromDIP(70), FromDIP(35)));
m_button_pause = new Button(this, _L("Pause"));
m_button_pause = new Button(this, _L_CONTEXT("Pause", "Verb"));
m_button_pause->SetBackgroundColor(clean_bg);
m_button_pause->SetBorderColor(clean_bd);
m_button_pause->SetTextColor(clean_text);
@@ -1193,7 +1193,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent)
m_ctrl_btn_panel->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), -1));
m_ctrl_btn_panel->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), -1));
m_btn_sizer = new wxBoxSizer(wxHORIZONTAL);
btn_pause_all = new Button(m_ctrl_btn_panel, _L("Pause"));
btn_pause_all = new Button(m_ctrl_btn_panel, _L_CONTEXT("Pause", "Verb"));
btn_pause_all->SetBackgroundColor(ctrl_bg);
btn_pause_all->SetCornerRadius(FromDIP(5));
btn_continue_all = new Button(m_ctrl_btn_panel, _L("Resume"));
-6
View File
@@ -1,6 +0,0 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
-11
View File
@@ -1,11 +0,0 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
+4
View File
@@ -589,6 +589,10 @@ std::vector<NativeCommand> build_command_catalog()
Slic3r::GUI::desktop_open_datadir_folder();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) {
wxGetApp().check_legacy_user_filament_presets(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) {
wxGetApp().troubleshoot();
return AppActionRunResult{AppActionRunResult::Level::Success};
+22 -26
View File
@@ -1880,13 +1880,6 @@ void PartPlate::render_exclude_area(bool force_default_color) {
// draw exclude area
glsafe(::glDepthMask(GL_FALSE));
if (m_selected) {
glsafe(::glColor4fv(select_color.data()));
}
else {
glsafe(::glColor4fv(unselect_color.data()));
}
m_exclude_triangles.set_color(m_selected ? select_color : unselect_color);
m_exclude_triangles.render();
glsafe(::glDepthMask(GL_TRUE));
@@ -2154,13 +2147,6 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
std::string cur = get_imex_mode();
const std::string effective = get_effective_imex_mode();
if (cur == kImexPrimaryMode) cur = _u8L("Primary");
if (effective != get_imex_mode()) {
try {
cur += (boost::format(_u8L(" (process preset: %1%)")) % effective).str();
} catch (const std::exception&) {
cur += (boost::format(" (process preset: %1%)") % effective).str();
}
}
// One format string, not two catalog fragments concatenated around a
// runtime value: translators need to move the mode name within the
// sentence, and the space-padded fragments were untranslatable alone.
@@ -2169,10 +2155,19 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
// TRANSLATED string drops or malforms %1%, and an exception escaping here
// would take down the frame rather than show a wrong tooltip. The English
// literal is the fallback and cannot itself throw.
try {
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)")) % cur).str();
} catch (const std::exception&) {
hover_tip = (boost::format("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)") % cur).str();
if (effective != get_imex_mode()) {
try {
// TRN Plate icon tooltip. %1% is the plate's IDEX/IQEX mode, %2% the mode its process preset sets.
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (process preset: %2%) (left-click to cycle, right-click for menu)")) % cur % effective).str();
} catch (const std::exception&) {
hover_tip = (boost::format("IDEX/IQEX mode: %1% (process preset: %2%) (left-click to cycle, right-click for menu)") % cur % effective).str();
}
} else {
try {
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)")) % cur).str();
} catch (const std::exception&) {
hover_tip = (boost::format("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)") % cur).str();
}
}
} else {
render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_textures[kind]);
@@ -3041,8 +3036,8 @@ bool PartPlate::check_filament_printable(const DynamicPrintConfig &config, wxStr
int filament_printable_status = filament_printables[filament_id];
int extruder_idx = filament_map[filament_id] - 1;
if (!(filament_printable_status >> extruder_idx & 1)) {
wxString extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
error_message = wxString::Format(_L("The %s nozzle can not print %s."), extruder_name, filament_type);
error_message = wxString::Format(extruder_idx == 0 ? _L("The left nozzle cannot print %s.") : _L("The right nozzle cannot print %s."),
filament_type);
return false;
}
}
@@ -3235,9 +3230,8 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
auto get_nozzle_msg = [](const float nozzle_diameter, const NozzleVolumeType volume_type) -> std::string {
std::ostringstream oss;
oss << std::fixed << std::setprecision(1) << nozzle_diameter;
std::string nozzle_msg = oss.str();
((nozzle_msg += "mm ") += _u8L(get_nozzle_volume_type_string(volume_type))) += _u8L(" nozzle");
return nozzle_msg;
// TRN %1% is the nozzle diameter, %2% the nozzle volume type, for example "Standard" or "High Flow".
return Slic3r::format(_u8L("%1%mm %2% nozzle"), oss.str(), _u8L(get_nozzle_volume_type_string(volume_type)));
};
auto get_incompatible_filament_msg = [](const std::set<std::string> &incompatible_selected_filaments) -> std::string {
@@ -3268,7 +3262,9 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
auto elem = incompatible_selected_map.begin();
NozzleVolumeType volume_type = elem->first;
auto incompatible_selected = elem->second;
error_msg = GUI::format(_L("It is not recommended to print the following filament(s) with %1%: %2%\n"), get_nozzle_msg(nozzle_diameter, volume_type),
error_msg = GUI::format(_L_PLURAL("It is not recommended to print the following filament with %1%: %2%\n",
"It is not recommended to print the following filaments with %1%: %2%\n", incompatible_selected.size()),
get_nozzle_msg(nozzle_diameter, volume_type),
get_incompatible_filament_msg(incompatible_selected));
} else {
std::string warning_msg = _u8L("It is not recommended to use the following nozzle and filament combinations:\n");
@@ -4676,8 +4672,8 @@ void PartPlate::update_slice_context(BackgroundSlicingProcess & process)
Slic3r::SlicingStatusEvent *event = new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status);
//BBS: GUI refactor: add plate info befor message
if (status.message_type == Slic3r::PrintStateBase::SlicingDefaultNotification) {
auto temp = Slic3r::format(_u8L(" plate %1%:"), std::to_string(m_plate_index + 1));
event->status.text = temp + event->status.text;
// TRN Slicing progress. %1% is the plate number, %2% the current slicing step.
event->status.text = Slic3r::format(_u8L("Slicing plate %1%: %2%"), m_plate_index + 1, event->status.text);
}
wxQueueEvent(m_plater, event);
};
+3 -1
View File
@@ -676,6 +676,7 @@ void PartSkipDialog::UpdateCountLabel()
}
m_cnt_label->SetLabel(wxString::Format("%d", check_cnt));
m_cnt_label->Fit();
// TRN Shown right after the number of selected objects, which is a separate label: "3/10 Selected". %d is the total number of objects.
m_tot_label->SetLabel(wxString::Format(_L("/%d Selected"), tot_cnt));
m_tot_label->Fit();
m_dlg_btn_sizer->Layout();
@@ -922,7 +923,8 @@ void PartSkipDialog::OnApplyDialog(wxCommandEvent &event)
confirm_dialog.SetMsgLabel(_L("Skipping all objects."));
confirm_dialog.SetTipLabel(_L("The printing job will be stopped. Continue?"));
} else {
confirm_dialog.SetMsgLabel(wxString::Format(_L("Skipping %d objects."), m_partskip_ids.size()));
const int skip_cnt = int(m_partskip_ids.size());
confirm_dialog.SetMsgLabel(wxString::Format(_L_PLURAL("Skipping %d object.", "Skipping %d objects.", skip_cnt), skip_cnt));
confirm_dialog.SetTipLabel(_L("This action cannot be undone. Continue?"));
}
+175 -112
View File
@@ -1686,7 +1686,7 @@ ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
plater->update_machine_sync_status();
}
});
combo_flow->SetToolTip(_L("Flow"));
combo_flow->SetToolTip(_L_CONTEXT("Flow", "Nozzle Flow"));
row.flow = combo_flow;
use_nozzle_row_font(combo_flow);
@@ -4125,13 +4125,12 @@ void Sidebar::update_presets(Preset::Type preset_type)
std::vector<NozzleType> nozzle_types(cfg_nozzle_type->size());
for (size_t idx = 0; idx < cfg_nozzle_type->size(); ++idx)
nozzle_types[idx] = NozzleType(cfg_nozzle_type->values[idx]);
nozzle_type = _L( // NEEDFIX this part can be replaced with shorter names
nozzle_types[0] == ntHardenedSteel ? "Hardened Steel" :
nozzle_types[0] == ntStainlessSteel ? "Stainless Steel" :
nozzle_types[0] == ntTungstenCarbide ? "Tungsten Carbide" :
nozzle_types[0] == ntBrass ? "Brass"
: "-" // Undefined
);
// NEEDFIX this part can be replaced with shorter names
nozzle_type = nozzle_types[0] == ntHardenedSteel ? _L("Hardened Steel") :
nozzle_types[0] == ntStainlessSteel ? _L("Stainless Steel") :
nozzle_types[0] == ntTungstenCarbide ? _L("Tungsten Carbide") :
nozzle_types[0] == ntBrass ? _L("Brass")
: wxString("-"); // Undefined
}
p->label_nozzle_type->SetLabel(nozzle_type);
p->label_nozzle_type->SetToolTip(nozzle_type == "-" ? "" : nozzle_type);
@@ -4965,7 +4964,7 @@ void Sidebar::collect_physical_filament_info(std::vector<std::string>& color_str
for (size_t i = 0; i < num_physical; ++i) {
auto* combo = p->combos_filament[i];
names.push_back(combo ? into_u8(combo->GetValue()) : "Filament " + std::to_string(i + 1));
names.push_back(into_u8(combo ? combo->GetValue() : wxString::Format(_L("Filament %d"), int(i + 1))));
}
auto& preset_bundle = *wxGetApp().preset_bundle;
@@ -5333,7 +5332,7 @@ void Sidebar::remove_all_mixed_filaments()
// Worth a confirmation: this drops filament slots the model may be painted with, and anything
// painted in one falls back to a plain filament.
MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"),
MessageDialog dlg(this, format_wxstr(_L_PLURAL("Remove %1% mixed filament?", "Remove all %1% mixed filaments?", count), count), _L("Mixed Filament"),
wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
if (dlg.ShowModal() != wxID_YES)
return;
@@ -6014,7 +6013,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na
bool Sidebar::is_new_project_in_gcode3mf()
{
if (p->plater->using_exported_file()) {
// A .gcode previewed with its own settings shows them here too, and is closed the same way.
if (p->plater->using_exported_file() || p->plater->gcode_settings_loaded()) {
auto filename = p->plater->get_preview_only_filename();
auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename);
MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"),
@@ -7368,6 +7368,9 @@ struct Plater::priv
void enable_sidebar(bool enabled);
void collapse_sidebar(bool collapse);
void update_sidebar(bool force_update = false);
// The sidebar shows the selected presets, which a G-code previewed without its own settings has nothing to do
// with, so it stays hidden then. With the file's settings loaded it shows them, as for a .gcode.3mf.
bool gcode_preview_without_settings() const { return q->m_only_gcode && !q->m_gcode_settings_loaded; }
void reset_window_layout();
Sidebar::DockingState get_sidebar_docking_state();
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
@@ -8593,7 +8596,7 @@ void Plater::priv::select_next_view_3D()
void Plater::priv::enable_sidebar(bool enabled)
{
if (q->m_only_gcode)
if (gcode_preview_without_settings())
enabled = false;
sidebar_layout.is_enabled = enabled;
@@ -8602,7 +8605,7 @@ void Plater::priv::enable_sidebar(bool enabled)
void Plater::priv::collapse_sidebar(bool collapse)
{
if (q->m_only_gcode)
if (gcode_preview_without_settings())
return;
sidebar_layout.is_collapsed = collapse;
@@ -8865,6 +8868,64 @@ static void log_substitutions(const PresetsConfigSubstitutions& substitutions, c
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
}
// After the settings of a project or a G-code were loaded into the presets: size the per-filament project
// lists to the loaded filament count, and drop state only a connected printer may set.
static void sync_loaded_project_filament_config(DynamicConfig& proj_cfg)
{
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
filament_multi_color->values.resize(filament_count);
}
// If there is no multi-color data or color is not match, use single color as default value
for (size_t i = 0; i < filament_count; i++) {
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
filament_multi_color->values[i] = filament_color->values[i];
}
}
// Sync filament colour type
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
if (filament_color_type && filament_color_type->size() != filament_count) {
filament_color_type->values.resize(filament_count);
for (size_t i = 0; i < filament_count; i++) {
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
filament_color_type->values[i] = "1";
}
}
}
}
// Filament Track Switch state is derived live from the connected
// printer; a loaded project must not carry a stale installed/active
// flag, so clear both and let device sync re-derive them.
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
dynamic_map->value = false;
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
@@ -9681,58 +9742,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
if (file_wipe_tower_y)
*wipe_tower_y = *file_wipe_tower_y;
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
filament_multi_color->values.resize(filament_count);
}
// If there is no multi-color data or color is not match, use single color as default value
for (size_t i = 0; i < filament_count; i++) {
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
filament_multi_color->values[i] = filament_color->values[i];
}
}
// Sync filament colour type
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
if (filament_color_type && filament_color_type->size() != filament_count) {
filament_color_type->values.resize(filament_count);
for (size_t i = 0; i < filament_count; i++) {
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
filament_color_type->values[i] = "1";
}
}
}
}
// Filament Track Switch state is derived live from the connected
// printer; a loaded project must not carry a stale installed/active
// flag, so clear both and let device sync re-derive them.
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
dynamic_map->value = false;
sync_loaded_project_filament_config(proj_cfg);
}
// Update filament combobox after loading config
if (published_config.published) {
@@ -10360,10 +10370,7 @@ std::vector<size_t> Plater::priv::load_model_objects(const ModelObjectPtrs& mode
}
if (!auto_drop) {
for (size_t i = 0; i < object->instances.size(); ++i) {
ModelInstance* instance = object->instances[i];
instance->auto_drop = auto_drop;
}
object->auto_drop = false;
// if under the bed, move over the bed
double dist_to_bed = std::min(object->min_z(), double(0));
@@ -10992,7 +10999,7 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */)
return false;
};
bool split_auto_drop = auto_drop;
if (current_model_object->instances[0]->auto_drop && is_atleast_one_floating()) {
if (current_model_object->auto_drop && is_atleast_one_floating()) {
MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"),
_L("Object with floating parts was detected"), wxICON_QUESTION | wxYES_NO);
@@ -12156,7 +12163,7 @@ void Plater::priv::reload_from_disk()
}
}
else {
wxString message = _L("Do you want to replace it") + " ?";
wxString message = _L("Do you want to replace it?");
MessageDialog dlg(q, message, _L("Message"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION);
if (dlg.ShowModal() == wxID_YES)
#if ENABLE_RELOAD_FROM_DISK_REWORK
@@ -12564,7 +12571,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice)
// Add sidebar and toolbar collapse logic
if (panel == view3D || panel == preview) {
this->enable_sidebar(!q->only_gcode_mode());
this->enable_sidebar(true);
}
if (panel == preview) {
if (q->only_gcode_mode()) {
@@ -13065,9 +13072,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
void Plater::priv::on_slicing_update(SlicingStatusEvent &evt)
{
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % evt.status.text;
//BBS: add slice project logic
std::string title_text = _u8L("Slicing");
evt.status.text = title_text + evt.status.text;
// The plate's status callback has already prefixed the text with "Slicing plate N:".
if (evt.status.percent >= 0) {
if (!m_worker.is_idle()) {
// Avoid a race condition
@@ -13635,8 +13640,8 @@ static std::vector<wxString> collect_imex_warnings(PartPlate* plate)
const int secondary_bed_temp = bed_temps->values[secondary_logical];
if (secondary_bed_temp > 0 && std::abs(secondary_bed_temp - primary_bed_temp) > 5) {
warnings.push_back(wxString::Format(
_L("Bed temperature conflict: T%d (%s) sets the bed to %d\u00B0C \u2014 "
"T%d (%s) requires %d\u00B0C. The primary tool controls the bed; "
_L("Bed temperature conflict: T%d (%s) sets the bed to %d℃ \u2014 "
"T%d (%s) requires %d℃. The primary tool controls the bed; "
"secondary tools print at whatever temperature it sets."),
primary_tool, primary_display_type, primary_bed_temp,
tool_idx, secondary_display_type, secondary_bed_temp));
@@ -14984,24 +14989,8 @@ bool Plater::priv::can_fix_through_cgal() const
std::vector<int> obj_idxs, vol_idxs;
sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs);
#if FIX_THROUGH_CGAL_ALWAYS
// Fixing always.
// A closed mesh may still self-intersect, so fixing is always allowed.
return ! obj_idxs.empty() || ! vol_idxs.empty();
#else // FIX_THROUGH_CGAL_ALWAYS
// Fixing only if the model is not manifold.
if (vol_idxs.empty()) {
for (auto obj_idx : obj_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count() > 0)
return true;
return false;
}
int obj_idx = obj_idxs.front();
for (auto vol_idx : vol_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0)
return true;
return false;
#endif // FIX_THROUGH_CGAL_ALWAYS
}
bool Plater::priv::can_simplify() const
@@ -15714,7 +15703,7 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model,
name = preset->label(false);
}
if (name.empty())
name = "Filament " + std::to_string(i + 1);
name = into_u8(wxString::Format(_L("Filament %d"), int(i + 1)));
entry.name = name;
if (entry.kind == TextureFilamentKind::ExistingMixed) {
@@ -16842,7 +16831,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
if (accels.empty()) {
accels.assign({accel});
const auto msg{_L("INFO:") + "\n" +
_L("No accelerations provided for calibration. Use default acceleration value ") + std::to_string(long(accel)) + _L(u8"mm/s²")};
format_wxstr(_L("No accelerations provided for calibration. Use default acceleration value %1%mm/s²"), long(accel))};
get_notification_manager()->push_notification(msg.utf8_string());
} else {
// set max acceleration in case of batch mode to get correct test pattern size
@@ -16907,7 +16896,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
speeds.assign({speed});
const auto msg{_L("INFO:") + "\n" +
_L("No speeds provided for calibration. Use default optimal speed ") + std::to_string(long(speed)) + _L("mm/s")};
format_wxstr(_L("No speeds provided for calibration. Use default optimal speed %1%mm/s"), long(speed))};
get_notification_manager()->push_notification(msg.utf8_string());
} else if (speeds.size() == 1) {
// If we have single value provided, set speed using global configuration.
@@ -17331,7 +17320,7 @@ void Plater::calib_temp(const Calib_Params& params) {
if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") &&
! boost::filesystem::exists(belt_temp_tower_asset(params))) {
MessageDialog dlg(static_cast<wxWindow *>(wxGetApp().mainframe),
format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. "
format_wxstr(_L("No belt temperature tower is available for the range %1% to %2%℃. "
"Use a range the tower models cover, for example 230 to 190."),
(int) lround(params.start), (int) lround(params.end)),
_L("Temperature tower"), wxICON_ERROR | wxOK);
@@ -18089,11 +18078,80 @@ void Plater::load_gcode(const wxString& filename)
m_last_loaded_gcode = filename;
// An OrcaSlicer G-code carries the settings it was sliced with. They replace the selected presets as a
// .gcode.3mf's do, so unsaved preset changes get the save-or-discard choice of opening a project rather
// than the new project's choice to keep them.
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const std::string path = into_u8(filename);
std::optional<DynamicPrintConfig> file_config;
ConfigSubstitutions substitutions;
{
wxBusyCursor wait;
try {
file_config = preset_bundle->config_from_gcode(path, ForwardCompatibilitySubstitutionRule::Enable, substitutions);
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no settings to load from " << path << ": " << ex.what();
}
}
// BSS: create a new project when load_gcode, force close previous one
if (new_project(false, true) != wxID_YES)
if (file_config) {
auto check = [](bool yes_or_no) {
return yes_or_no || wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified."));
};
if (close_with_confirm(check) == wxID_CANCEL || new_project(true, true) != wxID_YES)
return;
} else if (new_project(false, true) != wxID_YES)
return;
m_only_gcode = true;
m_gcode_settings_loaded = false;
// A previewed G-code's settings are not the selection the app should remember.
PresetBundle::SelectionExportBlocker no_selection_export(*preset_bundle);
// What loading the file's settings replaces, to put back if that load fails or the file is rejected.
std::optional<PresetBundle::Selection> previous;
if (file_config) {
previous = preset_bundle->selection();
if (!previous)
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": project presets are still loaded, not loading the settings of " << path;
}
auto unload_file_settings = [this, preset_bundle, &previous]() {
if (!previous)
return;
preset_bundle->restore_selection(*previous);
wxGetApp().load_current_presets(false, false);
// Switching back to the previous printer re-derives some project settings (TabPrinter::on_preset_loaded),
// and only the printer refresh shows them in the sidebar's flow selectors.
preset_bundle->project_config = std::move(previous->project_config);
sidebar().update_presets(Preset::TYPE_PRINTER);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
m_gcode_settings_loaded = false;
// The rejections after the switch to the preview have already shown the sidebar with the file's settings.
enable_sidebar(false);
previous.reset();
};
if (previous) {
try {
preset_bundle->load_config_model(path, std::move(*file_config));
m_gcode_settings_loaded = true;
} catch (const std::exception& ex) {
unload_file_settings();
get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::WarningNotificationLevel,
GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), path) +
"\n" + ex.what());
}
}
if (m_gcode_settings_loaded) {
wxGetApp().load_current_presets(false, false);
sync_loaded_project_filament_config(preset_bundle->project_config);
sidebar().update_presets(Preset::TYPE_FILAMENT);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
if (!substitutions.empty())
show_substitutions_info(substitutions, path);
}
// cleanup view before to start loading/processing
//BBS: update gcode to current partplate's
@@ -18121,6 +18179,7 @@ void Plater::load_gcode(const wxString& filename)
}
catch (const std::exception& ex)
{
unload_file_settings();
show_error(this, ex.what());
return;
}
@@ -18168,7 +18227,8 @@ void Plater::load_gcode(const wxString& filename)
p->preview->get_canvas3d()->zoom_to_plate(0);
if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) {
MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("Does not contain valid G-code."),
unload_file_settings();
MessageDialog(this, format_wxstr(_L("The selected file:\n%1%\ndoes not contain valid G-code."), filename),
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal();
set_project_filename(_L(DEFAULT_PROJECT_NAME));
} else {
@@ -22041,13 +22101,16 @@ void Plater::clear_before_change_mesh(int obj_idx)
mv->mmu_segmentation_facets.reset();
mv->fuzzy_skin_facets.reset();
}
if (paint_removed) {
// snapshot_time is captured by copy so the lambda knows where to undo/redo to.
get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing."));
}
if (paint_removed)
notify_painting_removed();
}
void Plater::notify_painting_removed()
{
get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing."));
}
void Plater::changed_mesh(int obj_idx)
@@ -23893,7 +23956,7 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
const PartPlate* plate = p->partplate_list.get_curr_plate();
if (!plate) {
t.label = "T" + std::to_string(physical_head) + " -> (no plate)";
t.label = Slic3r::format(_u8L("T%1% -> (no plate)"), physical_head);
return t;
}
@@ -23904,8 +23967,8 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
// No filament slot resolves to this physical head. Usually means the printer's
// extruder count is understated relative to the filament palette (user added a
// filament without extending printer_extruder_id in the Machine tab).
t.label = "T" + std::to_string(physical_head) + " — no filament routed\n"
"Add an extruder in the Machine tab so this head has a filament slot.";
// TRN IDEX/IQEX ghost tooltip. %1% is the number of the physical print head.
t.label = Slic3r::format(_u8L("T%1% — no filament routed\nAdd an extruder in the Machine tab so this head has a filament slot."), physical_head);
return t;
}
t.filament_slot_1based = logical + 1;
@@ -23913,8 +23976,8 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
// from one resolution, not two.
t.swatch = plate->get_imex_head_filament_color(physical_head, pem, map);
t.swatch.a(1.0f); // tooltip swatch opaque
t.label = "T" + std::to_string(physical_head) +
" -> filament " + std::to_string(t.filament_slot_1based);
// TRN IDEX/IQEX ghost tooltip. %1% is the number of the physical print head, %2% the filament it prints.
t.label = Slic3r::format(_u8L("T%1% -> filament %2%"), physical_head, t.filament_slot_1based);
return t;
}
+6
View File
@@ -418,6 +418,9 @@ public:
bool is_gcode_3mf() { return m_exported_file; }
bool only_gcode_mode() { return m_only_gcode; }
void set_only_gcode(bool only_gcode) { m_only_gcode = only_gcode; }
// The previewed G-code's own settings were loaded into the presets, as a .gcode.3mf's are: the bed already is the
// one it was sliced for, and the sidebar shows them.
bool gcode_settings_loaded() const { return m_only_gcode && m_gcode_settings_loaded; }
//BBS: add only gcode mode
bool using_exported_file() { return m_exported_file; }
@@ -599,6 +602,7 @@ public:
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
void clear_before_change_mesh(int obj_idx);
void notify_painting_removed();
void changed_mesh(int obj_idx);
void changed_object(ModelObject &object);
@@ -1081,6 +1085,8 @@ private:
wxString m_last_loaded_gcode;
//BBS: add only gcode mode
bool m_only_gcode { false };//just for .gcode file not for .gcode.3mf
// Set by load_gcode() for the file it previews, so it means nothing once m_only_gcode is cleared.
bool m_gcode_settings_loaded { false };
bool m_exported_file { false };
bool skip_thumbnail_invalid { false };
bool m_loading_project { false };
+5 -5
View File
@@ -299,7 +299,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
item.display_name = descriptor.name;
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
descriptor.normalized_error() :
(descriptor.description.empty() ? "No description." : descriptor.description);
(descriptor.description.empty() ? _u8L("No description.") : descriptor.description);
item.author = descriptor.author;
item.version = descriptor.version;
// The cloud merge overwrites `version` with the latest cloud version, so prefer the preserved local
@@ -639,9 +639,9 @@ nlohmann::json PluginsDialog::localized_strings() const
{"no_plugins", _u8L("No plugins found")},
{"no_match", _u8L("No plugins match \"%s\"")},
{"source_mine", _u8L("Mine")},
{"source_subscribed", _u8L("Subscribed")},
{"source_subscribed", _u8L_CONTEXT("Subscribed", "Plugin")},
{"source_orphaned", _u8L("Orphaned")},
{"source_local", _u8L("Local")},
{"source_local", _u8L_CONTEXT("Local", "Plugin")},
{"mixed_capabilities", _u8L("Some plugin capabilities are enabled")},
{"collapse_named", _u8L("Collapse %s capabilities")},
{"expand_named", _u8L("Expand %s capabilities")},
@@ -1094,7 +1094,7 @@ bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const st
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
error = _u8L("Unknown error");
}
if (!downloaded) {
@@ -1225,7 +1225,7 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
error = _u8L("Unknown error");
}
if (!updated) {
+5 -3
View File
@@ -587,7 +587,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxString tooltip)
{
std::vector<wxString> Regions = {_L("Asia-Pacific"), _L("China"), _L("Europe"), _L("North America"), _L("Others")};
std::vector<wxString> Regions = {_L("Asia-Pacific"), _L("China"), _L("Europe"), _L("North America"), _L_CONTEXT("Others", "Region")};
std::vector<wxString> local_regions = {"Asia-Pacific", "China", "Europe", "North America", "Others"};
auto vlist = Regions;
@@ -1435,8 +1435,10 @@ wxBoxSizer *PreferencesDialog::create_item_network_plugin_version(wxString title
#ifdef WIN32
wxBoxSizer* PreferencesDialog::create_item_link_association( wxString url_prefix, wxString website_name)
{
wxString title = _L("Associate") + (boost::format(" %1%://") % url_prefix.c_str()).str();
wxString tooltip = _L("Associate") + " " + url_prefix + ":// " + _L("with OrcaSlicer so that Orca can open models from") + " " + website_name;
// TRN %s is a URL scheme, for example "prusaslicer".
wxString title = wxString::Format(_L("Associate %s://"), url_prefix);
// TRN The first %s is a URL scheme, for example "prusaslicer"; the second is a website name.
wxString tooltip = wxString::Format(_L("Associate %s:// with OrcaSlicer so that Orca can open models from %s"), url_prefix, website_name);
std::wstring registered_bin; // not used, just here to provide a ref to check fn
bool reg_to_current_instance = wxGetApp().check_url_association(url_prefix.ToStdWstring(), registered_bin);
+1 -1
View File
@@ -218,7 +218,7 @@ nlohmann::json PresetBundleDialog::localized_strings() const
{"update", _u8L("Update")},
{"export_presets", _u8L("Export Presets")},
{"close", _u8L("Close")},
{"open_folder", _u8L("Open folder in explorer")},
{"open_folder", _u8L("Show in folder")},
{"delete_bundle", _u8L("Delete bundle")},
{"unsubscribe_bundle", _u8L("Unsubscribe bundle")},
{"update_available", _u8L("Update available")},
+4 -2
View File
@@ -817,7 +817,8 @@ void FlashforgePrintHostSendDialog::init()
content_sizer->Add(m_flashforge_options_sizer, 0, wxEXPAND);
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
m_status_text->SetLabel(wxString::Format(_L_PLURAL("Detected %d IFS slot on printer.", "Detected %d IFS slots on printer.", m_slots.size()),
static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
@@ -949,7 +950,8 @@ void FlashforgePrintHostSendDialog::load_slots()
m_use_material_station = m_supports_material_station;
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
m_status_text->SetLabel(wxString::Format(_L_PLURAL("Detected %d IFS slot on printer.", "Detected %d IFS slots on printer.", m_slots.size()),
static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
}
+2 -2
View File
@@ -1312,9 +1312,9 @@ void PrinterFileSystem::RequestUploadFile()
if (result != SUCCESS && result != CONTINUE && result != FILE_EXIST) {
std::string error_msg = "";
if (result == ERROR_CANCEL) {
error_msg = _L("User cancels task.").ToStdString();
error_msg = _u8L("User cancels task.");
} else if (result == FILE_READ_WRITE_ERR || result == FILE_OPEN_ERR) {
error_msg = _L("Failed to read file, please try again.").ToStdString();
error_msg = _u8L("Failed to read file, please try again.");
}
wxLogWarning("PrinterFileSystem::UploadFile error: %d\n", result);
SendChangedEvent(EVT_UPLOAD_CHANGED, FF_UPLOADCANCEL, error_msg, result);
+1 -1
View File
@@ -56,7 +56,7 @@ PurgeModeDialog::PurgeModeDialog(wxWindow *parent, PurgeModeDialogType dialog_ty
bool is_fast_mode = (m_dialog_type == PurgeModeDialogType::FastMode);
m_standard_panel = new PurgeModeBtnPanel(options_panel, _L("Standard"), _L("Perform full purging with speed and temperature transition for the best print quality."),
m_standard_panel = new PurgeModeBtnPanel(options_panel, _L_CONTEXT("Standard", "Mode"), _L("Perform full purging with speed and temperature transition for the best print quality."),
"shield");
m_standard_panel->SetMinSize(wxSize(FromDIP(200), FromDIP(150)));
m_standard_panel->Select(mode == pvmDefault);
+5 -2
View File
@@ -25,8 +25,11 @@ namespace Slic3r { namespace GUI {
RecenterDialog::RecenterDialog(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style)
: DPIDialog(parent, id, _L("Confirm"), pos, size, style)
{
hint1 = _L("Please home all axes (click ");
hint2 = _L(") to locate the toolhead's position. This prevents device moving beyond the printable boundary and causing equipment wear.");
// TRN %1% marks where the Home button icon is drawn; keep it in the translation.
const wxString hint = _L("Please home all axes (click %1%) to locate the toolhead's position. This prevents device moving beyond the printable boundary and causing equipment wear.");
const int icon_pos = hint.Find("%1%");
hint1 = icon_pos == wxNOT_FOUND ? hint : hint.Left(icon_pos);
hint2 = icon_pos == wxNOT_FOUND ? wxString() : hint.Mid(icon_pos + 3);
init_bitmap();
+5 -3
View File
@@ -946,7 +946,7 @@ void SelectMachineDialog::init_bind()
if (!obj) return;
if (obj->get_dev_id() == e.GetString()) {
m_printer_box->SetPrinterName(wxString::FromUTF8(obj->get_dev_name()) + "(LAN)");
m_printer_box->SetPrinterName(wxString::FromUTF8(obj->get_dev_name()) + _L("(LAN)"));
}
}
});
@@ -4358,7 +4358,9 @@ void SelectMachineDialog::update_filament_change_count()
m_mapping_sugs_sizer->Show(true);
m_change_filament_times_sizer->Show(true);
m_txt_change_filament_times->Show(true);
m_txt_change_filament_times->SetLabel(wxString::Format(_L("Cost %dg filament and %d changes more than optimal grouping."), saving_weight, hand_changes_count));
m_txt_change_filament_times->SetLabel(wxString::Format(_L_PLURAL("Cost %dg filament and %d change more than optimal grouping.",
"Cost %dg filament and %d changes more than optimal grouping.", hand_changes_count),
saving_weight, hand_changes_count));
m_txt_change_filament_times->Wrap(FromDIP(580));
m_txt_change_filament_times->Layout();
}
@@ -7062,7 +7064,7 @@ void PrinterInfoBox::SetPrinters(const std::vector<MachineObject*>& sorted_print
{
wxString shown_dev_name = wxString::FromUTF8(obj->get_dev_name());
if (obj->is_lan_mode_printer()) {
shown_dev_name += "(LAN)";
shown_dev_name += _L("(LAN)");
}
DropDown::Item drop_item;
+2 -1
View File
@@ -732,10 +732,11 @@ void SelectMachinePopup::update_user_devices()
if (mobj) {
AppConfig* config = wxGetApp().app_config;
if (config) {
config->clear_local_machine_access_code(mobj->get_dev_id(), mobj->printer_agent_id);
config->erase_local_machine(mobj->get_dev_id());
}
mobj->set_access_code("");
mobj->revoke_access();
}
if (GUI::wxGetApp().plater())
+28 -61
View File
@@ -638,26 +638,21 @@ void Selection::set_printable(bool printable)
}
bool Selection::get_auto_drop() const {
// The wipe tower is not a ModelObject and has no per-instance auto_drop;
// The wipe tower is not a ModelObject and has no auto_drop state;
// return the default to avoid indexing m_model->objects with its synthetic id.
if (!m_valid || is_wipe_tower())
return true;
std::set<std::pair<int, int>> instances_idxs;
std::set<int> object_idxs;
for (const auto& [obj_idx, inst_list] : m_cache.content) {
for (int inst_idx : inst_list) {
instances_idxs.insert({obj_idx, inst_idx});
}
object_idxs.insert(obj_idx);
}
// return false, if one of the instances in the selection has auto_drop disabled, otherwise return true
for (const auto& [obj_idx, inst_idx] : instances_idxs) {
// Return false if one selected object has auto_drop disabled.
for (int obj_idx : object_idxs) {
ModelObject* object = m_model->objects[obj_idx];
for (const auto* inst : object->instances) {
if (!inst->auto_drop) {
return false;
}
}
if (!object->auto_drop)
return false;
}
return true;
@@ -668,22 +663,18 @@ void Selection::set_auto_drop(bool enabled)
if (!m_valid)
return;
std::set<std::pair<int, int>> instances_idxs;
std::set<int> object_idxs;
for (const auto& [obj_idx, inst_list] : m_cache.content) {
for (int inst_idx : inst_list) {
instances_idxs.insert({obj_idx, inst_idx});
}
object_idxs.insert(obj_idx);
}
std::string snapshot_text = (boost::format("%1%") % (enabled ? "Set Selection Auto-Drop Enabled" : "Set Selection Auto-Drop Disabled")).str();
wxGetApp().plater()->take_snapshot(snapshot_text);
// set auto_drop value for all instances in object
for (const auto& [obj_idx, inst_idx] : instances_idxs) {
// auto_drop is shared by all instances of an object.
for (int obj_idx : object_idxs) {
ModelObject* object = m_model->objects[obj_idx];
for (auto* inst : object->instances) {
inst->auto_drop = enabled;
}
object->auto_drop = enabled;
}
// update scene
@@ -1766,50 +1757,27 @@ void Selection::translate(unsigned int object_idx, unsigned int instance_idx, co
//BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": obj %1%, instance %2%") % object_idx % instance_idx;
//BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": %1%, displacement {%2%, %3%, %4%}") % __LINE__ % displacement(X) % displacement(Y) % displacement(Z);
for (unsigned int i : m_list) {
GLVolume& v = *(*m_volumes)[i];
if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx)
v.set_instance_offset(v.get_instance_offset() + displacement);
}
std::set<unsigned int> done; // prevent processing volumes twice
done.insert(m_list.begin(), m_list.end());
for (unsigned int i : m_list) {
if (done.size() == m_volumes->size())
break;
int object_idx = (*m_volumes)[i]->object_idx();
if (object_idx >= 1000)
continue;
// Process unselected volumes of the object.
for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) {
if (done.size() == m_volumes->size())
break;
if (done.find(j) != done.end())
continue;
GLVolume& v = *(*m_volumes)[j];
if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx)
continue;
v.set_instance_offset(v.get_instance_offset() + displacement);
done.insert(j);
}
// Every volume of the instance, selected or not, so that the instance moves as a whole.
for (GLVolume* v : *m_volumes) {
if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx)
v->set_instance_offset(v->get_instance_offset() + displacement);
}
this->set_bounding_boxes_dirty();
}
void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement) {
void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local)
{
if (!m_valid) return;
for (unsigned int i : m_list) {
GLVolume &v = *(*m_volumes)[i];
if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx)
v.set_volume_offset(v.get_volume_offset() + displacement);
if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx) {
Vec3d local_displacement = displacement;
if (!local)
local_displacement = v.get_instance_transformation().get_matrix_no_offset().inverse() * displacement;
v.set_volume_offset(v.get_volume_offset() + local_displacement);
}
}
this->set_bounding_boxes_dirty();
@@ -3088,8 +3056,9 @@ void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_
else if (sync_rotation_type != SyncRotationType::NONE || mirrored)
new_inst_trafo_j.linear() = (old_inst_trafo_j.linear() * old_inst_trafo_i.linear().inverse()) * curr_inst_trafo_i.linear();
bool should_synchronize_z = m_model->objects[volume_j->object_idx()]->instances[volume_j->instance_idx()]->auto_drop == false;
if (should_synchronize_z && wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA)
// Instances of the same object must remain at the same height. auto_drop
// only controls snapping after the move, not the instance transform itself.
if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA)
new_inst_trafo_j.translation().z() = curr_inst_trafo_i.translation().z();
assert(is_rotation_xy_synchronized(curr_inst_trafo_i, new_inst_trafo_j));
@@ -3136,9 +3105,7 @@ void Selection::ensure_on_bed()
for (size_t i = 0; i < m_volumes->size(); ++i) {
GLVolume* volume = (*m_volumes)[i];
ModelObject* mo = m_model->objects[volume->object_idx()];
ModelInstance* mi = mo->instances[volume->instance_idx()];
if (mi->auto_drop == false
if (mo->auto_drop == false
|| volume->is_wipe_tower
|| volume->is_modifier
|| std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) != m_cache.sinking_volumes.end())
+2 -1
View File
@@ -360,7 +360,8 @@ public:
void translate(unsigned int object_idx, const Vec3d& displacement);
void translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement);
void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement);
// local: true (default) if displacement is already in instance-local space; false treats it as world-space delta (L^-1 applied).
void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local = true);
void rotate(unsigned int object_idx, unsigned int instance_idx, const Transform3d &overwrite_tran);
void rotate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Transform3d &overwrite_tran);
+1 -1
View File
@@ -1187,7 +1187,7 @@ void SendToPrinterDialog::update_user_printer()
m_list.push_back(it->second);
wxString dev_name_text = from_u8(it->second->get_dev_name());
if (it->second->is_lan_mode_printer()) {
dev_name_text += "(LAN)";
dev_name_text += _L("(LAN)");
}
machine_list_name.Add(dev_name_text);
break;
+4 -1
View File
@@ -3,6 +3,7 @@
#include "ActionRegistry.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include "NotificationManager.hpp"
@@ -93,6 +94,8 @@ nlohmann::json speed_dial_ui_strings()
const std::string alt = GUI::shortkey_alt_prefix();
const std::string ctrl = GUI::shortkey_ctrl_prefix();
return {
// TRN Screen-reader label of a Speed Dial action: the first %s is the action name, the second where it comes from (a menu, tab or settings group). Keep the two %s in this order.
{"sd_action_from", _u8L("%s from %s")},
{"shortcut_alt", alt},
{"shortcut_ctrl", ctrl},
@@ -155,7 +158,7 @@ SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight),
wxSize(kPopupWidth, kPopupMinHeight))) {
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20));
sizer->Add(new wxStaticText(this, wxID_ANY, _L("Web view is unavailable.")), wxSizerFlags().Border(wxALL, 20));
SetSizer(sizer);
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
}
+18 -13
View File
@@ -698,7 +698,7 @@ void PrintingTaskPanel::create_panel(wxWindow* parent)
m_button_pause_resume = new ScalableButton(progress_lr_panel, wxID_ANY, "print_control_pause", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER,true);
m_button_pause_resume->Bind(wxEVT_ENTER_WINDOW, [this](auto &e) {
if (m_button_pause_resume->GetToolTipText() == _L("Pause")) {
if (m_button_pause_resume->GetToolTipText() == _L_CONTEXT("Pause", "Verb")) {
m_button_pause_resume->SetBitmap_("print_control_pause_hover");
}
@@ -709,7 +709,7 @@ void PrintingTaskPanel::create_panel(wxWindow* parent)
m_button_pause_resume->Bind(wxEVT_LEAVE_WINDOW, [this](auto &e) {
auto buf = m_button_pause_resume->GetClientData();
if (m_button_pause_resume->GetToolTipText() == _L("Pause")) {
if (m_button_pause_resume->GetToolTipText() == _L_CONTEXT("Pause", "Verb")) {
m_button_pause_resume->SetBitmap_("print_control_pause");
}
@@ -1163,7 +1163,7 @@ void PrintingTaskPanel::enable_pause_resume_button(bool enable, std::string type
}
else if (type == "pause") {
m_button_pause_resume->SetBitmap_("print_control_pause");
if (m_button_pause_resume->GetToolTipText() != _L("Pause")) { m_button_pause_resume->SetToolTip(_L("Pause")); }
if (m_button_pause_resume->GetToolTipText() != _L_CONTEXT("Pause", "Verb")) { m_button_pause_resume->SetToolTip(_L_CONTEXT("Pause", "Verb")); }
}
}
}
@@ -1518,7 +1518,7 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
wxBoxSizer *bSizer_monitoring_title;
bSizer_monitoring_title = new wxBoxSizer(wxHORIZONTAL);
m_staticText_monitoring = new Label(m_panel_monitoring_title, _L("Camera"));
m_staticText_monitoring = new Label(m_panel_monitoring_title, _L_CONTEXT("Camera", "Printer camera"));
m_staticText_monitoring->Wrap(-1);
//m_staticText_monitoring->SetFont(PAGE_TITLE_FONT);
m_staticText_monitoring->SetForegroundColour(PAGE_TITLE_FONT_COL);
@@ -3876,7 +3876,9 @@ void StatusPanel::update_subtask(MachineObject *obj)
if (obj->queue_number <= 0) {
prepare_text = wxString::Format(_L("Cloud Slicing..."));
} else {
prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %s tasks ahead of you."), std::to_string(obj->queue_number));
prepare_text = wxString::Format(_L_PLURAL("In Cloud Slicing Queue, there is %d task ahead of you.",
"In Cloud Slicing Queue, there are %d tasks ahead of you.", obj->queue_number),
obj->queue_number);
show_percent = false;
}
} else
@@ -4954,7 +4956,7 @@ void StatusPanel::on_switch_speed(wxCommandEvent &event)
popUp->SetSize(em * 36, em * 8);
step->SetHint(_L("This only takes effect during printing"));
step->AppendItem(_L("Silent"), "");
step->AppendItem(_L("Standard"), "");
step->AppendItem(_L_CONTEXT("Standard", "Mode"), "");
step->AppendItem(_L("Sport"), "");
step->AppendItem(_L("Ludicrous"), "");
@@ -6063,22 +6065,26 @@ wxBoxSizer *ScoreDialog::get_button_sizer()
<< "] http_error [" << http_error << "] config [" << config << "] image_path [" << need_upload.second << "]";
break;
case -1:
error_info += need_upload.second + _L(" upload failed").ToUTF8().data() + "\n\thttp code:" + std::to_string(http_code) + "\n\thttp_error:" + http_error + "\n";
// TRN %s is the path of an image file.
error_info += wxString::Format(_L("%s upload failed"), need_upload.second) + "\n\thttp code:" + std::to_string(http_code) + "\n\thttp_error:" + http_error + "\n";
m_upload_status_code = StatusCode::UPLOAD_IMG_FAILED;
++it;
break;
case BAMBU_NETWORK_ERR_PARSE_CONFIG_FAILED:
error_info += need_upload.second + _L(" upload config prase failed\n").ToUTF8().data() + "\n";
// TRN %s is the path of an image file.
error_info += wxString::Format(_L("%s upload config parse failed"), need_upload.second) + "\n\n";
m_upload_status_code = StatusCode::UPLOAD_IMG_FAILED;
++it;
break;
case BAMBU_NETWORK_ERR_NO_CORRESPONDING_BUCKET:
error_info += need_upload.second + _L(" No corresponding storage bucket\n").ToUTF8().data() + "\n";
// TRN %s is the path of an image file.
error_info += wxString::Format(_L("%s: No corresponding storage bucket"), need_upload.second) + "\n\n";
m_upload_status_code = StatusCode::UPLOAD_IMG_FAILED;
++it;
break;
case BAMBU_NETWORK_ERR_OPEN_FILE_FAILED:
error_info += need_upload.second + _L(" cannot be opened\n").ToUTF8().data() + "\n";
// TRN %s is the path of an image file.
error_info += wxString::Format(_L("%s cannot be opened"), need_upload.second) + "\n\n";
m_upload_status_code = StatusCode::UPLOAD_IMG_FAILED;
++it;
break;
@@ -6118,9 +6124,8 @@ wxBoxSizer *ScoreDialog::get_button_sizer()
if (!error_info.empty()) { BOOST_LOG_TRIVIAL(info) << error_info; }
dlg_info = new MessageDialog(this,
_L("Your comment result cannot be uploaded due to the following reasons:\n\n error code: ") +
std::to_string(http_code) + "\n " + _L("error message: ") + http_error +
_L("\n\nWould you like to redirect to the webpage to give a rating?"),
wxString::Format(_L("Your comment result cannot be uploaded due to the following reasons:\n\n error code: %d\n error message: %s\n\nWould you like to redirect to the webpage to give a rating?"),
int(http_code), wxString::FromUTF8(http_error)),
wxString(_L("info")), wxOK | wxNO | wxCENTER);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": upload rating picture failed and http error" << http_error;
if (dlg_info->ShowModal() == wxID_OK) {

Some files were not shown because too many files have changed in this diff Show More