kicad-source/pcbnew/dialogs/dialog_create_array.cpp
John Beard 2391e5ecdc Pcbnew array: allow selecting the center point interactively
This involves recasting the array tool from the slightly
eccentric ARRAY_CREATOR class into a full-blown TOOL, which
allows it to usefuly store state asd the dialog hides. This
is modelled on the POSITION_RELATIVE_TOOL strategy.

The by-radius-and-point mechanism is removed entirely,
as it's very fiddly and hard to describe in the UI,
and what you probably really want to do is get your
item onto the circle with tools like "move exact",
and then define the circle origin (now using the picker
tool)

Fixes: https://gitlab.com/kicad/code/kicad/-/issues/16783
2024-10-28 22:01:14 +08:00

621 lines
22 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 1992-2024 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "dialogs/dialog_create_array.h"
#include <wx/msgdlg.h>
#include <base_units.h>
#include <footprint.h>
#include <pcb_edit_frame.h>
#include <tools/pcb_actions.h>
#include <tools/pcb_picker_tool.h>
#include <tool/tool_manager.h>
#include <widgets/text_ctrl_eval.h>
/**
* Struct containing the last-entered values for the dialog.
*/
struct CREATE_ARRAY_DIALOG_ENTRIES
{
/**
* Construct with some sensible defaults.
* In future, this could be loaded from config?
*/
CREATE_ARRAY_DIALOG_ENTRIES() :
m_OptionsSet( true ),
m_GridNx( 5 ),
m_GridNy( 5 ),
m_GridDx( pcbIUScale.mmToIU( 2.54 ) ),
m_GridDy( pcbIUScale.mmToIU( 2.54 ) ),
m_GridOffsetX( 0 ),
m_GridOffsetY( 0 ),
m_GridStagger( 1 ),
m_GridStaggerRows( true ),
m_GridPositionCentreOnItems( true ),
m_GridNumberingAxis( 0 ), // h then v
m_GridNumReverseAlt( false ),
m_GridNumStartSet( 1 ), // use specified start
m_Grid2dArrayNumbering( 0 ), // linear numbering
m_GridPrimaryAxisScheme( 0 ), // numeric
m_GridSecondaryAxisScheme( 0 ), // numeric
m_GridPrimaryNumOffset( wxT( "1" ) ), // numeric
m_GridSecondaryNumOffset( wxT( "1" ) ), // numeric
m_GridPrimaryAxisStep( 1 ),
m_GridSecondaryAxisStep( 1 ),
m_CircCentreX( 0 ),
m_CircCentreY( 0 ),
m_CircAngle( ANGLE_90 ),
m_CircCount( 4 ),
m_CircNumStartSet( 1 ), // use specified start
m_GridCircNumScheme( 0 ),
m_CircNumberingOffset( wxT( "1" ) ),
m_CircNumberingStep( 1 ),
m_CircRotatationStep( false ),
m_ArrayTypeTab( 0 ), // start on grid view
m_FootprintKeepAnnotations( false ),
m_FootprintReannotate( true ) // Assign unique by default
{
}
bool m_OptionsSet;
long m_GridNx;
long m_GridNy;
long m_GridDx;
long m_GridDy;
long m_GridOffsetX;
long m_GridOffsetY;
long m_GridStagger;
bool m_GridStaggerRows;
bool m_GridPositionCentreOnItems;
long m_GridNumberingAxis;
bool m_GridNumReverseAlt;
long m_GridNumStartSet;
long m_Grid2dArrayNumbering;
long m_GridPrimaryAxisScheme;
long m_GridSecondaryAxisScheme;
wxString m_GridPrimaryNumOffset;
wxString m_GridSecondaryNumOffset;
long m_GridPrimaryAxisStep;
long m_GridSecondaryAxisStep;
long m_CircCentreX;
long m_CircCentreY;
EDA_ANGLE m_CircAngle;
long m_CircCount;
bool m_CircFullCircle;
long m_CircNumStartSet;
long m_GridCircNumScheme;
wxString m_CircNumberingOffset;
long m_CircNumberingStep;
bool m_CircRotatationStep;
long m_ArrayTypeTab;
bool m_FootprintKeepAnnotations;
bool m_FootprintReannotate;
};
// Persistent options settings
static CREATE_ARRAY_DIALOG_ENTRIES s_arrayOptions;
/**
* Local mapping for list-box <-> numbering type
*/
struct NUMBERING_LIST_DATA
{
ARRAY_AXIS::NUMBERING_TYPE m_numbering_type;
wxString m_label;
};
/**
* List of type <--> name mappings (in order) for the numbering type
* list boxes
*/
static const std::vector<NUMBERING_LIST_DATA> numberingTypeData {
{
ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
_( "Numerals (0,1,2,...,9,10)" ),
},
{
ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_HEX,
_( "Hexadecimal (0,1,...,F,10,...)" ),
},
{
ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_NO_IOSQXZ,
_( "Alphabet, minus IOSQXZ" ),
},
{
ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
_( "Alphabet, full 26 characters" ),
},
};
DIALOG_CREATE_ARRAY::DIALOG_CREATE_ARRAY( PCB_BASE_FRAME* aParent,
std::unique_ptr<ARRAY_OPTIONS>& aSettings,
bool aIsFootprintEditor, const VECTOR2I& aOrigPos ) :
DIALOG_CREATE_ARRAY_BASE( aParent ),
m_frame( aParent ),
m_settings( aSettings ),
m_originalItemPosition( aOrigPos ), m_isFootprintEditor( aIsFootprintEditor ),
m_hSpacing( aParent, m_labelDx, m_entryDx, m_unitLabelDx ),
m_vSpacing( aParent, m_labelDy, m_entryDy, m_unitLabelDy ),
m_hOffset( aParent, m_labelOffsetX, m_entryOffsetX, m_unitLabelOffsetX ),
m_vOffset( aParent, m_labelOffsetY, m_entryOffsetY, m_unitLabelOffsetY ),
m_hCentre( aParent, m_labelCentreX, m_entryCentreX, m_unitLabelCentreX ),
m_vCentre( aParent, m_labelCentreY, m_entryCentreY, m_unitLabelCentreY ),
m_circAngle( aParent, m_labelCircAngle, m_entryCircAngle, m_unitLabelCircAngle ),
m_cfg_persister( pcbIUScale, s_arrayOptions.m_OptionsSet )
{
// Configure display origin transforms
m_hSpacing.SetCoordType( ORIGIN_TRANSFORMS::REL_X_COORD );
m_vSpacing.SetCoordType( ORIGIN_TRANSFORMS::REL_Y_COORD );
m_hOffset.SetCoordType( ORIGIN_TRANSFORMS::REL_X_COORD );
m_vOffset.SetCoordType( ORIGIN_TRANSFORMS::REL_Y_COORD );
m_hCentre.SetCoordType( ORIGIN_TRANSFORMS::ABS_X_COORD );
m_vCentre.SetCoordType( ORIGIN_TRANSFORMS::ABS_Y_COORD );
// Set up numbering scheme drop downs character set strings
for( const auto& numData : numberingTypeData )
{
const wxString label = wxGetTranslation( numData.m_label );
void* clientData = (void*) &numData;
m_choicePriAxisNumbering->Append( label, clientData );
m_choiceSecAxisNumbering->Append( label, clientData );
m_choiceCircNumbering->Append( label, clientData );
}
m_choicePriAxisNumbering->SetSelection( 0 );
m_choiceSecAxisNumbering->SetSelection( 0 );
m_choiceCircNumbering->SetSelection( 0 );
m_circAngle.SetUnits( EDA_UNITS::DEGREES );
// bind grid options to persister
m_cfg_persister.Add( *m_entryNx, s_arrayOptions.m_GridNx );
m_cfg_persister.Add( *m_entryNy, s_arrayOptions.m_GridNy );
m_cfg_persister.Add( m_hSpacing, s_arrayOptions.m_GridDx );
m_cfg_persister.Add( m_vSpacing, s_arrayOptions.m_GridDy );
m_cfg_persister.Add( m_hOffset, s_arrayOptions.m_GridOffsetX );
m_cfg_persister.Add( m_vOffset, s_arrayOptions.m_GridOffsetY );
m_cfg_persister.Add( *m_entryStagger, s_arrayOptions.m_GridStagger );
m_cfg_persister.Add( *m_staggerRows, s_arrayOptions.m_GridStaggerRows );
m_cfg_persister.Add( *m_rbCentreOnSource, s_arrayOptions.m_GridPositionCentreOnItems );
m_cfg_persister.Add( *m_radioBoxGridNumberingAxis, s_arrayOptions.m_GridNumberingAxis );
m_cfg_persister.Add( *m_checkBoxGridReverseNumbering, s_arrayOptions.m_GridNumReverseAlt );
m_cfg_persister.Add( *m_rbGridStartNumberingOpt, s_arrayOptions.m_GridNumStartSet );
m_cfg_persister.Add( *m_radioBoxGridNumberingScheme, s_arrayOptions.m_Grid2dArrayNumbering );
m_cfg_persister.Add( *m_choicePriAxisNumbering, s_arrayOptions.m_GridPrimaryAxisScheme );
m_cfg_persister.Add( *m_choiceSecAxisNumbering, s_arrayOptions.m_GridSecondaryAxisScheme );
m_cfg_persister.Add( *m_entryGridPriNumberingOffset, s_arrayOptions.m_GridPrimaryNumOffset );
m_cfg_persister.Add( *m_entryGridSecNumberingOffset, s_arrayOptions.m_GridSecondaryNumOffset );
m_cfg_persister.Add( *m_entryGridPriNumberingStep, s_arrayOptions.m_GridPrimaryAxisStep );
m_cfg_persister.Add( *m_entryGridSecNumberingStep, s_arrayOptions.m_GridSecondaryAxisStep );
// bind circular options to persister
m_cfg_persister.Add( m_hCentre, s_arrayOptions.m_CircCentreX );
m_cfg_persister.Add( m_vCentre, s_arrayOptions.m_CircCentreY );
m_cfg_persister.Add( *m_checkBoxFullCircle, s_arrayOptions.m_CircFullCircle );
m_cfg_persister.Add( m_circAngle, s_arrayOptions.m_CircAngle );
m_cfg_persister.Add( *m_entryCircCount, s_arrayOptions.m_CircCount );
m_cfg_persister.Add( *m_entryRotateItemsCb, s_arrayOptions.m_CircRotatationStep );
m_cfg_persister.Add( *m_rbCircStartNumberingOpt, s_arrayOptions.m_CircNumStartSet );
m_cfg_persister.Add( *m_choiceCircNumbering, s_arrayOptions.m_GridCircNumScheme );
m_cfg_persister.Add( *m_entryCircNumberingStart, s_arrayOptions.m_CircNumberingOffset );
m_cfg_persister.Add( *m_entryCircNumberingStep, s_arrayOptions.m_CircNumberingStep );
m_cfg_persister.Add( *m_gridTypeNotebook, s_arrayOptions.m_ArrayTypeTab );
m_cfg_persister.Add( *m_radioBtnKeepRefs, s_arrayOptions.m_FootprintKeepAnnotations );
m_cfg_persister.Add( *m_radioBtnUniqueRefs, s_arrayOptions.m_FootprintReannotate );
m_cfg_persister.RestoreConfigToControls();
// Run the callbacks once to process the dialog contents
setControlEnablement();
calculateCircularArrayProperties();
SetupStandardButtons();
Fit();
SetMinSize( GetSize() );
}
DIALOG_CREATE_ARRAY::~DIALOG_CREATE_ARRAY()
{
}
void DIALOG_CREATE_ARRAY::OnParameterChanged( wxCommandEvent& event )
{
if( m_checkBoxFullCircle->GetValue() && m_entryCircAngle == event.GetEventObject() )
{
return;
}
setControlEnablement();
calculateCircularArrayProperties();
}
void DIALOG_CREATE_ARRAY::OnSelectCenterButton( wxCommandEvent& event )
{
event.Skip();
PCB_PICKER_TOOL* pickerTool = m_frame->GetToolManager()->GetTool<PCB_PICKER_TOOL>();
wxCHECK( pickerTool, /* void */ );
if( event.GetEventObject() == m_btnSelectCenterItem )
{
m_frame->GetToolManager()->RunAction(
PCB_ACTIONS::selectItemInteractively,
PCB_PICKER_TOOL::INTERACTIVE_PARAMS{ this, _( "Select center item..." ) } );
}
else if( event.GetEventObject() == m_btnSelectCenterPoint )
{
m_frame->GetToolManager()->RunAction(
PCB_ACTIONS::selectPointInteractively,
PCB_PICKER_TOOL::INTERACTIVE_PARAMS{ this, _( "Select center point..." ) } );
}
else
{
wxFAIL_MSG( "Unknown event source" );
}
// Hide, but do not close, the dialog
Hide();
}
// Implement the RECEIVER interface for the callback from the TOOL
void DIALOG_CREATE_ARRAY::UpdatePickedItem( const EDA_ITEM* aItem )
{
if( aItem )
{
m_hCentre.SetValue( aItem->GetPosition().x );
m_vCentre.SetValue( aItem->GetPosition().y );
}
Show( true );
}
void DIALOG_CREATE_ARRAY::UpdatePickedPoint( const std::optional<VECTOR2I>& aPoint )
{
if( aPoint )
{
m_hCentre.SetValue( aPoint->x );
m_vCentre.SetValue( aPoint->y );
}
Show( true );
}
/**
* Validate and save a long integer entry
*
* @param entry the text entry to read from
* @param dest the value destination
* @param description description of the field (used if the value is not OK)
* @param errors a list of errors to add any error to
* @return valid
*/
static bool validateLongEntry( const wxTextEntry& entry, long& dest, const wxString& description,
wxArrayString& errors )
{
bool ok = true;
if( !entry.GetValue().ToLong( &dest ) )
{
wxString err;
err.Printf( _( "Bad numeric value for %s: %s" ), description, entry.GetValue() );
errors.Add( err );
ok = false;
}
return ok;
}
/**
* Validates and saves (if valid) the type and offset of an array axis numbering
*
* @param offsetEntry the entry of the offset (text)
* @param typeEntry the entry of the axis nmbering scheme (choice)
* @param type the destination of the type if valid
* @param offset the destination of the offset if valid
* @param errors error string accumulator
* @return if all valid
*/
static bool validateAxisOptions( const wxTextCtrl& offsetEntry, const wxChoice& typeEntry,
const wxTextCtrl& aStepEntry, ARRAY_AXIS& aAxis,
wxArrayString& errors )
{
void* clientData = typeEntry.GetClientData( typeEntry.GetSelection() );
const NUMBERING_LIST_DATA* numberingData = static_cast<NUMBERING_LIST_DATA*>( clientData );
wxCHECK_MSG( numberingData, false, wxT( "Failed to get client data from list control." ) );
aAxis.SetAxisType( numberingData->m_numbering_type );
const wxString text = offsetEntry.GetValue();
bool ok = aAxis.SetOffset( text );
if( !ok )
{
errors.Add( wxString::Format( _( "Could not determine numbering start from '%s': "
"expected value consistent with alphabet '%s'." ),
text,
aAxis.GetAlphabet() ) );
return false;
}
long step;
ok = validateLongEntry( aStepEntry, step, _( "step value" ), errors );
if( ok )
aAxis.SetStep( step );
return ok;
}
bool DIALOG_CREATE_ARRAY::TransferDataFromWindow()
{
std::cout << "DIALOG_CREATE_ARRAY::TransferDataFromWindow()" << std::endl;
std::unique_ptr<ARRAY_OPTIONS> newSettings;
wxArrayString errors;
const wxWindow* page = m_gridTypeNotebook->GetCurrentPage();
if( page == m_gridPanel )
{
auto newGrid = std::make_unique<ARRAY_GRID_OPTIONS>();
bool ok = true;
// ints
ok &= validateLongEntry(*m_entryNx, newGrid->m_nx, _("horizontal count"), errors);
ok &= validateLongEntry(*m_entryNy, newGrid->m_ny, _("vertical count"), errors);
newGrid->m_delta.x = m_hSpacing.GetIntValue();
newGrid->m_delta.y = m_vSpacing.GetIntValue();
newGrid->m_offset.x = m_hOffset.GetIntValue();
newGrid->m_offset.y = m_vOffset.GetIntValue();
newGrid->m_centred = m_rbCentreOnSource->GetValue();
ok &= validateLongEntry(*m_entryStagger, newGrid->m_stagger, _("stagger"), errors);
newGrid->m_stagger_rows = m_staggerRows->GetValue();
newGrid->m_horizontalThenVertical = m_radioBoxGridNumberingAxis->GetSelection() == 0;
newGrid->m_reverseNumberingAlternate = m_checkBoxGridReverseNumbering->GetValue();
newGrid->SetShouldNumber( m_isFootprintEditor );
if( m_isFootprintEditor )
{
newGrid->SetNumberingStartIsSpecified( m_rbGridStartNumberingOpt->GetSelection() == 1 );
if( newGrid->GetNumberingStartIsSpecified() )
{
newGrid->m_2dArrayNumbering = m_radioBoxGridNumberingScheme->GetSelection() != 0;
// validate from the input fields
bool numOk = validateAxisOptions( *m_entryGridPriNumberingOffset,
*m_choicePriAxisNumbering,
*m_entryGridPriNumberingStep,
newGrid->m_pri_axis, errors );
if( newGrid->m_2dArrayNumbering )
{
numOk &= validateAxisOptions( *m_entryGridSecNumberingOffset,
*m_choiceSecAxisNumbering,
*m_entryGridSecNumberingStep,
newGrid->m_sec_axis, errors );
}
ok &= numOk;
}
else
{
// artificial linear numeric scheme from 1
newGrid->m_2dArrayNumbering = false;
newGrid->m_pri_axis.SetAxisType( ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC );
newGrid->m_pri_axis.SetOffset( 1 );
}
}
// Only use settings if all values are good
if( ok )
newSettings = std::move( newGrid );
}
else if( page == m_circularPanel )
{
auto newCirc = std::make_unique<ARRAY_CIRCULAR_OPTIONS>();
bool ok = true;
double angle = EDA_UNIT_UTILS::UI::DoubleValueFromString( m_entryCircAngle->GetValue() );
newCirc->m_centre.x = m_hCentre.GetIntValue();
newCirc->m_centre.y = m_vCentre.GetIntValue();
newCirc->m_angle = EDA_ANGLE( angle, DEGREES_T );
ok = validateLongEntry(*m_entryCircCount, newCirc->m_nPts, _("point count"), errors);
newCirc->m_rotateItems = m_entryRotateItemsCb->GetValue();
newCirc->SetShouldNumber( m_isFootprintEditor );
if( m_isFootprintEditor )
{
newCirc->SetNumberingStartIsSpecified( m_rbCircStartNumberingOpt->GetSelection() == 1 );
if( newCirc->GetNumberingStartIsSpecified() )
{
ok &= validateAxisOptions( *m_entryCircNumberingStart, *m_choiceCircNumbering,
*m_entryCircNumberingStep, newCirc->m_axis, errors );
}
else
{
// artificial linear numeric scheme from 1
newCirc->m_axis.SetAxisType( ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC );
newCirc->m_axis.SetOffset( 1 ); // Start at "1"
}
}
// Only use settings if all values are good
if( ok )
newSettings = std::move( newCirc );
}
bool ret = false;
// If we got good settings, send them out and finish
if( newSettings )
{
// assign pointer and ownership here
m_settings = std::move( newSettings );
m_settings->SetSShouldReannotateFootprints( m_radioBtnUniqueRefs->GetValue() );
// persist the control state for next time
m_cfg_persister.ReadConfigFromControls();
ret = true;
}
else
{
wxString errorStr;
if( errors.IsEmpty() )
errorStr = _("Bad parameters");
else
errorStr = wxJoin( errors, '\n' );
wxMessageBox( errorStr );
ret = false;
}
// This dialog is not modal, so close it now
Close();
return ret;
}
void DIALOG_CREATE_ARRAY::setControlEnablement()
{
if( m_checkBoxFullCircle->GetValue() )
{
m_entryCircAngle->Disable();
}
else
{
m_entryCircAngle->Enable();
}
if( m_isFootprintEditor )
{
m_footprintReannotatePanel->Show( false );
m_gridPadNumberingPanel->Show( true );
m_circularPadNumberingPanel->Show( true );
// If we set the start number, we can set the other options,
// otherwise it's a hardcoded linear array
const bool use_set_start_grid = m_rbGridStartNumberingOpt->GetSelection() == 1;
m_radioBoxGridNumberingScheme->Enable( use_set_start_grid );
m_labelPriAxisNumbering->Enable( use_set_start_grid );
m_choicePriAxisNumbering->Enable( use_set_start_grid );
// Disable the secondary axis numbering option if the
// numbering scheme doesn't have two axes
const bool num2d = m_radioBoxGridNumberingScheme->GetSelection() != 0;
m_labelSecAxisNumbering->Enable( use_set_start_grid && num2d );
m_choiceSecAxisNumbering->Enable( use_set_start_grid && num2d );
// We can only set an offset if we're setting the start number
m_labelGridNumberingOffset->Enable( use_set_start_grid );
m_entryGridPriNumberingOffset->Enable( use_set_start_grid );
m_entryGridSecNumberingOffset->Enable( use_set_start_grid && num2d );
m_entryGridSecNumberingStep->Enable( use_set_start_grid && num2d );
// disable the circular number offset in the same way
const bool use_set_start_circ = m_rbCircStartNumberingOpt->GetSelection() == 1;
m_entryCircNumberingStart->Enable( use_set_start_circ );
}
else
{
// grid
m_rbGridStartNumberingOpt->Enable( false );
m_radioBoxGridNumberingScheme->Enable( false );
m_labelPriAxisNumbering->Enable( false );
m_labelSecAxisNumbering->Enable( false );
m_choiceSecAxisNumbering->Enable( false );
m_choicePriAxisNumbering->Enable( false );
m_labelGridNumberingOffset->Enable( false );
m_entryGridPriNumberingOffset->Enable( false );
m_entryGridSecNumberingOffset->Enable( false );
m_gridPadNumberingPanel->Show( false );
// circular
m_rbCircStartNumberingOpt->Enable( false );
m_entryCircNumberingStart->Enable( false );
m_circularPadNumberingPanel->Show( false );
m_footprintReannotatePanel->Show( true );
}
}
void DIALOG_CREATE_ARRAY::calculateCircularArrayProperties()
{
// In full circle mode, the division angle is computed from the number of points
if( m_checkBoxFullCircle->GetValue() )
{
long nPts;
if( m_entryCircCount->GetValue().ToLong( &nPts ) )
{
EDA_ANGLE division = EDA_ANGLE( 360, DEGREES_T ) / nPts;
m_circAngle.SetAngleValue( division );
}
}
}