/* * Embind bindings for KiCad pl_editor (drawing-sheet / page-layout editor) WASM. * * Picked up automatically by scripts/kicad/build-kicad-target.sh when building * the pl_editor app (it compiles wasm/bindings/_embind.cpp if present). */ #ifdef __EMSCRIPTEN__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace emscripten; using json = nlohmann::json; // Programmatically open a drawing-sheet file (.kicad_wks) 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 (PL_EDITOR_FRAME // overrides OpenProjectFiles). 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( wxTheApp->GetTopWindow() ) : nullptr; if( !frame ) return false; if( wxWindow* blocking = frame->Kiway().GetBlockingDialog() ) blocking->Close( true ); return frame->OpenProjectFiles( std::vector( 1, wxString::FromUTF8( path.c_str() ) ) ); } // Programmatically save the in-memory drawing sheet to a .kicad_wks file, without // driving the Save As dialog. pl_editor edits the process-wide singleton // DS_DATA_MODEL::GetTheInstance(), so this serializes exactly what the editor has // loaded — letting a test read the file back from MEMFS and assert the (uuid …) // round-trip / backfill. (Also a building block for the collab bridge's later // materialize-to-file path.) void kicadSaveDrawingSheet( std::string path ) { DS_DATA_MODEL::GetTheInstance().Save( wxString::FromUTF8( path.c_str() ) ); } // ───────────────────────────── Yjs collaborative bridge ───────────────────────────── // // pl_editor's half of the unified bridge contract (features/yjs-bridge/0001-0002). // This lives in the wasm layer (not the kicad fork) so fork divergence stays at the // single OnModify() hook; everything below uses only public DS_DATA_MODEL / DS_DATA_ITEM // API. The contract is a per-item delta { added:[item], changed:[item], removed:[uuid] } // where each item is { id, type, …fields }. text/segment/rect use decomposed scalars // (field-level merge); polygon/bitmap reuse the native per-item s-expr serializer as an // opaque `sexpr` blob (item-level merge) — see 0002 §field-mapping. // // C++ → JS (emit): OnModify → kicadCollabOnModify → snapshot-diff → window.kicadCollab.onDelta(json) // JS → C++ (apply): peer delta → Module.kicadCollabApply(json) → mutate model by uuid → HardRedraw // namespace { // Guard: set around apply() so the differ ignores model mutations we caused ourselves // (apply → HardRedraw/OnModify → differ would otherwise echo them back). 0001 §5. bool s_applyingRemote = false; // Last-emitted item-set, keyed by uuid → its field json. The differ's baseline. std::map s_snapshot; std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str() ); } const char* typeStr( DS_DATA_ITEM::DS_ITEM_TYPE t ) { switch( t ) { case DS_DATA_ITEM::DS_TEXT: return "text"; case DS_DATA_ITEM::DS_SEGMENT: return "segment"; case DS_DATA_ITEM::DS_RECT: return "rect"; case DS_DATA_ITEM::DS_POLYPOLYGON:return "polygon"; case DS_DATA_ITEM::DS_BITMAP: return "bitmap"; } return "unknown"; } EDA_DRAW_FRAME* topFrame() { return wxTheApp ? dynamic_cast( wxTheApp->GetTopWindow() ) : nullptr; } DS_DATA_ITEM* findByUuid( DS_DATA_MODEL& aModel, const std::string& aId ) { for( DS_DATA_ITEM* it : aModel.GetItems() ) { if( toUtf8( it->m_Uuid.AsString() ) == aId ) return it; } return nullptr; } // Serialize one item to its opaque s-expr blob via the native per-item formatter. The // blob already carries (uuid …), so a string compare detects any field change and the // blob re-parses to a fully-formed item on apply. (0002 mechanism 2.) std::string itemBlob( DS_DATA_ITEM* aItem ) { std::vector one{ aItem }; wxString str; DS_DATA_MODEL::GetTheInstance().SaveInString( one, &str ); return toUtf8( str ); } json itemToJson( DS_DATA_ITEM* aItem ) { json j; j["id"] = toUtf8( aItem->m_Uuid.AsString() ); j["type"] = typeStr( aItem->GetType() ); j["name"] = toUtf8( aItem->m_Name ); j["x"] = aItem->m_Pos.m_Pos.x; // mm (model's native units for data items) j["y"] = aItem->m_Pos.m_Pos.y; j["anchor"] = aItem->m_Pos.m_Anchor; switch( aItem->GetType() ) { case DS_DATA_ITEM::DS_TEXT: { auto* t = static_cast( aItem ); j["text"] = toUtf8( t->m_TextBase ); j["orient"] = t->m_Orient; j["hjustify"] = (int) t->m_Hjustify; j["vjustify"] = (int) t->m_Vjustify; j["italic"] = t->m_Italic; j["bold"] = t->m_Bold; j["sizeX"] = t->m_TextSize.x; j["sizeY"] = t->m_TextSize.y; break; } case DS_DATA_ITEM::DS_SEGMENT: case DS_DATA_ITEM::DS_RECT: j["ex"] = aItem->m_End.m_Pos.x; j["ey"] = aItem->m_End.m_Pos.y; j["eanchor"] = aItem->m_End.m_Anchor; j["linewidth"] = aItem->m_LineWidth; break; case DS_DATA_ITEM::DS_POLYPOLYGON: case DS_DATA_ITEM::DS_BITMAP: j["sexpr"] = itemBlob( aItem ); // opaque; item-level merge break; } return j; } std::map snapshotMap() { std::map out; for( DS_DATA_ITEM* it : DS_DATA_MODEL::GetTheInstance().GetItems() ) out[ toUtf8( it->m_Uuid.AsString() ) ] = itemToJson( it ); return out; } 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() ); } // Apply scalar fields from json onto an existing scalar item (text/segment/rect). void applyFields( DS_DATA_ITEM* aItem, const json& j ) { if( j.contains( "name" ) ) aItem->m_Name = wxString::FromUTF8( std::string( j["name"] ).c_str() ); if( j.contains( "x" ) ) aItem->m_Pos.m_Pos.x = j["x"].get(); if( j.contains( "y" ) ) aItem->m_Pos.m_Pos.y = j["y"].get(); if( j.contains( "anchor" ) ) aItem->m_Pos.m_Anchor = j["anchor"].get(); switch( aItem->GetType() ) { case DS_DATA_ITEM::DS_TEXT: { auto* t = static_cast( aItem ); if( j.contains( "text" ) ) t->m_TextBase = wxString::FromUTF8( std::string( j["text"] ).c_str() ); if( j.contains( "orient" ) ) t->m_Orient = j["orient"].get(); if( j.contains( "hjustify" ) ) t->m_Hjustify = (GR_TEXT_H_ALIGN_T) j["hjustify"].get(); if( j.contains( "vjustify" ) ) t->m_Vjustify = (GR_TEXT_V_ALIGN_T) j["vjustify"].get(); if( j.contains( "italic" ) ) t->m_Italic = j["italic"].get(); if( j.contains( "bold" ) ) t->m_Bold = j["bold"].get(); if( j.contains( "sizeX" ) ) t->m_TextSize.x = j["sizeX"].get(); if( j.contains( "sizeY" ) ) t->m_TextSize.y = j["sizeY"].get(); break; } case DS_DATA_ITEM::DS_SEGMENT: case DS_DATA_ITEM::DS_RECT: if( j.contains( "ex" ) ) aItem->m_End.m_Pos.x = j["ex"].get(); if( j.contains( "ey" ) ) aItem->m_End.m_Pos.y = j["ey"].get(); if( j.contains( "eanchor" ) ) aItem->m_End.m_Anchor = j["eanchor"].get(); if( j.contains( "linewidth" ) ) aItem->m_LineWidth = j["linewidth"].get(); break; default: break; } } DS_DATA_ITEM* createScalarItem( const std::string& aType ) { if( aType == "text" ) return new DS_DATA_ITEM_TEXT( wxEmptyString ); if( aType == "segment" ) return new DS_DATA_ITEM( DS_DATA_ITEM::DS_SEGMENT ); if( aType == "rect" ) return new DS_DATA_ITEM( DS_DATA_ITEM::DS_RECT ); return nullptr; } // Reconstruct a polygon/bitmap from its opaque blob by appending it through the normal // parser (the blob is a self-contained mini (kicad_wks …) model). Its (uuid …) round-trips. void addBlob( DS_DATA_MODEL& aModel, const json& j ) { if( !j.contains( "sexpr" ) ) return; std::string sexpr = j["sexpr"]; aModel.SetPageLayout( sexpr.c_str(), /*aAppend*/ true, wxT( "collab-blob" ) ); } } // namespace // JS → C++. Apply a remote per-item delta to the model, by uuid. Guarded so the // resulting model mutations are not re-emitted as local changes. void kicadCollabApply( std::string aJson ) { json delta = json::parse( aJson, nullptr, /*allow_exceptions*/ false ); if( delta.is_discarded() ) return; s_applyingRemote = true; DS_DATA_MODEL& model = DS_DATA_MODEL::GetTheInstance(); for( const json& rid : delta.value( "removed", json::array() ) ) { if( DS_DATA_ITEM* item = findByUuid( model, rid.get() ) ) { model.Remove( item ); delete item; } } for( const json& j : delta.value( "changed", json::array() ) ) { std::string id = j.value( "id", "" ); std::string type = j.value( "type", "" ); DS_DATA_ITEM* item = findByUuid( model, id ); if( type == "polygon" || type == "bitmap" ) { // Blob items merge at item granularity: replace wholesale. if( item ) { model.Remove( item ); delete item; } addBlob( model, j ); } else if( item ) { applyFields( item, j ); } } for( const json& j : delta.value( "added", json::array() ) ) { std::string id = j.value( "id", "" ); std::string type = j.value( "type", "" ); if( !id.empty() && findByUuid( model, id ) ) continue; // already present (our own echo) if( type == "polygon" || type == "bitmap" ) { addBlob( model, j ); } else if( DS_DATA_ITEM* item = createScalarItem( type ) ) { item->m_Uuid = KIID( wxString::FromUTF8( id.c_str() ) ); applyFields( item, j ); model.Append( item ); } } // Rebase the differ on the post-apply state so our own mutations aren't echoed, // then rebuild the GAL view from the model. (Selection re-acquire by uuid is a // deferred refinement — 0002.) s_snapshot = snapshotMap(); if( EDA_DRAW_FRAME* fr = topFrame() ) fr->HardRedraw(); s_applyingRemote = false; } // C++ → JS. The snapshot-differ ChangeSource: derive per-item add/remove/change events // by diffing the current model against the last-emitted snapshot, then emit the delta. // Called from PL_EDITOR_FRAME::OnModify() (pl_editor's single change chokepoint). extern "C" void kicadCollabOnModify() { if( s_applyingRemote ) return; std::map cur = snapshotMap(); json added = json::array(); json changed = json::array(); json removed = json::array(); for( const auto& [id, j] : cur ) { auto prev = s_snapshot.find( id ); if( prev == s_snapshot.end() ) added.push_back( j ); else if( prev->second != j ) changed.push_back( j ); } for( const auto& [id, j] : s_snapshot ) { if( !cur.count( id ) ) removed.push_back( id ); } s_snapshot = std::move( cur ); if( added.empty() && changed.empty() && removed.empty() ) return; emit( json{ { "added", added }, { "changed", changed }, { "removed", removed } } ); } // JS pull of the full current model as an all-"added" delta, used to seed the Y.Doc on // join and to (re)baseline the differ. Idempotent. std::string kicadCollabSnapshot() { std::map cur = snapshotMap(); json added = json::array(); for( const auto& [id, j] : cur ) added.push_back( j ); s_snapshot = cur; return json{ { "added", added }, { "changed", json::array() }, { "removed", json::array() } }.dump(); } // Test/PoC helper: perform a genuine local text insert (the same model mutation a // PL_DRAWING_TOOLS::PlaceItem(DS_TEXT) UI click produces — 0002 §text-insert path) and // fire OnModify, so the differ emits an `added` delta. Lets a two-tab demo / e2e create // a deterministic local edit in one tab and observe it propagate, without canvas UI // automation. Returns the new item's uuid. std::string kicadCollabTestAddText( std::string aText, double aX, double aY ) { DS_DATA_MODEL& model = DS_DATA_MODEL::GetTheInstance(); auto* item = new DS_DATA_ITEM_TEXT( wxString::FromUTF8( aText.c_str() ) ); item->m_Pos.m_Pos.x = aX; item->m_Pos.m_Pos.y = aY; item->m_Pos.m_Anchor = LT_CORNER; model.Append( item ); if( EDA_DRAW_FRAME* fr = topFrame() ) { fr->OnModify(); // -> kicadCollabOnModify -> emit added fr->HardRedraw(); // show it locally } return toUtf8( item->m_Uuid.AsString() ); } EMSCRIPTEN_BINDINGS(pl_editor) { // Programmatic file open (preferred over UI automation from the web app). function("kicadOpenFile", &kicadOpenFile); function("kicadSaveDrawingSheet", &kicadSaveDrawingSheet); // Yjs collaborative bridge entry points. function("kicadCollabApply", &kicadCollabApply); function("kicadCollabSnapshot", &kicadCollabSnapshot); function("kicadCollabTestAddText", &kicadCollabTestAddText); } #endif