fix(collab): name badges render for real — text overlay above a depth-offset shapes overlay

The full root-cause chain of the empty/mangled badges:
- GAL text justify is painter-residue → PRESENCE_TEXT_OVERLAY pins TOP-LEFT.
- VIEW_OVERLAY::ViewDraw hard-sets EVERY overlay to GetMinDepth(), so the
  shapes and text overlays always collided at one depth, where later-drawn
  fragments lose (and bitmap glyphs are textured quads whose transparent
  cells also write depth — punch-through produced cell-shaped holes instead).
  Fix rides the new fork VIEW_OVERLAY::SetDepthOffset: shapes at min+1,
  labels at min — 'rect first, text on top' now holds regardless of paint
  order. Verified: chips contain crisp names (dark-on-light, white-on-dark)
  for selection tags and cursor labels; presence suites 10/10.
- kicad pointer bump (fork 24c5854d5b).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
Gergő Törcsvári 2026-07-07 15:57:56 +02:00
commit c8c2c5f583
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
4 changed files with 89 additions and 52 deletions

2
kicad

@ -1 +1 @@
Subproject commit 4a7250d11a91042ba7cb58e3962881373a1c904d Subproject commit 24c5854d5bf76fc908b8ba8a3bed63675d5067cf

View file

@ -77,6 +77,7 @@ struct STYLE
double pinRingAlpha = 0.9; double pinRingAlpha = 0.9;
double pinFillAlpha = 1.0; double pinFillAlpha = 1.0;
double pinResolvedAlpha = 0.3; double pinResolvedAlpha = 0.3;
}; };
inline KIGFX::COLOR4D parseHexColor( const std::string& aHex, const KIGFX::COLOR4D& aFallback ) inline KIGFX::COLOR4D parseHexColor( const std::string& aHex, const KIGFX::COLOR4D& aFallback )
@ -179,14 +180,21 @@ inline KIGFX::COLOR4D chipTextColor( const KIGFX::COLOR4D& aBg )
} }
/** /**
* VIEW_OVERLAY that PINS the GAL text justify before executing its commands. * The TEXT half of the presence drawing. All labels go through this overlay
* A plain overlay draws with whatever justify the last painter left in the * because a plain VIEW_OVERLAY has two text hazards:
* GAL (CENTER is only the reset default), so BitmapText anchoring was * - it draws with whatever justify the last painter left in the GAL
* nondeterministic — labels wandered around their chips depending on what * (CENTER is only the reset default) → anchoring was nondeterministic;
* rendered before them. This guarantees TOP-LEFT anchoring for every * this pins TOP-LEFT, which the label math is written against.
* BitmapText below; the label math is written against that. * - every overlay draws its whole command list at ONE depth
* (VIEW_OVERLAY::ViewDraw hard-sets GetMinDepth()) and same-depth
* fragments drawn later LOSE the depth test; bitmap glyphs are textured
* QUADS whose transparent cells also write depth, so text can neither be
* drawn under a chip (erased) nor over one (punches cell-shaped holes).
* The SHAPES overlay is therefore pushed DEEPER via the fork's
* VIEW_OVERLAY::SetDepthOffset (chips at min+1, text at min) — "rect
* first, text on top" then holds regardless of paint order.
*/ */
class PRESENCE_OVERLAY : public KIGFX::VIEW_OVERLAY class PRESENCE_TEXT_OVERLAY : public KIGFX::VIEW_OVERLAY
{ {
public: public:
void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override
@ -198,20 +206,24 @@ public:
} }
}; };
/** VIEW::MakeOverlay's body, for our subclass (make + Add to the view). */ /** Depth offset for the SHAPES overlay — one unit deeper than the text. */
inline std::shared_ptr<PRESENCE_OVERLAY> makePresenceOverlay( KIGFX::VIEW* aView ) constexpr double PRESENCE_SHAPES_DEPTH_OFFSET = 1.0;
/** VIEW::MakeOverlay's body, for the text overlay (make + Add to the view). */
inline std::shared_ptr<PRESENCE_TEXT_OVERLAY> makePresenceTextOverlay( KIGFX::VIEW* aView )
{ {
auto overlay = std::make_shared<PRESENCE_OVERLAY>(); auto overlay = std::make_shared<PRESENCE_TEXT_OVERLAY>();
aView->Add( overlay.get() ); aView->Add( overlay.get() );
return overlay; return overlay;
} }
/** Name tag next to (or inside) a box, per the label placement knobs. `px` is /** Name tag next to (or inside) a box, per the label placement knobs. `px` is
* world-units-per-screen-pixel. Text anchoring is TOP-LEFT — guaranteed by * world-units-per-screen-pixel. The chip rect goes to the SHAPES overlay,
* PRESENCE_OVERLAY pinning the GAL justify (a plain overlay inherits * the text to the TEXT overlay (nearest depth + pinned TOP-LEFT justify —
* whatever the last painter left, which made anchoring nondeterministic). */ * see PRESENCE_TEXT_OVERLAY), so the text always renders on top. */
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::string& aText, inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv, const BOX2I& aBox,
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS ) const std::string& aText, const KIGFX::COLOR4D& aColor, double aPx,
const STYLE& aS )
{ {
if( !aS.labelShow || aText.empty() ) if( !aS.labelShow || aText.empty() )
return; return;
@ -234,17 +246,6 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
else else
y = aS.labelInside ? aBox.GetEnd().y - off - h : aBox.GetEnd().y + off; y = aS.labelInside ? aBox.GetEnd().y - off - h : aBox.GetEnd().y + off;
// The whole overlay draws at ONE depth: same-depth fragments drawn LATER
// lose the depth test, so anything covering the text erases it. Draw the
// TEXT FIRST and the chip rect AFTER — the rect is rejected exactly on
// the glyph pixels, punching the text through the chip.
aOv->SetIsStroke( true );
aOv->SetIsFill( false );
aOv->SetStrokeColor( aS.labelChip ? chipTextColor( aColor ) : aColor );
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aOv->BitmapText( wxString::FromUTF8( aText.c_str() ), VECTOR2I( KiROUND( x ), KiROUND( y ) ),
ANGLE_0 );
if( aS.labelChip ) if( aS.labelChip )
{ {
double padX = 3 * aPx, padY = 2 * aPx; double padX = 3 * aPx, padY = 2 * aPx;
@ -256,14 +257,21 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
aOv->SetIsStroke( true ); aOv->SetIsStroke( true );
aOv->SetIsFill( false ); aOv->SetIsFill( false );
} }
aTextOv->SetIsStroke( true );
aTextOv->SetIsFill( false );
aTextOv->SetStrokeColor( aS.labelChip ? chipTextColor( aColor ) : aColor );
aTextOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aTextOv->BitmapText( wxString::FromUTF8( aText.c_str() ),
VECTOR2I( KiROUND( x ), KiROUND( y ) ), ANGLE_0 );
} }
/** Selection highlight for one item, in the chosen shape. `aOutline` is the /** Selection highlight for one item, in the chosen shape. `aOutline` is the
* item's exact geometry for selShape 5 (pcbnew supplies it; eeschema passes * item's exact geometry for selShape 5 (pcbnew supplies it; eeschema passes
* nullptr and shape 5 falls back to the bbox rectangle). */ * nullptr and shape 5 falls back to the bbox rectangle). */
inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::string& aName, inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv, BOX2I aBox,
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS, const std::string& aName, const KIGFX::COLOR4D& aColor, double aPx,
const SHAPE_POLY_SET* aOutline = nullptr ) const STYLE& aS, const SHAPE_POLY_SET* aOutline = nullptr )
{ {
if( aS.selShape == 5 && aOutline && aOutline->OutlineCount() > 0 ) if( aS.selShape == 5 && aOutline && aOutline->OutlineCount() > 0 )
{ {
@ -276,7 +284,7 @@ inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::s
BOX2I labelBox = aOutline->BBox(); BOX2I labelBox = aOutline->BBox();
labelBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) ); labelBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
drawLabel( aOv, labelBox, aName, aColor, aPx, aS ); drawLabel( aOv, aTextOv, labelBox, aName, aColor, aPx, aS );
return; return;
} }
@ -356,11 +364,12 @@ inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::s
} }
} }
drawLabel( aOv, aBox, aName, aColor, aPx, aS ); drawLabel( aOv, aTextOv, aBox, aName, aColor, aPx, aS );
} }
/** Remote cursor (+ name label) in the chosen shape. */ /** Remote cursor (+ name label) in the chosen shape. */
inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const std::string& aName, inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv,
const VECTOR2D& aPos, const std::string& aName,
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS ) const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
{ {
double s = aS.cursorSizePx * aPx; double s = aS.cursorSizePx * aPx;
@ -407,15 +416,8 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
VECTOR2D at = aPos + VECTOR2D( ( aS.cursorSizePx + 4 ) * aPx, VECTOR2D at = aPos + VECTOR2D( ( aS.cursorSizePx + 4 ) * aPx,
( aS.cursorSizePx + 4 ) * aPx ); ( aS.cursorSizePx + 4 ) * aPx );
// Text FIRST, chip rect AFTER — see drawLabel (same-depth z rejection // Chip rect on the shapes overlay, text on the TEXT overlay (nearest
// punches the text through the chip). // depth) — text always renders on top of its chip.
aOv->SetIsStroke( true );
aOv->SetIsFill( false );
aOv->SetStrokeColor( aS.cursorLabelChip ? chipTextColor( aColor ) : c );
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
VECTOR2I( KiROUND( at.x ), KiROUND( at.y ) ), ANGLE_0 );
if( aS.cursorLabelChip ) if( aS.cursorLabelChip )
{ {
double padX = 3 * aPx, padY = 2 * aPx; double padX = 3 * aPx, padY = 2 * aPx;
@ -427,6 +429,13 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
aOv->SetIsStroke( true ); aOv->SetIsStroke( true );
aOv->SetIsFill( false ); aOv->SetIsFill( false );
} }
aTextOv->SetIsStroke( true );
aTextOv->SetIsFill( false );
aTextOv->SetStrokeColor( aS.cursorLabelChip ? chipTextColor( aColor ) : c );
aTextOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aTextOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
VECTOR2I( KiROUND( at.x ), KiROUND( at.y ) ), ANGLE_0 );
} }
} }

