diff --git a/src/slic3r/GUI/DesignCanvas.cpp b/src/slic3r/GUI/DesignCanvas.cpp index d51e49814c..d553fe3806 100644 --- a/src/slic3r/GUI/DesignCanvas.cpp +++ b/src/slic3r/GUI/DesignCanvas.cpp @@ -911,6 +911,12 @@ void DesignCanvas::set_datum_planes(std::vector planes, std::vector request_repaint(); } +void DesignCanvas::set_mate_connectors(std::vector g) +{ + m_sketch_tool.set_mate_connectors(std::move(g)); + request_repaint(); +} + bool DesignCanvas::toggle_planes() { const bool on = m_sketch_tool.toggle_show_planes(); diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp index c94f7f584b..78034ed66f 100644 --- a/src/slic3r/GUI/DesignCanvas.hpp +++ b/src/slic3r/GUI/DesignCanvas.hpp @@ -181,6 +181,8 @@ public: void set_highlight_sketches(std::vector> hl); void set_datum_planes(std::vector planes, std::vector sizes = {}); // draw datum/reference planes (u/v extents) + // Mate connectors, drawn as frames so their verse and polarity are visible (snaporca-wgsc). + void set_mate_connectors(std::vector g); void set_body_highlight(bool on); // tint the solid when its feature is tree-selected // The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel: // the panel clips it at ~73 characters with no warning (snaporca-8cc), the viewport's diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index dd70d4d94f..65bd47a1ba 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -8815,6 +8815,51 @@ void DesignPanel::refresh_datum_planes() 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)); + refresh_mate_connectors(); +} + +// Feed the connector frames to the viewport. resolve_datum_coordsys() emits them in document order +// over enabled CoordSys features, so the feature index can be recovered by walking the same filter — +// which is what tells us whether a given frame is the one the open Mate card will DRIVE. +void DesignPanel::refresh_mate_connectors() +{ + if (!m_viewport) return; + // Polarity is a property of the MATE, not of an open dialog: a connector that some committed + // mate drives must read as driven whenever it is on screen, or the glyph only tells the truth + // while a card happens to be open. The card, when open, wins — it is the live intent. + std::map role_by_feature; // feature index -> 1 fixed, 2 driven + for (const CadFeature& mf : m_doc.features) { + if (mf.type != CadFeatureType::Mate || !mf.enabled) continue; + if (mf.mate_cs_a >= 0) role_by_feature[mf.mate_cs_a] = 1; + if (mf.mate_cs_b >= 0) role_by_feature[mf.mate_cs_b] = 2; + } + const int cs_a = (m_active == Tool::Mate && m_mate_cs_a) ? m_mate_cs_a->GetSelection() : -1; + const int cs_b = (m_active == Tool::Mate && m_mate_cs_b) ? m_mate_cs_b->GetSelection() : -1; + + std::vector out; + const auto frames = m_doc.resolve_datum_coordsys(); + size_t k = 0; + for (size_t i = 0; i < m_doc.features.size() && k < frames.size(); ++i) { + const CadFeature& f = m_doc.features[i]; + if (f.type != CadFeatureType::CoordSys || !f.enabled) continue; + const auto& fr = frames[k]; + const int ordinal = int(k); + ++k; + if (!fr.error.empty()) continue; // unresolved: draw nothing, never a guess + DesignSketchTool::MateConnectorGlyph g; + g.origin = fr.origin; + g.x = fr.x; + g.y = fr.y; + const auto it = role_by_feature.find(int(i)); + g.role = (ordinal == cs_b) ? 2 : (ordinal == cs_a ? 1 + : (it != role_by_feature.end() ? it->second : 0)); + // A face-only connector whose face gave no usable in-plane edge fell back to the hint; that + // is the case the glyph has to confess rather than absorb. + g.roll_undefined = (f.coordsys_type == CoordSysType::FaceAndDirection + && f.coordsys_edge < 0); + out.push_back(g); + } + m_viewport->set_mate_connectors(std::move(out)); } void DesignPanel::update_datum_gizmo() diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 4326315e7f..2f9e3f52ba 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -272,6 +272,7 @@ private: void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card) void update_datum_gizmo(); // resize handles on the datum plane being created/edited (C3) void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport + void refresh_mate_connectors(); // push connector frames so verse + polarity are visible void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object) CadDocument m_doc; diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index c89650d7b0..d9b34aad6a 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -3331,6 +3331,151 @@ void DesignSketchTool::clear_extrude_gizmo() // Camera-billboarded depth arrow(s) along the profile normal, drawn in WORLD via a billboard // SketchPlane at the centroid (draw_strokes/draw_text lift 2D coords through m_plane.to_world, // so swapping m_plane to a screen-facing frame renders a flat, screen-aligned arrow + label). +// The mate-connector glyph. Three elements, each answering one question, and every dimension is in +// SCREEN PIXELS via upp (= 1/zoom) like every other gizmo here — a connector is a symbol, not a part, +// so it must not shrink with the model. +// +// disc the XY plane "I am a frame, and this is the plane I sit in" +// quadrant the +x/+y sector "this is where X is" -- the roll, otherwise invisible +// Z arrow +Z only, never -Z "this is the way I point" -- the VERSE +// +// POLARITY (which one is anchored, which one is about to move) is carried by the head: a filled +// cone travels, an open collar receives. No surveyed CAD system encodes this at all; both ends of +// their mates are drawn identically, which is why "which part moves?" is a standing complaint. +// +// SNAPORCA_GLYPH=A|B selects the treatment while this is being judged on the rig: +// A three short axis arms, no head differentiation (the Onshape baseline) +// B one-sided Z arrow, filled vs open head (the proposal) -- default +void DesignSketchTool::render_mate_connectors() +{ + if (m_mate_connectors.empty()) return; + static const bool style_A = [] { + const char* s = ::getenv("SNAPORCA_GLYPH"); + return s && (*s == 'A' || *s == 'a'); + }(); + + const Camera& cam = wxGetApp().plater()->get_camera(); + const Vec3d right = cam.get_dir_right().normalized(); + const Vec3d up = cam.get_dir_up().normalized(); + const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6); + const double R = 22.0 * upp; // disc radius, ~22 px + const double lw = std::max(0.9 * upp, 1e-4); + + const ColorRGBA gold (0.93f, 0.66f, 0.09f, 1.0f); + const ColorRGBA blue (0.18f, 0.44f, 0.93f, 1.0f); + const ColorRGBA grey (0.42f, 0.46f, 0.52f, 1.0f); + const ColorRGBA warn (0.76f, 0.26f, 0.05f, 1.0f); + + const SketchPlane saved = m_plane; + + for (const MateConnectorGlyph& g : m_mate_connectors) { + const Vec3d X = g.x.normalized(); + const Vec3d Y = g.y.normalized(); + const Vec3d Z = X.cross(Y).normalized(); + const ColorRGBA body = (g.role == 2) ? blue : grey; + + // ---- disc + quadrant, drawn IN the connector's own plane. This is the part that must not + // be billboarded: a disc that always faces the camera cannot show the frame's orientation, + // which is the only reason it is a disc and not a dot. + // Lifted off the surface by a sub-pixel epsilon. A connector's disc is EXACTLY coplanar with + // the face it sits on, so depth-testing it z-fights: on the rig the disc came out as a broken + // dotted arc that flickered with the camera. Depth off floats it through solids, depth on and + // coplanar tears it — the lift is what buys both. Scaled by upp so it stays sub-pixel at any + // zoom instead of becoming a visible gap when you zoom in. + SketchPlane cp; cp.origin = g.origin + Z * (0.7 * upp); + cp.x_axis = X; cp.y_axis = Y; cp.normal = Z; + m_plane = cp; + { + std::vector> segs; + const int N = 40; + for (int i = 0; i < N; ++i) { + const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N; + segs.emplace_back(Vec2d(R * std::cos(a0), R * std::sin(a0)), + Vec2d(R * std::cos(a1), R * std::sin(a1))); + } + // Depth test ON for the disc, deliberately. With it off, a connector on a face pointing + // AWAY from the camera still drew its disc over the solid, so the part looked covered in + // frames that were really on its back — and a disc floating over an edge read as + // detached rather than planted. render_hole_gizmo learned the same thing for its cube. + glsafe(::glEnable(GL_DEPTH_TEST)); + draw_strokes(m_mc_stroke_model, segs, lw, g.roll_undefined ? warn : body); + + // The quadrant: hatched when the roll could not be derived (a full circular face or a + // seam offers no in-plane direction), so the fallback is a mark you cannot miss rather + // than a silent guess. + std::vector> q; + const double qr = R * 0.84; + const int QN = 10; + for (int i = 0; i < QN; ++i) { + const double a0 = (M_PI_2 * i) / QN, a1 = (M_PI_2 * (i + 1)) / QN; + q.emplace_back(Vec2d(qr * std::cos(a0), qr * std::sin(a0)), + Vec2d(qr * std::cos(a1), qr * std::sin(a1))); + } + q.emplace_back(Vec2d(0, 0), Vec2d(qr, 0)); + q.emplace_back(Vec2d(0, 0), Vec2d(0, qr)); + if (!g.roll_undefined) + for (int i = 1; i < 5; ++i) { // fan lines read as "filled" at any angle + const double a = (M_PI_2 * i) / 5.0; + q.emplace_back(Vec2d(0, 0), Vec2d(qr * std::cos(a), qr * std::sin(a))); + } + draw_strokes(m_mc_stroke_model, q, lw, g.roll_undefined ? warn : gold); + } + + // ---- the axes. Billboarded at the origin: a 3D direction is projected onto the screen + // frame, which is the only way an arrow keeps a readable head at any viewing angle. + SketchPlane bb; bb.origin = g.origin; bb.x_axis = right; bb.y_axis = up; + bb.normal = cam.get_dir_forward().normalized(); + m_plane = bb; + + auto arrow = [&](const Vec3d& dir, double len, const ColorRGBA& col, bool filled_head) { + const Vec3d tipw = g.origin + dir * len; + const Vec2d tip2((tipw - g.origin).dot(right), (tipw - g.origin).dot(up)); + std::vector> segs; + // Foreshortening: when the axis points at (or away from) the camera it projects to + // nothing and an arrow degenerates into a dot. Draw a ring instead — "pointing at you" + // — rather than silently vanishing, which is what a naive projection does. + if (tip2.norm() < R * 0.28) { + const int N = 24; + const double rr = R * 0.30; + for (int i = 0; i < N; ++i) { + const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N; + segs.emplace_back(Vec2d(rr * std::cos(a0), rr * std::sin(a0)), + Vec2d(rr * std::cos(a1), rr * std::sin(a1))); + } + draw_strokes(m_mc_stroke_model, segs, lw, col); + return; + } + const Vec2d u = tip2.normalized(); + const Vec2d n(-u.y(), u.x()); + const double as = R * 0.42; + const Vec2d back = tip2 - u * as; + segs.emplace_back(Vec2d(0, 0), back); + segs.emplace_back(tip2, back + n * (as * 0.45)); + segs.emplace_back(tip2, back - n * (as * 0.45)); + if (filled_head) { + // A closed head reads solid; the open one is left as two barbs. At 20-odd pixels + // this is the difference that says "this body travels". + segs.emplace_back(back + n * (as * 0.45), back - n * (as * 0.45)); + segs.emplace_back(back + n * (as * 0.22), tip2); + segs.emplace_back(back - n * (as * 0.22), tip2); + } else { + segs.emplace_back(back + n * (as * 0.45), back - n * (as * 0.45)); + } + glsafe(::glDisable(GL_DEPTH_TEST)); + draw_strokes(m_mc_stroke_model, segs, lw, col); + }; + + if (style_A) { + arrow(X, R * 1.15, ColorRGBA(0.85f, 0.29f, 0.24f, 1.0f), true); + arrow(Y, R * 1.15, ColorRGBA(0.23f, 0.65f, 0.35f, 1.0f), true); + arrow(Z, R * 1.60, blue, true); + } else { + arrow(Z, R * 2.10, body, g.role == 2); // +Z only. Nothing is ever drawn on -Z. + } + } + m_plane = saved; +} + void DesignSketchTool::render_extrude_gizmo() { if (!m_ex_active) return; @@ -7009,6 +7154,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) // face/edge highlight overlay (whole-solid tint is handled by set_body_highlight). if (!m_active) { render_datum_planes(); + render_mate_connectors(); render_solid_highlight(); if (m_dbp_active) render_base_pick(); if (m_dz_active) render_datum_gizmo(); diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index 334169d75b..191f108486 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -262,6 +262,21 @@ public: m_datum_planes = std::move(planes); m_datum_sizes = std::move(sizes); } + // Mate connectors. Until now a connector was visible only to a program — resolve_datum_coordsys + // had exactly one consumer, the MCP socket — so the frame a mate is built on could not be seen + // at all. The glyph has to answer two questions on sight (snaporca-wgsc): which way does Z point + // (the VERSE), and which of the pair is anchored versus driven (the POLARITY). Nothing in any + // surveyed CAD system encodes the second one. + struct MateConnectorGlyph { + Vec3d origin{0, 0, 0}; + Vec3d x{1, 0, 0}; // roll reference; the filled quadrant spans x -> y + Vec3d y{0, 1, 0}; + int role{0}; // 0 = neutral, 1 = fixed (receives), 2 = driven (moves) + bool roll_undefined{false}; + }; + void set_mate_connectors(std::vector g) { m_mate_connectors = std::move(g); } + void clear_mate_connectors() { m_mate_connectors.clear(); } + // Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X, // 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the // revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click @@ -955,6 +970,9 @@ private: bool m_show_axes{false}; std::vector m_datum_planes; std::vector m_datum_sizes; // per-plane (u,v) full extent; empty -> default + void render_mate_connectors(); // disc + roll quadrant + one-sided Z arrow + std::vector m_mate_connectors; + GLModel m_mc_stroke_model; GLModel m_solid_face_model; GLModel m_solid_edge_model; GLModel m_solid_vertex_model;