feat(comments): figma-like comment pins + threads (collab-presence 0005)
Hybrid pins: the wasm draws the dot (kicadCollabSetPins rides the presence VIEW_OVERLAY, author color + white ring, drawn above selections; zero kicad-fork changes), the DOM owns interaction — - comments.ts: controller gluing the MIT kdoc_comments helpers to the editor: anchor resolution per tool IU (pins track item moves via kdoc_items observation), throttled pin snapshots, anchorAt nearest-item snap, jumpTo via new kicadCollabSetViewport; rebinds per sheet like presence. - CommentLayer.tsx: comment mode (click catcher + composer), pin hit targets over the GAL dots, thread popover (reply/edit/delete own, resolve/reopen, delete thread), panel with resolved filter + jump-to (popover centers when the pin is off-screen). Resolved pins drop figma-style. - WasmTool: controller lifecycle beside presence; live viewport feed; window.__pcbjamComments test handle (threads persist in the room ydoc). - e2e tests/web/comments.spec.ts: two-tab create → reply → resolve → panel filter → delete, passing vs real partykit; presence suites + collab units stay green; shared pointer bump (0004 model). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
parent
6bc54de92b
commit
635aa2926a
8 changed files with 997 additions and 4 deletions
|
|
@ -701,7 +701,18 @@ struct PEER
|
|||
std::vector<KIID> selection;
|
||||
};
|
||||
|
||||
// Comment pin dot (collab-presence 0005): the GAL half of the hybrid pin —
|
||||
// zero pan/zoom lag; the clickable hit target + thread popover are DOM.
|
||||
struct PIN
|
||||
{
|
||||
std::string id;
|
||||
VECTOR2D pos; // world coords (IU)
|
||||
KIGFX::COLOR4D color;
|
||||
bool resolved = false;
|
||||
};
|
||||
|
||||
std::vector<PEER> g_peers;
|
||||
std::vector<PIN> g_pins;
|
||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
||||
bool g_started = false;
|
||||
bool g_redrawScheduled = false;
|
||||
|
|
@ -835,6 +846,18 @@ void redrawOverlay()
|
|||
}
|
||||
}
|
||||
|
||||
// Comment pin dots (0005): filled circle in the author's color with a white
|
||||
// ring; resolved pins dim. Drawn last so they sit above selection outlines.
|
||||
for( const PIN& pin : g_pins )
|
||||
{
|
||||
g_overlay->SetIsStroke( true );
|
||||
g_overlay->SetIsFill( true );
|
||||
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() );
|
||||
fr->GetCanvas()->ForceRefresh();
|
||||
}
|
||||
|
|
@ -1587,6 +1610,51 @@ void schCollabSetRemote( std::string aJson )
|
|||
presence::scheduleRedraw();
|
||||
}
|
||||
|
||||
// JS → C++ (collab-presence 0005): comment pin dots (same wire as pcbnew).
|
||||
void schCollabSetPins( std::string aJson )
|
||||
{
|
||||
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||
|
||||
if( j.is_discarded() )
|
||||
return;
|
||||
|
||||
std::vector<presence::PIN> pins;
|
||||
|
||||
for( const json& p : j.value( "pins", json::array() ) )
|
||||
{
|
||||
presence::PIN pin;
|
||||
pin.id = p.value( "id", "" );
|
||||
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
||||
pin.color = presence::parseColor( p.value( "color", "" ) );
|
||||
pin.resolved = p.value( "resolved", false );
|
||||
pins.push_back( std::move( pin ) );
|
||||
}
|
||||
|
||||
presence::g_pins = std::move( pins );
|
||||
schCollabPresenceStart();
|
||||
presence::scheduleRedraw();
|
||||
}
|
||||
|
||||
// JS → C++ (0005): pan the view to a world position (comment panel "jump to pin").
|
||||
void schCollabSetViewport( double aCx, double aCy )
|
||||
{
|
||||
SCH_EDIT_FRAME* fr = schFrame();
|
||||
|
||||
if( !fr )
|
||||
return;
|
||||
|
||||
fr->CallAfter( [fr, aCx, aCy]() {
|
||||
COROUTINE<int, int> cor( [fr, aCx, aCy]( int ) -> int
|
||||
{
|
||||
fr->GetCanvas()->GetView()->SetCenter( VECTOR2D( aCx, aCy ) );
|
||||
fr->GetCanvas()->ForceRefresh();
|
||||
presence::emitViewportIfChanged();
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
}
|
||||
|
||||
// JS pull of the current viewport transform: `{cx,cy,scale,w,h}` with scale = px per
|
||||
// IU via the GAL matrix (GetScale() is the zoom, not px/IU — pcbnew 0002 lesson).
|
||||
std::string schCollabGetViewport()
|
||||
|
|
@ -1735,6 +1803,8 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
|||
// Presence (collab-presence 0003) — shared names with pcbnew's 0002 set.
|
||||
function("kicadCollabPresenceStart", &schCollabPresenceStart);
|
||||
function("kicadCollabSetRemote", &schCollabSetRemote);
|
||||
function("kicadCollabSetPins", &schCollabSetPins);
|
||||
function("kicadCollabSetViewport", &schCollabSetViewport);
|
||||
function("kicadCollabGetViewport", &schCollabGetViewport);
|
||||
function("kicadCollabGetSelection", &schCollabGetSelection);
|
||||
function("kicadCollabTestSelectFirst", &schCollabTestSelectFirst);
|
||||
|
|
|
|||
|
|
@ -45,9 +45,11 @@ std::string pcbCollabTestMoveFirst( int aDx, int aDy );
|
|||
std::string pcbCollabGetPos( std::string aId );
|
||||
bool pcbCollabTestRemoveItem( std::string aId );
|
||||
bool pcbCollabTestRotateItem( std::string aId, double aDeg );
|
||||
// Presence (collab-presence 0002) — pcbnew only until 0003 adds the sch side.
|
||||
// Presence (collab-presence 0002) + comment pins/panning (0005).
|
||||
void pcbCollabPresenceStart();
|
||||
void pcbCollabSetRemote( std::string aJson );
|
||||
void pcbCollabSetPins( std::string aJson );
|
||||
void pcbCollabSetViewport( double aCx, double aCy );
|
||||
std::string pcbCollabGetViewport();
|
||||
std::string pcbCollabGetSelection();
|
||||
std::string pcbCollabTestSelectFirst();
|
||||
|
|
@ -62,9 +64,11 @@ std::string schCollabTestMoveFirst( int aDx, int aDy );
|
|||
std::string schCollabGetPos( std::string aId );
|
||||
bool schCollabTestRemoveItem( std::string aId );
|
||||
bool schCollabTestRotateItem( std::string aId, double aDeg );
|
||||
// Presence (collab-presence 0003 — eeschema counterparts of the 0002 set).
|
||||
// Presence (collab-presence 0003 — eeschema counterparts) + pins (0005).
|
||||
void schCollabPresenceStart();
|
||||
void schCollabSetRemote( std::string aJson );
|
||||
void schCollabSetPins( std::string aJson );
|
||||
void schCollabSetViewport( double aCx, double aCy );
|
||||
std::string schCollabGetViewport();
|
||||
std::string schCollabGetSelection();
|
||||
std::string schCollabTestSelectFirst();
|
||||
|
|
@ -159,6 +163,16 @@ static void collabSetRemote( std::string aJson )
|
|||
pcbEditorActive() ? pcbCollabSetRemote( aJson ) : schCollabSetRemote( aJson );
|
||||
}
|
||||
|
||||
static void collabSetPins( std::string aJson )
|
||||
{
|
||||
pcbEditorActive() ? pcbCollabSetPins( aJson ) : schCollabSetPins( aJson );
|
||||
}
|
||||
|
||||
static void collabSetViewport( double aCx, double aCy )
|
||||
{
|
||||
pcbEditorActive() ? pcbCollabSetViewport( aCx, aCy ) : schCollabSetViewport( aCx, aCy );
|
||||
}
|
||||
|
||||
static std::string collabGetViewport()
|
||||
{
|
||||
return pcbEditorActive() ? pcbCollabGetViewport() : schCollabGetViewport();
|
||||
|
|
@ -196,9 +210,11 @@ EMSCRIPTEN_BINDINGS(kicad_editor) {
|
|||
// endpoint, field text — flow from the per-editor blocks unchanged).
|
||||
function("kicadCollabTestRemoveItem", &collabTestRemoveItem);
|
||||
function("kicadCollabTestRotateItem", &collabTestRotateItem);
|
||||
// Presence (collab-presence 0002).
|
||||
// Presence (collab-presence 0002/0003) + comment pins/panning (0005).
|
||||
function("kicadCollabPresenceStart", &collabPresenceStart);
|
||||
function("kicadCollabSetRemote", &collabSetRemote);
|
||||
function("kicadCollabSetPins", &collabSetPins);
|
||||
function("kicadCollabSetViewport", &collabSetViewport);
|
||||
function("kicadCollabGetViewport", &collabGetViewport);
|
||||
function("kicadCollabGetSelection", &collabGetSelection);
|
||||
function("kicadCollabTestSelectFirst", &collabTestSelectFirst);
|
||||
|
|
|
|||
|
|
@ -1106,7 +1106,19 @@ struct PEER
|
|||
std::vector<KIID> selection;
|
||||
};
|
||||
|
||||
// Comment pin dot (collab-presence 0005): the GAL half of the hybrid pin —
|
||||
// zero pan/zoom lag; the clickable hit target + thread popover are DOM,
|
||||
// positioned via the exported viewport transform.
|
||||
struct PIN
|
||||
{
|
||||
std::string id;
|
||||
VECTOR2D pos; // world coords (IU)
|
||||
KIGFX::COLOR4D color;
|
||||
bool resolved = false;
|
||||
};
|
||||
|
||||
std::vector<PEER> g_peers;
|
||||
std::vector<PIN> g_pins;
|
||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
||||
bool g_started = false;
|
||||
bool g_redrawScheduled = false;
|
||||
|
|
@ -1247,6 +1259,18 @@ void redrawOverlay()
|
|||
}
|
||||
}
|
||||
|
||||
// Comment pin dots (0005): filled circle in the author's color with a white
|
||||
// ring; resolved pins dim. Drawn last so they sit above selection outlines.
|
||||
for( const PIN& pin : g_pins )
|
||||
{
|
||||
g_overlay->SetIsStroke( true );
|
||||
g_overlay->SetIsFill( true );
|
||||
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() );
|
||||
// The canvas repaints on its own only with focus/input — force it, exactly as
|
||||
// the cross-probe flash does.
|
||||
|
|
@ -1660,6 +1684,54 @@ void pcbCollabSetRemote( std::string aJson )
|
|||
presence::scheduleRedraw();
|
||||
}
|
||||
|
||||
// JS → C++ (collab-presence 0005): comment pin dots — `{pins:[{id,x,y,color,
|
||||
// resolved}]}`, world IU coords resolved by the TS side from the ydoc anchors.
|
||||
// Snapshot semantics like SetRemote: cleared + fully redrawn each push.
|
||||
void pcbCollabSetPins( std::string aJson )
|
||||
{
|
||||
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||
|
||||
if( j.is_discarded() )
|
||||
return;
|
||||
|
||||
std::vector<presence::PIN> pins;
|
||||
|
||||
for( const json& p : j.value( "pins", json::array() ) )
|
||||
{
|
||||
presence::PIN pin;
|
||||
pin.id = p.value( "id", "" );
|
||||
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
||||
pin.color = presence::parseColor( p.value( "color", "" ) );
|
||||
pin.resolved = p.value( "resolved", false );
|
||||
pins.push_back( std::move( pin ) );
|
||||
}
|
||||
|
||||
presence::g_pins = std::move( pins );
|
||||
pcbCollabPresenceStart();
|
||||
presence::scheduleRedraw();
|
||||
}
|
||||
|
||||
// JS → C++ (0005): pan the view to a world position (comment panel "jump to
|
||||
// pin"). CallAfter + COROUTINE like every other view mutation from JS.
|
||||
void pcbCollabSetViewport( double aCx, double aCy )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
|
||||
if( !fr )
|
||||
return;
|
||||
|
||||
fr->CallAfter( [fr, aCx, aCy]() {
|
||||
COROUTINE<int, int> cor( [fr, aCx, aCy]( int ) -> int
|
||||
{
|
||||
fr->GetCanvas()->GetView()->SetCenter( VECTOR2D( aCx, aCy ) );
|
||||
fr->GetCanvas()->ForceRefresh();
|
||||
presence::emitViewportIfChanged();
|
||||
return 0;
|
||||
} );
|
||||
cor.Call( 0 );
|
||||
} );
|
||||
}
|
||||
|
||||
// JS pull of the current viewport transform (world↔screen mapping for the DOM layer):
|
||||
// `{cx,cy,scale,w,h}` — world center, pixels-per-IU scale, canvas size in px.
|
||||
std::string pcbCollabGetViewport()
|
||||
|
|
@ -1989,6 +2061,8 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
// with 0003 (the merged image dispatches pcb-only until then).
|
||||
function("kicadCollabPresenceStart", &pcbCollabPresenceStart);
|
||||
function("kicadCollabSetRemote", &pcbCollabSetRemote);
|
||||
function("kicadCollabSetPins", &pcbCollabSetPins);
|
||||
function("kicadCollabSetViewport", &pcbCollabSetViewport);
|
||||
function("kicadCollabGetViewport", &pcbCollabGetViewport);
|
||||
function("kicadCollabGetSelection", &pcbCollabGetSelection);
|
||||
function("kicadCollabTestSelectFirst", &pcbCollabTestSelectFirst);
|
||||
|
|
|
|||
Loading…
Reference in a new issue