fix(collab): deterministic nameplates — pin GAL text justify + punch chip text through the depth test

Two root causes of the wandering/invisible badge text:
- A plain VIEW_OVERLAY draws with whatever text justify the LAST painter left
  in the GAL (CENTER is only the reset default) — anchoring was
  nondeterministic. PRESENCE_OVERLAY (VIEW_OVERLAY subclass, replaces
  MakeOverlay) pins TOP-LEFT justify before executing its commands; the label
  math is written against that.
- The whole overlay draws at ONE depth and same-depth fragments drawn later
  LOSE the depth test — a chip rect over its text erased the text. Draw the
  text FIRST, the chip rect AFTER: the rect is rejected exactly on the glyph
  pixels, punching the text through.
Verified live: chips contain their names on light (dark text) and dark
(white text) user colors, for both selection tags and cursor labels.

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 12:46:11 +02:00
commit 4c287c2a27
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
3 changed files with 59 additions and 28 deletions

View file

@ -13,14 +13,18 @@
#ifdef __EMSCRIPTEN__
#include <font/text_attributes.h>
#include <gal/color4d.h>
#include <gal/graphics_abstraction_layer.h>
#include <geometry/eda_angle.h>
#include <geometry/shape_poly_set.h>
#include <math/util.h>
#include <math/box2.h>
#include <view/view.h>
#include <view/view_overlay.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include <wx/string.h>
@ -174,10 +178,38 @@ inline KIGFX::COLOR4D chipTextColor( const KIGFX::COLOR4D& aBg )
: KIGFX::COLOR4D( 1.0, 1.0, 1.0, 1.0 );
}
/**
* 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.
*/
class PRESENCE_OVERLAY : public KIGFX::VIEW_OVERLAY
{
public:
void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override
{
KIGFX::GAL* gal = aView->GetGAL();
gal->SetHorizontalJustify( GR_TEXT_H_ALIGN_LEFT );
gal->SetVerticalJustify( GR_TEXT_V_ALIGN_TOP );
KIGFX::VIEW_OVERLAY::ViewDraw( aLayer, aView );
}
};
/** VIEW::MakeOverlay's body, for our subclass (make + Add to the view). */
inline std::shared_ptr<PRESENCE_OVERLAY> makePresenceOverlay( KIGFX::VIEW* aView )
{
auto overlay = std::make_shared<PRESENCE_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. GAL BitmapText CENTERS on its position (the
* default justify is CENTER/CENTER GAL::ResetTextAttributes), so the block
* math below computes a top-left and hands GAL the block's center. */
* 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 )
{
@ -202,6 +234,17 @@ 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;
@ -212,18 +255,7 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
VECTOR2D( x + w + padX, y + h + padY ) );
aOv->SetIsStroke( true );
aOv->SetIsFill( false );
aOv->SetStrokeColor( chipTextColor( aColor ) );
}
else
{
aOv->SetIsStroke( true );
aOv->SetIsFill( false );
aOv->SetStrokeColor( aColor );
}
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aOv->BitmapText( wxString::FromUTF8( aText.c_str() ),
VECTOR2I( KiROUND( x + w / 2 ), KiROUND( y + h / 2 ) ), ANGLE_0 );
}
/** Selection highlight for one item, in the chosen shape. `aOutline` is the
@ -370,11 +402,20 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
{
double h = aS.cursorLabelSizePx * aPx;
double w = textWidth( aName, h );
// Top-left of the text block, below-right of the cursor glyph; GAL
// centers text on its position, so it gets the block center.
// Top-left of the text block, below-right of the cursor glyph
// (TOP-LEFT anchoring — PRESENCE_OVERLAY pins the GAL justify).
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 );
if( aS.cursorLabelChip )
{
double padX = 3 * aPx, padY = 2 * aPx;
@ -385,17 +426,7 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
at + VECTOR2D( w + padX, h + padY ) );
aOv->SetIsStroke( true );
aOv->SetIsFill( false );
aOv->SetStrokeColor( chipTextColor( aColor ) );
}
else
{
aOv->SetStrokeColor( c );
}
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
aOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
VECTOR2I( KiROUND( at.x + w / 2 ), KiROUND( at.y + h / 2 ) ),
ANGLE_0 );
}
}

View file

@ -799,7 +799,7 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
if( !g_overlay )
g_overlay = view->MakeOverlay();
g_overlay = pcbjam_presence::makePresenceOverlay( view );
g_overlay->Clear();

View file

@ -1207,7 +1207,7 @@ void redrawOverlay()
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
if( !g_overlay )
g_overlay = view->MakeOverlay();
g_overlay = pcbjam_presence::makePresenceOverlay( view );
g_overlay->Clear();