From 2d538d3d692191ae06be12ece7951d97b4833dcc Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Sat, 7 Jun 2025 11:20:54 -0700 Subject: [PATCH] ADDED: Remote symbol placement Allows activating a display panel that shows a webpage designed to provide instances of symbols, footprints, 3d models and spice models to the designer. The webpage can be installed via PCM or directly in the panel. Webpages should be registered with the KiCad project in order to avoid a warning when showing content. --- common/CMakeLists.txt | 1 + common/eda_base_frame.cpp | 1 + common/libraries/library_manager.cpp | 143 +- common/remote_login_server.cpp | 233 ++ common/widgets/lib_tree.cpp | 2 +- common/widgets/webview_panel.cpp | 101 +- eeschema/CMakeLists.txt | 10 +- .../dialogs/dialog_remote_symbol_config.cpp | 267 ++ .../dialogs/dialog_remote_symbol_config.h | 66 + eeschema/dialogs/panel_sch_data_sources.cpp | 150 ++ eeschema/dialogs/panel_sch_data_sources.h | 58 + eeschema/eeschema.cpp | 15 + eeschema/eeschema_config.cpp | 14 + eeschema/eeschema_settings.cpp | 69 + eeschema/eeschema_settings.h | 26 +- eeschema/libraries/symbol_library_adapter.cpp | 58 +- eeschema/menubar.cpp | 9 +- eeschema/sch_edit_frame.cpp | 65 +- eeschema/sch_edit_frame.h | 6 + eeschema/tools/sch_actions.cpp | 7 + eeschema/tools/sch_actions.h | 1 + eeschema/tools/sch_editor_control.cpp | 8 + eeschema/tools/sch_editor_control.h | 1 + eeschema/widgets/panel_remote_symbol.cpp | 2322 +++++++++++++++++ eeschema/widgets/panel_remote_symbol.h | 148 ++ include/eda_draw_frame.h | 2 + include/frame_type.h | 1 + include/libraries/library_manager.h | 11 + include/remote_login_server.h | 57 + include/widgets/webview_panel.h | 21 +- kicad/pcm/dialogs/dialog_pcm.cpp | 14 + kicad/pcm/dialogs/dialog_pcm.h | 2 + kicad/pcm/pcm_data.h | 2 + kicad/pcm/schemas/pcm.v1.schema.json | 1 + qa/data/config/9.99/eeschema.json | 4 + ...-symbol-server-rpc-v1-combined.schema.json | 377 +++ .../data/3dmodels/R_0603_1608Metric.step | 1049 ++++++++ .../footprints/R_0603_1608Metric.kicad_mod | 203 ++ .../server/data/images/capacitor.svg | 8 + .../server/data/images/resistor.svg | 7 + utils/webview_test/server/data/parts.json | 18 + .../server/data/spice/capacitor.cir | 4 + .../server/data/spice/resistor.cir | 4 + .../server/data/symbols/c.kicad_sym | 145 + .../server/data/symbols/r.kicad_sym | 130 + .../webview_test/server/package/metadata.json | 30 + .../package/resources/remote_symbol.json | 5 + utils/webview_test/server/package/server.zip | Bin 0 -> 1042 bytes .../server/webview_test_server.py | 1021 ++++++++ 49 files changed, 6822 insertions(+), 75 deletions(-) create mode 100644 common/remote_login_server.cpp create mode 100644 eeschema/dialogs/dialog_remote_symbol_config.cpp create mode 100644 eeschema/dialogs/dialog_remote_symbol_config.h create mode 100644 eeschema/dialogs/panel_sch_data_sources.cpp create mode 100644 eeschema/dialogs/panel_sch_data_sources.h create mode 100644 eeschema/widgets/panel_remote_symbol.cpp create mode 100644 eeschema/widgets/panel_remote_symbol.h create mode 100644 include/remote_login_server.h create mode 100644 utils/webview_test/kicad-symbol-server-rpc-v1-combined.schema.json create mode 100644 utils/webview_test/server/data/3dmodels/R_0603_1608Metric.step create mode 100644 utils/webview_test/server/data/footprints/R_0603_1608Metric.kicad_mod create mode 100644 utils/webview_test/server/data/images/capacitor.svg create mode 100644 utils/webview_test/server/data/images/resistor.svg create mode 100644 utils/webview_test/server/data/parts.json create mode 100644 utils/webview_test/server/data/spice/capacitor.cir create mode 100644 utils/webview_test/server/data/spice/resistor.cir create mode 100644 utils/webview_test/server/data/symbols/c.kicad_sym create mode 100644 utils/webview_test/server/data/symbols/r.kicad_sym create mode 100644 utils/webview_test/server/package/metadata.json create mode 100644 utils/webview_test/server/package/resources/remote_symbol.json create mode 100644 utils/webview_test/server/package/server.zip create mode 100644 utils/webview_test/server/webview_test_server.py diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 5f460c8721..a9fef2425e 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -720,6 +720,7 @@ set( COMMON_SRCS refdes_utils.cpp reference_image.cpp render_settings.cpp + remote_login_server.cpp status_popup.cpp stroke_params.cpp template_fieldnames.cpp diff --git a/common/eda_base_frame.cpp b/common/eda_base_frame.cpp index ca3d2381a1..37bd3131c8 100644 --- a/common/eda_base_frame.cpp +++ b/common/eda_base_frame.cpp @@ -1372,6 +1372,7 @@ void EDA_BASE_FRAME::ShowPreferences( wxString aStartPage, wxString aStartParent book->AddLazySubPage( LAZY_CTOR( PANEL_SCH_TOOLBARS ), _( "Toolbars" ) ); book->AddLazySubPage( LAZY_CTOR( PANEL_SCH_FIELD_NAME_TEMPLATES ), _( "Field Name Templates" ) ); + book->AddLazySubPage( LAZY_CTOR( PANEL_SCH_DATA_SOURCES ), _( "Data Sources" ) ); book->AddLazySubPage( LAZY_CTOR( PANEL_SCH_SIMULATOR ), _( "Simulator" ) ); } } diff --git a/common/libraries/library_manager.cpp b/common/libraries/library_manager.cpp index c7d4d51672..7ab9def604 100644 --- a/common/libraries/library_manager.cpp +++ b/common/libraries/library_manager.cpp @@ -656,6 +656,14 @@ std::optional LIBRARY_MANAGER::FindRowByURI( LIBRARY_TABLE_T } +void LIBRARY_MANAGER::ReloadLibraryEntry( LIBRARY_TABLE_TYPE aType, const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope ) +{ + if( std::optional adapter = Adapter( aType ); adapter ) + ( *adapter )->ReloadLibraryEntry( aNickname, aScope ); +} + + void LIBRARY_MANAGER::LoadProjectTables( const wxString& aProjectPath ) { if( wxFileName::IsDirReadable( aProjectPath ) ) @@ -810,7 +818,7 @@ void LIBRARY_MANAGER_ADAPTER::CheckTableRow( LIBRARY_TABLE_ROW& aRow ) aRow.SetErrorDescription( plugin.error().message ); } } - + LIBRARY_TABLE* LIBRARY_MANAGER_ADAPTER::GlobalTable() const @@ -1003,6 +1011,59 @@ std::vector> LIBRARY_MANAGER_ADAPTER::GetLibrary } +void LIBRARY_MANAGER_ADAPTER::ReloadLibraryEntry( const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope ) +{ + auto reloadScope = + [&]( LIBRARY_TABLE_SCOPE aScopeToReload, std::map& aTarget, + std::mutex& aMutex ) + { + bool wasLoaded = false; + + { + std::lock_guard lock( aMutex ); + auto it = aTarget.find( aNickname ); + + if( it != aTarget.end() && it->second.plugin ) + { + wasLoaded = true; + aTarget.erase( it ); + } + } + + if( wasLoaded ) + { + if( LIBRARY_RESULT result = + loadFromScope( aNickname, aScopeToReload, aTarget, aMutex ); + !result.has_value() ) + { + wxLogTrace( traceLibraries, + "ReloadLibraryEntry: failed to reload %s (%s): %s", + aNickname, magic_enum::enum_name( aScopeToReload ), + result.error().message ); + } + } + }; + + switch( aScope ) + { + case LIBRARY_TABLE_SCOPE::GLOBAL: + reloadScope( LIBRARY_TABLE_SCOPE::GLOBAL, globalLibs(), globalLibsMutex() ); + break; + + case LIBRARY_TABLE_SCOPE::PROJECT: + reloadScope( LIBRARY_TABLE_SCOPE::PROJECT, m_libraries, m_libraries_mutex ); + break; + + case LIBRARY_TABLE_SCOPE::BOTH: + case LIBRARY_TABLE_SCOPE::UNINITIALIZED: + reloadScope( LIBRARY_TABLE_SCOPE::PROJECT, m_libraries, m_libraries_mutex ); + reloadScope( LIBRARY_TABLE_SCOPE::GLOBAL, globalLibs(), globalLibsMutex() ); + break; + } +} + + bool LIBRARY_MANAGER_ADAPTER::IsWritable( const wxString& aNickname ) const { if( std::optional result = fetchIfLoaded( aNickname ) ) @@ -1078,56 +1139,56 @@ std::optional LIBRARY_MANAGER_ADAPTER::fetchIfLoaded( } -LIBRARY_RESULT LIBRARY_MANAGER_ADAPTER::loadIfNeeded( const wxString& aNickname ) +LIBRARY_RESULT LIBRARY_MANAGER_ADAPTER::loadFromScope( const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope, std::map& aTarget, std::mutex& aMutex ) { - auto tryLoadFromScope = - [&]( LIBRARY_TABLE_SCOPE aScope, std::map& aTarget, - std::mutex& aMutex ) -> LIBRARY_RESULT + bool present = false; + + { + std::lock_guard lock( aMutex ); + present = aTarget.contains( aNickname ) && aTarget.at( aNickname ).plugin; + } + + if( !present ) + { + if( auto result = m_manager.GetRow( Type(), aNickname, aScope ) ) + { + const LIBRARY_TABLE_ROW* row = *result; + wxLogTrace( traceLibraries, "Library %s (%s) not yet loaded, will attempt...", + aNickname, magic_enum::enum_name( aScope ) ); + + if( LIBRARY_RESULT plugin = createPlugin( row ); plugin.has_value() ) { - bool present = false; + std::lock_guard lock( aMutex ); - { - std::lock_guard lock( aMutex ); - present = aTarget.contains( aNickname ) && aTarget.at( aNickname ).plugin; - } - - if( !present ) - { - if( auto result = m_manager.GetRow( Type(), aNickname, aScope ) ) - { - const LIBRARY_TABLE_ROW* row = *result; - wxLogTrace( traceLibraries, "Library %s (%s) not yet loaded, will attempt...", - aNickname, magic_enum::enum_name( aScope ) ); - - if( LIBRARY_RESULT plugin = createPlugin( row ); plugin.has_value() ) - { - std::lock_guard lock( aMutex ); - - aTarget[ row->Nickname() ].status.load_status = LOAD_STATUS::LOADING; - aTarget[ row->Nickname() ].row = row; - aTarget[ row->Nickname() ].plugin.reset( *plugin ); - - return &aTarget.at( aNickname ); - } - else - { - return tl::unexpected( plugin.error() ); - } - } - - return nullptr; - } + aTarget[ row->Nickname() ].status.load_status = LOAD_STATUS::LOADING; + aTarget[ row->Nickname() ].row = row; + aTarget[ row->Nickname() ].plugin.reset( *plugin ); return &aTarget.at( aNickname ); - }; + } + else + { + return tl::unexpected( plugin.error() ); + } + } - LIBRARY_RESULT result = tryLoadFromScope( LIBRARY_TABLE_SCOPE::PROJECT, m_libraries, - m_libraries_mutex ); + return nullptr; + } + + return &aTarget.at( aNickname ); +} + + +LIBRARY_RESULT LIBRARY_MANAGER_ADAPTER::loadIfNeeded( const wxString& aNickname ) +{ + LIBRARY_RESULT result = + loadFromScope( aNickname, LIBRARY_TABLE_SCOPE::PROJECT, m_libraries, m_libraries_mutex ); if( !result.has_value() || *result ) return result; - result = tryLoadFromScope( LIBRARY_TABLE_SCOPE::GLOBAL, globalLibs(), globalLibsMutex() ); + result = loadFromScope( aNickname, LIBRARY_TABLE_SCOPE::GLOBAL, globalLibs(), globalLibsMutex() ); if( !result.has_value() || *result ) return result; diff --git a/common/remote_login_server.cpp b/common/remote_login_server.cpp new file mode 100644 index 0000000000..aa02a71bf8 --- /dev/null +++ b/common/remote_login_server.cpp @@ -0,0 +1,233 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include + +#include + + +wxDEFINE_EVENT( EVT_REMOTE_SYMBOL_LOGIN_RESULT, wxCommandEvent ); + +REMOTE_LOGIN_SERVER::REMOTE_LOGIN_SERVER( wxEvtHandler* aOwner, const wxString& aRedirectUrl ) : + m_owner( aOwner ), + m_redirectUrl( aRedirectUrl ), + m_port( 0 ), + m_done( false ) +{ + m_timeout.SetOwner( this ); + Bind( wxEVT_TIMER, &REMOTE_LOGIN_SERVER::OnTimeout, this, m_timeout.GetId() ); +} + +REMOTE_LOGIN_SERVER::~REMOTE_LOGIN_SERVER() +{ + m_timeout.Stop(); + Unbind( wxEVT_SOCKET, &REMOTE_LOGIN_SERVER::OnSocketEvent, this ); + Unbind( wxEVT_TIMER, &REMOTE_LOGIN_SERVER::OnTimeout, this, m_timeout.GetId() ); + Shutdown(); + m_timeout.SetOwner( nullptr ); +} + +bool REMOTE_LOGIN_SERVER::Start() +{ + wxIPV4address addr; + addr.AnyAddress(); + addr.Service( 0 ); + + std::unique_ptr server = + std::make_unique( addr, wxSOCKET_REUSEADDR ); + + if( !server->IsOk() ) + return false; + + server->SetEventHandler( *this ); + server->SetNotify( wxSOCKET_CONNECTION_FLAG ); + server->Notify( true ); + + wxIPV4address local; + server->GetLocal( local ); + m_port = local.Service(); + + Bind( wxEVT_SOCKET, &REMOTE_LOGIN_SERVER::OnSocketEvent, this ); + + m_server = std::move( server ); + m_timeout.StartOnce( 120000 ); + return m_port != 0; +} + +void REMOTE_LOGIN_SERVER::OnSocketEvent( wxSocketEvent& aEvent ) +{ + if( !m_server || aEvent.GetSocketEvent() != wxSOCKET_CONNECTION ) + return; + + std::unique_ptr client( m_server->Accept( false ) ); + + if( !client ) + return; + + HandleClient( client.get() ); +} + +void REMOTE_LOGIN_SERVER::OnTimeout( wxTimerEvent& aEvent ) +{ + wxUnusedVar( aEvent ); + Finish( false, wxString() ); +} + +void REMOTE_LOGIN_SERVER::HandleClient( wxSocketBase* aClient ) +{ + if( !aClient ) + return; + + aClient->SetTimeout( 5 ); + aClient->SetFlags( wxSOCKET_NONE ); + + std::string request; + request.reserve( 512 ); + + char buffer[512]; + + while( aClient->IsConnected() ) + { + if( !aClient->WaitForRead( 1, 0 ) ) + break; + + aClient->Read( buffer, sizeof( buffer ) ); + size_t count = aClient->LastCount(); + + if( count == 0 ) + break; + + request.append( buffer, count ); + + if( request.find( "\r\n\r\n" ) != std::string::npos || request.size() > 4096 ) + break; + } + + SendHttpResponse( aClient ); + + wxString requestWx = wxString::FromUTF8( request.data(), request.size() ); + int endOfLine = requestWx.Find( wxS( "\r\n" ) ); + wxString requestLine = endOfLine == wxNOT_FOUND ? requestWx : requestWx.Mid( 0, endOfLine ); + + wxString userId = ExtractUserId( requestLine ); + Finish( !userId.IsEmpty(), userId ); +} + +wxString REMOTE_LOGIN_SERVER::ExtractUserId( const wxString& aRequestLine ) const +{ + wxStringTokenizer tokenizer( aRequestLine, wxS( " " ) ); + + if( !tokenizer.HasMoreTokens() ) + return wxString(); + + tokenizer.GetNextToken(); + + if( !tokenizer.HasMoreTokens() ) + return wxString(); + + wxString path = tokenizer.GetNextToken(); + int queryPos = path.Find( '?' ); + + if( queryPos == wxNOT_FOUND ) + return wxString(); + + wxString query = path.Mid( queryPos + 1 ); + wxStringTokenizer queryTokenizer( query, wxS( "&" ) ); + + while( queryTokenizer.HasMoreTokens() ) + { + wxString pair = queryTokenizer.GetNextToken(); + int eqPos = pair.Find( '=' ); + + if( eqPos == wxNOT_FOUND ) + continue; + + wxString name = pair.Left( eqPos ); + wxString value = pair.Mid( eqPos + 1 ); + + if( name == wxS( "user_id" ) ) + return wxURI::Unescape( value ); + } + + return wxString(); +} + +void REMOTE_LOGIN_SERVER::SendHttpResponse( wxSocketBase* aClient ) +{ + if( !aClient ) + return; + + wxString redirect = m_redirectUrl; + + if( redirect.IsEmpty() ) + redirect = wxS( "about:blank" ); + + wxString html; + html << wxS( "" ) + << wxS( "" ) + << wxS( "" ) + << wxS( "" ) + << wxS( "

Login successful. Redirecting...

" ) + << wxS( "" ); + + wxScopedCharBuffer body = html.ToUTF8(); + + wxString response; + response << wxS( "HTTP/1.1 200 OK\r\n" ) + << wxS( "Content-Type: text/html; charset=utf-8\r\n" ) + << wxS( "Access-Control-Allow-Origin: *\r\n" ) + << wxS( "Cache-Control: no-store\r\n" ) + << wxS( "Connection: close\r\n" ) + << wxS( "Content-Length: " ) << body.length() << wxS( "\r\n\r\n" ); + + wxScopedCharBuffer header = response.ToUTF8(); + + aClient->Write( header.data(), header.length() ); + aClient->Write( body.data(), body.length() ); + aClient->Close(); +} + +void REMOTE_LOGIN_SERVER::Finish( bool aSuccess, const wxString& aUserId ) +{ + if( m_done ) + return; + + m_done = true; + m_timeout.Stop(); + + wxCommandEvent evt( EVT_REMOTE_SYMBOL_LOGIN_RESULT ); + evt.SetInt( aSuccess ? 1 : 0 ); + evt.SetString( aUserId ); + wxQueueEvent( m_owner, evt.Clone() ); + + Shutdown(); +} + +void REMOTE_LOGIN_SERVER::Shutdown() +{ + if( m_server ) + { + m_server->Notify( false ); + m_server.reset(); + } +} \ No newline at end of file diff --git a/common/widgets/lib_tree.cpp b/common/widgets/lib_tree.cpp index 62f4162f5d..882cd2d9e1 100644 --- a/common/widgets/lib_tree.cpp +++ b/common/widgets/lib_tree.cpp @@ -222,7 +222,7 @@ LIB_TREE::LIB_TREE( wxWindow* aParent, const wxString& aRecentSearchesKey, { m_query_ctrl->SetDescriptiveText( _( "Filter" ) ); m_query_ctrl->SetFocus(); - m_query_ctrl->SetValue( wxEmptyString ); + m_query_ctrl->ChangeValue( wxEmptyString ); updateRecentSearchMenu(); // Force an update of the adapter with the empty text to ensure preselect is done diff --git a/common/widgets/webview_panel.cpp b/common/widgets/webview_panel.cpp index b2992e05bd..88cd16aede 100644 --- a/common/widgets/webview_panel.cpp +++ b/common/widgets/webview_panel.cpp @@ -18,6 +18,10 @@ */ #include + +#include +#include + #include #include #include @@ -25,11 +29,15 @@ #include #include -WEBVIEW_PANEL::WEBVIEW_PANEL( wxWindow* aParent, wxWindowID aId, const wxPoint& aPos, - const wxSize& aSize, const int aStyle ) - : wxPanel( aParent, aId, aPos, aSize, aStyle ), - m_initialized( false ), - m_browser( wxWebView::New() ) +WEBVIEW_PANEL::WEBVIEW_PANEL( wxWindow* aParent, wxWindowID aId, const wxPoint& aPos, const wxSize& aSize, + const int aStyle, TOOL_MANAGER* aToolManager, TOOL_BASE* aTool ) : + wxPanel( aParent, aId, aPos, aSize, aStyle ), + m_initialized( false ), + m_handleExternalLinks( false ), + m_loadError( false ), + m_browser( wxWebView::New() ), + m_toolManager( aToolManager ), + m_tool( aTool ) { wxBoxSizer* sizer = new wxBoxSizer( wxVERTICAL ); @@ -66,6 +74,8 @@ WEBVIEW_PANEL::~WEBVIEW_PANEL() void WEBVIEW_PANEL::LoadURL( const wxString& aURL ) { + wxLogTrace( "webview", "Loading URL: %s", aURL ); + if( aURL.starts_with( "file:/" ) && !aURL.starts_with( "file:///" ) ) { wxString new_url = wxString( "file:///" ) + aURL.AfterFirst( '/' ); @@ -84,26 +94,52 @@ void WEBVIEW_PANEL::LoadURL( const wxString& aURL ) void WEBVIEW_PANEL::SetPage( const wxString& aHtmlContent ) { + wxLogTrace( "webview", "Setting page content" ); m_browser->SetPage( aHtmlContent, "file://" ); } bool WEBVIEW_PANEL::AddMessageHandler( const wxString& aName, MESSAGE_HANDLER aHandler ) { - m_msgHandlers.emplace( aName, std::move(aHandler) ); + wxLogTrace( "webview", "Adding message handler for: %s", aName ); + auto it = m_msgHandlers.find( aName ); + + if( it != m_msgHandlers.end() ) + { + it->second = std::move( aHandler ); + return true; + } + + m_msgHandlers.emplace( aName, std::move( aHandler ) ); + + if( m_initialized ) + { + if( !m_browser->AddScriptMessageHandler( aName ) ) + wxLogDebug( "Could not add script message handler %s", aName ); + } + return true; } void WEBVIEW_PANEL::ClearMessageHandlers() { + wxLogTrace( "webview", "Clearing all message handlers" ); + + for( const auto& handler : m_msgHandlers ) + m_browser->RemoveScriptMessageHandler( handler.first ); + m_msgHandlers.clear(); } void WEBVIEW_PANEL::OnNavigationRequest( wxWebViewEvent& aEvt ) { + m_loadError = false; + wxLogTrace( "webview", "Navigation request to URL: %s", aEvt.GetURL() ); // Default behavior: open external links in the system browser bool isExternal = aEvt.GetURL().StartsWith( "http://" ) || aEvt.GetURL().StartsWith( "https://" ); - if( isExternal ) + + if( isExternal && !m_handleExternalLinks ) { + wxLogTrace( "webview", "Opening external URL in system browser: %s", aEvt.GetURL() ); wxLaunchDefaultBrowser( aEvt.GetURL() ); aEvt.Veto(); } @@ -114,23 +150,17 @@ void WEBVIEW_PANEL::OnWebViewLoaded( wxWebViewEvent& aEvt ) if( !m_initialized ) { // Defer handler registration to avoid running during modal dialog/yield - CallAfter([this]() { - static bool handler_added_inner = false; - if (!handler_added_inner) { - - for( const auto& handler : m_msgHandlers ) + auto initFunc = [this]() { + for( const auto& handler : m_msgHandlers ) + { + if( !m_browser->AddScriptMessageHandler( handler.first ) ) { - if( !m_browser->AddScriptMessageHandler( handler.first ) ) - { - wxLogDebug( "Could not add script message handler %s", handler.first ); - } + wxLogDebug( "Could not add script message handler %s", handler.first ); } - - handler_added_inner = true; } - // Inject navigation hook for SPA/JS navigation to prevent webkit crashing without new window - m_browser->AddUserScript(R"( + // Inject navigation hook for SPA/JS navigation to prevent webkit crashing without new window + m_browser->AddUserScript( R"( (function() { // Change window.open to navigate in the same window window.open = function(url) { if (url) window.location.href = url; return null; }; @@ -153,11 +183,19 @@ void WEBVIEW_PANEL::OnWebViewLoaded( wxWebViewEvent& aEvt ) }); } })(); - )"); - }); + )" ); + + }; + + if( m_toolManager && m_tool ) + m_toolManager->RunMainStack( m_tool, initFunc ); + else + CallAfter( initFunc ); m_initialized = true; } + + aEvt.Skip(); } void WEBVIEW_PANEL::OnNewWindow( wxWebViewEvent& aEvt ) @@ -173,22 +211,30 @@ void WEBVIEW_PANEL::OnNewWindow( wxWebViewEvent& aEvt ) void WEBVIEW_PANEL::OnScriptMessage( wxWebViewEvent& aEvt ) { wxLogTrace( "webview", "Script message received: %s for handler %s", aEvt.GetString(), aEvt.GetMessageHandler() ); + wxString handler = aEvt.GetMessageHandler(); + handler.Trim(true).Trim(false); - if( aEvt.GetMessageHandler().IsEmpty() ) + if( handler.IsEmpty() ) { - wxLogDebug( "No message handler specified for script message: %s", aEvt.GetString() ); + for( auto handlerPair : m_msgHandlers ) + { + wxLogTrace( "webview", "No handler specified, trying: %s", handlerPair.first ); + handlerPair.second( aEvt.GetString() ); + } + return; } - auto it = m_msgHandlers.find( aEvt.GetMessageHandler() ); + auto it = m_msgHandlers.find( handler ); + if( it == m_msgHandlers.end() ) { - wxLogDebug( "No handler registered for message: %s", aEvt.GetMessageHandler() ); + wxLogDebug( "No handler registered for message: %s", handler ); return; } // Call the registered handler with the message - wxLogTrace( "webview", "Calling handler for message: %s", aEvt.GetMessageHandler() ); + wxLogTrace( "webview", "Calling handler for message: %s", handler ); it->second( aEvt.GetString() ); } @@ -200,5 +246,6 @@ void WEBVIEW_PANEL::OnScriptResult( wxWebViewEvent& aEvt ) void WEBVIEW_PANEL::OnError( wxWebViewEvent& aEvt ) { + m_loadError = true; wxLogDebug( "WebView error: %s", aEvt.GetString() ); } diff --git a/eeschema/CMakeLists.txt b/eeschema/CMakeLists.txt index 1be2c291d1..644d5e83a4 100644 --- a/eeschema/CMakeLists.txt +++ b/eeschema/CMakeLists.txt @@ -35,6 +35,7 @@ include_directories( ${CMAKE_SOURCE_DIR}/libs/sexpr/include ${INC_AFTER} ./dialogs + ${CMAKE_SOURCE_DIR}/kicad/pcm ./libview ./symbol_editor ./tools @@ -135,6 +136,7 @@ set( EESCHEMA_DLGS dialogs/dialog_pin_properties_base.cpp dialogs/dialog_plot_schematic.cpp dialogs/dialog_plot_schematic_base.cpp + dialogs/dialog_remote_symbol_config.cpp dialogs/dialog_erc_job_config.cpp dialogs/dialog_rescue_each.cpp dialogs/dialog_rescue_each_base.cpp @@ -185,6 +187,7 @@ set( EESCHEMA_DLGS dialogs/panel_setup_pinmap_base.cpp dialogs/panel_simulator_preferences.cpp dialogs/panel_simulator_preferences_base.cpp + dialogs/panel_sch_data_sources.cpp dialogs/panel_sym_color_settings.cpp dialogs/panel_sym_color_settings_base.cpp dialogs/panel_sym_display_options.cpp @@ -281,6 +284,7 @@ set( EESCHEMA_WIDGETS widgets/panel_sch_selection_filter_base.cpp widgets/panel_sch_selection_filter.cpp widgets/panel_symbol_chooser.cpp + widgets/panel_remote_symbol.cpp widgets/pinshape_combobox.cpp widgets/pintype_combobox.cpp widgets/symbol_diff_widget.cpp @@ -567,6 +571,7 @@ set_source_files_properties( ${CMAKE_SOURCE_DIR}/common/single_top.cpp PROPERTIE target_link_libraries( eeschema kicommon + pcm ${wxWidgets_LIBRARIES} ) @@ -584,6 +589,7 @@ if( KICAD_USE_PCH ) + @@ -599,7 +605,8 @@ target_include_directories( eeschema_kiface_objects target_link_libraries( eeschema_kiface_objects PUBLIC - common ) + common + pcm ) # Since we're not using target_link_libraries, we need to explicitly # declare the dependency @@ -620,6 +627,7 @@ target_link_libraries( eeschema_kiface PRIVATE common eeschema_kiface_objects + pcm markdown_lib scripting sexpr diff --git a/eeschema/dialogs/dialog_remote_symbol_config.cpp b/eeschema/dialogs/dialog_remote_symbol_config.cpp new file mode 100644 index 0000000000..e243e0b65e --- /dev/null +++ b/eeschema/dialogs/dialog_remote_symbol_config.cpp @@ -0,0 +1,267 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "dialog_remote_symbol_config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +DIALOG_REMOTE_SYMBOL_CONFIG::DIALOG_REMOTE_SYMBOL_CONFIG( wxWindow* aParent ) : + DIALOG_SHIM( aParent, wxID_ANY, _( "Remote Symbol Settings" ), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER ), + m_destinationCtrl( nullptr ), + m_prefixCtrl( nullptr ), + m_prefixHint( nullptr ), + m_projectRadio( nullptr ), + m_globalRadio( nullptr ), + m_resetButton( nullptr ), + m_browseButton( nullptr ), + m_settings( GetAppSettings( "eeschema" ) ) +{ + wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL ); + + wxStaticText* intro = new wxStaticText( this, wxID_ANY, + _( "Configure where downloaded remote libraries are stored and how they are named." ) ); + intro->Wrap( FromDIP( 420 ) ); + topSizer->Add( intro, 0, wxALL | wxEXPAND, FromDIP( 10 ) ); + + wxFlexGridSizer* gridSizer = new wxFlexGridSizer( 2, FromDIP( 6 ), FromDIP( 6 ) ); + gridSizer->AddGrowableCol( 1, 1 ); + + gridSizer->Add( new wxStaticText( this, wxID_ANY, _( "Destination directory:" ) ), + 0, wxALIGN_CENTER_VERTICAL ); + + wxBoxSizer* destSizer = new wxBoxSizer( wxHORIZONTAL ); + m_destinationCtrl = new wxTextCtrl( this, wxID_ANY ); + m_destinationCtrl->SetMinSize( FromDIP( wxSize( 320, -1 ) ) ); + m_destinationCtrl->SetToolTip( _( "Directory where downloaded symbol, footprint, and 3D model data will be written." ) ); + destSizer->Add( m_destinationCtrl, 1, wxALIGN_CENTER_VERTICAL ); + + m_browseButton = new wxButton( this, wxID_ANY, _( "Browse…" ) ); + destSizer->Add( m_browseButton, 0, wxLEFT, FromDIP( 4 ) ); + gridSizer->Add( destSizer, 1, wxEXPAND ); + + gridSizer->Add( new wxStaticText( this, wxID_ANY, _( "Library prefix:" ) ), + 0, wxALIGN_CENTER_VERTICAL ); + m_prefixCtrl = new wxTextCtrl( this, wxID_ANY ); + m_prefixCtrl->SetToolTip( _( "Prefix that will be applied to the generated libraries." ) ); + gridSizer->Add( m_prefixCtrl, 0, wxEXPAND ); + + gridSizer->AddSpacer( 0 ); + m_prefixHint = new wxStaticText( this, wxID_ANY, wxString() ); + m_prefixHint->SetFont( KIUI::GetSmallInfoFont( this ).Italic() ); + gridSizer->Add( m_prefixHint, 0, wxEXPAND ); + + gridSizer->Add( new wxStaticText( this, wxID_ANY, _( "Add libraries to:" ) ), + 0, wxALIGN_CENTER_VERTICAL ); + + wxBoxSizer* radioSizer = new wxBoxSizer( wxHORIZONTAL ); + m_projectRadio = new wxRadioButton( this, wxID_ANY, _( "Project library table" ), + wxDefaultPosition, wxDefaultSize, wxRB_GROUP ); + m_projectRadio->SetToolTip( _( "Adds the generated libraries to the project's library tables." ) ); + radioSizer->Add( m_projectRadio, 0, wxRIGHT | wxALIGN_CENTER_VERTICAL, FromDIP( 12 ) ); + + m_globalRadio = new wxRadioButton( this, wxID_ANY, _( "Global library table" ) ); + m_globalRadio->SetToolTip( _( "Adds the generated libraries to the global library tables." ) ); + radioSizer->Add( m_globalRadio, 0, wxALIGN_CENTER_VERTICAL ); + + gridSizer->Add( radioSizer, 0, wxEXPAND ); + + topSizer->Add( gridSizer, 0, wxALL | wxEXPAND, FromDIP( 10 ) ); + + m_resetButton = new wxButton( this, wxID_ANY, _( "Reset to Defaults" ) ); + m_resetButton->SetToolTip( _( "Restore the default destination and prefix." ) ); + topSizer->Add( m_resetButton, 0, wxLEFT | wxBOTTOM, FromDIP( 10 ) ); + + wxStdDialogButtonSizer* buttonSizer = CreateStdDialogButtonSizer( wxOK | wxCANCEL ); + topSizer->Add( buttonSizer, 0, wxALL | wxEXPAND, FromDIP( 10 ) ); + + SetSizer( topSizer ); + topSizer->Fit( this ); + + SetupStandardButtons(); + finishDialogSettings(); + + SetInitialFocus( m_destinationCtrl ); + + m_browseButton->Bind( wxEVT_BUTTON, &DIALOG_REMOTE_SYMBOL_CONFIG::onBrowseDestination, this ); + m_resetButton->Bind( wxEVT_BUTTON, &DIALOG_REMOTE_SYMBOL_CONFIG::onResetDefaults, this ); + m_prefixCtrl->Bind( wxEVT_TEXT, &DIALOG_REMOTE_SYMBOL_CONFIG::onPrefixChanged, this ); +} + + +bool DIALOG_REMOTE_SYMBOL_CONFIG::TransferDataToWindow() +{ + if( m_settings ) + applyRemoteSettings( m_settings->m_RemoteSymbol ); + else + { + EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG defaults; + applyRemoteSettings( defaults ); + } + + return true; +} + + +bool DIALOG_REMOTE_SYMBOL_CONFIG::TransferDataFromWindow() +{ + wxString destination = m_destinationCtrl->GetValue(); + wxString prefix = m_prefixCtrl->GetValue(); + + destination.Trim( true ).Trim( false ); + prefix.Trim( true ).Trim( false ); + + if( destination.IsEmpty() ) + { + wxMessageBox( _( "Please choose a destination directory." ), GetTitle(), wxOK | wxICON_WARNING, this ); + m_destinationCtrl->SetFocus(); + return false; + } + + if( prefix.IsEmpty() ) + { + wxMessageBox( _( "Please enter a library prefix." ), GetTitle(), wxOK | wxICON_WARNING, this ); + m_prefixCtrl->SetFocus(); + return false; + } + + if( !m_settings ) + m_settings = GetAppSettings( "eeschema" ); + + if( !m_settings ) + return true; + + m_settings->m_RemoteSymbol.destination_dir = destination; + m_settings->m_RemoteSymbol.library_prefix = prefix; + m_settings->m_RemoteSymbol.add_to_global_table = m_globalRadio->GetValue(); + + return true; +} + + +void DIALOG_REMOTE_SYMBOL_CONFIG::onBrowseDestination( wxCommandEvent& aEvent ) +{ + wxUnusedVar( aEvent ); + + wxString initialPath = ExpandEnvVarSubstitutions( m_destinationCtrl->GetValue(), &Prj() ); + + if( initialPath.IsEmpty() ) + { + if( Prj().IsNullProject() ) + initialPath = KIPLATFORM::ENV::GetDocumentsPath(); + else + initialPath = Prj().GetProjectPath(); + } + + wxDirDialog dlg( this, _( "Select Destination Directory" ), initialPath ); + + if( dlg.ShowModal() != wxID_OK ) + return; + + wxString path = dlg.GetPath(); + + if( !Prj().IsNullProject() ) + { + wxString projectRoot = Prj().GetProjectPath(); + + wxFileName selectedDir = wxFileName::DirName( path ); + + if( selectedDir.MakeRelativeTo( projectRoot ) ) + { + wxString relative = selectedDir.GetFullPath(); + + if( relative.IsEmpty() || relative == wxS( "." ) ) + path = wxS( "${KIPRJMOD}" ); + else + path = wxString::Format( wxS( "${KIPRJMOD}/%s" ), relative ); + } + } + + path.Replace( wxS( "\\" ), wxS( "/" ) ); + m_destinationCtrl->ChangeValue( path ); +} + + +void DIALOG_REMOTE_SYMBOL_CONFIG::onResetDefaults( wxCommandEvent& aEvent ) +{ + wxUnusedVar( aEvent ); + + EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG defaults; + applyRemoteSettings( defaults ); +} + + +void DIALOG_REMOTE_SYMBOL_CONFIG::onPrefixChanged( wxCommandEvent& aEvent ) +{ + wxUnusedVar( aEvent ); + updatePrefixHint(); +} + + +void DIALOG_REMOTE_SYMBOL_CONFIG::applyRemoteSettings( + const EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG& aConfig ) +{ + m_destinationCtrl->ChangeValue( aConfig.destination_dir ); + m_prefixCtrl->ChangeValue( aConfig.library_prefix ); + + if( aConfig.add_to_global_table ) + m_globalRadio->SetValue( true ); + else + m_projectRadio->SetValue( true ); + + updatePrefixHint(); +} + + +void DIALOG_REMOTE_SYMBOL_CONFIG::updatePrefixHint() +{ + const wxString prefix = m_prefixCtrl->GetValue(); + + if( prefix.IsEmpty() ) + { + m_prefixHint->SetLabel( + _( "Library names will be created with suffixes such as _symbols, _fp, and _3d." ) ); + } + else + { + m_prefixHint->SetLabel( wxString::Format( + _( "Will create libraries like %1$s_symbols, %1$s_fp, %1$s_3d, etc." ), prefix ) ); + } + + m_prefixHint->Wrap( FromDIP( 360 ) ); +} diff --git a/eeschema/dialogs/dialog_remote_symbol_config.h b/eeschema/dialogs/dialog_remote_symbol_config.h new file mode 100644 index 0000000000..3cc0b66248 --- /dev/null +++ b/eeschema/dialogs/dialog_remote_symbol_config.h @@ -0,0 +1,66 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef DIALOG_REMOTE_SYMBOL_CONFIG_H +#define DIALOG_REMOTE_SYMBOL_CONFIG_H + +#include +#include + +class wxTextCtrl; +class wxStaticText; +class wxRadioButton; +class wxButton; + +class EESCHEMA_SETTINGS; + +class DIALOG_REMOTE_SYMBOL_CONFIG : public DIALOG_SHIM +{ +public: + explicit DIALOG_REMOTE_SYMBOL_CONFIG( wxWindow* aParent ); + + bool TransferDataToWindow() override; + bool TransferDataFromWindow() override; + +private: + void onBrowseDestination( wxCommandEvent& aEvent ); + void onResetDefaults( wxCommandEvent& aEvent ); + void onPrefixChanged( wxCommandEvent& aEvent ); + + void applyRemoteSettings( const EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG& aConfig ); + void updatePrefixHint(); + +private: + wxTextCtrl* m_destinationCtrl; + wxTextCtrl* m_prefixCtrl; + wxStaticText* m_prefixHint; + wxRadioButton* m_projectRadio; + wxRadioButton* m_globalRadio; + wxButton* m_resetButton; + wxButton* m_browseButton; + + EESCHEMA_SETTINGS* m_settings; +}; + + +#endif // DIALOG_REMOTE_SYMBOL_CONFIG_H diff --git a/eeschema/dialogs/panel_sch_data_sources.cpp b/eeschema/dialogs/panel_sch_data_sources.cpp new file mode 100644 index 0000000000..d8d80449f8 --- /dev/null +++ b/eeschema/dialogs/panel_sch_data_sources.cpp @@ -0,0 +1,150 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include +#include "panel_sch_data_sources.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +PANEL_SCH_DATA_SOURCES::PANEL_SCH_DATA_SOURCES( wxWindow* aParent, EDA_BASE_FRAME* aFrame ) : + RESETTABLE_PANEL( aParent ), + m_frame( aFrame ), + m_pcm( std::make_shared( []( int ) {} ) ), + m_description( nullptr ), + m_status( nullptr ), + m_sourcesList( nullptr ), + m_manageButton( nullptr ) +{ + wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL ); + + m_description = new wxStaticText( this, wxID_ANY, + _( "Install schematic data sources from the Plugin and Content Manager. Data sources extend KiCad by linking schematic items to external data providers." ) ); + m_description->Wrap( FromDIP( 480 ) ); + m_description->SetFont( KIUI::GetInfoFont( this ) ); + topSizer->Add( m_description, 0, wxBOTTOM | wxEXPAND, FromDIP( 12 ) ); + + m_sourcesList = new wxListBox( this, wxID_ANY ); + m_sourcesList->SetMinSize( FromDIP( wxSize( -1, 160 ) ) ); + topSizer->Add( m_sourcesList, 1, wxBOTTOM | wxEXPAND, FromDIP( 12 ) ); + + m_status = new wxStaticText( this, wxID_ANY, wxEmptyString ); + m_status->SetFont( KIUI::GetSmallInfoFont( this ).Italic() ); + topSizer->Add( m_status, 0, wxBOTTOM | wxEXPAND, FromDIP( 12 ) ); + + m_manageButton = new wxButton( this, wxID_ANY, _( "Manage Data Sources..." ) ); + topSizer->Add( m_manageButton, 0, wxALIGN_RIGHT ); + + SetSizer( topSizer ); + + m_manageButton->Bind( wxEVT_BUTTON, &PANEL_SCH_DATA_SOURCES::OnManageDataSources, this ); +} + + +bool PANEL_SCH_DATA_SOURCES::TransferDataToWindow() +{ + if( KICAD_SETTINGS* cfg = GetAppSettings( "kicad" ) ) + m_pcm->SetRepositoryList( cfg->m_PcmRepositories ); + + populateInstalledSources(); + + return true; +} + + +bool PANEL_SCH_DATA_SOURCES::TransferDataFromWindow() +{ + return true; +} + + +void PANEL_SCH_DATA_SOURCES::ResetPanel() +{ + populateInstalledSources(); +} + + +void PANEL_SCH_DATA_SOURCES::populateInstalledSources() +{ + m_sourcesList->Clear(); + + std::vector entries; + + for( const PCM_INSTALLATION_ENTRY& entry : m_pcm->GetInstalledPackages() ) + { + PCM_PACKAGE_TYPE type = entry.package.category && entry.package.category.value() == PC_FAB + ? PT_FAB + : entry.package.type; + + if( type != PT_DATASOURCE ) + continue; + + wxString label = entry.package.name; + + if( !entry.current_version.IsEmpty() ) + label << wxS( " (" ) << entry.current_version << wxS( ")" ); + + if( !entry.repository_name.IsEmpty() ) + label << wxS( " — " ) << entry.repository_name; + + entries.push_back( label ); + } + + if( entries.empty() ) + { + m_status->SetLabel( _( "No data sources are currently installed." ) ); + return; + } + + std::sort( entries.begin(), entries.end(), + []( const wxString& a, const wxString& b ) + { + return a.CmpNoCase( b ) < 0; + } ); + + for( const wxString& label : entries ) + m_sourcesList->Append( label ); + + m_status->SetLabel( _( "Installed data sources are listed above." ) ); +} + + +void PANEL_SCH_DATA_SOURCES::OnManageDataSources( wxCommandEvent& aEvent ) +{ + EDA_BASE_FRAME* parentFrame = m_frame ? m_frame + : dynamic_cast( wxGetTopLevelParent( this ) ); + + DIALOG_PCM dialog( parentFrame, m_pcm ); + dialog.SetActivePackageType( PT_DATASOURCE ); + dialog.ShowModal(); + + populateInstalledSources(); +} diff --git a/eeschema/dialogs/panel_sch_data_sources.h b/eeschema/dialogs/panel_sch_data_sources.h new file mode 100644 index 0000000000..5d044062a5 --- /dev/null +++ b/eeschema/dialogs/panel_sch_data_sources.h @@ -0,0 +1,58 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef PANEL_SCH_DATA_SOURCES_H +#define PANEL_SCH_DATA_SOURCES_H + +#include + +#include +#include + +class wxButton; +class wxListBox; +class wxStaticText; +class EDA_BASE_FRAME; + + +class PANEL_SCH_DATA_SOURCES : public RESETTABLE_PANEL +{ +public: + PANEL_SCH_DATA_SOURCES( wxWindow* aParent, EDA_BASE_FRAME* aFrame ); + + bool TransferDataToWindow() override; + bool TransferDataFromWindow() override; + + void ResetPanel() override; + +private: + void populateInstalledSources(); + void OnManageDataSources( wxCommandEvent& aEvent ); + +private: + EDA_BASE_FRAME* m_frame; + std::shared_ptr m_pcm; + wxStaticText* m_description; + wxStaticText* m_status; + wxListBox* m_sourcesList; + wxButton* m_manageButton; +}; + + +#endif diff --git a/eeschema/eeschema.cpp b/eeschema/eeschema.cpp index f3b334d1c8..bb055e5b89 100644 --- a/eeschema/eeschema.cpp +++ b/eeschema/eeschema.cpp @@ -23,6 +23,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#include #include #include #include @@ -60,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -360,6 +362,19 @@ static struct IFACE : public KIFACE_BASE, public UNITS_PROVIDER case PANEL_SCH_FIELD_NAME_TEMPLATES: return new PANEL_TEMPLATE_FIELDNAMES( aParent, nullptr ); + case PANEL_SCH_DATA_SOURCES: + { + EDA_BASE_FRAME* frame = aKiway->Player( FRAME_SCH, false ); + + if( !frame ) + frame = aKiway->Player( FRAME_SCH_SYMBOL_EDITOR, false ); + + if( !frame ) + frame = aKiway->Player( FRAME_SCH_VIEWER, false ); + + return new class PANEL_SCH_DATA_SOURCES( aParent, frame ); + } + case PANEL_SCH_SIMULATOR: return new PANEL_SIMULATOR_PREFERENCES( aParent ); diff --git a/eeschema/eeschema_config.cpp b/eeschema/eeschema_config.cpp index 4f5ad25d60..2e30ca1c6b 100644 --- a/eeschema/eeschema_config.cpp +++ b/eeschema/eeschema_config.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -368,6 +369,19 @@ void SCH_EDIT_FRAME::SaveSettings( APP_SETTINGS_BASE* aCfg ) } m_designBlocksPane->SaveSettings(); + + wxAuiPaneInfo& remoteSymbolPane = m_auimgr.GetPane( RemoteSymbolPaneName() ); + cfg->m_AuiPanels.remote_symbol_show = remoteSymbolPane.IsShown(); + + if( remoteSymbolPane.IsDocked() ) + { + cfg->m_AuiPanels.remote_symbol_panel_docked_width = m_remoteSymbolPane->GetSize().x; + } + else + { + cfg->m_AuiPanels.remote_symbol_panel_float_height = remoteSymbolPane.floating_size.y; + cfg->m_AuiPanels.remote_symbol_panel_float_width = remoteSymbolPane.floating_size.x; + } } } diff --git a/eeschema/eeschema_settings.cpp b/eeschema/eeschema_settings.cpp index 04587d21bb..81badb1e70 100644 --- a/eeschema/eeschema_settings.cpp +++ b/eeschema/eeschema_settings.cpp @@ -149,6 +149,49 @@ const wxAuiPaneInfo& defaultDesignBlocksPaneInfo( wxWindow* aWindow ) } +const wxAuiPaneInfo& defaultRemoteSymbolPaneInfo( wxWindow* aWindow ) +{ + static wxAuiPaneInfo paneInfo; + + paneInfo.Name( EDA_DRAW_FRAME::RemoteSymbolPaneName() ) + .Caption( _( "Remote Symbols" ) ) + .CaptionVisible( true ) + .PaneBorder( true ) + .Right().Layer( 3 ).Position( 3 ) + .TopDockable( false ) + .BottomDockable( false ) + .CloseButton( true ) + .MinSize( aWindow->FromDIP( wxSize( 240, 60 ) ) ) + .BestSize( aWindow->FromDIP( wxSize( 300, 200 ) ) ) + .FloatingSize( aWindow->FromDIP( wxSize( 800, 600 ) ) ) + .FloatingPosition( aWindow->FromDIP( wxPoint( 80, 220 ) ) ) + .Show( false ); + + return paneInfo; +} + + +wxString EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG::DefaultDestinationDir() +{ + return wxS( "${KIPRJMOD}/RemoteLibrary" ); +} + + +wxString EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG::DefaultLibraryPrefix() +{ + return wxS( "remote" ); +} + + +void EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG::ResetToDefaults() +{ + destination_dir = DefaultDestinationDir(); + library_prefix = DefaultLibraryPrefix(); + add_to_global_table = false; + user_ids.clear(); +} + + EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() : APP_SETTINGS_BASE( "eeschema", eeschemaSchemaVersion ), m_Appearance(), @@ -246,6 +289,18 @@ EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() : m_params.emplace_back( new PARAM( "aui.design_blocks_panel_float_height", &m_AuiPanels.design_blocks_panel_float_height, -1 ) ); + m_params.emplace_back( new PARAM( "aui.remote_symbol_show", + &m_AuiPanels.remote_symbol_show, false ) ); + + m_params.emplace_back( new PARAM( "aui.remote_symbol_panel_docked_width", + &m_AuiPanels.remote_symbol_panel_docked_width, -1 ) ); + + m_params.emplace_back( new PARAM( "aui.remote_symbol_panel_float_width", + &m_AuiPanels.remote_symbol_panel_float_width, -1 ) ); + + m_params.emplace_back( new PARAM( "aui.remote_symbol_panel_float_height", + &m_AuiPanels.remote_symbol_panel_float_height, -1 ) ); + m_params.emplace_back( new PARAM( "aui.schematic_hierarchy_float", &m_AuiPanels.schematic_hierarchy_float, false ) ); @@ -297,6 +352,20 @@ EESCHEMA_SETTINGS::EESCHEMA_SETTINGS() : m_params.emplace_back( new PARAM( "autoplace_fields.align_to_grid", &m_AutoplaceFields.align_to_grid, true ) ); + m_params.emplace_back( new PARAM( "remote_symbols.destination_dir", + &m_RemoteSymbol.destination_dir, + REMOTE_SYMBOL_CONFIG::DefaultDestinationDir() ) ); + + m_params.emplace_back( new PARAM( "remote_symbols.library_prefix", + &m_RemoteSymbol.library_prefix, + REMOTE_SYMBOL_CONFIG::DefaultLibraryPrefix() ) ); + + m_params.emplace_back( new PARAM( "remote_symbols.add_to_global_table", + &m_RemoteSymbol.add_to_global_table, false ) ); + + m_params.emplace_back( new PARAM_WXSTRING_MAP( "remote_symbols.user_ids", + &m_RemoteSymbol.user_ids, {} ) ); + m_params.emplace_back( new PARAM( "drawing.default_bus_thickness", &m_Drawing.default_bus_thickness, DEFAULT_BUS_WIDTH_MILS ) ); diff --git a/eeschema/eeschema_settings.h b/eeschema/eeschema_settings.h index 7ab6c6b234..84b854ff6b 100644 --- a/eeschema/eeschema_settings.h +++ b/eeschema/eeschema_settings.h @@ -25,6 +25,8 @@ #include +#include + #include #include @@ -35,6 +37,7 @@ extern const wxAuiPaneInfo& defaultNetNavigatorPaneInfo(); extern const wxAuiPaneInfo& defaultPropertiesPaneInfo( wxWindow* aWindow ); extern const wxAuiPaneInfo& defaultSchSelectionFilterPaneInfo( wxWindow* aWindow ); extern const wxAuiPaneInfo& defaultDesignBlocksPaneInfo( wxWindow* aWindow ); +extern const wxAuiPaneInfo& defaultRemoteSymbolPaneInfo( wxWindow* aWindow ); @@ -104,6 +107,27 @@ public: int design_blocks_panel_docked_width; int design_blocks_panel_float_width; int design_blocks_panel_float_height; + bool remote_symbol_show; + int remote_symbol_panel_docked_width; + int remote_symbol_panel_float_width; + int remote_symbol_panel_float_height; + }; + + struct REMOTE_SYMBOL_CONFIG + { + REMOTE_SYMBOL_CONFIG() + { + ResetToDefaults(); + } + + wxString destination_dir; + wxString library_prefix; + bool add_to_global_table; + std::map user_ids; + + void ResetToDefaults(); + static wxString DefaultDestinationDir(); + static wxString DefaultLibraryPrefix(); }; struct AUTOPLACE_FIELDS @@ -321,6 +345,7 @@ private: public: APPEARANCE m_Appearance; AUI_PANELS m_AuiPanels; + REMOTE_SYMBOL_CONFIG m_RemoteSymbol; DRAWING m_Drawing; INPUT m_Input; @@ -346,4 +371,3 @@ public: wxString m_lastSymbolLibDir; }; - diff --git a/eeschema/libraries/symbol_library_adapter.cpp b/eeschema/libraries/symbol_library_adapter.cpp index 99524689f7..90df6b46bd 100644 --- a/eeschema/libraries/symbol_library_adapter.cpp +++ b/eeschema/libraries/symbol_library_adapter.cpp @@ -19,6 +19,7 @@ */ #include +#include #include #include @@ -212,7 +213,62 @@ LIB_SYMBOL* SYMBOL_LIBRARY_ADAPTER::LoadSymbol( const wxString& aNickname, const SYMBOL_LIBRARY_ADAPTER::SAVE_T SYMBOL_LIBRARY_ADAPTER::SaveSymbol( const wxString& aNickname, const LIB_SYMBOL* aSymbol, bool aOverwrite ) { - wxCHECK_MSG( false, SAVE_SKIPPED, "Unimplemented!" ); + wxCHECK( aSymbol, SAVE_SKIPPED ); + + LIBRARY_RESULT libResult = loadIfNeeded( aNickname ); + + if( !libResult.has_value() ) + { + wxLogTrace( traceLibraries, "SaveSymbol: unable to load library %s: %s", + aNickname, libResult.error().message ); + return SAVE_SKIPPED; + } + + LIB_DATA* lib = *libResult; + + if( !lib ) + { + wxLogTrace( traceLibraries, "SaveSymbol: library %s not found", aNickname ); + return SAVE_SKIPPED; + } + + SCH_IO* plugin = schplugin( lib ); + wxCHECK( plugin, SAVE_SKIPPED ); + + std::map options = lib->row->GetOptionsMap(); + + if( !aOverwrite ) + { + try + { + std::unique_ptr existing( plugin->LoadSymbol( getUri( lib->row ), + aSymbol->GetName(), + &options ) ); + + if( existing ) + return SAVE_SKIPPED; + } + catch( const IO_ERROR& e ) + { + wxLogTrace( traceLibraries, + "SaveSymbol: error checking for existing symbol %s:%s: %s", + aNickname, aSymbol->GetName(), e.What() ); + return SAVE_SKIPPED; + } + } + + try + { + plugin->SaveSymbol( getUri( lib->row ), aSymbol, &options ); + } + catch( const IO_ERROR& e ) + { + wxLogTrace( traceLibraries, "SaveSymbol: error saving %s:%s: %s", + aNickname, aSymbol->GetName(), e.What() ); + return SAVE_SKIPPED; + } + + return SAVE_OK; } diff --git a/eeschema/menubar.cpp b/eeschema/menubar.cpp index bb4399da64..55f0b97b28 100644 --- a/eeschema/menubar.cpp +++ b/eeschema/menubar.cpp @@ -36,6 +36,7 @@ #include "eeschema_id.h" #include "sch_edit_frame.h" #include +#include #include @@ -187,6 +188,13 @@ void SCH_EDIT_FRAME::doReCreateMenuBar() showHidePanels->Add( SCH_ACTIONS::showNetNavigator, ACTION_MENU::CHECK ); showHidePanels->Add( SCH_ACTIONS::showDesignBlockPanel, ACTION_MENU::CHECK, _( "Design Blocks" ) ); + wxMenuItem* remoteSymbolItem = showHidePanels->Add( SCH_ACTIONS::showRemoteSymbolPanel, ACTION_MENU::CHECK, _( "Remote Symbols" ) ); + + if( m_remoteSymbolPane && !m_remoteSymbolPane->HasDataSources() ) + { + remoteSymbolItem->Enable( false ); + remoteSymbolItem->SetHelp( _( "Install a remote symbol server using the Plugin and Content Manger to enable" ) ); + } viewMenu->Add( showHidePanels ); @@ -363,4 +371,3 @@ void SCH_EDIT_FRAME::doReCreateMenuBar() delete oldMenuBar; } - diff --git a/eeschema/sch_edit_frame.cpp b/eeschema/sch_edit_frame.cpp index bae030a90c..b719d2f2ab 100644 --- a/eeschema/sch_edit_frame.cpp +++ b/eeschema/sch_edit_frame.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -165,7 +166,8 @@ SCH_EDIT_FRAME::SCH_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) : m_netNavigatorFilterValue(), m_netNavigatorMenuNetName(), m_highlightedConnChanged( false ), - m_designBlocksPane( nullptr ) + m_designBlocksPane( nullptr ), + m_remoteSymbolPane( nullptr ) { m_maximizeByDefault = true; m_schematic = new SCHEMATIC( &Prj() ); @@ -226,6 +228,7 @@ SCH_EDIT_FRAME::SCH_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) : m_searchPane = new SCH_SEARCH_PANE( this ); m_propertiesPanel = new SCH_PROPERTIES_PANEL( this, this ); + m_remoteSymbolPane = new PANEL_REMOTE_SYMBOL( this ); m_propertiesPanel->SetSplitterProportion( eeconfig()->m_AuiPanels.properties_splitter ); @@ -265,6 +268,7 @@ SCH_EDIT_FRAME::SCH_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) : m_auimgr.AddPane( m_selectionFilterPanel, defaultSchSelectionFilterPaneInfo( this ) ); m_auimgr.AddPane( m_designBlocksPane, defaultDesignBlocksPaneInfo( this ) ); + m_auimgr.AddPane( m_remoteSymbolPane, defaultRemoteSymbolPaneInfo( this ) ); m_auimgr.AddPane( createHighlightedNetNavigator(), defaultNetNavigatorPaneInfo() ); @@ -298,11 +302,18 @@ SCH_EDIT_FRAME::SCH_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) : wxAuiPaneInfo& propertiesPane = m_auimgr.GetPane( PropertiesPaneName() ); wxAuiPaneInfo& selectionFilterPane = m_auimgr.GetPane( wxS( "SelectionFilter" ) ); wxAuiPaneInfo& designBlocksPane = m_auimgr.GetPane( DesignBlocksPaneName() ); + wxAuiPaneInfo& remoteSymbolPane = m_auimgr.GetPane( RemoteSymbolPaneName() ); hierarchy_pane.Show( aui_cfg.show_schematic_hierarchy ); netNavigatorPane.Show( aui_cfg.show_net_nav_panel ); propertiesPane.Show( aui_cfg.show_properties ); designBlocksPane.Show( aui_cfg.design_blocks_show ); + + if( m_remoteSymbolPane && !m_remoteSymbolPane->HasDataSources() ) + remoteSymbolPane.Show( false ); + else + remoteSymbolPane.Show( aui_cfg.remote_symbol_show ); + updateSelectionFilterVisbility(); // The selection filter doesn't need to grow in the vertical direction when docked @@ -352,6 +363,9 @@ SCH_EDIT_FRAME::SCH_EDIT_FRAME( KIWAY* aKiway, wxWindow* aParent ) : if( aui_cfg.design_blocks_show ) SetAuiPaneSize( m_auimgr, designBlocksPane, aui_cfg.design_blocks_panel_docked_width, -1 ); + if( aui_cfg.remote_symbol_show ) + SetAuiPaneSize( m_auimgr, remoteSymbolPane, aui_cfg.remote_symbol_panel_docked_width, -1 ); + if( aui_cfg.hierarchy_panel_docked_width > 0 ) { // If the net navigator is not show, let the hierarchy navigator take all of the vertical @@ -728,6 +742,12 @@ void SCH_EDIT_FRAME::setupUIConditions() return m_auimgr.GetPane( DesignBlocksPaneName() ).IsShown(); }; + auto remoteSymbolCond = + [ this ] (const SELECTION& aSel ) + { + return m_auimgr.GetPane( RemoteSymbolPaneName() ).IsShown(); + }; + auto undoCond = [ this ] (const SELECTION& aSel ) { @@ -763,6 +783,7 @@ void SCH_EDIT_FRAME::setupUIConditions() mgr->SetConditions( SCH_ACTIONS::showNetNavigator, CHECK( netNavigatorCond ) ); mgr->SetConditions( ACTIONS::showProperties, CHECK( propertiesCond ) ); mgr->SetConditions( SCH_ACTIONS::showDesignBlockPanel, CHECK( designBlockCond ) ); + mgr->SetConditions( SCH_ACTIONS::showRemoteSymbolPanel, CHECK( remoteSymbolCond ) ); mgr->SetConditions( ACTIONS::toggleGrid, CHECK( cond.GridVisible() ) ); mgr->SetConditions( ACTIONS::toggleGridOverrides, CHECK( cond.GridOverrides() ) ); @@ -2001,6 +2022,7 @@ void SCH_EDIT_FRAME::ShowChangedLanguage() m_auimgr.GetPane( m_selectionFilterPanel ).Caption( _( "Selection Filter" ) ); m_auimgr.GetPane( m_propertiesPanel ).Caption( _( "Properties" ) ); m_auimgr.GetPane( m_designBlocksPane ).Caption( _( "Design Blocks" ) ); + m_auimgr.GetPane( RemoteSymbolPaneName() ).Caption( _( "Remote Symbols" ) ); m_auimgr.Update(); m_hierarchy->UpdateHierarchyTree(); @@ -2901,6 +2923,47 @@ void SCH_EDIT_FRAME::ToggleLibraryTree() } +void SCH_EDIT_FRAME::ToggleRemoteSymbolPanel() +{ + EESCHEMA_SETTINGS* cfg = eeconfig(); + + wxCHECK( cfg, /* void */ ); + + wxAuiPaneInfo& remotePane = m_auimgr.GetPane( RemoteSymbolPaneName() ); + + remotePane.Show( !remotePane.IsShown() ); + + if( remotePane.IsShown() ) + { + if( remotePane.IsFloating() ) + { + remotePane.FloatingSize( cfg->m_AuiPanels.remote_symbol_panel_float_width, + cfg->m_AuiPanels.remote_symbol_panel_float_height ); + m_auimgr.Update(); + } + else if( cfg->m_AuiPanels.remote_symbol_panel_docked_width > 0 ) + { + SetAuiPaneSize( m_auimgr, remotePane, + cfg->m_AuiPanels.remote_symbol_panel_docked_width, -1 ); + } + } + else + { + if( remotePane.IsFloating() ) + { + cfg->m_AuiPanels.remote_symbol_panel_float_width = remotePane.floating_size.x; + cfg->m_AuiPanels.remote_symbol_panel_float_height = remotePane.floating_size.y; + } + else if( m_remoteSymbolPane ) + { + cfg->m_AuiPanels.remote_symbol_panel_docked_width = m_remoteSymbolPane->GetSize().x; + } + + m_auimgr.Update(); + } +} + + void SCH_EDIT_FRAME::SetSchematic( SCHEMATIC* aSchematic ) { wxCHECK( aSchematic, /* void */ ); diff --git a/eeschema/sch_edit_frame.h b/eeschema/sch_edit_frame.h index 12eee64517..fc3fc575a3 100644 --- a/eeschema/sch_edit_frame.h +++ b/eeschema/sch_edit_frame.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -57,6 +58,7 @@ class SCH_JUNCTION; class SCHEMATIC; class SCH_COMMIT; class SCH_DESIGN_BLOCK_PANE; +class PANEL_REMOTE_SYMBOL; class DIALOG_BOOK_REPORTER; class DIALOG_ERC; class DIALOG_SYMBOL_FIELDS_TABLE; @@ -816,6 +818,9 @@ public: void ToggleLibraryTree() override; + void ToggleRemoteSymbolPanel(); + + DIALOG_BOOK_REPORTER* GetSymbolDiffDialog(); DIALOG_ERC* GetErcDialog(); @@ -1061,6 +1066,7 @@ private: std::vector m_designBlockHistoryList; SCH_DESIGN_BLOCK_PANE* m_designBlocksPane; + PANEL_REMOTE_SYMBOL* m_remoteSymbolPane; wxChoice* m_currentVariantCtrl; diff --git a/eeschema/tools/sch_actions.cpp b/eeschema/tools/sch_actions.cpp index c7bdf06a7e..4f0c58bda2 100644 --- a/eeschema/tools/sch_actions.cpp +++ b/eeschema/tools/sch_actions.cpp @@ -124,6 +124,13 @@ TOOL_ACTION SCH_ACTIONS::showDesignBlockPanel( TOOL_ACTION_ARGS() .Tooltip( _( "Show/hide design blocks library" ) ) .Icon( BITMAPS::search_tree ) ); +TOOL_ACTION SCH_ACTIONS::showRemoteSymbolPanel( TOOL_ACTION_ARGS() + .Name( "eeschema.RemoteSymbols.showPanel" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Remote Symbols" ) ) + .Tooltip( _( "Show/hide the remote symbol panel" ) ) + .Icon( BITMAPS::library_browser ) ); + TOOL_ACTION SCH_ACTIONS::saveSheetAsDesignBlock( TOOL_ACTION_ARGS() .Name( "eeschema.SchDesignBlockControl.saveSheetAsDesignBlock" ) .Scope( AS_GLOBAL ) diff --git a/eeschema/tools/sch_actions.h b/eeschema/tools/sch_actions.h index e5cf213bb1..71fc9aecbb 100644 --- a/eeschema/tools/sch_actions.h +++ b/eeschema/tools/sch_actions.h @@ -202,6 +202,7 @@ public: // Design Block management static TOOL_ACTION showDesignBlockPanel; + static TOOL_ACTION showRemoteSymbolPanel; static TOOL_ACTION saveSheetAsDesignBlock; static TOOL_ACTION saveSelectionAsDesignBlock; static TOOL_ACTION saveSheetToDesignBlock; diff --git a/eeschema/tools/sch_editor_control.cpp b/eeschema/tools/sch_editor_control.cpp index 265d644352..49e6748727 100644 --- a/eeschema/tools/sch_editor_control.cpp +++ b/eeschema/tools/sch_editor_control.cpp @@ -2673,6 +2673,13 @@ int SCH_EDITOR_CONTROL::ToggleLibraryTree( const TOOL_EVENT& aEvent ) } +int SCH_EDITOR_CONTROL::ToggleRemoteSymbolPanel( const TOOL_EVENT& aEvent ) +{ + getEditFrame()->ToggleRemoteSymbolPanel(); + return 0; +} + + int SCH_EDITOR_CONTROL::ToggleHiddenPins( const TOOL_EVENT& aEvent ) { EESCHEMA_SETTINGS* cfg = m_frame->eeconfig(); @@ -3191,6 +3198,7 @@ void SCH_EDITOR_CONTROL::setTransitions() Go( &SCH_EDITOR_CONTROL::ToggleProperties, ACTIONS::showProperties.MakeEvent() ); Go( &SCH_EDITOR_CONTROL::ToggleLibraryTree, SCH_ACTIONS::showDesignBlockPanel.MakeEvent() ); Go( &SCH_EDITOR_CONTROL::ToggleLibraryTree, SCH_ACTIONS::showDesignBlockPanel.MakeEvent() ); + Go( &SCH_EDITOR_CONTROL::ToggleRemoteSymbolPanel, SCH_ACTIONS::showRemoteSymbolPanel.MakeEvent() ); Go( &SCH_EDITOR_CONTROL::ToggleHiddenPins, SCH_ACTIONS::toggleHiddenPins.MakeEvent() ); Go( &SCH_EDITOR_CONTROL::ToggleHiddenFields, SCH_ACTIONS::toggleHiddenFields.MakeEvent() ); diff --git a/eeschema/tools/sch_editor_control.h b/eeschema/tools/sch_editor_control.h index 380162ffe7..1d40670e38 100644 --- a/eeschema/tools/sch_editor_control.h +++ b/eeschema/tools/sch_editor_control.h @@ -133,6 +133,7 @@ public: int ShowNetNavigator( const TOOL_EVENT& aEvent ); int ToggleProperties( const TOOL_EVENT& aEvent ); int ToggleLibraryTree( const TOOL_EVENT& aEvent ); + int ToggleRemoteSymbolPanel( const TOOL_EVENT& aEvent ); int ToggleHiddenPins( const TOOL_EVENT& aEvent ); int ToggleHiddenFields( const TOOL_EVENT& aEvent ); diff --git a/eeschema/widgets/panel_remote_symbol.cpp b/eeschema/widgets/panel_remote_symbol.cpp new file mode 100644 index 0000000000..7c5a7a618a --- /dev/null +++ b/eeschema/widgets/panel_remote_symbol.cpp @@ -0,0 +1,2322 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include + +#include "picosha2.h" + +#include "panel_remote_symbol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef wxUSE_BASE64 +#define wxUSE_BASE64 1 +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace +{ + +class STRING_OUTPUTFORMATTER_BUFFER : public OUTPUTFORMATTER +{ +public: + STRING_OUTPUTFORMATTER_BUFFER() : OUTPUTFORMATTER( OUTPUTFMTBUFZ ) {} + + const std::string& GetString() const { return m_output; } + +protected: + void write( const char* aOutBuf, int aCount ) override + { + m_output.append( aOutBuf, aCount ); + } + +private: + std::string m_output; +}; + +std::string HashBuffer( const std::vector& aBuffer ) +{ + std::vector hash( picosha2::k_digest_size ); + picosha2::hash256( aBuffer.begin(), aBuffer.end(), hash.begin(), hash.end() ); + return picosha2::bytes_to_hex_string( hash.begin(), hash.end() ); +} + +std::string HashString( const std::string& aValue ) +{ + std::vector hash( picosha2::k_digest_size ); + picosha2::hash256( aValue.begin(), aValue.end(), hash.begin(), hash.end() ); + return picosha2::bytes_to_hex_string( hash.begin(), hash.end() ); +} + +bool HashFile( const wxFileName& aPath, std::string& aOutHash ) +{ + if( !aPath.FileExists() ) + return false; + + std::ifstream file( aPath.GetFullPath().ToStdString(), std::ios::binary ); + + if( !file ) + return false; + + std::vector hash( picosha2::k_digest_size ); + picosha2::hash256( file, hash.begin(), hash.end() ); + aOutHash = picosha2::bytes_to_hex_string( hash.begin(), hash.end() ); + return true; +} + +wxString AppendNumericSuffix( const wxString& aBase, int aSuffix ) +{ + return wxString::Format( wxS( "%s_%d" ), aBase, aSuffix ); +} + +wxString AppendNumericSuffixToFilename( const wxString& aFilename, int aSuffix ) +{ + wxFileName fn; + fn.SetFullName( aFilename ); + + const wxString base = fn.GetName(); + const wxString ext = fn.GetExt(); + wxString candidate = AppendNumericSuffix( base, aSuffix ); + + if( ext.IsEmpty() ) + return candidate; + + return candidate + wxS( "." ) + ext; +} + +std::string SerializeSymbolCanonical( const LIB_SYMBOL& aSymbol ) +{ + LIB_SYMBOL clone( aSymbol ); + STRING_OUTPUTFORMATTER_BUFFER formatter; + SCH_IO_KICAD_SEXPR_LIB_CACHE::SaveSymbol( &clone, formatter ); + return formatter.GetString(); +} + +std::unique_ptr TryCloneSymbol( SCH_IO* aPlugin, const wxFileName& aLibraryFile, + const wxString& aSymbolName ) +{ + if( !aPlugin || !aLibraryFile.FileExists() ) + return nullptr; + + try + { + LIB_SYMBOL* existing = aPlugin->LoadSymbol( aLibraryFile.GetFullPath(), aSymbolName ); + + if( !existing ) + return nullptr; + + return std::make_unique( *existing ); + } + catch( const IO_ERROR& ) + { + return nullptr; + } +} + +bool ComputeSymbolChecksum( SCH_IO* aPlugin, const wxFileName& aLibraryFile, + const wxString& aSymbolName, std::string& aOutChecksum ) +{ + std::unique_ptr symbol = TryCloneSymbol( aPlugin, aLibraryFile, aSymbolName ); + + if( !symbol ) + return false; + + aOutChecksum = HashString( SerializeSymbolCanonical( *symbol ) ); + return true; +} + +} // namespace + + + +wxString PANEL_REMOTE_SYMBOL::jsonString( const nlohmann::json& aObject, const char* aKey ) const +{ + auto it = aObject.find( aKey ); + + if( it != aObject.end() && it->is_string() ) + return wxString::FromUTF8( it->get() ); + + return wxString(); +} + +bool PANEL_REMOTE_SYMBOL::decodeBase64Payload( const std::string& aEncoded, + std::vector& aOutput, + wxString& aError ) const +{ + if( aEncoded.empty() ) + { + aError = _( "Missing payload data." ); + return false; + } + + wxMemoryBuffer buffer = wxBase64Decode( wxString::FromUTF8( aEncoded.c_str() ) ); + + if( buffer.IsEmpty() ) + { + aError = _( "Failed to decode base64 payload." ); + return false; + } + + aOutput.resize( buffer.GetDataLen() ); + memcpy( aOutput.data(), buffer.GetData(), buffer.GetDataLen() ); + return true; +} + +bool PANEL_REMOTE_SYMBOL::decompressIfNeeded( const std::string& aCompression, + const std::vector& aInput, + std::vector& aOutput, + wxString& aError ) const +{ + if( aCompression.empty() || aCompression == "NONE" ) + { + aOutput = aInput; + return true; + } + + if( aCompression != "ZSTD" ) + { + aError = wxString::Format( _( "Unsupported compression '%s'." ), wxString::FromUTF8( aCompression ) ); + return false; + } + + if( aInput.empty() ) + { + aError = _( "Compressed payload was empty." ); + return false; + } + + unsigned long long expectedSize = ZSTD_getFrameContentSize( aInput.data(), aInput.size() ); + + if( expectedSize == ZSTD_CONTENTSIZE_ERROR || expectedSize == ZSTD_CONTENTSIZE_UNKNOWN ) + expectedSize = static_cast( aInput.size() ) * 4; + + aOutput.resize( expectedSize ); + + size_t decompressed = ZSTD_decompress( aOutput.data(), expectedSize, aInput.data(), aInput.size() ); + + if( ZSTD_isError( decompressed ) ) + { + aError = wxString::Format( _( "ZSTD decompression failed: %s" ), + wxString::FromUTF8( ZSTD_getErrorName( decompressed ) ) ); + return false; + } + + aOutput.resize( decompressed ); + return true; +} + +wxString PANEL_REMOTE_SYMBOL::sanitizeForScript( const std::string& aJson ) const +{ + wxString script = wxString::FromUTF8( aJson.c_str() ); + script.Replace( "\\", "\\\\" ); + script.Replace( "'", "\\'" ); + return script; +} + +wxString PANEL_REMOTE_SYMBOL::normalizeDataSourceUrl( const wxString& aUrl ) const +{ + wxString normalized = aUrl; + normalized.Trim( true ).Trim( false ); + + while( normalized.Length() > 1 && normalized.EndsWith( wxS( "/" ) ) ) + normalized.RemoveLast(); + + return normalized; +} + +wxString PANEL_REMOTE_SYMBOL::currentDataSourceKey() const +{ + return m_activeDataSourceUrl; +} + +void PANEL_REMOTE_SYMBOL::loadStoredUserIdForActiveSource() +{ + m_activeUserId.clear(); + + if( m_activeDataSourceUrl.IsEmpty() ) + return; + + if( EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ) ) + { + auto it = settings->m_RemoteSymbol.user_ids.find( m_activeDataSourceUrl ); + + if( it != settings->m_RemoteSymbol.user_ids.end() ) + m_activeUserId = it->second; + } +} + +void PANEL_REMOTE_SYMBOL::storeUserIdForActiveSource( const wxString& aUserId ) +{ + if( aUserId.IsEmpty() ) + return; + + if( m_activeDataSourceUrl.IsEmpty() ) + return; + + if( EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ) ) + { + settings->m_RemoteSymbol.user_ids[m_activeDataSourceUrl] = aUserId; + Pgm().GetSettingsManager().Save( settings ); + } + + m_activeUserId = aUserId; +} + +wxString PANEL_REMOTE_SYMBOL::sanitizeFileComponent( const wxString& aValue, + const wxString& aDefault ) const +{ + wxString result = aValue; + result.Trim( true ).Trim( false ); + + if( result.IsEmpty() ) + result = aDefault; + + for( size_t i = 0; i < result.length(); ++i ) + { + wxUniChar ch = result[i]; + + if( ch == '/' || ch == '\\' || ch == ':' ) + result[i] = '_'; + } + + return result; +} + +bool PANEL_REMOTE_SYMBOL::writeBinaryFile( const wxFileName& aOutput, + const std::vector& aPayload, + wxString& aError ) const +{ + if( aPayload.empty() ) + { + aError = _( "Payload was empty." ); + return false; + } + + wxFileName targetDir = aOutput; + targetDir.SetFullName( wxEmptyString ); + + if( !targetDir.DirExists() ) + { + if( !targetDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), targetDir.GetFullPath() ); + return false; + } + } + + wxFFile file( aOutput.GetFullPath(), wxS( "wb" ) ); + + if( !file.IsOpened() ) + { + aError = wxString::Format( _( "Unable to open '%s' for writing." ), aOutput.GetFullPath() ); + return false; + } + + if( file.Write( aPayload.data(), aPayload.size() ) != aPayload.size() ) + { + aError = wxString::Format( _( "Failed to write '%s'." ), aOutput.GetFullPath() ); + return false; + } + + file.Close(); + return true; +} + + +std::unique_ptr PANEL_REMOTE_SYMBOL::loadSymbolFromPayload( const std::vector& aPayload, + const wxString& aLibItemName, + wxString& aError ) const +{ + if( aPayload.empty() ) + { + aError = _( "Symbol payload was empty." ); + return nullptr; + } + + wxString tempPath = wxFileName::CreateTempFileName( wxS( "remote_symbol" ) ); + + if( tempPath.IsEmpty() ) + { + aError = _( "Unable to create a temporary file for the symbol payload." ); + return nullptr; + } + + wxFileName tempFile( tempPath ); + + wxFFile file( tempFile.GetFullPath(), wxS( "wb" ) ); + + if( !file.IsOpened() ) + { + aError = _( "Unable to create a temporary file for the symbol payload." ); + wxRemoveFile( tempFile.GetFullPath() ); + return nullptr; + } + + if( file.Write( aPayload.data(), aPayload.size() ) != aPayload.size() ) + { + aError = _( "Failed to write the temporary symbol payload." ); + file.Close(); + wxRemoveFile( tempFile.GetFullPath() ); + return nullptr; + } + + file.Close(); + + IO_RELEASER plugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) ); + + if( !plugin ) + { + aError = _( "Unable to access the KiCad symbol plugin." ); + wxRemoveFile( tempFile.GetFullPath() ); + return nullptr; + } + + std::unique_ptr symbol; + + try + { + LIB_SYMBOL* loaded = plugin->LoadSymbol( tempFile.GetFullPath(), aLibItemName ); + + if( loaded ) + { + // Clone the symbol before the plugin's cache is destroyed. + // LoadSymbol returns a pointer owned by the plugin's internal cache, + // and the cache will be destroyed when 'plugin' goes out of scope. + symbol = std::make_unique( *loaded ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "loadSymbolFromPayload: loaded symbol %s from temporary file %s", + aLibItemName.ToUTF8().data(), tempFile.GetFullPath().ToUTF8().data() ); + } + else + { + aError = _( "Symbol payload did not include the expected symbol." ); + } + } + catch( const IO_ERROR& e ) + { + aError = wxString::Format( _( "Unable to decode the symbol payload: %s" ), e.What() ); + } + + wxRemoveFile( tempFile.GetFullPath() ); + return symbol; +} + + +PANEL_REMOTE_SYMBOL::PANEL_REMOTE_SYMBOL( SCH_EDIT_FRAME* aParent ) : + wxPanel( aParent ), + m_frame( aParent ), + m_dataSourceChoice( nullptr ), + m_configButton( nullptr ), + m_refreshButton( nullptr ), + m_webView( nullptr ), + m_pcm( std::make_shared( []( int ) {} ) ), + m_sessionId( 0 ), + m_messageIdCounter( 0 ), + m_pendingHandshake( false ), + m_loginServer(), + m_activeDataSourceUrl(), + m_activeUserId() +{ + wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL ); + + wxBoxSizer* controlsSizer = new wxBoxSizer( wxHORIZONTAL ); + m_dataSourceChoice = new wxChoice( this, wxID_ANY ); + m_dataSourceChoice->SetMinSize( FromDIP( wxSize( 160, -1 ) ) ); + m_dataSourceChoice->SetToolTip( _( "Select which remote data source to query." ) ); + controlsSizer->Add( m_dataSourceChoice, 1, wxEXPAND | wxRIGHT, FromDIP( 2 ) ); + + m_refreshButton = new BITMAP_BUTTON( this, wxID_ANY ); + m_refreshButton->SetBitmap( KiBitmapBundle( BITMAPS::reload ) ); + m_refreshButton->SetPadding( FromDIP( 2 ) ); + m_refreshButton->SetToolTip( _( "Refresh" ) ); + controlsSizer->Add( m_refreshButton, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP( 2 ) ); + + m_configButton = new BITMAP_BUTTON( this, wxID_ANY ); + m_configButton->SetBitmap( KiBitmapBundle( BITMAPS::config ) ); + m_configButton->SetPadding( FromDIP( 2 ) ); + m_configButton->SetToolTip( _( "Configure remote data sources." ) ); + controlsSizer->Add( m_configButton, 0, wxALIGN_CENTER_VERTICAL ); + + topSizer->Add( controlsSizer, 0, wxEXPAND | wxALL, FromDIP( 4 ) ); + + m_webView = new WEBVIEW_PANEL( this ); + m_webView->AddMessageHandler( wxS( "kicad" ), + [this]( const wxString& payload ) + { + onKicadMessage( payload ); + } ); + m_webView->SetHandleExternalLinks( true ); + + if( wxWebView* browser = m_webView->GetWebView() ) + browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this ); + + topSizer->Add( m_webView, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP( 2 ) ); + + SetSizer( topSizer ); + + m_dataSourceChoice->Bind( wxEVT_CHOICE, &PANEL_REMOTE_SYMBOL::onDataSourceChanged, this ); + m_configButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onConfigure, this ); + m_refreshButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onRefresh, this ); + Bind( EVT_REMOTE_SYMBOL_LOGIN_RESULT, &PANEL_REMOTE_SYMBOL::onRemoteLoginResult, this ); + + RefreshDataSources(); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "PANEL_REMOTE_SYMBOL constructed (frame=%p)", (void*)aParent ); +} + + +void PANEL_REMOTE_SYMBOL::RefreshDataSources() +{ + if( KICAD_SETTINGS* cfg = GetAppSettings( "kicad" ) ) + m_pcm->SetRepositoryList( cfg->m_PcmRepositories ); + + m_dataSources.clear(); + m_dataSourceChoice->Clear(); + + const std::vector installed = m_pcm->GetInstalledPackages(); + + for( const PCM_INSTALLATION_ENTRY& entry : installed ) + { + if( entry.package.type != PT_DATASOURCE ) + continue; + + wxString label = entry.package.name; + + if( !entry.current_version.IsEmpty() ) + label << wxS( " (" ) << entry.current_version << wxS( ")" ); + + m_dataSources.push_back( entry ); + m_dataSourceChoice->Append( label ); + } + + if( m_dataSources.empty() ) + { + m_dataSourceChoice->Enable( false ); + showMessage( _( "No remote data sources are currently installed." ) ); + return; + } + + m_dataSourceChoice->Enable( true ); + m_dataSourceChoice->SetSelection( 0 ); + loadDataSource( 0 ); +} + + +void PANEL_REMOTE_SYMBOL::onDataSourceChanged( wxCommandEvent& aEvent ) +{ + const int selection = aEvent.GetSelection(); + + if( selection == wxNOT_FOUND ) + return; + + loadDataSource( static_cast( selection ) ); +} + + +void PANEL_REMOTE_SYMBOL::onConfigure( wxCommandEvent& aEvent ) +{ + DIALOG_REMOTE_SYMBOL_CONFIG dlg( this ); + + dlg.ShowModal(); + + RefreshDataSources(); +} + + +void PANEL_REMOTE_SYMBOL::onRefresh( wxCommandEvent& aEvent ) +{ + if( m_webView && m_webView->GetWebView() ) + m_webView->GetWebView()->Reload(); +} + + +bool PANEL_REMOTE_SYMBOL::loadDataSource( size_t aIndex ) +{ + if( aIndex >= m_dataSources.size() ) + return false; + + return loadDataSource( m_dataSources[aIndex] ); +} + + +bool PANEL_REMOTE_SYMBOL::HasDataSources() const +{ + return !m_dataSources.empty(); +} + + +bool PANEL_REMOTE_SYMBOL::loadDataSource( const PCM_INSTALLATION_ENTRY& aEntry ) +{ + stopLoginServer(); + + std::optional jsonPath = findDataSourceJson( aEntry ); + + if( !jsonPath ) + { + wxLogWarning( "No JSON configuration found for data source %s", aEntry.package.identifier ); + showMessage( wxString::Format( _( "No configuration JSON found for '%s'." ), + aEntry.package.name ) ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: no json for %s", aEntry.package.identifier ); + return false; + } + + wxFFile file( jsonPath->GetFullPath(), "rb" ); + + if( !file.IsOpened() ) + { + wxLogWarning( "Unable to open remote data source JSON: %s", jsonPath->GetFullPath() ); + showMessage( wxString::Format( _( "Unable to open '%s'." ), jsonPath->GetFullPath() ) ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: cannot open %s", jsonPath->GetFullPath() ); + return false; + } + + wxString jsonContent; + + if( !file.ReadAll( &jsonContent, wxConvUTF8 ) ) + { + wxLogWarning( "Failed to read remote data source JSON: %s", jsonPath->GetFullPath() ); + showMessage( wxString::Format( _( "Unable to read '%s'." ), jsonPath->GetFullPath() ) ); + return false; + } + + if( std::optional url = extractUrlFromJson( jsonContent ) ) + { + m_activeDataSourceUrl = normalizeDataSourceUrl( *url ); + loadStoredUserIdForActiveSource(); + m_pendingHandshake = true; + m_webView->LoadURL( *url ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: loading URL %s", (*url).ToUTF8().data() ); + return true; + } + + wxLogWarning( "Remote data source JSON did not produce a valid URL for %s", aEntry.package.identifier ); + showMessage( wxString::Format( _( "Unable to load remote data for '%s'." ), aEntry.package.name ) ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "loadDataSource: failed to find URL in %s", jsonPath->GetFullPath() ); + return false; +} + + +std::optional +PANEL_REMOTE_SYMBOL::findDataSourceJson( const PCM_INSTALLATION_ENTRY& aEntry ) const +{ + wxString cleanId = aEntry.package.identifier; + cleanId.Replace( '.', '_' ); + + wxFileName baseDir = wxFileName::DirName( m_pcm->Get3rdPartyPath() ); + baseDir.AppendDir( wxS( "resources" ) ); + baseDir.AppendDir( cleanId ); + + const wxString resourcesPath = baseDir.GetFullPath(); + + if( !wxDirExists( resourcesPath ) ) + return std::nullopt; + + const std::vector preferredNames = { + wxS( "remote_symbol.json" ), + wxS( "datasource.json" ) + }; + + for( const wxString& candidate : preferredNames ) + { + wxFileName file( baseDir ); + file.SetFullName( candidate ); + + if( file.FileExists() ) + return file; + } + + wxDir dir( resourcesPath ); + + if( !dir.IsOpened() ) + return std::nullopt; + + wxString jsonFile; + + if( dir.GetFirst( &jsonFile, wxS( "*.json" ), wxDIR_FILES ) ) + { + wxFileName fallback( baseDir ); + fallback.SetFullName( jsonFile ); + return fallback; + } + + return std::nullopt; +} + + +void PANEL_REMOTE_SYMBOL::showMessage( const wxString& aMessage ) +{ + if( !m_webView ) + return; + + wxString html; + wxString escaped = aMessage; + escaped.Replace( "&", "&" ); + escaped.Replace( "<", "<" ); + escaped.Replace( ">", ">" ); + html << wxS( "

" ) << escaped << wxS( "

" ); + m_webView->SetPage( html ); +} + + +std::optional PANEL_REMOTE_SYMBOL::extractUrlFromJson( const wxString& aJsonContent ) const +{ + if( aJsonContent.IsEmpty() ) + return std::nullopt; + + wxScopedCharBuffer utf8 = aJsonContent.ToUTF8(); + + if( !utf8 || utf8.length() == 0 ) + return std::nullopt; + + try + { + nlohmann::json parsed = nlohmann::json::parse( utf8.data() ); + std::string url; + + if( parsed.is_string() ) + { + url = parsed.get(); + } + else if( parsed.is_object() ) + { + auto extractString = [&]( const char* key ) -> std::string + { + auto it = parsed.find( key ); + + if( it != parsed.end() && it->is_string() ) + return it->get(); + + return {}; + }; + + auto extractInt = [&]( const char* key ) -> std::optional + { + auto it = parsed.find( key ); + + if( it == parsed.end() ) + return std::nullopt; + + if( it->is_number_integer() ) + return it->get(); + + if( it->is_string() ) + { + try + { + return std::stoi( it->get() ); + } + catch( ... ) + { + } + } + + return std::nullopt; + }; + + std::string host = extractString( "host" ); + std::optional port = extractInt( "port" ); + std::string path = extractString( "path" ); + + if( !host.empty() ) + { + if( path.empty() ) + path = "/"; + + if( port && *port > 0 ) + url = wxString::Format( "%s:%d%s", host, *port, path ).ToStdString(); + else + url = host + path; + } + + if( url.empty() ) + url = extractString( "url" ); + + if( url.empty() ) + { + for( const char* key : { "website", "endpoint" } ) + { + url = extractString( key ); + + if( !url.empty() ) + break; + } + } + + if( url.empty() ) + { + for( const auto& [name, value] : parsed.items() ) + { + if( value.is_string() ) + { + const std::string candidate = value.get(); + + if( candidate.rfind( "http", 0 ) == 0 || candidate.rfind( "file", 0 ) == 0 ) + { + url = candidate; + break; + } + } + } + } + } + + if( url.empty() ) + return std::nullopt; + + return wxString::FromUTF8( url.c_str() ); + } + catch( const std::exception& e ) + { + wxLogWarning( "Failed to parse remote symbol JSON: %s", e.what() ); + return std::nullopt; + } +} + + +void PANEL_REMOTE_SYMBOL::onKicadMessage( const wxString& aMessage ) +{ + wxScopedCharBuffer utf8 = aMessage.ToUTF8(); + + if( !utf8 || utf8.length() == 0 ) + { + wxLogWarning( "Remote symbol RPC: empty payload." ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: empty payload" ); + return; + } + + try + { + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: received payload size=%d", (int)utf8.length() ); + handleRpcMessage( nlohmann::json::parse( utf8.data() ) ); + } + catch( const std::exception& e ) + { + wxLogWarning( "Remote symbol RPC parse error: %s", e.what() ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "onKicadMessage: parse error %s", e.what() ); + } +} + + +void PANEL_REMOTE_SYMBOL::onWebViewLoaded( wxWebViewEvent& aEvent ) +{ + wxUnusedVar( aEvent ); + + if( m_pendingHandshake ) + { + CallAfter( [this]() + { + if( m_pendingHandshake ) + { + m_pendingHandshake = false; + beginSessionHandshake(); + } + } ); + } + + aEvent.Skip(); +} + + +void PANEL_REMOTE_SYMBOL::beginSessionHandshake() +{ + if( !m_webView ) + return; + + m_sessionId = KIID(); + m_messageIdCounter = 0; + + nlohmann::json params = nlohmann::json::object(); + params["client_name"] = "KiCad"; + params["client_version"] = GetSemanticVersion().ToStdString(); + params["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION }; + + sendRpcMessage( wxS( "NEW_SESSION" ), std::move( params ) ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "beginSessionHandshake: NEW_SESSION sent, session=%s", m_sessionId.AsString() ); +} + +void PANEL_REMOTE_SYMBOL::stopLoginServer() +{ + if( m_loginServer ) + m_loginServer.reset(); +} + +void PANEL_REMOTE_SYMBOL::handleRemoteLogin( const nlohmann::json& aParams, int aMessageId ) +{ + const wxString loginUrl = jsonString( aParams, "url" ); + + if( loginUrl.IsEmpty() ) + { + respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INVALID_PARAMETERS" ), + _( "Missing login URL for remote authentication." ) ); + return; + } + + stopLoginServer(); + + std::unique_ptr server = std::make_unique( this, "https://www.google.com/" ); + + if( !server->Start() ) + { + respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INTERNAL_ERROR" ), + _( "Unable to start the local login listener." ) ); + return; + } + + const unsigned short port = server->GetPort(); + + if( port == 0 ) + { + respondWithError( wxS( "REMOTE_LOGIN" ), aMessageId, wxS( "INTERNAL_ERROR" ), + _( "Failed to allocate a callback port for login." ) ); + return; + } + + m_loginServer = std::move( server ); + + nlohmann::json reply = nlohmann::json::object(); + reply["port"] = static_cast( port ); + sendRpcMessage( wxS( "REMOTE_LOGIN" ), std::move( reply ), aMessageId ); + + wxString launchUrl = loginUrl; + + if( launchUrl.Find( '?' ) != wxNOT_FOUND ) + launchUrl << "&port=" << port; + else + launchUrl << "?port=" << port; + + if( !wxLaunchDefaultBrowser( launchUrl ) ) + { + wxLogWarning( "Remote login requested but default browser could not be opened (%s).", + launchUrl.ToUTF8().data() ); + } +} + +void PANEL_REMOTE_SYMBOL::onRemoteLoginResult( wxCommandEvent& aEvent ) +{ + const bool success = aEvent.GetInt() != 0; + const wxString userId = aEvent.GetString(); + + if( success && !userId.IsEmpty() ) + { + storeUserIdForActiveSource( userId ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "Remote login succeeded; stored user_id." ); + + if( m_webView && !m_activeDataSourceUrl.IsEmpty() ) + m_webView->LoadURL( m_activeDataSourceUrl ); + } + else + { + wxLogWarning( "Remote login callback did not provide a user_id." ); + } + + stopLoginServer(); +} + + +void PANEL_REMOTE_SYMBOL::sendRpcMessage( const wxString& aCommand, + nlohmann::json aParameters, + std::optional aResponseTo, + const wxString& aStatus, + const std::string& aData, + const wxString& aErrorCode, + const wxString& aErrorMessage ) +{ + if( !m_webView || m_webView->HasLoadError() ) + return; + + nlohmann::json payload = nlohmann::json::object(); + payload["version"] = REMOTE_SYMBOL_SESSION_VERSION; + payload["session_id"] = m_sessionId.AsStdString(); + payload["message_id"] = ++m_messageIdCounter; + payload["command"] = aCommand.ToStdString(); + + if( !m_activeUserId.IsEmpty() ) + payload["user_id"] = m_activeUserId.ToStdString(); + + if( aResponseTo ) + payload["response_to"] = *aResponseTo; + + if( !aStatus.IsEmpty() ) + payload["status"] = aStatus.ToStdString(); + + if( !aParameters.is_null() && !aParameters.empty() ) + payload["parameters"] = std::move( aParameters ); + + if( !aData.empty() ) + payload["data"] = aData; + + if( !aErrorCode.IsEmpty() ) + payload["error_code"] = aErrorCode.ToStdString(); + + if( !aErrorMessage.IsEmpty() ) + payload["error_message"] = aErrorMessage.ToStdString(); + + wxString script = wxString::Format( wxS( "window.kiclient.postMessage('%s');" ), + sanitizeForScript( payload.dump() ) ); + m_webView->RunScriptAsync( script ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "sendRpcMessage: %s", script ); +} + + +void PANEL_REMOTE_SYMBOL::respondWithError( const wxString& aCommand, int aResponseTo, + const wxString& aErrorCode, + const wxString& aErrorMessage ) +{ + sendRpcMessage( aCommand, nlohmann::json::object(), aResponseTo, wxS( "ERROR" ), + std::string(), aErrorCode, aErrorMessage ); +} + + +void PANEL_REMOTE_SYMBOL::handleRpcMessage( const nlohmann::json& aMessage ) +{ + if( !aMessage.is_object() ) + return; + + const wxString command = jsonString( aMessage, "command" ); + + if( command.IsEmpty() ) + return; + + auto messageIdIt = aMessage.find( "message_id" ); + + if( messageIdIt == aMessage.end() || !messageIdIt->is_number_integer() ) + return; + + const int messageId = messageIdIt->get(); + + const int version = aMessage.value( "version", 0 ); + + if( version != REMOTE_SYMBOL_SESSION_VERSION ) + { + respondWithError( command, messageId, wxS( "UNSUPPORTED_VERSION" ), + wxString::Format( _( "Unsupported RPC version %d." ), version ) ); + return; + } + + const wxString sessionId = jsonString( aMessage, "session_id" ); + + if( sessionId.IsEmpty() ) + { + respondWithError( command, messageId, wxS( "INVALID_PARAMETERS" ), + _( "Missing session identifier." ) ); + return; + } + + if( !sessionId.IsSameAs( m_sessionId.AsString() ) ) + wxLogWarning( "Remote symbol RPC session mismatch (expected %s, got %s).", + m_sessionId.AsString(), sessionId ); + + const wxString status = jsonString( aMessage, "status" ); + + if( status.IsSameAs( wxS( "ERROR" ), false ) ) + { + wxLogWarning( "Remote symbol RPC error (%s): %s", jsonString( aMessage, "error_code" ), + jsonString( aMessage, "error_message" ) ); + return; + } + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "handleRpcMessage: command=%s message_id=%d status=%s session=%s", + command.ToUTF8().data(), messageId, status.ToUTF8().data(), sessionId.ToUTF8().data() ); + + nlohmann::json params = nlohmann::json::object(); + auto paramsIt = aMessage.find( "parameters" ); + + if( paramsIt != aMessage.end() && paramsIt->is_object() ) + params = *paramsIt; + + const std::string data = aMessage.value( "data", std::string() ); + + if( command == wxS( "NEW_SESSION" ) ) + { + nlohmann::json reply = nlohmann::json::object(); + reply["client_name"] = "KiCad"; + reply["client_version"] = GetSemanticVersion().ToStdString(); + reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION }; + sendRpcMessage( command, std::move( reply ), messageId ); + return; + } + else if( command == wxS( "GET_KICAD_VERSION" ) ) + { + nlohmann::json reply = nlohmann::json::object(); + reply["kicad_version"] = GetSemanticVersion().ToStdString(); + sendRpcMessage( command, std::move( reply ), messageId ); + return; + } + else if( command == wxS( "LIST_SUPPORTED_VERSIONS" ) ) + { + nlohmann::json reply = nlohmann::json::object(); + reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION }; + sendRpcMessage( command, std::move( reply ), messageId ); + return; + } + else if( command == wxS( "CAPABILITIES" ) ) + { + nlohmann::json reply = nlohmann::json::object(); + reply["commands"] = { "NEW_SESSION", "GET_KICAD_VERSION", "LIST_SUPPORTED_VERSIONS", + "CAPABILITIES", "PING", "PONG", "REMOTE_LOGIN", "LOGOUT", "DL_SYMBOL", "DL_COMPONENT", "DL_FOOTPRINT", + "DL_SPICE", "DL_3DMODEL" }; + reply["compression"] = { "NONE", "ZSTD" }; + reply["max_message_size"] = 0; + sendRpcMessage( command, std::move( reply ), messageId ); + return; + } + else if( command == wxS( "PING" ) ) + { + nlohmann::json reply = nlohmann::json::object(); + + if( params.contains( "nonce" ) ) + reply["nonce"] = params["nonce"]; + + sendRpcMessage( wxS( "PONG" ), std::move( reply ), messageId ); + return; + } + else if( command == wxS( "PONG" ) ) + { + return; + } + else if( command == wxS( "REMOTE_LOGIN" ) ) + { + handleRemoteLogin( params, messageId ); + return; + } + else if( command == wxS( "LOGOUT" ) ) + { + m_activeUserId.Clear(); + + if( EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ) ) + { + if( !m_activeDataSourceUrl.IsEmpty() ) + { + settings->m_RemoteSymbol.user_ids.erase( m_activeDataSourceUrl ); + Pgm().GetSettingsManager().Save( settings ); + } + } + + sendRpcMessage( command, nlohmann::json::object(), messageId ); + beginSessionHandshake(); + return; + } + + const wxString compression = jsonString( params, "compression" ); + + if( command.StartsWith( wxS( "DL_" ) ) ) + { + if( compression.IsEmpty() ) + { + respondWithError( command, messageId, wxS( "INVALID_PARAMETERS" ), _( "Missing compression metadata." ) ); + return; + } + + std::vector decoded; + wxString error; + + if( !decodeBase64Payload( data, decoded, error ) ) + { + respondWithError( command, messageId, wxS( "INVALID_PAYLOAD" ), error ); + return; + } + + std::vector payload; + wxScopedCharBuffer compUtf8 = compression.ToUTF8(); + std::string compressionStr = compUtf8 ? std::string( compUtf8.data() ) : std::string(); + + if( !decompressIfNeeded( compressionStr, decoded, payload, error ) ) + { + respondWithError( command, messageId, wxS( "INVALID_PAYLOAD" ), error ); + return; + } + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "handleRpcMessage: decoded size=%zu decompressed size=%zu command=%s", + decoded.size(), payload.size(), command.ToUTF8().data() ); + + nlohmann::json responseParams = nlohmann::json::object(); + bool ok = false; + + if( command == wxS( "DL_SYMBOL" ) ) + ok = receiveSymbol( params, payload, error ); + else if( command == wxS( "DL_COMPONENT" ) ) + ok = receiveComponent( params, payload, error, &responseParams ); + else if( command == wxS( "DL_FOOTPRINT" ) ) + ok = receiveFootprint( params, payload, error ); + else if( command == wxS( "DL_3DMODEL" ) ) + ok = receive3DModel( params, payload, error ); + else if( command == wxS( "DL_SPICE" ) ) + ok = receiveSPICEModel( params, payload, error ); + else + { + respondWithError( command, messageId, wxS( "UNKNOWN_COMMAND" ), + wxString::Format( _( "Command '%s' is not supported." ), command ) ); + return; + } + + if( ok ) + sendRpcMessage( command, std::move( responseParams ), messageId ); + else + respondWithError( command, messageId, wxS( "INTERNAL_ERROR" ), + error.IsEmpty() ? _( "Unable to store payload." ) : error ); + + return; + } + + respondWithError( command, messageId, wxS( "UNKNOWN_COMMAND" ), + wxString::Format( _( "Command '%s' is not supported." ), command ) ); +} + + +bool PANEL_REMOTE_SYMBOL::ensureDestinationRoot( wxFileName& aOutDir, wxString& aError ) const +{ + EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ); + + if( !settings ) + { + aError = _( "Unable to load schematic settings." ); + return false; + } + + wxString destination = settings->m_RemoteSymbol.destination_dir; + + if( destination.IsEmpty() ) + destination = EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG::DefaultDestinationDir(); + + destination = ExpandEnvVarSubstitutions( destination, + &Pgm().GetSettingsManager().Prj() ); + destination.Trim( true ).Trim( false ); + + if( destination.IsEmpty() ) + { + aError = _( "Destination directory is not configured." ); + return false; + } + + wxFileName dir = wxFileName::DirName( destination ); + dir.Normalize( FN_NORMALIZE_FLAGS ); + + if( !dir.DirExists() ) + { + if( !dir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create directory '%s'." ), dir.GetFullPath() ); + return false; + } + } + + aOutDir = dir; + return true; +} + + +bool PANEL_REMOTE_SYMBOL::ensureSymbolLibraryEntry( const wxFileName& aLibraryFile, + const wxString& aNickname, bool aGlobalTable, + wxString& aError ) const +{ + LIBRARY_MANAGER& manager = Pgm().GetLibraryManager(); + std::optional tableOpt = manager.Table( LIBRARY_TABLE_TYPE::SYMBOL, + aGlobalTable ? LIBRARY_TABLE_SCOPE::GLOBAL : LIBRARY_TABLE_SCOPE::PROJECT ); + + if( !tableOpt ) + { + aError = _( "Unable to access the symbol library table." ); + return false; + } + + LIBRARY_TABLE* table = *tableOpt; + const wxString fullPath = aLibraryFile.GetFullPath(); + + if( table->HasRow( aNickname ) ) + { + if( std::optional rowOpt = table->Row( aNickname ); rowOpt ) + { + LIBRARY_TABLE_ROW* row = *rowOpt; + + if( row->URI() != fullPath ) + { + row->SetURI( fullPath ); + + if( !table->Save() ) + { + aError = _( "Failed to update the symbol library table." ); + return false; + } + } + } + + return true; + } + + LIBRARY_TABLE_ROW& row = table->InsertRow(); + row.SetNickname( aNickname ); + row.SetURI( fullPath ); + row.SetType( wxS( "KiCad" ) ); + row.SetOptions( wxString() ); + row.SetDescription( _( "Remote download" ) ); + row.SetOk( true ); + + if( !table->Save() ) + { + aError = _( "Failed to save the symbol library table." ); + return false; + } + + return true; +} + + +bool PANEL_REMOTE_SYMBOL::ensureFootprintLibraryEntry( const wxFileName& aLibraryDir, + const wxString& aNickname, + bool aGlobalTable, wxString& aError ) const +{ + LIBRARY_MANAGER& manager = Pgm().GetLibraryManager(); + std::optional tableOpt = manager.Table( LIBRARY_TABLE_TYPE::FOOTPRINT, + aGlobalTable ? LIBRARY_TABLE_SCOPE::GLOBAL : LIBRARY_TABLE_SCOPE::PROJECT ); + + if( !tableOpt ) + { + aError = _( "Unable to access the footprint library table." ); + return false; + } + + LIBRARY_TABLE* table = *tableOpt; + const wxString fullPath = aLibraryDir.GetFullPath(); + + if( table->HasRow( aNickname ) ) + { + if( std::optional rowOpt = table->Row( aNickname ); rowOpt ) + { + LIBRARY_TABLE_ROW* row = *rowOpt; + + if( row->URI() != fullPath ) + { + row->SetURI( fullPath ); + + if( !table->Save() ) + { + aError = _( "Failed to update the footprint library table." ); + return false; + } + } + } + + return true; + } + + LIBRARY_TABLE_ROW& row = table->InsertRow(); + row.SetNickname( aNickname ); + row.SetURI( fullPath ); + row.SetType( wxS( "KiCad" ) ); + row.SetOptions( wxString() ); + row.SetDescription( _( "Remote download" ) ); + row.SetOk( true ); + + if( !table->Save() ) + { + aError = _( "Failed to save the footprint library table." ); + return false; + } + + return true; +} + + +wxString PANEL_REMOTE_SYMBOL::sanitizedPrefix() const +{ + wxString prefix; + + if( EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ) ) + prefix = settings->m_RemoteSymbol.library_prefix; + + if( prefix.IsEmpty() ) + prefix = EESCHEMA_SETTINGS::REMOTE_SYMBOL_CONFIG::DefaultLibraryPrefix(); + + prefix.Trim( true ).Trim( false ); + + if( prefix.IsEmpty() ) + prefix = wxS( "remote" ); + + for( size_t i = 0; i < prefix.length(); ++i ) + { + wxUniChar ch = prefix[i]; + + if( !( wxIsalnum( ch ) || ch == '_' || ch == '-' ) ) + prefix[i] = '_'; + } + + return prefix; +} + + +bool PANEL_REMOTE_SYMBOL::placeDownloadedSymbol( const wxString& aNickname, + const wxString& aLibItemName, + wxString& aError ) +{ + if( !m_frame ) + { + aError = _( "No schematic editor is available for placement." ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: no frame available" ); + return false; + } + + if( aNickname.IsEmpty() || aLibItemName.IsEmpty() ) + { + aError = _( "Downloaded symbol metadata is incomplete." ); + return false; + } + + LIB_ID libId; + libId.SetLibNickname( aNickname ); + libId.SetLibItemName( aLibItemName ); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "placeDownloadedSymbol: attempting GetLibSymbol for %s:%s", + aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() ); + + // Ensure the library adapter has loaded this library + SYMBOL_LIBRARY_ADAPTER* adapter = PROJECT_SCH::SymbolLibAdapter( &m_frame->Prj() ); + if( adapter ) + { + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "placeDownloadedSymbol: got adapter, attempting LoadSymbol" ); + LIB_SYMBOL* adapterSymbol = adapter->LoadSymbol( aNickname, aLibItemName ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "placeDownloadedSymbol: adapter LoadSymbol returned %s", + adapterSymbol ? "valid symbol" : "nullptr" ); + } + + LIB_SYMBOL* libSymbol = m_frame->GetLibSymbol( libId ); + + if( !libSymbol ) + { + aError = _( "Unable to load the downloaded symbol for placement." ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: failed to load libSymbol %s:%s", + aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() ); + return false; + } + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: loaded libSymbol %s:%s", + aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() ); + + SCH_SYMBOL* symbol = new SCH_SYMBOL( *libSymbol, libId, &m_frame->GetCurrentSheet(), 1 ); + symbol->SetParent( m_frame->GetScreen() ); + + if( EESCHEMA_SETTINGS* cfg = m_frame->eeconfig(); cfg && cfg->m_AutoplaceFields.enable ) + { + symbol->AutoplaceFields( nullptr, AUTOPLACE_AUTO ); + } + + TOOL_MANAGER* toolMgr = m_frame->GetToolManager(); + + if( !toolMgr ) + { + delete symbol; + aError = _( "Unable to access the schematic placement tools." ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: no tool manager available" ); + return false; + } + + m_frame->Raise(); + toolMgr->PostAction( SCH_ACTIONS::placeSymbol, + SCH_ACTIONS::PLACE_SYMBOL_PARAMS{ symbol, true } ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "placeDownloadedSymbol: posted placeSymbol action for %s:%s", + aNickname.ToUTF8().data(), aLibItemName.ToUTF8().data() ); + return true; +} + + +bool PANEL_REMOTE_SYMBOL::receiveSymbol( const nlohmann::json& aParams, + const std::vector& aPayload, + wxString& aError ) +{ + const wxString mode = jsonString( aParams, "mode" ); + const bool placeAfterDownload = mode.IsSameAs( wxS( "PLACE" ), false ); + + if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) + && !mode.IsSameAs( wxS( "PLACE" ), false ) ) + { + aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode ); + return false; + } + + const wxString contentType = jsonString( aParams, "content_type" ); + + if( !contentType.IsSameAs( wxS( "KICAD_SYMBOL_V1" ), false ) ) + { + aError = _( "Unsupported symbol payload type." ); + return false; + } + + if( !m_frame ) + { + aError = _( "No schematic editor is available to store symbols." ); + return false; + } + + EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ); + + if( !settings ) + { + aError = _( "Unable to load schematic settings." ); + return false; + } + + const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table; + + wxFileName baseDir; + + if( !ensureDestinationRoot( baseDir, aError ) ) + return false; + + wxFileName symDir = baseDir; + symDir.AppendDir( wxS( "symbols" ) ); + + if( !symDir.DirExists() && !symDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), symDir.GetFullPath() ); + return false; + } + + wxString libItemName = jsonString( aParams, "name" ); + wxString baseName = libItemName; + + if( baseName.IsEmpty() ) + baseName = jsonString( aParams, "library" ); + + if( baseName.IsEmpty() ) + baseName = wxS( "symbol" ); + + baseName.Trim( true ).Trim( false ); + + if( libItemName.IsEmpty() ) + libItemName = baseName; + + wxString sanitizedName = sanitizeFileComponent( baseName, wxS( "symbol" ) ); + + if( libItemName.IsEmpty() ) + libItemName = sanitizedName; + + const wxString nickname = sanitizedPrefix() + wxS( "_sym_" ) + sanitizedName; + + wxFileName outFile( symDir ); + outFile.SetFullName( nickname + wxS( ".kicad_sym" ) ); + + if( !ensureSymbolLibraryEntry( outFile, nickname, addToGlobal, aError ) ) + return false; + + SYMBOL_LIBRARY_ADAPTER* adapter = PROJECT_SCH::SymbolLibAdapter( &m_frame->Prj() ); + + if( !adapter ) + { + aError = _( "Unable to access the symbol library manager." ); + return false; + } + + // Ensure the adapter has loaded this library (it was just added to the table) + std::optional libStatus = adapter->LoadOne( nickname ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "receiveSymbol: LoadOne(%s) returned %s", + nickname.ToUTF8().data(), + libStatus.has_value() ? "valid status" : "nullopt" ); + + std::unique_ptr downloadedSymbol = loadSymbolFromPayload( aPayload, libItemName, aError ); + + if( !downloadedSymbol ) + { + if( aError.IsEmpty() ) + aError = _( "Unable to parse the downloaded symbol." ); + + return false; + } + + downloadedSymbol->SetName( libItemName ); + + LIB_ID savedId; + savedId.SetLibNickname( nickname ); + savedId.SetLibItemName( libItemName ); + downloadedSymbol->SetLibId( savedId ); + + if( adapter->SaveSymbol( nickname, downloadedSymbol.get(), true ) + != SYMBOL_LIBRARY_ADAPTER::SAVE_OK ) + { + aError = _( "Unable to save the downloaded symbol." ); + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "receiveSymbol: failed to save symbol %s to library %s", + libItemName.ToUTF8().data(), nickname.ToUTF8().data() ); + return false; + } + + // SaveSymbol transfers ownership to the cache, so release our unique_ptr + downloadedSymbol.release(); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), + "receiveSymbol: saved symbol %s into library %s", libItemName.ToUTF8().data(), + nickname.ToUTF8().data() ); + + const LIBRARY_TABLE_SCOPE scope = addToGlobal ? LIBRARY_TABLE_SCOPE::GLOBAL + : LIBRARY_TABLE_SCOPE::PROJECT; + + // Place before forcing a library reload so we can use the existing in-memory cache. + // Reloading first invalidates the adapter's cache, causing GetLibSymbol() to fail + // because LoadSymbol() does not auto-load libraries (it only fetches if already loaded). + if( placeAfterDownload ) + { + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSymbol: placing symbol now (nickname=%s libItem=%s)", + nickname.ToUTF8().data(), libItemName.ToUTF8().data() ); + bool placedOk = placeDownloadedSymbol( nickname, libItemName, aError ); + + // Perform the reload afterwards so subsequent operations see the fresh library on disk. + Pgm().GetLibraryManager().ReloadLibraryEntry( LIBRARY_TABLE_TYPE::SYMBOL, nickname, scope ); + return placedOk; + } + + // No immediate placement requested; safe to reload now. + Pgm().GetLibraryManager().ReloadLibraryEntry( LIBRARY_TABLE_TYPE::SYMBOL, nickname, scope ); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSymbol: saved symbol (nickname=%s libItem=%s)" , + nickname.ToUTF8().data(), libItemName.ToUTF8().data() ); + + return true; +} + + +bool PANEL_REMOTE_SYMBOL::receiveFootprint( const nlohmann::json& aParams, + const std::vector& aPayload, + wxString& aError ) +{ + const wxString mode = jsonString( aParams, "mode" ); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveFootprint: mode=%s", mode.ToUTF8().data() ); + + if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) + && !mode.IsSameAs( wxS( "PLACE" ), false ) ) + { + aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode ); + return false; + } + + const wxString contentType = jsonString( aParams, "content_type" ); + + if( !contentType.IsSameAs( wxS( "KICAD_FOOTPRINT_V1" ), false ) ) + { + aError = _( "Unsupported footprint payload type." ); + return false; + } + + wxFileName baseDir; + + if( !ensureDestinationRoot( baseDir, aError ) ) + return false; + + wxFileName fpRoot = baseDir; + fpRoot.AppendDir( wxS( "footprints" ) ); + + if( !fpRoot.DirExists() && !fpRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), fpRoot.GetFullPath() ); + return false; + } + + wxString footprintName = jsonString( aParams, "name" ); + + if( footprintName.IsEmpty() ) + footprintName = sanitizedPrefix() + wxS( "_footprint" ); + + footprintName = sanitizeFileComponent( footprintName, sanitizedPrefix() + wxS( "_footprint" ) ); + + wxString libNickname = sanitizedPrefix() + wxS( "_fp_" ) + footprintName; + + wxFileName libDir = fpRoot; + libDir.AppendDir( libNickname + wxS( ".pretty" ) ); + + if( !libDir.DirExists() && !libDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), libDir.GetFullPath() ); + return false; + } + + wxString fileName = footprintName; + + if( !fileName.Lower().EndsWith( wxS( ".kicad_mod" ) ) ) + fileName += wxS( ".kicad_mod" ); + + wxFileName outFile( libDir ); + outFile.SetFullName( fileName ); + + if( !writeBinaryFile( outFile, aPayload, aError ) ) + return false; + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveFootprint: wrote footprint %s in lib %s", + outFile.GetFullPath(), libNickname.ToUTF8().data() ); + + EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ); + + if( !settings ) + { + aError = _( "Unable to load schematic settings." ); + return false; + } + + const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table; + + if( !ensureFootprintLibraryEntry( libDir, libNickname, addToGlobal, aError ) ) + return false; + + const LIBRARY_TABLE_SCOPE scope = addToGlobal ? LIBRARY_TABLE_SCOPE::GLOBAL + : LIBRARY_TABLE_SCOPE::PROJECT; + Pgm().GetLibraryManager().ReloadLibraryEntry( LIBRARY_TABLE_TYPE::FOOTPRINT, libNickname, scope ); + + return true; +} + + +bool PANEL_REMOTE_SYMBOL::receive3DModel( const nlohmann::json& aParams, + const std::vector& aPayload, + wxString& aError ) +{ + const wxString mode = jsonString( aParams, "mode" ); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receive3DModel: mode=%s", mode.ToUTF8().data() ); + + if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) + && !mode.IsSameAs( wxS( "PLACE" ), false ) ) + { + aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode ); + return false; + } + + const wxString contentType = jsonString( aParams, "content_type" ); + + if( !contentType.IsSameAs( wxS( "KICAD_3D_MODEL_STEP" ), false ) && + !contentType.IsSameAs( wxS( "KICAD_3D_MODEL_WRL" ), false ) ) + { + aError = _( "Unsupported 3D model payload type." ); + return false; + } + + wxFileName baseDir; + + if( !ensureDestinationRoot( baseDir, aError ) ) + return false; + + wxFileName modelDir = baseDir; + modelDir.AppendDir( sanitizedPrefix() + wxS( "_3d" ) ); + + if( !modelDir.DirExists() && !modelDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), modelDir.GetFullPath() ); + return false; + } + + wxString fileName = jsonString( aParams, "name" ); + + if( fileName.IsEmpty() ) + fileName = sanitizedPrefix() + wxS( "_model" ); + + fileName = sanitizeFileComponent( fileName, sanitizedPrefix() + wxS( "_model" ) ); + + wxString extension = wxS( ".step" ); + + if( contentType.IsSameAs( wxS( "KICAD_3D_MODEL_WRL" ), false ) ) + extension = wxS( ".wrl" ); + + if( !fileName.Lower().EndsWith( extension ) ) + fileName += extension; + + wxFileName outFile( modelDir ); + outFile.SetFullName( fileName ); + + bool ok = writeBinaryFile( outFile, aPayload, aError ); + + if( ok ) + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receive3DModel: wrote model %s", outFile.GetFullPath() ); + + return ok; +} + + +bool PANEL_REMOTE_SYMBOL::receiveSPICEModel( const nlohmann::json& aParams, + const std::vector& aPayload, + wxString& aError ) +{ + const wxString mode = jsonString( aParams, "mode" ); + + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSPICEModel: mode=%s", mode.ToUTF8().data() ); + + if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) + && !mode.IsSameAs( wxS( "PLACE" ), false ) ) + { + aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode ); + return false; + } + + const wxString contentType = jsonString( aParams, "content_type" ); + + if( !contentType.IsSameAs( wxS( "KICAD_SPICE_MODEL_V1" ), false ) ) + { + aError = _( "Unsupported SPICE payload type." ); + return false; + } + + wxFileName baseDir; + + if( !ensureDestinationRoot( baseDir, aError ) ) + return false; + + wxFileName spiceDir = baseDir; + spiceDir.AppendDir( sanitizedPrefix() + wxS( "_spice" ) ); + + if( !spiceDir.DirExists() && !spiceDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), spiceDir.GetFullPath() ); + return false; + } + + wxString fileName = jsonString( aParams, "name" ); + + if( fileName.IsEmpty() ) + fileName = sanitizedPrefix() + wxS( "_model.cir" ); + + fileName = sanitizeFileComponent( fileName, sanitizedPrefix() + wxS( "_model.cir" ) ); + + if( !fileName.Lower().EndsWith( wxS( ".cir" ) ) ) + fileName += wxS( ".cir" ); + + wxFileName outFile( spiceDir ); + outFile.SetFullName( fileName ); + + bool ok = writeBinaryFile( outFile, aPayload, aError ); + + if( ok ) + wxLogTrace( wxS( "KI_TRACE_REMOTE_SYMBOL" ), "receiveSPICEModel: wrote spice model %s", outFile.GetFullPath() ); + + return ok; +} + + +bool PANEL_REMOTE_SYMBOL::receiveComponent( const nlohmann::json& aParams, + const std::vector& aPayload, + wxString& aError, nlohmann::json* aResponseParams ) +{ + nlohmann::json components; + + try + { + components = nlohmann::json::parse( aPayload.begin(), aPayload.end() ); + } + catch( const std::exception& e ) + { + aError = wxString::Format( _( "Failed to parse component list: %s" ), e.what() ); + return false; + } + + if( !components.is_array() ) + { + aError = _( "Component list must be an array." ); + return false; + } + + if( components.empty() ) + { + aError = _( "Component list was empty." ); + return false; + } + + wxString libNickname = sanitizeFileComponent( wxString::FromUTF8( aParams.value( "library", "" ) ), + wxS( "Remote" ) ); + + if( libNickname.IsEmpty() ) + libNickname = wxS( "Remote" ); + + wxFileName baseDir; + + if( !ensureDestinationRoot( baseDir, aError ) ) + return false; + + auto ensureDirectory = [&]( wxFileName& aDir ) -> bool + { + if( aDir.DirExists() ) + return true; + + if( !aDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), aDir.GetFullPath() ); + return false; + } + + return true; + }; + + wxFileName librariesDir( baseDir ); + librariesDir.AppendDir( wxS( "symbols" ) ); + + if( !ensureDirectory( librariesDir ) ) + return false; + + wxFileName symbolLib( librariesDir ); + symbolLib.SetFullName( libNickname + wxS( ".kicad_sym" ) ); + + wxFileName footprintsDir( baseDir ); + footprintsDir.AppendDir( wxS( "footprints" ) ); + + if( !ensureDirectory( footprintsDir ) ) + return false; + + wxFileName footprintLibDir( footprintsDir ); + footprintLibDir.AppendDir( libNickname + wxS( ".pretty" ) ); + + if( !ensureDirectory( footprintLibDir ) ) + return false; + + wxFileName modelDir( baseDir ); + modelDir.AppendDir( wxS( "3dmodels" ) ); + + if( !ensureDirectory( modelDir ) ) + return false; + + modelDir.AppendDir( libNickname ); + + if( !ensureDirectory( modelDir ) ) + return false; + + EESCHEMA_SETTINGS* settings = GetAppSettings( "eeschema" ); + + if( !settings ) + { + aError = _( "Unable to load schematic settings." ); + return false; + } + + const bool addToGlobal = settings->m_RemoteSymbol.add_to_global_table; + + std::unique_ptr symbolPlugin( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) ); + + if( !symbolPlugin ) + { + aError = _( "Unable to access the KiCad symbol plugin." ); + return false; + } + + struct COMPONENT_PAYLOAD + { + std::string type; + wxString declaredName; + wxString sanitizedName; + wxString finalName; + wxString payloadSymbolName; + std::string checksum; + std::vector content; + bool skip = false; + }; + + std::vector prepared; + prepared.reserve( components.size() ); + + for( const nlohmann::json& component : components ) + { + if( !component.is_object() ) + { + aError = _( "Component entries must be objects." ); + return false; + } + + COMPONENT_PAYLOAD entry; + entry.type = component.value( "type", "" ); + + if( entry.type.empty() ) + { + aError = _( "Component entry was missing a type." ); + return false; + } + + std::transform( entry.type.begin(), entry.type.end(), entry.type.begin(), + []( unsigned char c ) { return static_cast( std::tolower( c ) ); } ); + + entry.declaredName = wxString::FromUTF8( component.value( "name", entry.type ).c_str() ); + entry.declaredName.Trim( true ).Trim( false ); + + wxString fallback = sanitizedPrefix() + wxS( "_" ) + wxString::FromUTF8( entry.type ); + entry.sanitizedName = sanitizeFileComponent( entry.declaredName, fallback ); + + if( entry.sanitizedName.IsEmpty() ) + entry.sanitizedName = fallback; + + entry.finalName = entry.sanitizedName; + entry.payloadSymbolName = entry.declaredName.IsEmpty() ? entry.sanitizedName : entry.declaredName; + + const std::string encoded = component.value( "content", std::string() ); + + if( !decodeBase64Payload( encoded, entry.content, aError ) ) + return false; + + const std::string compression = component.value( "compression", std::string() ); + + if( !compression.empty() && compression != "NONE" ) + { + std::vector decompressed; + + if( !decompressIfNeeded( compression, entry.content, decompressed, aError ) ) + return false; + + entry.content = std::move( decompressed ); + } + + entry.checksum = component.value( "checksum", std::string() ); + + if( entry.checksum.empty() ) + entry.checksum = HashBuffer( entry.content ); + + prepared.emplace_back( std::move( entry ) ); + } + + nlohmann::json skipped = nlohmann::json::array(); + nlohmann::json renamedReport = nlohmann::json::array(); + std::map> renames; + std::set reservedSymbolNames; + std::set reservedFootprintNames; + std::set reservedModelNames; + + auto recordRename = [&]( const std::string& aType, const wxString& aFrom, const wxString& aTo ) + { + if( aFrom == aTo ) + return; + + renames[aType][aFrom] = aTo; + + renamedReport.push_back( { { "type", aType }, + { "from", aFrom.ToStdString() }, + { "to", aTo.ToStdString() } } ); + }; + + auto recordSkip = [&]( const std::string& aType, const wxString& aName ) + { + skipped.push_back( { { "type", aType }, { "name", aName.ToStdString() } } ); + }; + + auto footprintPathFor = [&]( const wxString& aName ) + { + wxFileName fn( footprintLibDir ); + fn.SetFullName( aName + wxS( ".kicad_mod" ) ); + return fn; + }; + + auto modelPathFor = [&]( const wxString& aName ) + { + wxFileName fn( modelDir ); + fn.SetFullName( aName ); + return fn; + }; + + for( COMPONENT_PAYLOAD& entry : prepared ) + { + if( entry.type == "footprint" ) + { + wxFileName existing = footprintPathFor( entry.sanitizedName ); + std::string localChecksum; + + if( HashFile( existing, localChecksum ) && localChecksum == entry.checksum ) + { + entry.skip = true; + recordSkip( entry.type, entry.sanitizedName ); + continue; + } + + wxString candidate = entry.sanitizedName; + int suffix = 1; + + auto collides = [&]( const wxString& name ) + { + if( reservedFootprintNames.contains( name ) ) + return true; + + return footprintPathFor( name ).FileExists(); + }; + + while( collides( candidate ) ) + candidate = AppendNumericSuffix( entry.sanitizedName, suffix++ ); + + reservedFootprintNames.insert( candidate ); + recordRename( entry.type, entry.sanitizedName, candidate ); + entry.finalName = candidate; + } + else if( entry.type == "3dmodel" ) + { + wxFileName existing = modelPathFor( entry.sanitizedName ); + std::string localChecksum; + + if( HashFile( existing, localChecksum ) && localChecksum == entry.checksum ) + { + entry.skip = true; + recordSkip( entry.type, entry.sanitizedName ); + continue; + } + + wxString candidate = entry.sanitizedName; + int suffix = 1; + + auto collides = [&]( const wxString& name ) + { + if( reservedModelNames.contains( name ) ) + return true; + + return modelPathFor( name ).FileExists(); + }; + + while( collides( candidate ) ) + candidate = AppendNumericSuffixToFilename( entry.sanitizedName, suffix++ ); + + reservedModelNames.insert( candidate ); + recordRename( entry.type, entry.sanitizedName, candidate ); + entry.finalName = candidate; + } + else if( entry.type == "symbol" ) + { + std::string localChecksum; + + if( ComputeSymbolChecksum( symbolPlugin.get(), symbolLib, entry.sanitizedName, localChecksum ) + && localChecksum == entry.checksum ) + { + entry.skip = true; + recordSkip( entry.type, entry.sanitizedName ); + continue; + } + + wxString candidate = entry.sanitizedName; + int suffix = 1; + + auto exists = [&]( const wxString& name ) + { + if( reservedSymbolNames.contains( name ) ) + return true; + + std::unique_ptr symbol = TryCloneSymbol( symbolPlugin.get(), symbolLib, name ); + return static_cast( symbol ); + }; + + while( exists( candidate ) ) + candidate = AppendNumericSuffix( entry.sanitizedName, suffix++ ); + + reservedSymbolNames.insert( candidate ); + recordRename( entry.type, entry.sanitizedName, candidate ); + entry.finalName = candidate; + } + else + { + aError = wxString::Format( _( "Unsupported component type '%s'." ), + wxString::FromUTF8( entry.type ) ); + return false; + } + } + + bool footprintLibraryReady = false; + bool symbolLibraryReady = false; + + auto ensureFootprintLibrary = [&]() -> bool + { + if( footprintLibraryReady ) + return true; + + if( !ensureFootprintLibraryEntry( footprintLibDir, libNickname, addToGlobal, aError ) ) + return false; + + footprintLibraryReady = true; + return true; + }; + + auto ensureSymbolLibrary = [&]() -> bool + { + if( symbolLibraryReady ) + return true; + + if( !ensureSymbolLibraryEntry( symbolLib, libNickname, addToGlobal, aError ) ) + return false; + + symbolLibraryReady = true; + return true; + }; + + for( COMPONENT_PAYLOAD& entry : prepared ) + { + if( entry.skip ) + continue; + + if( entry.type == "footprint" ) + { + if( !ensureFootprintLibrary() ) + return false; + + if( !renames["3dmodel"].empty() ) + { + std::string contentStr( entry.content.begin(), entry.content.end() ); + + for( const auto& [oldModel, newModel] : renames["3dmodel"] ) + { + const std::string from = oldModel.ToStdString(); + const std::string to = newModel.ToStdString(); + size_t pos = contentStr.find( from ); + + while( pos != std::string::npos ) + { + contentStr.replace( pos, from.size(), to ); + pos = contentStr.find( from, pos + to.size() ); + } + } + + entry.content.assign( contentStr.begin(), contentStr.end() ); + } + + wxFileName target = footprintPathFor( entry.finalName ); + + if( !writeBinaryFile( target, entry.content, aError ) ) + return false; + } + else if( entry.type == "3dmodel" ) + { + wxFileName target = modelPathFor( entry.finalName ); + + if( !target.GetPath().IsEmpty() ) + { + wxFileName parent( target.GetPath(), wxEmptyString ); + + if( !parent.DirExists() && !parent.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) ) + { + aError = wxString::Format( _( "Unable to create '%s'." ), parent.GetFullPath() ); + return false; + } + } + + if( !writeBinaryFile( target, entry.content, aError ) ) + return false; + } + else if( entry.type == "symbol" ) + { + if( !ensureSymbolLibrary() ) + return false; + + if( !renames["footprint"].empty() ) + { + std::string contentStr( entry.content.begin(), entry.content.end() ); + + for( const auto& [oldFp, newFp] : renames["footprint"] ) + { + const std::string from = oldFp.ToStdString(); + const std::string to = newFp.ToStdString(); + size_t pos = contentStr.find( from ); + + while( pos != std::string::npos ) + { + contentStr.replace( pos, from.size(), to ); + pos = contentStr.find( from, pos + to.size() ); + } + } + + entry.content.assign( contentStr.begin(), contentStr.end() ); + } + + std::unique_ptr symbol = loadSymbolFromPayload( entry.content, + entry.payloadSymbolName, + aError ); + + if( !symbol ) + return false; + + if( entry.finalName != entry.sanitizedName ) + symbol->SetName( entry.finalName ); + + try + { + if( !symbolLib.FileExists() ) + symbolPlugin->SaveLibrary( symbolLib.GetFullPath() ); + + symbolPlugin->SaveSymbol( symbolLib.GetFullPath(), symbol.get() ); + symbol.release(); + } + catch( const IO_ERROR& ioe ) + { + aError = ioe.What(); + return false; + } + } + } + + if( aResponseParams ) + { + nlohmann::json response = nlohmann::json::object(); + + if( !skipped.empty() ) + response["skipped"] = skipped; + + if( !renamedReport.empty() ) + response["renamed"] = renamedReport; + + *aResponseParams = std::move( response ); + } + + return true; +} \ No newline at end of file diff --git a/eeschema/widgets/panel_remote_symbol.h b/eeschema/widgets/panel_remote_symbol.h new file mode 100644 index 0000000000..3ed15617c3 --- /dev/null +++ b/eeschema/widgets/panel_remote_symbol.h @@ -0,0 +1,148 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef PANEL_REMOTE_SYMBOL_H +#define PANEL_REMOTE_SYMBOL_H + + +#include +#include +#include +#include + +#include +#include +#include +#include + +class BITMAP_BUTTON; +class LIB_SYMBOL; +class SCH_EDIT_FRAME; +class WEBVIEW_PANEL; +class wxChoice; +class wxCommandEvent; +class wxWebViewEvent; +class REMOTE_LOGIN_SERVER; + +#define REMOTE_SYMBOL_SESSION_VERSION 1 + +class PANEL_REMOTE_SYMBOL : public wxPanel +{ +public: + explicit PANEL_REMOTE_SYMBOL( SCH_EDIT_FRAME* aParent ); + + void RefreshDataSources(); + bool HasDataSources() const; + +private: + void onDataSourceChanged( wxCommandEvent& aEvent ); + void onConfigure( wxCommandEvent& aEvent ); + void onRefresh( wxCommandEvent& aEvent ); + void onWebViewLoaded( wxWebViewEvent& aEvent ); + void onRemoteLoginResult( wxCommandEvent& aEvent ); + + bool loadDataSource( size_t aIndex ); + bool loadDataSource( const PCM_INSTALLATION_ENTRY& aEntry ); + std::optional findDataSourceJson( const PCM_INSTALLATION_ENTRY& aEntry ) const; + void showMessage( const wxString& aMessage ); + std::optional extractUrlFromJson( const wxString& aJsonContent ) const; + void onKicadMessage( const wxString& aMessage ); + void handleRpcMessage( const nlohmann::json& aMessage ); + void beginSessionHandshake(); + void handleRemoteLogin( const nlohmann::json& aParams, int aMessageId ); + void stopLoginServer(); + void storeUserIdForActiveSource( const wxString& aUserId ); + void loadStoredUserIdForActiveSource(); + wxString currentDataSourceKey() const; + + void sendRpcMessage( const wxString& aCommand, + nlohmann::json aParameters = nlohmann::json::object(), + std::optional aResponseTo = std::nullopt, + const wxString& aStatus = wxS( "OK" ), + const std::string& aData = std::string(), + const wxString& aErrorCode = wxEmptyString, + const wxString& aErrorMessage = wxEmptyString ); + + void respondWithError( const wxString& aCommand, int aResponseTo, + const wxString& aErrorCode, const wxString& aErrorMessage ); + + bool ensureDestinationRoot( wxFileName& aOutDir, wxString& aError ) const; + bool ensureSymbolLibraryEntry( const wxFileName& aLibraryFile, const wxString& aNickname, + bool aGlobalTable, wxString& aError ) const; + bool ensureFootprintLibraryEntry( const wxFileName& aLibraryDir, const wxString& aNickname, + bool aGlobalTable, wxString& aError ) const; + wxString sanitizedPrefix() const; + + bool receiveFootprint( const nlohmann::json& aParams, const std::vector& aPayload, + wxString& aError ); + bool receiveSymbol( const nlohmann::json& aParams, const std::vector& aPayload, + wxString& aError ); + bool receive3DModel( const nlohmann::json& aParams, const std::vector& aPayload, + wxString& aError ); + bool receiveSPICEModel( const nlohmann::json& aParams, const std::vector& aPayload, + wxString& aError ); + bool receiveComponent( const nlohmann::json& aParams, const std::vector& aPayload, + wxString& aError, nlohmann::json* aResponseParams = nullptr ); + bool placeDownloadedSymbol( const wxString& aNickname, const wxString& aLibItemName, + wxString& aError ); + + wxString sanitizeFileComponent( const wxString& aComponent, const wxString& aDefault ) const; + wxString sanitizeForScript( const std::string& aJson ) const; + + wxString jsonString( const nlohmann::json& aObject, const char* aKey ) const; + wxString normalizeDataSourceUrl( const wxString& aUrl ) const; + + bool decodeBase64Payload( const std::string& aMessage, + std::vector& aOutPayload, + wxString& aError ) const; + + bool decompressIfNeeded( const std::string& aCompression, + const std::vector& aInput, + std::vector& aOutput, + wxString& aError ) const; + + bool writeBinaryFile( const wxFileName& aFile, + const std::vector& aData, + wxString& aError ) const; + + std::unique_ptr loadSymbolFromPayload( const std::vector& aPayload, + const wxString& aLibItemName, + wxString& aError ) const; + +private: + SCH_EDIT_FRAME* m_frame; + wxChoice* m_dataSourceChoice; + BITMAP_BUTTON* m_configButton; + BITMAP_BUTTON* m_refreshButton; + WEBVIEW_PANEL* m_webView; + std::shared_ptr m_pcm; + std::vector m_dataSources; + KIID m_sessionId; + int m_messageIdCounter; + bool m_pendingHandshake; + std::unique_ptr m_loginServer; + wxString m_activeDataSourceUrl; + wxString m_activeUserId; +}; + +#endif // PANEL_REMOTE_SYMBOL_H diff --git a/include/eda_draw_frame.h b/include/eda_draw_frame.h index 1181b9cf64..23a192d2b1 100644 --- a/include/eda_draw_frame.h +++ b/include/eda_draw_frame.h @@ -438,6 +438,8 @@ public: static const wxString DesignBlocksPaneName() { return wxS( "DesignBlocks" ); } + static const wxString RemoteSymbolPaneName() { return wxS( "RemoteSymbol" ); } + static const wxString AppearancePanelName() { return wxS( "LayersManager" ); } /** diff --git a/include/frame_type.h b/include/frame_type.h index 2045e14365..cb11f18daf 100644 --- a/include/frame_type.h +++ b/include/frame_type.h @@ -82,6 +82,7 @@ enum FRAME_T PANEL_SCH_TOOLBARS, PANEL_SCH_FIELD_NAME_TEMPLATES, PANEL_SCH_SIMULATOR, + PANEL_SCH_DATA_SOURCES, PANEL_FP_DISPLAY_OPTIONS, PANEL_FP_GRIDS, diff --git a/include/libraries/library_manager.h b/include/libraries/library_manager.h index 11fc1940e6..f0a1a0eb42 100644 --- a/include/libraries/library_manager.h +++ b/include/libraries/library_manager.h @@ -148,6 +148,9 @@ public: /// Returns a list of all library nicknames and their status (even if they failed to load) std::vector> GetLibraryStatuses() const; + void ReloadLibraryEntry( const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ); + /// Return true if the given nickname exists and is not a read-only library virtual bool IsWritable( const wxString& aNickname ) const; @@ -174,6 +177,11 @@ protected: /// Fetches a loaded library, triggering a load of that library if it isn't loaded yet LIBRARY_RESULT loadIfNeeded( const wxString& aNickname ); + LIBRARY_RESULT loadFromScope( const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope, + std::map& aTarget, + std::mutex& aMutex ); + /// Aborts any async load in progress; blocks until fully done aborting void abortLoad(); @@ -272,6 +280,9 @@ public: LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ) const; + void ReloadLibraryEntry( LIBRARY_TABLE_TYPE aType, const wxString& aNickname, + LIBRARY_TABLE_SCOPE aScope = LIBRARY_TABLE_SCOPE::BOTH ); + void LoadProjectTables( const wxString& aProjectPath ); /** diff --git a/include/remote_login_server.h b/include/remote_login_server.h new file mode 100644 index 0000000000..45c9e37af5 --- /dev/null +++ b/include/remote_login_server.h @@ -0,0 +1,57 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/gpl-3.0.html + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once +#include +#include +#include +#include + +#include + +wxDECLARE_EVENT( EVT_REMOTE_SYMBOL_LOGIN_RESULT, wxCommandEvent ); + +class REMOTE_LOGIN_SERVER : public wxEvtHandler +{ +public: + REMOTE_LOGIN_SERVER( wxEvtHandler* aOwner, const wxString& aRedirectUrl ); + ~REMOTE_LOGIN_SERVER() override; + + bool Start(); + unsigned short GetPort() const { return m_port; } + +private: + void OnSocketEvent( wxSocketEvent& aEvent ); + void OnTimeout( wxTimerEvent& aEvent ); + void HandleClient( wxSocketBase* aClient ); + wxString ExtractUserId( const wxString& aRequestLine ) const; + void SendHttpResponse( wxSocketBase* aClient ); + void Finish( bool aSuccess, const wxString& aUserId ); + void Shutdown(); + + wxEvtHandler* m_owner; + wxString m_redirectUrl; + std::unique_ptr m_server; + wxTimer m_timeout; + unsigned short m_port; + bool m_done; +}; \ No newline at end of file diff --git a/include/widgets/webview_panel.h b/include/widgets/webview_panel.h index ddffdf5168..cf950daa87 100644 --- a/include/widgets/webview_panel.h +++ b/include/widgets/webview_panel.h @@ -25,13 +25,17 @@ #include #include +class TOOL_MANAGER; +class TOOL_BASE; + class WEBVIEW_PANEL : public wxPanel { public: using MESSAGE_HANDLER = std::function; explicit WEBVIEW_PANEL( wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, - const wxSize& size = wxDefaultSize, const int style = 0 ); + const wxSize& size = wxDefaultSize, const int style = 0, + TOOL_MANAGER* aToolManager = nullptr, TOOL_BASE* aTool = nullptr ); ~WEBVIEW_PANEL() override; wxWebView* GetWebView() const { return m_browser; } @@ -42,6 +46,16 @@ public: bool AddMessageHandler( const wxString& name, MESSAGE_HANDLER handler ); void ClearMessageHandlers(); + void SetHandleExternalLinks( bool aHandle ) { m_handleExternalLinks = aHandle; } + bool GetHandleExternalLinks() const { return m_handleExternalLinks; } + + void RunScriptAsync( const wxString& aScript, void* aClientData = nullptr ) const + { + m_browser->RunScriptAsync( aScript, aClientData ); + } + + bool HasLoadError() const { return m_loadError; } + protected: void OnNavigationRequest( wxWebViewEvent& evt ); void OnWebViewLoaded( wxWebViewEvent& evt ); @@ -51,9 +65,14 @@ protected: void OnError( wxWebViewEvent& evt ); private: + bool m_initialized; + bool m_handleExternalLinks; + bool m_loadError; wxWebView* m_browser; std::map m_msgHandlers; + TOOL_MANAGER* m_toolManager; + TOOL_BASE* m_tool; }; #endif // WEBVIEW_PANEL_H diff --git a/kicad/pcm/dialogs/dialog_pcm.cpp b/kicad/pcm/dialogs/dialog_pcm.cpp index 3defdbb021..d6d89c4aad 100644 --- a/kicad/pcm/dialogs/dialog_pcm.cpp +++ b/kicad/pcm/dialogs/dialog_pcm.cpp @@ -51,6 +51,7 @@ static std::vector> PACKAGE_TYPE_LIST = { { PT_PLUGIN, _( "Plugins (%d)" ) }, { PT_FAB, _( "Fabrication plugins (%d)" ) }, { PT_LIBRARY, _( "Libraries (%d)" ) }, + { PT_DATASOURCE, _( "Data sources (%d)" ) }, { PT_COLORTHEME, _( "Color themes (%d)" ) }, }; @@ -201,6 +202,19 @@ DIALOG_PCM::~DIALOG_PCM() } +void DIALOG_PCM::SetActivePackageType( PCM_PACKAGE_TYPE aType ) +{ + for( size_t i = 0; i < PACKAGE_TYPE_LIST.size(); ++i ) + { + if( PACKAGE_TYPE_LIST[i].first == aType ) + { + m_contentNotebook->SetSelection( i ); + break; + } + } +} + + void DIALOG_PCM::OnUpdateEventButtons( wxUpdateUIEvent& event ) { event.Enable( !m_pendingActions.empty() ); diff --git a/kicad/pcm/dialogs/dialog_pcm.h b/kicad/pcm/dialogs/dialog_pcm.h index d42e3a7eae..cd22c5b336 100644 --- a/kicad/pcm/dialogs/dialog_pcm.h +++ b/kicad/pcm/dialogs/dialog_pcm.h @@ -83,6 +83,8 @@ public: return m_changed_package_types; }; + void SetActivePackageType( PCM_PACKAGE_TYPE aType ); + private: /** * @brief Gets package data from PCM and displays it on repository tab diff --git a/kicad/pcm/pcm_data.h b/kicad/pcm/pcm_data.h index 1f1c738961..71e19bd578 100644 --- a/kicad/pcm/pcm_data.h +++ b/kicad/pcm/pcm_data.h @@ -44,6 +44,7 @@ enum PCM_PACKAGE_TYPE PT_PLUGIN, PT_FAB, PT_LIBRARY, + PT_DATASOURCE, PT_COLORTHEME, }; @@ -172,6 +173,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM( PCM_PACKAGE_TYPE, { { PT_PLUGIN, "plugin" }, { PT_FAB, "fab" }, { PT_LIBRARY, "library" }, + { PT_DATASOURCE, "datasource" }, { PT_COLORTHEME, "colortheme" }, } ) diff --git a/kicad/pcm/schemas/pcm.v1.schema.json b/kicad/pcm/schemas/pcm.v1.schema.json index ec36b80cc0..85b8151994 100644 --- a/kicad/pcm/schemas/pcm.v1.schema.json +++ b/kicad/pcm/schemas/pcm.v1.schema.json @@ -33,6 +33,7 @@ "enum": [ "plugin", "library", + "datasource", "fab", "colortheme" ], diff --git a/qa/data/config/9.99/eeschema.json b/qa/data/config/9.99/eeschema.json index 61dca48319..2b45d63282 100644 --- a/qa/data/config/9.99/eeschema.json +++ b/qa/data/config/9.99/eeschema.json @@ -48,6 +48,10 @@ "design_blocks_panel_float_height": -1, "design_blocks_panel_float_width": -1, "design_blocks_show": false, + "remote_symbol_panel_docked_width": -1, + "remote_symbol_panel_float_height": -1, + "remote_symbol_panel_float_width": -1, + "remote_symbol_show": false, "float_net_nav_panel": false, "hierarchy_panel_docked_height": -1, "hierarchy_panel_docked_width": -1, diff --git a/utils/webview_test/kicad-symbol-server-rpc-v1-combined.schema.json b/utils/webview_test/kicad-symbol-server-rpc-v1-combined.schema.json new file mode 100644 index 0000000000..b742f96b75 --- /dev/null +++ b/utils/webview_test/kicad-symbol-server-rpc-v1-combined.schema.json @@ -0,0 +1,377 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://go.kicad.org/schemas/kicad-rpc-v1-message-combined.json", + "title": "KiCad Remote Content Protocol Message (v1, combined)", + "type": "object", + "additionalProperties": false, + + "required": [ + "version", + "session_id", + "message_id", + "command" + ], + + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "description": "Protocol major version. For this schema: 1." + }, + + "session_id": { + "type": "string", + "description": "Session UUID (UUID-4).", + "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$" + }, + + "message_id": { + "type": "integer", + "minimum": 1, + "description": "Monotonically increasing per-session message ID, starting at 1." + }, + + "response_to": { + "type": "integer", + "minimum": 1, + "description": "If present, refers to the message_id of the message being responded to." + }, + + "command": { + "type": "string", + "description": "Command name for this message.", + "enum": [ + "NEW_SESSION", + "GET_KICAD_VERSION", + "LIST_SUPPORTED_VERSIONS", + "CAPABILITIES", + "PING", + "PONG", + "LOGOUT", + "DL_FOOTPRINT", + "DL_SYMBOL", + "DL_SPICE", + "DL_3DMODEL" + ] + }, + + "status": { + "type": "string", + "description": "Status of the message, especially for responses.", + "enum": ["OK", "ERROR", "PENDING"] + }, + + "error_code": { + "type": "string", + "description": "Machine-readable error code when status == ERROR.", + "enum": [ + "UNKNOWN_COMMAND", + "UNSUPPORTED_VERSION", + "INVALID_PARAMETERS", + "INVALID_PAYLOAD", + "INTERNAL_ERROR", + "TOO_LARGE", + "UNAUTHORIZED_ORIGIN", + "NOT_FOUND", + "CONFLICT" + ] + }, + + "error_message": { + "type": "string", + "description": "Human-readable error description when status == ERROR." + }, + + "parameters": { + "type": "object", + "description": "Command-specific parameters. Keys are strings; values are arbitrary JSON.", + "additionalProperties": true + }, + + "data": { + "type": "string", + "description": "Optional payload data. Typically base64-encoded, possibly compressed.", + "contentEncoding": "base64" + } + }, + + "allOf": [ + { + "if": { + "properties": { + "status": { "const": "ERROR" } + }, + "required": ["status"] + }, + "then": { + "required": ["error_code"] + } + }, + { + "oneOf": [ + { + "title": "NEW_SESSION message", + "properties": { + "command": { "const": "NEW_SESSION" }, + "parameters": { + "type": "object", + "properties": { + "client_name": { "type": "string" }, + "client_version": { "type": "string" }, + "supported_versions": { + "type": "array", + "items": { "type": "integer", "minimum": 1 } + }, + "server_name": { "type": "string" }, + "server_version": { "type": "string" }, + "reason": { "type": "string" } + }, + "additionalProperties": true + } + } + }, + + { + "title": "GET_KICAD_VERSION message", + "properties": { + "command": { "const": "GET_KICAD_VERSION" }, + "parameters": { + "type": "object", + "properties": { + "kicad_version": { "type": "string" } + }, + "additionalProperties": true + } + } + }, + + { + "title": "LIST_SUPPORTED_VERSIONS message", + "properties": { + "command": { "const": "LIST_SUPPORTED_VERSIONS" }, + "parameters": { + "type": "object", + "properties": { + "supported_versions": { + "type": "array", + "items": { "type": "integer", "minimum": 1 } + } + }, + "additionalProperties": true + } + } + }, + + { + "title": "CAPABILITIES message", + "properties": { + "command": { "const": "CAPABILITIES" }, + "parameters": { + "type": "object", + "properties": { + "commands": { + "type": "array", + "items": { "type": "string" } + }, + "max_message_size": { + "type": "number", + "minimum": 0 + }, + "compression": { + "type": "array", + "items": { + "type": "string", + "enum": ["NONE", "ZSTD"] + } + } + }, + "additionalProperties": true + } + } + }, + + { + "title": "PING message", + "properties": { + "command": { "const": "PING" }, + "parameters": { + "type": "object", + "properties": { + "nonce": { + "type": ["string", "number"] + } + }, + "additionalProperties": true + } + } + }, + + { + "title": "PONG message", + "properties": { + "command": { "const": "PONG" }, + "parameters": { + "type": "object", + "properties": { + "nonce": { + "type": ["string", "number"] + }, + "latency_ms": { + "type": "number", + "minimum": 0 + } + }, + "additionalProperties": true + } + } + }, + + { + "title": "LOGOUT message", + "properties": { + "command": { "const": "LOGOUT" }, + "parameters": { + "type": "object", + "additionalProperties": true + } + } + }, + + { + "title": "DL_FOOTPRINT message", + "properties": { + "command": { "const": "DL_FOOTPRINT" }, + "parameters": { + "type": "object", + "required": ["mode", "compression", "content_type"], + "properties": { + "mode": { + "type": "string", + "enum": ["SAVE", "PLACE"] + }, + "compression": { + "type": "string", + "enum": ["NONE", "ZSTD"] + }, + "content_type": { + "type": "string", + "enum": ["KICAD_FOOTPRINT_V1"] + }, + "library": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": true + }, + "required": ["command", "parameters", "data"] + } + }, + + { + "title": "DL_SYMBOL message", + "properties": { + "command": { "const": "DL_SYMBOL" }, + "parameters": { + "type": "object", + "required": ["mode", "compression", "content_type"], + "properties": { + "mode": { + "type": "string", + "enum": ["SAVE", "PLACE"] + }, + "compression": { + "type": "string", + "enum": ["NONE", "ZSTD"] + }, + "content_type": { + "type": "string", + "enum": ["KICAD_SYMBOL_V1"] + }, + "library": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": true + }, + "required": ["command", "parameters", "data"] + } + }, + + { + "title": "DL_SPICE message", + "properties": { + "command": { "const": "DL_SPICE" }, + "parameters": { + "type": "object", + "required": ["mode", "compression", "content_type"], + "properties": { + "mode": { + "type": "string", + "enum": ["SAVE", "PLACE"] + }, + "compression": { + "type": "string", + "enum": ["NONE", "ZSTD"] + }, + "content_type": { + "type": "string", + "enum": ["KICAD_SPICE_MODEL_V1"] + }, + "library": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": true + }, + "required": ["command", "parameters", "data"] + } + }, + + { + "title": "DL_3DMODEL message", + "properties": { + "command": { "const": "DL_3DMODEL" }, + "parameters": { + "type": "object", + "required": ["mode", "compression", "content_type"], + "properties": { + "mode": { + "type": "string", + "enum": ["SAVE", "PLACE"] + }, + "compression": { + "type": "string", + "enum": ["NONE", "ZSTD"] + }, + "content_type": { + "type": "string", + "enum": [ + "KICAD_3D_MODEL_STEP", + "KICAD_3D_MODEL_WRL" + ] + }, + "library": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": true + }, + "required": ["command", "parameters", "data"] + } + } + ] + } + ] +} diff --git a/utils/webview_test/server/data/3dmodels/R_0603_1608Metric.step b/utils/webview_test/server/data/3dmodels/R_0603_1608Metric.step new file mode 100644 index 0000000000..a885593ca3 --- /dev/null +++ b/utils/webview_test/server/data/3dmodels/R_0603_1608Metric.step @@ -0,0 +1,1049 @@ +ISO-10303-21; +HEADER; +/* R_0603_1608Metric.step 3D STEP model for use in ECAD systems + * Copyright (C) 2018, kicad StepUp + * + * This work is licensed under the [Creative Commons CC-BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/legalcode), + * with the following exception: + * To the extent that the creation of electronic designs that use 'Licensed Material' can be considered to be 'Adapted Material', + * then the copyright holder waives article 3 of the license with respect to these designs and any generated files which use data provided + * as part of the 'Licensed Material'. + * You are free to use the library data in your own projects without the obligation to share your project files under this or any other license agreement. + * However, if you wish to redistribute these libraries, or parts thereof (including in modified form) as a collection then the exception above does not apply. + * Please refer to https://github.com/KiCad/kicad-packages3D/blob/master/LICENSE.md for further clarification of the exception. + * Disclaimer of Warranties and Limitation of Liability. + * These libraries are provided in the hope that they will be useful, but are provided without warranty of any kind, express or implied. + * *USE 3D CAD DATA AT YOUR OWN RISK* + * *DO NOT RELY UPON ANY INFORMATION FOUND HERE WITHOUT INDEPENDENT VERIFICATION.* + * + */ + +FILE_DESCRIPTION( +/* description */ ('model of R_0603_1608Metric'), +/* implementation_level */ '2;1'); + +FILE_NAME( +/* name */ 'R_0603_1608Metric.step', +/* time_stamp */ '2018-01-04T00:45:34', +/* author */ ('kicad StepUp','ksu'), +/* organization */ ('FreeCAD'), +/* preprocessor_version */ 'OCC', +/* originating_system */ 'kicad StepUp', +/* authorisation */ ''); + +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; + +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', +'automotive_design',2000,#2); +#2 = APPLICATION_CONTEXT( +'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('R_0603_1608Metric','R_0603_1608Metric','',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#805); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = MANIFOLD_SOLID_BREP('',#16); +#16 = CLOSED_SHELL('',(#17,#57,#140,#165,#190,#257,#274,#291,#340,#357, +#374,#423,#440,#509,#540,#564,#633,#657,#674,#691,#708,#725,#742, +#759,#776,#793)); +#17 = ADVANCED_FACE('',(#18),#52,.F.); +#18 = FACE_BOUND('',#19,.F.); +#19 = EDGE_LOOP('',(#20,#30,#38,#46)); +#20 = ORIENTED_EDGE('',*,*,#21,.F.); +#21 = EDGE_CURVE('',#22,#24,#26,.T.); +#22 = VERTEX_POINT('',#23); +#23 = CARTESIAN_POINT('',(-0.8,-0.4,4.5E-02)); +#24 = VERTEX_POINT('',#25); +#25 = CARTESIAN_POINT('',(-0.8,-0.4,0.405)); +#26 = LINE('',#27,#28); +#27 = CARTESIAN_POINT('',(-0.8,-0.4,0.)); +#28 = VECTOR('',#29,1.); +#29 = DIRECTION('',(0.,0.,1.)); +#30 = ORIENTED_EDGE('',*,*,#31,.T.); +#31 = EDGE_CURVE('',#22,#32,#34,.T.); +#32 = VERTEX_POINT('',#33); +#33 = CARTESIAN_POINT('',(-0.8,0.4,4.5E-02)); +#34 = LINE('',#35,#36); +#35 = CARTESIAN_POINT('',(-0.8,-0.4,4.5E-02)); +#36 = VECTOR('',#37,1.); +#37 = DIRECTION('',(0.,1.,0.)); +#38 = ORIENTED_EDGE('',*,*,#39,.T.); +#39 = EDGE_CURVE('',#32,#40,#42,.T.); +#40 = VERTEX_POINT('',#41); +#41 = CARTESIAN_POINT('',(-0.8,0.4,0.405)); +#42 = LINE('',#43,#44); +#43 = CARTESIAN_POINT('',(-0.8,0.4,0.)); +#44 = VECTOR('',#45,1.); +#45 = DIRECTION('',(0.,0.,1.)); +#46 = ORIENTED_EDGE('',*,*,#47,.F.); +#47 = EDGE_CURVE('',#24,#40,#48,.T.); +#48 = LINE('',#49,#50); +#49 = CARTESIAN_POINT('',(-0.8,-0.4,0.405)); +#50 = VECTOR('',#51,1.); +#51 = DIRECTION('',(0.,1.,0.)); +#52 = PLANE('',#53); +#53 = AXIS2_PLACEMENT_3D('',#54,#55,#56); +#54 = CARTESIAN_POINT('',(-0.8,-0.4,0.)); +#55 = DIRECTION('',(1.,0.,0.)); +#56 = DIRECTION('',(0.,0.,1.)); +#57 = ADVANCED_FACE('',(#58),#135,.F.); +#58 = FACE_BOUND('',#59,.F.); +#59 = EDGE_LOOP('',(#60,#70,#77,#78,#87,#95,#104,#112,#120,#128)); +#60 = ORIENTED_EDGE('',*,*,#61,.F.); +#61 = EDGE_CURVE('',#62,#64,#66,.T.); +#62 = VERTEX_POINT('',#63); +#63 = CARTESIAN_POINT('',(-0.755,-0.4,-2.775557561563E-17)); +#64 = VERTEX_POINT('',#65); +#65 = CARTESIAN_POINT('',(-0.545,-0.4,-2.775557561563E-17)); +#66 = LINE('',#67,#68); +#67 = CARTESIAN_POINT('',(-0.8,-0.4,0.)); +#68 = VECTOR('',#69,1.); +#69 = DIRECTION('',(1.,0.,0.)); +#70 = ORIENTED_EDGE('',*,*,#71,.F.); +#71 = EDGE_CURVE('',#22,#62,#72,.T.); +#72 = CIRCLE('',#73,4.5E-02); +#73 = AXIS2_PLACEMENT_3D('',#74,#75,#76); +#74 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#75 = DIRECTION('',(0.,-1.,0.)); +#76 = DIRECTION('',(0.,0.,1.)); +#77 = ORIENTED_EDGE('',*,*,#21,.T.); +#78 = ORIENTED_EDGE('',*,*,#79,.T.); +#79 = EDGE_CURVE('',#24,#80,#82,.T.); +#80 = VERTEX_POINT('',#81); +#81 = CARTESIAN_POINT('',(-0.755,-0.4,0.45)); +#82 = CIRCLE('',#83,4.5E-02); +#83 = AXIS2_PLACEMENT_3D('',#84,#85,#86); +#84 = CARTESIAN_POINT('',(-0.755,-0.4,0.405)); +#85 = DIRECTION('',(0.,1.,0.)); +#86 = DIRECTION('',(0.,0.,1.)); +#87 = ORIENTED_EDGE('',*,*,#88,.T.); +#88 = EDGE_CURVE('',#80,#89,#91,.T.); +#89 = VERTEX_POINT('',#90); +#90 = CARTESIAN_POINT('',(-0.545,-0.4,0.45)); +#91 = LINE('',#92,#93); +#92 = CARTESIAN_POINT('',(-0.8,-0.4,0.45)); +#93 = VECTOR('',#94,1.); +#94 = DIRECTION('',(1.,0.,0.)); +#95 = ORIENTED_EDGE('',*,*,#96,.F.); +#96 = EDGE_CURVE('',#97,#89,#99,.T.); +#97 = VERTEX_POINT('',#98); +#98 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#99 = CIRCLE('',#100,4.5E-02); +#100 = AXIS2_PLACEMENT_3D('',#101,#102,#103); +#101 = CARTESIAN_POINT('',(-0.545,-0.4,0.405)); +#102 = DIRECTION('',(0.,-1.,0.)); +#103 = DIRECTION('',(0.,0.,1.)); +#104 = ORIENTED_EDGE('',*,*,#105,.F.); +#105 = EDGE_CURVE('',#106,#97,#108,.T.); +#106 = VERTEX_POINT('',#107); +#107 = CARTESIAN_POINT('',(-0.755,-0.4,0.405)); +#108 = LINE('',#109,#110); +#109 = CARTESIAN_POINT('',(-0.755,-0.4,0.405)); +#110 = VECTOR('',#111,1.); +#111 = DIRECTION('',(1.,0.,0.)); +#112 = ORIENTED_EDGE('',*,*,#113,.F.); +#113 = EDGE_CURVE('',#114,#106,#116,.T.); +#114 = VERTEX_POINT('',#115); +#115 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#116 = LINE('',#117,#118); +#117 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#118 = VECTOR('',#119,1.); +#119 = DIRECTION('',(0.,0.,1.)); +#120 = ORIENTED_EDGE('',*,*,#121,.T.); +#121 = EDGE_CURVE('',#114,#122,#124,.T.); +#122 = VERTEX_POINT('',#123); +#123 = CARTESIAN_POINT('',(-0.5,-0.4,4.5E-02)); +#124 = LINE('',#125,#126); +#125 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#126 = VECTOR('',#127,1.); +#127 = DIRECTION('',(1.,0.,0.)); +#128 = ORIENTED_EDGE('',*,*,#129,.T.); +#129 = EDGE_CURVE('',#122,#64,#130,.T.); +#130 = CIRCLE('',#131,4.5E-02); +#131 = AXIS2_PLACEMENT_3D('',#132,#133,#134); +#132 = CARTESIAN_POINT('',(-0.545,-0.4,4.5E-02)); +#133 = DIRECTION('',(0.,1.,0.)); +#134 = DIRECTION('',(0.,0.,1.)); +#135 = PLANE('',#136); +#136 = AXIS2_PLACEMENT_3D('',#137,#138,#139); +#137 = CARTESIAN_POINT('',(-0.8,-0.4,0.)); +#138 = DIRECTION('',(0.,1.,0.)); +#139 = DIRECTION('',(0.,0.,1.)); +#140 = ADVANCED_FACE('',(#141),#160,.T.); +#141 = FACE_BOUND('',#142,.F.); +#142 = EDGE_LOOP('',(#143,#144,#152,#159)); +#143 = ORIENTED_EDGE('',*,*,#71,.T.); +#144 = ORIENTED_EDGE('',*,*,#145,.T.); +#145 = EDGE_CURVE('',#62,#146,#148,.T.); +#146 = VERTEX_POINT('',#147); +#147 = CARTESIAN_POINT('',(-0.755,0.4,-2.775557561563E-17)); +#148 = LINE('',#149,#150); +#149 = CARTESIAN_POINT('',(-0.755,-0.4,-2.775557561563E-17)); +#150 = VECTOR('',#151,1.); +#151 = DIRECTION('',(0.,1.,0.)); +#152 = ORIENTED_EDGE('',*,*,#153,.F.); +#153 = EDGE_CURVE('',#32,#146,#154,.T.); +#154 = CIRCLE('',#155,4.5E-02); +#155 = AXIS2_PLACEMENT_3D('',#156,#157,#158); +#156 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#157 = DIRECTION('',(0.,-1.,0.)); +#158 = DIRECTION('',(0.,0.,1.)); +#159 = ORIENTED_EDGE('',*,*,#31,.F.); +#160 = CYLINDRICAL_SURFACE('',#161,4.5E-02); +#161 = AXIS2_PLACEMENT_3D('',#162,#163,#164); +#162 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#163 = DIRECTION('',(0.,1.,0.)); +#164 = DIRECTION('',(-1.,0.,0.)); +#165 = ADVANCED_FACE('',(#166),#185,.T.); +#166 = FACE_BOUND('',#167,.T.); +#167 = EDGE_LOOP('',(#168,#169,#177,#184)); +#168 = ORIENTED_EDGE('',*,*,#79,.T.); +#169 = ORIENTED_EDGE('',*,*,#170,.T.); +#170 = EDGE_CURVE('',#80,#171,#173,.T.); +#171 = VERTEX_POINT('',#172); +#172 = CARTESIAN_POINT('',(-0.755,0.4,0.45)); +#173 = LINE('',#174,#175); +#174 = CARTESIAN_POINT('',(-0.755,-0.4,0.45)); +#175 = VECTOR('',#176,1.); +#176 = DIRECTION('',(0.,1.,0.)); +#177 = ORIENTED_EDGE('',*,*,#178,.F.); +#178 = EDGE_CURVE('',#40,#171,#179,.T.); +#179 = CIRCLE('',#180,4.5E-02); +#180 = AXIS2_PLACEMENT_3D('',#181,#182,#183); +#181 = CARTESIAN_POINT('',(-0.755,0.4,0.405)); +#182 = DIRECTION('',(0.,1.,0.)); +#183 = DIRECTION('',(0.,0.,1.)); +#184 = ORIENTED_EDGE('',*,*,#47,.F.); +#185 = CYLINDRICAL_SURFACE('',#186,4.5E-02); +#186 = AXIS2_PLACEMENT_3D('',#187,#188,#189); +#187 = CARTESIAN_POINT('',(-0.755,-0.4,0.405)); +#188 = DIRECTION('',(0.,1.,0.)); +#189 = DIRECTION('',(-1.,0.,0.)); +#190 = ADVANCED_FACE('',(#191),#252,.T.); +#191 = FACE_BOUND('',#192,.T.); +#192 = EDGE_LOOP('',(#193,#201,#202,#203,#204,#212,#221,#229,#237,#245) +); +#193 = ORIENTED_EDGE('',*,*,#194,.F.); +#194 = EDGE_CURVE('',#146,#195,#197,.T.); +#195 = VERTEX_POINT('',#196); +#196 = CARTESIAN_POINT('',(-0.545,0.4,-2.775557561563E-17)); +#197 = LINE('',#198,#199); +#198 = CARTESIAN_POINT('',(-0.8,0.4,0.)); +#199 = VECTOR('',#200,1.); +#200 = DIRECTION('',(1.,0.,0.)); +#201 = ORIENTED_EDGE('',*,*,#153,.F.); +#202 = ORIENTED_EDGE('',*,*,#39,.T.); +#203 = ORIENTED_EDGE('',*,*,#178,.T.); +#204 = ORIENTED_EDGE('',*,*,#205,.T.); +#205 = EDGE_CURVE('',#171,#206,#208,.T.); +#206 = VERTEX_POINT('',#207); +#207 = CARTESIAN_POINT('',(-0.545,0.4,0.45)); +#208 = LINE('',#209,#210); +#209 = CARTESIAN_POINT('',(-0.8,0.4,0.45)); +#210 = VECTOR('',#211,1.); +#211 = DIRECTION('',(1.,0.,0.)); +#212 = ORIENTED_EDGE('',*,*,#213,.F.); +#213 = EDGE_CURVE('',#214,#206,#216,.T.); +#214 = VERTEX_POINT('',#215); +#215 = CARTESIAN_POINT('',(-0.5,0.4,0.405)); +#216 = CIRCLE('',#217,4.5E-02); +#217 = AXIS2_PLACEMENT_3D('',#218,#219,#220); +#218 = CARTESIAN_POINT('',(-0.545,0.4,0.405)); +#219 = DIRECTION('',(0.,-1.,0.)); +#220 = DIRECTION('',(0.,0.,1.)); +#221 = ORIENTED_EDGE('',*,*,#222,.F.); +#222 = EDGE_CURVE('',#223,#214,#225,.T.); +#223 = VERTEX_POINT('',#224); +#224 = CARTESIAN_POINT('',(-0.755,0.4,0.405)); +#225 = LINE('',#226,#227); +#226 = CARTESIAN_POINT('',(-0.755,0.4,0.405)); +#227 = VECTOR('',#228,1.); +#228 = DIRECTION('',(1.,0.,0.)); +#229 = ORIENTED_EDGE('',*,*,#230,.F.); +#230 = EDGE_CURVE('',#231,#223,#233,.T.); +#231 = VERTEX_POINT('',#232); +#232 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#233 = LINE('',#234,#235); +#234 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#235 = VECTOR('',#236,1.); +#236 = DIRECTION('',(0.,0.,1.)); +#237 = ORIENTED_EDGE('',*,*,#238,.T.); +#238 = EDGE_CURVE('',#231,#239,#241,.T.); +#239 = VERTEX_POINT('',#240); +#240 = CARTESIAN_POINT('',(-0.5,0.4,4.5E-02)); +#241 = LINE('',#242,#243); +#242 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#243 = VECTOR('',#244,1.); +#244 = DIRECTION('',(1.,0.,0.)); +#245 = ORIENTED_EDGE('',*,*,#246,.T.); +#246 = EDGE_CURVE('',#239,#195,#247,.T.); +#247 = CIRCLE('',#248,4.5E-02); +#248 = AXIS2_PLACEMENT_3D('',#249,#250,#251); +#249 = CARTESIAN_POINT('',(-0.545,0.4,4.5E-02)); +#250 = DIRECTION('',(0.,1.,0.)); +#251 = DIRECTION('',(0.,0.,1.)); +#252 = PLANE('',#253); +#253 = AXIS2_PLACEMENT_3D('',#254,#255,#256); +#254 = CARTESIAN_POINT('',(-0.8,0.4,0.)); +#255 = DIRECTION('',(0.,1.,0.)); +#256 = DIRECTION('',(0.,0.,1.)); +#257 = ADVANCED_FACE('',(#258),#269,.F.); +#258 = FACE_BOUND('',#259,.F.); +#259 = EDGE_LOOP('',(#260,#261,#262,#263)); +#260 = ORIENTED_EDGE('',*,*,#194,.F.); +#261 = ORIENTED_EDGE('',*,*,#145,.F.); +#262 = ORIENTED_EDGE('',*,*,#61,.T.); +#263 = ORIENTED_EDGE('',*,*,#264,.T.); +#264 = EDGE_CURVE('',#64,#195,#265,.T.); +#265 = LINE('',#266,#267); +#266 = CARTESIAN_POINT('',(-0.545,-0.4,-2.775557561563E-17)); +#267 = VECTOR('',#268,1.); +#268 = DIRECTION('',(0.,1.,0.)); +#269 = PLANE('',#270); +#270 = AXIS2_PLACEMENT_3D('',#271,#272,#273); +#271 = CARTESIAN_POINT('',(-0.8,-0.4,0.)); +#272 = DIRECTION('',(0.,0.,1.)); +#273 = DIRECTION('',(1.,0.,0.)); +#274 = ADVANCED_FACE('',(#275),#286,.T.); +#275 = FACE_BOUND('',#276,.T.); +#276 = EDGE_LOOP('',(#277,#278,#279,#280)); +#277 = ORIENTED_EDGE('',*,*,#129,.T.); +#278 = ORIENTED_EDGE('',*,*,#264,.T.); +#279 = ORIENTED_EDGE('',*,*,#246,.F.); +#280 = ORIENTED_EDGE('',*,*,#281,.F.); +#281 = EDGE_CURVE('',#122,#239,#282,.T.); +#282 = LINE('',#283,#284); +#283 = CARTESIAN_POINT('',(-0.5,-0.4,4.5E-02)); +#284 = VECTOR('',#285,1.); +#285 = DIRECTION('',(0.,1.,0.)); +#286 = CYLINDRICAL_SURFACE('',#287,4.5E-02); +#287 = AXIS2_PLACEMENT_3D('',#288,#289,#290); +#288 = CARTESIAN_POINT('',(-0.545,-0.4,4.5E-02)); +#289 = DIRECTION('',(0.,1.,0.)); +#290 = DIRECTION('',(1.,0.,0.)); +#291 = ADVANCED_FACE('',(#292),#335,.F.); +#292 = FACE_BOUND('',#293,.F.); +#293 = EDGE_LOOP('',(#294,#295,#296,#297,#305,#313,#321,#329)); +#294 = ORIENTED_EDGE('',*,*,#121,.F.); +#295 = ORIENTED_EDGE('',*,*,#113,.T.); +#296 = ORIENTED_EDGE('',*,*,#105,.T.); +#297 = ORIENTED_EDGE('',*,*,#298,.T.); +#298 = EDGE_CURVE('',#97,#299,#301,.T.); +#299 = VERTEX_POINT('',#300); +#300 = CARTESIAN_POINT('',(0.5,-0.4,0.405)); +#301 = LINE('',#302,#303); +#302 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#303 = VECTOR('',#304,1.); +#304 = DIRECTION('',(1.,0.,0.)); +#305 = ORIENTED_EDGE('',*,*,#306,.T.); +#306 = EDGE_CURVE('',#299,#307,#309,.T.); +#307 = VERTEX_POINT('',#308); +#308 = CARTESIAN_POINT('',(0.755,-0.4,0.405)); +#309 = LINE('',#310,#311); +#310 = CARTESIAN_POINT('',(-0.755,-0.4,0.405)); +#311 = VECTOR('',#312,1.); +#312 = DIRECTION('',(1.,0.,0.)); +#313 = ORIENTED_EDGE('',*,*,#314,.F.); +#314 = EDGE_CURVE('',#315,#307,#317,.T.); +#315 = VERTEX_POINT('',#316); +#316 = CARTESIAN_POINT('',(0.755,-0.4,4.5E-02)); +#317 = LINE('',#318,#319); +#318 = CARTESIAN_POINT('',(0.755,-0.4,4.5E-02)); +#319 = VECTOR('',#320,1.); +#320 = DIRECTION('',(0.,0.,1.)); +#321 = ORIENTED_EDGE('',*,*,#322,.F.); +#322 = EDGE_CURVE('',#323,#315,#325,.T.); +#323 = VERTEX_POINT('',#324); +#324 = CARTESIAN_POINT('',(0.5,-0.4,4.5E-02)); +#325 = LINE('',#326,#327); +#326 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#327 = VECTOR('',#328,1.); +#328 = DIRECTION('',(1.,0.,0.)); +#329 = ORIENTED_EDGE('',*,*,#330,.F.); +#330 = EDGE_CURVE('',#122,#323,#331,.T.); +#331 = LINE('',#332,#333); +#332 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#333 = VECTOR('',#334,1.); +#334 = DIRECTION('',(1.,0.,0.)); +#335 = PLANE('',#336); +#336 = AXIS2_PLACEMENT_3D('',#337,#338,#339); +#337 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#338 = DIRECTION('',(0.,1.,0.)); +#339 = DIRECTION('',(0.,0.,1.)); +#340 = ADVANCED_FACE('',(#341),#352,.T.); +#341 = FACE_BOUND('',#342,.T.); +#342 = EDGE_LOOP('',(#343,#344,#345,#346)); +#343 = ORIENTED_EDGE('',*,*,#205,.F.); +#344 = ORIENTED_EDGE('',*,*,#170,.F.); +#345 = ORIENTED_EDGE('',*,*,#88,.T.); +#346 = ORIENTED_EDGE('',*,*,#347,.T.); +#347 = EDGE_CURVE('',#89,#206,#348,.T.); +#348 = LINE('',#349,#350); +#349 = CARTESIAN_POINT('',(-0.545,-0.4,0.45)); +#350 = VECTOR('',#351,1.); +#351 = DIRECTION('',(0.,1.,0.)); +#352 = PLANE('',#353); +#353 = AXIS2_PLACEMENT_3D('',#354,#355,#356); +#354 = CARTESIAN_POINT('',(-0.8,-0.4,0.45)); +#355 = DIRECTION('',(0.,0.,1.)); +#356 = DIRECTION('',(1.,0.,0.)); +#357 = ADVANCED_FACE('',(#358),#369,.T.); +#358 = FACE_BOUND('',#359,.F.); +#359 = EDGE_LOOP('',(#360,#361,#362,#363)); +#360 = ORIENTED_EDGE('',*,*,#96,.T.); +#361 = ORIENTED_EDGE('',*,*,#347,.T.); +#362 = ORIENTED_EDGE('',*,*,#213,.F.); +#363 = ORIENTED_EDGE('',*,*,#364,.F.); +#364 = EDGE_CURVE('',#97,#214,#365,.T.); +#365 = LINE('',#366,#367); +#366 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#367 = VECTOR('',#368,1.); +#368 = DIRECTION('',(0.,1.,0.)); +#369 = CYLINDRICAL_SURFACE('',#370,4.5E-02); +#370 = AXIS2_PLACEMENT_3D('',#371,#372,#373); +#371 = CARTESIAN_POINT('',(-0.545,-0.4,0.405)); +#372 = DIRECTION('',(0.,1.,0.)); +#373 = DIRECTION('',(1.,0.,0.)); +#374 = ADVANCED_FACE('',(#375),#418,.T.); +#375 = FACE_BOUND('',#376,.T.); +#376 = EDGE_LOOP('',(#377,#378,#379,#380,#388,#396,#404,#412)); +#377 = ORIENTED_EDGE('',*,*,#238,.F.); +#378 = ORIENTED_EDGE('',*,*,#230,.T.); +#379 = ORIENTED_EDGE('',*,*,#222,.T.); +#380 = ORIENTED_EDGE('',*,*,#381,.T.); +#381 = EDGE_CURVE('',#214,#382,#384,.T.); +#382 = VERTEX_POINT('',#383); +#383 = CARTESIAN_POINT('',(0.5,0.4,0.405)); +#384 = LINE('',#385,#386); +#385 = CARTESIAN_POINT('',(-0.5,0.4,0.405)); +#386 = VECTOR('',#387,1.); +#387 = DIRECTION('',(1.,0.,0.)); +#388 = ORIENTED_EDGE('',*,*,#389,.T.); +#389 = EDGE_CURVE('',#382,#390,#392,.T.); +#390 = VERTEX_POINT('',#391); +#391 = CARTESIAN_POINT('',(0.755,0.4,0.405)); +#392 = LINE('',#393,#394); +#393 = CARTESIAN_POINT('',(-0.755,0.4,0.405)); +#394 = VECTOR('',#395,1.); +#395 = DIRECTION('',(1.,0.,0.)); +#396 = ORIENTED_EDGE('',*,*,#397,.F.); +#397 = EDGE_CURVE('',#398,#390,#400,.T.); +#398 = VERTEX_POINT('',#399); +#399 = CARTESIAN_POINT('',(0.755,0.4,4.5E-02)); +#400 = LINE('',#401,#402); +#401 = CARTESIAN_POINT('',(0.755,0.4,4.5E-02)); +#402 = VECTOR('',#403,1.); +#403 = DIRECTION('',(0.,0.,1.)); +#404 = ORIENTED_EDGE('',*,*,#405,.F.); +#405 = EDGE_CURVE('',#406,#398,#408,.T.); +#406 = VERTEX_POINT('',#407); +#407 = CARTESIAN_POINT('',(0.5,0.4,4.5E-02)); +#408 = LINE('',#409,#410); +#409 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#410 = VECTOR('',#411,1.); +#411 = DIRECTION('',(1.,0.,0.)); +#412 = ORIENTED_EDGE('',*,*,#413,.F.); +#413 = EDGE_CURVE('',#239,#406,#414,.T.); +#414 = LINE('',#415,#416); +#415 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#416 = VECTOR('',#417,1.); +#417 = DIRECTION('',(1.,0.,0.)); +#418 = PLANE('',#419); +#419 = AXIS2_PLACEMENT_3D('',#420,#421,#422); +#420 = CARTESIAN_POINT('',(-0.755,0.4,4.5E-02)); +#421 = DIRECTION('',(0.,1.,0.)); +#422 = DIRECTION('',(0.,0.,1.)); +#423 = ADVANCED_FACE('',(#424),#435,.F.); +#424 = FACE_BOUND('',#425,.F.); +#425 = EDGE_LOOP('',(#426,#427,#428,#434)); +#426 = ORIENTED_EDGE('',*,*,#281,.F.); +#427 = ORIENTED_EDGE('',*,*,#330,.T.); +#428 = ORIENTED_EDGE('',*,*,#429,.T.); +#429 = EDGE_CURVE('',#323,#406,#430,.T.); +#430 = LINE('',#431,#432); +#431 = CARTESIAN_POINT('',(0.5,-0.4,4.5E-02)); +#432 = VECTOR('',#433,1.); +#433 = DIRECTION('',(0.,1.,0.)); +#434 = ORIENTED_EDGE('',*,*,#413,.F.); +#435 = PLANE('',#436); +#436 = AXIS2_PLACEMENT_3D('',#437,#438,#439); +#437 = CARTESIAN_POINT('',(-0.755,-0.4,4.5E-02)); +#438 = DIRECTION('',(0.,0.,1.)); +#439 = DIRECTION('',(1.,0.,0.)); +#440 = ADVANCED_FACE('',(#441),#504,.F.); +#441 = FACE_BOUND('',#442,.F.); +#442 = EDGE_LOOP('',(#443,#453,#460,#461,#462,#463,#472,#480,#489,#497) +); +#443 = ORIENTED_EDGE('',*,*,#444,.F.); +#444 = EDGE_CURVE('',#445,#447,#449,.T.); +#445 = VERTEX_POINT('',#446); +#446 = CARTESIAN_POINT('',(0.545,-0.4,-2.775557561563E-17)); +#447 = VERTEX_POINT('',#448); +#448 = CARTESIAN_POINT('',(0.755,-0.4,-2.775557561563E-17)); +#449 = LINE('',#450,#451); +#450 = CARTESIAN_POINT('',(0.5,-0.4,0.)); +#451 = VECTOR('',#452,1.); +#452 = DIRECTION('',(1.,0.,0.)); +#453 = ORIENTED_EDGE('',*,*,#454,.F.); +#454 = EDGE_CURVE('',#323,#445,#455,.T.); +#455 = CIRCLE('',#456,4.5E-02); +#456 = AXIS2_PLACEMENT_3D('',#457,#458,#459); +#457 = CARTESIAN_POINT('',(0.545,-0.4,4.5E-02)); +#458 = DIRECTION('',(0.,-1.,0.)); +#459 = DIRECTION('',(0.,0.,1.)); +#460 = ORIENTED_EDGE('',*,*,#322,.T.); +#461 = ORIENTED_EDGE('',*,*,#314,.T.); +#462 = ORIENTED_EDGE('',*,*,#306,.F.); +#463 = ORIENTED_EDGE('',*,*,#464,.T.); +#464 = EDGE_CURVE('',#299,#465,#467,.T.); +#465 = VERTEX_POINT('',#466); +#466 = CARTESIAN_POINT('',(0.545,-0.4,0.45)); +#467 = CIRCLE('',#468,4.5E-02); +#468 = AXIS2_PLACEMENT_3D('',#469,#470,#471); +#469 = CARTESIAN_POINT('',(0.545,-0.4,0.405)); +#470 = DIRECTION('',(0.,1.,0.)); +#471 = DIRECTION('',(0.,0.,1.)); +#472 = ORIENTED_EDGE('',*,*,#473,.T.); +#473 = EDGE_CURVE('',#465,#474,#476,.T.); +#474 = VERTEX_POINT('',#475); +#475 = CARTESIAN_POINT('',(0.755,-0.4,0.45)); +#476 = LINE('',#477,#478); +#477 = CARTESIAN_POINT('',(0.5,-0.4,0.45)); +#478 = VECTOR('',#479,1.); +#479 = DIRECTION('',(1.,0.,0.)); +#480 = ORIENTED_EDGE('',*,*,#481,.F.); +#481 = EDGE_CURVE('',#482,#474,#484,.T.); +#482 = VERTEX_POINT('',#483); +#483 = CARTESIAN_POINT('',(0.8,-0.4,0.405)); +#484 = CIRCLE('',#485,4.5E-02); +#485 = AXIS2_PLACEMENT_3D('',#486,#487,#488); +#486 = CARTESIAN_POINT('',(0.755,-0.4,0.405)); +#487 = DIRECTION('',(0.,-1.,0.)); +#488 = DIRECTION('',(0.,0.,1.)); +#489 = ORIENTED_EDGE('',*,*,#490,.F.); +#490 = EDGE_CURVE('',#491,#482,#493,.T.); +#491 = VERTEX_POINT('',#492); +#492 = CARTESIAN_POINT('',(0.8,-0.4,4.5E-02)); +#493 = LINE('',#494,#495); +#494 = CARTESIAN_POINT('',(0.8,-0.4,0.)); +#495 = VECTOR('',#496,1.); +#496 = DIRECTION('',(0.,0.,1.)); +#497 = ORIENTED_EDGE('',*,*,#498,.T.); +#498 = EDGE_CURVE('',#491,#447,#499,.T.); +#499 = CIRCLE('',#500,4.5E-02); +#500 = AXIS2_PLACEMENT_3D('',#501,#502,#503); +#501 = CARTESIAN_POINT('',(0.755,-0.4,4.5E-02)); +#502 = DIRECTION('',(0.,1.,0.)); +#503 = DIRECTION('',(0.,0.,1.)); +#504 = PLANE('',#505); +#505 = AXIS2_PLACEMENT_3D('',#506,#507,#508); +#506 = CARTESIAN_POINT('',(0.5,-0.4,0.)); +#507 = DIRECTION('',(0.,1.,0.)); +#508 = DIRECTION('',(0.,0.,1.)); +#509 = ADVANCED_FACE('',(#510),#535,.F.); +#510 = FACE_BOUND('',#511,.F.); +#511 = EDGE_LOOP('',(#512,#513,#521,#529)); +#512 = ORIENTED_EDGE('',*,*,#298,.F.); +#513 = ORIENTED_EDGE('',*,*,#514,.T.); +#514 = EDGE_CURVE('',#97,#515,#517,.T.); +#515 = VERTEX_POINT('',#516); +#516 = CARTESIAN_POINT('',(-0.5,-0.4,0.45)); +#517 = LINE('',#518,#519); +#518 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#519 = VECTOR('',#520,1.); +#520 = DIRECTION('',(0.,0.,1.)); +#521 = ORIENTED_EDGE('',*,*,#522,.T.); +#522 = EDGE_CURVE('',#515,#523,#525,.T.); +#523 = VERTEX_POINT('',#524); +#524 = CARTESIAN_POINT('',(0.5,-0.4,0.45)); +#525 = LINE('',#526,#527); +#526 = CARTESIAN_POINT('',(-0.5,-0.4,0.45)); +#527 = VECTOR('',#528,1.); +#528 = DIRECTION('',(1.,0.,0.)); +#529 = ORIENTED_EDGE('',*,*,#530,.F.); +#530 = EDGE_CURVE('',#299,#523,#531,.T.); +#531 = LINE('',#532,#533); +#532 = CARTESIAN_POINT('',(0.5,-0.4,0.405)); +#533 = VECTOR('',#534,1.); +#534 = DIRECTION('',(0.,0.,1.)); +#535 = PLANE('',#536); +#536 = AXIS2_PLACEMENT_3D('',#537,#538,#539); +#537 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#538 = DIRECTION('',(0.,1.,0.)); +#539 = DIRECTION('',(0.,0.,1.)); +#540 = ADVANCED_FACE('',(#541),#559,.F.); +#541 = FACE_BOUND('',#542,.F.); +#542 = EDGE_LOOP('',(#543,#544,#545,#553)); +#543 = ORIENTED_EDGE('',*,*,#514,.F.); +#544 = ORIENTED_EDGE('',*,*,#364,.T.); +#545 = ORIENTED_EDGE('',*,*,#546,.T.); +#546 = EDGE_CURVE('',#214,#547,#549,.T.); +#547 = VERTEX_POINT('',#548); +#548 = CARTESIAN_POINT('',(-0.5,0.4,0.45)); +#549 = LINE('',#550,#551); +#550 = CARTESIAN_POINT('',(-0.5,0.4,0.405)); +#551 = VECTOR('',#552,1.); +#552 = DIRECTION('',(0.,0.,1.)); +#553 = ORIENTED_EDGE('',*,*,#554,.F.); +#554 = EDGE_CURVE('',#515,#547,#555,.T.); +#555 = LINE('',#556,#557); +#556 = CARTESIAN_POINT('',(-0.5,-0.4,0.45)); +#557 = VECTOR('',#558,1.); +#558 = DIRECTION('',(0.,1.,0.)); +#559 = PLANE('',#560); +#560 = AXIS2_PLACEMENT_3D('',#561,#562,#563); +#561 = CARTESIAN_POINT('',(-0.5,-0.4,0.405)); +#562 = DIRECTION('',(1.,0.,0.)); +#563 = DIRECTION('',(0.,0.,1.)); +#564 = ADVANCED_FACE('',(#565),#628,.T.); +#565 = FACE_BOUND('',#566,.T.); +#566 = EDGE_LOOP('',(#567,#577,#584,#585,#586,#587,#596,#604,#613,#621) +); +#567 = ORIENTED_EDGE('',*,*,#568,.F.); +#568 = EDGE_CURVE('',#569,#571,#573,.T.); +#569 = VERTEX_POINT('',#570); +#570 = CARTESIAN_POINT('',(0.545,0.4,-2.775557561563E-17)); +#571 = VERTEX_POINT('',#572); +#572 = CARTESIAN_POINT('',(0.755,0.4,-2.775557561563E-17)); +#573 = LINE('',#574,#575); +#574 = CARTESIAN_POINT('',(0.5,0.4,0.)); +#575 = VECTOR('',#576,1.); +#576 = DIRECTION('',(1.,0.,0.)); +#577 = ORIENTED_EDGE('',*,*,#578,.F.); +#578 = EDGE_CURVE('',#406,#569,#579,.T.); +#579 = CIRCLE('',#580,4.5E-02); +#580 = AXIS2_PLACEMENT_3D('',#581,#582,#583); +#581 = CARTESIAN_POINT('',(0.545,0.4,4.5E-02)); +#582 = DIRECTION('',(0.,-1.,0.)); +#583 = DIRECTION('',(0.,0.,1.)); +#584 = ORIENTED_EDGE('',*,*,#405,.T.); +#585 = ORIENTED_EDGE('',*,*,#397,.T.); +#586 = ORIENTED_EDGE('',*,*,#389,.F.); +#587 = ORIENTED_EDGE('',*,*,#588,.T.); +#588 = EDGE_CURVE('',#382,#589,#591,.T.); +#589 = VERTEX_POINT('',#590); +#590 = CARTESIAN_POINT('',(0.545,0.4,0.45)); +#591 = CIRCLE('',#592,4.5E-02); +#592 = AXIS2_PLACEMENT_3D('',#593,#594,#595); +#593 = CARTESIAN_POINT('',(0.545,0.4,0.405)); +#594 = DIRECTION('',(0.,1.,0.)); +#595 = DIRECTION('',(0.,0.,1.)); +#596 = ORIENTED_EDGE('',*,*,#597,.T.); +#597 = EDGE_CURVE('',#589,#598,#600,.T.); +#598 = VERTEX_POINT('',#599); +#599 = CARTESIAN_POINT('',(0.755,0.4,0.45)); +#600 = LINE('',#601,#602); +#601 = CARTESIAN_POINT('',(0.5,0.4,0.45)); +#602 = VECTOR('',#603,1.); +#603 = DIRECTION('',(1.,0.,0.)); +#604 = ORIENTED_EDGE('',*,*,#605,.F.); +#605 = EDGE_CURVE('',#606,#598,#608,.T.); +#606 = VERTEX_POINT('',#607); +#607 = CARTESIAN_POINT('',(0.8,0.4,0.405)); +#608 = CIRCLE('',#609,4.5E-02); +#609 = AXIS2_PLACEMENT_3D('',#610,#611,#612); +#610 = CARTESIAN_POINT('',(0.755,0.4,0.405)); +#611 = DIRECTION('',(0.,-1.,0.)); +#612 = DIRECTION('',(0.,0.,1.)); +#613 = ORIENTED_EDGE('',*,*,#614,.F.); +#614 = EDGE_CURVE('',#615,#606,#617,.T.); +#615 = VERTEX_POINT('',#616); +#616 = CARTESIAN_POINT('',(0.8,0.4,4.5E-02)); +#617 = LINE('',#618,#619); +#618 = CARTESIAN_POINT('',(0.8,0.4,0.)); +#619 = VECTOR('',#620,1.); +#620 = DIRECTION('',(0.,0.,1.)); +#621 = ORIENTED_EDGE('',*,*,#622,.T.); +#622 = EDGE_CURVE('',#615,#571,#623,.T.); +#623 = CIRCLE('',#624,4.5E-02); +#624 = AXIS2_PLACEMENT_3D('',#625,#626,#627); +#625 = CARTESIAN_POINT('',(0.755,0.4,4.5E-02)); +#626 = DIRECTION('',(0.,1.,0.)); +#627 = DIRECTION('',(0.,0.,1.)); +#628 = PLANE('',#629); +#629 = AXIS2_PLACEMENT_3D('',#630,#631,#632); +#630 = CARTESIAN_POINT('',(0.5,0.4,0.)); +#631 = DIRECTION('',(0.,1.,0.)); +#632 = DIRECTION('',(0.,0.,1.)); +#633 = ADVANCED_FACE('',(#634),#652,.T.); +#634 = FACE_BOUND('',#635,.T.); +#635 = EDGE_LOOP('',(#636,#637,#638,#646)); +#636 = ORIENTED_EDGE('',*,*,#381,.F.); +#637 = ORIENTED_EDGE('',*,*,#546,.T.); +#638 = ORIENTED_EDGE('',*,*,#639,.T.); +#639 = EDGE_CURVE('',#547,#640,#642,.T.); +#640 = VERTEX_POINT('',#641); +#641 = CARTESIAN_POINT('',(0.5,0.4,0.45)); +#642 = LINE('',#643,#644); +#643 = CARTESIAN_POINT('',(-0.5,0.4,0.45)); +#644 = VECTOR('',#645,1.); +#645 = DIRECTION('',(1.,0.,0.)); +#646 = ORIENTED_EDGE('',*,*,#647,.F.); +#647 = EDGE_CURVE('',#382,#640,#648,.T.); +#648 = LINE('',#649,#650); +#649 = CARTESIAN_POINT('',(0.5,0.4,0.405)); +#650 = VECTOR('',#651,1.); +#651 = DIRECTION('',(0.,0.,1.)); +#652 = PLANE('',#653); +#653 = AXIS2_PLACEMENT_3D('',#654,#655,#656); +#654 = CARTESIAN_POINT('',(-0.5,0.4,0.405)); +#655 = DIRECTION('',(0.,1.,0.)); +#656 = DIRECTION('',(0.,0.,1.)); +#657 = ADVANCED_FACE('',(#658),#669,.T.); +#658 = FACE_BOUND('',#659,.F.); +#659 = EDGE_LOOP('',(#660,#661,#667,#668)); +#660 = ORIENTED_EDGE('',*,*,#454,.T.); +#661 = ORIENTED_EDGE('',*,*,#662,.T.); +#662 = EDGE_CURVE('',#445,#569,#663,.T.); +#663 = LINE('',#664,#665); +#664 = CARTESIAN_POINT('',(0.545,-0.4,-2.775557561563E-17)); +#665 = VECTOR('',#666,1.); +#666 = DIRECTION('',(0.,1.,0.)); +#667 = ORIENTED_EDGE('',*,*,#578,.F.); +#668 = ORIENTED_EDGE('',*,*,#429,.F.); +#669 = CYLINDRICAL_SURFACE('',#670,4.5E-02); +#670 = AXIS2_PLACEMENT_3D('',#671,#672,#673); +#671 = CARTESIAN_POINT('',(0.545,-0.4,4.5E-02)); +#672 = DIRECTION('',(0.,1.,0.)); +#673 = DIRECTION('',(-1.,0.,0.)); +#674 = ADVANCED_FACE('',(#675),#686,.F.); +#675 = FACE_BOUND('',#676,.F.); +#676 = EDGE_LOOP('',(#677,#678,#679,#680)); +#677 = ORIENTED_EDGE('',*,*,#568,.F.); +#678 = ORIENTED_EDGE('',*,*,#662,.F.); +#679 = ORIENTED_EDGE('',*,*,#444,.T.); +#680 = ORIENTED_EDGE('',*,*,#681,.T.); +#681 = EDGE_CURVE('',#447,#571,#682,.T.); +#682 = LINE('',#683,#684); +#683 = CARTESIAN_POINT('',(0.755,-0.4,-2.775557561563E-17)); +#684 = VECTOR('',#685,1.); +#685 = DIRECTION('',(0.,1.,0.)); +#686 = PLANE('',#687); +#687 = AXIS2_PLACEMENT_3D('',#688,#689,#690); +#688 = CARTESIAN_POINT('',(0.5,-0.4,0.)); +#689 = DIRECTION('',(0.,0.,1.)); +#690 = DIRECTION('',(1.,0.,0.)); +#691 = ADVANCED_FACE('',(#692),#703,.T.); +#692 = FACE_BOUND('',#693,.T.); +#693 = EDGE_LOOP('',(#694,#695,#696,#697)); +#694 = ORIENTED_EDGE('',*,*,#498,.T.); +#695 = ORIENTED_EDGE('',*,*,#681,.T.); +#696 = ORIENTED_EDGE('',*,*,#622,.F.); +#697 = ORIENTED_EDGE('',*,*,#698,.F.); +#698 = EDGE_CURVE('',#491,#615,#699,.T.); +#699 = LINE('',#700,#701); +#700 = CARTESIAN_POINT('',(0.8,-0.4,4.5E-02)); +#701 = VECTOR('',#702,1.); +#702 = DIRECTION('',(0.,1.,0.)); +#703 = CYLINDRICAL_SURFACE('',#704,4.5E-02); +#704 = AXIS2_PLACEMENT_3D('',#705,#706,#707); +#705 = CARTESIAN_POINT('',(0.755,-0.4,4.5E-02)); +#706 = DIRECTION('',(0.,1.,0.)); +#707 = DIRECTION('',(1.,0.,0.)); +#708 = ADVANCED_FACE('',(#709),#720,.T.); +#709 = FACE_BOUND('',#710,.T.); +#710 = EDGE_LOOP('',(#711,#712,#713,#714)); +#711 = ORIENTED_EDGE('',*,*,#490,.F.); +#712 = ORIENTED_EDGE('',*,*,#698,.T.); +#713 = ORIENTED_EDGE('',*,*,#614,.T.); +#714 = ORIENTED_EDGE('',*,*,#715,.F.); +#715 = EDGE_CURVE('',#482,#606,#716,.T.); +#716 = LINE('',#717,#718); +#717 = CARTESIAN_POINT('',(0.8,-0.4,0.405)); +#718 = VECTOR('',#719,1.); +#719 = DIRECTION('',(0.,1.,0.)); +#720 = PLANE('',#721); +#721 = AXIS2_PLACEMENT_3D('',#722,#723,#724); +#722 = CARTESIAN_POINT('',(0.8,-0.4,0.)); +#723 = DIRECTION('',(1.,0.,0.)); +#724 = DIRECTION('',(0.,0.,1.)); +#725 = ADVANCED_FACE('',(#726),#737,.T.); +#726 = FACE_BOUND('',#727,.F.); +#727 = EDGE_LOOP('',(#728,#729,#735,#736)); +#728 = ORIENTED_EDGE('',*,*,#481,.T.); +#729 = ORIENTED_EDGE('',*,*,#730,.T.); +#730 = EDGE_CURVE('',#474,#598,#731,.T.); +#731 = LINE('',#732,#733); +#732 = CARTESIAN_POINT('',(0.755,-0.4,0.45)); +#733 = VECTOR('',#734,1.); +#734 = DIRECTION('',(0.,1.,0.)); +#735 = ORIENTED_EDGE('',*,*,#605,.F.); +#736 = ORIENTED_EDGE('',*,*,#715,.F.); +#737 = CYLINDRICAL_SURFACE('',#738,4.5E-02); +#738 = AXIS2_PLACEMENT_3D('',#739,#740,#741); +#739 = CARTESIAN_POINT('',(0.755,-0.4,0.405)); +#740 = DIRECTION('',(0.,1.,0.)); +#741 = DIRECTION('',(1.,0.,0.)); +#742 = ADVANCED_FACE('',(#743),#754,.T.); +#743 = FACE_BOUND('',#744,.T.); +#744 = EDGE_LOOP('',(#745,#746,#752,#753)); +#745 = ORIENTED_EDGE('',*,*,#597,.F.); +#746 = ORIENTED_EDGE('',*,*,#747,.F.); +#747 = EDGE_CURVE('',#465,#589,#748,.T.); +#748 = LINE('',#749,#750); +#749 = CARTESIAN_POINT('',(0.545,-0.4,0.45)); +#750 = VECTOR('',#751,1.); +#751 = DIRECTION('',(0.,1.,0.)); +#752 = ORIENTED_EDGE('',*,*,#473,.T.); +#753 = ORIENTED_EDGE('',*,*,#730,.T.); +#754 = PLANE('',#755); +#755 = AXIS2_PLACEMENT_3D('',#756,#757,#758); +#756 = CARTESIAN_POINT('',(0.5,-0.4,0.45)); +#757 = DIRECTION('',(0.,0.,1.)); +#758 = DIRECTION('',(1.,0.,0.)); +#759 = ADVANCED_FACE('',(#760),#771,.T.); +#760 = FACE_BOUND('',#761,.T.); +#761 = EDGE_LOOP('',(#762,#763,#764,#765)); +#762 = ORIENTED_EDGE('',*,*,#464,.T.); +#763 = ORIENTED_EDGE('',*,*,#747,.T.); +#764 = ORIENTED_EDGE('',*,*,#588,.F.); +#765 = ORIENTED_EDGE('',*,*,#766,.F.); +#766 = EDGE_CURVE('',#299,#382,#767,.T.); +#767 = LINE('',#768,#769); +#768 = CARTESIAN_POINT('',(0.5,-0.4,0.405)); +#769 = VECTOR('',#770,1.); +#770 = DIRECTION('',(0.,1.,0.)); +#771 = CYLINDRICAL_SURFACE('',#772,4.5E-02); +#772 = AXIS2_PLACEMENT_3D('',#773,#774,#775); +#773 = CARTESIAN_POINT('',(0.545,-0.4,0.405)); +#774 = DIRECTION('',(0.,1.,0.)); +#775 = DIRECTION('',(-1.,0.,0.)); +#776 = ADVANCED_FACE('',(#777),#788,.T.); +#777 = FACE_BOUND('',#778,.T.); +#778 = EDGE_LOOP('',(#779,#780,#781,#782)); +#779 = ORIENTED_EDGE('',*,*,#530,.F.); +#780 = ORIENTED_EDGE('',*,*,#766,.T.); +#781 = ORIENTED_EDGE('',*,*,#647,.T.); +#782 = ORIENTED_EDGE('',*,*,#783,.F.); +#783 = EDGE_CURVE('',#523,#640,#784,.T.); +#784 = LINE('',#785,#786); +#785 = CARTESIAN_POINT('',(0.5,-0.4,0.45)); +#786 = VECTOR('',#787,1.); +#787 = DIRECTION('',(0.,1.,0.)); +#788 = PLANE('',#789); +#789 = AXIS2_PLACEMENT_3D('',#790,#791,#792); +#790 = CARTESIAN_POINT('',(0.5,-0.4,0.405)); +#791 = DIRECTION('',(1.,0.,0.)); +#792 = DIRECTION('',(0.,0.,1.)); +#793 = ADVANCED_FACE('',(#794),#800,.T.); +#794 = FACE_BOUND('',#795,.T.); +#795 = EDGE_LOOP('',(#796,#797,#798,#799)); +#796 = ORIENTED_EDGE('',*,*,#554,.F.); +#797 = ORIENTED_EDGE('',*,*,#522,.T.); +#798 = ORIENTED_EDGE('',*,*,#783,.T.); +#799 = ORIENTED_EDGE('',*,*,#639,.F.); +#800 = PLANE('',#801); +#801 = AXIS2_PLACEMENT_3D('',#802,#803,#804); +#802 = CARTESIAN_POINT('',(-0.5,-0.4,0.45)); +#803 = DIRECTION('',(0.,0.,1.)); +#804 = DIRECTION('',(1.,0.,0.)); +#805 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#809)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#806,#807,#808)) REPRESENTATION_CONTEXT('Context #1', +'3D Context with UNIT and UNCERTAINTY') ); +#806 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#807 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#808 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#809 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#806, +'distance_accuracy_value','confusion accuracy'); +#810 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#811 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(#812, +#820,#827,#834,#841,#848,#855,#862,#870,#877,#884,#891,#898,#905, +#913,#920,#927,#934,#941,#948,#955,#962,#969,#976,#983,#990),#805); +#812 = STYLED_ITEM('color',(#813),#17); +#813 = PRESENTATION_STYLE_ASSIGNMENT((#814)); +#814 = SURFACE_STYLE_USAGE(.BOTH.,#815); +#815 = SURFACE_SIDE_STYLE('',(#816)); +#816 = SURFACE_STYLE_FILL_AREA(#817); +#817 = FILL_AREA_STYLE('',(#818)); +#818 = FILL_AREA_STYLE_COLOUR('',#819); +#819 = COLOUR_RGB('',0.824000000954,0.819999992847,0.78100001812); +#820 = STYLED_ITEM('color',(#821),#57); +#821 = PRESENTATION_STYLE_ASSIGNMENT((#822)); +#822 = SURFACE_STYLE_USAGE(.BOTH.,#823); +#823 = SURFACE_SIDE_STYLE('',(#824)); +#824 = SURFACE_STYLE_FILL_AREA(#825); +#825 = FILL_AREA_STYLE('',(#826)); +#826 = FILL_AREA_STYLE_COLOUR('',#819); +#827 = STYLED_ITEM('color',(#828),#140); +#828 = PRESENTATION_STYLE_ASSIGNMENT((#829)); +#829 = SURFACE_STYLE_USAGE(.BOTH.,#830); +#830 = SURFACE_SIDE_STYLE('',(#831)); +#831 = SURFACE_STYLE_FILL_AREA(#832); +#832 = FILL_AREA_STYLE('',(#833)); +#833 = FILL_AREA_STYLE_COLOUR('',#819); +#834 = STYLED_ITEM('color',(#835),#165); +#835 = PRESENTATION_STYLE_ASSIGNMENT((#836)); +#836 = SURFACE_STYLE_USAGE(.BOTH.,#837); +#837 = SURFACE_SIDE_STYLE('',(#838)); +#838 = SURFACE_STYLE_FILL_AREA(#839); +#839 = FILL_AREA_STYLE('',(#840)); +#840 = FILL_AREA_STYLE_COLOUR('',#819); +#841 = STYLED_ITEM('color',(#842),#190); +#842 = PRESENTATION_STYLE_ASSIGNMENT((#843)); +#843 = SURFACE_STYLE_USAGE(.BOTH.,#844); +#844 = SURFACE_SIDE_STYLE('',(#845)); +#845 = SURFACE_STYLE_FILL_AREA(#846); +#846 = FILL_AREA_STYLE('',(#847)); +#847 = FILL_AREA_STYLE_COLOUR('',#819); +#848 = STYLED_ITEM('color',(#849),#257); +#849 = PRESENTATION_STYLE_ASSIGNMENT((#850)); +#850 = SURFACE_STYLE_USAGE(.BOTH.,#851); +#851 = SURFACE_SIDE_STYLE('',(#852)); +#852 = SURFACE_STYLE_FILL_AREA(#853); +#853 = FILL_AREA_STYLE('',(#854)); +#854 = FILL_AREA_STYLE_COLOUR('',#819); +#855 = STYLED_ITEM('color',(#856),#274); +#856 = PRESENTATION_STYLE_ASSIGNMENT((#857)); +#857 = SURFACE_STYLE_USAGE(.BOTH.,#858); +#858 = SURFACE_SIDE_STYLE('',(#859)); +#859 = SURFACE_STYLE_FILL_AREA(#860); +#860 = FILL_AREA_STYLE('',(#861)); +#861 = FILL_AREA_STYLE_COLOUR('',#819); +#862 = STYLED_ITEM('color',(#863),#291); +#863 = PRESENTATION_STYLE_ASSIGNMENT((#864)); +#864 = SURFACE_STYLE_USAGE(.BOTH.,#865); +#865 = SURFACE_SIDE_STYLE('',(#866)); +#866 = SURFACE_STYLE_FILL_AREA(#867); +#867 = FILL_AREA_STYLE('',(#868)); +#868 = FILL_AREA_STYLE_COLOUR('',#869); +#869 = COLOUR_RGB('',0.894999980927,0.89099997282,0.813000023365); +#870 = STYLED_ITEM('color',(#871),#340); +#871 = PRESENTATION_STYLE_ASSIGNMENT((#872)); +#872 = SURFACE_STYLE_USAGE(.BOTH.,#873); +#873 = SURFACE_SIDE_STYLE('',(#874)); +#874 = SURFACE_STYLE_FILL_AREA(#875); +#875 = FILL_AREA_STYLE('',(#876)); +#876 = FILL_AREA_STYLE_COLOUR('',#819); +#877 = STYLED_ITEM('color',(#878),#357); +#878 = PRESENTATION_STYLE_ASSIGNMENT((#879)); +#879 = SURFACE_STYLE_USAGE(.BOTH.,#880); +#880 = SURFACE_SIDE_STYLE('',(#881)); +#881 = SURFACE_STYLE_FILL_AREA(#882); +#882 = FILL_AREA_STYLE('',(#883)); +#883 = FILL_AREA_STYLE_COLOUR('',#819); +#884 = STYLED_ITEM('color',(#885),#374); +#885 = PRESENTATION_STYLE_ASSIGNMENT((#886)); +#886 = SURFACE_STYLE_USAGE(.BOTH.,#887); +#887 = SURFACE_SIDE_STYLE('',(#888)); +#888 = SURFACE_STYLE_FILL_AREA(#889); +#889 = FILL_AREA_STYLE('',(#890)); +#890 = FILL_AREA_STYLE_COLOUR('',#869); +#891 = STYLED_ITEM('color',(#892),#423); +#892 = PRESENTATION_STYLE_ASSIGNMENT((#893)); +#893 = SURFACE_STYLE_USAGE(.BOTH.,#894); +#894 = SURFACE_SIDE_STYLE('',(#895)); +#895 = SURFACE_STYLE_FILL_AREA(#896); +#896 = FILL_AREA_STYLE('',(#897)); +#897 = FILL_AREA_STYLE_COLOUR('',#869); +#898 = STYLED_ITEM('color',(#899),#440); +#899 = PRESENTATION_STYLE_ASSIGNMENT((#900)); +#900 = SURFACE_STYLE_USAGE(.BOTH.,#901); +#901 = SURFACE_SIDE_STYLE('',(#902)); +#902 = SURFACE_STYLE_FILL_AREA(#903); +#903 = FILL_AREA_STYLE('',(#904)); +#904 = FILL_AREA_STYLE_COLOUR('',#819); +#905 = STYLED_ITEM('color',(#906),#509); +#906 = PRESENTATION_STYLE_ASSIGNMENT((#907)); +#907 = SURFACE_STYLE_USAGE(.BOTH.,#908); +#908 = SURFACE_SIDE_STYLE('',(#909)); +#909 = SURFACE_STYLE_FILL_AREA(#910); +#910 = FILL_AREA_STYLE('',(#911)); +#911 = FILL_AREA_STYLE_COLOUR('',#912); +#912 = COLOUR_RGB('',8.200000226498E-02,8.600000292063E-02, +9.399999678135E-02); +#913 = STYLED_ITEM('color',(#914),#540); +#914 = PRESENTATION_STYLE_ASSIGNMENT((#915)); +#915 = SURFACE_STYLE_USAGE(.BOTH.,#916); +#916 = SURFACE_SIDE_STYLE('',(#917)); +#917 = SURFACE_STYLE_FILL_AREA(#918); +#918 = FILL_AREA_STYLE('',(#919)); +#919 = FILL_AREA_STYLE_COLOUR('',#912); +#920 = STYLED_ITEM('color',(#921),#564); +#921 = PRESENTATION_STYLE_ASSIGNMENT((#922)); +#922 = SURFACE_STYLE_USAGE(.BOTH.,#923); +#923 = SURFACE_SIDE_STYLE('',(#924)); +#924 = SURFACE_STYLE_FILL_AREA(#925); +#925 = FILL_AREA_STYLE('',(#926)); +#926 = FILL_AREA_STYLE_COLOUR('',#819); +#927 = STYLED_ITEM('color',(#928),#633); +#928 = PRESENTATION_STYLE_ASSIGNMENT((#929)); +#929 = SURFACE_STYLE_USAGE(.BOTH.,#930); +#930 = SURFACE_SIDE_STYLE('',(#931)); +#931 = SURFACE_STYLE_FILL_AREA(#932); +#932 = FILL_AREA_STYLE('',(#933)); +#933 = FILL_AREA_STYLE_COLOUR('',#912); +#934 = STYLED_ITEM('color',(#935),#657); +#935 = PRESENTATION_STYLE_ASSIGNMENT((#936)); +#936 = SURFACE_STYLE_USAGE(.BOTH.,#937); +#937 = SURFACE_SIDE_STYLE('',(#938)); +#938 = SURFACE_STYLE_FILL_AREA(#939); +#939 = FILL_AREA_STYLE('',(#940)); +#940 = FILL_AREA_STYLE_COLOUR('',#819); +#941 = STYLED_ITEM('color',(#942),#674); +#942 = PRESENTATION_STYLE_ASSIGNMENT((#943)); +#943 = SURFACE_STYLE_USAGE(.BOTH.,#944); +#944 = SURFACE_SIDE_STYLE('',(#945)); +#945 = SURFACE_STYLE_FILL_AREA(#946); +#946 = FILL_AREA_STYLE('',(#947)); +#947 = FILL_AREA_STYLE_COLOUR('',#819); +#948 = STYLED_ITEM('color',(#949),#691); +#949 = PRESENTATION_STYLE_ASSIGNMENT((#950)); +#950 = SURFACE_STYLE_USAGE(.BOTH.,#951); +#951 = SURFACE_SIDE_STYLE('',(#952)); +#952 = SURFACE_STYLE_FILL_AREA(#953); +#953 = FILL_AREA_STYLE('',(#954)); +#954 = FILL_AREA_STYLE_COLOUR('',#819); +#955 = STYLED_ITEM('color',(#956),#708); +#956 = PRESENTATION_STYLE_ASSIGNMENT((#957)); +#957 = SURFACE_STYLE_USAGE(.BOTH.,#958); +#958 = SURFACE_SIDE_STYLE('',(#959)); +#959 = SURFACE_STYLE_FILL_AREA(#960); +#960 = FILL_AREA_STYLE('',(#961)); +#961 = FILL_AREA_STYLE_COLOUR('',#819); +#962 = STYLED_ITEM('color',(#963),#725); +#963 = PRESENTATION_STYLE_ASSIGNMENT((#964)); +#964 = SURFACE_STYLE_USAGE(.BOTH.,#965); +#965 = SURFACE_SIDE_STYLE('',(#966)); +#966 = SURFACE_STYLE_FILL_AREA(#967); +#967 = FILL_AREA_STYLE('',(#968)); +#968 = FILL_AREA_STYLE_COLOUR('',#819); +#969 = STYLED_ITEM('color',(#970),#742); +#970 = PRESENTATION_STYLE_ASSIGNMENT((#971)); +#971 = SURFACE_STYLE_USAGE(.BOTH.,#972); +#972 = SURFACE_SIDE_STYLE('',(#973)); +#973 = SURFACE_STYLE_FILL_AREA(#974); +#974 = FILL_AREA_STYLE('',(#975)); +#975 = FILL_AREA_STYLE_COLOUR('',#819); +#976 = STYLED_ITEM('color',(#977),#759); +#977 = PRESENTATION_STYLE_ASSIGNMENT((#978)); +#978 = SURFACE_STYLE_USAGE(.BOTH.,#979); +#979 = SURFACE_SIDE_STYLE('',(#980)); +#980 = SURFACE_STYLE_FILL_AREA(#981); +#981 = FILL_AREA_STYLE('',(#982)); +#982 = FILL_AREA_STYLE_COLOUR('',#819); +#983 = STYLED_ITEM('color',(#984),#776); +#984 = PRESENTATION_STYLE_ASSIGNMENT((#985)); +#985 = SURFACE_STYLE_USAGE(.BOTH.,#986); +#986 = SURFACE_SIDE_STYLE('',(#987)); +#987 = SURFACE_STYLE_FILL_AREA(#988); +#988 = FILL_AREA_STYLE('',(#989)); +#989 = FILL_AREA_STYLE_COLOUR('',#912); +#990 = STYLED_ITEM('color',(#991),#793); +#991 = PRESENTATION_STYLE_ASSIGNMENT((#992)); +#992 = SURFACE_STYLE_USAGE(.BOTH.,#993); +#993 = SURFACE_SIDE_STYLE('',(#994)); +#994 = SURFACE_STYLE_FILL_AREA(#995); +#995 = FILL_AREA_STYLE('',(#996)); +#996 = FILL_AREA_STYLE_COLOUR('',#912); +ENDSEC; +END-ISO-10303-21; diff --git a/utils/webview_test/server/data/footprints/R_0603_1608Metric.kicad_mod b/utils/webview_test/server/data/footprints/R_0603_1608Metric.kicad_mod new file mode 100644 index 0000000000..5aafdd31d4 --- /dev/null +++ b/utils/webview_test/server/data/footprints/R_0603_1608Metric.kicad_mod @@ -0,0 +1,203 @@ +(footprint "R_0603_1608Metric" + (version 20240108) + (generator "pcbnew") + (generator_version "8.0") + (layer "F.Cu") + (descr "Resistor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: IPC-SM-782 page 72, https://www.pcb-3d.com/wordpress/wp-content/uploads/ipc-sm-782a_amendment_1_and_2.pdf), generated with kicad-footprint-generator") + (tags "resistor") + (property "Reference" "REF**" + (at 0 -1.43 0) + (layer "F.SilkS") + (uuid "6df974b4-b819-4abf-a570-cfac24b90f22") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "R_0603_1608Metric" + (at 0 1.43 0) + (layer "F.Fab") + (uuid "43a2d3f9-8880-4891-bb65-e1f1adf1c2a5") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "6f0e4074-96ae-4ea8-867e-199cfa095a90") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "0814cc48-e15c-47d7-a42f-32beb2895ccf") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "9ae02bb0-5305-4720-96a2-b8307ae3a157") + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (attr smd) + (fp_line + (start -0.237258 -0.5225) + (end 0.237258 -0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "8ec8ba9e-3d89-4f37-94bb-eddf5e4d7489") + ) + (fp_line + (start -0.237258 0.5225) + (end 0.237258 0.5225) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "2bf1d867-cdfa-44dc-9124-ce584dcd094b") + ) + (fp_line + (start -1.48 -0.73) + (end 1.48 -0.73) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "24e272be-4051-4b9b-9127-1c7f22532469") + ) + (fp_line + (start -1.48 0.73) + (end -1.48 -0.73) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "39fddbf8-ad85-4516-95fc-b7f57b1b5476") + ) + (fp_line + (start 1.48 -0.73) + (end 1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "2bbe1e5e-3213-443e-930c-a0ae97a345e1") + ) + (fp_line + (start 1.48 0.73) + (end -1.48 0.73) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "dc275714-5365-4c02-a2fb-5cbd77201d52") + ) + (fp_line + (start -0.8 -0.4125) + (end 0.8 -0.4125) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "30e382fb-e120-4499-bc15-156f98cd93e8") + ) + (fp_line + (start -0.8 0.4125) + (end -0.8 -0.4125) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "d04dd1a4-2178-41f5-9bb7-ad470915f47a") + ) + (fp_line + (start 0.8 -0.4125) + (end 0.8 0.4125) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "0746adbb-419c-491f-ae91-c14351e88a28") + ) + (fp_line + (start 0.8 0.4125) + (end -0.8 0.4125) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "9ddd7cd8-091d-447c-bc2a-eed5612caf40") + ) + (fp_text user "${REFERENCE}" + (at 0 0 0) + (layer "F.Fab") + (uuid "2dfd6efa-e1ab-4e55-ab2b-7f9e70f33afa") + (effects + (font + (size 0.4 0.4) + (thickness 0.06) + ) + ) + ) + (pad "1" smd roundrect + (at -0.825 0) + (size 0.8 0.95) + (layers "F.Cu" "F.Paste" "F.Mask") + (roundrect_rratio 0.25) + (uuid "958f80d5-b7aa-4100-9f8f-c88ef419994d") + ) + (pad "2" smd roundrect + (at 0.825 0) + (size 0.8 0.95) + (layers "F.Cu" "F.Paste" "F.Mask") + (roundrect_rratio 0.25) + (uuid "dbf61a86-b574-45d2-ba89-95e1ee8fabb2") + ) + (model "${KICAD8_3DMODEL_DIR}/Resistor_SMD.3dshapes/R_0603_1608Metric.wrl" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) +) \ No newline at end of file diff --git a/utils/webview_test/server/data/images/capacitor.svg b/utils/webview_test/server/data/images/capacitor.svg new file mode 100644 index 0000000000..3ffbca2f95 --- /dev/null +++ b/utils/webview_test/server/data/images/capacitor.svg @@ -0,0 +1,8 @@ + + + + + + + C0603 + diff --git a/utils/webview_test/server/data/images/resistor.svg b/utils/webview_test/server/data/images/resistor.svg new file mode 100644 index 0000000000..f98638ff6f --- /dev/null +++ b/utils/webview_test/server/data/images/resistor.svg @@ -0,0 +1,7 @@ + + + + + + R0603 + diff --git a/utils/webview_test/server/data/parts.json b/utils/webview_test/server/data/parts.json new file mode 100644 index 0000000000..a62d03b811 --- /dev/null +++ b/utils/webview_test/server/data/parts.json @@ -0,0 +1,18 @@ +{ + "Parts": [ + { + "Name": "Resistor 0603 1k", + "Image": "images/resistor.svg", + "Symbol": "symbols/r.kicad_sym", + "Footprint": "footprints/R_0603_1608Metric.kicad_mod", + "SPICE Model": "spice/resistor.cir", + "3D Model": "3dmodels/R_0603_1608Metric.step" + }, + { + "Name": "Capacitor 0603 100nF", + "Symbol": "symbols/c.kicad_sym", + "Footprint": "footprints/R_0603_1608Metric.kicad_mod", + "SPICE Model": "spice/capacitor.cir" + } + ] +} diff --git a/utils/webview_test/server/data/spice/capacitor.cir b/utils/webview_test/server/data/spice/capacitor.cir new file mode 100644 index 0000000000..593842b07d --- /dev/null +++ b/utils/webview_test/server/data/spice/capacitor.cir @@ -0,0 +1,4 @@ +* Simple capacitor SPICE model used by the WebView test harness +.subckt C0603 1 2 +C1 1 2 100n +.ends C0603 diff --git a/utils/webview_test/server/data/spice/resistor.cir b/utils/webview_test/server/data/spice/resistor.cir new file mode 100644 index 0000000000..1d367bf283 --- /dev/null +++ b/utils/webview_test/server/data/spice/resistor.cir @@ -0,0 +1,4 @@ +* Simple resistor SPICE model used by the WebView test harness +.subckt R0603 1 2 +R1 1 2 1k +.ends R0603 diff --git a/utils/webview_test/server/data/symbols/c.kicad_sym b/utils/webview_test/server/data/symbols/c.kicad_sym new file mode 100644 index 0000000000..312166be9a --- /dev/null +++ b/utils/webview_test/server/data/symbols/c.kicad_sym @@ -0,0 +1,145 @@ +(kicad_symbol_lib + (version 20250925) + (generator "kicad_symbol_editor") + (generator_version "9.99") + (symbol "C" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Unpolarized capacitor" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "C_*" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "C_0_1" + (polyline + (pts + (xy -2.032 0.762) (xy 2.032 0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.032 -0.762) (xy 2.032 -0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "C_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) +) diff --git a/utils/webview_test/server/data/symbols/r.kicad_sym b/utils/webview_test/server/data/symbols/r.kicad_sym new file mode 100644 index 0000000000..f142cd56bb --- /dev/null +++ b/utils/webview_test/server/data/symbols/r.kicad_sym @@ -0,0 +1,130 @@ +(kicad_symbol_lib + (version 20250925) + (generator "kicad_symbol_editor") + (generator_version "9.99") + (symbol "R" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (duplicate_pin_numbers_are_jumpers no) + (property "Reference" "R" + (at 2.032 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "R" + (at 0 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Description" "Resistor" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_keywords" "R res resistor" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "ki_fp_filters" "R_*" + (at 0 0 0) + (hide yes) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "R_0_1" + (rectangle + (start -1.016 -2.54) + (end 1.016 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) +) diff --git a/utils/webview_test/server/package/metadata.json b/utils/webview_test/server/package/metadata.json new file mode 100644 index 0000000000..232b96342a --- /dev/null +++ b/utils/webview_test/server/package/metadata.json @@ -0,0 +1,30 @@ +{ + "name": "WebView Test Datasource", + "description": "Local development datasource for the schematic remote symbol panel.", + "description_full": "Installs a datasource that points to the local WebView test server shipped with the KiCad source tree. Start the server found in utils/webview_test/server/webview_test_server.py and install this package from file via the Plugin and Content Manager.", + "identifier": "org.kicad.dev.webview.test", + "type": "datasource", + "author": { + "name": "KiCad Developers", + "contact": { + "url": "https://www.kicad.org/" + } + }, + "license": "GPL-3.0", + "resources": { + "server": "http://localhost:8080/", + "instructions": "Start the local test server with python3 webview_test_server.py before using this datasource." + }, + "versions": [ + { + "kicad_version": "7.0.0", + "version": "0.1.0", + "status": "stable", + "platforms": [ + "windows", + "macos", + "linux" + ] + } + ] +} diff --git a/utils/webview_test/server/package/resources/remote_symbol.json b/utils/webview_test/server/package/resources/remote_symbol.json new file mode 100644 index 0000000000..f62b2b4a61 --- /dev/null +++ b/utils/webview_test/server/package/resources/remote_symbol.json @@ -0,0 +1,5 @@ +{ + "host": "http://localhost", + "port": 8080, + "path": "/" +} diff --git a/utils/webview_test/server/package/server.zip b/utils/webview_test/server/package/server.zip new file mode 100644 index 0000000000000000000000000000000000000000..3b62f0645b0fbecf5a7ff122a5700cb5b7d31f12 GIT binary patch literal 1042 zcmWIWW@Zs#U|`^2s4Yv2PT-ns_n47^;RZ7U122ONLvCtGVoG93qFz>UeqLw@Cj)bz z8gJ&xe>|C`72FJrEH8lSz(i@#!7OP*fxW-A+iaIdO_!0X@m(-QO&}waUsWQ~`nBjq zFDI$W?56*Fl{5F2MO7Kzn`xeZZ_o1Wq7KPVJI=Drn!n@s!osHa5}a}>Ctm~w+_;kx z&>qP8*3)|S#(qCfW?8ul7WRu&Urb|s5z8PYut=(4$`Vl)8QH>z7VqXT>g!5<YE666mO2VmEZ22a2p5~N=+T+Ec}ZqP-e14~7`q?jl6&)zn7S!URwS+|xg z@cUD(^}8o5BRTTp&z~O;uZ;+oWn8_gVq#FPb+B=9XJNrav+~!wORJ|%;pO#wx_sjD z+1_!c-(T0Lqdw!FO~A}%xCZRMTaO&87n<~@5q zUA2Gu)y%K#0l<{P1xzUn43-IL(O?FY-~y(UqSWI2(xT+lVtsHjSqn@eAdHqwKxRNv z+rzZ9Xs_@GXN`eoH~_H>iWxI1PMy%#3*5wX zNmZ3K?AI^LpC>yl6AWc1!`)VfD&Fn^RR{$!ikunh3v#U6!Wl! b9nc0)*kQPsl?~)QW*}q*`u01}90mpe!RUB- literal 0 HcmV?d00001 diff --git a/utils/webview_test/server/webview_test_server.py b/utils/webview_test/server/webview_test_server.py new file mode 100644 index 0000000000..c116c65edc --- /dev/null +++ b/utils/webview_test/server/webview_test_server.py @@ -0,0 +1,1021 @@ +#!/usr/bin/env python3 +""" +Development server for exercising the schematic remote symbol webview. + +The server loads a JSON catalogue of parts, renders a small gallery UI, and +exposes buttons that stream each part's assets (footprint, models, etc.) to +KiCad via the WebView bridge. Each asset is compressed with zstd and then +base64-encoded before being embedded in the served HTML page. +""" + +from __future__ import annotations + +import argparse +import base64 +import http.server +import hashlib +import json +import logging +import mimetypes +import socketserver +import urllib.parse +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import zstandard +import re + + +_DEFAULT_PORT = 8080 +_DEFAULT_CONFIG = (Path(__file__).parent / "data" / "parts.json").resolve() +_ZSTD_COMPRESSOR = zstandard.ZstdCompressor(level=10) + + +def _content_type_for_command(command: str) -> str: + mapping = { + "DL_SYMBOL": "KICAD_SYMBOL_V1", + "DL_FOOTPRINT": "KICAD_FOOTPRINT_V1", + "DL_SPICE": "KICAD_SPICE_MODEL_V1", + "DL_3DMODEL": "KICAD_3D_MODEL_STEP", + } + + return mapping.get(command.upper(), "UNKNOWN") + + +_GENERIC_IMAGE_DATA_URI = ( + "data:image/svg+xml;base64," + + base64.b64encode( + b""" + + + + + + + + + + No Image +""" + ).decode("ascii") +) +_HTML_TEMPLATE = """\ + + + + + KiCad WebView Test Harness + + + +
+

Remote Symbol Demo Browser

+

+ Select a part to stream its assets to KiCad. Each asset is zstd-compressed, + base64-encoded, and delivered in sequence with acknowledgement (ACK) handling. +

+ +
+
+

Transfer log

+
No transfers yet.
+
+
+ + + +""" + + +_LOGIN_HTML_TEMPLATE = """\\ + + + + + KiCad Login + + + + + + + +""" + + +@dataclass(frozen=True) +class AssetRecord: + """In-memory representation of a downloadable asset.""" + + command: str + label: str + filename: str + data: str + parameters: Dict[str, Any] + size_bytes: int + + def to_dict(self) -> Dict[str, Any]: + return { + "command": self.command, + "label": self.label, + "filename": self.filename, + "data": self.data, + "parameters": self.parameters, + "size_bytes": self.size_bytes, + } + + +@dataclass(frozen=True) +class PartRecord: + """Renderable part entry.""" + + name: str + image: str + assets: Sequence[AssetRecord] + + def to_dict(self) -> Dict[str, Any]: + return { + "name": self.name, + "image": self.image, + "assets": [asset.to_dict() for asset in self.assets], + } + + +class _ServerState: + """Holds parsed configuration and rendered HTML.""" + + def __init__(self, config_path: Path) -> None: + self.config_path = config_path + self._parts = self._load_parts() + self._page_bytes = self._render_page() + self._login_page_bytes = _LOGIN_HTML_TEMPLATE.encode("utf-8") + + @property + def page(self) -> bytes: + return self._page_bytes + + @property + def login_page(self) -> bytes: + return self._login_page_bytes + + def _render_page(self) -> bytes: + parts_payload = [part.to_dict() for part in self._parts] + parts_json = json.dumps(parts_payload, separators=(",", ":")).replace(" List[PartRecord]: + if not self.config_path.is_file(): + raise FileNotFoundError(f"Config file '{self.config_path}' does not exist.") + + data = json.loads(self.config_path.read_text(encoding="utf-8")) + if isinstance(data, dict): + parts_data = data.get("Parts") or data.get("parts") + elif isinstance(data, list): + parts_data = data + else: + raise ValueError("Configuration must be a list or contain a 'Parts' array.") + + if not isinstance(parts_data, list): + raise ValueError("Invalid configuration: expected an array of part objects.") + + parts: List[PartRecord] = [] + for index, entry in enumerate(parts_data, start=1): + if not isinstance(entry, dict): + raise ValueError(f"Part entry #{index} is not an object.") + + part = self._build_part(entry, index) + parts.append(part) + + logging.info("Loaded %d part(s) from %s", len(parts), self.config_path) + return parts + + def _build_part(self, entry: Dict[str, Any], index: int) -> PartRecord: + name = self._get_field(entry, "Name") + if not name: + raise ValueError(f"Part #{index} is missing the 'Name' field.") + + symbol_path = self._get_field(entry, "Symbol") + if not symbol_path: + raise ValueError(f"Part '{name}' is missing the required 'Symbol' field.") + + assets: List[AssetRecord] = [] + component_entries: List[Dict[str, Any]] = [] + library_name = Path(symbol_path).stem + + field_plan = [ + ("Footprint", "DL_FOOTPRINT", "SAVE", "footprint"), + ("SPICE Model", "DL_SPICE", "SAVE", None), + ("3D Model", "DL_3DMODEL", "SAVE", "3dmodel"), + ("Symbol", "DL_SYMBOL", "PLACE", "symbol"), + ] + + for label, command, mode, component_type in field_plan: + path_value = self._get_field(entry, label) + if not path_value: + continue + + file_path = self._resolve_asset_path(path_value) + raw_bytes = file_path.read_bytes() + encoded = _encode_bytes(raw_bytes) + extracted_name = _extract_asset_name(file_path, command) + + assets.append( + AssetRecord( + command=command, + label=label, + filename=file_path.name, + data=encoded, + parameters={ + "mode": mode, + "compression": "ZSTD", + "content_type": _content_type_for_command(command), + "library": library_name, + "name": extracted_name, + }, + size_bytes=len(raw_bytes), + ) + ) + + if component_type: + if component_type == "3dmodel": + component_name = file_path.name + else: + component_name = extracted_name or file_path.stem + + component_entries.append( + { + "type": component_type, + "name": component_name, + "checksum": _hash_bytes(raw_bytes), + "compression": "ZSTD", + "content": encoded, + } + ) + + if not assets: + raise ValueError(f"Part '{name}' did not produce any downloadable assets.") + + if component_entries: + component_json = json.dumps(component_entries, separators=(",", ":")).encode("utf-8") + component_bundle = _encode_bytes(component_json) + assets.insert( + 0, + AssetRecord( + command="DL_COMPONENT", + label="Component Bundle", + filename=f"{library_name}_component_bundle.json", + data=component_bundle, + parameters={ + "compression": "ZSTD", + "library": library_name, + }, + size_bytes=len(component_json), + ), + ) + + image_path_value = self._get_field(entry, "Image") + image_data = self._encode_image(image_path_value) + + return PartRecord(name=name, image=image_data, assets=assets) + + def _get_field(self, entry: Dict[str, Any], key: str) -> Optional[str]: + direct = entry.get(key) + if direct: + return str(direct) + + lowered = key.lower() + for candidate in (lowered, key.replace(" ", ""), key.replace(" ", "_").lower()): + value = entry.get(candidate) + if value: + return str(value) + + return None + + def _resolve_asset_path(self, relative_path: str) -> Path: + base = self.config_path.parent + candidate = (base / relative_path).resolve() + + try: + candidate.relative_to(base) + except ValueError as err: + raise ValueError(f"Asset path '{relative_path}' must stay within {base}") from err + + if not candidate.is_file(): + raise FileNotFoundError(f"Asset '{relative_path}' does not exist under {base}") + + return candidate + + def _encode_image(self, relative_path: Optional[str]) -> str: + if not relative_path: + return _GENERIC_IMAGE_DATA_URI + + image_path = self._resolve_asset_path(relative_path) + mime = mimetypes.guess_type(image_path.name)[0] or "application/octet-stream" + encoded = base64.b64encode(image_path.read_bytes()).decode("ascii") + return f"data:{mime};base64,{encoded}" + + +def _encode_bytes(payload: bytes) -> str: + compressed = _ZSTD_COMPRESSOR.compress(payload) + return base64.b64encode(compressed).decode("ascii") + + +def _encode_file(file_path: Path) -> str: + """Read, zstd-compress, and base64-encode a file.""" + + return _encode_bytes(file_path.read_bytes()) + + +def _hash_bytes(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest() + + +def _extract_asset_name(file_path: Path, command: str) -> str: + """Try to extract the internal symbol/footprint name from the file. + + Falls back to the filename stem when extraction fails or for binary + assets. + """ + try: + text = file_path.read_text(encoding="utf-8", errors="ignore") + except Exception: + return file_path.stem + + cmd = (command or "").upper() + + # KiCad symbol libraries use s-expressions: (symbol "NAME" ...) + if cmd == "DL_SYMBOL": + m = re.search(r'\(symbol\s+"([^"]+)"', text) + if m: + return m.group(1) + # Legacy library format (DEF ...) + m = re.search(r'^\s*DEF\s+(\S+)', text, flags=re.MULTILINE) + if m: + return m.group(1) + + # Footprints: (footprint "NAME" ...) in modern .kicad_mod files + if cmd == "DL_FOOTPRINT": + m = re.search(r'\(footprint\s+"([^"]+)"', text) + if m: + return m.group(1) + # Another possible older token is 'module ' + m = re.search(r'^\s*module\s+([^\s(]+)', text, flags=re.MULTILINE) + if m: + return m.group(1) + + return file_path.stem + + +def _build_handler(state: _ServerState) -> type[http.server.BaseHTTPRequestHandler]: + class _WebViewTestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self) -> None: + parsed = urllib.parse.urlparse(self.path) + path = parsed.path + + if path in ("/", "/index.html"): + content = state.page + elif path == "/login": + content = state.login_page + else: + self.send_error(404) + return + + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + + def log_message(self, format: str, *args: Any) -> None: # noqa: A003 + logging.info("%s - %s", self.client_address[0], format % args) + + return _WebViewTestHandler + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Serve the KiCad WebView test page.") + parser.add_argument( + "--port", + type=int, + default=_DEFAULT_PORT, + help=f"Local TCP port to bind (default: {_DEFAULT_PORT})", + ) + parser.add_argument( + "--config", + type=Path, + default=_DEFAULT_CONFIG, + help=f"Path to the parts configuration JSON file (default: {_DEFAULT_CONFIG})", + ) + + return parser.parse_args() + + +def main() -> None: + args = _parse_args() + config_path = args.config.resolve() + state = _ServerState(config_path=config_path) + + handler_cls = _build_handler(state) + socketserver.TCPServer.allow_reuse_address = True + + with socketserver.TCPServer(("", args.port), handler_cls) as httpd: + logging.info( + "Serving WebView test page on http://localhost:%d/ using config %s", + args.port, + config_path, + ) + logging.info("Press Ctrl+C to stop.") + + try: + httpd.serve_forever() + except KeyboardInterrupt: + logging.info("Stopping server.") + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + main()