diff --git a/common/common.cpp b/common/common.cpp index d578dbb599..ea760509a4 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -115,6 +115,7 @@ wxString ExpandTextVars( const wxString& aSource, const std::function s_e24 = { + 100, 110, 120, 130, 150, 160, 180, 200, 220, 240, 270, 300, + 330, 360, 390, 430, 470, 510, 560, 620, 680, 750, 820, 910 + }; + + // E192 series values in 100-999 decade (3 significant figures) + static constexpr std::array s_e192 = { + 100, 101, 102, 104, 105, 106, 107, 109, 110, 111, 113, 114, 115, 117, 118, 120, 121, 123, + 124, 126, 127, 129, 130, 132, 133, 135, 137, 138, 140, 142, 143, 145, 147, 149, 150, 152, + 154, 156, 158, 160, 162, 164, 165, 167, 169, 172, 174, 176, 178, 180, 182, 184, 187, 189, + 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, 215, 218, 221, 223, 226, 229, 232, 234, + 237, 240, 243, 246, 249, 252, 255, 258, 261, 264, 267, 271, 274, 277, 280, 284, 287, 291, + 294, 298, 301, 305, 309, 312, 316, 320, 324, 328, 332, 336, 340, 344, 348, 352, 357, 361, + 365, 370, 374, 379, 383, 388, 392, 397, 402, 407, 412, 417, 422, 427, 432, 437, 442, 448, + 453, 459, 464, 470, 475, 481, 487, 493, 499, 505, 511, 517, 523, 530, 536, 542, 549, 556, + 562, 569, 576, 583, 590, 597, 604, 612, 619, 626, 634, 642, 649, 657, 665, 673, 681, 690, + 698, 706, 715, 723, 732, 741, 750, 759, 768, 777, 787, 796, 806, 816, 825, 835, 845, 856, + 866, 876, 887, 898, 909, 920, 931, 942, 953, 965, 976, 988 + }; + + static auto parseSeriesString( const std::string& aSeries ) -> int + { + if( aSeries == "E3" || aSeries == "e3" ) + return 3; + else if( aSeries == "E6" || aSeries == "e6" ) + return 6; + else if( aSeries == "E12" || aSeries == "e12" ) + return 12; + else if( aSeries == "E24" || aSeries == "e24" ) + return 24; + else if( aSeries == "E48" || aSeries == "e48" ) + return 48; + else if( aSeries == "E96" || aSeries == "e96" ) + return 96; + else if( aSeries == "E192" || aSeries == "e192" ) + return 192; + else + return -1; // Invalid series + } + + static auto getSeriesValue( int aSeries, size_t aIndex ) -> uint16_t + { + // E1, E3, E6, E12, E24 are derived from E24 + if( aSeries <= 24 ) + { + const size_t skipValue = 24 / aSeries; + return s_e24[aIndex * skipValue]; + } + // E48, E96, E192 are derived from E192 + else + { + const size_t skipValue = 192 / aSeries; + return s_e192[aIndex * skipValue]; + } + } + + static auto getSeriesSize( int aSeries ) -> size_t + { + return static_cast( aSeries ); + } + +public: + static auto FindNearest( double aValue, const std::string& aSeries ) -> std::optional + { + const int series = parseSeriesString( aSeries ); + if( series < 0 ) + return std::nullopt; + + if( aValue <= 0.0 ) + return std::nullopt; + + // Scale value to 100-999 decade + const double logValue = std::log10( aValue ); + const int decade = static_cast( std::floor( logValue ) ); + const double scaledValue = aValue / std::pow( 10.0, decade ); + const double normalized = scaledValue * 100.0; + + // Find nearest value in series + const size_t seriesSize = getSeriesSize( series ); + double minDiff = std::numeric_limits::max(); + uint16_t nearest = 100; + + for( size_t i = 0; i < seriesSize; ++i ) + { + const uint16_t val = getSeriesValue( series, i ); + const double diff = std::abs( normalized - val ); + if( diff < minDiff ) + { + minDiff = diff; + nearest = val; + } + } + + // Scale back to original decade + return ( nearest / 100.0 ) * std::pow( 10.0, decade ); + } + + static auto FindUp( double aValue, const std::string& aSeries ) -> std::optional + { + const int series = parseSeriesString( aSeries ); + if( series < 0 ) + return std::nullopt; + + if( aValue <= 0.0 ) + return std::nullopt; + + // Scale value to 100-999 decade + const double logValue = std::log10( aValue ); + const int decade = static_cast( std::floor( logValue ) ); + const double scaledValue = aValue / std::pow( 10.0, decade ); + const double normalized = scaledValue * 100.0; + + // Find next higher value in series + const size_t seriesSize = getSeriesSize( series ); + + // Check current decade + for( size_t i = 0; i < seriesSize; ++i ) + { + const uint16_t val = getSeriesValue( series, i ); + if( val > normalized ) + return ( val / 100.0 ) * std::pow( 10.0, decade ); + } + + // Wrap to next decade + const uint16_t firstVal = getSeriesValue( series, 0 ); + return ( firstVal / 100.0 ) * std::pow( 10.0, decade + 1 ); + } + + static auto FindDown( double aValue, const std::string& aSeries ) -> std::optional + { + const int series = parseSeriesString( aSeries ); + if( series < 0 ) + return std::nullopt; + + if( aValue <= 0.0 ) + return std::nullopt; + + // Scale value to 100-999 decade + const double logValue = std::log10( aValue ); + const int decade = static_cast( std::floor( logValue ) ); + const double scaledValue = aValue / std::pow( 10.0, decade ); + const double normalized = scaledValue * 100.0; + + // Find next lower value in series + const size_t seriesSize = getSeriesSize( series ); + + // Check current decade (search backwards) + for( int i = seriesSize - 1; i >= 0; --i ) + { + const uint16_t val = getSeriesValue( series, i ); + if( val < normalized ) + return ( val / 100.0 ) * std::pow( 10.0, decade ); + } + + // Wrap to previous decade + const uint16_t lastVal = getSeriesValue( series, seriesSize - 1 ); + return ( lastVal / 100.0 ) * std::pow( 10.0, decade - 1 ); + } +}; + + EVAL_VISITOR::EVAL_VISITOR( VariableCallback aVariableCallback, ERROR_COLLECTOR& aErrorCollector ) : m_variableCallback( std::move( aVariableCallback ) ), m_errors( aErrorCollector ), @@ -571,6 +737,30 @@ auto EVAL_VISITOR::evaluateFunction( const FUNC_DATA& aFunc ) const -> Result( condition ? argValues[1] : argValues[2] ); } + // E-series functions (handle value as number, series as string) + else if( ( name == "enearest" || name == "eup" || name == "edown" ) && argc >= 1 && argc <= 2 ) + { + auto valueResult = VALUE_UTILS::ToDouble( argValues[0] ); + if( !valueResult ) + return MakeError( valueResult.GetError() ); + + const auto value = valueResult.GetValue(); + const auto series = argc > 1 ? VALUE_UTILS::ToString( argValues[1] ) : "E24"; + std::optional result; + + if( name == "enearest" ) + result = ESERIES_UTILS::FindNearest( value, series ); + else if( name == "eup" ) + result = ESERIES_UTILS::FindUp( value, series ); + else if( name == "edown" ) + result = ESERIES_UTILS::FindDown( value, series ); + + if( !result ) + return MakeError( fmt::format( "Invalid E-series: {}", series ) ); + + return MakeValue( result.value() ); + } + // Mathematical functions (return numbers) - convert args to doubles first std::vector numArgs; for( const auto& val : argValues ) @@ -616,6 +806,45 @@ auto EVAL_VISITOR::evaluateFunction( const FUNC_DATA& aFunc ) const -> Result( sum / static_cast( argc ) ); } + else if( name == "shunt" && argc == 2 ) + { + const auto r1 = numArgs[0]; + const auto r2 = numArgs[1]; + const auto sum = r1 + r2; + + // Calculate parallel resistance: (r1*r2)/(r1+r2) + // If sum is not positive, return 0.0 (handles edge cases like shunt(0,0)) + if( sum > 0.0 ) + return MakeValue( ( r1 * r2 ) / sum ); + else + return MakeValue( 0.0 ); + } + else if( name == "db" && argc == 1 ) + { + // Power ratio to dB: 10*log10(ratio) + if( numArgs[0] <= 0.0 ) + return MakeError( "db() argument must be positive" ); + + return MakeValue( 10.0 * std::log10( numArgs[0] ) ); + } + else if( name == "dbv" && argc == 1 ) + { + // Voltage/current ratio to dB: 20*log10(ratio) + if( numArgs[0] <= 0.0 ) + return MakeError( "dbv() argument must be positive" ); + + return MakeValue( 20.0 * std::log10( numArgs[0] ) ); + } + else if( name == "fromdb" && argc == 1 ) + { + // dB to power ratio: 10^(dB/10) + return MakeValue( std::pow( 10.0, numArgs[0] / 10.0 ) ); + } + else if( name == "fromdbv" && argc == 1 ) + { + // dB to voltage/current ratio: 10^(dB/20) + return MakeValue( std::pow( 10.0, numArgs[0] / 20.0 ) ); + } return MakeError( fmt::format( "Unknown function: {} with {} arguments", name, argc ) ); } diff --git a/common/tool/actions.cpp b/common/tool/actions.cpp index de0d02b7cc..9c8fad6f53 100644 --- a/common/tool/actions.cpp +++ b/common/tool/actions.cpp @@ -568,6 +568,13 @@ TOOL_ACTION ACTIONS::editTable( TOOL_ACTION_ARGS() .FriendlyName( _( "Edit Table..." ) ) .Icon( BITMAPS::table_edit ) ); +TOOL_ACTION ACTIONS::exportTableCSV( TOOL_ACTION_ARGS() + .Name( "common.TableEditor.exportTableCSV" ) + .Scope( AS_GLOBAL ) + .MenuText( _( "Export Table to CSV..." ) ) + .Tooltip( _( "Export table contents to CSV file with resolved text variables" ) ) + .Icon( BITMAPS::export_file ) ); + TOOL_ACTION ACTIONS::activatePointEditor( TOOL_ACTION_ARGS() .Name( "common.Control.activatePointEditor" ) .ToolbarState( TOOLBAR_STATE::HIDDEN ) diff --git a/eeschema/sch_label.cpp b/eeschema/sch_label.cpp index 517211dc70..dc00e284f7 100644 --- a/eeschema/sch_label.cpp +++ b/eeschema/sch_label.cpp @@ -813,7 +813,9 @@ bool SCH_LABEL_BASE::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* toke return true; } - if( token->Contains( ':' ) ) + // Don't process cross-references if the token contains escape markers + // (from escaped variables like \${R1:VALUE}) + if( token->Contains( ':' ) && !token->Contains( wxT( "\x01ESC_" ) ) ) { if( schematic->ResolveCrossReference( token, aDepth + 1 ) ) return true; diff --git a/eeschema/sch_sheet.cpp b/eeschema/sch_sheet.cpp index 9fc6c8910b..f089085597 100644 --- a/eeschema/sch_sheet.cpp +++ b/eeschema/sch_sheet.cpp @@ -237,7 +237,9 @@ bool SCH_SHEET::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, in if( !schematic ) return false; - if( token->Contains( ':' ) ) + // Don't process cross-references if the token contains escape markers + // (from escaped variables like \${R1:VALUE}) + if( token->Contains( ':' ) && !token->Contains( wxT( "\x01ESC_" ) ) ) { if( schematic->ResolveCrossReference( token, aDepth + 1 ) ) return true; diff --git a/eeschema/sch_symbol.cpp b/eeschema/sch_symbol.cpp index 077e0c6564..cf459943a7 100644 --- a/eeschema/sch_symbol.cpp +++ b/eeschema/sch_symbol.cpp @@ -1516,6 +1516,8 @@ void SCH_SYMBOL::GetContextualTextVars( wxArrayString* aVars ) const aVars->push_back( wxT( "NET_NAME()" ) ); aVars->push_back( wxT( "NET_CLASS()" ) ); aVars->push_back( wxT( "PIN_NAME()" ) ); + aVars->push_back( wxT( "REFERENCE()" ) ); + aVars->push_back( wxT( "SHORT_REFERENCE()" ) ); aVars->push_back( wxT( "UNIT()" ) ); } @@ -1606,7 +1608,9 @@ bool SCH_SYMBOL::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, i return true; } - if( token->Contains( ':' ) ) + // Don't process cross-references if the token contains escape markers + // (from escaped variables like \${R1:VALUE}) + if( token->Contains( ':' ) && !token->Contains( wxT( "\x01ESC_" ) ) ) { if( schematic->ResolveCrossReference( token, aDepth + 1 ) ) return true; @@ -1751,30 +1755,51 @@ bool SCH_SYMBOL::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, i else if( token->StartsWith( wxT( "SHORT_NET_NAME(" ) ) || token->StartsWith( wxT( "NET_NAME(" ) ) || token->StartsWith( wxT( "NET_CLASS(" ) ) || token->StartsWith( wxT( "PIN_NAME(" ) ) || token->StartsWith( wxT( "PIN_BASE_NAME(" ) ) || token->StartsWith( wxT( "PIN_ALT_LIST(" ) ) + || token->StartsWith( wxT( "REFERENCE(" ) ) || token->StartsWith( wxT( "SHORT_REFERENCE(" ) ) || token->StartsWith( wxT( "UNIT(" ) ) ) { wxString pinNumber = token->AfterFirst( '(' ); pinNumber = pinNumber.BeforeLast( ')' ); + bool isReferenceFunction = token->StartsWith( wxT( "REFERENCE(" ) ); + bool isShortReferenceFunction = token->StartsWith( wxT( "SHORT_REFERENCE(" ) ); bool isUnitFunction = token->StartsWith( wxT( "UNIT(" ) ); // First, try to find the pin in the current unit (for backward compatibility) - // For UNIT(), always search all pins to find which unit the pin belongs to - std::vector pinsToSearch = isUnitFunction ? GetAllLibPins() : GetPins( aPath ); + // For REFERENCE/SHORT_REFERENCE/UNIT functions, always search all pins to find which unit the pin belongs to + std::vector pinsToSearch = ( isReferenceFunction || isShortReferenceFunction || isUnitFunction ) + ? GetAllLibPins() : GetPins( aPath ); for( SCH_PIN* pin : pinsToSearch ) { if( pin->GetNumber() == pinNumber ) { - if( isUnitFunction ) + if( isReferenceFunction || isShortReferenceFunction || isUnitFunction ) { - // Return the full unit reference (e.g., "J601A") int pinUnit = pin->GetUnit(); wxString result; - if( pinUnit > 0 ) - result = GetRef( aPath, false ) + SubReference( pinUnit, false ); - else + + if( isReferenceFunction ) + { + // Return the full unit reference (e.g., "J601A") + if( pinUnit > 0 ) + result = GetRef( aPath, false ) + SubReference( pinUnit, false ); + else + result = GetRef( aPath, false ); + } + else if( isShortReferenceFunction ) + { + // Return the reference without unit (e.g., "J601") result = GetRef( aPath, false ); + } + else if( isUnitFunction ) + { + // Return only the unit letter (e.g., "A") + if( pinUnit > 0 ) + result = SubReference( pinUnit, false ); + else + result = wxEmptyString; + } *token = result; return true; @@ -1791,13 +1816,14 @@ bool SCH_SYMBOL::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, i } else if( token->StartsWith( wxT( "PIN_ALT_LIST" ) ) ) { - // Build complete list: base name first, then all alternates - wxString altList = pin->GetBaseName(); + // Build list of alternate names only (no base name) + wxString altList; const std::map& alts = pin->GetAlternates(); for( const auto& [altName, altDef] : alts ) { - altList += wxT( ", " ); + if( !altList.IsEmpty() ) + altList += wxT( ", " ); altList += altName; } @@ -1827,7 +1853,8 @@ bool SCH_SYMBOL::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, i } // If pin not found in current unit, search all units (auto-resolution) - if( !isUnitFunction ) + // Skip this for REFERENCE/SHORT_REFERENCE/UNIT functions as they already searched all units + if( !isReferenceFunction && !isShortReferenceFunction && !isUnitFunction ) { for( SCH_PIN* pin : GetAllLibPins() ) { @@ -1841,13 +1868,14 @@ bool SCH_SYMBOL::ResolveTextVar( const SCH_SHEET_PATH* aPath, wxString* token, i } else if( token->StartsWith( wxT( "PIN_ALT_LIST" ) ) ) { - // Build complete list: base name first, then all alternates - wxString altList = pin->GetBaseName(); + // Build list of alternate names only (no base name) + wxString altList; const std::map& alts = pin->GetAlternates(); for( const auto& [altName, altDef] : alts ) { - altList += wxT( ", " ); + if( !altList.IsEmpty() ) + altList += wxT( ", " ); altList += altName; } diff --git a/eeschema/sch_text_help.md b/eeschema/sch_text_help.md index 349706c8ab..eb0e94ba13 100644 --- a/eeschema/sch_text_help.md +++ b/eeschema/sch_text_help.md @@ -138,15 +138,35 @@ + +  
${refdes:REFERENCE(pin)} + +  
Full reference with unit for pin + + +  
${J1:REFERENCE(3)} + +  
J1B (for multi-unit symbol) + + +  
${refdes:SHORT_REFERENCE(pin)} + +  
Reference without unit letter for pin + + +  
${J1:SHORT_REFERENCE(3)} + +  
J1 +  
${refdes:UNIT(pin)} -  
Unit designation for pin +  
Unit letter only for pin  
${J1:UNIT(3)} -  
J1B (for multi-unit symbol) +  
B (unit letter for pin 3)  
${refdes:NET_NAME(pin)} @@ -181,12 +201,12 @@  
${refdes:PIN_ALT_LIST(pin)} -  
All available pin functions (base name + alternates) +  
All alternate pin functions (excludes base name)  
${U1:PIN_ALT_LIST(5)} -  
PA9, USART1_TX, TIM1_CH2, I2C1_SCL +  
USART1_TX, TIM1_CH2, I2C1_SCL  
${refdes:SHORT_NET_NAME(pin)} @@ -240,7 +260,7 @@ -  
${J1:UNIT(@{${ROW}+2})} +  
${J1:REFERENCE(@{${ROW}+2})}  
J1B (when ROW=0, pin 2 in unit B) diff --git a/eeschema/sch_text_help_md.h b/eeschema/sch_text_help_md.h index 0e342661ff..a0107aaab7 100644 --- a/eeschema/sch_text_help_md.h +++ b/eeschema/sch_text_help_md.h @@ -140,14 +140,34 @@ _HKI( "\n" " \n" " \n" " \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" " \n" " \n" -" \n" +" \n" " \n" " \n" " \n" " \n" -" \n" +" \n" " \n" " \n" " \n" @@ -182,12 +202,12 @@ _HKI( "
 
${refdes:REFERENCE(pin)}
 
Full reference with unit for pin
 
${J1:REFERENCE(3)}
 
J1B (for multi-unit symbol)
 
${refdes:SHORT_REFERENCE(pin)}
 
Reference without unit letter for pin
 
${J1:SHORT_REFERENCE(3)}
 
J1
 
${refdes:UNIT(pin)}
 
Unit designation for pin
 
Unit letter only for pin
 
${J1:UNIT(3)}
 
J1B (for multi-unit symbol)
 
B (unit letter for pin 3)
 
${refdes:NET_NAME(pin)}
\n" " \n" " \n" " \n" -" \n" +" \n" " \n" " \n" " \n" " \n" -" \n" +" \n" " \n" " \n" " \n" @@ -241,7 +261,7 @@ _HKI( "
 
${refdes:PIN_ALT_LIST(pin)}
 
All available pin functions (base name + alternates)
 
All alternate pin functions (excludes base name)
 
${U1:PIN_ALT_LIST(5)}
 
PA9, USART1_TX, TIM1_CH2, I2C1_SCL
 
USART1_TX, TIM1_CH2, I2C1_SCL
 
${refdes:SHORT_NET_NAME(pin)}
\n" " \n" " \n" " \n" -" \n" +" \n" " \n" " \n" " \n" diff --git a/eeschema/schematic.cpp b/eeschema/schematic.cpp index 0ae6d0f06f..1a3511ac24 100644 --- a/eeschema/schematic.cpp +++ b/eeschema/schematic.cpp @@ -700,6 +700,11 @@ std::set SCHEMATIC::GetNetClassAssignmentCandidates() bool SCHEMATIC::ResolveCrossReference( wxString* token, int aDepth ) const { + // Don't process cross-references if the token contains escape markers + // (from escaped variables like \${R1:VALUE}) + if( token->Contains( wxT( "\x01ESC_" ) ) ) + return false; + wxString remainder; wxString ref = token->BeforeFirst( ':', &remainder ); KIID_PATH path( ref ); diff --git a/eeschema/tools/sch_edit_table_tool.cpp b/eeschema/tools/sch_edit_table_tool.cpp index 834c0c9112..c7481e2006 100644 --- a/eeschema/tools/sch_edit_table_tool.cpp +++ b/eeschema/tools/sch_edit_table_tool.cpp @@ -24,6 +24,9 @@ #include #include #include +#include +#include +#include SCH_EDIT_TABLE_TOOL::SCH_EDIT_TABLE_TOOL() : @@ -81,6 +84,111 @@ int SCH_EDIT_TABLE_TOOL::EditTable( const TOOL_EVENT& aEvent ) } +int SCH_EDIT_TABLE_TOOL::ExportTableToCSV( const TOOL_EVENT& aEvent ) +{ + SCH_SELECTION& selection = m_selectionTool->RequestSelection( SCH_COLLECTOR::EditableItems ); + bool clearSelection = selection.IsHover(); + SCH_TABLE* parentTable = nullptr; + + // Find the table from the selection + for( EDA_ITEM* item : selection.Items() ) + { + if( item->Type() != SCH_TABLECELL_T ) + return 0; + + SCH_TABLE* table = static_cast( item->GetParent() ); + + if( !parentTable ) + { + parentTable = table; + } + else if( parentTable != table ) + { + parentTable = nullptr; + break; + } + } + + if( !parentTable ) + return 0; + + // Get current sheet path for variable resolution + SCH_SHEET_PATH& currentSheet = m_frame->GetCurrentSheet(); + + // Show file save dialog + wxFileDialog saveDialog( m_frame, _( "Export Table to CSV" ), + wxEmptyString, wxEmptyString, + _( "CSV files (*.csv)|*.csv" ), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); + + if( saveDialog.ShowModal() == wxID_CANCEL ) + { + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + return 0; + } + + wxString filePath = saveDialog.GetPath(); + + // Open file for writing + std::ofstream outFile( filePath.ToStdString() ); + + if( !outFile.is_open() ) + { + wxMessageBox( wxString::Format( _( "Failed to open file:\n%s" ), filePath ), + _( "Export Error" ), wxOK | wxICON_ERROR, m_frame ); + + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + return 0; + } + + // Helper function to escape CSV fields + auto escapeCSV = []( const wxString& field ) -> wxString + { + wxString escaped = field; + + // If field contains comma, quote, or newline, wrap in quotes and escape quotes + if( escaped.Contains( ',' ) || escaped.Contains( '\"' ) || escaped.Contains( '\n' ) ) + { + escaped.Replace( "\"", "\"\"" ); // Escape quotes by doubling them + escaped = "\"" + escaped + "\""; + } + + return escaped; + }; + + // Export table data + for( int row = 0; row < parentTable->GetRowCount(); ++row ) + { + for( int col = 0; col < parentTable->GetColCount(); ++col ) + { + SCH_TABLECELL* cell = parentTable->GetCell( row, col ); + + // Get resolved text (with variables expanded) + wxString cellText = cell->GetShownText( nullptr, ¤tSheet, false, 0 ); + + // Write escaped cell text + outFile << escapeCSV( cellText ).ToStdString(); + + // Add comma separator unless it's the last column + if( col < parentTable->GetColCount() - 1 ) + outFile << ','; + } + + // End of row + outFile << '\n'; + } + + outFile.close(); + + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + + return 0; +} + + SCH_TABLECELL* SCH_EDIT_TABLE_TOOL::copyCell( SCH_TABLECELL* aSource ) { // Use copy constructor to copy all formatting properties (font, colors, borders, etc.) @@ -118,4 +226,5 @@ void SCH_EDIT_TABLE_TOOL::setTransitions() Go( &SCH_EDIT_TABLE_TOOL::UnmergeCells, ACTIONS::unmergeCells.MakeEvent() ); Go( &SCH_EDIT_TABLE_TOOL::EditTable, ACTIONS::editTable.MakeEvent() ); + Go( &SCH_EDIT_TABLE_TOOL::ExportTableToCSV, ACTIONS::exportTableCSV.MakeEvent() ); } diff --git a/eeschema/tools/sch_edit_table_tool.h b/eeschema/tools/sch_edit_table_tool.h index 2f2425f9e4..a989226554 100644 --- a/eeschema/tools/sch_edit_table_tool.h +++ b/eeschema/tools/sch_edit_table_tool.h @@ -55,6 +55,7 @@ public: int UnmergeCells( const TOOL_EVENT& aEvent ) { return doUnmergeCells( aEvent ); } int EditTable( const TOOL_EVENT& aEvent ); + int ExportTableToCSV( const TOOL_EVENT& aEvent ); private: ///< Set up handlers for various events. diff --git a/include/tool/actions.h b/include/tool/actions.h index 2fd930bebb..6bbce13bd0 100644 --- a/include/tool/actions.h +++ b/include/tool/actions.h @@ -110,6 +110,7 @@ public: static TOOL_ACTION mergeCells; static TOOL_ACTION unmergeCells; static TOOL_ACTION editTable; + static TOOL_ACTION exportTableCSV; // Find and Replace static TOOL_ACTION showSearch; diff --git a/include/tool/edit_table_tool_base.h b/include/tool/edit_table_tool_base.h index cbc965e555..b5f26184a5 100644 --- a/include/tool/edit_table_tool_base.h +++ b/include/tool/edit_table_tool_base.h @@ -108,6 +108,9 @@ protected: selToolMenu.AddSeparator( 100 ); selToolMenu.AddItem( ACTIONS::editTable, cellSelection && SELECTION_CONDITIONS::Idle, 100 ); + selToolMenu.AddSeparator( 100 ); + selToolMenu.AddItem( ACTIONS::exportTableCSV, cellSelection && SELECTION_CONDITIONS::Idle, 100 ); + selToolMenu.AddSeparator( 100 ); } diff --git a/pcbnew/tools/pcb_edit_table_tool.cpp b/pcbnew/tools/pcb_edit_table_tool.cpp index 5e3de5e25e..8e2e1971ed 100644 --- a/pcbnew/tools/pcb_edit_table_tool.cpp +++ b/pcbnew/tools/pcb_edit_table_tool.cpp @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include PCB_EDIT_TABLE_TOOL::PCB_EDIT_TABLE_TOOL() : @@ -122,6 +125,108 @@ int PCB_EDIT_TABLE_TOOL::EditTable( const TOOL_EVENT& aEvent ) } +int PCB_EDIT_TABLE_TOOL::ExportTableToCSV( const TOOL_EVENT& aEvent ) +{ + const SELECTION& selection = getTableCellSelection(); + bool clearSelection = selection.IsHover(); + PCB_TABLE* parentTable = nullptr; + + // Find the table from the selection + for( EDA_ITEM* item : selection.Items() ) + { + if( item->Type() != PCB_TABLECELL_T ) + return 0; + + PCB_TABLE* table = static_cast( item->GetParent() ); + + if( !parentTable ) + { + parentTable = table; + } + else if( parentTable != table ) + { + parentTable = nullptr; + break; + } + } + + if( !parentTable ) + return 0; + + // Show file save dialog + wxFileDialog saveDialog( frame(), _( "Export Table to CSV" ), + wxEmptyString, wxEmptyString, + _( "CSV files (*.csv)|*.csv" ), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); + + if( saveDialog.ShowModal() == wxID_CANCEL ) + { + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + return 0; + } + + wxString filePath = saveDialog.GetPath(); + + // Open file for writing + std::ofstream outFile( filePath.ToStdString() ); + + if( !outFile.is_open() ) + { + wxMessageBox( wxString::Format( _( "Failed to open file:\n%s" ), filePath ), + _( "Export Error" ), wxOK | wxICON_ERROR, frame() ); + + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + return 0; + } + + // Helper function to escape CSV fields + auto escapeCSV = []( const wxString& field ) -> wxString + { + wxString escaped = field; + + // If field contains comma, quote, or newline, wrap in quotes and escape quotes + if( escaped.Contains( ',' ) || escaped.Contains( '\"' ) || escaped.Contains( '\n' ) ) + { + escaped.Replace( "\"", "\"\"" ); // Escape quotes by doubling them + escaped = "\"" + escaped + "\""; + } + + return escaped; + }; + + // Export table data + for( int row = 0; row < parentTable->GetRowCount(); ++row ) + { + for( int col = 0; col < parentTable->GetColCount(); ++col ) + { + PCB_TABLECELL* cell = parentTable->GetCell( row, col ); + + // Get resolved text (with variables expanded) + wxString cellText = cell->GetShownText( false, 0 ); + + // Write escaped cell text + outFile << escapeCSV( cellText ).ToStdString(); + + // Add comma separator unless it's the last column + if( col < parentTable->GetColCount() - 1 ) + outFile << ','; + } + + // End of row + outFile << '\n'; + } + + outFile.close(); + + if( clearSelection ) + m_toolMgr->RunAction( ACTIONS::selectionClear ); + + return 0; +} + + void PCB_EDIT_TABLE_TOOL::setTransitions() { Go( &PCB_EDIT_TABLE_TOOL::AddRowAbove, ACTIONS::addRowAbove.MakeEvent() ); @@ -137,4 +242,5 @@ void PCB_EDIT_TABLE_TOOL::setTransitions() Go( &PCB_EDIT_TABLE_TOOL::UnmergeCells, ACTIONS::unmergeCells.MakeEvent() ); Go( &PCB_EDIT_TABLE_TOOL::EditTable, ACTIONS::editTable.MakeEvent() ); + Go( &PCB_EDIT_TABLE_TOOL::ExportTableToCSV, ACTIONS::exportTableCSV.MakeEvent() ); } diff --git a/pcbnew/tools/pcb_edit_table_tool.h b/pcbnew/tools/pcb_edit_table_tool.h index c6fa3174d7..beef76a2de 100644 --- a/pcbnew/tools/pcb_edit_table_tool.h +++ b/pcbnew/tools/pcb_edit_table_tool.h @@ -52,6 +52,7 @@ public: int UnmergeCells( const TOOL_EVENT& aEvent ) { return doUnmergeCells( aEvent ); } int EditTable( const TOOL_EVENT& aEvent ); + int ExportTableToCSV( const TOOL_EVENT& aEvent ); private: ///< Set up handlers for various events. diff --git a/qa/tests/common/text_eval/README.md b/qa/tests/common/text_eval/README.md index 00fa910720..6c5a44c558 100644 --- a/qa/tests/common/text_eval/README.md +++ b/qa/tests/common/text_eval/README.md @@ -64,6 +64,15 @@ Integration tests simulating real-world KiCad usage scenarios: - `max(...)` - Maximum of multiple values - `sum(...)` - Sum of multiple values - `avg(...)` - Average of multiple values +- `shunt(r1, r2)` - Parallel resistance calculation: (r1×r2)/(r1+r2) +- `db(ratio)` - Power ratio to decibels: 10×log₁₀(ratio) +- `dbv(ratio)` - Voltage/current ratio to decibels: 20×log₁₀(ratio) +- `fromdb(db)` - Decibels to power ratio: 10^(dB/10) +- `fromdbv(db)` - Decibels to voltage/current ratio: 10^(dB/20) +- `enearest(value, [series])` - Nearest E-series standard value (default: E24) +- `eup(value, [series])` - Next higher E-series standard value (default: E24) +- `edown(value, [series])` - Next lower E-series standard value (default: E24) + - Series options: "E3", "E6", "E12", "E24", "E48", "E96", "E192" ### String Functions - `upper(str)` - Convert to uppercase
 
${J1:UNIT(@{${ROW}+2})}
 
${J1:REFERENCE(@{${ROW}+2})}
 
J1B (when ROW=0, pin 2 in unit B)