View file

@ -720,6 +720,9 @@ std::vector<PIN> g_pins;
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner). // — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
pcbjam_presence::STYLE g_style; pcbjam_presence::STYLE g_style;
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay; std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
// Labels render from their own overlay at the nearest depth (chip rects
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_textOverlay;
bool g_started = false; bool g_started = false;
bool g_redrawScheduled = false; bool g_redrawScheduled = false;
bool g_selCheckScheduled = false; bool g_selCheckScheduled = false;
@ -799,9 +802,18 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView(); KIGFX::VIEW* view = fr->GetCanvas()->GetView();
if( !g_overlay ) if( !g_overlay )
g_overlay = pcbjam_presence::makePresenceOverlay( view ); {
g_overlay = view->MakeOverlay();
// Shapes sit one depth unit BELOW the text overlay (fork
// SetDepthOffset) so labels always render on top of chips/boxes.
g_overlay->SetDepthOffset( pcbjam_presence::PRESENCE_SHAPES_DEPTH_OFFSET );
}
if( !g_textOverlay )
g_textOverlay = pcbjam_presence::makePresenceTextOverlay( view );
g_overlay->Clear(); g_overlay->Clear();
g_textOverlay->Clear();
// Screen-constant sizing via the GAL matrix (GetScale() is the zoom, not px/IU). // Screen-constant sizing via the GAL matrix (GetScale() is the zoom, not px/IU).
double px = view->ToWorld( 1.0 ); double px = view->ToWorld( 1.0 );
@ -821,13 +833,14 @@ void redrawOverlay()
if( !item ) if( !item )
continue; // not in this schematic (yet) — skip silently continue; // not in this schematic (yet) — skip silently
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name, pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
color, px, g_style ); item->ViewBBox(), peer.name, color, px,
g_style );
} }
if( peer.hasCursor ) if( peer.hasCursor )
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px, pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
g_style ); peer.name, color, px, g_style );
} }
// Comment pin dots (0005), drawn last so they sit above selection outlines. // Comment pin dots (0005), drawn last so they sit above selection outlines.
@ -838,6 +851,7 @@ void redrawOverlay()
} }
view->Update( g_overlay.get() ); view->Update( g_overlay.get() );
view->Update( g_textOverlay.get() );
fr->GetCanvas()->ForceRefresh(); fr->GetCanvas()->ForceRefresh();
} }

