mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
Design tab: reference planes at bed centre, slot dims, hole cube handle, real threads
Port of the snaporca CAD work to the mainline fork. - Onshape default planes (XY/XZ/YZ) at the bed centre (transparent, labelled); modeling origin unified to the bed centre (CadDocument::modeling_origin); world-axis triad moved to the bed centre on the Design canvas only. - Datum plane: clickable ghost-plane base pick + draggable offset arrow. - Slot: dims reassessed to inter-centre distance / radius / angle; fixed the duplicate cap-arc radius quote. - Hole: 3D cube move-handle on the face; binds to the face on the first click; decluttered side-distance construction lines. - Thread: derive the M spec (diameter/pitch/depth) from a picked cylindrical surface or circular edge (GeometryEngine::circle_of_edge); fuse the helical ridge onto the existing body; MakePipeShell fixed-binormal sweep (uniform, no twist) + self-intersection/param guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
852804450c
commit
71b7a73e86
+391
-65
@@ -37,6 +37,7 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/BuildVolume.hpp"
|
||||
#include "slic3r/GUI/MainFrame.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
|
||||
@@ -394,6 +395,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
auto* b_plane = icon_btn("design_plane", _L("Plane"));
|
||||
b_plane->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
populate_plane_choices(m_plane_base); // refresh base list w/ existing datum planes
|
||||
reset_plane_refs(); // fresh datum: no captured face/edge refs
|
||||
open_tool(Tool::Plane);
|
||||
});
|
||||
fadd(b_plane);
|
||||
@@ -474,38 +476,40 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
open_tool(Tool::Hole);
|
||||
}},
|
||||
{"design_thread", _L("Thread"), _L("Thread a picked cylindrical face (hole bore or cylinder)"),
|
||||
{"design_thread", _L("Thread"), _L("Thread a cylindrical surface (inner bore / outer) or a circular edge"),
|
||||
[this] {
|
||||
// #3: invoke on a picked CYLINDRICAL face — a hole bore (internal thread) or a
|
||||
// cylinder's lateral surface (external) — deriving axis, radius and internal/
|
||||
// external from it. Otherwise fall back to the plane dropdown.
|
||||
// Driven by a picked CYLINDRICAL surface (inner bore = internal, outer = external)
|
||||
// OR a circular EDGE (a cylinder's rim) — axis + diameter come from the geometry, so
|
||||
// the user never types a radius. The diameter field shows what was derived.
|
||||
m_thread_on_face = false;
|
||||
m_thread_face_body = -1;
|
||||
if (m_sel_solid_face >= 0 && m_sel_solid_body >= 0
|
||||
&& m_sel_solid_body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Face face = GeometryEngine::face_by_index(
|
||||
m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_face);
|
||||
const GeometryEngine::CylinderFace cf = GeometryEngine::cylinder_of_face(face);
|
||||
if (cf.ok) {
|
||||
SketchPlane p; // plane on the axis (origin at the base)
|
||||
p.origin = cf.base;
|
||||
p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
if (m_thread_radius) m_thread_radius->SetValue(cf.radius);
|
||||
if (m_thread_height) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
if (m_thread_x) m_thread_x->SetValue(0.0); // on the axis
|
||||
if (m_thread_y) m_thread_y->SetValue(0.0);
|
||||
} else if (m_sel_solid_face >= 0) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
m_status->SetLabel(_L("Pick a cylindrical face (hole bore or cylinder) for a thread"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
GeometryEngine::CylinderFace cf;
|
||||
if (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Shape& shape = m_doc.bodies[m_sel_solid_body].shape;
|
||||
if (m_sel_solid_face >= 0)
|
||||
cf = GeometryEngine::cylinder_of_face(GeometryEngine::face_by_index(shape, m_sel_solid_face));
|
||||
if (!cf.ok && m_sel_solid_edge >= 0)
|
||||
cf = GeometryEngine::circle_of_edge(GeometryEngine::edge_by_index(shape, m_sel_solid_edge));
|
||||
}
|
||||
if (cf.ok) {
|
||||
SketchPlane p; // plane on the axis (origin at the base)
|
||||
p.origin = cf.base;
|
||||
p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
infer_thread_spec(2.0 * cf.radius); // M diameter + pitch + depth from the cylinder
|
||||
if (m_thread_height && cf.height > 1e-6) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
if (m_thread_x) m_thread_x->SetValue(0.0); // on the axis
|
||||
if (m_thread_y) m_thread_y->SetValue(0.0);
|
||||
} else if (m_sel_solid_face >= 0 || m_sel_solid_edge >= 0) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
m_status->SetLabel(_L("Pick a cylindrical surface (bore / outer) or a circular edge for a thread"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
open_tool(Tool::Thread);
|
||||
}},
|
||||
@@ -664,6 +668,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
_L("Click a segment to trim it back to its nearest intersection; right-click exits"));
|
||||
skbtn("design_extend", DesignSketchTool::Mode::Extend, _L("Extend"),
|
||||
_L("Click a line or arc to extend it to the nearest entity; right-click exits"));
|
||||
// Constrain — grouped with the edit tools so it's easy to find (nde #13: it was buried
|
||||
// far-right next to Construction and went unnoticed). Commits the live sketch in place
|
||||
// and drops into Constrain mode (geometric/dimensional palette).
|
||||
auto* b_constrain_sk = icon_btn("design_constrain",
|
||||
_L("Constrain — add geometric/dimensional relations"));
|
||||
b_constrain_sk->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { enter_constrain_inline(); });
|
||||
sadd(b_constrain_sk);
|
||||
dropdown("design_move", _L("Move / rotate / scale"), {
|
||||
{"design_move", DesignSketchTool::Mode::Move, _L("Move (translate)"), _L("Pick entities, then drag the handle or click the distance; click empty to apply")},
|
||||
{"design_rotate", DesignSketchTool::Mode::Rotate, _L("Rotate (about centroid)"), _L("Pick entities, then drag around the pivot or click the angle; click empty to apply")},
|
||||
@@ -692,15 +703,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
if (m_viewport) m_viewport->set_sketch_polygon_circumscribed(m_poly_circ->GetValue()); });
|
||||
sadd(m_poly_circ);
|
||||
add_sep(m_tb_sketch);
|
||||
// Constrain — reachable mid-sketch: commits the live sketch in place and drops into
|
||||
// Constrain mode, where the geometric/dimensional palette + Trim + Extend (scissors)
|
||||
// are picked and applied. (Trim/Extend are pick-then-apply, so they live there, not as
|
||||
// bare toolbar buttons.)
|
||||
auto* b_constrain_sk = icon_btn("design_constrain",
|
||||
_L("Constrain — add geometric/dimensional relations; Trim/Extend live here"));
|
||||
b_constrain_sk->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { enter_constrain_inline(); });
|
||||
sadd(b_constrain_sk);
|
||||
add_sep(m_tb_sketch);
|
||||
m_construction->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) {
|
||||
if (m_viewport && m_viewport->is_sketching())
|
||||
m_viewport->set_sketch_construction(m_construction->GetValue()); });
|
||||
@@ -994,8 +996,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_std);
|
||||
|
||||
m_thread_radius = make_spin(m_form, 5.0);
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Radius")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
|
||||
// surface / circular edge, so it's a readout users rarely type. Stored field holds the diameter.
|
||||
m_thread_radius = make_spin(m_form, 10.0);
|
||||
tform->Add(new wxStaticText(m_form, wxID_ANY, _L("Diameter")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_radius);
|
||||
|
||||
m_thread_pitch = make_spin(m_form, 2.0);
|
||||
@@ -1219,6 +1223,21 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_box_plane->Add(card_header("design_sketch", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(new wxStaticLine(m_form), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
// Plane type chooses which inputs matter (Onshape/Fusion parity):
|
||||
// Offset = Base (or Face A) + Offset (+ Tilt about a base axis)
|
||||
// Angle = Edge A (line) + Base/Face A reference + Angle°
|
||||
// Midplane = Face A + Face B (halfway between)
|
||||
// Tangent = Face A (a cylinder) + Angle° around its axis
|
||||
// Two edges = Edge A + Edge B
|
||||
// Coincident = Face A (lie on that face)
|
||||
m_plane_type = new wxChoice(m_form, wxID_ANY);
|
||||
for (const wxString& t : { _L("Offset"), _L("Angle"), _L("Midplane"),
|
||||
_L("Tangent"), _L("Two edges"), _L("Coincident") })
|
||||
m_plane_type->Append(t);
|
||||
m_plane_type->SetSelection(0);
|
||||
m_box_plane->Add(new wxStaticText(m_form, wxID_ANY, _L("Plane type")), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(m_plane_type, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
|
||||
auto* plform = new wxFlexGridSizer(2, 6, 8);
|
||||
|
||||
m_plane_base = new wxChoice(m_form, wxID_ANY);
|
||||
@@ -1231,7 +1250,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
plform->Add(m_plane_offset);
|
||||
|
||||
m_plane_tilt = make_spin(m_form, 0.0, -180.0, 180.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt°")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Angle°")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_tilt);
|
||||
|
||||
m_plane_tilt_axis = new wxChoice(m_form, wxID_ANY);
|
||||
@@ -1241,6 +1260,26 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Tilt axis")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_tilt_axis);
|
||||
|
||||
// Contextual reference picks: arm a target, then click a solid face/edge in the canvas.
|
||||
auto pick_row = [&](const wxString& label, wxButton*& btn, wxStaticText*& lbl, PlanePick target) {
|
||||
btn = new wxButton(m_form, wxID_ANY, label);
|
||||
lbl = new wxStaticText(m_form, wxID_ANY, _L("(none)"));
|
||||
btn->Bind(wxEVT_BUTTON, [this, target](wxCommandEvent&) { arm_plane_pick(target); });
|
||||
plform->Add(btn);
|
||||
plform->Add(lbl, 0, wxALIGN_CENTER_VERTICAL);
|
||||
};
|
||||
pick_row(_L("Pick Face A"), m_plane_pick_faceA, m_plane_faceA_lbl, PlanePick::FaceA);
|
||||
pick_row(_L("Pick Face B"), m_plane_pick_faceB, m_plane_faceB_lbl, PlanePick::FaceB);
|
||||
pick_row(_L("Pick Edge A"), m_plane_pick_edgeA, m_plane_edgeA_lbl, PlanePick::EdgeA);
|
||||
pick_row(_L("Pick Edge B"), m_plane_pick_edgeB, m_plane_edgeB_lbl, PlanePick::EdgeB);
|
||||
|
||||
m_plane_usize = make_spin(m_form, 60.0, 1.0, 100000.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Size U")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_usize);
|
||||
m_plane_vsize = make_spin(m_form, 60.0, 1.0, 100000.0);
|
||||
plform->Add(new wxStaticText(m_form, wxID_ANY, _L("Size V")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
plform->Add(m_plane_vsize);
|
||||
|
||||
m_box_plane->Add(plform, 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
}
|
||||
root->Add(m_box_plane, 0, wxEXPAND);
|
||||
@@ -1685,6 +1724,60 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
: _L("(pick a side face)"));
|
||||
refresh_preview();
|
||||
}
|
||||
// Hole card open: clicking a solid FACE re-targets the hole ONTO that face (Orca-style),
|
||||
// so the hole lives on the object's face — not on a stale dropdown/datum plane. Uses the
|
||||
// face under the cursor from the FIRST click (handle_solid_click reports it even at the
|
||||
// Whole level), so no whole->face cycle is needed.
|
||||
if (m_active == Tool::Hole && face >= 0 && body >= 0 && body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Face fc = GeometryEngine::face_by_index(m_doc.bodies[body].shape, face);
|
||||
if (!fc.IsNull()) {
|
||||
m_hole_face_plane = face_plane_inward(fc);
|
||||
m_hole_on_face = true;
|
||||
m_hole_face_body = body;
|
||||
m_hole_has_bounds = GeometryEngine::face_plane_bounds(
|
||||
fc, m_hole_face_plane.origin, m_hole_face_plane.x_axis,
|
||||
m_hole_face_plane.y_axis, m_hole_umin, m_hole_umax, m_hole_vmin, m_hole_vmax);
|
||||
if (m_hole_x) m_hole_x->SetValue(0.0); // centre of the picked face
|
||||
if (m_hole_y) m_hole_y->SetValue(0.0);
|
||||
if (m_hole_plane) m_hole_plane->SetSelection(index_from_plane(m_hole_face_plane));
|
||||
refresh_preview(); // re-place the gizmo + ghost on the new face
|
||||
}
|
||||
}
|
||||
// Thread card open: clicking a cylindrical face or circular edge re-derives the thread.
|
||||
if (m_active == Tool::Thread && body >= 0 && body < int(m_doc.bodies.size())) {
|
||||
const TopoDS_Shape& shape = m_doc.bodies[body].shape;
|
||||
GeometryEngine::CylinderFace cf;
|
||||
if (face >= 0)
|
||||
cf = GeometryEngine::cylinder_of_face(GeometryEngine::face_by_index(shape, face));
|
||||
if (!cf.ok && edge >= 0)
|
||||
cf = GeometryEngine::circle_of_edge(GeometryEngine::edge_by_index(shape, edge));
|
||||
if (cf.ok) {
|
||||
SketchPlane p; p.origin = cf.base; p.normal = cf.axis;
|
||||
const Vec3d ref = std::abs(cf.axis.z()) < 0.9 ? Vec3d(0, 0, 1) : Vec3d(1, 0, 0);
|
||||
p.x_axis = ref.cross(cf.axis).normalized();
|
||||
p.y_axis = cf.axis.cross(p.x_axis).normalized();
|
||||
m_thread_face_plane = p;
|
||||
m_thread_on_face = true;
|
||||
m_thread_face_body = m_sel_solid_body;
|
||||
infer_thread_spec(2.0 * cf.radius); // M diameter + pitch + depth from the cylinder
|
||||
if (m_thread_height && cf.height > 1e-6) m_thread_height->SetValue(cf.height);
|
||||
if (m_thread_internal) m_thread_internal->SetValue(cf.internal);
|
||||
refresh_preview();
|
||||
}
|
||||
}
|
||||
// Plane tool with a pick armed: capture the right kind of reference (face for Face A/B,
|
||||
// edge for Edge A/B). If the click wasn't the right kind, stay armed so the user retries.
|
||||
if (m_active == Tool::Plane && m_plane_pick != PlanePick::None) {
|
||||
bool got = false;
|
||||
switch (m_plane_pick) {
|
||||
case PlanePick::FaceA: if (m_sel_solid_face >= 0) { m_pl_faceA_body = m_sel_solid_body; m_pl_faceA = m_sel_solid_face; got = true; } break;
|
||||
case PlanePick::FaceB: if (m_sel_solid_face >= 0) { m_pl_faceB_body = m_sel_solid_body; m_pl_faceB = m_sel_solid_face; got = true; } break;
|
||||
case PlanePick::EdgeA: if (m_sel_solid_edge >= 0) { m_pl_edgeA_body = m_sel_solid_body; m_pl_edgeA = m_sel_solid_edge; got = true; } break;
|
||||
case PlanePick::EdgeB: if (m_sel_solid_edge >= 0) { m_pl_edgeB_body = m_sel_solid_body; m_pl_edgeB = m_sel_solid_edge; got = true; } break;
|
||||
default: break;
|
||||
}
|
||||
if (got) { m_plane_pick = PlanePick::None; refresh_plane_labels(); }
|
||||
}
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
const int nb = int(m_doc.bodies.size());
|
||||
const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString();
|
||||
@@ -1703,6 +1796,53 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
refresh_preview();
|
||||
});
|
||||
|
||||
// Datum-plane resize handles (C3): a handle drag reports the new u/v extent. Mirror it into the
|
||||
// Size spins and, when editing a committed datum, into the feature so the rendered rectangle
|
||||
// follows live. SetValue doesn't emit a command event, so no refresh_preview recursion.
|
||||
m_viewport->set_on_datum_size_changed([this](double u, double v) {
|
||||
if (m_plane_usize) m_plane_usize->SetValue(u);
|
||||
if (m_plane_vsize) m_plane_vsize->SetValue(v);
|
||||
if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane) {
|
||||
m_doc.features[m_edit_index].plane_u_size = u;
|
||||
m_doc.features[m_edit_index].plane_v_size = v;
|
||||
refresh_datum_planes(); // committed datum rectangle follows the drag
|
||||
}
|
||||
m_viewport->request_repaint();
|
||||
});
|
||||
|
||||
// Offset arrow drag: mirror the new offset into the spin + (when editing) the committed feature.
|
||||
m_viewport->set_on_datum_offset_changed([this](double off) {
|
||||
if (m_plane_offset) m_plane_offset->SetValue(off);
|
||||
if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane) {
|
||||
m_doc.features[m_edit_index].plane_offset = off;
|
||||
refresh_datum_planes();
|
||||
}
|
||||
m_viewport->request_repaint();
|
||||
});
|
||||
|
||||
// Clicking a ghost base plane sets the base graphically (replaces the dropdown). A base pick
|
||||
// drops any offset-from-face choice so the picked base plane wins, then re-resolves the preview.
|
||||
m_viewport->set_on_datum_base_picked([this](int base) {
|
||||
if (m_active == Tool::Plane) {
|
||||
// Plane tool open: the click sets the datum's base plane (replaces the dropdown).
|
||||
if (m_plane_base && base >= 0 && base < int(m_plane_base->GetCount()))
|
||||
m_plane_base->SetSelection(base);
|
||||
m_pl_faceA_body = m_pl_faceA = -1;
|
||||
refresh_plane_labels();
|
||||
refresh_preview(); // re-resolve the frame + move the gizmo/ghosts to the new base
|
||||
} else {
|
||||
// Fallback (no object yet): clicking a reference plane selects it as the sketch plane.
|
||||
if (m_draw_plane && base >= 0 && base < int(m_draw_plane->GetCount()))
|
||||
m_draw_plane->SetSelection(base);
|
||||
const char* nm = (base == 0) ? "XY" : (base == 1) ? "XZ" : (base == 2) ? "YZ" : "datum";
|
||||
m_status->SetForegroundColour(wxColour(120, 210, 120));
|
||||
m_status->SetLabel(wxString::Format(_L("%s plane selected — press Sketch to draw on it"), nm));
|
||||
m_status->Refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a
|
||||
// display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched,
|
||||
// so face/edge ids the dress-up ops target stay valid).
|
||||
@@ -1742,7 +1882,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_viewport->set_on_thread_changed([this](double x, double y, double radius, double height) {
|
||||
if (m_thread_x) m_thread_x->SetValue(x);
|
||||
if (m_thread_y) m_thread_y->SetValue(y);
|
||||
if (m_thread_radius) m_thread_radius->SetValue(radius);
|
||||
if (m_thread_radius) m_thread_radius->SetValue(2.0 * radius); // gizmo reports radius; field = diameter
|
||||
if (m_thread_height) m_thread_height->SetValue(height);
|
||||
refresh_preview();
|
||||
});
|
||||
@@ -2101,9 +2241,10 @@ void DesignPanel::add_imported_sketch(
|
||||
on_face = true;
|
||||
}
|
||||
}
|
||||
if (!on_face)
|
||||
f.plane = m_draw_plane ? plane_from_choice(m_draw_plane->GetSelection())
|
||||
: SketchPlane::XY();
|
||||
if (!on_face) {
|
||||
if (m_draw_plane) f.plane = plane_from_choice(m_draw_plane->GetSelection());
|
||||
else { f.plane = SketchPlane::XY(); f.plane.origin += m_doc.modeling_origin; }
|
||||
}
|
||||
|
||||
// Drop the live face selection (its body is now remembered on import_face_body): otherwise
|
||||
// the next Extrude would push/pull that face instead of extruding the placed art.
|
||||
@@ -2289,8 +2430,10 @@ void DesignPanel::on_add_dressup()
|
||||
|
||||
SketchPlane DesignPanel::hole_plane() const
|
||||
{
|
||||
return m_hole_on_face ? m_hole_face_plane
|
||||
: plane_from_index(m_hole_plane->GetSelection());
|
||||
if (m_hole_on_face) return m_hole_face_plane;
|
||||
SketchPlane p = plane_from_index(m_hole_plane->GetSelection());
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_hole()
|
||||
@@ -2323,8 +2466,10 @@ void DesignPanel::on_add_hole()
|
||||
|
||||
SketchPlane DesignPanel::thread_plane() const
|
||||
{
|
||||
return m_thread_on_face ? m_thread_face_plane
|
||||
: plane_from_index(m_thread_plane->GetSelection());
|
||||
if (m_thread_on_face) return m_thread_face_plane;
|
||||
SketchPlane p = plane_from_index(m_thread_plane->GetSelection());
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
|
||||
void DesignPanel::apply_thread_standard()
|
||||
@@ -2341,13 +2486,13 @@ void DesignPanel::apply_thread_standard()
|
||||
if (m_thread_pitch) m_thread_pitch->SetValue(s->pitch_mm);
|
||||
if (m_thread_depth) m_thread_depth->SetValue(s->thread_depth_mm());
|
||||
|
||||
// Nominal radius: external rod = major radius; internal tapped bore = minor
|
||||
// (tap-drill) radius. On a picked cylindrical face the radius comes from the
|
||||
// real geometry, so don't override it there.
|
||||
// Nominal diameter: external rod = major diameter; internal tapped bore = minor (tap-drill)
|
||||
// diameter. On a picked cylindrical surface/edge the diameter comes from the real geometry,
|
||||
// so don't override it there. (The field holds DIAMETER.)
|
||||
if (!m_thread_on_face && m_thread_radius) {
|
||||
const bool internal = m_thread_internal && m_thread_internal->GetValue();
|
||||
const double d = internal ? s->minor_diameter_mm() : s->major_diameter_mm;
|
||||
m_thread_radius->SetValue(0.5 * d);
|
||||
m_thread_radius->SetValue(d);
|
||||
}
|
||||
|
||||
if (m_status)
|
||||
@@ -2355,6 +2500,24 @@ void DesignPanel::apply_thread_standard()
|
||||
m_thread_std->GetString(sel), s->pitch_mm));
|
||||
}
|
||||
|
||||
void DesignPanel::infer_thread_spec(double diameter)
|
||||
{
|
||||
// Snap to the nearest standard thread by nominal (major) diameter, so picking a Ø9.9 boss
|
||||
// gives M10 — the M diameter, pitch AND depth all follow from the cylinder's base diameter.
|
||||
const auto& stds = thread_standards();
|
||||
int best = -1; double bestErr = 1e30;
|
||||
for (int i = 0; i < int(stds.size()); ++i) {
|
||||
const double e = std::abs(stds[i].major_diameter_mm - diameter);
|
||||
if (e < bestErr) { bestErr = e; best = i; }
|
||||
}
|
||||
if (best < 0) { if (m_thread_radius) m_thread_radius->SetValue(diameter); return; }
|
||||
const ThreadSpec& s = stds[best];
|
||||
if (m_thread_std) m_thread_std->SetSelection(best + 1); // row 0 is "Custom"
|
||||
if (m_thread_radius) m_thread_radius->SetValue(s.major_diameter_mm); // field = DIAMETER
|
||||
if (m_thread_pitch) m_thread_pitch->SetValue(s.pitch_mm);
|
||||
if (m_thread_depth) m_thread_depth->SetValue(s.thread_depth_mm());
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_thread()
|
||||
{
|
||||
bool internal = m_thread_internal->GetValue();
|
||||
@@ -2365,7 +2528,7 @@ void DesignPanel::on_add_thread()
|
||||
SketchPlane plane = thread_plane();
|
||||
|
||||
m_feature_counter++;
|
||||
const int tidx = m_doc.add_thread(m_thread_radius->GetValue(), m_thread_pitch->GetValue(),
|
||||
const int tidx = m_doc.add_thread(m_thread_radius->GetValue() * 0.5, m_thread_pitch->GetValue(),
|
||||
m_thread_height->GetValue(), m_thread_depth->GetValue(),
|
||||
internal, m_thread_x->GetValue(), m_thread_y->GetValue(),
|
||||
plane, "Thread" + std::to_string(m_feature_counter));
|
||||
@@ -2554,18 +2717,63 @@ void DesignPanel::populate_plane_choices(wxChoice* c) const
|
||||
|
||||
SketchPlane DesignPanel::plane_from_choice(int row) const
|
||||
{
|
||||
if (row < 3) return plane_from_index(row); // 0=XY,1=XZ,2=YZ
|
||||
if (row < 3) { // 0=XY,1=XZ,2=YZ through the modeling origin
|
||||
SketchPlane p = plane_from_index(row);
|
||||
p.origin += m_doc.modeling_origin;
|
||||
return p;
|
||||
}
|
||||
// Datums are already in world coords (resolve_datum_planes applied the origin to their base).
|
||||
auto datums = m_doc.resolve_datum_planes();
|
||||
const int di = row - 3;
|
||||
return (di >= 0 && di < int(datums.size())) ? datums[di].second : SketchPlane::XY();
|
||||
if (di >= 0 && di < int(datums.size())) return datums[di].second;
|
||||
SketchPlane p = SketchPlane::XY(); p.origin += m_doc.modeling_origin; return p;
|
||||
}
|
||||
|
||||
void DesignPanel::apply_plane_refs(CadFeature& f) const
|
||||
{
|
||||
f.plane_type = (PlaneType)m_plane_type->GetSelection();
|
||||
f.plane_face_body = m_pl_faceA_body; f.plane_face = m_pl_faceA;
|
||||
f.plane_face2_body = m_pl_faceB_body; f.plane_face2 = m_pl_faceB;
|
||||
f.plane_edge_body = m_pl_edgeA_body; f.plane_edge = m_pl_edgeA;
|
||||
f.plane_edge2_body = m_pl_edgeB_body; f.plane_edge2 = m_pl_edgeB;
|
||||
f.plane_u_size = m_plane_usize->GetValue();
|
||||
f.plane_v_size = m_plane_vsize->GetValue();
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_plane_labels()
|
||||
{
|
||||
auto txt = [](int idx) { return idx >= 0 ? wxString::Format("#%d", idx) : wxString(_L("(none)")); };
|
||||
if (m_plane_faceA_lbl) m_plane_faceA_lbl->SetLabel(txt(m_pl_faceA));
|
||||
if (m_plane_faceB_lbl) m_plane_faceB_lbl->SetLabel(txt(m_pl_faceB));
|
||||
if (m_plane_edgeA_lbl) m_plane_edgeA_lbl->SetLabel(txt(m_pl_edgeA));
|
||||
if (m_plane_edgeB_lbl) m_plane_edgeB_lbl->SetLabel(txt(m_pl_edgeB));
|
||||
}
|
||||
|
||||
void DesignPanel::reset_plane_refs()
|
||||
{
|
||||
m_pl_faceA_body = m_pl_faceA = -1; m_pl_faceB_body = m_pl_faceB = -1;
|
||||
m_pl_edgeA_body = m_pl_edgeA = -1; m_pl_edgeB_body = m_pl_edgeB = -1;
|
||||
m_plane_pick = PlanePick::None;
|
||||
refresh_plane_labels();
|
||||
}
|
||||
|
||||
void DesignPanel::arm_plane_pick(PlanePick target)
|
||||
{
|
||||
m_plane_pick = target;
|
||||
const bool face = (target == PlanePick::FaceA || target == PlanePick::FaceB);
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(face ? _L("Click a solid FACE in the viewport")
|
||||
: _L("Click a solid EDGE in the viewport"));
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
void DesignPanel::on_add_plane()
|
||||
{
|
||||
m_feature_counter++;
|
||||
m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
|
||||
"Plane" + std::to_string(m_feature_counter));
|
||||
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
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(_L("Plane added — pick it as a sketch plane"));
|
||||
@@ -2644,6 +2852,13 @@ void DesignPanel::on_tab_shown()
|
||||
{
|
||||
if (m_viewport) m_viewport->refresh_bed();
|
||||
|
||||
// Modeling origin = bed centre, set BEFORE any recompute/datum-resolve so sketches and datums
|
||||
// land in the middle of the bed (not the bed corner = world 0).
|
||||
if (Plater* pl = wxGetApp().plater()) {
|
||||
const Vec2d bc = pl->build_volume().bed_center();
|
||||
m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0);
|
||||
}
|
||||
|
||||
// Rehydrate the parametric model from a freshly loaded project (the 3MF carried the
|
||||
// recipe in Metadata/SnapOrca_cad.bin). Only when nothing is in progress here, so we
|
||||
// never clobber an active design when the user just toggles back to the Design tab.
|
||||
@@ -2653,6 +2868,7 @@ void DesignPanel::on_tab_shown()
|
||||
if (!blob.empty()) load_recipe(blob);
|
||||
}
|
||||
}
|
||||
update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
|
||||
}
|
||||
|
||||
void DesignPanel::load_recipe(const std::string& blob)
|
||||
@@ -2681,11 +2897,8 @@ void DesignPanel::refresh_tree()
|
||||
wxTreeItemId root = m_tree->AddRoot("root");
|
||||
// Datum/reference planes carry no solid; feed them to the viewport so they render as
|
||||
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
|
||||
if (m_viewport) {
|
||||
std::vector<SketchPlane> dplanes;
|
||||
for (const auto& dp : m_doc.resolve_datum_planes()) dplanes.push_back(dp.second);
|
||||
m_viewport->set_datum_planes(std::move(dplanes));
|
||||
}
|
||||
refresh_datum_planes();
|
||||
update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
|
||||
for (const auto& f : m_doc.features) {
|
||||
const int img = tree_icon_for(f.type);
|
||||
wxTreeItemId id = m_tree->AppendItem(root, wxString::FromUTF8(f.name), img, img);
|
||||
@@ -2903,8 +3116,15 @@ void DesignPanel::after_tree_edit(bool ok)
|
||||
sync_sketch_display(); // empty body: show any un-consumed committed sketch
|
||||
m_status->SetLabel(wxString());
|
||||
} else {
|
||||
// nde #19/20: a delete/reorder that leaves bodies behind must re-feed the per-body
|
||||
// GLVolumes — otherwise the viewport keeps showing the pre-edit solid (the deleted
|
||||
// feature's artifact lingered). feed_bodies() is idempotent for the edit/replace path.
|
||||
if (m_viewport != nullptr) feed_bodies();
|
||||
set_status_ok();
|
||||
}
|
||||
// Force a frame: under software GL (llvmpipe on the :10 test box) reload()'s scheduled
|
||||
// Refresh() is dropped, so a deleted solid stayed on screen until the next orbit.
|
||||
if (m_viewport != nullptr) m_viewport->request_repaint();
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
@@ -4341,7 +4561,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
|
||||
break;
|
||||
case CadFeatureType::Thread:
|
||||
m_thread_plane->SetSelection(index_from_plane(f.plane));
|
||||
m_thread_radius->SetValue(f.thread_radius);
|
||||
m_thread_radius->SetValue(f.thread_radius * 2.0); // field = diameter
|
||||
m_thread_pitch->SetValue(f.thread_pitch);
|
||||
m_thread_height->SetValue(f.thread_height);
|
||||
m_thread_depth->SetValue(f.thread_depth);
|
||||
@@ -4382,6 +4602,15 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
|
||||
m_plane_offset->SetValue(f.plane_offset);
|
||||
m_plane_tilt->SetValue(f.plane_angle_tilt);
|
||||
m_plane_tilt_axis->SetSelection(f.plane_axis);
|
||||
m_plane_type->SetSelection((int)f.plane_type);
|
||||
m_pl_faceA_body = f.plane_face_body; m_pl_faceA = f.plane_face;
|
||||
m_pl_faceB_body = f.plane_face2_body; m_pl_faceB = f.plane_face2;
|
||||
m_pl_edgeA_body = f.plane_edge_body; m_pl_edgeA = f.plane_edge;
|
||||
m_pl_edgeB_body = f.plane_edge2_body; m_pl_edgeB = f.plane_edge2;
|
||||
m_plane_usize->SetValue(f.plane_u_size);
|
||||
m_plane_vsize->SetValue(f.plane_v_size);
|
||||
m_plane_pick = PlanePick::None;
|
||||
refresh_plane_labels();
|
||||
break;
|
||||
case CadFeatureType::Loft:
|
||||
m_loft_refs = f.loft_profile_refs; // show_tool re-checks these in the list
|
||||
@@ -4637,7 +4866,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const
|
||||
case Tool::Thread:
|
||||
f.type = CadFeatureType::Thread;
|
||||
f.plane = thread_plane();
|
||||
f.thread_radius = m_thread_radius->GetValue();
|
||||
f.thread_radius = m_thread_radius->GetValue() * 0.5; // field = diameter -> kernel radius
|
||||
f.thread_pitch = m_thread_pitch->GetValue();
|
||||
f.thread_height = m_thread_height->GetValue();
|
||||
f.thread_depth = m_thread_depth->GetValue();
|
||||
@@ -4688,6 +4917,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const
|
||||
f.plane_offset = m_plane_offset->GetValue();
|
||||
f.plane_angle_tilt = m_plane_tilt->GetValue();
|
||||
f.plane_axis = m_plane_tilt_axis->GetSelection();
|
||||
apply_plane_refs(f); // plane_type + face/edge refs + u/v size from the card
|
||||
break;
|
||||
case Tool::Loft: {
|
||||
f.type = CadFeatureType::Loft;
|
||||
@@ -4773,7 +5003,7 @@ void DesignPanel::update_thread_gizmo()
|
||||
const SketchPlane plane = thread_plane();
|
||||
m_viewport->begin_thread_gizmo(plane,
|
||||
m_thread_x->GetValue(), m_thread_y->GetValue(),
|
||||
m_thread_radius->GetValue(), m_thread_height->GetValue());
|
||||
m_thread_radius->GetValue() * 0.5, m_thread_height->GetValue());
|
||||
}
|
||||
|
||||
// Anchor an inward thickness arrow at the picked open face's centroid (along -outward-normal).
|
||||
@@ -4939,6 +5169,97 @@ void DesignPanel::update_extrude_gizmo()
|
||||
end == ExtrudeEnd::TwoSided, m_flip->GetValue());
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_datum_planes()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
std::vector<SketchPlane> dplanes;
|
||||
for (const auto& dp : m_doc.resolve_datum_planes()) dplanes.push_back(dp.second);
|
||||
// Parallel per-plane extents, in the SAME order resolve_datum_planes emits
|
||||
// (enabled Plane features, document order), so each datum draws at its u/v size.
|
||||
std::vector<Vec2d> dsizes;
|
||||
for (const auto& f : m_doc.features)
|
||||
if (f.type == CadFeatureType::Plane && f.enabled)
|
||||
dsizes.emplace_back(f.plane_u_size, f.plane_v_size);
|
||||
m_viewport->set_datum_planes(std::move(dplanes), std::move(dsizes));
|
||||
}
|
||||
|
||||
void DesignPanel::update_datum_gizmo()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
if (m_active != Tool::Plane) { m_viewport->clear_datum_gizmo(); update_reference_planes(); return; }
|
||||
|
||||
// Resolve the candidate plane's FRAME against the doc. resolve_datum_planes() is const and
|
||||
// doesn't rebuild bodies, so transiently swap/append the candidate to read its resolved frame,
|
||||
// then restore — works for both a fresh (uncommitted) plane and an edit of a committed one.
|
||||
CadFeature f = build_candidate(Tool::Plane);
|
||||
const bool editing = (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size()) &&
|
||||
m_doc.features[m_edit_index].type == CadFeatureType::Plane);
|
||||
CadFeature saved;
|
||||
int slot;
|
||||
if (editing) { saved = m_doc.features[m_edit_index]; m_doc.features[m_edit_index] = f; slot = m_edit_index; }
|
||||
else { m_doc.features.push_back(f); slot = int(m_doc.features.size()) - 1; }
|
||||
|
||||
auto datums = m_doc.resolve_datum_planes();
|
||||
// Ordinal of `slot` among enabled Plane features = its index in the resolved list.
|
||||
int ord = -1;
|
||||
for (int i = 0; i <= slot; ++i)
|
||||
if (m_doc.features[i].type == CadFeatureType::Plane && m_doc.features[i].enabled) ++ord;
|
||||
const bool ok = (ord >= 0 && ord < int(datums.size()));
|
||||
SketchPlane frame; if (ok) frame = datums[ord].second;
|
||||
|
||||
if (editing) m_doc.features[m_edit_index] = saved; else m_doc.features.pop_back();
|
||||
|
||||
if (!ok) { m_viewport->clear_datum_gizmo(); update_reference_planes(); return; }
|
||||
|
||||
// Offset arrow anchor = the base/face origin (datum origin walked back along its normal by the
|
||||
// offset). Works for both Offset-from-base (no tilt) and Offset-from-face exactly.
|
||||
const Vec3d anchor = frame.origin - frame.normal * f.plane_offset;
|
||||
const bool offset_on = (f.plane_type == PlaneType::Offset);
|
||||
m_viewport->set_datum_gizmo(frame, f.plane_u_size, f.plane_v_size,
|
||||
anchor, frame.normal, f.plane_offset, offset_on);
|
||||
update_reference_planes(); // base ghosts (origins + datums) follow the tool/model state
|
||||
}
|
||||
|
||||
// Onshape default planes: the XY/XZ/YZ reference planes are persistent, transparent, labelled, and
|
||||
// larger than the bed — shown as the FALLBACK when there is no object yet. When the Plane tool is
|
||||
// open they additionally surface existing datums so a base can be picked. Single authority for the
|
||||
// reference-plane overlay (set/clear_base_pick).
|
||||
void DesignPanel::update_reference_planes()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
// Default planes pass through the modeling origin (bed centre) — same point the kernel uses to
|
||||
// resolve XY/XZ/YZ datum bases, so the ghosts, the datums and new sketches all coincide.
|
||||
const Vec3d o = m_doc.modeling_origin;
|
||||
SketchPlane xy = SketchPlane::XY(); xy.origin += o;
|
||||
SketchPlane xz = SketchPlane::XZ(); xz.origin += o;
|
||||
SketchPlane yz = SketchPlane::YZ(); yz.origin += o;
|
||||
std::vector<SketchPlane> bp = { xy, xz, yz };
|
||||
std::vector<int> bi = { 0, 1, 2 };
|
||||
std::vector<std::string> bl = { "XY", "XZ", "YZ" };
|
||||
|
||||
if (m_active == Tool::Plane) {
|
||||
// Base picking only makes sense for the Offset method (others reference faces/edges).
|
||||
if (m_plane_type && (PlaneType)m_plane_type->GetSelection() == PlaneType::Offset) {
|
||||
auto datums = m_doc.resolve_datum_planes(); // already in world coords (origin applied)
|
||||
for (int i = 0; i < int(datums.size()); ++i) {
|
||||
bp.push_back(datums[i].second); bi.push_back(3 + i); bl.push_back(datums[i].first);
|
||||
}
|
||||
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
|
||||
} else {
|
||||
m_viewport->clear_base_pick();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Fallback (Onshape default planes): show the 3 reference planes while there is no SOLID body
|
||||
// yet — so they persist through the 2D-sketch phase and reappear after a sketch is confirmed
|
||||
// (a sketch creates no body). They no longer block selection: clicking existing geometry wins,
|
||||
// a base-plane pick only fires on a click that hit nothing else (see on_mouse fall-through).
|
||||
if (m_doc.bodies.empty())
|
||||
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
|
||||
else
|
||||
m_viewport->clear_base_pick();
|
||||
}
|
||||
|
||||
void DesignPanel::refresh_preview()
|
||||
{
|
||||
if (m_active == Tool::None) { m_viewport->clear_preview(); return; }
|
||||
@@ -4951,6 +5272,7 @@ void DesignPanel::refresh_preview()
|
||||
m_status->SetLabel(m_active == Tool::Plane ? _L("Plane ready") : _L("Sketch ready"));
|
||||
for (wxButton* b : m_confirm_btns) if (b) b->Enable(true);
|
||||
m_status->Refresh();
|
||||
update_datum_gizmo(); // Plane card: show/refresh the in-canvas resize handles
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5027,6 +5349,8 @@ void DesignPanel::refresh_preview()
|
||||
update_revolve_gizmo();
|
||||
// Same for the Pattern spacing arrow / angle-arc (self-gates: only while the Pattern card is open).
|
||||
update_pattern_gizmo();
|
||||
// Datum-plane resize handles (self-gates: only while the Plane card is open).
|
||||
update_datum_gizmo();
|
||||
}
|
||||
|
||||
void DesignPanel::open_tool(Tool t)
|
||||
@@ -5179,6 +5503,8 @@ void DesignPanel::close_tool()
|
||||
m_viewport->clear_shell_gizmo();
|
||||
m_viewport->clear_revolve_gizmo();
|
||||
m_viewport->clear_pattern_gizmo();
|
||||
m_viewport->clear_datum_gizmo();
|
||||
update_reference_planes(); // back to no-tool: show the origin planes if there is no object yet
|
||||
m_form->Layout();
|
||||
m_form->FitInside();
|
||||
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
|
||||
|
||||
Reference in New Issue
Block a user