feat(collab): ship the tuned presence style as per-editor defaults
Defaults picked with the PresenceTuner (2026-07-07): - pcbnew: exact-outline selection, 6px stroke @0.7, 46% fill, bottom-end chips @7.5px, cross cursor 8/3px @1.0 with chip label, 9px pins w/ 3px ring. - eeschema (eeschemaDefaultStyle): same look softened for the schematic canvas — hairline 1px outline, 14% fill, 0.5 cursor alpha. - New chipBgAlpha knob (default 0.7 = the border alpha) for label AND cursor badges, + tuner slider. - Tuner: per-tool defaults + per-tool localStorage keys; Reset returns each editor to its own shipped look; tool prop threaded from WasmTool. Verified: pure defaults (no style push) render the picked look in both editors; presence suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
parent
c8c2c5f583
commit
ef15c0d863
4 changed files with 84 additions and 48 deletions
|
|
@ -33,36 +33,40 @@ namespace pcbjam_presence {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
// Defaults = the SHIPPED look, picked with the PresenceTuner 2026-07-07.
|
||||||
struct STYLE
|
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)
|
// 5 exact item outline (pcbnew; eeschema falls back to rect)
|
||||||
int selShape = 0;
|
int selShape = 5;
|
||||||
double selStrokeWidth = 2.5; // px
|
double selStrokeWidth = 6.0; // px
|
||||||
double selStrokeAlpha = 0.9;
|
double selStrokeAlpha = 0.7;
|
||||||
double selFillAlpha = 0.0; // 0 = no fill
|
double selFillAlpha = 0.46; // 0 = no fill
|
||||||
double selPaddingPx = 4.0; // bbox inflate
|
double selPaddingPx = 4.0; // bbox inflate
|
||||||
double selCornerPx = 8.0; // bracket arm length / rounding radius
|
double selCornerPx = 8.0; // bracket arm length / rounding radius
|
||||||
|
|
||||||
// ── selection name tag ────────────────────────────────────────────────
|
// ── selection name tag ────────────────────────────────────────────────
|
||||||
bool labelShow = true;
|
bool labelShow = true;
|
||||||
double labelSizePx = 9.0;
|
double labelSizePx = 7.5;
|
||||||
bool labelChip = false; // filled background chip + white text
|
bool labelChip = true; // filled background chip + contrast text
|
||||||
int labelVPos = 0; // 0 top · 1 bottom
|
int labelVPos = 1; // 0 top · 1 bottom
|
||||||
int labelHPos = 0; // 0 start · 1 end · 2 center
|
int labelHPos = 1; // 0 start · 1 end · 2 center
|
||||||
bool labelInside = false; // inside vs outside the box
|
bool labelInside = false; // inside vs outside the box
|
||||||
double labelOffsetPx = 8.0;
|
double labelOffsetPx = 0.0;
|
||||||
|
// Chip background opacity (label AND cursor chips) — matches the border
|
||||||
|
// alpha so badges sit consistently with the selection strokes.
|
||||||
|
double chipBgAlpha = 0.7;
|
||||||
|
|
||||||
// ── remote cursor ─────────────────────────────────────────────────────
|
// ── remote cursor ─────────────────────────────────────────────────────
|
||||||
// 0 cross · 1 pointer triangle · 2 circle + dot
|
// 0 cross · 1 pointer triangle · 2 circle + dot
|
||||||
int cursorShape = 0;
|
int cursorShape = 0;
|
||||||
double cursorSizePx = 7.0;
|
double cursorSizePx = 8.0;
|
||||||
double cursorWidthPx = 2.0;
|
double cursorWidthPx = 3.0;
|
||||||
double cursorAlpha = 0.9;
|
double cursorAlpha = 1.0;
|
||||||
bool cursorLabel = true;
|
bool cursorLabel = true;
|
||||||
double cursorLabelSizePx = 10.0;
|
double cursorLabelSizePx = 10.0;
|
||||||
bool cursorLabelChip = false;
|
bool cursorLabelChip = true;
|
||||||
|
|
||||||
// ── colors ────────────────────────────────────────────────────────────
|
// ── colors ────────────────────────────────────────────────────────────
|
||||||
// fixedColor: every peer in ONE color ("" = off). palette: recolor peers
|
// fixedColor: every peer in ONE color ("" = off). palette: recolor peers
|
||||||
|
|
@ -72,14 +76,29 @@ struct STYLE
|
||||||
std::vector<std::string> palette;
|
std::vector<std::string> palette;
|
||||||
|
|
||||||
// ── comment pin dots ──────────────────────────────────────────────────
|
// ── comment pin dots ──────────────────────────────────────────────────
|
||||||
double pinRadiusPx = 7.0;
|
double pinRadiusPx = 9.0;
|
||||||
double pinRingPx = 1.5;
|
double pinRingPx = 3.0;
|
||||||
double pinRingAlpha = 0.9;
|
double pinRingAlpha = 1.0;
|
||||||
double pinFillAlpha = 1.0;
|
double pinFillAlpha = 1.0;
|
||||||
double pinResolvedAlpha = 0.3;
|
double pinResolvedAlpha = 0.3;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shipped eeschema defaults (picked with the PresenceTuner 2026-07-07): the
|
||||||
|
* schematic canvas is light and sparse, so the exact-outline highlight wears
|
||||||
|
* a hairline stroke, a subtler fill and a softer cursor than pcbnew's.
|
||||||
|
* Everything else matches the struct (= pcbnew) defaults.
|
||||||
|
*/
|
||||||
|
inline STYLE eeschemaDefaultStyle()
|
||||||
|
{
|
||||||
|
STYLE s;
|
||||||
|
s.selStrokeWidth = 1.0;
|
||||||
|
s.selFillAlpha = 0.14;
|
||||||
|
s.cursorAlpha = 0.5;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
inline KIGFX::COLOR4D parseHexColor( const std::string& aHex, const KIGFX::COLOR4D& aFallback )
|
inline KIGFX::COLOR4D parseHexColor( const std::string& aHex, const KIGFX::COLOR4D& aFallback )
|
||||||
{
|
{
|
||||||
if( aHex.size() == 7 && aHex[0] == '#' )
|
if( aHex.size() == 7 && aHex[0] == '#' )
|
||||||
|
|
@ -110,6 +129,7 @@ inline void patchStyle( STYLE& aStyle, const json& j )
|
||||||
aStyle.labelHPos = j.value( "labelHPos", aStyle.labelHPos );
|
aStyle.labelHPos = j.value( "labelHPos", aStyle.labelHPos );
|
||||||
aStyle.labelInside = j.value( "labelInside", aStyle.labelInside );
|
aStyle.labelInside = j.value( "labelInside", aStyle.labelInside );
|
||||||
aStyle.labelOffsetPx = j.value( "labelOffsetPx", aStyle.labelOffsetPx );
|
aStyle.labelOffsetPx = j.value( "labelOffsetPx", aStyle.labelOffsetPx );
|
||||||
|
aStyle.chipBgAlpha = j.value( "chipBgAlpha", aStyle.chipBgAlpha );
|
||||||
|
|
||||||
aStyle.cursorShape = j.value( "cursorShape", aStyle.cursorShape );
|
aStyle.cursorShape = j.value( "cursorShape", aStyle.cursorShape );
|
||||||
aStyle.cursorSizePx = j.value( "cursorSizePx", aStyle.cursorSizePx );
|
aStyle.cursorSizePx = j.value( "cursorSizePx", aStyle.cursorSizePx );
|
||||||
|
|
@ -251,7 +271,7 @@ inline void drawLabel( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv, c
|
||||||
double padX = 3 * aPx, padY = 2 * 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( aS.chipBgAlpha ) );
|
||||||
aOv->Rectangle( VECTOR2D( x - padX, y - padY ),
|
aOv->Rectangle( VECTOR2D( x - padX, y - padY ),
|
||||||
VECTOR2D( x + w + padX, y + h + padY ) );
|
VECTOR2D( x + w + padX, y + h + padY ) );
|
||||||
aOv->SetIsStroke( true );
|
aOv->SetIsStroke( true );
|
||||||
|
|
@ -423,7 +443,7 @@ inline void drawCursor( KIGFX::VIEW_OVERLAY* aOv, KIGFX::VIEW_OVERLAY* aTextOv,
|
||||||
double padX = 3 * aPx, padY = 2 * 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( aS.chipBgAlpha ) );
|
||||||
aOv->Rectangle( at + VECTOR2D( -padX, -padY ),
|
aOv->Rectangle( at + VECTOR2D( -padX, -padY ),
|
||||||
at + VECTOR2D( w + padX, h + padY ) );
|
at + VECTOR2D( w + padX, h + padY ) );
|
||||||
aOv->SetIsStroke( true );
|
aOv->SetIsStroke( true );
|
||||||
|
|
|
||||||
|
|
@ -718,7 +718,8 @@ 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)
|
// Every visual knob (shapes, widths, alphas, label placement, color overrides)
|
||||||
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
|
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
|
||||||
pcbjam_presence::STYLE g_style;
|
// eeschema ships its own defaults (hairline outline, subtler fill/cursor).
|
||||||
|
pcbjam_presence::STYLE g_style = pcbjam_presence::eeschemaDefaultStyle();
|
||||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
||||||
// Labels render from their own overlay at the nearest depth (chip rects
|
// Labels render from their own overlay at the nearest depth (chip rects
|
||||||
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
|
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
|
||||||
|
|
|
||||||
|
|
@ -31,47 +31,61 @@ export function hasTunerBridge(mod: unknown): mod is TunerModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mirror of collab_presence_style.h STYLE — defaults MUST match the C++. */
|
/** Mirror of collab_presence_style.h STYLE — defaults MUST match the C++
|
||||||
|
* (= the shipped look picked with this tuner, 2026-07-07). */
|
||||||
const DEFAULT_STYLE = {
|
const DEFAULT_STYLE = {
|
||||||
selShape: 0,
|
selShape: 5,
|
||||||
selStrokeWidth: 2.5,
|
selStrokeWidth: 6,
|
||||||
selStrokeAlpha: 0.9,
|
selStrokeAlpha: 0.7,
|
||||||
selFillAlpha: 0,
|
selFillAlpha: 0.46,
|
||||||
selPaddingPx: 4,
|
selPaddingPx: 4,
|
||||||
selCornerPx: 8,
|
selCornerPx: 8,
|
||||||
labelShow: true,
|
labelShow: true,
|
||||||
labelSizePx: 9,
|
labelSizePx: 7.5,
|
||||||
labelChip: false,
|
labelChip: true,
|
||||||
labelVPos: 0,
|
labelVPos: 1,
|
||||||
labelHPos: 0,
|
labelHPos: 1,
|
||||||
labelInside: false,
|
labelInside: false,
|
||||||
labelOffsetPx: 8,
|
labelOffsetPx: 0,
|
||||||
|
chipBgAlpha: 0.7,
|
||||||
cursorShape: 0,
|
cursorShape: 0,
|
||||||
cursorSizePx: 7,
|
cursorSizePx: 8,
|
||||||
cursorWidthPx: 2,
|
cursorWidthPx: 3,
|
||||||
cursorAlpha: 0.9,
|
cursorAlpha: 1,
|
||||||
cursorLabel: true,
|
cursorLabel: true,
|
||||||
cursorLabelSizePx: 10,
|
cursorLabelSizePx: 10,
|
||||||
cursorLabelChip: false,
|
cursorLabelChip: true,
|
||||||
fixedColor: "",
|
fixedColor: "",
|
||||||
palette: [] as string[],
|
palette: [] as string[],
|
||||||
pinRadiusPx: 7,
|
pinRadiusPx: 9,
|
||||||
pinRingPx: 1.5,
|
pinRingPx: 3,
|
||||||
pinRingAlpha: 0.9,
|
pinRingAlpha: 1,
|
||||||
pinFillAlpha: 1,
|
pinFillAlpha: 1,
|
||||||
pinResolvedAlpha: 0.3,
|
pinResolvedAlpha: 0.3,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Style = typeof DEFAULT_STYLE;
|
type Style = typeof DEFAULT_STYLE;
|
||||||
|
|
||||||
const STORE_KEY = "pcbjam:presence-style";
|
/** eeschema ships softer defaults (hairline outline, subtler fill/cursor) —
|
||||||
|
* MUST match collab_presence_style.h eeschemaDefaultStyle(). */
|
||||||
|
const EESCHEMA_OVERRIDES: Partial<Style> = {
|
||||||
|
selStrokeWidth: 1,
|
||||||
|
selFillAlpha: 0.14,
|
||||||
|
cursorAlpha: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
function loadStored(): Style {
|
function defaultsFor(tool: string): Style {
|
||||||
|
return tool === "eeschema" ? { ...DEFAULT_STYLE, ...EESCHEMA_OVERRIDES } : { ...DEFAULT_STYLE };
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeKey = (tool: string) => `pcbjam:presence-style:${tool}`;
|
||||||
|
|
||||||
|
function loadStored(tool: string): Style {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORE_KEY);
|
const raw = localStorage.getItem(storeKey(tool));
|
||||||
return raw ? { ...DEFAULT_STYLE, ...JSON.parse(raw) } : { ...DEFAULT_STYLE };
|
return raw ? { ...defaultsFor(tool), ...JSON.parse(raw) } : defaultsFor(tool);
|
||||||
} catch {
|
} catch {
|
||||||
return { ...DEFAULT_STYLE };
|
return defaultsFor(tool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,9 +109,9 @@ const PALETTE_PRESETS: Record<string, readonly string[]> = {
|
||||||
"okabe-ito": ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"],
|
"okabe-ito": ["#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
export function PresenceTuner({ mod, tool }: { mod: TunerModule; tool: string }) {
|
||||||
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(tool));
|
||||||
const [demo, setDemo] = React.useState(false);
|
const [demo, setDemo] = React.useState(false);
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
|
@ -105,11 +119,11 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
mod.kicadCollabSetStyle(JSON.stringify(style));
|
mod.kicadCollabSetStyle(JSON.stringify(style));
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORE_KEY, JSON.stringify(style));
|
localStorage.setItem(storeKey(tool), JSON.stringify(style));
|
||||||
} catch {
|
} catch {
|
||||||
/* private mode */
|
/* private mode */
|
||||||
}
|
}
|
||||||
}, [mod, style]);
|
}, [mod, tool, style]);
|
||||||
|
|
||||||
const set = <K extends keyof Style>(k: K, v: Style[K]) =>
|
const set = <K extends keyof Style>(k: K, v: Style[K]) =>
|
||||||
setStyle((s) => ({ ...s, [k]: v }));
|
setStyle((s) => ({ ...s, [k]: v }));
|
||||||
|
|
@ -241,8 +255,8 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStyle({ ...DEFAULT_STYLE });
|
setStyle(defaultsFor(tool));
|
||||||
localStorage.removeItem(STORE_KEY);
|
localStorage.removeItem(storeKey(tool));
|
||||||
}}
|
}}
|
||||||
className="rounded px-2 py-1 text-[11px] ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
className="rounded px-2 py-1 text-[11px] ring-1 ring-inset ring-white/25 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
|
|
@ -264,6 +278,7 @@ export function PresenceTuner({ mod }: { mod: TunerModule }) {
|
||||||
<Check label="show" v={style.labelShow} onChange={(v) => set("labelShow", v)} />
|
<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)} />
|
<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)} />
|
<Check label="chip background" v={style.labelChip} onChange={(v) => set("labelChip", v)} />
|
||||||
|
<Range label="chip α" v={style.chipBgAlpha} min={0.2} max={1} step={0.05} onChange={(v) => set("chipBgAlpha", v)} />
|
||||||
<Select label="v-pos" value={style.labelVPos} options={VPOS} onChange={(v) => set("labelVPos", 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)} />
|
<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)} />
|
<Check label="inside box" v={style.labelInside} onChange={(v) => set("labelInside", v)} />
|
||||||
|
|
|
||||||
|
|
@ -1184,7 +1184,7 @@ export function WasmTool({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}
|
{/* DEV: presence style tuner (VITE_PRESENCE_TUNER=1). */}
|
||||||
{ready && tunerMod && <PresenceTuner mod={tunerMod} />}
|
{ready && tunerMod && <PresenceTuner mod={tunerMod} tool={tool} />}
|
||||||
|
|
||||||
{/* 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. */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue