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 pinFillAlpha = 1.0;
double pinResolvedAlpha = 0.3;
};
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.
* A plain overlay draws with whatever justify the last painter left in the
* GAL (CENTER is only the reset default), so BitmapText anchoring was
* nondeterministic labels wandered around their chips depending on what
* rendered before them. This guarantees TOP-LEFT anchoring for every
* BitmapText below; the label math is written against that.
* The TEXT half of the presence drawing. All labels go through this overlay
* because a plain VIEW_OVERLAY has two text hazards:
* - it draws with whatever justify the last painter left in the GAL
* (CENTER is only the reset default) anchoring was nondeterministic;
* this pins TOP-LEFT, which the label math is written against.
* - 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:
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). */
inline std::shared_ptr<PRESENCE_OVERLAY> makePresenceOverlay( KIGFX::VIEW* aView )
/** Depth offset for the SHAPES overlay — one unit deeper than the text. */
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() );
return overlay;
}
/** 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
* PRESENCE_OVERLAY pinning the GAL justify (a plain overlay inherits
* whatever the last painter left, which made anchoring nondeterministic). */
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::string& aText,
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
* world-units-per-screen-pixel. The chip rect goes to the SHAPES overlay,
* the text to the TEXT overlay (nearest depth + pinned TOP-LEFT justify
* see PRESENCE_TEXT_OVERLAY), so the text always renders on top. */
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv, const BOX2I& aBox,
const std::string& aText, const KIGFX::COLOR4D& aColor, double aPx,
const STYLE& aS )
{
if( !aS.labelShow || aText.empty() )
return;
@ -234,17 +246,6 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
else
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 )
{
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->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
* item's exact geometry for selShape 5 (pcbnew supplies it; eeschema passes
* nullptr and shape 5 falls back to the bbox rectangle). */
inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::string& aName,
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS,
const SHAPE_POLY_SET* aOutline = nullptr )
inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv, BOX2I aBox,
const std::string& aName, const KIGFX::COLOR4D& aColor, double aPx,
const STYLE& aS, const SHAPE_POLY_SET* aOutline = nullptr )
{
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();
labelBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
drawLabel( aOv, labelBox, aName, aColor, aPx, aS );
drawLabel( aOv, aTextOv, labelBox, aName, aColor, aPx, aS );
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. */
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 )
{
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,
( aS.cursorSizePx + 4 ) * aPx );
// Text FIRST, chip rect AFTER — see drawLabel (same-depth z rejection
// punches the text through the 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 );
// Chip rect on the shapes overlay, text on the TEXT overlay (nearest
// depth) — text always renders on top of its chip.
if( aS.cursorLabelChip )
{
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->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).
pcbjam_presence::STYLE g_style;
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_redrawScheduled = false;
bool g_selCheckScheduled = false;
@ -799,9 +802,18 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
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_textOverlay->Clear();
// Screen-constant sizing via the GAL matrix (GetScale() is the zoom, not px/IU).
double px = view->ToWorld( 1.0 );
@ -821,13 +833,14 @@ void redrawOverlay()
if( !item )
continue; // not in this schematic (yet) — skip silently
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name,
color, px, g_style );
pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
item->ViewBBox(), peer.name, color, px,
g_style );
}
if( peer.hasCursor )
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px,
g_style );
pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
peer.name, color, px, g_style );
}
// 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_textOverlay.get() );
fr->GetCanvas()->ForceRefresh();
}

View file

@ -1126,6 +1126,9 @@ std::vector<PIN> g_pins;
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
pcbjam_presence::STYLE g_style;
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_redrawScheduled = false;
bool g_selCheckScheduled = false;
@ -1207,9 +1210,18 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
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_textOverlay->Clear();
BOARD* board = fr->GetBoard();
// 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,
color, px, g_style, &outline );
pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
item->ViewBBox(), peer.name, color, px,
g_style, &outline );
}
if( peer.hasCursor )
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px,
g_style );
pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
peer.name, color, px, g_style );
}
// 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_textOverlay.get() );
// The canvas repaints on its own only with focus/input — force it, exactly as
// the cross-probe flash does.
fr->GetCanvas()->ForceRefresh();