/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Jon Evans * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace kiapi::common::commands; using kiapi::common::types::CommandStatus; using kiapi::common::types::DocumentType; using kiapi::common::types::ItemRequestStatus; API_HANDLER_PCB::API_HANDLER_PCB( PCB_EDIT_FRAME* aFrame ) : API_HANDLER_EDITOR( aFrame ) { registerHandler( &API_HANDLER_PCB::handleRunAction ); registerHandler( &API_HANDLER_PCB::handleGetOpenDocuments ); registerHandler( &API_HANDLER_PCB::handleGetItems ); registerHandler( &API_HANDLER_PCB::handleGetStackup ); registerHandler( &API_HANDLER_PCB::handleGetGraphicsDefaults ); registerHandler( &API_HANDLER_PCB::handleGetTextExtents ); registerHandler( &API_HANDLER_PCB::handleInteractiveMoveItems ); registerHandler( &API_HANDLER_PCB::handleGetNets ); registerHandler( &API_HANDLER_PCB::handleRefillZones ); } PCB_EDIT_FRAME* API_HANDLER_PCB::frame() const { return static_cast( m_frame ); } HANDLER_RESULT API_HANDLER_PCB::handleRunAction( RunAction& aRequest, const HANDLER_CONTEXT& ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); RunActionResponse response; if( frame()->GetToolManager()->RunAction( aRequest.action(), true ) ) response.set_status( RunActionStatus::RAS_OK ); else response.set_status( RunActionStatus::RAS_INVALID ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetOpenDocuments( GetOpenDocuments& aMsg, const HANDLER_CONTEXT& ) { 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( frame()->GetCurrentFileName() ); doc.set_type( DocumentType::DOCTYPE_PCB ); doc.set_board_filename( fn.GetFullName() ); doc.mutable_project()->set_name( frame()->Prj().GetProjectName().ToStdString() ); doc.mutable_project()->set_path( frame()->Prj().GetProjectDirectory().ToStdString() ); response.mutable_documents()->Add( std::move( doc ) ); return response; } void API_HANDLER_PCB::pushCurrentCommit( const HANDLER_CONTEXT& aCtx, const wxString& aMessage ) { API_HANDLER_EDITOR::pushCurrentCommit( aCtx, aMessage ); frame()->Refresh(); } std::unique_ptr API_HANDLER_PCB::createCommit() { return std::make_unique( frame() ); } std::optional API_HANDLER_PCB::getItemById( const KIID& aId ) const { BOARD_ITEM* item = frame()->GetBoard()->GetItem( aId ); if( item == DELETED_BOARD_ITEM::GetInstance() ) return std::nullopt; return item; } bool API_HANDLER_PCB::validateDocumentInternal( const DocumentSpecifier& aDocument ) const { if( aDocument.type() != DocumentType::DOCTYPE_PCB ) return false; wxFileName fn( frame()->GetCurrentFileName() ); return 0 == aDocument.board_filename().compare( fn.GetFullName() ); } HANDLER_RESULT> API_HANDLER_PCB::createItemForType( KICAD_T aType, BOARD_ITEM_CONTAINER* aContainer ) { if( !aContainer ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "Tried to create an item in a null container" ); return tl::unexpected( e ); } if( aType == PCB_PAD_T && !dynamic_cast( aContainer ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create a pad in {}, which is not a footprint", aContainer->GetFriendlyName().ToStdString() ) ); return tl::unexpected( e ); } else if( aType == PCB_FOOTPRINT_T && !dynamic_cast( aContainer ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create a footprint in {}, which is not a board", aContainer->GetFriendlyName().ToStdString() ) ); return tl::unexpected( e ); } std::unique_ptr created = CreateItemForType( aType, aContainer ); if( !created ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "Tried to create an item of type {}, which is unhandled", magic_enum::enum_name( aType ) ) ); return tl::unexpected( e ); } return created; } HANDLER_RESULT API_HANDLER_PCB::handleCreateUpdateItemsInternal( bool aCreate, const HANDLER_CONTEXT& aCtx, const types::ItemHeader &aHeader, const google::protobuf::RepeatedPtrField& aItems, std::function aItemHandler ) { ApiResponseStatus e; auto containerResult = validateItemHeaderDocument( aHeader ); if( !containerResult && containerResult.error().status() == ApiStatusCode::AS_UNHANDLED ) { // 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 ); } else if( !containerResult ) { e.CopyFrom( containerResult.error() ); return tl::unexpected( e ); } BOARD* board = frame()->GetBoard(); BOARD_ITEM_CONTAINER* container = board; if( containerResult->has_value() ) { const KIID& containerId = **containerResult; std::optional optItem = getItemById( containerId ); if( optItem ) { container = dynamic_cast( *optItem ); if( !container ) { e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "The requested container {} is not a valid board item container", containerId.AsStdString() ) ); return tl::unexpected( e ); } } else { e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "The requested container {} does not exist in this document", containerId.AsStdString() ) ); return tl::unexpected( e ); } } BOARD_COMMIT* commit = static_cast( getCurrentCommit( aCtx ) ); for( const google::protobuf::Any& anyItem : aItems ) { ItemStatus status; std::optional type = TypeNameFromAny( anyItem ); if( !type ) { status.set_code( ItemStatusCode::ISC_INVALID_TYPE ); status.set_error_message( fmt::format( "Could not decode a valid type from {}", anyItem.type_url() ) ); aItemHandler( status, anyItem ); continue; } HANDLER_RESULT> creationResult = createItemForType( *type, container ); if( !creationResult ) { status.set_code( ItemStatusCode::ISC_INVALID_TYPE ); status.set_error_message( creationResult.error().error_message() ); aItemHandler( status, anyItem ); continue; } std::unique_ptr item( std::move( *creationResult ) ); 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 ); } std::optional optItem = getItemById( item->m_Uuid ); if( aCreate && optItem ) { status.set_code( ItemStatusCode::ISC_EXISTING ); status.set_error_message( fmt::format( "an item with UUID {} already exists", item->m_Uuid.AsStdString() ) ); aItemHandler( status, anyItem ); continue; } else if( !aCreate && !optItem ) { status.set_code( ItemStatusCode::ISC_NONEXISTENT ); status.set_error_message( fmt::format( "an item with UUID {} does not exist", item->m_Uuid.AsStdString() ) ); aItemHandler( status, anyItem ); continue; } if( aCreate && !board->GetEnabledLayers().Contains( item->GetLayer() ) ) { status.set_code( ItemStatusCode::ISC_INVALID_DATA ); status.set_error_message( fmt::format( "attempted to add item on disabled layer {}", LayerName( item->GetLayer() ).ToStdString() ) ); aItemHandler( status, anyItem ); continue; } status.set_code( ItemStatusCode::ISC_OK ); google::protobuf::Any newItem; if( aCreate ) { item->Serialize( newItem ); commit->Add( item.release() ); } else { BOARD_ITEM* boardItem = *optItem; commit->Modify( boardItem ); boardItem->SwapItemData( item.get() ); boardItem->Serialize( newItem ); } aItemHandler( status, newItem ); } if( !m_activeClients.count( aCtx.ClientName ) ) { pushCurrentCommit( aCtx, aCreate ? _( "Created items via API" ) : _( "Added items via API" ) ); } return ItemRequestStatus::IRS_OK; } HANDLER_RESULT API_HANDLER_PCB::handleGetItems( GetItems& aMsg, const HANDLER_CONTEXT& ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); 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 = frame()->GetBoard(); std::vector items; std::set typesRequested, typesInserted; bool handledAnything = false; for( int typeRaw : aMsg.types() ) { auto typeMessage = static_cast( typeRaw ); KICAD_T type = FromProtoEnum( typeMessage ); if( type == TYPE_NOT_INIT ) 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; case PCB_PAD_T: { handledAnything = true; for( FOOTPRINT* fp : board->Footprints() ) { std::copy( fp->Pads().begin(), fp->Pads().end(), std::back_inserter( items ) ); } typesInserted.insert( PCB_PAD_T ); break; } case PCB_FOOTPRINT_T: { handledAnything = true; std::copy( board->Footprints().begin(), board->Footprints().end(), std::back_inserter( items ) ); typesInserted.insert( PCB_FOOTPRINT_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; } void API_HANDLER_PCB::deleteItemsInternal( std::map& aItemsToDelete, const HANDLER_CONTEXT& aCtx ) { BOARD* board = frame()->GetBoard(); std::vector validatedItems; for( std::pair pair : aItemsToDelete ) { if( BOARD_ITEM* item = board->GetItem( pair.first ) ) { validatedItems.push_back( item ); aItemsToDelete[pair.first] = 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) } COMMIT* commit = getCurrentCommit( aCtx ); for( BOARD_ITEM* item : validatedItems ) commit->Remove( item ); if( !m_activeClients.count( aCtx.ClientName ) ) pushCurrentCommit( aCtx, _( "Deleted items via API" ) ); } std::optional API_HANDLER_PCB::getItemFromDocument( const DocumentSpecifier& aDocument, const KIID& aId ) { if( !validateDocument( aDocument ) ) return std::nullopt; return getItemById( aId ); } HANDLER_RESULT API_HANDLER_PCB::handleGetStackup( GetBoardStackup& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); const BOARD* board = frame()->GetBoard(); BoardStackupResponse response; google::protobuf::Any any; const BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings(); if( frame()->GetBoard()->GetDesignSettings().m_HasStackup ) { const BOARD_STACKUP& stackup = bds.GetStackupDescriptor(); stackup.Serialize( any ); } else { BOARD_STACKUP stackup; stackup.BuildDefaultStackupList( &bds, board->GetCopperLayerCount() ); stackup.Serialize( any ); } any.UnpackTo( response.mutable_stackup() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetGraphicsDefaults( GetGraphicsDefaults& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); const BOARD_DESIGN_SETTINGS& bds = frame()->GetBoard()->GetDesignSettings(); GraphicsDefaultsResponse response; // TODO: This should change to be an enum class constexpr std::array classOrder = { kiapi::board::BLC_SILKSCREEN, kiapi::board::BLC_COPPER, kiapi::board::BLC_EDGES, kiapi::board::BLC_COURTYARD, kiapi::board::BLC_FABRICATION, kiapi::board::BLC_OTHER }; for( int i = 0; i < LAYER_CLASS_COUNT; ++i ) { kiapi::board::BoardLayerGraphicsDefaults* l = response.mutable_defaults()->add_layers(); l->set_layer( classOrder[i] ); l->mutable_line_thickness()->set_value_nm( bds.m_LineThickness[i] ); kiapi::common::types::TextAttributes* text = l->mutable_text(); text->mutable_size()->set_x_nm( bds.m_TextSize[i].x ); text->mutable_size()->set_y_nm( bds.m_TextSize[i].y ); text->mutable_stroke_width()->set_value_nm( bds.m_TextThickness[i] ); text->set_italic( bds.m_TextItalic[i] ); text->set_keep_upright( bds.m_TextUpright[i] ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetTextExtents( GetTextExtents& aMsg, const HANDLER_CONTEXT& aCtx ) { PCB_TEXT text( frame()->GetBoard() ); google::protobuf::Any any; any.PackFrom( aMsg.text() ); if( !text.Deserialize( any ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "Could not decode text in GetTextExtents message" ); return tl::unexpected( e ); } commands::BoundingBoxResponse response; BOX2I bbox = text.GetTextBox(); EDA_ANGLE angle = text.GetTextAngle(); if( !angle.IsZero() ) bbox = bbox.GetBoundingBoxRotated( text.GetTextPos(), text.GetTextAngle() ); response.mutable_position()->set_x_nm( bbox.GetPosition().x ); response.mutable_position()->set_y_nm( bbox.GetPosition().y ); response.mutable_size()->set_x_nm( bbox.GetSize().x ); response.mutable_size()->set_y_nm( bbox.GetSize().y ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleInteractiveMoveItems( InteractiveMoveItems& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); TOOL_MANAGER* mgr = frame()->GetToolManager(); std::vector toSelect; for( const kiapi::common::types::KIID& id : aMsg.items() ) { if( std::optional item = getItemById( KIID( id.value() ) ) ) toSelect.emplace_back( static_cast( *item ) ); } if( toSelect.empty() ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "None of the given items exist on the board", aMsg.board().board_filename() ) ); return tl::unexpected( e ); } PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); selectionTool->GetSelection().SetReferencePoint( toSelect[0]->GetPosition() ); mgr->RunAction( PCB_ACTIONS::selectionClear ); mgr->RunAction( PCB_ACTIONS::selectItems, &toSelect ); COMMIT* commit = getCurrentCommit( aCtx ); mgr->PostAction( PCB_ACTIONS::move, commit ); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetNets( GetNets& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); NetsResponse response; BOARD* board = frame()->GetBoard(); std::set netclassFilter; for( const std::string& nc : aMsg.netclass_filter() ) netclassFilter.insert( wxString( nc.c_str(), wxConvUTF8 ) ); for( NETINFO_ITEM* net : board->GetNetInfo() ) { NETCLASS* nc = net->GetNetClass(); if( !netclassFilter.empty() && nc && !netclassFilter.count( nc->GetName() ) ) continue; board::types::Net* netProto = response.add_nets(); netProto->set_name( net->GetNetname() ); netProto->mutable_code()->set_value( net->GetNetCode() ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleRefillZones( RefillZones& aMsg, const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aMsg.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); if( aMsg.zones().empty() ) { TOOL_MANAGER* mgr = frame()->GetToolManager(); frame()->CallAfter( [mgr]() { mgr->RunAction( PCB_ACTIONS::zoneFillAll ); } ); } else { // TODO ApiResponseStatus e; e.set_status( ApiStatusCode::AS_UNIMPLEMENTED ); return tl::unexpected( e ); } return Empty(); }