Add symbol editor clipboard handling
Mirrors handling for schematic editor. Outputs SVG+PNG+Kicad text to the clipboard, applications choose which to paste
This commit is contained in:
@@ -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, "<g id=\"{}\" inkscape:label=\"{}\" inkscape:groupmode=\"layer\">\n",
|
||||
TO_UTF8( aLayerName ), TO_UTF8( aLayerName ) );
|
||||
}
|
||||
|
||||
|
||||
void SVG_PLOTTER::EndLayer()
|
||||
{
|
||||
// Close any pending graphics context group first
|
||||
fmt::print( m_outputFile, "</g>\n" );
|
||||
// Then close the layer group
|
||||
fmt::print( m_outputFile, "</g>\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.
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <dialogs/dialog_lib_edit_pin_table.h>
|
||||
#include <dialogs/dialog_update_symbol_fields.h>
|
||||
#include <view/view_controls.h>
|
||||
#include <view/view.h>
|
||||
#include <richio.h>
|
||||
#include <sch_io/kicad_sexpr/sch_io_kicad_sexpr.h>
|
||||
#include <sch_textbox.h>
|
||||
@@ -52,6 +53,297 @@
|
||||
#include <math/util.h> // for KiROUND
|
||||
#include <io/kicad/kicad_io_utils.h>
|
||||
#include <trace_helpers.h>
|
||||
#include <plotters/plotters_pslike.h>
|
||||
#include <sch_painter.h>
|
||||
#include <sch_plotter.h>
|
||||
#include <locale_io.h>
|
||||
#include <gal/gal_print.h>
|
||||
#include <gal/graphics_abstraction_layer.h>
|
||||
#include <zoom_defines.h>
|
||||
#include <wx/ffile.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double clipboardPpi = 96.0;
|
||||
constexpr int clipboardMaxBitmapSize = 4096;
|
||||
constexpr double clipboardBboxInflation = 0.02;
|
||||
|
||||
|
||||
void appendMimeData( std::vector<CLIPBOARD_MIME_DATA>& 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_t>( 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<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
|
||||
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<KIGFX::GAL_PRINT> 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<KIGFX::SCH_PAINTER> painter = std::make_unique<KIGFX::SCH_PAINTER>( gal );
|
||||
std::unique_ptr<KIGFX::VIEW> view = std::make_unique<KIGFX::VIEW>();
|
||||
|
||||
// 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<std::unique_ptr<SCH_ITEM>> 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<SCH_ITEM*>( 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<unsigned char>( alpha );
|
||||
|
||||
if( alpha > 0 )
|
||||
{
|
||||
rgbResult[idx] = static_cast<unsigned char>( std::min( 255, rB * 255 / alpha ) );
|
||||
rgbResult[idx + 1] = static_cast<unsigned char>( std::min( 255, gB * 255 / alpha ) );
|
||||
rgbResult[idx + 2] = static_cast<unsigned char>( 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<CLIPBOARD_MIME_DATA> mimeData;
|
||||
|
||||
// Get the bounding box for just the selected items
|
||||
BOX2I bbox;
|
||||
|
||||
for( EDA_ITEM* item : selection )
|
||||
{
|
||||
SCH_ITEM* schItem = static_cast<SCH_ITEM*>( 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<SCH_ITEM*>( 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 <boost/test/unit_test.hpp>
|
||||
|
||||
#include <sch_symbol.h>
|
||||
#include <sch_line.h>
|
||||
#include <sch_junction.h>
|
||||
#include <sch_bus_entry.h>
|
||||
#include <sch_no_connect.h>
|
||||
#include <sch_text.h>
|
||||
#include <sch_label.h>
|
||||
#include <sch_shape.h>
|
||||
#include <sch_sheet.h>
|
||||
#include <lib_symbol.h>
|
||||
#include <plotters/plotters_pslike.h>
|
||||
#include <sch_painter.h>
|
||||
#include <sch_plotter.h>
|
||||
#include <locale_io.h>
|
||||
#include <settings/color_settings.h>
|
||||
#include <wx/ffile.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
|
||||
class SCHEMATIC_CLIPBOARD_FIXTURE
|
||||
{
|
||||
public:
|
||||
SCHEMATIC_CLIPBOARD_FIXTURE() = default;
|
||||
~SCHEMATIC_CLIPBOARD_FIXTURE() = default;
|
||||
|
||||
std::unique_ptr<SCH_LINE> CreateWire( int x1, int y1, int x2, int y2 )
|
||||
{
|
||||
auto wire = std::make_unique<SCH_LINE>(
|
||||
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<SCH_LINE> CreateBus( int x1, int y1, int x2, int y2 )
|
||||
{
|
||||
auto bus = std::make_unique<SCH_LINE>(
|
||||
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<SCH_JUNCTION> CreateJunction( int x, int y )
|
||||
{
|
||||
auto junction = std::make_unique<SCH_JUNCTION>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
|
||||
m_items.push_back( junction.get() );
|
||||
return junction;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_NO_CONNECT> CreateNoConnect( int x, int y )
|
||||
{
|
||||
auto noConnect = std::make_unique<SCH_NO_CONNECT>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
|
||||
m_items.push_back( noConnect.get() );
|
||||
return noConnect;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_BUS_WIRE_ENTRY> CreateBusEntry( int x, int y )
|
||||
{
|
||||
auto entry = std::make_unique<SCH_BUS_WIRE_ENTRY>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ) );
|
||||
m_items.push_back( entry.get() );
|
||||
return entry;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_TEXT> CreateText( int x, int y, const wxString& text )
|
||||
{
|
||||
auto schText = std::make_unique<SCH_TEXT>(
|
||||
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<SCH_LABEL> CreateLabel( int x, int y, const wxString& text )
|
||||
{
|
||||
auto label = std::make_unique<SCH_LABEL>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
|
||||
m_items.push_back( label.get() );
|
||||
return label;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_GLOBALLABEL> CreateGlobalLabel( int x, int y, const wxString& text )
|
||||
{
|
||||
auto label = std::make_unique<SCH_GLOBALLABEL>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
|
||||
m_items.push_back( label.get() );
|
||||
return label;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_HIERLABEL> CreateHierLabel( int x, int y, const wxString& text )
|
||||
{
|
||||
auto label = std::make_unique<SCH_HIERLABEL>(
|
||||
VECTOR2I( schIUScale.MilsToIU( x ), schIUScale.MilsToIU( y ) ), text );
|
||||
m_items.push_back( label.get() );
|
||||
return label;
|
||||
}
|
||||
|
||||
std::unique_ptr<SCH_SHAPE> CreateRectangle( int x1, int y1, int x2, int y2 )
|
||||
{
|
||||
auto rect = std::make_unique<SCH_SHAPE>( 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<SCH_SHAPE> CreateCircle( int cx, int cy, int radius )
|
||||
{
|
||||
auto circle = std::make_unique<SCH_SHAPE>( 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<SCH_SHAPE> CreatePolyline( const std::vector<std::pair<int, int>>& points )
|
||||
{
|
||||
auto poly = std::make_unique<SCH_SHAPE>( 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<SCH_ITEM*> 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()
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 <boost/test/unit_test.hpp>
|
||||
|
||||
#include <lib_symbol.h>
|
||||
#include <sch_pin.h>
|
||||
#include <sch_shape.h>
|
||||
#include <sch_text.h>
|
||||
#include <sch_textbox.h>
|
||||
#include <plotters/plotters_pslike.h>
|
||||
#include <sch_painter.h>
|
||||
#include <sch_plotter.h>
|
||||
#include <locale_io.h>
|
||||
#include <settings/color_settings.h>
|
||||
#include <wx/ffile.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
|
||||
class SYMBOL_CLIPBOARD_FIXTURE
|
||||
{
|
||||
public:
|
||||
SYMBOL_CLIPBOARD_FIXTURE()
|
||||
{
|
||||
m_symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestSymbol" ), nullptr );
|
||||
}
|
||||
|
||||
~SYMBOL_CLIPBOARD_FIXTURE() = default;
|
||||
|
||||
void AddPin( int x, int y, const wxString& name, const wxString& number )
|
||||
{
|
||||
std::unique_ptr<SCH_PIN> pin = std::make_unique<SCH_PIN>( 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<SCH_SHAPE> rect = std::make_unique<SCH_SHAPE>( 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<SCH_SHAPE> circle = std::make_unique<SCH_SHAPE>( 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<std::pair<int, int>>& points )
|
||||
{
|
||||
std::unique_ptr<SCH_SHAPE> poly = std::make_unique<SCH_SHAPE>( 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<SCH_SHAPE> arc = std::make_unique<SCH_SHAPE>( 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<SCH_TEXT> txt = std::make_unique<SCH_TEXT>( 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<SVG_PLOTTER> plotter = std::make_unique<SVG_PLOTTER>();
|
||||
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<LIB_SYMBOL> 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" ) ) );
|
||||
BOOST_CHECK( svg.Contains( wxT( "</svg>" ) ) );
|
||||
// SVG should contain paths or lines for pin elements
|
||||
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<line" ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that a symbol with a rectangle produces SVG output
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE( SvgExport_ContainsRectangle )
|
||||
{
|
||||
AddRectangle( -100, -100, 100, 100 );
|
||||
|
||||
wxString svg = PlotToSvgString();
|
||||
|
||||
BOOST_CHECK( !svg.IsEmpty() );
|
||||
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
|
||||
// Rectangle should produce a path or rect element
|
||||
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<rect" ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that a symbol with a circle produces SVG output
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE( SvgExport_ContainsCircle )
|
||||
{
|
||||
AddCircle( 0, 0, 50 );
|
||||
|
||||
wxString svg = PlotToSvgString();
|
||||
|
||||
BOOST_CHECK( !svg.IsEmpty() );
|
||||
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
|
||||
// Circle should produce a circle or ellipse element
|
||||
BOOST_CHECK( svg.Contains( wxT( "<circle" ) ) || svg.Contains( wxT( "<ellipse" ) )
|
||||
|| svg.Contains( wxT( "<path" ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that a symbol with a polyline produces SVG output
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE( SvgExport_ContainsPolyline )
|
||||
{
|
||||
AddPolyline( { { 0, 0 }, { 50, 50 }, { 100, 0 }, { 100, 100 } } );
|
||||
|
||||
wxString svg = PlotToSvgString();
|
||||
|
||||
BOOST_CHECK( !svg.IsEmpty() );
|
||||
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
|
||||
// Polyline should produce a path or polyline element
|
||||
BOOST_CHECK( svg.Contains( wxT( "<path" ) ) || svg.Contains( wxT( "<polyline" ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that a symbol with text produces SVG output
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE( SvgExport_ContainsText )
|
||||
{
|
||||
AddText( 0, 0, wxT( "Test Label" ) );
|
||||
|
||||
wxString svg = PlotToSvgString();
|
||||
|
||||
BOOST_CHECK( !svg.IsEmpty() );
|
||||
BOOST_CHECK( svg.Contains( wxT( "<svg" ) ) );
|
||||
// Text should produce a text element or paths for glyphs
|
||||
BOOST_CHECK( svg.Contains( wxT( "<text" ) ) || svg.Contains( wxT( "<path" ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test that bounding box is calculated correctly for pins
|
||||
*/
|
||||
BOOST_AUTO_TEST_CASE( BoundingBox_Pins )
|
||||
{
|
||||
AddPin( 0, 0, wxT( "PIN1" ), wxT( "1" ) );
|
||||
AddPin( 200, 0, wxT( "PIN2" ), wxT( "2" ) );
|
||||
|
||||
BOX2I bbox = GetSymbolBoundingBox();
|
||||
|
||||
// Bounding box should span from first pin to second pin (plus pin length and decoration)
|
||||
BOOST_CHECK( bbox.GetWidth() > 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( "<svg" ) ) );
|
||||
BOOST_CHECK( svg.Contains( wxT( "</svg>" ) ) );
|
||||
|
||||
// Should have multiple path elements for all the components
|
||||
int pathCount = 0;
|
||||
size_t pos = 0;
|
||||
|
||||
while( ( pos = svg.find( wxT( "<path" ), pos ) ) != wxString::npos )
|
||||
{
|
||||
pathCount++;
|
||||
pos++;
|
||||
}
|
||||
|
||||
// A complex symbol should have multiple paths
|
||||
BOOST_CHECK( pathCount >= 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()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user