Design: draw the mate connector, so its verse and polarity are visible

A mate connector was visible only to a program. resolve_datum_coordsys had exactly ONE consumer
in the whole tree -- McpControl.cpp, the agent socket -- so the frame every mate is built on
could not be seen at all, and the two questions a connector has to answer on sight had no
answer in the viewport: which way does Z point (the VERSE), and which of the pair is anchored
versus about to move (the POLARITY).

The glyph is Onshape's proven core plus the part nobody ships. Disc for the XY plane, one gold
quadrant for the roll -- the only in-glyph answer to "where is X", which matters because
Fastened and Slider lock the clocking -- and a Z arrow drawn on +Z ONLY, never double-headed.
Polarity is carried by the head: a filled cone travels, an open collar receives. Onshape,
Fusion, Inventor and FreeCAD all draw both ends of a mate identically, which is why "which part
moves?" is a standing complaint; nothing here invents new semantics, it just stops hiding them.

Polarity is read from the committed Mate features, not only from the open card. A connector some
mate drives must read as driven whenever it is on screen, or the glyph tells the truth only while
a dialog happens to be open. The card, when open, still wins -- that is the live intent.

Judged on the rig rather than in a mock, which changed three decisions:

- Three RGB axis arms lose to one Z arrow. Rendered side by side (SNAPORCA_GLYPH=A selects the
  Onshape-style trio), the three heads are as large as the 22 px disc, they bury the quadrant, and
  at an oblique angle they pile into a smudge -- and the trio is indistinguishable from the move
  gizmo and the bed triad, which are already RGB arrow trios in this viewport.

- Depth off floats, depth on tears. With GL_DEPTH_TEST off, connectors on faces pointing AWAY from
  the camera drew their discs over the solid, so the part looked covered in frames that were on its
  back. Turning depth on fixed that and immediately z-fought: the disc is exactly coplanar with its
  face and came out a broken dotted arc. Depth ON plus a 0.7*upp lift along Z buys both, and scaling
  the lift by upp keeps it sub-pixel instead of opening a visible gap on zoom-in.

- Foreshortening degenerates an arrow into a dot when the axis points at the camera. It now draws a
  ring instead of silently vanishing, which is what a naive projection does.

Everything is sized 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.

Research and the empirical findings are written up in DESIGN_MATE_CONNECTORS.md section 8b;
rig images in artifacts/shots/g-0*.png, vendor reference glyphs in artifacts/glyphs/.

Not fixed here, and recorded rather than papered over: the quadrant collapses to a blob at a
grazing angle, which is exactly when the roll is hardest to read (F4); roll-undefined in red makes
the least important connector the loudest thing on screen (F5); and a true grazing view, a curved
face, and overlap with the move gizmo are still untested. Also surfaced while testing and unrelated
to drawing: add_mate accepted a mate between two connectors on the SAME body and duly transformed
the body relative to itself -- a concrete instance of the missing validation already filed as G6.

Fork parity unchanged: DesignCanvas.cpp 16, DesignPanel.cpp 30, the other four files 0.
This commit is contained in:
Tommaso Bianchi
2026-08-05 09:50:09 +02:00
parent 24f4076bb5
commit 0a2faedc32
6 changed files with 218 additions and 0 deletions
+45
View File
@@ -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<int, int> 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<DesignSketchTool::MateConnectorGlyph> 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()