feat(collab): dev-time presence style tuner (VITE_PRESENCE_TUNER=1)
Parametrizes every visual knob of the presence overlay so we can pick the shipped look live, then wire the winners into the defaults: - collab_presence_style.h: shared STYLE struct + drawing (now used by BOTH editor TUs — no more duplicated overlay code): selection shape (rect / corner brackets / underline / rounded rect / filled-only), border width + alpha, infill alpha, padding, corner radius; name tag show/size/chip- background/inside-outside/top-bottom/start-end-center/offset; cursor shape (cross / pointer / circle+dot), size/width/alpha + label knobs; fixed-color and palette-by-name-hash overrides (try palettes without changing what senders publish); pin radius/ring/alphas. Defaults == shipped look. - kicadCollabSetStyle(json) live-patch export + kicadCollabTestListItems(n) (real KIIDs for synthetic previews); merged dispatch; pins now carry the author name so palette overrides recolor them consistently. - PresenceTuner.tsx: floating dev panel (env-gated, tree-shaken otherwise) — grouped sliders/selects, demo peers+pins injection for SOLO tuning, localStorage persistence across reloads, Copy JSON export, reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
parent
635aa2926a
commit
ce99773af4
8 changed files with 907 additions and 77 deletions
385
wasm/bindings/collab_presence_style.h
Normal file
385
wasm/bindings/collab_presence_style.h
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
/*
|
||||||
|
* Presence overlay styling (collab-presence tuner) — shared by the pcbnew and
|
||||||
|
* eeschema binding TUs so the drawing never diverges between editors.
|
||||||
|
*
|
||||||
|
* Every visual knob of the remote-presence rendering (selection boxes, name
|
||||||
|
* tags, cursors, comment pin dots) lives in STYLE, JSON-patchable at runtime
|
||||||
|
* via kicadCollabSetStyle — the dev-time PresenceTuner panel drives it to find
|
||||||
|
* the look we want; the chosen values then become the defaults here.
|
||||||
|
* Defaults == the shipped look.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <gal/color4d.h>
|
||||||
|
#include <geometry/eda_angle.h>
|
||||||
|
#include <math/util.h>
|
||||||
|
#include <math/box2.h>
|
||||||
|
#include <view/view_overlay.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <wx/string.h>
|
||||||
|
|
||||||
|
namespace pcbjam_presence {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
struct STYLE
|
||||||
|
{
|
||||||
|
// ── selection box ──────────────────────────────────────────────────────
|
||||||
|
// 0 rect · 1 corner brackets · 2 underline · 3 rounded rect · 4 filled only
|
||||||
|
int selShape = 0;
|
||||||
|
double selStrokeWidth = 2.5; // px
|
||||||
|
double selStrokeAlpha = 0.9;
|
||||||
|
double selFillAlpha = 0.0; // 0 = no fill
|
||||||
|
double selPaddingPx = 4.0; // bbox inflate
|
||||||
|
double selCornerPx = 8.0; // bracket arm length / rounding radius
|
||||||
|
|
||||||
|
// ── selection name tag ────────────────────────────────────────────────
|
||||||
|
bool labelShow = true;
|
||||||
|
double labelSizePx = 9.0;
|
||||||
|
bool labelChip = false; // filled background chip + white text
|
||||||
|
int labelVPos = 0; // 0 top · 1 bottom
|
||||||
|
int labelHPos = 0; // 0 start · 1 end · 2 center
|
||||||
|
bool labelInside = false; // inside vs outside the box
|
||||||
|
double labelOffsetPx = 8.0;
|
||||||
|
|
||||||
|
// ── remote cursor ─────────────────────────────────────────────────────
|
||||||
|
// 0 cross · 1 pointer triangle · 2 circle + dot
|
||||||
|
int cursorShape = 0;
|
||||||
|
double cursorSizePx = 7.0;
|
||||||
|
double cursorWidthPx = 2.0;
|
||||||
|
double cursorAlpha = 0.9;
|
||||||
|
bool cursorLabel = true;
|
||||||
|
double cursorLabelSizePx = 10.0;
|
||||||
|
bool cursorLabelChip = false;
|
||||||
|
|
||||||
|
// ── colors ────────────────────────────────────────────────────────────
|
||||||
|
// fixedColor: every peer in ONE color ("" = off). palette: recolor peers
|
||||||
|
// by name hash from this list ([] = off) — for trying palettes without
|
||||||
|
// changing what senders publish. Peer-provided color is the fallback.
|
||||||
|
std::string fixedColor;
|
||||||
|
std::vector<std::string> palette;
|
||||||
|
|
||||||
|
// ── comment pin dots ──────────────────────────────────────────────────
|
||||||
|
double pinRadiusPx = 7.0;
|
||||||
|
double pinRingPx = 1.5;
|
||||||
|
double pinRingAlpha = 0.9;
|
||||||
|
double pinFillAlpha = 1.0;
|
||||||
|
double pinResolvedAlpha = 0.3;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline KIGFX::COLOR4D parseHexColor( const std::string& aHex, const KIGFX::COLOR4D& aFallback )
|
||||||
|
{
|
||||||
|
if( aHex.size() == 7 && aHex[0] == '#' )
|
||||||
|
{
|
||||||
|
long v = strtol( aHex.c_str() + 1, nullptr, 16 );
|
||||||
|
return KIGFX::COLOR4D( ( ( v >> 16 ) & 0xff ) / 255.0, ( ( v >> 8 ) & 0xff ) / 255.0,
|
||||||
|
( v & 0xff ) / 255.0, 1.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return aFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patch aStyle from a (partial) JSON object — unknown keys ignored, absent
|
||||||
|
* keys keep their value, so the tuner can send full or incremental states. */
|
||||||
|
inline void patchStyle( STYLE& aStyle, const json& j )
|
||||||
|
{
|
||||||
|
aStyle.selShape = j.value( "selShape", aStyle.selShape );
|
||||||
|
aStyle.selStrokeWidth = j.value( "selStrokeWidth", aStyle.selStrokeWidth );
|
||||||
|
aStyle.selStrokeAlpha = j.value( "selStrokeAlpha", aStyle.selStrokeAlpha );
|
||||||
|
aStyle.selFillAlpha = j.value( "selFillAlpha", aStyle.selFillAlpha );
|
||||||
|
aStyle.selPaddingPx = j.value( "selPaddingPx", aStyle.selPaddingPx );
|
||||||
|
aStyle.selCornerPx = j.value( "selCornerPx", aStyle.selCornerPx );
|
||||||
|
|
||||||
|
aStyle.labelShow = j.value( "labelShow", aStyle.labelShow );
|
||||||
|
aStyle.labelSizePx = j.value( "labelSizePx", aStyle.labelSizePx );
|
||||||
|
aStyle.labelChip = j.value( "labelChip", aStyle.labelChip );
|
||||||
|
aStyle.labelVPos = j.value( "labelVPos", aStyle.labelVPos );
|
||||||
|
aStyle.labelHPos = j.value( "labelHPos", aStyle.labelHPos );
|
||||||
|
aStyle.labelInside = j.value( "labelInside", aStyle.labelInside );
|
||||||
|
aStyle.labelOffsetPx = j.value( "labelOffsetPx", aStyle.labelOffsetPx );
|
||||||
|
|
||||||
|
aStyle.cursorShape = j.value( "cursorShape", aStyle.cursorShape );
|
||||||
|
aStyle.cursorSizePx = j.value( "cursorSizePx", aStyle.cursorSizePx );
|
||||||
|
aStyle.cursorWidthPx = j.value( "cursorWidthPx", aStyle.cursorWidthPx );
|
||||||
|
aStyle.cursorAlpha = j.value( "cursorAlpha", aStyle.cursorAlpha );
|
||||||
|
aStyle.cursorLabel = j.value( "cursorLabel", aStyle.cursorLabel );
|
||||||
|
aStyle.cursorLabelSizePx = j.value( "cursorLabelSizePx", aStyle.cursorLabelSizePx );
|
||||||
|
aStyle.cursorLabelChip = j.value( "cursorLabelChip", aStyle.cursorLabelChip );
|
||||||
|
|
||||||
|
aStyle.fixedColor = j.value( "fixedColor", aStyle.fixedColor );
|
||||||
|
|
||||||
|
if( j.contains( "palette" ) && j["palette"].is_array() )
|
||||||
|
{
|
||||||
|
aStyle.palette.clear();
|
||||||
|
|
||||||
|
for( const json& c : j["palette"] )
|
||||||
|
{
|
||||||
|
if( c.is_string() )
|
||||||
|
aStyle.palette.push_back( c.get<std::string>() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aStyle.pinRadiusPx = j.value( "pinRadiusPx", aStyle.pinRadiusPx );
|
||||||
|
aStyle.pinRingPx = j.value( "pinRingPx", aStyle.pinRingPx );
|
||||||
|
aStyle.pinRingAlpha = j.value( "pinRingAlpha", aStyle.pinRingAlpha );
|
||||||
|
aStyle.pinFillAlpha = j.value( "pinFillAlpha", aStyle.pinFillAlpha );
|
||||||
|
aStyle.pinResolvedAlpha = j.value( "pinResolvedAlpha", aStyle.pinResolvedAlpha );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The color a peer renders with under this style (fixed > palette-by-name-hash
|
||||||
|
* > the sender-provided color). */
|
||||||
|
inline KIGFX::COLOR4D peerColor( const STYLE& aStyle, const std::string& aName,
|
||||||
|
const KIGFX::COLOR4D& aProvided )
|
||||||
|
{
|
||||||
|
if( !aStyle.fixedColor.empty() )
|
||||||
|
return parseHexColor( aStyle.fixedColor, aProvided );
|
||||||
|
|
||||||
|
if( !aStyle.palette.empty() )
|
||||||
|
{
|
||||||
|
unsigned h = 0x811c9dc5;
|
||||||
|
|
||||||
|
for( char c : aName )
|
||||||
|
{
|
||||||
|
h ^= (unsigned char) c;
|
||||||
|
h *= 0x01000193;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseHexColor( aStyle.palette[h % aStyle.palette.size()], aProvided );
|
||||||
|
}
|
||||||
|
|
||||||
|
return aProvided;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rough bitmap-font advance (the GAL stroke/bitmap glyphs are ~0.75 em wide) —
|
||||||
|
// good enough to size label chips and right-align labels for the tuner.
|
||||||
|
inline double textWidth( const std::string& aText, double aGlyphH )
|
||||||
|
{
|
||||||
|
return aText.size() * aGlyphH * 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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
|
||||||
|
* edge, vertically centered-ish — offsets are tuned around that. */
|
||||||
|
inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, const BOX2I& aBox, const std::string& aText,
|
||||||
|
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
||||||
|
{
|
||||||
|
if( !aS.labelShow || aText.empty() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
double h = aS.labelSizePx * aPx;
|
||||||
|
double w = textWidth( aText, h );
|
||||||
|
double off = aS.labelOffsetPx * aPx;
|
||||||
|
|
||||||
|
double x = aBox.GetOrigin().x; // start
|
||||||
|
|
||||||
|
if( aS.labelHPos == 1 )
|
||||||
|
x = aBox.GetEnd().x - w; // end
|
||||||
|
else if( aS.labelHPos == 2 )
|
||||||
|
x = ( aBox.GetOrigin().x + aBox.GetEnd().x ) / 2.0 - w / 2.0; // center
|
||||||
|
|
||||||
|
double y;
|
||||||
|
|
||||||
|
if( aS.labelVPos == 0 )
|
||||||
|
y = aS.labelInside ? aBox.GetOrigin().y + off : aBox.GetOrigin().y - off;
|
||||||
|
else
|
||||||
|
y = aS.labelInside ? aBox.GetEnd().y - off : aBox.GetEnd().y + off;
|
||||||
|
|
||||||
|
if( aS.labelChip )
|
||||||
|
{
|
||||||
|
double padX = 3 * aPx, padY = 2.5 * aPx;
|
||||||
|
aOv->SetIsStroke( false );
|
||||||
|
aOv->SetIsFill( true );
|
||||||
|
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
||||||
|
aOv->Rectangle( VECTOR2D( x - padX, y - h / 2 - padY ),
|
||||||
|
VECTOR2D( x + w + padX, y + h / 2 + padY ) );
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
||||||
|
}
|
||||||
|
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 ), KiROUND( y ) ),
|
||||||
|
ANGLE_0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selection highlight for one item bbox, in the chosen shape. */
|
||||||
|
inline void drawSelectionBox( KIGFX::VIEW_OVERLAY* aOv, BOX2I aBox, const std::string& aName,
|
||||||
|
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
||||||
|
{
|
||||||
|
aBox.Inflate( KiROUND( aS.selPaddingPx * aPx ) );
|
||||||
|
|
||||||
|
const VECTOR2D tl = aBox.GetOrigin();
|
||||||
|
const VECTOR2D br = aBox.GetEnd();
|
||||||
|
const VECTOR2D tr( br.x, tl.y );
|
||||||
|
const VECTOR2D bl( tl.x, br.y );
|
||||||
|
|
||||||
|
bool fill = aS.selFillAlpha > 0.001 || aS.selShape == 4;
|
||||||
|
double fillAlpha = aS.selShape == 4 && aS.selFillAlpha <= 0.001 ? 0.18 : aS.selFillAlpha;
|
||||||
|
|
||||||
|
aOv->SetIsStroke( aS.selShape != 4 );
|
||||||
|
aOv->SetIsFill( fill );
|
||||||
|
aOv->SetStrokeColor( aColor.WithAlpha( aS.selStrokeAlpha ) );
|
||||||
|
aOv->SetFillColor( aColor.WithAlpha( fillAlpha ) );
|
||||||
|
aOv->SetLineWidth( aS.selStrokeWidth * aPx );
|
||||||
|
|
||||||
|
switch( aS.selShape )
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case 0: // rectangle (fill rides along when selFillAlpha > 0)
|
||||||
|
case 4: // filled only
|
||||||
|
aOv->Rectangle( tl, br );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // corner brackets
|
||||||
|
{
|
||||||
|
if( fill )
|
||||||
|
{
|
||||||
|
aOv->SetIsStroke( false );
|
||||||
|
aOv->Rectangle( tl, br );
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
double arm = std::min( { aS.selCornerPx * aPx, ( br.x - tl.x ) / 2.0,
|
||||||
|
( br.y - tl.y ) / 2.0 } );
|
||||||
|
aOv->Line( tl, tl + VECTOR2D( arm, 0 ) );
|
||||||
|
aOv->Line( tl, tl + VECTOR2D( 0, arm ) );
|
||||||
|
aOv->Line( tr, tr + VECTOR2D( -arm, 0 ) );
|
||||||
|
aOv->Line( tr, tr + VECTOR2D( 0, arm ) );
|
||||||
|
aOv->Line( bl, bl + VECTOR2D( arm, 0 ) );
|
||||||
|
aOv->Line( bl, bl + VECTOR2D( 0, -arm ) );
|
||||||
|
aOv->Line( br, br + VECTOR2D( -arm, 0 ) );
|
||||||
|
aOv->Line( br, br + VECTOR2D( 0, -arm ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: // underline
|
||||||
|
aOv->Line( bl, br );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // rounded rectangle (lines + quarter arcs)
|
||||||
|
{
|
||||||
|
if( fill )
|
||||||
|
{
|
||||||
|
aOv->SetIsStroke( false );
|
||||||
|
aOv->Rectangle( tl, br );
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
}
|
||||||
|
|
||||||
|
double r = std::min( { aS.selCornerPx * aPx, ( br.x - tl.x ) / 2.0,
|
||||||
|
( br.y - tl.y ) / 2.0 } );
|
||||||
|
aOv->Line( tl + VECTOR2D( r, 0 ), tr + VECTOR2D( -r, 0 ) );
|
||||||
|
aOv->Line( bl + VECTOR2D( r, 0 ), br + VECTOR2D( -r, 0 ) );
|
||||||
|
aOv->Line( tl + VECTOR2D( 0, r ), bl + VECTOR2D( 0, -r ) );
|
||||||
|
aOv->Line( tr + VECTOR2D( 0, r ), br + VECTOR2D( 0, -r ) );
|
||||||
|
// Screen-y grows down: the "top-left" corner arc spans 180°→270°.
|
||||||
|
aOv->Arc( tl + VECTOR2D( r, r ), r, EDA_ANGLE( 180, DEGREES_T ), EDA_ANGLE( 270, DEGREES_T ) );
|
||||||
|
aOv->Arc( tr + VECTOR2D( -r, r ), r, EDA_ANGLE( 270, DEGREES_T ), EDA_ANGLE( 360, DEGREES_T ) );
|
||||||
|
aOv->Arc( br + VECTOR2D( -r, -r ), r, EDA_ANGLE( 0, DEGREES_T ), EDA_ANGLE( 90, DEGREES_T ) );
|
||||||
|
aOv->Arc( bl + VECTOR2D( r, -r ), r, EDA_ANGLE( 90, DEGREES_T ), EDA_ANGLE( 180, DEGREES_T ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLabel( aOv, 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,
|
||||||
|
const KIGFX::COLOR4D& aColor, double aPx, const STYLE& aS )
|
||||||
|
{
|
||||||
|
double s = aS.cursorSizePx * aPx;
|
||||||
|
KIGFX::COLOR4D c = aColor.WithAlpha( aS.cursorAlpha );
|
||||||
|
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetStrokeColor( c );
|
||||||
|
aOv->SetLineWidth( aS.cursorWidthPx * aPx );
|
||||||
|
|
||||||
|
switch( aS.cursorShape )
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case 0: // cross
|
||||||
|
aOv->Cross( aPos, KiROUND( s ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // pointer triangle (mouse-arrow-ish, filled)
|
||||||
|
{
|
||||||
|
aOv->SetIsFill( true );
|
||||||
|
aOv->SetFillColor( c );
|
||||||
|
VECTOR2D pts[3] = { aPos, aPos + VECTOR2D( 0.45 * s * 2, 1.6 * s ),
|
||||||
|
aPos + VECTOR2D( 1.1 * s, 1.1 * s ) };
|
||||||
|
aOv->Polygon( pts, 3 );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: // circle + center dot
|
||||||
|
aOv->Circle( aPos, s );
|
||||||
|
aOv->SetIsFill( true );
|
||||||
|
aOv->SetFillColor( c );
|
||||||
|
aOv->Circle( aPos, 1.5 * aPx );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( aS.cursorLabel && !aName.empty() )
|
||||||
|
{
|
||||||
|
double h = aS.cursorLabelSizePx * aPx;
|
||||||
|
VECTOR2D at = aPos + VECTOR2D( ( aS.cursorSizePx + 4 ) * aPx,
|
||||||
|
( aS.cursorSizePx + 9 ) * aPx );
|
||||||
|
|
||||||
|
if( aS.cursorLabelChip )
|
||||||
|
{
|
||||||
|
double w = textWidth( aName, h ), padX = 3 * aPx, padY = 2.5 * aPx;
|
||||||
|
aOv->SetIsStroke( false );
|
||||||
|
aOv->SetIsFill( true );
|
||||||
|
aOv->SetFillColor( aColor.WithAlpha( 0.92 ) );
|
||||||
|
aOv->Rectangle( at + VECTOR2D( -padX, -h / 2 - padY ),
|
||||||
|
at + VECTOR2D( w + padX, h / 2 + padY ) );
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( false );
|
||||||
|
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, 1 ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
aOv->SetStrokeColor( c );
|
||||||
|
}
|
||||||
|
|
||||||
|
aOv->SetGlyphSize( VECTOR2I( KiROUND( h ), KiROUND( h ) ) );
|
||||||
|
aOv->BitmapText( wxString::FromUTF8( aName.c_str() ),
|
||||||
|
VECTOR2I( KiROUND( at.x ), KiROUND( at.y ) ), ANGLE_0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Comment pin dot. */
|
||||||
|
inline void drawPin( KIGFX::VIEW_OVERLAY* aOv, const VECTOR2D& aPos, const KIGFX::COLOR4D& aColor,
|
||||||
|
bool aResolved, double aPx, const STYLE& aS )
|
||||||
|
{
|
||||||
|
double fillAlpha = aResolved ? aS.pinResolvedAlpha : aS.pinFillAlpha;
|
||||||
|
double ringAlpha = aResolved ? aS.pinRingAlpha * 0.4 : aS.pinRingAlpha;
|
||||||
|
|
||||||
|
aOv->SetIsStroke( true );
|
||||||
|
aOv->SetIsFill( true );
|
||||||
|
aOv->SetFillColor( aColor.WithAlpha( fillAlpha ) );
|
||||||
|
aOv->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, ringAlpha ) );
|
||||||
|
aOv->SetLineWidth( aS.pinRingPx * aPx );
|
||||||
|
aOv->Circle( aPos, aS.pinRadiusPx * aPx );
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pcbjam_presence
|
||||||
|
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
#include <sch_sheet_path.h>
|
#include <sch_sheet_path.h>
|
||||||
#include <schematic_settings.h>
|
#include <schematic_settings.h>
|
||||||
#include <tool/coroutine.h>
|
#include <tool/coroutine.h>
|
||||||
|
#include "collab_presence_style.h"
|
||||||
|
|
||||||
using namespace emscripten;
|
using namespace emscripten;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
@ -706,6 +707,7 @@ struct PEER
|
||||||
struct PIN
|
struct PIN
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
|
std::string name; // author (palette-override rehash key)
|
||||||
VECTOR2D pos; // world coords (IU)
|
VECTOR2D pos; // world coords (IU)
|
||||||
KIGFX::COLOR4D color;
|
KIGFX::COLOR4D color;
|
||||||
bool resolved = false;
|
bool resolved = false;
|
||||||
|
|
@ -713,6 +715,9 @@ struct PIN
|
||||||
|
|
||||||
std::vector<PEER> g_peers;
|
std::vector<PEER> g_peers;
|
||||||
std::vector<PIN> g_pins;
|
std::vector<PIN> g_pins;
|
||||||
|
// Every visual knob (shapes, widths, alphas, label placement, color overrides)
|
||||||
|
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
|
||||||
|
pcbjam_presence::STYLE g_style;
|
||||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
||||||
bool g_started = false;
|
bool g_started = false;
|
||||||
bool g_redrawScheduled = false;
|
bool g_redrawScheduled = false;
|
||||||
|
|
@ -800,12 +805,12 @@ void redrawOverlay()
|
||||||
// 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 );
|
||||||
|
|
||||||
|
// All shapes/sizes/placements come from g_style (collab_presence_style.h);
|
||||||
|
// the drawing itself is shared with pcbnew's TU so the editors never
|
||||||
|
// diverge visually.
|
||||||
for( const PEER& peer : g_peers )
|
for( const PEER& peer : g_peers )
|
||||||
{
|
{
|
||||||
g_overlay->SetIsFill( false );
|
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, peer.name, peer.color );
|
||||||
g_overlay->SetIsStroke( true );
|
|
||||||
g_overlay->SetStrokeColor( peer.color );
|
|
||||||
g_overlay->SetLineWidth( 2.5 * px );
|
|
||||||
|
|
||||||
for( const KIID& id : peer.selection )
|
for( const KIID& id : peer.selection )
|
||||||
{
|
{
|
||||||
|
|
@ -815,47 +820,20 @@ void redrawOverlay()
|
||||||
if( !item )
|
if( !item )
|
||||||
continue; // not in this schematic (yet) — skip silently
|
continue; // not in this schematic (yet) — skip silently
|
||||||
|
|
||||||
BOX2I bb = item->ViewBBox();
|
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name,
|
||||||
bb.Inflate( KiROUND( 4 * px ) );
|
color, px, g_style );
|
||||||
g_overlay->Rectangle( bb.GetOrigin(), bb.GetEnd() );
|
|
||||||
|
|
||||||
// Who selected it — name tag just above the box's top-left corner.
|
|
||||||
if( !peer.name.empty() )
|
|
||||||
{
|
|
||||||
g_overlay->SetGlyphSize( VECTOR2I( KiROUND( 9 * px ), KiROUND( 9 * px ) ) );
|
|
||||||
g_overlay->BitmapText( wxString::FromUTF8( peer.name.c_str() ),
|
|
||||||
VECTOR2I( bb.GetOrigin().x,
|
|
||||||
KiROUND( bb.GetOrigin().y - 8 * px ) ),
|
|
||||||
ANGLE_0 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if( peer.hasCursor )
|
if( peer.hasCursor )
|
||||||
{
|
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px,
|
||||||
g_overlay->SetLineWidth( 2.0 * px );
|
g_style );
|
||||||
g_overlay->Cross( peer.cursor, KiROUND( 7 * px ) );
|
|
||||||
|
|
||||||
if( !peer.name.empty() )
|
|
||||||
{
|
|
||||||
g_overlay->SetGlyphSize( VECTOR2I( KiROUND( 10 * px ), KiROUND( 10 * px ) ) );
|
|
||||||
g_overlay->BitmapText( wxString::FromUTF8( peer.name.c_str() ),
|
|
||||||
VECTOR2I( KiROUND( peer.cursor.x + 10 * px ),
|
|
||||||
KiROUND( peer.cursor.y + 16 * px ) ),
|
|
||||||
ANGLE_0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment pin dots (0005): filled circle in the author's color with a white
|
// Comment pin dots (0005), drawn last so they sit above selection outlines.
|
||||||
// ring; resolved pins dim. Drawn last so they sit above selection outlines.
|
|
||||||
for( const PIN& pin : g_pins )
|
for( const PIN& pin : g_pins )
|
||||||
{
|
{
|
||||||
g_overlay->SetIsStroke( true );
|
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, pin.name, pin.color );
|
||||||
g_overlay->SetIsFill( true );
|
pcbjam_presence::drawPin( g_overlay.get(), pin.pos, color, pin.resolved, px, g_style );
|
||||||
g_overlay->SetFillColor( pin.resolved ? pin.color.WithAlpha( 0.3 ) : pin.color );
|
|
||||||
g_overlay->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, pin.resolved ? 0.35 : 0.9 ) );
|
|
||||||
g_overlay->SetLineWidth( 1.5 * px );
|
|
||||||
g_overlay->Circle( pin.pos, 7 * px );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view->Update( g_overlay.get() );
|
view->Update( g_overlay.get() );
|
||||||
|
|
@ -1624,6 +1602,7 @@ void schCollabSetPins( std::string aJson )
|
||||||
{
|
{
|
||||||
presence::PIN pin;
|
presence::PIN pin;
|
||||||
pin.id = p.value( "id", "" );
|
pin.id = p.value( "id", "" );
|
||||||
|
pin.name = p.value( "name", "" );
|
||||||
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
||||||
pin.color = presence::parseColor( p.value( "color", "" ) );
|
pin.color = presence::parseColor( p.value( "color", "" ) );
|
||||||
pin.resolved = p.value( "resolved", false );
|
pin.resolved = p.value( "resolved", false );
|
||||||
|
|
@ -1635,6 +1614,44 @@ void schCollabSetPins( std::string aJson )
|
||||||
presence::scheduleRedraw();
|
presence::scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JS → C++ (presence tuner): live-patch the overlay STYLE and repaint —
|
||||||
|
// see collab_presence_style.h + pcbnew's counterpart.
|
||||||
|
void schCollabSetStyle( std::string aJson )
|
||||||
|
{
|
||||||
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
pcbjam_presence::patchStyle( presence::g_style, j );
|
||||||
|
presence::scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test/tuner helper: the first N item uuids of the CURRENT sheet — real,
|
||||||
|
// resolvable KIIDs for synthetic remote-selection previews.
|
||||||
|
std::string schCollabTestListItems( int aCount )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
json out = json::array();
|
||||||
|
|
||||||
|
if( fr )
|
||||||
|
{
|
||||||
|
if( SCH_SCREEN* screen = currentScreen( fr ) )
|
||||||
|
{
|
||||||
|
for( SCH_ITEM* item : screen->Items() )
|
||||||
|
{
|
||||||
|
if( (int) out.size() >= aCount )
|
||||||
|
break;
|
||||||
|
|
||||||
|
out.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.dump();
|
||||||
|
}
|
||||||
|
|
||||||
// JS → C++ (0005): pan the view to a world position (comment panel "jump to pin").
|
// JS → C++ (0005): pan the view to a world position (comment panel "jump to pin").
|
||||||
void schCollabSetViewport( double aCx, double aCy )
|
void schCollabSetViewport( double aCx, double aCy )
|
||||||
{
|
{
|
||||||
|
|
@ -1805,6 +1822,8 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
||||||
function("kicadCollabSetRemote", &schCollabSetRemote);
|
function("kicadCollabSetRemote", &schCollabSetRemote);
|
||||||
function("kicadCollabSetPins", &schCollabSetPins);
|
function("kicadCollabSetPins", &schCollabSetPins);
|
||||||
function("kicadCollabSetViewport", &schCollabSetViewport);
|
function("kicadCollabSetViewport", &schCollabSetViewport);
|
||||||
|
function("kicadCollabSetStyle", &schCollabSetStyle);
|
||||||
|
function("kicadCollabTestListItems", &schCollabTestListItems);
|
||||||
function("kicadCollabGetViewport", &schCollabGetViewport);
|
function("kicadCollabGetViewport", &schCollabGetViewport);
|
||||||
function("kicadCollabGetSelection", &schCollabGetSelection);
|
function("kicadCollabGetSelection", &schCollabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ void pcbCollabPresenceStart();
|
||||||
void pcbCollabSetRemote( std::string aJson );
|
void pcbCollabSetRemote( std::string aJson );
|
||||||
void pcbCollabSetPins( std::string aJson );
|
void pcbCollabSetPins( std::string aJson );
|
||||||
void pcbCollabSetViewport( double aCx, double aCy );
|
void pcbCollabSetViewport( double aCx, double aCy );
|
||||||
|
void pcbCollabSetStyle( std::string aJson );
|
||||||
|
std::string pcbCollabTestListItems( int aCount );
|
||||||
std::string pcbCollabGetViewport();
|
std::string pcbCollabGetViewport();
|
||||||
std::string pcbCollabGetSelection();
|
std::string pcbCollabGetSelection();
|
||||||
std::string pcbCollabTestSelectFirst();
|
std::string pcbCollabTestSelectFirst();
|
||||||
|
|
@ -69,6 +71,8 @@ void schCollabPresenceStart();
|
||||||
void schCollabSetRemote( std::string aJson );
|
void schCollabSetRemote( std::string aJson );
|
||||||
void schCollabSetPins( std::string aJson );
|
void schCollabSetPins( std::string aJson );
|
||||||
void schCollabSetViewport( double aCx, double aCy );
|
void schCollabSetViewport( double aCx, double aCy );
|
||||||
|
void schCollabSetStyle( std::string aJson );
|
||||||
|
std::string schCollabTestListItems( int aCount );
|
||||||
std::string schCollabGetViewport();
|
std::string schCollabGetViewport();
|
||||||
std::string schCollabGetSelection();
|
std::string schCollabGetSelection();
|
||||||
std::string schCollabTestSelectFirst();
|
std::string schCollabTestSelectFirst();
|
||||||
|
|
@ -173,6 +177,16 @@ static void collabSetViewport( double aCx, double aCy )
|
||||||
pcbEditorActive() ? pcbCollabSetViewport( aCx, aCy ) : schCollabSetViewport( aCx, aCy );
|
pcbEditorActive() ? pcbCollabSetViewport( aCx, aCy ) : schCollabSetViewport( aCx, aCy );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void collabSetStyle( std::string aJson )
|
||||||
|
{
|
||||||
|
pcbEditorActive() ? pcbCollabSetStyle( aJson ) : schCollabSetStyle( aJson );
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string collabTestListItems( int aCount )
|
||||||
|
{
|
||||||
|
return pcbEditorActive() ? pcbCollabTestListItems( aCount ) : schCollabTestListItems( aCount );
|
||||||
|
}
|
||||||
|
|
||||||
static std::string collabGetViewport()
|
static std::string collabGetViewport()
|
||||||
{
|
{
|
||||||
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
||||||
|
|
@ -215,6 +229,8 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
||||||
function("kicadCollabSetRemote", &collabSetRemote);
|
function("kicadCollabSetRemote", &collabSetRemote);
|
||||||
function("kicadCollabSetPins", &collabSetPins);
|
function("kicadCollabSetPins", &collabSetPins);
|
||||||
function("kicadCollabSetViewport", &collabSetViewport);
|
function("kicadCollabSetViewport", &collabSetViewport);
|
||||||
|
function("kicadCollabSetStyle", &collabSetStyle);
|
||||||
|
function("kicadCollabTestListItems", &collabTestListItems);
|
||||||
function("kicadCollabGetViewport", &collabGetViewport);
|
function("kicadCollabGetViewport", &collabGetViewport);
|
||||||
function("kicadCollabGetSelection", &collabGetSelection);
|
function("kicadCollabGetSelection", &collabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
#include <view/view_overlay.h>
|
#include <view/view_overlay.h>
|
||||||
#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 <chrono>
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -1112,6 +1113,7 @@ struct PEER
|
||||||
struct PIN
|
struct PIN
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
|
std::string name; // author (palette-override rehash key)
|
||||||
VECTOR2D pos; // world coords (IU)
|
VECTOR2D pos; // world coords (IU)
|
||||||
KIGFX::COLOR4D color;
|
KIGFX::COLOR4D color;
|
||||||
bool resolved = false;
|
bool resolved = false;
|
||||||
|
|
@ -1119,6 +1121,9 @@ struct PIN
|
||||||
|
|
||||||
std::vector<PEER> g_peers;
|
std::vector<PEER> g_peers;
|
||||||
std::vector<PIN> g_pins;
|
std::vector<PIN> g_pins;
|
||||||
|
// Every visual knob (shapes, widths, alphas, label placement, color overrides)
|
||||||
|
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
|
||||||
|
pcbjam_presence::STYLE g_style;
|
||||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
||||||
bool g_started = false;
|
bool g_started = false;
|
||||||
bool g_redrawScheduled = false;
|
bool g_redrawScheduled = false;
|
||||||
|
|
@ -1213,12 +1218,12 @@ void redrawOverlay()
|
||||||
// ~7 orders of magnitude (invisible cursors).
|
// ~7 orders of magnitude (invisible cursors).
|
||||||
double px = view->ToWorld( 1.0 );
|
double px = view->ToWorld( 1.0 );
|
||||||
|
|
||||||
|
// All shapes/sizes/placements come from g_style (collab_presence_style.h);
|
||||||
|
// the drawing itself is shared with eeschema's TU so the editors never
|
||||||
|
// diverge visually.
|
||||||
for( const PEER& peer : g_peers )
|
for( const PEER& peer : g_peers )
|
||||||
{
|
{
|
||||||
g_overlay->SetIsFill( false );
|
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, peer.name, peer.color );
|
||||||
g_overlay->SetIsStroke( true );
|
|
||||||
g_overlay->SetStrokeColor( peer.color );
|
|
||||||
g_overlay->SetLineWidth( 2.5 * px );
|
|
||||||
|
|
||||||
for( const KIID& id : peer.selection )
|
for( const KIID& id : peer.selection )
|
||||||
{
|
{
|
||||||
|
|
@ -1227,48 +1232,20 @@ void redrawOverlay()
|
||||||
if( !item )
|
if( !item )
|
||||||
continue; // not on this board (yet) — skip silently
|
continue; // not on this board (yet) — skip silently
|
||||||
|
|
||||||
BOX2I bb = item->ViewBBox();
|
pcbjam_presence::drawSelectionBox( g_overlay.get(), item->ViewBBox(), peer.name,
|
||||||
bb.Inflate( KiROUND( 4 * px ) );
|
color, px, g_style );
|
||||||
g_overlay->Rectangle( bb.GetOrigin(), bb.GetEnd() );
|
|
||||||
|
|
||||||
// Who selected it — name tag just above the box's top-left corner
|
|
||||||
// (world y grows downward, so "above" is -y).
|
|
||||||
if( !peer.name.empty() )
|
|
||||||
{
|
|
||||||
g_overlay->SetGlyphSize( VECTOR2I( KiROUND( 9 * px ), KiROUND( 9 * px ) ) );
|
|
||||||
g_overlay->BitmapText( wxString::FromUTF8( peer.name.c_str() ),
|
|
||||||
VECTOR2I( bb.GetOrigin().x,
|
|
||||||
KiROUND( bb.GetOrigin().y - 8 * px ) ),
|
|
||||||
ANGLE_0 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if( peer.hasCursor )
|
if( peer.hasCursor )
|
||||||
{
|
pcbjam_presence::drawCursor( g_overlay.get(), peer.cursor, peer.name, color, px,
|
||||||
g_overlay->SetLineWidth( 2.0 * px );
|
g_style );
|
||||||
g_overlay->Cross( peer.cursor, KiROUND( 7 * px ) );
|
|
||||||
|
|
||||||
if( !peer.name.empty() )
|
|
||||||
{
|
|
||||||
g_overlay->SetGlyphSize( VECTOR2I( KiROUND( 10 * px ), KiROUND( 10 * px ) ) );
|
|
||||||
g_overlay->BitmapText( wxString::FromUTF8( peer.name.c_str() ),
|
|
||||||
VECTOR2I( KiROUND( peer.cursor.x + 10 * px ),
|
|
||||||
KiROUND( peer.cursor.y + 16 * px ) ),
|
|
||||||
ANGLE_0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment pin dots (0005): filled circle in the author's color with a white
|
// Comment pin dots (0005), drawn last so they sit above selection outlines.
|
||||||
// ring; resolved pins dim. Drawn last so they sit above selection outlines.
|
|
||||||
for( const PIN& pin : g_pins )
|
for( const PIN& pin : g_pins )
|
||||||
{
|
{
|
||||||
g_overlay->SetIsStroke( true );
|
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, pin.name, pin.color );
|
||||||
g_overlay->SetIsFill( true );
|
pcbjam_presence::drawPin( g_overlay.get(), pin.pos, color, pin.resolved, px, g_style );
|
||||||
g_overlay->SetFillColor( pin.resolved ? pin.color.WithAlpha( 0.3 ) : pin.color );
|
|
||||||
g_overlay->SetStrokeColor( KIGFX::COLOR4D( 1, 1, 1, pin.resolved ? 0.35 : 0.9 ) );
|
|
||||||
g_overlay->SetLineWidth( 1.5 * px );
|
|
||||||
g_overlay->Circle( pin.pos, 7 * px );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view->Update( g_overlay.get() );
|
view->Update( g_overlay.get() );
|
||||||
|
|
@ -1700,6 +1677,7 @@ void pcbCollabSetPins( std::string aJson )
|
||||||
{
|
{
|
||||||
presence::PIN pin;
|
presence::PIN pin;
|
||||||
pin.id = p.value( "id", "" );
|
pin.id = p.value( "id", "" );
|
||||||
|
pin.name = p.value( "name", "" );
|
||||||
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
||||||
pin.color = presence::parseColor( p.value( "color", "" ) );
|
pin.color = presence::parseColor( p.value( "color", "" ) );
|
||||||
pin.resolved = p.value( "resolved", false );
|
pin.resolved = p.value( "resolved", false );
|
||||||
|
|
@ -1711,6 +1689,40 @@ void pcbCollabSetPins( std::string aJson )
|
||||||
presence::scheduleRedraw();
|
presence::scheduleRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JS → C++ (presence tuner): live-patch the overlay STYLE (partial JSON —
|
||||||
|
// see collab_presence_style.h) and repaint. Dev-time only in practice, but
|
||||||
|
// harmless in production (nothing calls it without VITE_PRESENCE_TUNER).
|
||||||
|
void pcbCollabSetStyle( std::string aJson )
|
||||||
|
{
|
||||||
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
pcbjam_presence::patchStyle( presence::g_style, j );
|
||||||
|
presence::scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
std::string pcbCollabTestListItems( int aCount )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
json out = json::array();
|
||||||
|
|
||||||
|
if( fr )
|
||||||
|
{
|
||||||
|
forEachTopItem( *fr->GetBoard(), [&]( BOARD_ITEM* item )
|
||||||
|
{
|
||||||
|
if( (int) out.size() < aCount && !item->GetParentFootprint() )
|
||||||
|
out.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.dump();
|
||||||
|
}
|
||||||
|
|
||||||
// JS → C++ (0005): pan the view to a world position (comment panel "jump to
|
// JS → C++ (0005): pan the view to a world position (comment panel "jump to
|
||||||
// pin"). CallAfter + COROUTINE like every other view mutation from JS.
|
// pin"). CallAfter + COROUTINE like every other view mutation from JS.
|
||||||
void pcbCollabSetViewport( double aCx, double aCy )
|
void pcbCollabSetViewport( double aCx, double aCy )
|
||||||
|
|
@ -2063,6 +2075,8 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
||||||
function("kicadCollabSetRemote", &pcbCollabSetRemote);
|
function("kicadCollabSetRemote", &pcbCollabSetRemote);
|
||||||
function("kicadCollabSetPins", &pcbCollabSetPins);
|
function("kicadCollabSetPins", &pcbCollabSetPins);
|
||||||
function("kicadCollabSetViewport", &pcbCollabSetViewport);
|
function("kicadCollabSetViewport", &pcbCollabSetViewport);
|
||||||
|
function("kicadCollabSetStyle", &pcbCollabSetStyle);
|
||||||
|
function("kicadCollabTestListItems", &pcbCollabTestListItems);
|
||||||
function("kicadCollabGetViewport", &pcbCollabGetViewport);
|
function("kicadCollabGetViewport", &pcbCollabGetViewport);
|
||||||
function("kicadCollabGetSelection", &pcbCollabGetSelection);
|
function("kicadCollabGetSelection", &pcbCollabGetSelection);
|
||||||
function("kicadCollabTestSelectFirst", &pcbCollabTestSelectFirst);
|
function("kicadCollabTestSelectFirst", &pcbCollabTestSelectFirst);
|
||||||
|
|
|
||||||
374
web/standalone/src/components/PresenceTuner.tsx
Normal file
374
web/standalone/src/components/PresenceTuner.tsx
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { PRESENCE_COLORS } from "@pcbjam/shared";
|
||||||
|
import { Palette, X } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEV-TIME presence style tuner (VITE_PRESENCE_TUNER=1): live-patches the wasm
|
||||||
|
* overlay style via kicadCollabSetStyle so we can try shapes, widths, alphas,
|
||||||
|
* label placement, palettes etc. and pick the shipped defaults. Settings
|
||||||
|
* persist in localStorage across reloads; "Copy JSON" exports the current
|
||||||
|
* values to wire into collab_presence_style.h. "Demo peers/pins" injects
|
||||||
|
* synthetic remote state (kicadCollabSetRemote/SetPins) so a SOLO tab can
|
||||||
|
* preview everything — note a real awareness/comment change overwrites the
|
||||||
|
* synthetic snapshot (re-toggle to restore).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TunerModule {
|
||||||
|
kicadCollabSetStyle(json: string): void;
|
||||||
|
kicadCollabSetRemote(json: string): void;
|
||||||
|
kicadCollabSetPins(json: string): void;
|
||||||
|
kicadCollabGetViewport(): string;
|
||||||
|
kicadCollabTestListItems(n: number): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasTunerBridge(mod: unknown): mod is TunerModule {
|
||||||
|
const m = mod as Partial<TunerModule> | undefined;
|
||||||
|
return (
|
||||||
|
typeof m?.kicadCollabSetStyle === "function" &&
|
||||||
|
typeof m?.kicadCollabTestListItems === "function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirror of collab_presence_style.h STYLE — defaults MUST match the C++. */
|
||||||
|
const DEFAULT_STYLE = {
|
||||||
|
selShape: 0,
|
||||||
|
selStrokeWidth: 2.5,
|
||||||
|
selStrokeAlpha: 0.9,
|
||||||
|
selFillAlpha: 0,
|
||||||
|
selPaddingPx: 4,
|
||||||
|
selCornerPx: 8,
|
||||||
|
labelShow: true,
|
||||||
|
labelSizePx: 9,
|
||||||
|
labelChip: false,
|
||||||
|
labelVPos: 0,
|
||||||
|
labelHPos: 0,
|
||||||
|
labelInside: false,
|
||||||
|
labelOffsetPx: 8,
|
||||||
|
cursorShape: 0,
|
||||||
|
cursorSizePx: 7,
|
||||||
|
cursorWidthPx: 2,
|
||||||
|
cursorAlpha: 0.9,
|
||||||
|
cursorLabel: true,
|
||||||
|
cursorLabelSizePx: 10,
|
||||||
|
cursorLabelChip: false,
|
||||||
|
fixedColor: "",
|
||||||
|
palette: [] as string[],
|
||||||
|
pinRadiusPx: 7,
|
||||||
|
pinRingPx: 1.5,
|
||||||
|
pinRingAlpha: 0.9,
|
||||||
|
pinFillAlpha: 1,
|
||||||
|
pinResolvedAlpha: 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Style = typeof DEFAULT_STYLE;
|
||||||
|
|
||||||
|
const STORE_KEY = "pcbjam:presence-style";
|
||||||
|
|
||||||
|
function loadStored(): Style {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORE_KEY);
|
||||||
|
return raw ? { ...DEFAULT_STYLE, ...JSON.parse(raw) } : { ...DEFAULT_STYLE };
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_STYLE };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEL_SHAPES = ["rectangle", "corner brackets", "underline", "rounded rect", "filled only"];
|
||||||
|
const CURSOR_SHAPES = ["cross", "pointer", "circle + dot"];
|
||||||
|
const VPOS = ["top", "bottom"];
|
||||||
|
const HPOS = ["start", "end", "center"];
|
||||||
|
|
||||||
|
export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
|
const [open, setOpen] = React.useState(true);
|
||||||
|
const [style, setStyle] = React.useState<Style>(loadStored);
|
||||||
|
const [demo, setDemo] = React.useState(false);
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
// Push on mount (restores a stored style after reload) + on every change.
|
||||||
|
React.useEffect(() => {
|
||||||
|
mod.kicadCollabSetStyle(JSON.stringify(style));
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORE_KEY, JSON.stringify(style));
|
||||||
|
} catch {
|
||||||
|
/* private mode */
|
||||||
|
}
|
||||||
|
}, [mod, style]);
|
||||||
|
|
||||||
|
const set = <K extends keyof Style>(k: K, v: Style[K]) =>
|
||||||
|
setStyle((s) => ({ ...s, [k]: v }));
|
||||||
|
|
||||||
|
const injectDemo = React.useCallback(
|
||||||
|
(on: boolean) => {
|
||||||
|
setDemo(on);
|
||||||
|
if (!on) {
|
||||||
|
mod.kicadCollabSetRemote(JSON.stringify({ peers: [] }));
|
||||||
|
mod.kicadCollabSetPins(JSON.stringify({ pins: [] }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const vp = JSON.parse(mod.kicadCollabGetViewport());
|
||||||
|
const items = JSON.parse(mod.kicadCollabTestListItems(3)) as string[];
|
||||||
|
const spanX = vp.w / vp.scale;
|
||||||
|
const spanY = vp.h / vp.scale;
|
||||||
|
const peers = [
|
||||||
|
{
|
||||||
|
id: "demo-bob",
|
||||||
|
name: "bob",
|
||||||
|
color: PRESENCE_COLORS[8],
|
||||||
|
cursor: { x: vp.cx - spanX * 0.18, y: vp.cy - spanY * 0.12 },
|
||||||
|
selection: items.slice(0, 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-carol",
|
||||||
|
name: "carol",
|
||||||
|
color: PRESENCE_COLORS[4],
|
||||||
|
cursor: { x: vp.cx + spanX * 0.15, y: vp.cy + spanY * 0.16 },
|
||||||
|
selection: items.slice(1, 3),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const pins = [
|
||||||
|
{
|
||||||
|
id: "demo-pin-1",
|
||||||
|
name: "bob",
|
||||||
|
x: vp.cx - spanX * 0.05,
|
||||||
|
y: vp.cy + spanY * 0.2,
|
||||||
|
color: PRESENCE_COLORS[8],
|
||||||
|
resolved: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-pin-2",
|
||||||
|
name: "dave",
|
||||||
|
x: vp.cx + spanX * 0.22,
|
||||||
|
y: vp.cy - spanY * 0.18,
|
||||||
|
color: PRESENCE_COLORS[0],
|
||||||
|
resolved: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mod.kicadCollabSetRemote(JSON.stringify({ peers }));
|
||||||
|
mod.kicadCollabSetPins(JSON.stringify({ pins }));
|
||||||
|
} catch {
|
||||||
|
/* frame not up yet */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mod],
|
||||||
|
);
|
||||||
|
|
||||||
|
const copyJson = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify(style, null, 2));
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
} catch {
|
||||||
|
/* clipboard unavailable */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
title="Presence style tuner"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="absolute left-3 top-12 z-40 flex h-8 w-8 items-center justify-center rounded-full bg-fuchsia-700 text-white shadow-lg"
|
||||||
|
>
|
||||||
|
<Palette size={15} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute left-3 top-12 z-40 flex max-h-[80vh] w-72 flex-col overflow-hidden rounded-lg bg-black/90 text-white shadow-xl ring-1 ring-inset ring-fuchsia-400/40">
|
||||||
|
<div className="flex items-center justify-between bg-fuchsia-900/60 px-3 py-2">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs font-semibold">
|
||||||
|
<Palette size={13} /> Presence tuner <span className="text-white/50">(dev)</span>
|
||||||
|
</span>
|
||||||
|
<button onClick={() => setOpen(false)} className="text-white/70 hover:text-white">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 border-b border-white/10 px-3 py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => injectDemo(!demo)}
|
||||||
|
className={`rounded px-2 py-1 text-[11px] ring-1 ring-inset ring-white/25 ${
|
||||||
|
demo ? "bg-fuchsia-600" : "hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{demo ? "Demo on" : "Demo peers"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={copyJson}
|
||||||
|
className="rounded px-2 py-1 text-[11px] ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
{copied ? "Copied ✓" : "Copy JSON"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setStyle({ ...DEFAULT_STYLE });
|
||||||
|
localStorage.removeItem(STORE_KEY);
|
||||||
|
}}
|
||||||
|
className="rounded px-2 py-1 text-[11px] ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-y-auto px-3 pb-3 text-[11px]">
|
||||||
|
<Section title="Selection box">
|
||||||
|
<Select label="shape" value={style.selShape} options={SEL_SHAPES} onChange={(v) => set("selShape", v)} />
|
||||||
|
<Range label="border px" v={style.selStrokeWidth} min={0.5} max={8} step={0.5} onChange={(v) => set("selStrokeWidth", v)} />
|
||||||
|
<Range label="border α" v={style.selStrokeAlpha} min={0.1} max={1} step={0.05} onChange={(v) => set("selStrokeAlpha", v)} />
|
||||||
|
<Range label="infill α" v={style.selFillAlpha} min={0} max={0.6} step={0.02} onChange={(v) => set("selFillAlpha", v)} />
|
||||||
|
<Range label="padding px" v={style.selPaddingPx} min={0} max={20} step={1} onChange={(v) => set("selPaddingPx", v)} />
|
||||||
|
<Range label="corner px" v={style.selCornerPx} min={2} max={24} step={1} onChange={(v) => set("selCornerPx", v)} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Name tag">
|
||||||
|
<Check label="show" v={style.labelShow} onChange={(v) => set("labelShow", v)} />
|
||||||
|
<Range label="size px" v={style.labelSizePx} min={5} max={20} step={0.5} onChange={(v) => set("labelSizePx", v)} />
|
||||||
|
<Check label="chip background" v={style.labelChip} onChange={(v) => set("labelChip", v)} />
|
||||||
|
<Select label="v-pos" value={style.labelVPos} options={VPOS} onChange={(v) => set("labelVPos", v)} />
|
||||||
|
<Select label="h-pos" value={style.labelHPos} options={HPOS} onChange={(v) => set("labelHPos", v)} />
|
||||||
|
<Check label="inside box" v={style.labelInside} onChange={(v) => set("labelInside", v)} />
|
||||||
|
<Range label="offset px" v={style.labelOffsetPx} min={0} max={24} step={1} onChange={(v) => set("labelOffsetPx", v)} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Cursor">
|
||||||
|
<Select label="shape" value={style.cursorShape} options={CURSOR_SHAPES} onChange={(v) => set("cursorShape", v)} />
|
||||||
|
<Range label="size px" v={style.cursorSizePx} min={3} max={20} step={0.5} onChange={(v) => set("cursorSizePx", v)} />
|
||||||
|
<Range label="line px" v={style.cursorWidthPx} min={0.5} max={6} step={0.25} onChange={(v) => set("cursorWidthPx", v)} />
|
||||||
|
<Range label="alpha" v={style.cursorAlpha} min={0.2} max={1} step={0.05} onChange={(v) => set("cursorAlpha", v)} />
|
||||||
|
<Check label="name label" v={style.cursorLabel} onChange={(v) => set("cursorLabel", v)} />
|
||||||
|
<Range label="label px" v={style.cursorLabelSizePx} min={5} max={20} step={0.5} onChange={(v) => set("cursorLabelSizePx", v)} />
|
||||||
|
<Check label="label chip" v={style.cursorLabelChip} onChange={(v) => set("cursorLabelChip", v)} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Colors">
|
||||||
|
<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">
|
||||||
|
<Range label="radius px" v={style.pinRadiusPx} min={3} max={16} step={0.5} onChange={(v) => set("pinRadiusPx", v)} />
|
||||||
|
<Range label="ring px" v={style.pinRingPx} min={0} max={5} step={0.25} onChange={(v) => set("pinRingPx", v)} />
|
||||||
|
<Range label="ring α" v={style.pinRingAlpha} min={0} max={1} step={0.05} onChange={(v) => set("pinRingAlpha", v)} />
|
||||||
|
<Range label="fill α" v={style.pinFillAlpha} min={0.2} max={1} step={0.05} onChange={(v) => set("pinFillAlpha", v)} />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<details open className="mt-2">
|
||||||
|
<summary className="cursor-pointer text-xs font-semibold text-fuchsia-300">{title}</summary>
|
||||||
|
<div className="mt-1 space-y-1">{children}</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Range({
|
||||||
|
label,
|
||||||
|
v,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
v: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<span className="w-24 shrink-0 text-white/60">{label}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={v}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
className="min-w-0 flex-1"
|
||||||
|
/>
|
||||||
|
<span className="w-9 text-right text-white/80">{v}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Check({ label, v, onChange }: { label: string; v: boolean; onChange: (v: boolean) => void }) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<span className="w-24 shrink-0 text-white/60">{label}</span>
|
||||||
|
<input type="checkbox" checked={v} onChange={(e) => onChange(e.target.checked)} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
options: string[];
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<span className="w-24 shrink-0 text-white/60">{label}</span>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
className="flex-1 rounded bg-white/10 px-1 py-0.5 text-white"
|
||||||
|
>
|
||||||
|
{options.map((o, i) => (
|
||||||
|
<option key={o} value={i} className="bg-neutral-900">
|
||||||
|
{o}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
libsSourceConfig,
|
libsSourceConfig,
|
||||||
modelsSourceConfig,
|
modelsSourceConfig,
|
||||||
presenceUser,
|
presenceUser,
|
||||||
|
PRESENCE_TUNER_ENABLED,
|
||||||
yjsProviderConfig,
|
yjsProviderConfig,
|
||||||
type DocSource,
|
type DocSource,
|
||||||
} from "@/lib/config";
|
} from "@/lib/config";
|
||||||
|
|
@ -65,6 +66,7 @@ import {
|
||||||
} from "@/wasm/collab/comments";
|
} from "@/wasm/collab/comments";
|
||||||
import { PresenceRoster } from "@/components/PresenceRoster";
|
import { PresenceRoster } from "@/components/PresenceRoster";
|
||||||
import { CommentLayer } from "@/components/CommentLayer";
|
import { CommentLayer } from "@/components/CommentLayer";
|
||||||
|
import { hasTunerBridge, PresenceTuner, type TunerModule } from "@/components/PresenceTuner";
|
||||||
import {
|
import {
|
||||||
createSheetCollabManager,
|
createSheetCollabManager,
|
||||||
registerSheetChangedHook,
|
registerSheetChangedHook,
|
||||||
|
|
@ -646,6 +648,9 @@ export function WasmTool({
|
||||||
const [commentsCtl, setCommentsCtl] = React.useState<CommentsController | null>(null);
|
const [commentsCtl, setCommentsCtl] = React.useState<CommentsController | null>(null);
|
||||||
const [viewportState, setViewportState] = React.useState<ViewportState | null>(null);
|
const [viewportState, setViewportState] = React.useState<ViewportState | null>(null);
|
||||||
const commentsRef = React.useRef<CommentsController | null>(null);
|
const commentsRef = React.useRef<CommentsController | null>(null);
|
||||||
|
// Dev-time presence style tuner (VITE_PRESENCE_TUNER=1) — set once the wasm
|
||||||
|
// exposes the style bridge, mounts the floating panel.
|
||||||
|
const [tunerMod, setTunerMod] = React.useState<TunerModule | null>(null);
|
||||||
|
|
||||||
const append = React.useCallback(
|
const append = React.useCallback(
|
||||||
(msg: string) => setLogs((prev) => [...prev.slice(-800), msg]),
|
(msg: string) => setLogs((prev) => [...prev.slice(-800), msg]),
|
||||||
|
|
@ -822,6 +827,9 @@ export function WasmTool({
|
||||||
// Test/debug handle (mirrors window.kicadCollab): lets the e2e reset
|
// Test/debug handle (mirrors window.kicadCollab): lets the e2e reset
|
||||||
// persisted threads deterministically without driving the whole UI.
|
// persisted threads deterministically without driving the whole UI.
|
||||||
(win as { __pcbjamComments?: CommentsController }).__pcbjamComments = ctl;
|
(win as { __pcbjamComments?: CommentsController }).__pcbjamComments = ctl;
|
||||||
|
if (PRESENCE_TUNER_ENABLED && hasTunerBridge(win.Module)) {
|
||||||
|
setTunerMod(win.Module);
|
||||||
|
}
|
||||||
// Seed the transform (pushes only happen on input events after this).
|
// Seed the transform (pushes only happen on input events after this).
|
||||||
try {
|
try {
|
||||||
const vp = JSON.parse(win.Module.kicadCollabGetViewport() || "null");
|
const vp = JSON.parse(win.Module.kicadCollabGetViewport() || "null");
|
||||||
|
|
@ -1167,6 +1175,9 @@ export function WasmTool({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}
|
||||||
|
{ready && tunerMod && <PresenceTuner mod={tunerMod} />}
|
||||||
|
|
||||||
{/* Lib pre-sync still warming IDB after the editor opened (big set) — small
|
{/* Lib pre-sync still warming IDB after the editor opened (big set) — small
|
||||||
unobtrusive indicator so the user knows browsing is still filling in. */}
|
unobtrusive indicator so the user knows browsing is still filling in. */}
|
||||||
{ready && libSync && (
|
{ready && libSync && (
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,14 @@ export function presenceUser(): PresenceUser {
|
||||||
return { id: slug, name: slug, color: colorForUser(slug) };
|
return { id: slug, name: slug, color: colorForUser(slug) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEV-TIME presence style tuner (collab-presence): VITE_PRESENCE_TUNER=1 mounts
|
||||||
|
* a floating panel that live-patches the wasm overlay style
|
||||||
|
* (kicadCollabSetStyle) — shapes, widths, alphas, label placement, palettes —
|
||||||
|
* so we can pick the shipped look. Off (and tree-shaken) in normal builds.
|
||||||
|
*/
|
||||||
|
export const PRESENCE_TUNER_ENABLED = import.meta.env.VITE_PRESENCE_TUNER === "1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The active scope (first URL segment) for API calls. Mirrors how `userSlug()`
|
* The active scope (first URL segment) for API calls. Mirrors how `userSlug()`
|
||||||
* reads the URL, so the source layer scopes requests without threading scope
|
* reads the URL, so the source layer scopes requests without threading scope
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,9 @@ export function createComments(opts: {
|
||||||
.filter((t) => !t.resolved)
|
.filter((t) => !t.resolved)
|
||||||
.map((t) => ({
|
.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
|
// Author name rides along so the tuner's palette override can
|
||||||
|
// recolor pins consistently with that user's cursor/boxes.
|
||||||
|
name: t.createdBy,
|
||||||
x: t.world.x,
|
x: t.world.x,
|
||||||
y: t.world.y,
|
y: t.world.y,
|
||||||
color: colorForUser(t.createdBy),
|
color: colorForUser(t.createdBy),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue