/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Jon Evans * 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 #include #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 types::CommandStatus; using types::DocumentType; using 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::handleSaveDocument ); registerHandler( &API_HANDLER_PCB::handleSaveCopyOfDocument ); registerHandler( &API_HANDLER_PCB::handleRevertDocument ); registerHandler( &API_HANDLER_PCB::handleGetItems ); registerHandler( &API_HANDLER_PCB::handleGetItemsById ); registerHandler( &API_HANDLER_PCB::handleGetSelection ); registerHandler( &API_HANDLER_PCB::handleClearSelection ); registerHandler( &API_HANDLER_PCB::handleAddToSelection ); registerHandler( &API_HANDLER_PCB::handleRemoveFromSelection ); registerHandler( &API_HANDLER_PCB::handleGetStackup ); registerHandler( &API_HANDLER_PCB::handleGetGraphicsDefaults ); registerHandler( &API_HANDLER_PCB::handleGetBoundingBox ); registerHandler( &API_HANDLER_PCB::handleGetPadShapeAsPolygon ); registerHandler( &API_HANDLER_PCB::handleCheckPadstackPresenceOnLayers ); registerHandler( &API_HANDLER_PCB::handleGetTitleBlockInfo ); registerHandler( &API_HANDLER_PCB::handleExpandTextVariables ); registerHandler( &API_HANDLER_PCB::handleGetBoardOrigin ); registerHandler( &API_HANDLER_PCB::handleSetBoardOrigin ); registerHandler( &API_HANDLER_PCB::handleInteractiveMoveItems ); registerHandler( &API_HANDLER_PCB::handleGetNets ); registerHandler( &API_HANDLER_PCB::handleGetNetClassForNets ); registerHandler( &API_HANDLER_PCB::handleRefillZones ); registerHandler( &API_HANDLER_PCB::handleSaveDocumentToString ); registerHandler( &API_HANDLER_PCB::handleSaveSelectionToString ); registerHandler( &API_HANDLER_PCB::handleParseAndCreateItemsFromString ); registerHandler( &API_HANDLER_PCB::handleGetVisibleLayers ); registerHandler( &API_HANDLER_PCB::handleSetVisibleLayers ); registerHandler( &API_HANDLER_PCB::handleGetActiveLayer ); registerHandler( &API_HANDLER_PCB::handleSetActiveLayer ); registerHandler( &API_HANDLER_PCB::handleGetBoardEditorAppearanceSettings ); registerHandler( &API_HANDLER_PCB::handleSetBoardEditorAppearanceSettings ); } PCB_EDIT_FRAME* API_HANDLER_PCB::frame() const { return static_cast( m_frame ); } HANDLER_RESULT API_HANDLER_PCB::handleRunAction( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); RunActionResponse response; if( frame()->GetToolManager()->RunAction( aCtx.Request.action(), true ) ) response.set_status( RunActionStatus::RAS_OK ); else response.set_status( RunActionStatus::RAS_INVALID ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetOpenDocuments( const HANDLER_CONTEXT& aCtx ) { if( aCtx.Request.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; } HANDLER_RESULT API_HANDLER_PCB::handleSaveDocument( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); frame()->SaveBoard(); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleSaveCopyOfDocument( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); wxFileName boardPath( frame()->Prj().AbsolutePath( wxString::FromUTF8( aCtx.Request.path() ) ) ); if( !boardPath.IsOk() || !boardPath.IsDirWritable() ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "save path '{}' could not be opened", boardPath.GetFullPath().ToStdString() ) ); return tl::unexpected( e ); } if( boardPath.FileExists() && ( !boardPath.IsFileWritable() || !aCtx.Request.options().overwrite() ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "save path '{}' exists and cannot be overwritten", boardPath.GetFullPath().ToStdString() ) ); return tl::unexpected( e ); } if( boardPath.GetExt() != FILEEXT::KiCadPcbFileExtension ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( fmt::format( "save path '{}' must have a kicad_pcb extension", boardPath.GetFullPath().ToStdString() ) ); return tl::unexpected( e ); } BOARD* board = frame()->GetBoard(); if( board->GetFileName().Matches( boardPath.GetFullPath() ) ) { frame()->SaveBoard(); return Empty(); } bool includeProject = true; if( aCtx.Request.has_options() ) includeProject = aCtx.Request.options().include_project(); frame()->SavePcbCopy( boardPath.GetFullPath(), includeProject, /* aHeadless = */ true ); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleRevertDocument( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); wxFileName fn = frame()->Prj().AbsolutePath( frame()->GetBoard()->GetFileName() ); frame()->GetScreen()->SetContentModified( false ); frame()->ReleaseFile(); frame()->OpenProjectFiles( std::vector( 1, fn.GetFullPath() ), KICTL_REVERT ); return Empty(); } void API_HANDLER_PCB::pushCurrentCommit( const std::string& aClientName, const wxString& aMessage ) { API_HANDLER_EDITOR::pushCurrentCommit( aClientName, 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()->ResolveItem( aId, true ); if( !item ) 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 std::string& aClientName, 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( aClientName ) ); 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; } if( type == PCB_DIMENSION_T ) { board::types::Dimension dimension; anyItem.UnpackTo( &dimension ); switch( dimension.dimension_style_case() ) { case board::types::Dimension::kAligned: type = PCB_DIM_ALIGNED_T; break; case board::types::Dimension::kOrthogonal: type = PCB_DIM_ORTHOGONAL_T; break; case board::types::Dimension::kRadial: type = PCB_DIM_RADIAL_T; break; case board::types::Dimension::kLeader: type = PCB_DIM_LEADER_T; break; case board::types::Dimension::kCenter: type = PCB_DIM_CENTER_T; break; case board::types::Dimension::DIMENSION_STYLE_NOT_SET: break; } } 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() & item->GetLayerSet() ).any() ) { status.set_code( ItemStatusCode::ISC_INVALID_DATA ); status.set_error_message( "attempted to add item with no overlapping layers with the board" ); 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; // Footprints can't be modified by CopyFrom at the moment because the commit system // doesn't currently know what to do with a footprint that has had its children // replaced with other children; which results in things like the view not having its // cached geometry for footprint children updated when you move a footprint around. // And also, groups are special because they can contain any item type, so we // can't use CopyFrom on them either. if( boardItem->Type() == PCB_FOOTPRINT_T || boardItem->Type() == PCB_GROUP_T ) { commit->Remove( boardItem ); item->Serialize( newItem ); commit->Add( item.release() ); } else { commit->Modify( boardItem ); boardItem->CopyFrom( item.get() ); boardItem->Serialize( newItem ); } } aItemHandler( status, newItem ); } if( !m_activeClients.count( aClientName ) ) { pushCurrentCommit( aClientName, aCreate ? _( "Created items via API" ) : _( "Modified items via API" ) ); } return ItemRequestStatus::IRS_OK; } HANDLER_RESULT API_HANDLER_PCB::handleGetItems( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.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 : aCtx.Request.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; } case PCB_SHAPE_T: case PCB_TEXT_T: case PCB_TEXTBOX_T: { handledAnything = true; bool inserted = false; for( BOARD_ITEM* item : board->Drawings() ) { if( item->Type() == type ) { items.emplace_back( item ); inserted = true; } } if( inserted ) typesInserted.insert( PCB_SHAPE_T ); break; } case PCB_ZONE_T: { handledAnything = true; std::copy( board->Zones().begin(), board->Zones().end(), std::back_inserter( items ) ); typesInserted.insert( PCB_ZONE_T ); break; } case PCB_GROUP_T: { handledAnything = true; std::copy( board->Groups().begin(), board->Groups().end(), std::back_inserter( items ) ); typesInserted.insert( PCB_GROUP_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 API_HANDLER_PCB::handleGetItemsById( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.header() ) ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_UNHANDLED ); return tl::unexpected( e ); } GetItemsResponse response; std::vector items; for( const kiapi::common::types::KIID& id : aCtx.Request.items() ) { if( std::optional item = getItemById( KIID( id.value() ) ) ) items.emplace_back( *item ); } if( items.empty() ) { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "none of the requested IDs were found or valid" ); return tl::unexpected( e ); } for( const BOARD_ITEM* item : items ) { 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 std::string& aClientName ) { BOARD* board = frame()->GetBoard(); std::vector validatedItems; for( std::pair pair : aItemsToDelete ) { if( BOARD_ITEM* item = board->ResolveItem( pair.first, true ) ) { 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( aClientName ); for( BOARD_ITEM* item : validatedItems ) commit->Remove( item ); if( !m_activeClients.count( aClientName ) ) pushCurrentCommit( aClientName, _( "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::handleGetSelection( const HANDLER_CONTEXT& aCtx ) { if( !validateItemHeaderDocument( aCtx.Request.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::set filter; for( int typeRaw : aCtx.Request.types() ) { auto typeMessage = static_cast( typeRaw ); KICAD_T type = FromProtoEnum( typeMessage ); if( type == TYPE_NOT_INIT ) continue; filter.insert( type ); } TOOL_MANAGER* mgr = frame()->GetToolManager(); PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); SelectionResponse response; for( EDA_ITEM* item : selectionTool->GetSelection() ) { if( filter.empty() || filter.contains( item->Type() ) ) item->Serialize( *response.add_items() ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleClearSelection( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.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 ); } TOOL_MANAGER* mgr = frame()->GetToolManager(); mgr->RunAction( ACTIONS::selectionClear ); frame()->Refresh(); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleAddToSelection( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.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 ); } TOOL_MANAGER* mgr = frame()->GetToolManager(); PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); std::vector toAdd; for( const types::KIID& id : aCtx.Request.items() ) { if( std::optional item = getItemById( KIID( id.value() ) ) ) toAdd.emplace_back( *item ); } selectionTool->AddItemsToSel( &toAdd ); frame()->Refresh(); SelectionResponse response; for( EDA_ITEM* item : selectionTool->GetSelection() ) item->Serialize( *response.add_items() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleRemoveFromSelection( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.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 ); } TOOL_MANAGER* mgr = frame()->GetToolManager(); PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); std::vector toRemove; for( const types::KIID& id : aCtx.Request.items() ) { if( std::optional item = getItemById( KIID( id.value() ) ) ) toRemove.emplace_back( *item ); } selectionTool->RemoveItemsFromSel( &toRemove ); frame()->Refresh(); SelectionResponse response; for( EDA_ITEM* item : selectionTool->GetSelection() ) item->Serialize( *response.add_items() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetStackup( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BoardStackupResponse response; google::protobuf::Any any; frame()->GetBoard()->GetStackupOrDefault().Serialize( any ); any.UnpackTo( response.mutable_stackup() ); // User-settable layer names are not stored in BOARD_STACKUP at the moment for( board::BoardStackupLayer& layer : *response.mutable_stackup()->mutable_layers() ) { if( layer.type() == board::BoardStackupLayerType::BSLT_DIELECTRIC ) continue; PCB_LAYER_ID id = FromProtoEnum( layer.layer() ); wxCHECK2( id != UNDEFINED_LAYER, continue ); layer.set_user_name( frame()->GetBoard()->GetLayerName( id ) ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetGraphicsDefaults( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.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::handleGetBoardOrigin( const HANDLER_CONTEXT& aCtx ) { if( HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); !documentValidation ) { return tl::unexpected( documentValidation.error() ); } VECTOR2I origin; const BOARD_DESIGN_SETTINGS& settings = frame()->GetBoard()->GetDesignSettings(); switch( aCtx.Request.type() ) { case BOT_GRID: origin = settings.GetGridOrigin(); break; case BOT_DRILL: origin = settings.GetAuxOrigin(); break; default: case BOT_UNKNOWN: { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "Unexpected origin type" ); return tl::unexpected( e ); } } types::Vector2 reply; PackVector2( reply, origin ); return reply; } HANDLER_RESULT API_HANDLER_PCB::handleSetBoardOrigin( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); !documentValidation ) { return tl::unexpected( documentValidation.error() ); } VECTOR2I origin = UnpackVector2( aCtx.Request.origin() ); switch( aCtx.Request.type() ) { case BOT_GRID: { PCB_EDIT_FRAME* f = frame(); frame()->CallAfter( [f, origin]() { // gridSetOrigin takes ownership and frees this VECTOR2D* dorigin = new VECTOR2D( origin ); TOOL_MANAGER* mgr = f->GetToolManager(); mgr->RunAction( PCB_ACTIONS::gridSetOrigin, dorigin ); f->Refresh(); } ); break; } case BOT_DRILL: { PCB_EDIT_FRAME* f = frame(); frame()->CallAfter( [f, origin]() { TOOL_MANAGER* mgr = f->GetToolManager(); mgr->RunAction( PCB_ACTIONS::drillSetOrigin, origin ); f->Refresh(); } ); break; } default: case BOT_UNKNOWN: { ApiResponseStatus e; e.set_status( ApiStatusCode::AS_BAD_REQUEST ); e.set_error_message( "Unexpected origin type" ); return tl::unexpected( e ); } } return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetBoundingBox( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); if( !validateItemHeaderDocument( aCtx.Request.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 ); } GetBoundingBoxResponse response; bool includeText = aCtx.Request.mode() == BoundingBoxMode::BBM_ITEM_AND_CHILD_TEXT; for( const types::KIID& idMsg : aCtx.Request.items() ) { KIID id( idMsg.value() ); std::optional optItem = getItemById( id ); if( !optItem ) continue; BOARD_ITEM* item = *optItem; BOX2I bbox; if( item->Type() == PCB_FOOTPRINT_T ) bbox = static_cast( item )->GetBoundingBox( includeText ); else bbox = item->GetBoundingBox(); response.add_items()->set_value( idMsg.value() ); PackBox2( *response.add_boxes(), bbox ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetPadShapeAsPolygon( const HANDLER_CONTEXT& aCtx ) { if( HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); !documentValidation ) { return tl::unexpected( documentValidation.error() ); } PadShapeAsPolygonResponse response; PCB_LAYER_ID layer = FromProtoEnum( aCtx.Request.layer() ); for( const types::KIID& padRequest : aCtx.Request.pads() ) { KIID id( padRequest.value() ); std::optional optPad = getItemById( id ); if( !optPad || ( *optPad )->Type() != PCB_PAD_T ) continue; response.add_pads()->set_value( padRequest.value() ); PAD* pad = static_cast( *optPad ); SHAPE_POLY_SET poly; pad->TransformShapeToPolygon( poly, pad->Padstack().EffectiveLayerFor( layer ), 0, ARC_HIGH_DEF, ERROR_INSIDE ); types::PolygonWithHoles* polyMsg = response.mutable_polygons()->Add(); PackPolyLine( *polyMsg->mutable_outline(), poly.COutline( 0 ) ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleCheckPadstackPresenceOnLayers( const HANDLER_CONTEXT& aCtx ) { using board::types::BoardLayer; if( HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); !documentValidation ) { return tl::unexpected( documentValidation.error() ); } PadstackPresenceResponse response; LSET layers; for( const int layer : aCtx.Request.layers() ) layers.set( FromProtoEnum( static_cast( layer ) ) ); for( const types::KIID& padRequest : aCtx.Request.items() ) { KIID id( padRequest.value() ); std::optional optItem = getItemById( id ); if( !optItem ) continue; switch( ( *optItem )->Type() ) { case PCB_PAD_T: { PAD* pad = static_cast( *optItem ); for( PCB_LAYER_ID layer : layers ) { PadstackPresenceEntry* entry = response.add_entries(); entry->mutable_item()->set_value( pad->m_Uuid.AsStdString() ); entry->set_layer( ToProtoEnum( layer ) ); entry->set_presence( pad->FlashLayer( layer ) ? PSP_PRESENT : PSP_NOT_PRESENT ); } break; } case PCB_VIA_T: { PCB_VIA* via = static_cast( *optItem ); for( PCB_LAYER_ID layer : layers ) { PadstackPresenceEntry* entry = response.add_entries(); entry->mutable_item()->set_value( via->m_Uuid.AsStdString() ); entry->set_layer( ToProtoEnum( layer ) ); entry->set_presence( via->FlashLayer( layer ) ? PSP_PRESENT : PSP_NOT_PRESENT ); } break; } default: break; } } return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetTitleBlockInfo( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BOARD* board = frame()->GetBoard(); const TITLE_BLOCK& block = board->GetTitleBlock(); types::TitleBlockInfo response; response.set_title( block.GetTitle().ToUTF8() ); response.set_date( block.GetDate().ToUTF8() ); response.set_revision( block.GetRevision().ToUTF8() ); response.set_company( block.GetCompany().ToUTF8() ); response.set_comment1( block.GetComment( 0 ).ToUTF8() ); response.set_comment2( block.GetComment( 1 ).ToUTF8() ); response.set_comment3( block.GetComment( 2 ).ToUTF8() ); response.set_comment4( block.GetComment( 3 ).ToUTF8() ); response.set_comment5( block.GetComment( 4 ).ToUTF8() ); response.set_comment6( block.GetComment( 5 ).ToUTF8() ); response.set_comment7( block.GetComment( 6 ).ToUTF8() ); response.set_comment8( block.GetComment( 7 ).ToUTF8() ); response.set_comment9( block.GetComment( 8 ).ToUTF8() ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleExpandTextVariables( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); ExpandTextVariablesResponse reply; BOARD* board = frame()->GetBoard(); std::function textResolver = [&]( wxString* token ) -> bool { // Handles m_board->GetTitleBlock() *and* m_board->GetProject() return board->ResolveTextVar( token, 0 ); }; for( const std::string& textMsg : aCtx.Request.text() ) { wxString text = ExpandTextVars( wxString::FromUTF8( textMsg ), &textResolver ); reply.add_text( text.ToUTF8() ); } return reply; } HANDLER_RESULT API_HANDLER_PCB::handleInteractiveMoveItems( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); TOOL_MANAGER* mgr = frame()->GetToolManager(); std::vector toSelect; for( const kiapi::common::types::KIID& id : aCtx.Request.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", aCtx.Request.board().board_filename() ) ); return tl::unexpected( e ); } PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); selectionTool->GetSelection().SetReferencePoint( toSelect[0]->GetPosition() ); mgr->RunAction( ACTIONS::selectionClear ); mgr->RunAction( ACTIONS::selectItems, &toSelect ); COMMIT* commit = getCurrentCommit( aCtx.ClientName ); mgr->PostAPIAction( PCB_ACTIONS::move, commit ); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetNets( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); NetsResponse response; BOARD* board = frame()->GetBoard(); std::set netclassFilter; for( const std::string& nc : aCtx.Request.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::handleGetNetClassForNets( const HANDLER_CONTEXT& aCtx ) { NetClassForNetsResponse response; BOARD* board = frame()->GetBoard(); const NETINFO_LIST& nets = board->GetNetInfo(); google::protobuf::Any any; for( const board::types::Net& net : aCtx.Request.net() ) { NETINFO_ITEM* netInfo = nets.GetNetItem( wxString::FromUTF8( net.name() ) ); if( !netInfo ) continue; netInfo->GetNetClass()->Serialize( any ); auto [pair, rc] = response.mutable_classes()->insert( { net.name(), {} } ); any.UnpackTo( &pair->second ); } return response; } HANDLER_RESULT API_HANDLER_PCB::handleRefillZones( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); if( aCtx.Request.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(); } HANDLER_RESULT API_HANDLER_PCB::handleSaveDocumentToString( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); SavedDocumentResponse response; response.mutable_document()->CopyFrom( aCtx.Request.document() ); CLIPBOARD_IO io; io.SetWriter( [&]( const wxString& aData ) { response.set_contents( aData.ToUTF8() ); } ); io.SaveBoard( wxEmptyString, frame()->GetBoard(), nullptr ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleSaveSelectionToString( const HANDLER_CONTEXT& aCtx ) { SavedSelectionResponse response; TOOL_MANAGER* mgr = frame()->GetToolManager(); PCB_SELECTION_TOOL* selectionTool = mgr->GetTool(); PCB_SELECTION& selection = selectionTool->GetSelection(); CLIPBOARD_IO io; io.SetWriter( [&]( const wxString& aData ) { response.set_contents( aData.ToUTF8() ); } ); io.SetBoard( frame()->GetBoard() ); io.SaveSelection( selection, false ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleParseAndCreateItemsFromString( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.document() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); CreateItemsResponse response; return response; } HANDLER_RESULT API_HANDLER_PCB::handleGetVisibleLayers( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BoardLayers response; for( PCB_LAYER_ID layer : frame()->GetBoard()->GetVisibleLayers() ) response.add_layers( ToProtoEnum( layer ) ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleSetVisibleLayers( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); LSET visible; LSET enabled = frame()->GetBoard()->GetEnabledLayers(); for( int layerIdx : aCtx.Request.layers() ) { PCB_LAYER_ID layer = FromProtoEnum( static_cast( layerIdx ) ); if( enabled.Contains( layer ) ) visible.set( layer ); } frame()->GetBoard()->SetVisibleLayers( visible ); frame()->GetAppearancePanel()->OnBoardChanged(); frame()->GetCanvas()->SyncLayersVisibility( frame()->GetBoard() ); frame()->Refresh(); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetActiveLayer( const HANDLER_CONTEXT& aCtx ) { HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); BoardLayerResponse response; response.set_layer( ToProtoEnum( frame()->GetActiveLayer() ) ); return response; } HANDLER_RESULT API_HANDLER_PCB::handleSetActiveLayer( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); HANDLER_RESULT documentValidation = validateDocument( aCtx.Request.board() ); if( !documentValidation ) return tl::unexpected( documentValidation.error() ); PCB_LAYER_ID layer = FromProtoEnum( aCtx.Request.layer() ); if( !frame()->GetBoard()->GetEnabledLayers().Contains( layer ) ) { ApiResponseStatus err; err.set_status( ApiStatusCode::AS_BAD_REQUEST ); err.set_error_message( fmt::format( "Layer {} is not a valid layer for the given board", magic_enum::enum_name( layer ) ) ); return tl::unexpected( err ); } frame()->SetActiveLayer( layer ); return Empty(); } HANDLER_RESULT API_HANDLER_PCB::handleGetBoardEditorAppearanceSettings( const HANDLER_CONTEXT& aCtx ) { BoardEditorAppearanceSettings reply; // TODO: might be nice to put all these things in one place and have it derive SERIALIZABLE const PCB_DISPLAY_OPTIONS& displayOptions = frame()->GetDisplayOptions(); reply.set_inactive_layer_display( ToProtoEnum( displayOptions.m_ContrastModeDisplay ) ); reply.set_net_color_display( ToProtoEnum( displayOptions.m_NetColorMode ) ); reply.set_board_flip( frame()->GetCanvas()->GetView()->IsMirroredX() ? BoardFlipMode::BFM_FLIPPED_X : BoardFlipMode::BFM_NORMAL ); PCBNEW_SETTINGS* editorSettings = frame()->GetPcbNewSettings(); reply.set_ratsnest_display( ToProtoEnum( editorSettings->m_Display.m_RatsnestMode ) ); return reply; } HANDLER_RESULT API_HANDLER_PCB::handleSetBoardEditorAppearanceSettings( const HANDLER_CONTEXT& aCtx ) { if( std::optional busy = checkForBusy() ) return tl::unexpected( *busy ); PCB_DISPLAY_OPTIONS options = frame()->GetDisplayOptions(); KIGFX::PCB_VIEW* view = frame()->GetCanvas()->GetView(); PCBNEW_SETTINGS* editorSettings = frame()->GetPcbNewSettings(); const BoardEditorAppearanceSettings& newSettings = aCtx.Request.settings(); options.m_ContrastModeDisplay = FromProtoEnum( newSettings.inactive_layer_display() ); options.m_NetColorMode = FromProtoEnum( newSettings.net_color_display() ); bool flip = newSettings.board_flip() == BoardFlipMode::BFM_FLIPPED_X; if( flip != view->IsMirroredX() ) { view->SetMirror( !view->IsMirroredX(), view->IsMirroredY() ); view->RecacheAllItems(); } editorSettings->m_Display.m_RatsnestMode = FromProtoEnum( newSettings.ratsnest_display() ); frame()->SetDisplayOptions( options ); frame()->GetCanvas()->GetView()->UpdateAllLayersColor(); frame()->GetCanvas()->Refresh(); return Empty(); }