kicad/pcbnew/api/api_handler_pcb.cpp

495 lines
15 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 Jon Evans <jon@craftyjon.com>
* Copyright (C) 2023 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 <http://www.gnu.org/licenses/>.
*/
#include <magic_enum.hpp>
#include <api/api_handler_pcb.h>
#include <board_commit.h>
#include <pcb_edit_frame.h>
#include <pcb_track.h>
#include <tool/tool_manager.h>
#include <api/common/types/base_types.pb.h>
using namespace kiapi::common::commands;
using kiapi::common::types::CommandStatus;
using kiapi::common::types::DocumentType;
using kiapi::common::types::ItemRequestStatus;
static const wxString s_defaultCommitMessage = wxS( "Modification from API" );
API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* aFrame ) :
API_HANDLER(),
m_frame( aFrame ),
m_transactionInProgress( false )
{
registerHandler<RunAction, RunActionResponse>( &API_HANDLER_PCB::handleRunAction );
registerHandler<GetOpenDocuments, GetOpenDocumentsResponse>(
&API_HANDLER_PCB::handleGetOpenDocuments );
registerHandler<BeginCommit, BeginCommitResponse>( &API_HANDLER_PCB::handleBeginCommit );
registerHandler<EndCommit, EndCommitResponse>( &API_HANDLER_PCB::handleEndCommit );
registerHandler<CreateItems, CreateItemsResponse>( &API_HANDLER_PCB::handleCreateItems );
registerHandler<GetItems, GetItemsResponse>( &API_HANDLER_PCB::handleGetItems );
registerHandler<UpdateItems, UpdateItemsResponse>( &API_HANDLER_PCB::handleUpdateItems );
registerHandler<DeleteItems, DeleteItemsResponse>( &API_HANDLER_PCB::handleDeleteItems );
}
HANDLER_RESULT<RunActionResponse> API_HANDLER_PCB::handleRunAction( RunAction& aRequest )
{
RunActionResponse response;
if( m_frame->GetToolManager()->RunAction( aRequest.action(), true ) )
response.set_status( RunActionStatus::RAS_OK );
else
response.set_status( RunActionStatus::RAS_INVALID );
return response;
}
HANDLER_RESULT<GetOpenDocumentsResponse> API_HANDLER_PCB::handleGetOpenDocuments(
GetOpenDocuments& aMsg )
{
if( aMsg.type() != DocumentType::DOCTYPE_PCB )
{
ApiResponseStatus e;
// No message needed for AS_UNHANDLED; this is an internal flag for the API server
e.set_status( ApiStatusCode::AS_UNHANDLED );
return tl::unexpected( e );
}
GetOpenDocumentsResponse response;
common::types::DocumentSpecifier doc;
wxFileName fn( m_frame->GetCurrentFileName() );
doc.set_type( DocumentType::DOCTYPE_PCB );
doc.set_board_filename( fn.GetFullName() );
response.mutable_documents()->Add( std::move( doc ) );
return response;
}
HANDLER_RESULT<BeginCommitResponse> API_HANDLER_PCB::handleBeginCommit( BeginCommit& aMsg )
{
BeginCommitResponse response;
if( m_commit )
{
// TODO: right now there is no way for m_transactionInProgress to be true here, but
// we should still check it as a safety measure and return a specific error
//if( !m_transactionInProgress )
m_commit->Revert();
}
m_commit.reset( new BOARD_COMMIT( m_frame ) );
// TODO: return an opaque ID for this new commit to make this more robust
m_transactionInProgress = true;
return response;
}
HANDLER_RESULT<EndCommitResponse> API_HANDLER_PCB::handleEndCommit( EndCommit& aMsg )
{
EndCommitResponse response;
// TODO: return more specific error if m_transactionInProgress is false
if( !m_transactionInProgress )
{
// Make sure we don't get stuck with a commit we can never push
m_commit.reset();
response.set_result( CommitResult::CR_NO_COMMIT );
return response;
}
if( !m_commit )
{
response.set_result( CommitResult::CR_NO_COMMIT );
return response;
}
pushCurrentCommit( aMsg.message() );
m_transactionInProgress = false;
response.set_result( CommitResult::CR_OK );
return response;
}
BOARD_COMMIT* API_HANDLER_PCB::getCurrentCommit()
{
if( !m_commit )
m_commit.reset( new BOARD_COMMIT( m_frame ) );
return m_commit.get();
}
void API_HANDLER_PCB::pushCurrentCommit( const std::string& aMessage )
{
wxCHECK( m_commit, /* void */ );
wxString msg( aMessage.c_str(), wxConvUTF8 );
if( msg.IsEmpty() )
msg = s_defaultCommitMessage;
m_commit->Push( msg );
m_commit.reset();
m_frame->Refresh();
}
bool API_HANDLER_PCB::validateItemHeaderDocument( const common::types::ItemHeader& aHeader )
{
// TODO: this should return a more complex error type.
// We should provide detailed feedback when a header fails validation, and distinguish between
// "skip this handler" and "this is the right handler, but the request is invalid"
if( !aHeader.has_document() || aHeader.document().type() != DocumentType::DOCTYPE_PCB )
return false;
wxFileName fn( m_frame->GetCurrentFileName() );
return aHeader.document().board_filename().compare( fn.GetFullName() ) == 0;
}
std::unique_ptr<BOARD_ITEM> API_HANDLER_PCB::createItemForType( KICAD_T aType,
BOARD_ITEM_CONTAINER* aContainer )
{
switch( aType )
{
case PCB_TRACE_T: return std::make_unique<PCB_TRACK>( aContainer );
case PCB_ARC_T: return std::make_unique<PCB_ARC>( aContainer );
case PCB_VIA_T: return std::make_unique<PCB_VIA>( aContainer );
default: return nullptr;
}
}
HANDLER_RESULT<CreateItemsResponse> API_HANDLER_PCB::handleCreateItems( CreateItems& aMsg )
{
ApiResponseStatus e;
if( !validateItemHeaderDocument( aMsg.header() ) )
{
// No message needed for AS_UNHANDLED; this is an internal flag for the API server
e.set_status( ApiStatusCode::AS_UNHANDLED );
return tl::unexpected( e );
}
BOARD* board = m_frame->GetBoard();
BOARD_ITEM_SET boardItems = board->GetItemSet();
std::map<KIID, BOARD_ITEM*> itemUuidMap;
std::for_each( boardItems.begin(), boardItems.end(),
[&]( BOARD_ITEM* aItem )
{
itemUuidMap[aItem->m_Uuid] = aItem;
} );
BOARD_COMMIT* commit = getCurrentCommit();
CreateItemsResponse response;
for( const google::protobuf::Any& anyItem : aMsg.items() )
{
ItemCreationResult itemResult;
std::optional<KICAD_T> type = TypeNameFromAny( anyItem );
if( !type )
{
itemResult.set_status( ItemCreationStatus::ICS_INVALID_TYPE );
response.mutable_created_items()->Add( std::move( itemResult ) );
continue;
}
std::unique_ptr<BOARD_ITEM> item = createItemForType( *type, board );
if( !item )
{
itemResult.set_status( ItemCreationStatus::ICS_INVALID_TYPE );
e.set_error_message( fmt::format( "item type {} not supported for board",
magic_enum::enum_name( *type ) ) );
response.mutable_created_items()->Add( std::move( itemResult ) );
continue;
}
if( !item->Deserialize( anyItem ) )
{
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( fmt::format( "could not unpack {} from request",
item->GetClass().ToStdString() ) );
return tl::unexpected( e );
}
if( itemUuidMap.count( item->m_Uuid ) )
{
itemResult.set_status( ItemCreationStatus::ICS_EXISTING );
response.mutable_created_items()->Add( std::move( itemResult ) );
continue;
}
itemResult.set_status( ItemCreationStatus::ICS_OK );
item->Serialize( *itemResult.mutable_item() );
commit->Add( item.release() );
response.mutable_created_items()->Add( std::move( itemResult ) );
}
pushCurrentCommit( "Added items via API" );
response.set_status( ItemRequestStatus::IRS_OK );
return response;
}
HANDLER_RESULT<GetItemsResponse> API_HANDLER_PCB::handleGetItems( GetItems& aMsg )
{
if( !validateItemHeaderDocument( aMsg.header() ) )
{
ApiResponseStatus e;
// No message needed for AS_UNHANDLED; this is an internal flag for the API server
e.set_status( ApiStatusCode::AS_UNHANDLED );
return tl::unexpected( e );
}
GetItemsResponse response;
BOARD* board = m_frame->GetBoard();
std::vector<BOARD_ITEM*> items;
std::set<KICAD_T> typesRequested, typesInserted;
bool handledAnything = false;
for( const common::types::ItemType& typeMessage : aMsg.types() )
{
KICAD_T type;
if( std::optional<KICAD_T> opt_type = magic_enum::enum_cast<KICAD_T>( typeMessage.type() ) )
type = *opt_type;
else
continue;
typesRequested.emplace( type );
if( typesInserted.count( type ) )
continue;
switch( type )
{
case PCB_TRACE_T:
case PCB_ARC_T:
case PCB_VIA_T:
handledAnything = true;
std::copy( board->Tracks().begin(), board->Tracks().end(),
std::back_inserter( items ) );
typesInserted.insert( { PCB_TRACE_T, PCB_ARC_T, PCB_VIA_T } );
break;
default:
break;
}
}
if( !handledAnything )
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( "none of the requested types are valid for a Board object" );
return tl::unexpected( e );
}
for( const BOARD_ITEM* item : items )
{
if( !typesRequested.count( item->Type() ) )
continue;
google::protobuf::Any itemBuf;
item->Serialize( itemBuf );
response.mutable_items()->Add( std::move( itemBuf ) );
}
response.set_status( ItemRequestStatus::IRS_OK );
return response;
}
HANDLER_RESULT<UpdateItemsResponse> API_HANDLER_PCB::handleUpdateItems( UpdateItems& aMsg )
{
ApiResponseStatus e;
if( !validateItemHeaderDocument( aMsg.header() ) )
{
// No message needed for AS_UNHANDLED; this is an internal flag for the API server
e.set_status( ApiStatusCode::AS_UNHANDLED );
return tl::unexpected( e );
}
BOARD* board = m_frame->GetBoard();
BOARD_ITEM_SET boardItems = board->GetItemSet();
std::map<KIID, BOARD_ITEM*> itemUuidMap;
std::for_each( boardItems.begin(), boardItems.end(),
[&]( BOARD_ITEM* aItem )
{
itemUuidMap[aItem->m_Uuid] = aItem;
} );
BOARD_COMMIT* commit = getCurrentCommit();
UpdateItemsResponse response;
for( const google::protobuf::Any& anyItem : aMsg.items() )
{
ItemUpdateResult itemResult;
std::optional<KICAD_T> type = TypeNameFromAny( anyItem );
if( !type )
{
itemResult.set_status( ItemUpdateStatus::IUS_INVALID_TYPE );
response.mutable_updated_items()->Add( std::move( itemResult ) );
continue;
}
std::unique_ptr<BOARD_ITEM> temporaryItem = createItemForType( *type, board );
if( !temporaryItem )
{
itemResult.set_status( ItemUpdateStatus::IUS_INVALID_TYPE );
response.mutable_updated_items()->Add( std::move( itemResult ) );
continue;
}
if( !temporaryItem->Deserialize( anyItem ) )
{
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( fmt::format( "could not unpack {} from request",
magic_enum::enum_name( *type ) ) );
return tl::unexpected( e );
}
if( !itemUuidMap.count( temporaryItem->m_Uuid ) )
{
itemResult.set_status( ItemUpdateStatus::IUS_NONEXISTENT );
response.mutable_updated_items()->Add( std::move( itemResult ) );
continue;
}
BOARD_ITEM* boardItem = itemUuidMap[temporaryItem->m_Uuid];
boardItem->SwapItemData( temporaryItem.get() );
itemResult.set_status( ItemUpdateStatus::IUS_OK );
boardItem->Serialize( *itemResult.mutable_item() );
commit->Modify( boardItem );
itemResult.set_status( ItemUpdateStatus::IUS_OK );
response.mutable_updated_items()->Add( std::move( itemResult ) );
}
response.set_status( ItemRequestStatus::IRS_OK );
pushCurrentCommit( "Updated items via API" );
return response;
}
HANDLER_RESULT<DeleteItemsResponse> API_HANDLER_PCB::handleDeleteItems( DeleteItems& aMsg )
{
if( !validateItemHeaderDocument( aMsg.header() ) )
{
ApiResponseStatus e;
// No message needed for AS_UNHANDLED; this is an internal flag for the API server
e.set_status( ApiStatusCode::AS_UNHANDLED );
return tl::unexpected( e );
}
std::map<KIID, ItemDeletionStatus> itemsToDelete;
for( const common::types::KIID& kiidBuf : aMsg.item_ids() )
{
if( !kiidBuf.value().empty() )
{
KIID kiid( kiidBuf.value() );
itemsToDelete[kiid] = ItemDeletionStatus::IDS_NONEXISTENT;
}
}
if( itemsToDelete.empty() )
{
ApiResponseStatus e;
e.set_status( ApiStatusCode::AS_BAD_REQUEST );
e.set_error_message( "no valid items to delete were given" );
return tl::unexpected( e );
}
BOARD* board = m_frame->GetBoard();
// This is somewhat inefficient on paper, but the total number of items on a board is
// not computationally-speaking very high even on what we'd consider a large design.
// If this ends up not being the case, we should consider doing something like refactoring
// BOARD to contain all items in a contiguous memory arena and constructing views over it
// when we want to filter to just tracks, etc.
BOARD_ITEM_SET items = board->GetItemSet();
std::vector<BOARD_ITEM*> validatedItems;
for( BOARD_ITEM* item : items )
{
if( itemsToDelete.count( item->m_Uuid ) )
{
validatedItems.push_back( item );
itemsToDelete[item->m_Uuid] = ItemDeletionStatus::IDS_OK;
}
// Note: we don't currently support locking items from API modification, but here is where
// to add it in the future (and return IDS_IMMUTABLE)
}
BOARD_COMMIT* commit = getCurrentCommit();
for( BOARD_ITEM* item : validatedItems )
commit->Remove( item );
if( !m_transactionInProgress )
pushCurrentCommit( "Deleted items via API" );
DeleteItemsResponse response;
for( const auto& [id, status] : itemsToDelete )
{
ItemDeletionResult result;
result.mutable_id()->set_value( id.AsStdString() );
result.set_status( status );
}
response.set_status( ItemRequestStatus::IRS_OK );
return response;
}