feat(collab): tuner round 2 — center-anchored labels, exact outlines, varied demo set, clearer color modes
- collab_presence_style.h: GAL BitmapText CENTERS on its position (confirmed in GAL::ResetTextAttributes — the mispositioned nameplates); labels/chips now hand GAL the block center. New selection shape 5 'exact outline': pcbnew hugs real geometry (footprint bounding hull, TransformShapeToPolygon for the rest, padding inflates the polygon); eeschema falls back to rect. - kicadCollabTestDemoSet (both TUs + merged): labeled demo groups — smallest + largest footprint and the two busiest nets' segments (symbols + wire bundles on sch) — so the style preview covers the real range of shapes. - PresenceTuner: Colors section rebuilt as explicit modes (per-user / fixed / palette) with preset palettes (default, pastel, vivid, okabe-ito), buffered hex editing + Apply (the old always-filtering textarea ate keystrokes), an 'overlay only' hint; demo injection consumes the varied demo set; 'exact outline (pcb)' in the shape list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
parent
ce99773af4
commit
0f19811602
5 changed files with 374 additions and 62 deletions
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include <gal/color4d.h>
|
#include <gal/color4d.h>
|
||||||
#include <geometry/eda_angle.h>
|
#include <geometry/eda_angle.h>
|
||||||
|
#include <geometry/shape_poly_set.h>
|
||||||
#include <math/util.h>
|
#include <math/util.h>
|
||||||
#include <math/box2.h>
|
#include <math/box2.h>
|
||||||
#include <view/view_overlay.h>
|
#include <view/view_overlay.h>
|
||||||
|
|
@ -32,6 +33,7 @@ struct STYLE
|
||||||
{
|
{
|
||||||
// ── selection box ──────────────────────────────────────────────────────
|
// ── selection box ──────────────────────────────────────────────────────
|
||||||
// 0 rect · 1 corner brackets · 2 underline · 3 rounded rect · 4 filled only
|
// 0 rect · 1 corner brackets · 2 underline · 3 rounded rect · 4 filled only
|
||||||
|
// 5 exact item outline (pcbnew; eeschema falls back to rect)
|
||||||
int selShape = 0;
|
int selShape = 0;
|
||||||
double selStrokeWidth = 2.5; // px
|
double selStrokeWidth = 2.5; // px
|
||||||
double selStrokeAlpha = 0.9;
|
double selStrokeAlpha = 0.9;
|
||||||
|
|
@ -164,8 +166,9 @@ inline double textWidth( const std::string& aText, double aGlyphH )
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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. BitmapText anchors near its position's left
|
* world-units-per-screen-pixel. GAL BitmapText CENTERS on its position (the
|
||||||
* edge, vertically centered-ish — offsets are tuned around that. */
|
* default justify is CENTER/CENTER — GAL::ResetTextAttributes), so the block
|
||||||
|
* math below computes a top-left and hands GAL the block's center. */
|
||||||
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::string& aText,
|
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::string& aText,
|
||||||
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
||||||
{
|
{
|
||||||
|
|
@ -183,21 +186,21 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
|
||||||
else if( aS.labelHPos == 2 )
|
else if( aS.labelHPos == 2 )
|
||||||
x = ( aBox.GetOrigin().x + aBox.GetEnd().x ) / 2.0 - w / 2.0; // center
|
x = ( aBox.GetOrigin().x + aBox.GetEnd().x ) / 2.0 - w / 2.0; // center
|
||||||
|
|
||||||
double y;
|
double y; // top of the text block
|
||||||
|
|
||||||
if( aS.labelVPos == 0 )
|
if( aS.labelVPos == 0 )
|
||||||
y = aS.labelInside ? aBox.GetOrigin().y + off : aBox.GetOrigin().y - off;
|
y = aS.labelInside ? aBox.GetOrigin().y + off : aBox.GetOrigin().y - off - h;
|
||||||
else
|
else
|
||||||
y = aS.labelInside ? aBox.GetEnd().y - off : aBox.GetEnd().y + off;
|
y = aS.labelInside ? aBox.GetEnd().y - off - h : aBox.GetEnd().y + off;
|
||||||
|
|
||||||
if( aS.labelChip )
|
if( aS.labelChip )
|
||||||
{
|
{
|
||||||
double padX = 3 * aPx, padY = 2.5 * aPx;
|
double padX = 3 * aPx, padY = 2 * aPx;
|
||||||
aOv->SetIsStroke( false );
|
aOv->SetIsStroke( false );
|
||||||
aOv->SetIsFill( true );
|
aOv->SetIsFill( true );
|
||||||
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
||||||
aOv->Rectangle( VECTOR2D( x - padX, y - h / 2 - padY ),
|
aOv->Rectangle( VECTOR2D( x - padX, y - padY ),
|
||||||
VECTOR2D( x + w + padX, y + h / 2 + padY ) );
|
VECTOR2D( x + w + padX, y + h + padY ) );
|
||||||
aOv->SetIsStroke( true );
|
aOv->SetIsStroke( true );
|
||||||
aOv->SetIsFill( false );
|
aOv->SetIsFill( false );
|
||||||
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
||||||
|
|
@ -210,14 +213,32 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::s
|
||||||
}
|
}
|
||||||
|
|
||||||
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
|
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
|
||||||
aOv->BitmapText( wxString::FromUTF8( aText.c_str() ), VECTOR2I( KiROUND( x ), KiROUND( y ) ),
|
aOv->BitmapText( wxString::FromUTF8( aText.c_str() ),
|
||||||
ANGLE_0 );
|
VECTOR2I( KiROUND( x + w / 2 ), KiROUND( y + h / 2 ) ), ANGLE_0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selection highlight for one item bbox, in the chosen shape. */
|
/** 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,
|
inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::string& aName,
|
||||||
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS,
|
||||||
|
const SHAPE_POLY_SET* aOutline = nullptr )
|
||||||
{
|
{
|
||||||
|
if( aS.selShape == 5 && aOutline && aOutline->OutlineCount() > 0 )
|
||||||
|
{
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( aS.selFillAlpha > 0.001 );
|
||||||
|
aOv->SetStrokeColor( aColor.WithAlpha( aS.selStrokeAlpha ) );
|
||||||
|
aOv->SetFillColor( aColor.WithAlpha( aS.selFillAlpha ) );
|
||||||
|
aOv->SetLineWidth( aS.selStrokeWidth * aPx );
|
||||||
|
aOv->Polygon( *aOutline );
|
||||||
|
|
||||||
|
BOX2I labelBox = aOutline->BBox();
|
||||||
|
labelBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
|
||||||
|
drawLabel( aOv, labelBox, aName, aColor, aPx, aS );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
aBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
|
aBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
|
||||||
|
|
||||||
const VECTOR2D tl = aBox.GetOrigin();
|
const VECTOR2D tl = aBox.GetOrigin();
|
||||||
|
|
@ -339,17 +360,20 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
|
||||||
if( aS.cursorLabel && !aName.empty() )
|
if( aS.cursorLabel && !aName.empty() )
|
||||||
{
|
{
|
||||||
double h = aS.cursorLabelSizePx * aPx;
|
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.
|
||||||
VECTOR2D at = aPos + VECTOR2D( ( aS.cursorSizePx + 4 ) * aPx,
|
VECTOR2D at = aPos + VECTOR2D( ( aS.cursorSizePx + 4 ) * aPx,
|
||||||
( aS.cursorSizePx + 9 ) * aPx );
|
( aS.cursorSizePx + 4 ) * aPx );
|
||||||
|
|
||||||
if( aS.cursorLabelChip )
|
if( aS.cursorLabelChip )
|
||||||
{
|
{
|
||||||
double w = textWidth( aName, h ), padX = 3 * aPx, padY = 2.5 * aPx;
|
double padX = 3 * aPx, padY = 2 * aPx;
|
||||||
aOv->SetIsStroke( false );
|
aOv->SetIsStroke( false );
|
||||||
aOv->SetIsFill( true );
|
aOv->SetIsFill( true );
|
||||||
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
||||||
aOv->Rectangle( at + VECTOR2D( -padX, -h / 2 - padY ),
|
aOv->Rectangle( at + VECTOR2D( -padX, -padY ),
|
||||||
at + VECTOR2D( w + padX, h / 2 + padY ) );
|
at + VECTOR2D( w + padX, h + padY ) );
|
||||||
aOv->SetIsStroke( true );
|
aOv->SetIsStroke( true );
|
||||||
aOv->SetIsFill( false );
|
aOv->SetIsFill( false );
|
||||||
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
||||||
|
|
@ -361,7 +385,8 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const st
|
||||||
|
|
||||||
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
|
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
|
||||||
aOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
|
aOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
|
||||||
VECTOR2I( KiROUND( at.x ), KiROUND( at.y ) ), ANGLE_0 );
|
VECTOR2I( KiROUND( at.x + w / 2 ), KiROUND( at.y + h / 2 ) ),
|
||||||
|
ANGLE_0 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
#include <schematic_settings.h>
|
#include <schematic_settings.h>
|
||||||
#include <tool/coroutine.h>
|
#include <tool/coroutine.h>
|
||||||
#include "collab_presence_style.h"
|
#include "collab_presence_style.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
using namespace emscripten;
|
using namespace emscripten;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
@ -1627,6 +1628,66 @@ void schCollabSetStyle( std::string aJson )
|
||||||
presence::scheduleRedraw();
|
presence::scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuner helper: a VARIED demo-selection set for the current sheet — smallest +
|
||||||
|
// largest symbol and two bundles of wires (net-ish), mirroring pcbnew's
|
||||||
|
// pcbCollabTestDemoSet so the style preview shows the range of shapes.
|
||||||
|
std::string schCollabTestDemoSet()
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
json groups = json::array();
|
||||||
|
|
||||||
|
if( fr )
|
||||||
|
{
|
||||||
|
if( SCH_SCREEN* screen = currentScreen( fr ) )
|
||||||
|
{
|
||||||
|
SCH_SYMBOL* smallest = nullptr;
|
||||||
|
SCH_SYMBOL* largest = nullptr;
|
||||||
|
double minA = 0, maxA = 0;
|
||||||
|
std::vector<std::string> wires;
|
||||||
|
|
||||||
|
for( SCH_ITEM* item : screen->Items() )
|
||||||
|
{
|
||||||
|
if( item->Type() == SCH_SYMBOL_T )
|
||||||
|
{
|
||||||
|
BOX2I bb = item->GetBoundingBox();
|
||||||
|
double a = (double) bb.GetWidth() * bb.GetHeight();
|
||||||
|
|
||||||
|
auto* sym = static_cast<SCH_SYMBOL*>( item );
|
||||||
|
|
||||||
|
if( !smallest || a < minA ) { smallest = sym; minA = a; }
|
||||||
|
if( !largest || a > maxA ) { largest = sym; maxA = a; }
|
||||||
|
}
|
||||||
|
else if( item->Type() == SCH_LINE_T && wires.size() < 8 )
|
||||||
|
{
|
||||||
|
wires.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( smallest )
|
||||||
|
groups.push_back( { { "label", "symbol small" },
|
||||||
|
{ "ids", { toUtf8( smallest->m_Uuid.AsString() ) } } } );
|
||||||
|
|
||||||
|
if( largest && largest != smallest )
|
||||||
|
groups.push_back( { { "label", "symbol large" },
|
||||||
|
{ "ids", { toUtf8( largest->m_Uuid.AsString() ) } } } );
|
||||||
|
|
||||||
|
if( wires.size() >= 2 )
|
||||||
|
{
|
||||||
|
size_t half = wires.size() / 2;
|
||||||
|
groups.push_back( { { "label", "wires A" },
|
||||||
|
{ "ids", std::vector<std::string>( wires.begin(),
|
||||||
|
wires.begin() + half ) } } );
|
||||||
|
groups.push_back( { { "label", "wires B" },
|
||||||
|
{ "ids", std::vector<std::string>( wires.begin() + half,
|
||||||
|
wires.end() ) } } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json{ { "groups", groups } }.dump();
|
||||||
|
}
|
||||||
|
|
||||||
// Test/tuner helper: the first N item uuids of the CURRENT sheet — real,
|
// Test/tuner helper: the first N item uuids of the CURRENT sheet — real,
|
||||||
// resolvable KIIDs for synthetic remote-selection previews.
|
// resolvable KIIDs for synthetic remote-selection previews.
|
||||||
std::string schCollabTestListItems( int aCount )
|
std::string schCollabTestListItems( int aCount )
|
||||||
|
|
@ -1824,6 +1885,7 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
||||||
function("kicadCollabSetViewport", &schCollabSetViewport);
|
function("kicadCollabSetViewport", &schCollabSetViewport);
|
||||||
function("kicadCollabSetStyle", &schCollabSetStyle);
|
function("kicadCollabSetStyle", &schCollabSetStyle);
|
||||||
function("kicadCollabTestListItems", &schCollabTestListItems);
|
function("kicadCollabTestListItems", &schCollabTestListItems);
|
||||||
|
function("kicadCollabTestDemoSet", &schCollabTestDemoSet);
|
||||||
function("kicadCollabGetViewport", &schCollabGetViewport);
|
function("kicadCollabGetViewport", &schCollabGetViewport);
|
||||||
function("kicadCollabGetSelection", &schCollabGetSelection);
|
function("kicadCollabGetSelection", &schCollabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ void pcbCollabSetPins( std::string aJson );
|
||||||
void pcbCollabSetViewport( double aCx, double aCy );
|
void pcbCollabSetViewport( double aCx, double aCy );
|
||||||
void pcbCollabSetStyle( std::string aJson );
|
void pcbCollabSetStyle( std::string aJson );
|
||||||
std::string pcbCollabTestListItems( int aCount );
|
std::string pcbCollabTestListItems( int aCount );
|
||||||
|
std::string pcbCollabTestDemoSet();
|
||||||
std::string pcbCollabGetViewport();
|
std::string pcbCollabGetViewport();
|
||||||
std::string pcbCollabGetSelection();
|
std::string pcbCollabGetSelection();
|
||||||
std::string pcbCollabTestSelectFirst();
|
std::string pcbCollabTestSelectFirst();
|
||||||
|
|
@ -73,6 +74,7 @@ void schCollabSetPins( std::string aJson );
|
||||||
void schCollabSetViewport( double aCx, double aCy );
|
void schCollabSetViewport( double aCx, double aCy );
|
||||||
void schCollabSetStyle( std::string aJson );
|
void schCollabSetStyle( std::string aJson );
|
||||||
std::string schCollabTestListItems( int aCount );
|
std::string schCollabTestListItems( int aCount );
|
||||||
|
std::string schCollabTestDemoSet();
|
||||||
std::string schCollabGetViewport();
|
std::string schCollabGetViewport();
|
||||||
std::string schCollabGetSelection();
|
std::string schCollabGetSelection();
|
||||||
std::string schCollabTestSelectFirst();
|
std::string schCollabTestSelectFirst();
|
||||||
|
|
@ -187,6 +189,11 @@ static std::string collabTestListItems( int aCount )
|
||||||
return pcbEditorActive() ? pcbCollabTestListItems( aCount ) : schCollabTestListItems( aCount );
|
return pcbEditorActive() ? pcbCollabTestListItems( aCount ) : schCollabTestListItems( aCount );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string collabTestDemoSet()
|
||||||
|
{
|
||||||
|
return pcbEditorActive() ? pcbCollabTestDemoSet() : schCollabTestDemoSet();
|
||||||
|
}
|
||||||
|
|
||||||
static std::string collabGetViewport()
|
static std::string collabGetViewport()
|
||||||
{
|
{
|
||||||
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
||||||
|
|
@ -231,6 +238,7 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
||||||
function("kicadCollabSetViewport", &collabSetViewport);
|
function("kicadCollabSetViewport", &collabSetViewport);
|
||||||
function("kicadCollabSetStyle", &collabSetStyle);
|
function("kicadCollabSetStyle", &collabSetStyle);
|
||||||
function("kicadCollabTestListItems", &collabTestListItems);
|
function("kicadCollabTestListItems", &collabTestListItems);
|
||||||
|
function("kicadCollabTestDemoSet", &collabTestDemoSet);
|
||||||
function("kicadCollabGetViewport", &collabGetViewport);
|
function("kicadCollabGetViewport", &collabGetViewport);
|
||||||
function("kicadCollabGetSelection", &collabGetSelection);
|
function("kicadCollabGetSelection", &collabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@
|
||||||
#include <pcb_draw_panel_gal.h>
|
#include <pcb_draw_panel_gal.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include "collab_presence_style.h"
|
#include "collab_presence_style.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -1232,8 +1233,40 @@ void redrawOverlay()
|
||||||
if( !item )
|
if( !item )
|
||||||
continue; // not on this board (yet) — skip silently
|
continue; // not on this board (yet) — skip silently
|
||||||
|
|
||||||
|
// Exact-geometry outline (style shape 5): footprints hug their
|
||||||
|
// bounding hull, everything else its transformed shape. Runs on
|
||||||
|
// the coroutine fiber (virtual dispatch), falls back to the bbox
|
||||||
|
// on anything that can't produce a polygon.
|
||||||
|
SHAPE_POLY_SET outline;
|
||||||
|
|
||||||
|
if( g_style.selShape == 5 )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int pad = KiROUND( g_style.selPaddingPx * px );
|
||||||
|
int err = KiROUND( px ) + 1;
|
||||||
|
|
||||||
|
if( item->Type() == PCB_FOOTPRINT_T )
|
||||||
|
{
|
||||||
|
outline = static_cast<FOOTPRINT*>( item )->GetBoundingHull();
|
||||||
|
|
||||||
|
if( pad > 0 )
|
||||||
|
outline.Inflate( pad, CORNER_STRATEGY::ROUND_ALL_CORNERS, err );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item->TransformShapeToPolygon( outline, (PCB_LAYER_ID) itemLayer( item ),
|
||||||
|
pad, err, ERROR_OUTSIDE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( ... )
|
||||||
|
{
|
||||||
|
outline.RemoveAllContours();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name,
|
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name,
|
||||||
color, px, g_style );
|
color, px, g_style, &outline );
|
||||||
}
|
}
|
||||||
|
|
||||||
if( peer.hasCursor )
|
if( peer.hasCursor )
|
||||||
|
|
@ -1703,6 +1736,71 @@ void pcbCollabSetStyle( std::string aJson )
|
||||||
presence::scheduleRedraw();
|
presence::scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tuner helper: a VARIED demo-selection set — labeled uuid groups (smallest +
|
||||||
|
// largest footprint, the two busiest nets' track segments) so the style
|
||||||
|
// preview shows the real range of shapes instead of two overlapping items.
|
||||||
|
std::string pcbCollabTestDemoSet()
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
json groups = json::array();
|
||||||
|
|
||||||
|
if( fr )
|
||||||
|
{
|
||||||
|
BOARD* board = fr->GetBoard();
|
||||||
|
|
||||||
|
// Footprints by bbox area → smallest and largest.
|
||||||
|
FOOTPRINT* smallest = nullptr;
|
||||||
|
FOOTPRINT* largest = nullptr;
|
||||||
|
double minA = 0, maxA = 0;
|
||||||
|
|
||||||
|
for( FOOTPRINT* f : board->Footprints() )
|
||||||
|
{
|
||||||
|
BOX2I bb = f->GetBoundingBox();
|
||||||
|
double a = (double) bb.GetWidth() * bb.GetHeight();
|
||||||
|
|
||||||
|
if( !smallest || a < minA ) { smallest = f; minA = a; }
|
||||||
|
if( !largest || a > maxA ) { largest = f; maxA = a; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if( smallest )
|
||||||
|
groups.push_back( { { "label", "fp small (" + toUtf8( smallest->GetReference() ) + ")" },
|
||||||
|
{ "ids", { toUtf8( smallest->m_Uuid.AsString() ) } } } );
|
||||||
|
|
||||||
|
if( largest && largest != smallest )
|
||||||
|
groups.push_back( { { "label", "fp large (" + toUtf8( largest->GetReference() ) + ")" },
|
||||||
|
{ "ids", { toUtf8( largest->m_Uuid.AsString() ) } } } );
|
||||||
|
|
||||||
|
// Tracks grouped by net → the two busiest nets (capped segment lists).
|
||||||
|
std::map<std::string, std::vector<std::string>> nets;
|
||||||
|
|
||||||
|
for( PCB_TRACK* t : board->Tracks() )
|
||||||
|
{
|
||||||
|
std::string net = toUtf8( t->GetNetname() );
|
||||||
|
|
||||||
|
if( !net.empty() )
|
||||||
|
nets[net].push_back( toUtf8( t->m_Uuid.AsString() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::vector<std::string>>> byCount( nets.begin(),
|
||||||
|
nets.end() );
|
||||||
|
std::sort( byCount.begin(), byCount.end(),
|
||||||
|
[]( const auto& a, const auto& b ) { return a.second.size() > b.second.size(); } );
|
||||||
|
|
||||||
|
for( size_t i = 0; i < byCount.size() && i < 2; ++i )
|
||||||
|
{
|
||||||
|
auto ids = byCount[i].second;
|
||||||
|
|
||||||
|
if( ids.size() > 12 )
|
||||||
|
ids.resize( 12 );
|
||||||
|
|
||||||
|
groups.push_back( { { "label", "net " + byCount[i].first }, { "ids", ids } } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json{ { "groups", groups } }.dump();
|
||||||
|
}
|
||||||
|
|
||||||
// Test/tuner helper: the first N top-level item uuids — real, resolvable KIIDs
|
// Test/tuner helper: the first N top-level item uuids — real, resolvable KIIDs
|
||||||
// for synthetic remote-selection previews (a solo tab has no peer to borrow from).
|
// for synthetic remote-selection previews (a solo tab has no peer to borrow from).
|
||||||
std::string pcbCollabTestListItems( int aCount )
|
std::string pcbCollabTestListItems( int aCount )
|
||||||
|
|
@ -2077,6 +2175,7 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
||||||
function("kicadCollabSetViewport", &pcbCollabSetViewport);
|
function("kicadCollabSetViewport", &pcbCollabSetViewport);
|
||||||
function("kicadCollabSetStyle", &pcbCollabSetStyle);
|
function("kicadCollabSetStyle", &pcbCollabSetStyle);
|
||||||
function("kicadCollabTestListItems", &pcbCollabTestListItems);
|
function("kicadCollabTestListItems", &pcbCollabTestListItems);
|
||||||
|
function("kicadCollabTestDemoSet", &pcbCollabTestDemoSet);
|
||||||
function("kicadCollabGetViewport", &pcbCollabGetViewport);
|
function("kicadCollabGetViewport", &pcbCollabGetViewport);
|
||||||
function("kicadCollabGetSelection", &pcbCollabGetSelection);
|
function("kicadCollabGetSelection", &pcbCollabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &pcbCollabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &pcbCollabTestSelectFirst);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ export interface TunerModule {
|
||||||
kicadCollabSetPins(json: string): void;
|
kicadCollabSetPins(json: string): void;
|
||||||
kicadCollabGetViewport(): string;
|
kicadCollabGetViewport(): string;
|
||||||
kicadCollabTestListItems(n: number): string;
|
kicadCollabTestListItems(n: number): string;
|
||||||
|
/** Varied demo groups (small/large footprint, busiest nets) — newer builds. */
|
||||||
|
kicadCollabTestDemoSet?(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTunerBridge(mod: unknown): mod is TunerModule {
|
export function hasTunerBridge(mod: unknown): mod is TunerModule {
|
||||||
|
|
@ -73,11 +75,26 @@ function loadStored(): Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEL_SHAPES = ["rectangle", "corner brackets", "underline", "rounded rect", "filled only"];
|
const SEL_SHAPES = [
|
||||||
|
"rectangle",
|
||||||
|
"corner brackets",
|
||||||
|
"underline",
|
||||||
|
"rounded rect",
|
||||||
|
"filled only",
|
||||||
|
"exact outline (pcb)",
|
||||||
|
];
|
||||||
const CURSOR_SHAPES = ["cross", "pointer", "circle + dot"];
|
const CURSOR_SHAPES = ["cross", "pointer", "circle + dot"];
|
||||||
const VPOS = ["top", "bottom"];
|
const VPOS = ["top", "bottom"];
|
||||||
const HPOS = ["start", "end", "center"];
|
const HPOS = ["start", "end", "center"];
|
||||||
|
|
||||||
|
/** Preset palettes for quick trials ("palette" color mode). */
|
||||||
|
const PALETTE_PRESETS: Record<string, readonly string[]> = {
|
||||||
|
default: PRESENCE_COLORS,
|
||||||
|
pastel: ["#f9a8d4", "#fca5a5", "#fdba74", "#fde047", "#86efac", "#5eead4", "#93c5fd", "#c4b5fd"],
|
||||||
|
vivid: ["#e11d48", "#ea580c", "#ca8a04", "#16a34a", "#0d9488", "#0284c7", "#4f46e5", "#9333ea"],
|
||||||
|
"okabe-ito": ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"],
|
||||||
|
};
|
||||||
|
|
||||||
export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
const [open, setOpen] = React.useState(true);
|
const [open, setOpen] = React.useState(true);
|
||||||
const [style, setStyle] = React.useState<Style>(loadStored);
|
const [style, setStyle] = React.useState<Style>(loadStored);
|
||||||
|
|
@ -107,23 +124,44 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const vp = JSON.parse(mod.kicadCollabGetViewport());
|
const vp = JSON.parse(mod.kicadCollabGetViewport());
|
||||||
const items = JSON.parse(mod.kicadCollabTestListItems(3)) as string[];
|
|
||||||
const spanX = vp.w / vp.scale;
|
const spanX = vp.w / vp.scale;
|
||||||
const spanY = vp.h / vp.scale;
|
const spanY = vp.h / vp.scale;
|
||||||
|
// Varied selections (small + large footprint, two busiest nets) from
|
||||||
|
// the demo-set helper; older wasm falls back to "first N items".
|
||||||
|
let bobSel: string[] = [];
|
||||||
|
let carolSel: string[] = [];
|
||||||
|
try {
|
||||||
|
const groups = (
|
||||||
|
JSON.parse(mod.kicadCollabTestDemoSet?.() ?? '{"groups":[]}') as {
|
||||||
|
groups: Array<{ label: string; ids: string[] }>;
|
||||||
|
}
|
||||||
|
).groups;
|
||||||
|
// bob: small fp + net A · carol: large fp + net B (whatever exists).
|
||||||
|
for (const [i, g] of groups.entries()) {
|
||||||
|
(i % 2 === 0 ? bobSel : carolSel).push(...g.ids);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* fall through to the flat list */
|
||||||
|
}
|
||||||
|
if (!bobSel.length && !carolSel.length) {
|
||||||
|
const items = JSON.parse(mod.kicadCollabTestListItems(3)) as string[];
|
||||||
|
bobSel = items.slice(0, 1);
|
||||||
|
carolSel = items.slice(1, 3);
|
||||||
|
}
|
||||||
const peers = [
|
const peers = [
|
||||||
{
|
{
|
||||||
id: "demo-bob",
|
id: "demo-bob",
|
||||||
name: "bob",
|
name: "bob",
|
||||||
color: PRESENCE_COLORS[8],
|
color: PRESENCE_COLORS[8],
|
||||||
cursor: { x: vp.cx - spanX * 0.18, y: vp.cy - spanY * 0.12 },
|
cursor: { x: vp.cx - spanX * 0.18, y: vp.cy - spanY * 0.12 },
|
||||||
selection: items.slice(0, 1),
|
selection: bobSel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "demo-carol",
|
id: "demo-carol",
|
||||||
name: "carol",
|
name: "carol",
|
||||||
color: PRESENCE_COLORS[4],
|
color: PRESENCE_COLORS[4],
|
||||||
cursor: { x: vp.cx + spanX * 0.15, y: vp.cy + spanY * 0.16 },
|
cursor: { x: vp.cx + spanX * 0.15, y: vp.cy + spanY * 0.16 },
|
||||||
selection: items.slice(1, 3),
|
selection: carolSel,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const pins = [
|
const pins = [
|
||||||
|
|
@ -242,46 +280,7 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
<Check label="label chip" v={style.cursorLabelChip} onChange={(v) => set("cursorLabelChip", v)} />
|
<Check label="label chip" v={style.cursorLabelChip} onChange={(v) => set("cursorLabelChip", v)} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Colors">
|
<ColorsSection style={style} set={set} />
|
||||||
<div className="mb-1 flex items-center gap-2">
|
|
||||||
<span className="w-24 text-white/60">fixed color</span>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={style.fixedColor || "#3b82f6"}
|
|
||||||
onChange={(e) => set("fixedColor", e.target.value)}
|
|
||||||
className="h-5 w-8"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => set("fixedColor", "")}
|
|
||||||
className="rounded px-1.5 ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
|
||||||
>
|
|
||||||
off
|
|
||||||
</button>
|
|
||||||
<span className="text-white/40">{style.fixedColor || "per-user"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mb-1">
|
|
||||||
<span className="mr-2 text-white/60">palette override (hex, comma-sep)</span>
|
|
||||||
<textarea
|
|
||||||
value={style.palette.join(",")}
|
|
||||||
onChange={(e) =>
|
|
||||||
set(
|
|
||||||
"palette",
|
|
||||||
e.target.value
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => /^#[0-9a-fA-F]{6}$/.test(s)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="#ef4444,#22c55e,… (empty = shipped palette)"
|
|
||||||
className="mt-1 h-10 w-full resize-none rounded bg-white/10 p-1 text-[10px] text-white outline-none"
|
|
||||||
/>
|
|
||||||
<div className="mt-1 flex gap-1">
|
|
||||||
{(style.palette.length ? style.palette : [...PRESENCE_COLORS]).map((c, i) => (
|
|
||||||
<span key={i} className="h-3 w-3 rounded-sm" style={{ background: c }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title="Comment pins">
|
<Section title="Comment pins">
|
||||||
<Range label="radius px" v={style.pinRadiusPx} min={3} max={16} step={0.5} onChange={(v) => set("pinRadiusPx", v)} />
|
<Range label="radius px" v={style.pinRadiusPx} min={3} max={16} step={0.5} onChange={(v) => set("pinRadiusPx", v)} />
|
||||||
|
|
@ -294,6 +293,125 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color overrides, made explicit: one MODE at a time —
|
||||||
|
* per-user · the colors peers publish (shipped behavior; overrides off)
|
||||||
|
* fixed · everyone in ONE picked color
|
||||||
|
* palette · recolor everyone from a trial palette (by name hash)
|
||||||
|
* Recolors the CANVAS overlay only (boxes/cursors/pins); roster avatars and
|
||||||
|
* comment popovers keep the sender colors. Palette editing is buffered and
|
||||||
|
* applied on demand so typing partial hex values doesn't fight the input.
|
||||||
|
*/
|
||||||
|
function ColorsSection({
|
||||||
|
style,
|
||||||
|
set,
|
||||||
|
}: {
|
||||||
|
style: Style;
|
||||||
|
set: <K extends keyof Style>(k: K, v: Style[K]) => void;
|
||||||
|
}) {
|
||||||
|
const mode = style.fixedColor ? "fixed" : style.palette.length ? "palette" : "per-user";
|
||||||
|
const [fixedPick, setFixedPick] = React.useState(style.fixedColor || "#3b82f6");
|
||||||
|
const [paletteText, setPaletteText] = React.useState(
|
||||||
|
(style.palette.length ? style.palette : [...PRESENCE_COLORS]).join(", "),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsePalette = (text: string) =>
|
||||||
|
text
|
||||||
|
.split(/[,\s]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => /^#[0-9a-fA-F]{6}$/.test(s));
|
||||||
|
|
||||||
|
const setMode = (m: "per-user" | "fixed" | "palette") => {
|
||||||
|
if (m === "per-user") {
|
||||||
|
set("fixedColor", "");
|
||||||
|
set("palette", []);
|
||||||
|
} else if (m === "fixed") {
|
||||||
|
set("palette", []);
|
||||||
|
set("fixedColor", fixedPick);
|
||||||
|
} else {
|
||||||
|
set("fixedColor", "");
|
||||||
|
set("palette", parsePalette(paletteText));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPalette = (colors: readonly string[]) => {
|
||||||
|
setPaletteText(colors.join(", "));
|
||||||
|
set("fixedColor", "");
|
||||||
|
set("palette", [...colors]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title="Colors">
|
||||||
|
<p className="mb-1 text-[10px] leading-snug text-white/45">
|
||||||
|
Recolors the canvas overlay (boxes, cursors, pins). Roster avatars keep
|
||||||
|
the sender colors.
|
||||||
|
</p>
|
||||||
|
<div className="mb-1 flex gap-2">
|
||||||
|
{(["per-user", "fixed", "palette"] as const).map((m) => (
|
||||||
|
<label key={m} className="flex items-center gap-1">
|
||||||
|
<input type="radio" checked={mode === m} onChange={() => setMode(m)} />
|
||||||
|
<span className={mode === m ? "text-white" : "text-white/60"}>{m}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === "fixed" && (
|
||||||
|
<div className="mb-1 flex items-center gap-2">
|
||||||
|
<span className="w-24 text-white/60">color</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={fixedPick}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFixedPick(e.target.value);
|
||||||
|
set("fixedColor", e.target.value);
|
||||||
|
}}
|
||||||
|
className="h-5 w-8"
|
||||||
|
/>
|
||||||
|
<span className="text-white/40">{style.fixedColor}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === "palette" && (
|
||||||
|
<div className="mb-1 space-y-1">
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{Object.entries(PALETTE_PRESETS).map(([name, colors]) => (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
onClick={() => applyPalette(colors)}
|
||||||
|
className="rounded px-1.5 py-0.5 text-[10px] ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={paletteText}
|
||||||
|
onChange={(e) => setPaletteText(e.target.value)}
|
||||||
|
placeholder="#ef4444, #22c55e, …"
|
||||||
|
className="h-12 w-full resize-none rounded bg-white/10 p-1 font-mono text-[10px] text-white outline-none"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => set("palette", parsePalette(paletteText))}
|
||||||
|
className="rounded bg-fuchsia-700 px-2 py-0.5 text-[10px] hover:bg-fuchsia-600"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
<span className="text-[10px] text-white/40">
|
||||||
|
{parsePalette(paletteText).length} colors
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(style.palette.length ? style.palette : parsePalette(paletteText)).map((c, i) => (
|
||||||
|
<span key={i} className="h-3 w-3 rounded-sm" style={{ background: c }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<details open className="mt-2">
|
<details open className="mt-2">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue