refactor(collab): dedup embind collab/presence layer into shared headers (collab_common.h + collab_presence_core.h)
The eeschema/pcbnew binding TUs had ~1000 lines of copy-pasted collab code. Factored into two header-only shared files (zero build-script changes — the collab_presence_style.h precedent): - collab_common.h (pcbjam_collab): toUtf8, runOnFiber (the CallAfter+COROUTINE fiber idiom — was ~25 inline copies), the window.kicadCollab wire emitters (onDelta/onItems/onCursor/onSelection/onViewport), frame-generic undo test hooks. - collab_presence_core.h (pcbjam_presence::CORE): PEER/PIN + all presence state and machinery (start/canvas binds/lock query, setRemote/setPins/ setStyle, selection check + dedupe, overlay redraw loop, viewport push/pull, releaseSelection, locks probe), written against the EDA_DRAW_FRAME + SELECTION_TOOL base classes. Per-editor hooks: frame, selectionTool, selectionEmitPayload, resolveItem, drawPeerShapes. One CORE instance per TU (anonymous-namespace presenceCore()) so the merged image keeps per-editor state separation. - NEW per-editor resolveXsel(frame, peer): ONE cross-app resolver shared by the ghost render AND kicadCollabTestGetCrossMapped — the mapping loop was duplicated within each TU, letting the test probe drift from the pixels. Deliberately NOT factored: the Yjs differ/apply halves (itemToJson/makeItem/ flushDiff/doApply*) — structurally parallel but the bodies encode per-editor sync semantics and editor-specific asyncify devirtualization workarounds that must stay visible. kicadOpenFile/kicadCollabOnSave keep the existing KICAD_MERGED_EMBIND mechanism. TestClearSelection stays editor-typed (ClearSelection is not on the SELECTION_TOOL base). eeschema_embind 2203→1721 lines, pcbnew_embind 2518→1993. JS-facing names, signatures and the kicad_editor_embind.cpp dispatcher are unchanged. Verified: kicad_editor image builds clean; tests/kicad presence+locks 18/18 (incl. ghost-render pixel compares), collab+ysync-repros 31 passed/2 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013u9h8fkQktH7KRECaHJmUG
This commit is contained in:
parent
d44ba9bdfb
commit
855137c650
4 changed files with 1067 additions and 1346 deletions
120
wasm/bindings/collab_common.h
Normal file
120
wasm/bindings/collab_common.h
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Shared plumbing for the per-editor collab binding TUs (eeschema_embind.cpp,
|
||||||
|
* pcbnew_embind.cpp) — the frame-type-free half of the bridge: string/JSON
|
||||||
|
* wire emitters to window.kicadCollab, the CallAfter+COROUTINE fiber idiom,
|
||||||
|
* and the frame-generic test hooks. Header-only (the collab_presence_style.h
|
||||||
|
* pattern), so the build script needs no extra objects and the merged
|
||||||
|
* kicad_editor image links it without ODR issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <emscripten.h>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <wx/event.h>
|
||||||
|
#include <wx/string.h>
|
||||||
|
#include <eda_base_frame.h>
|
||||||
|
#include <tool/actions.h>
|
||||||
|
#include <tool/coroutine.h>
|
||||||
|
#include <tool/tool_manager.h>
|
||||||
|
|
||||||
|
namespace pcbjam_collab {
|
||||||
|
|
||||||
|
inline std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str() ); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a body on the editor's main loop AND on a libcontext fiber stack — the
|
||||||
|
* exact context native tool edits run in. Embind ccalls / bare CallAfter
|
||||||
|
* stacks mis-dispatch asyncify-instrumented virtual calls (invoke_* through a
|
||||||
|
* stale table type traps, or silently no-ops); commits, GAL overlay work and
|
||||||
|
* the s-expr formatters must therefore run through this. CallAfter queues
|
||||||
|
* onto the app's pending-event list (drained every frame by the wasm main
|
||||||
|
* loop, src/wasm/evtloop.cpp); COROUTINE::Call moves the body to the fiber.
|
||||||
|
*/
|
||||||
|
inline void runOnFiber( wxEvtHandler* aHandler, std::function<void()> aBody )
|
||||||
|
{
|
||||||
|
aHandler->CallAfter( [aBody]() {
|
||||||
|
COROUTINE<int, int> cor( [&aBody]( int ) -> int
|
||||||
|
{
|
||||||
|
aBody();
|
||||||
|
return 0;
|
||||||
|
} );
|
||||||
|
cor.Call( 0 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── C++ → JS wire emitters (no-ops without a JS listener) ───────────────────
|
||||||
|
|
||||||
|
/** Legacy scalar delta wire: window.kicadCollab.onDelta. */
|
||||||
|
inline void emitDelta( const nlohmann::json& aDelta )
|
||||||
|
{
|
||||||
|
std::string s = aDelta.dump();
|
||||||
|
EM_ASM( {
|
||||||
|
if( window.kicadCollab && window.kicadCollab.onDelta )
|
||||||
|
window.kicadCollab.onDelta( UTF8ToString( $0 ) );
|
||||||
|
}, s.c_str() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** v2 per-item s-expr blob wire (ysync 0008): window.kicadCollab.onItems. */
|
||||||
|
inline void emitItemsWire( const nlohmann::json& aWire )
|
||||||
|
{
|
||||||
|
std::string s = aWire.dump();
|
||||||
|
EM_ASM( {
|
||||||
|
if( window.kicadCollab && window.kicadCollab.onItems )
|
||||||
|
window.kicadCollab.onItems( UTF8ToString( $0 ) );
|
||||||
|
}, s.c_str() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local cursor position (presence): window.kicadCollab.onCursor. */
|
||||||
|
inline void emitCursor( double aX, double aY, bool aActive )
|
||||||
|
{
|
||||||
|
EM_ASM( {
|
||||||
|
if( window.kicadCollab && window.kicadCollab.onCursor )
|
||||||
|
window.kicadCollab.onCursor( $0, $1, $2 );
|
||||||
|
}, aX, aY, aActive ? 1 : 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local selection payload (presence): window.kicadCollab.onSelection. */
|
||||||
|
inline void emitSelection( const std::string& aJson )
|
||||||
|
{
|
||||||
|
EM_ASM( {
|
||||||
|
if( window.kicadCollab && window.kicadCollab.onSelection )
|
||||||
|
window.kicadCollab.onSelection( UTF8ToString( $0 ) );
|
||||||
|
}, aJson.c_str() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Viewport transform for the DOM layers (0005): window.kicadCollab.onViewport. */
|
||||||
|
inline void emitViewport( double aCx, double aCy, double aPxPerIu, int aW, int aH )
|
||||||
|
{
|
||||||
|
EM_ASM( {
|
||||||
|
if( window.kicadCollab && window.kicadCollab.onViewport )
|
||||||
|
window.kicadCollab.onViewport( $0, $1, $2, $3, $4 );
|
||||||
|
}, aCx, aCy, aPxPerIu, aW, aH );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── frame-generic test hooks (ysync miss 09) ────────────────────────────────
|
||||||
|
|
||||||
|
/** Run Edit>Undo exactly like the UI would (main-loop + fiber stack) —
|
||||||
|
* exercises the local-ops-only undo policy and the stale-picker UUID guard. */
|
||||||
|
inline bool testUndo( EDA_BASE_FRAME* aFrame )
|
||||||
|
{
|
||||||
|
if( !aFrame )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
runOnFiber( aFrame, [aFrame]() { aFrame->GetToolManager()->RunAction( ACTIONS::undo ); } );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Local undo stack depth — remote applies must not grow it (miss 09). */
|
||||||
|
inline int testUndoDepth( EDA_BASE_FRAME* aFrame )
|
||||||
|
{
|
||||||
|
return aFrame ? aFrame->GetUndoCommandCount() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pcbjam_collab
|
||||||
|
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
612
wasm/bindings/collab_presence_core.h
Normal file
612
wasm/bindings/collab_presence_core.h
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
/*
|
||||||
|
* Collab presence core (collab-presence 0002/0003/0005/0006/0007) — the
|
||||||
|
* editor-agnostic half of the presence layer, shared by the pcbnew and
|
||||||
|
* eeschema binding TUs so the two implementations can't drift.
|
||||||
|
*
|
||||||
|
* Everything here talks only to the shared KiCad base classes
|
||||||
|
* (EDA_DRAW_FRAME, SELECTION_TOOL, KIGFX::VIEW) — the small per-editor
|
||||||
|
* remainder (how to resolve a KIID, how to draw one peer's selection/xsel,
|
||||||
|
* what the selection emit payload carries) plugs in through CORE's hooks.
|
||||||
|
* Each binding TU owns ONE CORE instance in its anonymous namespace, so the
|
||||||
|
* merged kicad_editor image keeps today's per-editor state separation.
|
||||||
|
*
|
||||||
|
* The drawing itself (shapes, labels, cursors, pins, style knobs) stays in
|
||||||
|
* collab_presence_style.h; this header owns the state + event/scheduling
|
||||||
|
* machinery around it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <wx/event.h>
|
||||||
|
#include <wx/string.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <class_draw_panel_gal.h>
|
||||||
|
#include <eda_draw_frame.h>
|
||||||
|
#include <eda_item.h>
|
||||||
|
#include <kiid.h>
|
||||||
|
#include <pcbjam_remote_lock.h>
|
||||||
|
#include <tool/selection_tool.h>
|
||||||
|
#include <tool/tool_manager.h>
|
||||||
|
#include <view/view.h>
|
||||||
|
#include <view/view_overlay.h>
|
||||||
|
|
||||||
|
#include "collab_common.h"
|
||||||
|
#include "collab_presence_style.h"
|
||||||
|
|
||||||
|
namespace pcbjam_presence {
|
||||||
|
|
||||||
|
struct PEER
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
KIGFX::COLOR4D color;
|
||||||
|
bool hasCursor = false;
|
||||||
|
VECTOR2D cursor; // world coords (IU)
|
||||||
|
std::vector<KIID> selection;
|
||||||
|
// Cross-app selection (0006): the peer's selection in the OTHER editor,
|
||||||
|
// as SYMBOL uuids (pcbnew footprint paths are stripped to their tail by
|
||||||
|
// the TS side). Ghost-rendered by the per-editor drawPeerShapes hook.
|
||||||
|
std::vector<KIID> xsel;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
std::string name; // author (palette-override rehash key)
|
||||||
|
VECTOR2D pos; // world coords (IU)
|
||||||
|
KIGFX::COLOR4D color;
|
||||||
|
bool resolved = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline long long nowMs()
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch() )
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline KIGFX::COLOR4D parsePeerColor( const std::string& aHex )
|
||||||
|
{
|
||||||
|
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, 0.9 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return KIGFX::COLOR4D( 0.23, 0.51, 0.96, 0.9 ); // palette blue fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-editor presence state + machinery. One instance per binding TU; the
|
||||||
|
* hooks below are set once at construction (see presenceCore() in each TU).
|
||||||
|
*/
|
||||||
|
struct CORE
|
||||||
|
{
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
// ── per-editor hooks ──────────────────────────────────────────────────
|
||||||
|
/** The live editor frame, or nullptr (the usual dynamic_cast probe). */
|
||||||
|
std::function<EDA_DRAW_FRAME*()> frame;
|
||||||
|
|
||||||
|
/** The editor's selection tool (GetTool<> needs the concrete type). */
|
||||||
|
std::function<SELECTION_TOOL*( EDA_DRAW_FRAME* )> selectionTool;
|
||||||
|
|
||||||
|
/** What checkSelection hands to JS: a bare uuid array (eeschema) or the
|
||||||
|
* 0006 {uuids, fpPaths} payload (pcbnew). */
|
||||||
|
std::function<json( EDA_DRAW_FRAME* )> selectionEmitPayload;
|
||||||
|
|
||||||
|
/** Resolve a KIID to a live item, or nullptr (releaseSelection). */
|
||||||
|
std::function<EDA_ITEM*( EDA_DRAW_FRAME*, const KIID& )> resolveItem;
|
||||||
|
|
||||||
|
/** Draw ONE peer's selection boxes + cross-app ghosts into the overlays
|
||||||
|
* (item resolution and exact-outline rendering are editor-specific).
|
||||||
|
* Cursors and pins are drawn by the shared redraw loop. */
|
||||||
|
std::function<void( CORE&, EDA_DRAW_FRAME*, const PEER&, const KIGFX::COLOR4D&, double )>
|
||||||
|
drawPeerShapes;
|
||||||
|
|
||||||
|
// ── state ─────────────────────────────────────────────────────────────
|
||||||
|
std::vector<PEER> peers;
|
||||||
|
std::vector<PIN> pins;
|
||||||
|
// Remote soft-locks (0007): uuid → holding peer's display name, derived
|
||||||
|
// by the TS side from ALL other clients' live selections (own user's
|
||||||
|
// other tabs included). Consulted by the fork's PCBJAM_REMOTE_LOCK query
|
||||||
|
// from the selection/move tools. Ephemeral — replaced on every setRemote.
|
||||||
|
std::map<KIID, std::string> locks;
|
||||||
|
// Every visual knob — see collab_presence_style.h; live-patched by
|
||||||
|
// kicadCollabSetStyle (tuner). Editors seed their own defaults.
|
||||||
|
STYLE style;
|
||||||
|
|
||||||
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> overlay;
|
||||||
|
// Labels render from their own overlay at the nearest depth (chip rects
|
||||||
|
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
|
||||||
|
std::shared_ptr<KIGFX::VIEW_OVERLAY> textOverlay;
|
||||||
|
|
||||||
|
bool started = false;
|
||||||
|
bool redrawScheduled = false;
|
||||||
|
bool selCheckScheduled = false;
|
||||||
|
std::string lastSelectionJson; // dedupe: emit only when the payload changed
|
||||||
|
long long lastCursorEmitMs = 0;
|
||||||
|
double lastVpScale = 0.0;
|
||||||
|
VECTOR2D lastVpCenter;
|
||||||
|
|
||||||
|
// ── local state emit (selection / cursor / viewport) ──────────────────
|
||||||
|
|
||||||
|
json selectionUuids( EDA_DRAW_FRAME* aFrame )
|
||||||
|
{
|
||||||
|
json uuids = json::array();
|
||||||
|
|
||||||
|
SELECTION_TOOL* selTool = selectionTool( aFrame );
|
||||||
|
|
||||||
|
if( !selTool )
|
||||||
|
return uuids;
|
||||||
|
|
||||||
|
for( EDA_ITEM* item : selTool->GetSelection() )
|
||||||
|
uuids.push_back( pcbjam_collab::toUtf8( item->m_Uuid.AsString() ) );
|
||||||
|
|
||||||
|
return uuids;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-settle selection emit: read the selection AFTER the triggering event
|
||||||
|
// finished (CallAfter), dedupe against the last emitted set, hand to JS.
|
||||||
|
void checkSelection()
|
||||||
|
{
|
||||||
|
selCheckScheduled = false;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::string s = selectionEmitPayload( fr ).dump();
|
||||||
|
|
||||||
|
if( s == lastSelectionJson )
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastSelectionJson = s;
|
||||||
|
pcbjam_collab::emitSelection( s );
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleSelCheck()
|
||||||
|
{
|
||||||
|
if( selCheckScheduled )
|
||||||
|
return;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
selCheckScheduled = true;
|
||||||
|
fr->CallAfter( [this]() { checkSelection(); } );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewport push (world↔screen mapping for the DOM layers, 0005). Zoom also
|
||||||
|
// invalidates the overlay's screen-constant sizes → schedule a redraw.
|
||||||
|
void emitViewportIfChanged()
|
||||||
|
{
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
||||||
|
double scale = view->GetScale(); // zoom — cheap change detector only
|
||||||
|
VECTOR2D c = view->GetCenter();
|
||||||
|
|
||||||
|
if( scale == lastVpScale && c == lastVpCenter )
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastVpScale = scale;
|
||||||
|
lastVpCenter = c;
|
||||||
|
|
||||||
|
const VECTOR2I& sz = view->GetScreenPixelSize();
|
||||||
|
// px per IU via the GAL matrix — GetScale() is the zoom, not px/IU.
|
||||||
|
pcbjam_collab::emitViewport( c.x, c.y, view->ToScreen( 1.0 ), sz.x, sz.y );
|
||||||
|
|
||||||
|
if( !peers.empty() )
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabGetViewport: `{cx,cy,scale,w,h}`, scale = px per IU via the
|
||||||
|
* GAL matrix (GetScale() is the zoom, not px/IU — pcbnew 0002 lesson). */
|
||||||
|
std::string viewportJson()
|
||||||
|
{
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
||||||
|
VECTOR2D c = view->GetCenter();
|
||||||
|
const VECTOR2I& sz = view->GetScreenPixelSize();
|
||||||
|
|
||||||
|
return json{ { "cx", c.x }, { "cy", c.y }, { "scale", view->ToScreen( 1.0 ) },
|
||||||
|
{ "w", sz.x }, { "h", sz.y } }.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabSetViewport (0005): pan to a world position (comment panel
|
||||||
|
* "jump to pin"). Fiber like every other view mutation from JS. */
|
||||||
|
void panTo( double aCx, double aCy )
|
||||||
|
{
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [this, fr, aCx, aCy]() {
|
||||||
|
fr->GetCanvas()->GetView()->SetCenter( VECTOR2D( aCx, aCy ) );
|
||||||
|
fr->GetCanvas()->ForceRefresh();
|
||||||
|
emitViewportIfChanged();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
void onMotion( wxMouseEvent& aEvt )
|
||||||
|
{
|
||||||
|
aEvt.Skip();
|
||||||
|
|
||||||
|
long long now = nowMs();
|
||||||
|
|
||||||
|
if( now - lastCursorEmitMs < 50 ) // ≤20 emits/s, event-driven (no timers)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastCursorEmitMs = now;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Screen→world via the non-virtual VIEW::ToWorld (the virtual
|
||||||
|
// VIEW_CONTROLS::GetMousePosition is an asyncify dispatch risk here).
|
||||||
|
wxPoint p = aEvt.GetPosition();
|
||||||
|
VECTOR2D world = fr->GetCanvas()->GetView()->ToWorld( VECTOR2D( p.x, p.y ), true );
|
||||||
|
|
||||||
|
pcbjam_collab::emitCursor( world.x, world.y, true );
|
||||||
|
emitViewportIfChanged(); // catches drag-pan while moving
|
||||||
|
}
|
||||||
|
|
||||||
|
void onLeave( wxMouseEvent& aEvt )
|
||||||
|
{
|
||||||
|
aEvt.Skip();
|
||||||
|
pcbjam_collab::emitCursor( 0, 0, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── remote render ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Repaint the remote-peers overlay. Runs in CallAfter + COROUTINE: the
|
||||||
|
// first MakeOverlay() view->Add and the items' virtual ViewBBox() need the
|
||||||
|
// fiber stack (asyncify virtual dispatch — same constraint as the apply).
|
||||||
|
void redrawOverlay()
|
||||||
|
{
|
||||||
|
redrawScheduled = false;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
||||||
|
|
||||||
|
if( !overlay )
|
||||||
|
{
|
||||||
|
overlay = view->MakeOverlay();
|
||||||
|
// Shapes sit one depth unit BELOW the text overlay (fork
|
||||||
|
// SetDepthOffset) so labels always render on top of chips/boxes.
|
||||||
|
overlay->SetDepthOffset( PRESENCE_SHAPES_DEPTH_OFFSET );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !textOverlay )
|
||||||
|
textOverlay = makePresenceTextOverlay( view );
|
||||||
|
|
||||||
|
overlay->Clear();
|
||||||
|
textOverlay->Clear();
|
||||||
|
|
||||||
|
// Screen-constant sizing: px → world units, so cursors/outline widths
|
||||||
|
// don't scale with zoom. MUST go through the GAL matrix
|
||||||
|
// (ToWorld(double)) — the naive 1/GetScale() is the ZOOM factor, not
|
||||||
|
// px-per-IU, and under-sizes the drawing by ~7 orders of magnitude.
|
||||||
|
double px = view->ToWorld( 1.0 );
|
||||||
|
|
||||||
|
for( const PEER& peer : peers )
|
||||||
|
{
|
||||||
|
KIGFX::COLOR4D color = peerColor( style, peer.name, peer.color );
|
||||||
|
|
||||||
|
// Selection boxes + cross-app ghosts: editor-specific resolution.
|
||||||
|
drawPeerShapes( *this, fr, peer, color, px );
|
||||||
|
|
||||||
|
if( peer.hasCursor )
|
||||||
|
drawCursor( overlay.get(), textOverlay.get(), peer.cursor, peer.name, color,
|
||||||
|
px, style );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment pin dots (0005), drawn last so they sit above selection outlines.
|
||||||
|
for( const PIN& pin : pins )
|
||||||
|
{
|
||||||
|
KIGFX::COLOR4D color = peerColor( style, pin.name, pin.color );
|
||||||
|
drawPin( overlay.get(), pin.pos, color, pin.resolved, px, style );
|
||||||
|
}
|
||||||
|
|
||||||
|
view->Update( overlay.get() );
|
||||||
|
view->Update( textOverlay.get() );
|
||||||
|
// The canvas repaints on its own only with focus/input — force it,
|
||||||
|
// exactly as the cross-probe flash does.
|
||||||
|
fr->GetCanvas()->ForceRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleRedraw()
|
||||||
|
{
|
||||||
|
if( redrawScheduled )
|
||||||
|
return;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
redrawScheduled = true;
|
||||||
|
pcbjam_collab::runOnFiber( fr, [this]() { redrawOverlay(); } );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JS entry-point bodies ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** kicadCollabPresenceStart: install the input hooks on the GAL canvas
|
||||||
|
* (idempotent — the canvas is the same window for the whole session) and
|
||||||
|
* the fork's remote soft-lock query (0007). */
|
||||||
|
void start()
|
||||||
|
{
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr || started )
|
||||||
|
return;
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
// Remote soft-locks (0007): let the selection/move tools consult the
|
||||||
|
// peers' live selections through the fork's process-global query.
|
||||||
|
PCBJAM_REMOTE_LOCK::SetQuery(
|
||||||
|
[this]( const KIID& aId, wxString* aHolder ) -> bool
|
||||||
|
{
|
||||||
|
auto it = locks.find( aId );
|
||||||
|
|
||||||
|
if( it == locks.end() )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if( aHolder )
|
||||||
|
*aHolder = wxString::FromUTF8( it->second.c_str() );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} );
|
||||||
|
|
||||||
|
wxWindow* canvas = fr->GetCanvas();
|
||||||
|
|
||||||
|
canvas->Bind( wxEVT_MOTION, [this]( wxMouseEvent& e ) { onMotion( e ); } );
|
||||||
|
canvas->Bind( wxEVT_LEAVE_WINDOW, [this]( wxMouseEvent& e ) { onLeave( e ); } );
|
||||||
|
|
||||||
|
// Handlers Skip() so the view controls' own processing is untouched;
|
||||||
|
// selection checks run POST-event via CallAfter (the selection tool
|
||||||
|
// acts on the same event after us).
|
||||||
|
auto selAndViewport = [this]( wxEvent& e )
|
||||||
|
{
|
||||||
|
e.Skip();
|
||||||
|
scheduleSelCheck();
|
||||||
|
|
||||||
|
if( EDA_DRAW_FRAME* f = frame() )
|
||||||
|
f->CallAfter( [this]() { emitViewportIfChanged(); } );
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas->Bind( wxEVT_LEFT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
||||||
|
canvas->Bind( wxEVT_RIGHT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
||||||
|
canvas->Bind( wxEVT_KEY_UP, [selAndViewport]( wxKeyEvent& e ) { selAndViewport( e ); } );
|
||||||
|
canvas->Bind( wxEVT_MOUSEWHEEL, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabSetRemote: full remote-peers snapshot — `{peers:[{id,name,
|
||||||
|
* color,cursor:{x,y}|null,selection:[uuid],xsel:[uuid]}],locks:[{uuid,
|
||||||
|
* name}]}`, trivially derived from awareness.getStates() and idempotent
|
||||||
|
* (the overlay is cleared + fully redrawn). Empty peers clears it. */
|
||||||
|
void setRemote( const std::string& aJson )
|
||||||
|
{
|
||||||
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<PEER> parsed;
|
||||||
|
|
||||||
|
for( const json& p : j.value( "peers", json::array() ) )
|
||||||
|
{
|
||||||
|
PEER peer;
|
||||||
|
peer.name = p.value( "name", "" );
|
||||||
|
peer.color = parsePeerColor( p.value( "color", "" ) );
|
||||||
|
|
||||||
|
if( p.contains( "cursor" ) && p["cursor"].is_object() )
|
||||||
|
{
|
||||||
|
peer.hasCursor = true;
|
||||||
|
peer.cursor = VECTOR2D( p["cursor"].value( "x", 0.0 ), p["cursor"].value( "y", 0.0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
for( const json& u : p.value( "selection", json::array() ) )
|
||||||
|
{
|
||||||
|
if( u.is_string() )
|
||||||
|
peer.selection.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-app selection (0006): symbol uuids from the OTHER editor.
|
||||||
|
for( const json& u : p.value( "xsel", json::array() ) )
|
||||||
|
{
|
||||||
|
if( u.is_string() )
|
||||||
|
peer.xsel.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.push_back( std::move( peer ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote soft-locks (0007): every other client's held uuids with the
|
||||||
|
// holder's display name for the infobar.
|
||||||
|
std::map<KIID, std::string> parsedLocks;
|
||||||
|
|
||||||
|
for( const json& l : j.value( "locks", json::array() ) )
|
||||||
|
{
|
||||||
|
std::string uuid = l.is_object() ? l.value( "uuid", "" ) : "";
|
||||||
|
|
||||||
|
if( !uuid.empty() )
|
||||||
|
parsedLocks[ KIID( wxString::FromUTF8( uuid.c_str() ) ) ] = l.value( "name", "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
peers = std::move( parsed );
|
||||||
|
locks = std::move( parsedLocks );
|
||||||
|
start();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabSetPins (0005): comment pin dots — `{pins:[{id,name,x,y,
|
||||||
|
* color,resolved}]}`, world IU coords resolved by the TS side from the
|
||||||
|
* ydoc anchors. Snapshot semantics like setRemote. */
|
||||||
|
void setPins( const std::string& aJson )
|
||||||
|
{
|
||||||
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<PIN> parsed;
|
||||||
|
|
||||||
|
for( const json& p : j.value( "pins", json::array() ) )
|
||||||
|
{
|
||||||
|
PIN pin;
|
||||||
|
pin.id = p.value( "id", "" );
|
||||||
|
pin.name = p.value( "name", "" );
|
||||||
|
pin.pos = VECTOR2D( p.value( "x", 0.0 ), p.value( "y", 0.0 ) );
|
||||||
|
pin.color = parsePeerColor( p.value( "color", "" ) );
|
||||||
|
pin.resolved = p.value( "resolved", false );
|
||||||
|
parsed.push_back( std::move( pin ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
pins = std::move( parsed );
|
||||||
|
start();
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabSetStyle (presence tuner): live-patch the overlay STYLE
|
||||||
|
* (partial JSON — see collab_presence_style.h) and repaint. */
|
||||||
|
void setStyle( const std::string& aJson )
|
||||||
|
{
|
||||||
|
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
patchStyle( style, j );
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** kicadCollabReleaseSelection (0007): the local client LOST the selection
|
||||||
|
* tiebreak — release the contested items (only those) from the live
|
||||||
|
* selection. If an interactive tool (move/drag) holds them, cancel it
|
||||||
|
* first (ESC semantics — the preview reverts); a bare cancel is NOT sent
|
||||||
|
* when idle, since ESC on the base selection tool would clear the whole
|
||||||
|
* selection. Ends with a forced selection re-emit (programmatic changes
|
||||||
|
* close no canvas event). */
|
||||||
|
void releaseSelection( const std::string& aUuidsJson, const std::string& aHolder )
|
||||||
|
{
|
||||||
|
json j = json::parse( aUuidsJson, nullptr, /*allow_exceptions*/ false );
|
||||||
|
|
||||||
|
if( j.is_discarded() || !j.is_array() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<KIID> ids;
|
||||||
|
|
||||||
|
for( const json& u : j )
|
||||||
|
{
|
||||||
|
if( u.is_string() )
|
||||||
|
ids.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( ids.empty() )
|
||||||
|
return;
|
||||||
|
|
||||||
|
wxString holder = wxString::FromUTF8( aHolder.c_str() );
|
||||||
|
|
||||||
|
fr->CallAfter( [this, fr, ids, holder]() {
|
||||||
|
if( !fr->ToolStackIsEmpty() )
|
||||||
|
fr->GetToolManager()->RunAction( ACTIONS::cancelInteractive );
|
||||||
|
|
||||||
|
SELECTION_TOOL* st = selectionTool( fr );
|
||||||
|
|
||||||
|
if( !st )
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool released = false;
|
||||||
|
|
||||||
|
for( const KIID& id : ids )
|
||||||
|
{
|
||||||
|
EDA_ITEM* item = resolveItem( fr, id );
|
||||||
|
|
||||||
|
if( item && item->IsSelected() )
|
||||||
|
{
|
||||||
|
st->RemoveItemFromSel( item );
|
||||||
|
released = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( released )
|
||||||
|
{
|
||||||
|
fr->ShowInfoBarWarning( wxString::Format( _( "%s is editing this — released from "
|
||||||
|
"your selection." ),
|
||||||
|
holder ),
|
||||||
|
true );
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleSelCheck();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test helper: REALLY select an item through the selection tool, then run
|
||||||
|
* the presence check (programmatic selects close no canvas event). */
|
||||||
|
void selectItem( EDA_ITEM* aItem )
|
||||||
|
{
|
||||||
|
EDA_DRAW_FRAME* fr = frame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return;
|
||||||
|
|
||||||
|
fr->CallAfter( [this, fr, aItem]() {
|
||||||
|
if( SELECTION_TOOL* st = selectionTool( fr ) )
|
||||||
|
{
|
||||||
|
st->AddItemToSel( aItem );
|
||||||
|
scheduleSelCheck();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test probe (0007): the current remote soft-lock set as `[{uuid, name}]`. */
|
||||||
|
std::string locksJson()
|
||||||
|
{
|
||||||
|
json arr = json::array();
|
||||||
|
|
||||||
|
for( const auto& [id, name] : locks )
|
||||||
|
arr.push_back( { { "uuid", pcbjam_collab::toUtf8( id.AsString() ) }, { "name", name } } );
|
||||||
|
|
||||||
|
return arr.dump();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pcbjam_presence
|
||||||
|
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
@ -56,6 +56,8 @@
|
||||||
#include <tool/actions.h>
|
#include <tool/actions.h>
|
||||||
#include <tool/coroutine.h>
|
#include <tool/coroutine.h>
|
||||||
#include <pcbjam_remote_lock.h>
|
#include <pcbjam_remote_lock.h>
|
||||||
|
#include "collab_common.h"
|
||||||
|
#include "collab_presence_core.h"
|
||||||
#include "collab_presence_style.h"
|
#include "collab_presence_style.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -109,7 +111,7 @@ namespace {
|
||||||
// Guard so SCH_COMMIT::Push's listener callbacks during apply() aren't re-emitted.
|
// Guard so SCH_COMMIT::Push's listener callbacks during apply() aren't re-emitted.
|
||||||
bool s_applyingRemote = false;
|
bool s_applyingRemote = false;
|
||||||
|
|
||||||
std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str() ); }
|
using pcbjam_collab::toUtf8;
|
||||||
|
|
||||||
SCH_EDIT_FRAME* schFrame()
|
SCH_EDIT_FRAME* schFrame()
|
||||||
{
|
{
|
||||||
|
|
@ -309,26 +311,9 @@ json snapshotItems( SCH_EDIT_FRAME* aFrame )
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void emit( const json& aDelta )
|
// Wire emitters (legacy scalar delta + v2 items): shared, collab_common.h.
|
||||||
{
|
using pcbjam_collab::emitDelta;
|
||||||
std::string s = aDelta.dump();
|
using pcbjam_collab::emitItemsWire;
|
||||||
EM_ASM( {
|
|
||||||
if( window.kicadCollab && window.kicadCollab.onDelta )
|
|
||||||
window.kicadCollab.onDelta( UTF8ToString( $0 ) );
|
|
||||||
}, s.c_str() );
|
|
||||||
}
|
|
||||||
|
|
||||||
// v2 "items" wire emit (ysync 0008): per-item s-expr blobs instead of decomposed
|
|
||||||
// scalars. A JS runtime registers window.kicadCollab.onItems to opt in; both wires
|
|
||||||
// are emitted side by side until the scalar path is retired.
|
|
||||||
void emitItems( const json& aWire )
|
|
||||||
{
|
|
||||||
std::string s = aWire.dump();
|
|
||||||
EM_ASM( {
|
|
||||||
if( window.kicadCollab && window.kicadCollab.onItems )
|
|
||||||
window.kicadCollab.onItems( UTF8ToString( $0 ) );
|
|
||||||
}, s.c_str() );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify the standalone that the editor switched to a different sheet — a different
|
// Notify the standalone that the editor switched to a different sheet — a different
|
||||||
// .kicad_sch == a different collab room (ysync subschemas). The path is the active
|
// .kicad_sch == a different collab room (ysync subschemas). The path is the active
|
||||||
|
|
@ -522,10 +507,10 @@ void flushDiff()
|
||||||
g_baseline = std::move( cur );
|
g_baseline = std::move( cur );
|
||||||
|
|
||||||
if( !added.empty() || !changed.empty() || !removed.empty() )
|
if( !added.empty() || !changed.empty() || !removed.empty() )
|
||||||
emit( json{ { "added", added }, { "changed", changed }, { "removed", removed } } );
|
emitDelta( json{ { "added", added }, { "changed", changed }, { "removed", removed } } );
|
||||||
|
|
||||||
if( !wAdded.empty() || !wChanged.empty() || !removed.empty() )
|
if( !wAdded.empty() || !wChanged.empty() || !removed.empty() )
|
||||||
emitItems( json{ { "added", wAdded }, { "changed", wChanged }, { "removed", removed } } );
|
emitItemsWire( json{ { "added", wAdded }, { "changed", wChanged }, { "removed", removed } } );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coalesce all the listener callbacks of one commit (and any other edits in the same loop
|
// Coalesce all the listener callbacks of one commit (and any other edits in the same loop
|
||||||
|
|
@ -541,21 +526,10 @@ void scheduleFlush()
|
||||||
g_flushScheduled = true;
|
g_flushScheduled = true;
|
||||||
|
|
||||||
if( SCH_EDIT_FRAME* fr = schFrame() )
|
if( SCH_EDIT_FRAME* fr = schFrame() )
|
||||||
{
|
pcbjam_collab::runOnFiber( fr, []() { flushDiff(); } );
|
||||||
fr->CallAfter( []() {
|
|
||||||
COROUTINE<int, int> cor( []( int ) -> int
|
|
||||||
{
|
|
||||||
flushDiff();
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
flushDiff();
|
flushDiff();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// A hierarchical sheet was just created locally ("Add Sheet"): write its new child screen
|
// A hierarchical sheet was just created locally ("Add Sheet"): write its new child screen
|
||||||
// to the child .kicad_sch file and tell the standalone (window.kicadCollab.onSheetCreated),
|
// to the child .kicad_sch file and tell the standalone (window.kicadCollab.onSheetCreated),
|
||||||
|
|
@ -579,14 +553,12 @@ void scheduleSheetSave( SCH_SHEET* aSheet )
|
||||||
std::string childAbs = toUtf8( childFn.GetFullPath() );
|
std::string childAbs = toUtf8( childFn.GetFullPath() );
|
||||||
std::string uuid = toUtf8( aSheet->m_Uuid.AsString() );
|
std::string uuid = toUtf8( aSheet->m_Uuid.AsString() );
|
||||||
|
|
||||||
fr->CallAfter( [fr, childAbs, uuid]() {
|
pcbjam_collab::runOnFiber( fr, [fr, childAbs, uuid]() {
|
||||||
COROUTINE<int, int> cor( [fr, childAbs, uuid]( int ) -> int
|
|
||||||
{
|
|
||||||
KIID kid( wxString::FromUTF8( uuid.c_str() ) );
|
KIID kid( wxString::FromUTF8( uuid.c_str() ) );
|
||||||
SCH_ITEM* item = fr->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true );
|
SCH_ITEM* item = fr->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true );
|
||||||
|
|
||||||
if( !item || item->Type() != SCH_SHEET_T )
|
if( !item || item->Type() != SCH_SHEET_T )
|
||||||
return 0;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -596,16 +568,13 @@ void scheduleSheetSave( SCH_SHEET* aSheet )
|
||||||
}
|
}
|
||||||
catch( ... )
|
catch( ... )
|
||||||
{
|
{
|
||||||
return 0; // a write failure must not abort the runtime
|
return; // a write failure must not abort the runtime
|
||||||
}
|
}
|
||||||
|
|
||||||
EM_ASM( {
|
EM_ASM( {
|
||||||
if( window.kicadCollab && window.kicadCollab.onSheetCreated )
|
if( window.kicadCollab && window.kicadCollab.onSheetCreated )
|
||||||
window.kicadCollab.onSheetCreated( UTF8ToString( $0 ) );
|
window.kicadCollab.onSheetCreated( UTF8ToString( $0 ) );
|
||||||
}, childAbs.c_str() );
|
}, childAbs.c_str() );
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -686,153 +655,75 @@ SCHEMATIC* ensureBridge()
|
||||||
// ───────────────────────── collab presence (collab-presence 0003) ─────────────────────────
|
// ───────────────────────── collab presence (collab-presence 0003) ─────────────────────────
|
||||||
//
|
//
|
||||||
// eeschema port of pcbnew's presence layer (0002 — see pcbnew_embind.cpp for the full
|
// eeschema port of pcbnew's presence layer (0002 — see pcbnew_embind.cpp for the full
|
||||||
// design rationale): emit THIS tab's selection + cursor to JS, render REMOTE peers'
|
// design rationale). The state + event/scheduling machinery live in the shared
|
||||||
// cursors + selection outlines into a per-user-colored VIEW_OVERLAY. Zero kicad-fork
|
// pcbjam_presence::CORE (collab_presence_core.h); this TU supplies only the
|
||||||
// changes: wx-layer Bind() triggers on the GAL canvas + the COLLAB_LISTENER piggyback,
|
// eeschema-specific hooks: frame/tool lookup, KIID resolution via
|
||||||
// selection read post-settle from SCH_SELECTION_TOOL, KIIDs resolved via
|
// SCHEMATIC::ResolveItem, the bare-uuid selection payload, and the per-peer draw
|
||||||
// SCHEMATIC::ResolveItem. Rooms are per-sheet (warm pool), so peers publishing
|
// (cross-app ghosts gated to the CURRENT sheet). Zero kicad-fork changes: wx-layer
|
||||||
// cursor/selection in the bound room are BY CONSTRUCTION on this same sheet file —
|
// Bind() triggers on the GAL canvas + the COLLAB_LISTENER piggyback, selection read
|
||||||
// no sheet filtering is needed here; the JS side rebinds the whole presence layer on
|
// post-settle from SCH_SELECTION_TOOL. Rooms are per-sheet (warm pool), so peers
|
||||||
// sheet navigation (onSheetChanged) and clears the overlay in between.
|
// publishing cursor/selection in the bound room are BY CONSTRUCTION on this same
|
||||||
namespace presence {
|
// sheet file — no sheet filtering is needed for same-room selections; the JS side
|
||||||
|
// rebinds the whole presence layer on sheet navigation (onSheetChanged) and clears
|
||||||
|
// the overlay in between.
|
||||||
|
|
||||||
struct PEER
|
// Cross-app selection (0006): the schematic items a peer's xsel (symbol uuids
|
||||||
|
// from a pcbnew peer) resolves to on the CURRENT sheet. Unlike same-room
|
||||||
|
// selections (per-sheet rooms scope those by construction) xsel arrives
|
||||||
|
// project-wide, so only items that resolve onto the sheet THIS canvas is
|
||||||
|
// showing count — a bbox from another sheet's screen would land at meaningless
|
||||||
|
// coordinates. ONE resolver shared by the ghost render and the test probe
|
||||||
|
// (kicadCollabTestGetCrossMapped), so the assertion can't drift from the pixels.
|
||||||
|
std::vector<SCH_ITEM*> resolveXsel( SCH_EDIT_FRAME* aFrame, const pcbjam_presence::PEER& aPeer )
|
||||||
{
|
{
|
||||||
std::string name;
|
std::vector<SCH_ITEM*> items;
|
||||||
KIGFX::COLOR4D color;
|
|
||||||
bool hasCursor = false;
|
|
||||||
VECTOR2D cursor; // world coords (IU)
|
|
||||||
std::vector<KIID> selection;
|
|
||||||
// Cross-app selection (0006): SYMBOL uuids derived from a pcbnew peer's
|
|
||||||
// footprint paths (the TS side strips the KIID_PATH down to its tail).
|
|
||||||
// Ghost-rendered when the symbol resolves onto the CURRENT sheet.
|
|
||||||
std::vector<KIID> xsel;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Comment pin dot (collab-presence 0005): the GAL half of the hybrid pin —
|
for( const KIID& id : aPeer.xsel )
|
||||||
// zero pan/zoom lag; the clickable hit target + thread popover are DOM.
|
|
||||||
struct PIN
|
|
||||||
{
|
{
|
||||||
std::string id;
|
SCH_SHEET_PATH path;
|
||||||
std::string name; // author (palette-override rehash key)
|
SCH_ITEM* item = aFrame->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
||||||
VECTOR2D pos; // world coords (IU)
|
|
||||||
KIGFX::COLOR4D color;
|
if( item && path.LastScreen() == aFrame->GetScreen() )
|
||||||
bool resolved = false;
|
items.push_back( item );
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
pcbjam_presence::CORE& presenceCore()
|
||||||
|
{
|
||||||
|
static pcbjam_presence::CORE core = []()
|
||||||
|
{
|
||||||
|
pcbjam_presence::CORE c;
|
||||||
|
|
||||||
std::vector<PEER> g_peers;
|
|
||||||
std::vector<PIN> g_pins;
|
|
||||||
// Remote soft-locks (collab-presence 0007) — see pcbnew_embind.cpp.
|
|
||||||
std::map<KIID, std::string> g_locks;
|
|
||||||
// Every visual knob (shapes, widths, alphas, label placement, color overrides)
|
|
||||||
// — see collab_presence_style.h; live-patched by kicadCollabSetStyle (tuner).
|
|
||||||
// eeschema ships its own defaults (hairline outline, subtler fill/cursor).
|
// eeschema ships its own defaults (hairline outline, subtler fill/cursor).
|
||||||
pcbjam_presence::STYLE g_style = pcbjam_presence::eeschemaDefaultStyle();
|
c.style = pcbjam_presence::eeschemaDefaultStyle();
|
||||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_overlay;
|
|
||||||
// Labels render from their own overlay at the nearest depth (chip rects
|
|
||||||
// would otherwise erase same-depth text) — see PRESENCE_TEXT_OVERLAY.
|
|
||||||
std::shared_ptr<KIGFX::VIEW_OVERLAY> g_textOverlay;
|
|
||||||
bool g_started = false;
|
|
||||||
bool g_redrawScheduled = false;
|
|
||||||
bool g_selCheckScheduled = false;
|
|
||||||
std::string g_lastSelectionJson; // dedupe: emit only when the uuid set changed
|
|
||||||
long long g_lastCursorEmitMs = 0;
|
|
||||||
double g_lastVpScale = 0.0;
|
|
||||||
VECTOR2D g_lastVpCenter;
|
|
||||||
|
|
||||||
long long nowMs()
|
c.frame = []() -> EDA_DRAW_FRAME* { return schFrame(); };
|
||||||
|
|
||||||
|
c.selectionTool = []( EDA_DRAW_FRAME* fr ) -> SELECTION_TOOL*
|
||||||
{
|
{
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
return fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>();
|
||||||
std::chrono::steady_clock::now().time_since_epoch() )
|
};
|
||||||
.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
KIGFX::COLOR4D parseColor( const std::string& aHex )
|
// Selection emit = the bare uuid array (0006: eeschema uuids ARE the
|
||||||
|
// symbol uuids; pcbnew's counterpart adds fpPaths).
|
||||||
|
c.selectionEmitPayload = []( EDA_DRAW_FRAME* fr ) -> json
|
||||||
{
|
{
|
||||||
if( aHex.size() == 7 && aHex[0] == '#' )
|
return presenceCore().selectionUuids( fr );
|
||||||
|
};
|
||||||
|
|
||||||
|
c.resolveItem = []( EDA_DRAW_FRAME* fr, const KIID& id ) -> EDA_ITEM*
|
||||||
{
|
{
|
||||||
long v = strtol( aHex.c_str() + 1, nullptr, 16 );
|
return static_cast<SCH_EDIT_FRAME*>( fr )->Schematic()
|
||||||
return KIGFX::COLOR4D( ( ( v >> 16 ) & 0xff ) / 255.0, ( ( v >> 8 ) & 0xff ) / 255.0,
|
.ResolveItem( id, nullptr, /*allowNull*/ true );
|
||||||
( v & 0xff ) / 255.0, 0.9 );
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return KIGFX::COLOR4D( 0.23, 0.51, 0.96, 0.9 ); // palette blue fallback
|
c.drawPeerShapes = []( pcbjam_presence::CORE& aCore, EDA_DRAW_FRAME* aFrame,
|
||||||
}
|
const pcbjam_presence::PEER& peer, const KIGFX::COLOR4D& color,
|
||||||
|
double px )
|
||||||
json selectionUuids( SCH_EDIT_FRAME* aFrame )
|
|
||||||
{
|
{
|
||||||
json uuids = json::array();
|
SCH_EDIT_FRAME* fr = static_cast<SCH_EDIT_FRAME*>( aFrame );
|
||||||
|
|
||||||
SCH_SELECTION_TOOL* selTool = aFrame->GetToolManager()->GetTool<SCH_SELECTION_TOOL>();
|
|
||||||
|
|
||||||
if( !selTool )
|
|
||||||
return uuids;
|
|
||||||
|
|
||||||
for( EDA_ITEM* item : selTool->GetSelection() )
|
|
||||||
uuids.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
|
||||||
|
|
||||||
return uuids;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-settle selection emit (see pcbnew): dedupe against the last emitted set.
|
|
||||||
void checkSelection()
|
|
||||||
{
|
|
||||||
g_selCheckScheduled = false;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::string s = selectionUuids( fr ).dump();
|
|
||||||
|
|
||||||
if( s == g_lastSelectionJson )
|
|
||||||
return;
|
|
||||||
|
|
||||||
g_lastSelectionJson = s;
|
|
||||||
|
|
||||||
EM_ASM( {
|
|
||||||
if( window.kicadCollab && window.kicadCollab.onSelection )
|
|
||||||
window.kicadCollab.onSelection( UTF8ToString( $0 ) );
|
|
||||||
}, s.c_str() );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repaint the remote-peers overlay (CallAfter + COROUTINE — MakeOverlay's view->Add
|
|
||||||
// and the items' virtual ViewBBox() need the fiber stack).
|
|
||||||
void redrawOverlay()
|
|
||||||
{
|
|
||||||
g_redrawScheduled = false;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
||||||
|
|
||||||
if( !g_overlay )
|
|
||||||
{
|
|
||||||
g_overlay = view->MakeOverlay();
|
|
||||||
// Shapes sit one depth unit BELOW the text overlay (fork
|
|
||||||
// SetDepthOffset) so labels always render on top of chips/boxes.
|
|
||||||
g_overlay->SetDepthOffset( pcbjam_presence::PRESENCE_SHAPES_DEPTH_OFFSET );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !g_textOverlay )
|
|
||||||
g_textOverlay = pcbjam_presence::makePresenceTextOverlay( view );
|
|
||||||
|
|
||||||
g_overlay->Clear();
|
|
||||||
g_textOverlay->Clear();
|
|
||||||
|
|
||||||
// Screen-constant sizing via the GAL matrix (GetScale() is the zoom, not px/IU).
|
|
||||||
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 )
|
|
||||||
{
|
|
||||||
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, peer.name, peer.color );
|
|
||||||
|
|
||||||
for( const KIID& id : peer.selection )
|
for( const KIID& id : peer.selection )
|
||||||
{
|
{
|
||||||
|
|
@ -842,155 +733,34 @@ void redrawOverlay()
|
||||||
if( !item )
|
if( !item )
|
||||||
continue; // not in this schematic (yet) — skip silently
|
continue; // not in this schematic (yet) — skip silently
|
||||||
|
|
||||||
pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
|
pcbjam_presence::drawSelectionBox( aCore.overlay.get(), aCore.textOverlay.get(),
|
||||||
item->ViewBBox(), peer.name, color, px,
|
item->ViewBBox(), peer.name, color, px,
|
||||||
g_style );
|
aCore.style );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-app ghosts (0006): a pcbnew peer's selection, as symbol uuids.
|
// Cross-app ghosts (0006) — see resolveXsel for the sheet gating.
|
||||||
// Unlike same-room selections (per-sheet rooms scope those by
|
|
||||||
// construction) these arrive project-wide, so only draw items that
|
|
||||||
// resolve onto the sheet THIS canvas is showing — a bbox from another
|
|
||||||
// sheet's screen would land at meaningless coordinates here.
|
|
||||||
if( !peer.xsel.empty() )
|
if( !peer.xsel.empty() )
|
||||||
{
|
{
|
||||||
pcbjam_presence::STYLE ghost = pcbjam_presence::ghostStyle( g_style );
|
pcbjam_presence::STYLE ghost = pcbjam_presence::ghostStyle( aCore.style );
|
||||||
|
|
||||||
for( const KIID& id : peer.xsel )
|
for( SCH_ITEM* item : resolveXsel( fr, peer ) )
|
||||||
{
|
{
|
||||||
SCH_SHEET_PATH path;
|
pcbjam_presence::drawSelectionBox( aCore.overlay.get(), aCore.textOverlay.get(),
|
||||||
SCH_ITEM* item = fr->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
|
||||||
|
|
||||||
if( !item || path.LastScreen() != fr->GetScreen() )
|
|
||||||
continue;
|
|
||||||
|
|
||||||
pcbjam_presence::drawSelectionBox( g_overlay.get(), g_textOverlay.get(),
|
|
||||||
item->ViewBBox(), peer.name, color, px,
|
item->ViewBBox(), peer.name, color, px,
|
||||||
ghost );
|
ghost );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if( peer.hasCursor )
|
return c;
|
||||||
pcbjam_presence::drawCursor( g_overlay.get(), g_textOverlay.get(), peer.cursor,
|
}();
|
||||||
peer.name, color, px, g_style );
|
|
||||||
|
return core;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment pin dots (0005), drawn last so they sit above selection outlines.
|
|
||||||
for( const PIN& pin : g_pins )
|
|
||||||
{
|
|
||||||
KIGFX::COLOR4D color = pcbjam_presence::peerColor( g_style, pin.name, pin.color );
|
|
||||||
pcbjam_presence::drawPin( g_overlay.get(), pin.pos, color, pin.resolved, px, g_style );
|
|
||||||
}
|
|
||||||
|
|
||||||
view->Update( g_overlay.get() );
|
|
||||||
view->Update( g_textOverlay.get() );
|
|
||||||
fr->GetCanvas()->ForceRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void scheduleRedraw()
|
|
||||||
{
|
|
||||||
if( g_redrawScheduled )
|
|
||||||
return;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
g_redrawScheduled = true;
|
|
||||||
|
|
||||||
fr->CallAfter( []() {
|
|
||||||
COROUTINE<int, int> cor( []( int ) -> int
|
|
||||||
{
|
|
||||||
redrawOverlay();
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Viewport push/pull (world↔screen mapping for the DOM layers, 0005).
|
|
||||||
void emitViewportIfChanged()
|
|
||||||
{
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
||||||
double scale = view->GetScale(); // zoom — cheap change detector only
|
|
||||||
VECTOR2D c = view->GetCenter();
|
|
||||||
|
|
||||||
if( scale == g_lastVpScale && c == g_lastVpCenter )
|
|
||||||
return;
|
|
||||||
|
|
||||||
g_lastVpScale = scale;
|
|
||||||
g_lastVpCenter = c;
|
|
||||||
|
|
||||||
const VECTOR2I& sz = view->GetScreenPixelSize();
|
|
||||||
// px per IU via the GAL matrix — GetScale() is the zoom, not px/IU.
|
|
||||||
double pxPerIu = view->ToScreen( 1.0 );
|
|
||||||
|
|
||||||
EM_ASM( {
|
|
||||||
if( window.kicadCollab && window.kicadCollab.onViewport )
|
|
||||||
window.kicadCollab.onViewport( $0, $1, $2, $3, $4 );
|
|
||||||
}, c.x, c.y, pxPerIu, sz.x, sz.y );
|
|
||||||
|
|
||||||
if( !g_peers.empty() )
|
|
||||||
scheduleRedraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
void emitCursor( double aX, double aY, bool aActive )
|
|
||||||
{
|
|
||||||
EM_ASM( {
|
|
||||||
if( window.kicadCollab && window.kicadCollab.onCursor )
|
|
||||||
window.kicadCollab.onCursor( $0, $1, $2 );
|
|
||||||
}, aX, aY, aActive ? 1 : 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
void onMotion( wxMouseEvent& aEvt )
|
|
||||||
{
|
|
||||||
aEvt.Skip();
|
|
||||||
|
|
||||||
long long now = nowMs();
|
|
||||||
|
|
||||||
if( now - g_lastCursorEmitMs < 50 ) // ≤20 emits/s, event-driven (no timers)
|
|
||||||
return;
|
|
||||||
|
|
||||||
g_lastCursorEmitMs = now;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
wxPoint p = aEvt.GetPosition();
|
|
||||||
VECTOR2D world = fr->GetCanvas()->GetView()->ToWorld( VECTOR2D( p.x, p.y ), true );
|
|
||||||
|
|
||||||
emitCursor( world.x, world.y, true );
|
|
||||||
emitViewportIfChanged(); // catches drag-pan while moving
|
|
||||||
}
|
|
||||||
|
|
||||||
void onLeave( wxMouseEvent& aEvt )
|
|
||||||
{
|
|
||||||
aEvt.Skip();
|
|
||||||
emitCursor( 0, 0, false );
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace presence
|
|
||||||
|
|
||||||
void schedulePresenceSelCheck()
|
void schedulePresenceSelCheck()
|
||||||
{
|
{
|
||||||
if( presence::g_selCheckScheduled )
|
presenceCore().scheduleSelCheck();
|
||||||
return;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
presence::g_selCheckScheduled = true;
|
|
||||||
fr->CallAfter( []() { presence::checkSelection(); } );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
@ -1323,22 +1093,12 @@ void schCollabApply( std::string aJson )
|
||||||
if( !fr )
|
if( !fr )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Defer to the editor's main-loop context so SCH_COMMIT runs like a normal edit, AND run the
|
// Defer to the editor's main-loop context + fiber stack (runOnFiber) so SCH_COMMIT runs
|
||||||
// mutation inside a COROUTINE so it executes on a libcontext fiber stack — the exact context
|
// like a normal edit: SCH_COMMIT::Push's CHT_ADD of a *new* SCH_SHAPE/SCH_SYMBOL
|
||||||
// KiCad tool actions (native draws/edits) run in. SCH_COMMIT::Push's CHT_ADD of a *new*
|
// dispatches GAL virtuals (view->Add → ViewGetLayers) through asyncify-instrumented
|
||||||
// SCH_SHAPE/SCH_SYMBOL dispatches GAL virtuals (view->Add → ViewGetLayers) through asyncify-
|
// invoke_*; off the fiber stack those mis-dispatch and trap inside KiCad core, which the
|
||||||
// instrumented invoke_*; off the fiber stack those mis-dispatch and trap inside KiCad core
|
// bridge can't devirtualize. On the fiber stack they dispatch correctly. (0007.)
|
||||||
// ("memory access out of bounds" / "table index out of bounds"), which the bridge can't
|
pcbjam_collab::runOnFiber( fr, [fr, delta]() { doApply( fr, delta ); } );
|
||||||
// devirtualize. On the fiber stack they dispatch correctly. CallAfter runs on the app main
|
|
||||||
// stack (ProcessPendingEvents), which is where COROUTINE::Call must be invoked from. (0007.)
|
|
||||||
fr->CallAfter( [fr, delta]() {
|
|
||||||
COROUTINE<int, int> cor( [fr, delta]( int ) -> int
|
|
||||||
{
|
|
||||||
doApply( fr, delta );
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1372,14 +1132,7 @@ void schCollabApplyItems( std::string aJson )
|
||||||
if( !fr )
|
if( !fr )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fr->CallAfter( [fr, wire]() {
|
pcbjam_collab::runOnFiber( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
||||||
COROUTINE<int, int> cor( [fr, wire]( int ) -> int
|
|
||||||
{
|
|
||||||
doApplyItems( fr, wire );
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1485,15 +1238,10 @@ bool schCollabTestRemoveItem( std::string aId )
|
||||||
|
|
||||||
SCH_SCREEN* screen = path.LastScreen();
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
|
|
||||||
fr->CallAfter( [fr, item, screen]() {
|
pcbjam_collab::runOnFiber( fr, [fr, item, screen]() {
|
||||||
COROUTINE<int, int> cor( [fr, item, screen]( int ) -> int
|
|
||||||
{
|
|
||||||
SCH_COMMIT commit( fr );
|
SCH_COMMIT commit( fr );
|
||||||
commit.Remove( item, screen );
|
commit.Remove( item, screen );
|
||||||
commit.Push( wxT( "Collab test remove" ) );
|
commit.Push( wxT( "Collab test remove" ) );
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1519,9 +1267,7 @@ bool schCollabTestRotateItem( std::string aId, double aDeg )
|
||||||
SCH_SCREEN* screen = path.LastScreen();
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
int steps = ( (int) ( aDeg / 90.0 + ( aDeg >= 0 ? 0.5 : -0.5 ) ) % 4 + 4 ) % 4;
|
int steps = ( (int) ( aDeg / 90.0 + ( aDeg >= 0 ? 0.5 : -0.5 ) ) % 4 + 4 ) % 4;
|
||||||
|
|
||||||
fr->CallAfter( [fr, item, screen, steps]() {
|
pcbjam_collab::runOnFiber( fr, [fr, item, screen, steps]() {
|
||||||
COROUTINE<int, int> cor( [fr, item, screen, steps]( int ) -> int
|
|
||||||
{
|
|
||||||
SCH_COMMIT commit( fr );
|
SCH_COMMIT commit( fr );
|
||||||
commit.Modify( item, screen );
|
commit.Modify( item, screen );
|
||||||
|
|
||||||
|
|
@ -1529,41 +1275,20 @@ bool schCollabTestRotateItem( std::string aId, double aDeg )
|
||||||
item->Rotate( item->GetPosition(), /*aRotateCCW*/ true );
|
item->Rotate( item->GetPosition(), /*aRotateCCW*/ true );
|
||||||
|
|
||||||
commit.Push( wxT( "Collab test rotate" ) );
|
commit.Push( wxT( "Collab test rotate" ) );
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Edit>Undo exactly like the UI would (main-loop + fiber stack) — miss 09:
|
// Run Edit>Undo / read the undo depth — miss 09; frame-generic, collab_common.h.
|
||||||
// exercises the local-ops-only undo policy and the stale-picker UUID guard.
|
|
||||||
bool schCollabTestUndo()
|
bool schCollabTestUndo()
|
||||||
{
|
{
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
return pcbjam_collab::testUndo( schFrame() );
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
fr->CallAfter( [fr]() {
|
|
||||||
COROUTINE<int, int> cor( [fr]( int ) -> int
|
|
||||||
{
|
|
||||||
fr->GetToolManager()->RunAction( ACTIONS::undo );
|
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local undo stack depth — remote applies must not grow it (miss 09).
|
|
||||||
int schCollabTestUndoDepth()
|
int schCollabTestUndoDepth()
|
||||||
{
|
{
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
return pcbjam_collab::testUndoDepth( schFrame() );
|
||||||
|
|
||||||
return fr ? fr->GetUndoCommandCount() : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a symbol's Value field text — bug 04: fields live inside the symbol (not
|
// Set a symbol's Value field text — bug 04: fields live inside the symbol (not
|
||||||
|
|
@ -1587,16 +1312,11 @@ bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
||||||
SCH_SCREEN* screen = path.LastScreen();
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
wxString text = wxString::FromUTF8( aText.c_str() );
|
wxString text = wxString::FromUTF8( aText.c_str() );
|
||||||
|
|
||||||
fr->CallAfter( [fr, sym, screen, text]() {
|
pcbjam_collab::runOnFiber( fr, [fr, sym, screen, text]() {
|
||||||
COROUTINE<int, int> cor( [fr, sym, screen, text]( int ) -> int
|
|
||||||
{
|
|
||||||
SCH_COMMIT commit( fr );
|
SCH_COMMIT commit( fr );
|
||||||
commit.Modify( sym, screen );
|
commit.Modify( sym, screen );
|
||||||
sym->SetValueFieldText( text );
|
sym->SetValueFieldText( text );
|
||||||
commit.Push( wxT( "Collab test field text" ) );
|
commit.Push( wxT( "Collab test field text" ) );
|
||||||
return 0;
|
|
||||||
} );
|
|
||||||
cor.Call( 0 );
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -1627,51 +1347,12 @@ extern "C" void kicadCollabOnSave( const char* aPath )
|
||||||
|
|
||||||
// ── presence entry points (collab-presence 0003 — eeschema port of the 0002 set) ────────────
|
// ── presence entry points (collab-presence 0003 — eeschema port of the 0002 set) ────────────
|
||||||
|
|
||||||
// Install the presence input hooks on the GAL canvas (idempotent). The canvas is the
|
// Install the presence input hooks on the GAL canvas + the fork's soft-lock
|
||||||
// same window across sheet navigation, so one install serves the whole session.
|
// query (idempotent) — shared core. The canvas is the same window across
|
||||||
|
// sheet navigation, so one install serves the whole session.
|
||||||
void schCollabPresenceStart()
|
void schCollabPresenceStart()
|
||||||
{
|
{
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
presenceCore().start();
|
||||||
|
|
||||||
if( !fr || presence::g_started )
|
|
||||||
return;
|
|
||||||
|
|
||||||
presence::g_started = true;
|
|
||||||
|
|
||||||
// Remote soft-locks (0007): let the selection/move tools consult the
|
|
||||||
// peers' live selections through the fork's process-global query.
|
|
||||||
PCBJAM_REMOTE_LOCK::SetQuery(
|
|
||||||
[]( const KIID& aId, wxString* aHolder ) -> bool
|
|
||||||
{
|
|
||||||
auto it = presence::g_locks.find( aId );
|
|
||||||
|
|
||||||
if( it == presence::g_locks.end() )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if( aHolder )
|
|
||||||
*aHolder = wxString::FromUTF8( it->second.c_str() );
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} );
|
|
||||||
|
|
||||||
wxWindow* canvas = fr->GetCanvas();
|
|
||||||
|
|
||||||
canvas->Bind( wxEVT_MOTION, []( wxMouseEvent& e ) { presence::onMotion( e ); } );
|
|
||||||
canvas->Bind( wxEVT_LEAVE_WINDOW, []( wxMouseEvent& e ) { presence::onLeave( e ); } );
|
|
||||||
|
|
||||||
auto selAndViewport = []( wxEvent& e )
|
|
||||||
{
|
|
||||||
e.Skip();
|
|
||||||
schedulePresenceSelCheck();
|
|
||||||
|
|
||||||
if( SCH_EDIT_FRAME* f = schFrame() )
|
|
||||||
f->CallAfter( []() { presence::emitViewportIfChanged(); } );
|
|
||||||
};
|
|
||||||
|
|
||||||
canvas->Bind( wxEVT_LEFT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
|
||||||
canvas->Bind( wxEVT_RIGHT_UP, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
|
||||||
canvas->Bind( wxEVT_KEY_UP, [selAndViewport]( wxKeyEvent& e ) { selAndViewport( e ); } );
|
|
||||||
canvas->Bind( wxEVT_MOUSEWHEEL, [selAndViewport]( wxMouseEvent& e ) { selAndViewport( e ); } );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS → C++: full remote-peers snapshot (same wire as pcbnew's kicadCollabSetRemote).
|
// JS → C++: full remote-peers snapshot (same wire as pcbnew's kicadCollabSetRemote).
|
||||||
|
|
@ -1679,95 +1360,20 @@ void schCollabPresenceStart()
|
||||||
// sheet switch — peers here are always this sheet's.
|
// sheet switch — peers here are always this sheet's.
|
||||||
void schCollabSetRemote( std::string aJson )
|
void schCollabSetRemote( std::string aJson )
|
||||||
{
|
{
|
||||||
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
presenceCore().setRemote( aJson );
|
||||||
|
|
||||||
if( j.is_discarded() )
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<presence::PEER> peers;
|
|
||||||
|
|
||||||
for( const json& p : j.value( "peers", json::array() ) )
|
|
||||||
{
|
|
||||||
presence::PEER peer;
|
|
||||||
peer.name = p.value( "name", "" );
|
|
||||||
peer.color = presence::parseColor( p.value( "color", "" ) );
|
|
||||||
|
|
||||||
if( p.contains( "cursor" ) && p["cursor"].is_object() )
|
|
||||||
{
|
|
||||||
peer.hasCursor = true;
|
|
||||||
peer.cursor = VECTOR2D( p["cursor"].value( "x", 0.0 ), p["cursor"].value( "y", 0.0 ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
for( const json& u : p.value( "selection", json::array() ) )
|
|
||||||
{
|
|
||||||
if( u.is_string() )
|
|
||||||
peer.selection.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cross-app selection (0006): symbol uuids from a pcbnew peer.
|
|
||||||
for( const json& u : p.value( "xsel", json::array() ) )
|
|
||||||
{
|
|
||||||
if( u.is_string() )
|
|
||||||
peer.xsel.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
peers.push_back( std::move( peer ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote soft-locks (0007): `locks: [{uuid, name}]` — see pcbnew_embind.cpp.
|
|
||||||
std::map<KIID, std::string> locks;
|
|
||||||
|
|
||||||
for( const json& l : j.value( "locks", json::array() ) )
|
|
||||||
{
|
|
||||||
std::string uuid = l.is_object() ? l.value( "uuid", "" ) : "";
|
|
||||||
|
|
||||||
if( !uuid.empty() )
|
|
||||||
locks[ KIID( wxString::FromUTF8( uuid.c_str() ) ) ] = l.value( "name", "" );
|
|
||||||
}
|
|
||||||
|
|
||||||
presence::g_peers = std::move( peers );
|
|
||||||
presence::g_locks = std::move( locks );
|
|
||||||
schCollabPresenceStart();
|
|
||||||
presence::scheduleRedraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS → C++ (collab-presence 0005): comment pin dots (same wire as pcbnew).
|
// JS → C++ (collab-presence 0005): comment pin dots (same wire as pcbnew).
|
||||||
void schCollabSetPins( std::string aJson )
|
void schCollabSetPins( std::string aJson )
|
||||||
{
|
{
|
||||||
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
presenceCore().setPins( aJson );
|
||||||
|
|
||||||
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.name = p.value( "name", "" );
|
|
||||||
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++ (presence tuner): live-patch the overlay STYLE and repaint —
|
// JS → C++ (presence tuner): live-patch the overlay STYLE and repaint —
|
||||||
// see collab_presence_style.h + pcbnew's counterpart.
|
// see collab_presence_style.h + pcbnew's counterpart.
|
||||||
void schCollabSetStyle( std::string aJson )
|
void schCollabSetStyle( std::string aJson )
|
||||||
{
|
{
|
||||||
json j = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
presenceCore().setStyle( aJson );
|
||||||
|
|
||||||
if( j.is_discarded() )
|
|
||||||
return;
|
|
||||||
|
|
||||||
pcbjam_presence::patchStyle( presence::g_style, j );
|
|
||||||
presence::scheduleRedraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuner helper: a VARIED demo-selection set for the current sheet — smallest +
|
// Tuner helper: a VARIED demo-selection set for the current sheet — smallest +
|
||||||
|
|
@ -1858,38 +1464,14 @@ std::string schCollabTestListItems( int aCount )
|
||||||
// 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 )
|
||||||
{
|
{
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
presenceCore().panTo( aCx, aCy );
|
||||||
|
|
||||||
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
|
// 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).
|
// IU via the GAL matrix (GetScale() is the zoom, not px/IU — pcbnew 0002 lesson).
|
||||||
std::string schCollabGetViewport()
|
std::string schCollabGetViewport()
|
||||||
{
|
{
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
return presenceCore().viewportJson();
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return "";
|
|
||||||
|
|
||||||
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
|
||||||
VECTOR2D c = view->GetCenter();
|
|
||||||
const VECTOR2I& sz = view->GetScreenPixelSize();
|
|
||||||
|
|
||||||
return json{ { "cx", c.x }, { "cy", c.y }, { "scale", view->ToScreen( 1.0 ) },
|
|
||||||
{ "w", sz.x }, { "h", sz.y } }.dump();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS pull of the CURRENT selection's uuids (presence seed + e2e no-leak probe).
|
// JS pull of the CURRENT selection's uuids (presence seed + e2e no-leak probe).
|
||||||
|
|
@ -1900,7 +1482,7 @@ std::string schCollabGetSelection()
|
||||||
if( !fr )
|
if( !fr )
|
||||||
return "[]";
|
return "[]";
|
||||||
|
|
||||||
return presence::selectionUuids( fr ).dump();
|
return presenceCore().selectionUuids( fr ).dump();
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS pull of the current selection in the 0006 payload shape. eeschema has no
|
// JS pull of the current selection in the 0006 payload shape. eeschema has no
|
||||||
|
|
@ -1914,13 +1496,13 @@ std::string schCollabGetSelectionFull()
|
||||||
return "{\"uuids\":[],\"fpPaths\":[]}";
|
return "{\"uuids\":[],\"fpPaths\":[]}";
|
||||||
|
|
||||||
json payload;
|
json payload;
|
||||||
payload["uuids"] = presence::selectionUuids( fr );
|
payload["uuids"] = presenceCore().selectionUuids( fr );
|
||||||
payload["fpPaths"] = json::array();
|
payload["fpPaths"] = json::array();
|
||||||
return payload.dump();
|
return payload.dump();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test probe (0006): the schematic-item uuids the current peers' cross-app
|
// Test probe (0006): the schematic-item uuids the current peers' cross-app
|
||||||
// selections resolve to ON THE CURRENT SHEET (mirrors the redraw's gate).
|
// selections resolve to ON THE CURRENT SHEET — same resolveXsel the render uses.
|
||||||
std::string schCollabTestGetCrossMapped()
|
std::string schCollabTestGetCrossMapped()
|
||||||
{
|
{
|
||||||
json arr = json::array();
|
json arr = json::array();
|
||||||
|
|
@ -1930,17 +1512,11 @@ std::string schCollabTestGetCrossMapped()
|
||||||
if( !fr )
|
if( !fr )
|
||||||
return arr.dump();
|
return arr.dump();
|
||||||
|
|
||||||
for( const presence::PEER& peer : presence::g_peers )
|
for( const pcbjam_presence::PEER& peer : presenceCore().peers )
|
||||||
{
|
{
|
||||||
for( const KIID& id : peer.xsel )
|
for( SCH_ITEM* item : resolveXsel( fr, peer ) )
|
||||||
{
|
|
||||||
SCH_SHEET_PATH path;
|
|
||||||
SCH_ITEM* item = fr->Schematic().ResolveItem( id, &path, /*allowNull*/ true );
|
|
||||||
|
|
||||||
if( item && path.LastScreen() == fr->GetScreen() )
|
|
||||||
arr.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
arr.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return arr.dump();
|
return arr.dump();
|
||||||
}
|
}
|
||||||
|
|
@ -1970,13 +1546,7 @@ std::string schCollabTestSelectFirst()
|
||||||
if( !target )
|
if( !target )
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
fr->CallAfter( [fr, target]() {
|
presenceCore().selectItem( target );
|
||||||
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
|
||||||
{
|
|
||||||
st->AddItemToSel( target );
|
|
||||||
schedulePresenceSelCheck();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
return toUtf8( target->m_Uuid.AsString() );
|
return toUtf8( target->m_Uuid.AsString() );
|
||||||
}
|
}
|
||||||
|
|
@ -2012,13 +1582,7 @@ std::string schCollabTestSelectComponent()
|
||||||
if( !target )
|
if( !target )
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
fr->CallAfter( [fr, target]() {
|
presenceCore().selectItem( target );
|
||||||
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
|
||||||
{
|
|
||||||
st->AddItemToSel( target );
|
|
||||||
schedulePresenceSelCheck();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
return toUtf8( target->m_Uuid.AsString() );
|
return toUtf8( target->m_Uuid.AsString() );
|
||||||
}
|
}
|
||||||
|
|
@ -2028,72 +1592,13 @@ std::string schCollabTestSelectComponent()
|
||||||
// forced re-emit).
|
// forced re-emit).
|
||||||
void schCollabReleaseSelection( std::string aUuidsJson, std::string aHolder )
|
void schCollabReleaseSelection( std::string aUuidsJson, std::string aHolder )
|
||||||
{
|
{
|
||||||
json j = json::parse( aUuidsJson, nullptr, /*allow_exceptions*/ false );
|
presenceCore().releaseSelection( aUuidsJson, aHolder );
|
||||||
|
|
||||||
if( j.is_discarded() || !j.is_array() )
|
|
||||||
return;
|
|
||||||
|
|
||||||
SCH_EDIT_FRAME* fr = schFrame();
|
|
||||||
|
|
||||||
if( !fr )
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::vector<KIID> ids;
|
|
||||||
|
|
||||||
for( const json& u : j )
|
|
||||||
{
|
|
||||||
if( u.is_string() )
|
|
||||||
ids.emplace_back( wxString::FromUTF8( u.get<std::string>().c_str() ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( ids.empty() )
|
|
||||||
return;
|
|
||||||
|
|
||||||
wxString holder = wxString::FromUTF8( aHolder.c_str() );
|
|
||||||
|
|
||||||
fr->CallAfter( [fr, ids, holder]() {
|
|
||||||
if( !fr->ToolStackIsEmpty() )
|
|
||||||
fr->GetToolManager()->RunAction( ACTIONS::cancelInteractive );
|
|
||||||
|
|
||||||
SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>();
|
|
||||||
|
|
||||||
if( !st )
|
|
||||||
return;
|
|
||||||
|
|
||||||
bool released = false;
|
|
||||||
|
|
||||||
for( const KIID& id : ids )
|
|
||||||
{
|
|
||||||
SCH_ITEM* item = fr->Schematic().ResolveItem( id, nullptr, /*allowNull*/ true );
|
|
||||||
|
|
||||||
if( item && item->IsSelected() )
|
|
||||||
{
|
|
||||||
st->RemoveItemFromSel( item );
|
|
||||||
released = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( released )
|
|
||||||
{
|
|
||||||
fr->ShowInfoBarWarning( wxString::Format( _( "%s is editing this — released from "
|
|
||||||
"your selection." ),
|
|
||||||
holder ),
|
|
||||||
true );
|
|
||||||
}
|
|
||||||
|
|
||||||
schedulePresenceSelCheck();
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test probe (0007): the current remote soft-lock set as `[{uuid, name}]`.
|
// Test probe (0007): the current remote soft-lock set as `[{uuid, name}]`.
|
||||||
std::string schCollabTestGetLocked()
|
std::string schCollabTestGetLocked()
|
||||||
{
|
{
|
||||||
json arr = json::array();
|
return presenceCore().locksJson();
|
||||||
|
|
||||||
for( const auto& [id, name] : presence::g_locks )
|
|
||||||
arr.push_back( { { "uuid", toUtf8( id.AsString() ) }, { "name", name } } );
|
|
||||||
|
|
||||||
return arr.dump();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test helper: clear the selection through the tool + run the presence check.
|
// Test helper: clear the selection through the tool + run the presence check.
|
||||||
|
|
@ -2105,10 +1610,12 @@ bool schCollabTestClearSelection()
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
fr->CallAfter( [fr]() {
|
fr->CallAfter( [fr]() {
|
||||||
|
// ClearSelection is not on the shared SELECTION_TOOL base — the one
|
||||||
|
// presence hook that stays editor-typed.
|
||||||
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
if( SCH_SELECTION_TOOL* st = fr->GetToolManager()->GetTool<SCH_SELECTION_TOOL>() )
|
||||||
{
|
{
|
||||||
st->ClearSelection();
|
st->ClearSelection();
|
||||||
schedulePresenceSelCheck();
|
presenceCore().scheduleSelCheck();
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue