mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 23:01:17 +00:00
Commit Design bodies to the plate as one assembly, keeping their positions
Commit to Plate now sends all visible bodies to Prepare as one object with a part per body, so their relative placement survives. A dropdown beside the button switches to Commit to Plate (as bodies), the previous one-object-per-body behaviour, and the choice is remembered.
This commit is contained in:
@@ -13,8 +13,9 @@ known.
|
||||
Its coupling to the rest of the application is deliberately narrow. It adds no stage to the
|
||||
slicing pipeline and touches neither the preset system nor `Tab`. It reaches the rest of Orca
|
||||
in two places: **Commit to Plate**, which hands finished solids to Prepare as ordinary model
|
||||
objects, and one optional 3MF archive entry that carries the recipe. Everything else is
|
||||
contained in `src/libslic3r/CAD/` and `src/slic3r/GUI/CAD/`.
|
||||
objects (by default one assembly object with a part per body, which keeps the bodies' relative
|
||||
placement, or one object per body), and one optional 3MF archive entry that carries the recipe.
|
||||
Everything else is contained in `src/libslic3r/CAD/` and `src/slic3r/GUI/CAD/`.
|
||||
|
||||
The user-facing manual lives in the wiki
|
||||
([Design Tab](https://www.orcaslicer.com/wiki/design_tab)), not here. This document covers the
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><path d="M33.5,4H5.5A1.5,1.5,0,0,0,4,5.5v28A1.5,1.5,0,0,0,5.5,35h16a.5.5,0,0,0,0-1H5.5a.5.5,0,0,1-.5-.5V5.5A.5.5,0,0,1,5.5,5h28a.5.5,0,0,1,.5.5v16a.5.5,0,0,0,1,0V5.5A1.5,1.5,0,0,0,33.5,4Z" style="fill:#2b3436"/><path d="M19.5,12.5H32.5M6.5,19.5H32.5M6.5,26.5H21.5M12.5,19.5V32.5M19.5,6.5V32.5M26.5,6.5V21.5" style="fill:none;stroke:#2b3436;stroke-linecap:round"/><ellipse cx="12" cy="8.5" rx="5.5" ry="2" style="fill:none;stroke:#009688"/><path d="M6.5,8.5v7a5.5,2,0,0,0,11,0v-7" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><path d="M33.5,24h-8A1.5,1.5,0,0,0,24,25.5v8A1.5,1.5,0,0,0,25.5,35h8A1.5,1.5,0,0,0,35,33.5v-8A1.5,1.5,0,0,0,33.5,24Zm.5,9.5a.5.5,0,0,1-.5.5h-8a.5.5,0,0,1-.5-.5v-8a.5.5,0,0,1,.5-.5h8a.5.5,0,0,1,.5.5Z" style="fill:#009688"/><path d="M32.5,29H30V26.5a.5.5,0,0,0-1,0V29H26.5a.5.5,0,0,0,0,1H29v2.5a.5.5,0,0,0,1,0V30h2.5a.5.5,0,0,0,0-1Z" style="fill:#009688"/></svg>
|
||||
|
After Width: | Height: | Size: 1000 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><path d="M33.5,4H5.5A1.5,1.5,0,0,0,4,5.5v28A1.5,1.5,0,0,0,5.5,35h16a.5.5,0,0,0,0-1H5.5a.5.5,0,0,1-.5-.5V5.5A.5.5,0,0,1,5.5,5h28a.5.5,0,0,1,.5.5v16a.5.5,0,0,0,1,0V5.5A1.5,1.5,0,0,0,33.5,4Z" style="fill:#b6b6b6"/><path d="M19.5,12.5H32.5M6.5,19.5H32.5M6.5,26.5H21.5M12.5,19.5V32.5M19.5,6.5V32.5M26.5,6.5V21.5" style="fill:none;stroke:#b6b6b6;stroke-linecap:round"/><ellipse cx="12" cy="8.5" rx="5.5" ry="2" style="fill:none;stroke:#009688"/><path d="M6.5,8.5v7a5.5,2,0,0,0,11,0v-7" style="fill:none;stroke:#009688;stroke-linecap:round;stroke-linejoin:round"/><path d="M33.5,24h-8A1.5,1.5,0,0,0,24,25.5v8A1.5,1.5,0,0,0,25.5,35h8A1.5,1.5,0,0,0,35,33.5v-8A1.5,1.5,0,0,0,33.5,24Zm.5,9.5a.5.5,0,0,1-.5.5h-8a.5.5,0,0,1-.5-.5v-8a.5.5,0,0,1,.5-.5h8a.5.5,0,0,1,.5.5Z" style="fill:#009688"/><path d="M32.5,29H30V26.5a.5.5,0,0,0-1,0V29H26.5a.5.5,0,0,0,0,1H29v2.5a.5.5,0,0,0,1,0V30h2.5a.5.5,0,0,0,0-1Z" style="fill:#009688"/></svg>
|
||||
|
After Width: | Height: | Size: 1000 B |
@@ -45,6 +45,11 @@
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#if defined(__WXMSW__) && wxUSE_POPUPWIN
|
||||
#include <wx/popupwin.h>
|
||||
extern wxPopupWindow* wxCurrentPopupWindow;
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
@@ -198,7 +203,14 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
|
||||
// …but NOT while an inline value field is open: the field floats over the canvas, so
|
||||
// the smallest pointer jiggle re-enters the viewport and would yank focus off the
|
||||
// field (the "no cursor focus on the number, click to focus" bug).
|
||||
if (m_canvas_widget && !m_sketch_tool.inline_busy()) m_canvas_widget->SetFocus();
|
||||
bool take_focus = m_canvas_widget && !m_sketch_tool.inline_busy();
|
||||
#if defined(__WXMSW__) && wxUSE_POPUPWIN
|
||||
// …nor while a popup is open. A ribbon dropdown opens over the viewport, and the pointer
|
||||
// crosses the viewport on its way to it: taking focus then reactivates the frame, which
|
||||
// on MSW dismisses the popup under the pointer. GLCanvas3D::on_mouse has the same guard.
|
||||
take_focus = take_focus && !wxCurrentPopupWindow;
|
||||
#endif
|
||||
if (take_focus) m_canvas_widget->SetFocus();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
|
||||
@@ -267,6 +267,15 @@ static ::Button* sidebar_icon_btn(wxWindow* parent, const char* icon, const wxSt
|
||||
return b;
|
||||
}
|
||||
|
||||
// Commit to Plate's faces: Prepare's add-plate glyph, drawn for its light GL toolbar, and the same
|
||||
// glyph with a body on the plate for "as bodies". Each ships a "_dark" twin for the dark ribbon,
|
||||
// picked the way GLToolbar picks it.
|
||||
static std::string commit_icon(bool bodies)
|
||||
{
|
||||
const std::string name = bodies ? "toolbar_add_plate_bodies" : "toolbar_add_plate";
|
||||
return dp_dark() ? name + "_dark" : name;
|
||||
}
|
||||
|
||||
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
|
||||
enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
|
||||
|
||||
@@ -1606,11 +1615,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// Some Orca glyphs (toolbar_flatten) are drawn for Prepare's light GL toolbar and come out
|
||||
// the same tone as a dark ribbon. Those ship a "_dark" twin, picked per theme here (and
|
||||
// again on a theme switch) the way GLToolbar picks it.
|
||||
auto doc_btn = [this](const char* icon, const wxString& tip, bool has_dark_twin = false) {
|
||||
auto doc_btn = [this](const char* icon, const wxString& tip, bool has_dark_twin = false,
|
||||
int cell_w = 40, int icon_px = 34) {
|
||||
const std::string name(icon);
|
||||
auto themed = [name, has_dark_twin] { return has_dark_twin && dp_dark() ? name + "_dark" : name; };
|
||||
auto* b = new ScalableButton(m_toolbar, wxID_ANY, themed(), "", FromDIP(wxSize(40, 40)),
|
||||
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 34);
|
||||
auto* b = new ScalableButton(m_toolbar, wxID_ANY, themed(), "", FromDIP(wxSize(cell_w, 40)),
|
||||
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, icon_px);
|
||||
if (has_dark_twin)
|
||||
m_icon_refresh.push_back([b, themed] { b->SetBitmap_(themed()); });
|
||||
b->SetToolTip(tip);
|
||||
@@ -1668,12 +1678,73 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_section_flip_btn->Enable(false); // only usable while a section view is active
|
||||
tb_slot["flip"].push_back(m_section_flip_btn);
|
||||
|
||||
// Commit is the tab's primary action and sits far right, next to Confirm/Cancel.
|
||||
// Commit is the tab's primary action and sits far right, next to Confirm/Cancel. A split
|
||||
// button like Prepare's Slice: the face commits in the current mode, the chevron's
|
||||
// dropdown only switches the mode.
|
||||
m_tb_commit = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* b_commit = doc_btn("toolbar_add_plate", _L("Commit to Plate — send the solid to Prepare"),
|
||||
true);
|
||||
b_commit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_commit(); });
|
||||
m_tb_commit->Add(b_commit, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
|
||||
m_commit_btn = doc_btn("toolbar_add_plate", wxEmptyString); // face and tip: set_commit_mode
|
||||
m_commit_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_commit(); });
|
||||
auto* b_mode = doc_btn("drop_down", _L("Choose how Commit to Plate sends the bodies"), false, 16, 16);
|
||||
m_tb_commit->Add(m_commit_btn, 0, wxALIGN_CENTER_VERTICAL);
|
||||
m_tb_commit->Add(b_mode, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
|
||||
|
||||
// Same themed-DropDown shape as the FEATURE flyouts; row index == int(CommitMode).
|
||||
struct CommitFlyout {
|
||||
std::vector<DropDown::Item> items;
|
||||
wxLongLong closed_ms; // when the list last closed (wxGetLocalTimeMillis)
|
||||
DropDown drop; // declared LAST: destroyed before the vector it references
|
||||
CommitFlyout() : drop(items) {}
|
||||
};
|
||||
auto cf = std::make_shared<CommitFlyout>();
|
||||
cf->items.resize(2);
|
||||
cf->items[int(CommitMode::Assembly)].text = _L("Commit to Plate");
|
||||
cf->items[int(CommitMode::Assembly)].tip = _L("All bodies become one object with a part per body, "
|
||||
"keeping their relative positions");
|
||||
cf->items[int(CommitMode::Bodies)].text = _L("Commit to Plate (as bodies)");
|
||||
cf->items[int(CommitMode::Bodies)].tip = _L("Each body becomes its own object, placed on its own");
|
||||
CommitFlyout* cp = cf.get();
|
||||
auto refresh_rows = [this, cp] {
|
||||
for (size_t i = 0; i < cp->items.size(); ++i)
|
||||
cp->items[i].icon = create_scaled_bitmap(commit_icon(i == size_t(CommitMode::Bodies)), m_toolbar, 18);
|
||||
};
|
||||
refresh_rows();
|
||||
cp->drop.Create(m_commit_btn);
|
||||
cp->drop.SetUseContentWidth(true, false);
|
||||
cp->drop.Invalidate(true);
|
||||
m_commit_drop = &cp->drop;
|
||||
cp->drop.Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) {
|
||||
const bool bodies = e.GetInt() == int(CommitMode::Bodies);
|
||||
set_commit_mode(bodies ? CommitMode::Bodies : CommitMode::Assembly);
|
||||
wxGetApp().app_config->set("design_commit_mode", bodies ? "bodies" : "assembly");
|
||||
});
|
||||
cp->drop.Bind(EVT_DISMISS, [cp](wxCommandEvent&) { cp->closed_ms = wxGetLocalTimeMillis(); });
|
||||
b_mode->Bind(wxEVT_BUTTON, [this, b_mode, cp](wxCommandEvent&) {
|
||||
// A click on ▾ while the list is open closes it, and that same click reaches this
|
||||
// button too (on MSW the list's deferred dismissal runs before the button's mouse-up):
|
||||
// it must not reopen the list.
|
||||
if (cp->drop.IsShown() || wxGetLocalTimeMillis() - cp->closed_ms < 300)
|
||||
return;
|
||||
// A fresh content measure before Popup(), as the FEATURE flyouts do. Invalidate(true)
|
||||
// also clears the selection, which is the check on the current mode: put it back.
|
||||
m_commit_drop->Invalidate(true);
|
||||
m_commit_drop->SetUseContentWidth(false, false);
|
||||
m_commit_drop->SetUseContentWidth(true, false);
|
||||
m_commit_drop->SetSelection(int(m_commit_mode));
|
||||
// Right-aligned under the split button, which sits at the ribbon's far right.
|
||||
wxPoint pos = b_mode->ClientToScreen(wxPoint(b_mode->GetSize().x, -6));
|
||||
pos.x -= m_commit_drop->GetSize().x;
|
||||
m_commit_drop->Position(pos, wxSize(0, b_mode->GetSize().y + b_mode->FromDIP(12)));
|
||||
m_commit_drop->Popup();
|
||||
});
|
||||
m_icon_refresh.push_back([this, refresh_rows] {
|
||||
refresh_rows();
|
||||
m_commit_drop->Invalidate(true);
|
||||
set_commit_mode(m_commit_mode); // the face's theme twin, and the check Invalidate cleared
|
||||
});
|
||||
m_flyout_keepalive.push_back(cf);
|
||||
// Assembly unless the user picked "as bodies": it is the mode that keeps the design as drawn.
|
||||
set_commit_mode(wxGetApp().app_config->get("design_commit_mode") == "bodies" ? CommitMode::Bodies
|
||||
: CommitMode::Assembly);
|
||||
}
|
||||
|
||||
// Feature-group layout, in the requested left-to-right order.
|
||||
@@ -9898,6 +9969,19 @@ void DesignPanel::on_export_step()
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
void DesignPanel::set_commit_mode(CommitMode mode)
|
||||
{
|
||||
m_commit_mode = mode;
|
||||
const bool bodies = mode == CommitMode::Bodies;
|
||||
if (m_commit_btn) {
|
||||
m_commit_btn->SetBitmap_(commit_icon(bodies));
|
||||
m_commit_btn->SetToolTip(bodies ? _L("Commit to Plate (as bodies) — send each body to Prepare as its own object")
|
||||
: _L("Commit to Plate — send the solid to Prepare"));
|
||||
}
|
||||
if (m_commit_drop)
|
||||
m_commit_drop->SetSelection(int(mode)); // the check on the current row
|
||||
}
|
||||
|
||||
void DesignPanel::on_commit()
|
||||
{
|
||||
// A feature tool open with a live preview ghost (e.g. a fillet being previewed) is
|
||||
@@ -9914,24 +9998,28 @@ void DesignPanel::on_commit()
|
||||
if (obj_list == nullptr)
|
||||
return;
|
||||
|
||||
// Multi-body: ship each (visible) body as its own plate object so they arrive on the
|
||||
// slicer plate as independent, separately-arrangeable parts (Onshape "Commit all parts").
|
||||
// Multi-body: ship the (visible) bodies as the parts of one assembly object that keeps
|
||||
// their placement (the default), or each as its own plate object so they arrive as
|
||||
// independent, separately-arrangeable parts (Onshape "Commit all parts").
|
||||
// Hidden bodies are skipped — what you see on the Design plate is what gets committed.
|
||||
sync_body_visible();
|
||||
rebuild_disp_meshes(); // ship moved bodies at their Move-gizmo positions
|
||||
if (m_disp_body_meshes.size() > 1) {
|
||||
int committed = 0;
|
||||
std::vector<std::pair<const TriangleMesh*, wxString>> parts;
|
||||
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;
|
||||
obj_list->load_mesh_object(m_disp_body_meshes[b],
|
||||
"Design Body " + std::to_string(b + 1));
|
||||
++committed;
|
||||
parts.emplace_back(&m_disp_body_meshes[b], wxString::FromUTF8("Design Body " + std::to_string(b + 1)));
|
||||
}
|
||||
if (committed == 0) { // every body hidden — nothing to ship
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class wxStaticText;
|
||||
class wxStaticLine;
|
||||
class Button; // Orca-styled button (Widgets/Button.hpp)
|
||||
class CheckBox; // Orca teal checkbox (Widgets/CheckBox.hpp)
|
||||
class DropDown; // Orca themed popup list (Widgets/DropDown.hpp)
|
||||
class wxSizer;
|
||||
// wxBoxSizer, wxTextCtrl and wxListCtrl are used here as pointers only, so a forward
|
||||
// declaration is enough — but they must be declared. Every ordinary build happened to pull
|
||||
@@ -237,6 +238,11 @@ private:
|
||||
// a modal dialog editing the feature's placement transform in place.
|
||||
void on_transform_imported(int feat_idx);
|
||||
void on_commit();
|
||||
// What Commit to Plate sends: one object with a part per body (an assembly, which keeps the
|
||||
// bodies' relative placement), or, "as bodies", one Prepare object per body. The split
|
||||
// button's dropdown switches it, like Prepare's Slice button, without committing.
|
||||
enum class CommitMode { Assembly, Bodies };
|
||||
void set_commit_mode(CommitMode mode);
|
||||
void on_export_step(); // write all bodies to a .step file (native B-rep)
|
||||
// Rehydrate the parametric model from a project's saved recipe (3MF
|
||||
// Metadata/orca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
|
||||
@@ -400,7 +406,10 @@ private:
|
||||
void show_move_card(bool show);
|
||||
void apply_move_card(); // numeric move/rotate -> same xform the gizmo builds
|
||||
void push_polygon_params();
|
||||
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate, beside Confirm/Cancel
|
||||
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate split button, beside Confirm/Cancel
|
||||
CommitMode m_commit_mode{CommitMode::Assembly};
|
||||
ScalableButton* m_commit_btn{nullptr}; // main face: runs the current commit mode
|
||||
DropDown* m_commit_drop{nullptr}; // commit-mode choices, owned via m_flyout_keepalive
|
||||
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
|
||||
CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
|
||||
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
|
||||
|
||||
@@ -2662,6 +2662,11 @@ void ObjectList::load_shape_object(const std::string &type_name)
|
||||
}
|
||||
|
||||
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center)
|
||||
{
|
||||
load_mesh_object(std::vector<std::pair<const TriangleMesh*, wxString>>{{&mesh, name}}, name, center);
|
||||
}
|
||||
|
||||
void ObjectList::load_mesh_object(const std::vector<std::pair<const TriangleMesh*, wxString>> &parts, const wxString &name, bool center)
|
||||
{
|
||||
// Add mesh to model as a new object
|
||||
Model& model = wxGetApp().plater()->model();
|
||||
@@ -2671,14 +2676,19 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name
|
||||
#endif /* _DEBUG */
|
||||
|
||||
std::vector<size_t> object_idxs;
|
||||
auto bb = mesh.bounding_box();
|
||||
BoundingBoxf3 bb;
|
||||
ModelObject* new_object = model.add_object();
|
||||
new_object->name = into_u8(name);
|
||||
new_object->add_instance(); // each object should have at least one instance
|
||||
|
||||
ModelVolume* new_volume = new_object->add_volume(mesh);
|
||||
// add_volume() centres each part on its own geometry, so the part offsets carry the
|
||||
// meshes' placement relative to each other.
|
||||
for (const auto& [mesh, part_name] : parts) {
|
||||
bb.merge(mesh->bounding_box());
|
||||
ModelVolume* new_volume = new_object->add_volume(*mesh);
|
||||
new_volume->name = into_u8(part_name);
|
||||
}
|
||||
new_object->sort_volumes(true);
|
||||
new_volume->name = into_u8(name);
|
||||
// set a default extruder value, since user can't add it manually
|
||||
// BBS
|
||||
new_object->config.set_key_value("extruder", new ConfigOptionInt(1));
|
||||
|
||||
@@ -314,6 +314,8 @@ public:
|
||||
void load_generic_subobject(const std::string& type_name, const ModelVolumeType type);
|
||||
void load_shape_object(const std::string &type_name);
|
||||
void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true);
|
||||
// One object holding one part per mesh; the parts keep their placement relative to each other.
|
||||
void load_mesh_object(const std::vector<std::pair<const TriangleMesh*, wxString>> &parts, const wxString &name, bool center = true);
|
||||
// BBS
|
||||
void switch_to_object_process();
|
||||
bool del_object(const int obj_idx, bool refresh_immediately = true);
|
||||
|
||||
Reference in New Issue
Block a user