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 0000000000..3b62f0645b Binary files /dev/null and b/utils/webview_test/server/package/server.zip differ 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()