View file

@ -1126,6 +1126,9 @@ std::vector<PIN> g_pins;
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner). // — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
pcbjam_presence::STYLE g_style; pcbjam_presence::STYLE g_style;
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay; std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
// Labels render from their own overlay at the nearest depth (chip rects
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_textOverlay;
bool g_started = false; bool g_started = false;
bool g_redrawScheduled = false; bool g_redrawScheduled = false;
bool g_selCheckScheduled = false; bool g_selCheckScheduled = false;
@ -1207,9 +1210,18 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView(); KIGFX::VIEW* view = fr->GetCanvas()->GetView();
if( !g_overlay ) if( !g_overlay )
g_overlay = pcbjam_presence::makePresenceOverlay( view ); {
g_overlay = view->MakeOverlay();
// Shapes sit one depth unit BELOW the text overlay (fork
// SetDepthOffset) so labels always render on top of chips/boxes.
g_overlay->SetDepthOffset( pcbjam_presence::PRESENCE_SHAPES_DEPTH_OFFSET );
}
if( !g_textOverlay )
g_textOverlay = pcbjam_presence::makePresenceTextOverlay( view );
g_overlay->Clear(); g_overlay->Clear();
g_textOverlay->Clear();
BOARD* board = fr->GetBoard(); BOARD* board = fr->GetBoard();
// Screen-constant sizing: px → world units, so cursors/outline widths don't // Screen-constant sizing: px → world units, so cursors/outline widths don't
@ -1265,13 +1277,14 @@ void redrawOverlay()
} }
} }
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name, pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
color, px, g_style, &outline ); item->ViewBBox(), peer.name, color, px,
g_style, &outline );
} }
if( peer.hasCursor ) if( peer.hasCursor )
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px, pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
g_style ); peer.name, color, px, g_style );
} }
// Comment pin dots (0005), drawn last so they sit above selection outlines. // Comment pin dots (0005), drawn last so they sit above selection outlines.
@ -1282,6 +1295,7 @@ void redrawOverlay()
} }
view->Update( g_overlay.get() ); view->Update( g_overlay.get() );
view->Update( g_textOverlay.get() );
// The canvas repaints on its own only with focus/input — force it, exactly as // The canvas repaints on its own only with focus/input — force it, exactly as
// the cross-probe flash does. // the cross-probe flash does.
fr->GetCanvas()->ForceRefresh(); fr->GetCanvas()->ForceRefresh();