2026-06-01 16:30:29 +02:00
|
|
|
/*
|
|
|
|
|
* Embind bindings for KiCad eeschema WASM.
|
|
|
|
|
*
|
|
|
|
|
* Picked up automatically by scripts/kicad/build-kicad-target.sh when building
|
|
|
|
|
* the eeschema app (it compiles wasm/bindings/<app>_embind.cpp if present).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#ifdef __EMSCRIPTEN__
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <emscripten.h>
|
2026-06-01 16:30:29 +02:00
|
|
|
#include <emscripten/bind.h>
|
|
|
|
|
#include <kiway_player.h>
|
|
|
|
|
#include <kiway.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <memory>
|
|
|
|
|
#include <set>
|
|
|
|
|
#include <string>
|
2026-06-01 16:30:29 +02:00
|
|
|
#include <vector>
|
|
|
|
|
#include <wx/app.h>
|
|
|
|
|
#include <wx/string.h>
|
|
|
|
|
#include <wx/window.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
#include <kiid.h>
|
2026-06-03 19:37:15 +02:00
|
|
|
#include <layer_ids.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <schematic.h>
|
|
|
|
|
#include <sch_edit_frame.h>
|
|
|
|
|
#include <sch_commit.h>
|
|
|
|
|
#include <sch_item.h>
|
2026-06-03 19:37:15 +02:00
|
|
|
#include <sch_line.h>
|
|
|
|
|
#include <sch_junction.h>
|
2026-06-04 09:40:13 +02:00
|
|
|
#include <sch_no_connect.h>
|
|
|
|
|
#include <sch_text.h>
|
|
|
|
|
#include <sch_label.h>
|
2026-06-04 12:37:05 +02:00
|
|
|
#include <sch_shape.h>
|
|
|
|
|
#include <eda_shape.h>
|
|
|
|
|
#include <stroke_params.h>
|
2026-06-03 17:49:11 +02:00
|
|
|
#include <sch_screen.h>
|
|
|
|
|
#include <sch_sheet_path.h>
|
2026-06-04 12:37:05 +02:00
|
|
|
#include <schematic_settings.h>
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
using namespace emscripten;
|
2026-06-03 17:49:11 +02:00
|
|
|
using json = nlohmann::json;
|
2026-06-01 16:30:29 +02:00
|
|
|
|
|
|
|
|
// Programmatically open a project file (schematic) in the running editor frame,
|
|
|
|
|
// without UI automation. Mirrors single_top.cpp's MacOpenFile path: the editor
|
|
|
|
|
// frame is the app's top window and is a KIWAY_PLAYER. Returns the result of
|
|
|
|
|
// OpenProjectFiles, or false if no frame is available — letting the JS caller
|
|
|
|
|
// fall back to driving File→Open.
|
|
|
|
|
bool kicadOpenFile( std::string path )
|
|
|
|
|
{
|
|
|
|
|
KIWAY_PLAYER* frame =
|
|
|
|
|
wxTheApp ? static_cast<KIWAY_PLAYER*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
|
|
|
|
|
if( !frame )
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if( wxWindow* blocking = frame->Kiway().GetBlockingDialog() )
|
|
|
|
|
blocking->Close( true );
|
|
|
|
|
|
|
|
|
|
return frame->OpenProjectFiles(
|
|
|
|
|
std::vector<wxString>( 1, wxString::FromUTF8( path.c_str() ) ) );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
// ───────────────────────────── Yjs collaborative bridge ─────────────────────────────
|
|
|
|
|
//
|
|
|
|
|
// eeschema's half of the unified bridge contract (features/yjs-bridge/0001, 0003).
|
|
|
|
|
// Unlike pl_editor it needs NO kicad-fork change: SCH_ITEM already carries a stable
|
|
|
|
|
// KIID, and eeschema has native change machinery, so the adapter is a thin re-use:
|
|
|
|
|
// ChangeSource (emit) = a SCHEMATIC_LISTENER subclass (SCH_COMMIT::Push fires it)
|
|
|
|
|
// apply = SCH_COMMIT Modify/Remove + Push (drives connectivity recompute)
|
|
|
|
|
// The generic JS reconciler / transport / WasmTool wiring are reused unchanged.
|
|
|
|
|
//
|
|
|
|
|
// Scope of this first commit (0003 §"first PoC"): position-level sync of existing
|
|
|
|
|
// items — changed (move/edit) and removed. Decomposed field coverage via reflection,
|
|
|
|
|
// constructing arbitrary new item types on `added`, and symbol-instance / multi-sheet
|
|
|
|
|
// scoping are deferred (see TODOs). Items are resolved by globally-unique uuid, so
|
|
|
|
|
// changed/removed already work across the whole hierarchy without sheet scoping.
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
// Guard so SCH_COMMIT::Push's listener callbacks during apply() aren't re-emitted.
|
|
|
|
|
bool s_applyingRemote = false;
|
|
|
|
|
|
|
|
|
|
std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str() ); }
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* schFrame()
|
|
|
|
|
{
|
|
|
|
|
return wxTheApp ? dynamic_cast<SCH_EDIT_FRAME*>( wxTheApp->GetTopWindow() ) : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
json itemToJson( SCH_ITEM* aItem )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I p = aItem->GetPosition();
|
2026-06-03 19:37:15 +02:00
|
|
|
json j = {
|
2026-06-03 17:49:11 +02:00
|
|
|
{ "id", toUtf8( aItem->m_Uuid.AsString() ) },
|
|
|
|
|
{ "type", toUtf8( aItem->GetClass() ) },
|
2026-06-03 19:37:15 +02:00
|
|
|
{ "x", p.x }, // internal units; integral, so no quantization needed
|
2026-06-03 17:49:11 +02:00
|
|
|
{ "y", p.y },
|
|
|
|
|
};
|
2026-06-03 19:37:15 +02:00
|
|
|
|
|
|
|
|
// Per-type fields needed to reconstruct the item on `added` (0003 converters).
|
|
|
|
|
// Hand-mapped for the wire-editing types; other types sync position-only (move) and
|
|
|
|
|
// are skipped on `added` (logged) until their converters are added.
|
|
|
|
|
if( aItem->Type() == SCH_LINE_T )
|
|
|
|
|
{
|
|
|
|
|
auto* line = static_cast<SCH_LINE*>( aItem );
|
|
|
|
|
j["sx"] = line->GetStartPoint().x;
|
|
|
|
|
j["sy"] = line->GetStartPoint().y;
|
|
|
|
|
j["ex"] = line->GetEndPoint().x;
|
|
|
|
|
j["ey"] = line->GetEndPoint().y;
|
|
|
|
|
j["layer"] = (int) line->GetLayer();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 09:40:13 +02:00
|
|
|
if( EDA_TEXT* txt = dynamic_cast<EDA_TEXT*>( aItem ) )
|
|
|
|
|
j["text"] = toUtf8( txt->GetText() );
|
|
|
|
|
|
|
|
|
|
if( SCH_LABEL_BASE* lbl = dynamic_cast<SCH_LABEL_BASE*>( aItem ) )
|
|
|
|
|
j["shape"] = (int) lbl->GetShape();
|
|
|
|
|
|
2026-06-04 12:37:05 +02:00
|
|
|
// Graphic shapes (circle / rectangle / arc / line / bezier): geometry is start/end plus,
|
|
|
|
|
// for an arc, the center, and for a bezier the two control points. Stroke + fill complete it.
|
|
|
|
|
if( aItem->Type() == SCH_SHAPE_T )
|
|
|
|
|
{
|
|
|
|
|
auto* shp = static_cast<SCH_SHAPE*>( aItem );
|
|
|
|
|
j["stype"] = (int) shp->GetShape(); // SHAPE_T
|
|
|
|
|
j["sx"] = shp->GetStart().x;
|
|
|
|
|
j["sy"] = shp->GetStart().y;
|
|
|
|
|
j["ex"] = shp->GetEnd().x;
|
|
|
|
|
j["ey"] = shp->GetEnd().y;
|
|
|
|
|
j["layer"] = (int) shp->GetLayer();
|
|
|
|
|
j["width"] = shp->GetStroke().GetWidth();
|
|
|
|
|
j["fill"] = (int) shp->GetFillMode();
|
|
|
|
|
|
|
|
|
|
if( shp->GetShape() == SHAPE_T::ARC )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I c = shp->GetCenter();
|
|
|
|
|
j["cx"] = c.x;
|
|
|
|
|
j["cy"] = c.y;
|
|
|
|
|
}
|
|
|
|
|
else if( shp->GetShape() == SHAPE_T::BEZIER )
|
|
|
|
|
{
|
|
|
|
|
j["c1x"] = shp->GetBezierC1().x;
|
|
|
|
|
j["c1y"] = shp->GetBezierC1().y;
|
|
|
|
|
j["c2x"] = shp->GetBezierC2().x;
|
|
|
|
|
j["c2y"] = shp->GetBezierC2().y;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
return j;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construct a new SCH_ITEM from a delta item (for `added`), with the delta's uuid
|
|
|
|
|
// (m_Uuid is const → const_cast, exactly as the s-expr parser does). Returns nullptr for
|
|
|
|
|
// types without a converter yet.
|
|
|
|
|
SCH_ITEM* makeItem( const json& j )
|
|
|
|
|
{
|
|
|
|
|
std::string type = j.value( "type", "" );
|
|
|
|
|
SCH_ITEM* item = nullptr;
|
|
|
|
|
|
|
|
|
|
if( type == "SCH_LINE" )
|
|
|
|
|
{
|
|
|
|
|
int layer = j.value( "layer", (int) LAYER_NOTES );
|
|
|
|
|
auto* line = new SCH_LINE( VECTOR2I( j.value( "sx", 0 ), j.value( "sy", 0 ) ), layer );
|
|
|
|
|
line->SetEndPoint( VECTOR2I( j.value( "ex", 0 ), j.value( "ey", 0 ) ) );
|
|
|
|
|
item = line;
|
|
|
|
|
}
|
|
|
|
|
else if( type == "SCH_JUNCTION" )
|
|
|
|
|
{
|
|
|
|
|
item = new SCH_JUNCTION( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ) );
|
|
|
|
|
}
|
2026-06-04 09:40:13 +02:00
|
|
|
else if( type == "SCH_NO_CONNECT" )
|
|
|
|
|
{
|
|
|
|
|
item = new SCH_NO_CONNECT( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ) );
|
|
|
|
|
}
|
|
|
|
|
else if( type == "SCH_TEXT" )
|
|
|
|
|
{
|
2026-06-04 12:37:05 +02:00
|
|
|
auto* txt = new SCH_TEXT( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ),
|
|
|
|
|
wxString::FromUTF8( j.value( "text", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
// Mirror the interactive text tool (sch_drawing_tools.cpp createNewText): parent the
|
|
|
|
|
// item to the schematic and apply the project's default text size, so a remotely-added
|
|
|
|
|
// text resolves variables / renders identically to a locally-placed one.
|
|
|
|
|
if( SCH_EDIT_FRAME* fr = schFrame() )
|
|
|
|
|
{
|
|
|
|
|
txt->SetParent( &fr->Schematic() );
|
|
|
|
|
int sz = fr->Schematic().Settings().m_DefaultTextSize;
|
|
|
|
|
txt->SetTextSize( VECTOR2I( sz, sz ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
item = txt;
|
2026-06-04 09:40:13 +02:00
|
|
|
}
|
|
|
|
|
else if( type == "SCH_LABEL" || type == "SCH_GLOBALLABEL" || type == "SCH_HIERLABEL" )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I pos( j.value( "x", 0 ), j.value( "y", 0 ) );
|
|
|
|
|
wxString text = wxString::FromUTF8( j.value( "text", "" ).c_str() );
|
|
|
|
|
|
|
|
|
|
SCH_LABEL_BASE* lbl = ( type == "SCH_LABEL" ) ? (SCH_LABEL_BASE*) new SCH_LABEL( pos, text )
|
|
|
|
|
: ( type == "SCH_GLOBALLABEL" ) ? (SCH_LABEL_BASE*) new SCH_GLOBALLABEL( pos, text )
|
|
|
|
|
: (SCH_LABEL_BASE*) new SCH_HIERLABEL( pos, text );
|
|
|
|
|
|
|
|
|
|
if( j.contains( "shape" ) )
|
|
|
|
|
lbl->SetShape( (LABEL_FLAG_SHAPE) j["shape"].get<int>() );
|
|
|
|
|
|
|
|
|
|
item = lbl;
|
|
|
|
|
}
|
2026-06-04 12:37:05 +02:00
|
|
|
// SCH_SHAPE `added` reconstruction is DEFERRED (returns nullptr → logged "no converter").
|
|
|
|
|
// A constructed shape builds fine, but committing a *new* one traps in SCH_COMMIT::Push's
|
|
|
|
|
// CHT_ADD path (the GAL view->Add of a new SCH_SHAPE) with a wasm "memory access out of
|
|
|
|
|
// bounds". The identical view->Add succeeds when the shape is loaded from file and when an
|
|
|
|
|
// existing shape is MOVED (CHT_MODIFY) — so this is the asyncify indirect-call (invoke_viii)
|
|
|
|
|
// mis-dispatch class: a SCH_SHAPE add-path virtual that mis-dispatches only from the
|
|
|
|
|
// programmatic CallAfter/apply context (same family as the original SCH_ITEM::Move trap,
|
|
|
|
|
// 0003). The dyncall shim only catches the "signature mismatch" variant, and this trap
|
|
|
|
|
// fires inside the wrong function so it can't be safely retried. itemToJson still emits the
|
|
|
|
|
// shape geometry (forward-compatible + drives `changed`/move); only `added` is skipped.
|
|
|
|
|
// Same blocker as SCH_SYMBOL add — see features/yjs-bridge/0006.
|
2026-06-03 19:37:15 +02:00
|
|
|
|
|
|
|
|
if( item )
|
|
|
|
|
const_cast<KIID&>( item->m_Uuid ) = KIID( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
return item;
|
2026-06-03 17:49:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Full current model as an array of item json, deduped by uuid across the hierarchy.
|
|
|
|
|
json snapshotItems( SCHEMATIC& aSch )
|
|
|
|
|
{
|
|
|
|
|
json arr = json::array();
|
|
|
|
|
std::set<std::string> seen;
|
|
|
|
|
|
|
|
|
|
for( const SCH_SHEET_PATH& path : aSch.Hierarchy() )
|
|
|
|
|
{
|
|
|
|
|
SCH_SCREEN* screen = const_cast<SCH_SHEET_PATH&>( path ).LastScreen();
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
|
|
|
|
std::string id = toUtf8( item->m_Uuid.AsString() );
|
|
|
|
|
|
|
|
|
|
if( seen.insert( id ).second )
|
|
|
|
|
arr.push_back( itemToJson( item ) );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void emit( const json& aDelta )
|
|
|
|
|
{
|
|
|
|
|
std::string s = aDelta.dump();
|
|
|
|
|
EM_ASM( {
|
|
|
|
|
if( window.kicadCollab && window.kicadCollab.onDelta )
|
|
|
|
|
window.kicadCollab.onDelta( UTF8ToString( $0 ) );
|
|
|
|
|
}, s.c_str() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ChangeSource: native SCHEMATIC_LISTENER. SCH_COMMIT::Push fires these in bulk for
|
|
|
|
|
// every local edit (move, add, remove, …) — that's our emit trigger.
|
|
|
|
|
class COLLAB_LISTENER : public SCHEMATIC_LISTENER
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
void OnSchItemsAdded( SCHEMATIC&, std::vector<SCH_ITEM*>& aItems ) override
|
|
|
|
|
{
|
|
|
|
|
emitItems( "added", aItems );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnSchItemsChanged( SCHEMATIC&, std::vector<SCH_ITEM*>& aItems ) override
|
|
|
|
|
{
|
|
|
|
|
emitItems( "changed", aItems );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnSchItemsRemoved( SCHEMATIC&, std::vector<SCH_ITEM*>& aItems ) override
|
|
|
|
|
{
|
|
|
|
|
if( s_applyingRemote )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
json removed = json::array();
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : aItems )
|
|
|
|
|
removed.push_back( toUtf8( item->m_Uuid.AsString() ) );
|
|
|
|
|
|
|
|
|
|
if( !removed.empty() )
|
|
|
|
|
emit( json{ { "added", json::array() }, { "changed", json::array() },
|
|
|
|
|
{ "removed", removed } } );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void emitItems( const char* aKey, std::vector<SCH_ITEM*>& aItems )
|
|
|
|
|
{
|
|
|
|
|
if( s_applyingRemote )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
json arr = json::array();
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : aItems )
|
|
|
|
|
arr.push_back( itemToJson( item ) );
|
|
|
|
|
|
|
|
|
|
if( arr.empty() )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
json d = { { "added", json::array() }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } };
|
|
|
|
|
d[aKey] = arr;
|
|
|
|
|
emit( d );
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
COLLAB_LISTENER* g_listener = nullptr;
|
|
|
|
|
|
|
|
|
|
// Get the live SCHEMATIC and ensure our listener is registered on it (idempotent).
|
|
|
|
|
SCHEMATIC* ensureBridge()
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
SCHEMATIC& sch = fr->Schematic();
|
|
|
|
|
|
|
|
|
|
if( !g_listener )
|
|
|
|
|
{
|
|
|
|
|
g_listener = new COLLAB_LISTENER();
|
|
|
|
|
sch.AddListener( g_listener );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &sch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
// The actual model mutation, via SCH_COMMIT so connectivity/ERC recompute as for a UI
|
|
|
|
|
// edit. (Editor write ops like SCH_ITEM::Move are called through invoke_vii, whose
|
|
|
|
|
// asyncify-instrumented dynCall trampoline traps on a stale type — fixed at the JS shim
|
|
|
|
|
// layer in scripts/common/shims/dyncall-binding.js.tmpl; see 0003.)
|
2026-06-03 17:49:11 +02:00
|
|
|
void doApply( SCH_EDIT_FRAME* aFrame, const json& aDelta )
|
|
|
|
|
{
|
|
|
|
|
SCHEMATIC& sch = aFrame->Schematic();
|
|
|
|
|
|
|
|
|
|
s_applyingRemote = true;
|
|
|
|
|
|
|
|
|
|
SCH_COMMIT commit( aFrame );
|
|
|
|
|
bool staged = false;
|
|
|
|
|
|
|
|
|
|
for( const json& rid : aDelta.value( "removed", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
KIID id( wxString::FromUTF8( rid.get<std::string>().c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = sch.ResolveItem( id, &path, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Remove( item, path.LastScreen() );
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for( const json& j : aDelta.value( "changed", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
SCH_SHEET_PATH path;
|
|
|
|
|
KIID id( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = sch.ResolveItem( id, &path, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Modify( item, path.LastScreen() );
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
if( item->Type() == SCH_LINE_T && j.contains( "sx" ) )
|
|
|
|
|
{
|
|
|
|
|
// Wires reshape (endpoints move independently), so set both points.
|
|
|
|
|
auto* line = static_cast<SCH_LINE*>( item );
|
|
|
|
|
line->SetStartPoint( VECTOR2I( j["sx"].get<int>(), j["sy"].get<int>() ) );
|
|
|
|
|
line->SetEndPoint( VECTOR2I( j["ex"].get<int>(), j["ey"].get<int>() ) );
|
|
|
|
|
}
|
|
|
|
|
else if( j.contains( "x" ) && j.contains( "y" ) )
|
2026-06-03 17:49:11 +02:00
|
|
|
{
|
|
|
|
|
VECTOR2I newPos( j["x"].get<int>(), j["y"].get<int>() );
|
|
|
|
|
item->Move( newPos - item->GetPosition() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
for( const json& j : aDelta.value( "added", json::array() ) )
|
|
|
|
|
{
|
|
|
|
|
KIID id( wxString::FromUTF8( j.value( "id", "" ).c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( sch.ResolveItem( id, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
continue; // already present (our own echo)
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = makeItem( j ) )
|
|
|
|
|
{
|
|
|
|
|
commit.Add( item, aFrame->GetScreen() );
|
|
|
|
|
staged = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
EM_ASM( { console.log( "[collab] eeschema apply: no converter for added type " + UTF8ToString( $0 ) ); },
|
|
|
|
|
j.value( "type", "?" ).c_str() );
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-03 17:49:11 +02:00
|
|
|
|
|
|
|
|
if( staged )
|
|
|
|
|
commit.Push( wxT( "Collaborative edit" ) );
|
|
|
|
|
|
|
|
|
|
s_applyingRemote = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
// Test/PoC move (the SCH_COMMIT body for kicadCollabTestMoveFirst, deferred via CallAfter).
|
|
|
|
|
void collabTestMove( SCH_EDIT_FRAME* aFrame, SCH_ITEM* aItem, SCH_SCREEN* aScreen, int aDx,
|
|
|
|
|
int aDy )
|
|
|
|
|
{
|
|
|
|
|
SCH_COMMIT commit( aFrame );
|
|
|
|
|
commit.Modify( aItem, aScreen );
|
|
|
|
|
aItem->Move( VECTOR2I( aDx, aDy ) );
|
|
|
|
|
commit.Push( wxT( "Collab test move" ) );
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:49:11 +02:00
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// JS → C++. Apply a remote per-item delta by uuid, through SCH_COMMIT so connectivity/
|
|
|
|
|
// ERC/hierarchy recompute the same way a UI edit would (0003 §apply).
|
|
|
|
|
//
|
|
|
|
|
// SCH_COMMIT must run in the editor's Asyncify-rooted main loop — invoking it from this
|
|
|
|
|
// embind ccall, or from an emscripten_async_call/setTimeout callback, traps with an
|
|
|
|
|
// "indirect call signature mismatch" because those are not the asyncify root (0001 §5).
|
|
|
|
|
// wxEvtHandler::CallAfter queues onto the app's pending-event list, which the wasm main
|
|
|
|
|
// loop drains every frame via ProcessPendingEvents() (src/wasm/evtloop.cpp) — i.e. the
|
|
|
|
|
// exact context real UI edits run in. So defer the whole mutation there.
|
|
|
|
|
void kicadCollabApply( std::string aJson )
|
|
|
|
|
{
|
|
|
|
|
json delta = json::parse( aJson, nullptr, /*allow_exceptions*/ false );
|
|
|
|
|
|
|
|
|
|
if( delta.is_discarded() )
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return;
|
|
|
|
|
|
2026-06-03 19:37:15 +02:00
|
|
|
// Defer to the editor's main-loop context so SCH_COMMIT runs like a normal edit.
|
2026-06-03 17:49:11 +02:00
|
|
|
fr->CallAfter( [fr, delta]() { doApply( fr, delta ); } );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// JS pull of the full current model as an all-"added" delta (seed/baseline). Also
|
|
|
|
|
// registers the change listener on first call.
|
|
|
|
|
std::string kicadCollabSnapshot()
|
|
|
|
|
{
|
|
|
|
|
SCHEMATIC* sch = ensureBridge();
|
|
|
|
|
json added = sch ? snapshotItems( *sch ) : json::array();
|
|
|
|
|
|
|
|
|
|
return json{ { "added", added }, { "changed", json::array() },
|
|
|
|
|
{ "removed", json::array() } }.dump();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Test/PoC helper: move the first schematic item by (dx,dy) IU via a real SCH_COMMIT,
|
|
|
|
|
// firing the listener — a deterministic local edit for the two-tab demo / e2e.
|
|
|
|
|
// Returns the moved item's uuid.
|
|
|
|
|
std::string kicadCollabTestMoveFirst( int aDx, int aDy )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
SCHEMATIC& sch = fr->Schematic();
|
|
|
|
|
|
|
|
|
|
for( const SCH_SHEET_PATH& path : sch.Hierarchy() )
|
|
|
|
|
{
|
|
|
|
|
SCH_SCREEN* screen = const_cast<SCH_SHEET_PATH&>( path ).LastScreen();
|
|
|
|
|
|
|
|
|
|
if( !screen )
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
for( SCH_ITEM* item : screen->Items() )
|
|
|
|
|
{
|
2026-06-03 19:37:15 +02:00
|
|
|
fr->CallAfter( [fr, item, screen, aDx, aDy]() { collabTestMove( fr, item, screen, aDx, aDy ); } );
|
2026-06-03 17:49:11 +02:00
|
|
|
return toUtf8( item->m_Uuid.AsString() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Test helper: read an item's position by uuid as "x,y" (internal units).
|
|
|
|
|
std::string kicadCollabGetPos( std::string aId )
|
|
|
|
|
{
|
|
|
|
|
SCH_EDIT_FRAME* fr = schFrame();
|
|
|
|
|
|
|
|
|
|
if( !fr )
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
KIID id( wxString::FromUTF8( aId.c_str() ) );
|
|
|
|
|
|
|
|
|
|
if( SCH_ITEM* item = fr->Schematic().ResolveItem( id, nullptr, /*allowNull*/ true ) )
|
|
|
|
|
{
|
|
|
|
|
VECTOR2I p = item->GetPosition();
|
|
|
|
|
return std::to_string( p.x ) + "," + std::to_string( p.y );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 16:30:29 +02:00
|
|
|
EMSCRIPTEN_BINDINGS(eeschema) {
|
|
|
|
|
// Programmatic file open (preferred over UI automation from the web app).
|
|
|
|
|
function("kicadOpenFile", &kicadOpenFile);
|
2026-06-03 17:49:11 +02:00
|
|
|
// Yjs collaborative bridge entry points (same contract as pl_editor).
|
|
|
|
|
function("kicadCollabApply", &kicadCollabApply);
|
|
|
|
|
function("kicadCollabSnapshot", &kicadCollabSnapshot);
|
|
|
|
|
function("kicadCollabTestMoveFirst", &kicadCollabTestMoveFirst);
|
|
|
|
|
function("kicadCollabGetPos", &kicadCollabGetPos);
|
2026-06-01 16:30:29 +02:00
|
|
|
}
|
|
|
|
|
#endif
|