/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2017 Jean-Pierre Charras, jp.charras at wanadoo.fr * Copyright (C) 1992-2023 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 #include #include #include #include #include #include #include #include // for KiROUND #include #include #include #include DIALOG_PAD_PRIMITIVES_PROPERTIES::DIALOG_PAD_PRIMITIVES_PROPERTIES( wxWindow* aParent, PCB_BASE_FRAME* aFrame, PCB_SHAPE* aShape ) : DIALOG_PAD_PRIMITIVES_PROPERTIES_BASE( aParent ), m_shape( aShape ), m_startX( aFrame, m_startXLabel, m_startXCtrl, m_startXUnits ), m_startY( aFrame, m_startYLabel, m_startYCtrl, m_startYUnits ), m_ctrl1X( aFrame, m_ctrl1XLabel, m_ctrl1XCtrl, m_ctrl1XUnits ), m_ctrl1Y( aFrame, m_ctrl1YLabel, m_ctrl1YCtrl, m_ctrl1YUnits ), m_ctrl2X( aFrame, m_ctrl2XLabel, m_ctrl2XCtrl, m_ctrl2XUnits ), m_ctrl2Y( aFrame, m_ctrl2YLabel, m_ctrl2YCtrl, m_ctrl2YUnits ), m_endX( aFrame, m_endXLabel, m_endXCtrl, m_endXUnits ), m_endY( aFrame, m_endYLabel, m_endYCtrl, m_endYUnits ), m_radius( aFrame, m_radiusLabel, m_radiusCtrl, m_radiusUnits ), m_thickness( aFrame, m_thicknessLabel, m_thicknessCtrl, m_thicknessUnits ) { SetInitialFocus( m_startXCtrl ); TransferDataToWindow(); SetupStandardButtons(); finishDialogSettings(); } bool DIALOG_PAD_PRIMITIVES_PROPERTIES::TransferDataToWindow() { if( m_shape == nullptr ) return false; m_thickness.SetValue( m_shape->GetWidth() ); m_filledCtrl->SetValue( m_shape->IsFilled() ); switch( m_shape->GetShape() ) { case SHAPE_T::SEGMENT: SetTitle( _( "Segment" ) ); m_startX.SetValue( m_shape->GetStart().x ); m_startY.SetValue( m_shape->GetStart().y ); m_endX.SetValue( m_shape->GetEnd().x ); m_endY.SetValue( m_shape->GetEnd().y ); m_ctrl1X.Show( false, true ); m_ctrl1Y.Show( false, true ); m_ctrl2X.Show( false, true ); m_ctrl2Y.Show( false, true ); m_staticTextPosCtrl1->Show( false ); m_staticTextPosCtrl1->SetSize( 0, 0 ); m_staticTextPosCtrl2->Show( false ); m_staticTextPosCtrl2->SetSize( 0, 0 ); m_radius.Show( false ); m_filledCtrl->Show( false ); break; case SHAPE_T::BEZIER: SetTitle( _( "Bezier" ) ); m_startX.SetValue( m_shape->GetStart().x ); m_startY.SetValue( m_shape->GetStart().y ); m_endX.SetValue( m_shape->GetEnd().x ); m_endY.SetValue( m_shape->GetEnd().y ); m_ctrl1X.SetValue( m_shape->GetBezierC1().x ); m_ctrl1Y.SetValue( m_shape->GetBezierC1().y ); m_ctrl2X.SetValue( m_shape->GetBezierC2().x ); m_ctrl2Y.SetValue( m_shape->GetBezierC2().y ); m_radius.Show( false ); m_filledCtrl->Show( false ); break; case SHAPE_T::ARC: SetTitle( _( "Arc" ) ); m_startX.SetValue( m_shape->GetStart().x ); m_startY.SetValue( m_shape->GetStart().y ); m_staticTextPosEnd->SetLabel( _( "Center Point:" ) ); m_endX.SetValue( m_shape->GetCenter().x ); m_endY.SetValue( m_shape->GetCenter().y ); m_radiusLabel->SetLabel( _( "Angle:" ) ); m_radius.SetUnits( EDA_UNITS::DEGREES ); m_radius.SetAngleValue( m_shape->GetArcAngle() ); m_ctrl1X.Show( false, true ); m_ctrl1Y.Show( false, true ); m_ctrl2X.Show( false, true ); m_ctrl2Y.Show( false, true ); m_staticTextPosCtrl1->Show( false ); m_staticTextPosCtrl1->SetSize( 0, 0 ); m_staticTextPosCtrl2->Show( false ); m_staticTextPosCtrl2->SetSize( 0, 0 ); m_filledCtrl->Show( false ); break; case SHAPE_T::CIRCLE: if( m_shape->GetWidth() ) SetTitle( _( "Ring" ) ); else SetTitle( _( "Circle" ) ); // End point does not exist for a circle or ring: m_staticTextPosEnd->Show( false ); m_endX.Show( false ); m_endY.Show( false ); // Circle center uses position controls: m_staticTextPosStart->SetLabel( _( "Center Point:" ) ); m_startX.SetValue( m_shape->GetStart().x ); m_startY.SetValue( m_shape->GetStart().y ); m_radius.SetValue( m_shape->GetRadius() ); m_ctrl1X.Show( false, true ); m_ctrl1Y.Show( false, true ); m_ctrl2X.Show( false, true ); m_ctrl2Y.Show( false, true ); m_staticTextPosCtrl1->Show( false ); m_staticTextPosCtrl1->SetSize( 0, 0 ); m_staticTextPosCtrl2->Show( false ); m_staticTextPosCtrl2->SetSize( 0, 0 ); m_filledCtrl->Show( true ); break; case SHAPE_T::RECT: if( m_shape->IsAnnotationProxy() ) SetTitle( _( "Number Box" ) ); else SetTitle( _( "Rectangle" ) ); m_startX.SetValue( m_shape->GetStart().x ); m_startY.SetValue( m_shape->GetStart().y ); m_endX.SetValue( m_shape->GetEnd().x ); m_endY.SetValue( m_shape->GetEnd().y ); m_ctrl1X.Show( false, true ); m_ctrl1Y.Show( false, true ); m_ctrl2X.Show( false, true ); m_ctrl2Y.Show( false, true ); m_staticTextPosCtrl1->Show( false ); m_staticTextPosCtrl1->SetSize( 0, 0 ); m_staticTextPosCtrl2->Show( false ); m_staticTextPosCtrl2->SetSize( 0, 0 ); m_radius.Show( false ); m_filledCtrl->Show( false ); break; case SHAPE_T::POLY: // polygon has a specific dialog editor. So nothing here break; default: SetTitle( "Unknown Basic Shape" ); break; } return true; } bool DIALOG_PAD_PRIMITIVES_PROPERTIES::TransferDataFromWindow() { if( m_thickness.GetValue() == 0 && !m_filledCtrl->GetValue() ) { DisplayError( this, _( "Line width may not be 0 for unfilled shapes." ) ); m_thicknessCtrl->SetFocus(); return false; } // Transfer data out of the GUI. STROKE_PARAMS stroke = m_shape->GetStroke(); stroke.SetWidth( m_thickness.GetIntValue() ); m_shape->SetStroke( stroke ); m_shape->SetFilled( m_filledCtrl->GetValue() ); switch( m_shape->GetShape() ) { case SHAPE_T::SEGMENT: case SHAPE_T::RECT: m_shape->SetStart( VECTOR2I( m_startX.GetIntValue(), m_startY.GetIntValue() ) ); m_shape->SetEnd( VECTOR2I( m_endX.GetIntValue(), m_endY.GetIntValue() ) ); break; case SHAPE_T::BEZIER: m_shape->SetStart( VECTOR2I( m_startX.GetIntValue(), m_startY.GetIntValue() ) ); m_shape->SetEnd( VECTOR2I( m_endX.GetIntValue(), m_endY.GetIntValue() ) ); m_shape->SetBezierC1( VECTOR2I( m_ctrl1X.GetIntValue(), m_ctrl1Y.GetIntValue())); m_shape->SetBezierC2( VECTOR2I( m_ctrl2X.GetIntValue(), m_ctrl2Y.GetIntValue())); break; case SHAPE_T::ARC: m_shape->SetCenter( VECTOR2I( m_endX.GetIntValue(), m_endY.GetIntValue() ) ); m_shape->SetStart( VECTOR2I( m_startX.GetIntValue(), m_startY.GetIntValue() ) ); m_shape->SetArcAngleAndEnd( m_radius.GetAngleValue() ); break; case SHAPE_T::CIRCLE: m_shape->SetStart( VECTOR2I( m_startX.GetIntValue(), m_startY.GetIntValue() ) ); m_shape->SetEnd( m_shape->GetStart() + VECTOR2I( m_radius.GetIntValue(), 0 ) ); break; case SHAPE_T::POLY: // polygon has a specific dialog editor. So nothing here break; default: break; } return true; } DIALOG_PAD_PRIMITIVE_POLY_PROPS::DIALOG_PAD_PRIMITIVE_POLY_PROPS( wxWindow* aParent, PCB_BASE_FRAME* aFrame, PCB_SHAPE* aShape ) : DIALOG_PAD_PRIMITIVE_POLY_PROPS_BASE( aParent ), m_shape( aShape ), m_thickness( aFrame, m_thicknessLabel, m_thicknessCtrl, m_thicknessUnits ) { if( !m_shape->GetPolyShape().IsEmpty() ) { for( const VECTOR2I& pt : m_shape->GetPolyShape().Outline( 0 ).CPoints() ) m_currPoints.emplace_back( pt ); } m_addButton->SetBitmap( KiBitmap( BITMAPS::small_plus ) ); m_deleteButton->SetBitmap( KiBitmap( BITMAPS::small_trash ) ); m_warningIcon->SetBitmap( KiBitmap( BITMAPS::dialog_warning ) ); m_gridCornersList->SetUnitsProvider( aFrame ); m_gridCornersList->SetAutoEvalCols( { 0, 1 } ); m_gridCornersList->SetDefaultRowSize( m_gridCornersList->GetDefaultRowSize() + 4 ); // Test for acceptable polygon (more than 2 corners, and not self-intersecting) and // remove any redundant corners. A warning message is displayed if not OK. doValidate( true ); TransferDataToWindow(); SetupStandardButtons(); m_gridCornersList->Connect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging ), nullptr, this ); // Now all widgets have the size fixed, call FinishDialogSettings finishDialogSettings(); } DIALOG_PAD_PRIMITIVE_POLY_PROPS::~DIALOG_PAD_PRIMITIVE_POLY_PROPS() { m_gridCornersList->Disconnect( wxEVT_GRID_CELL_CHANGING, wxGridEventHandler( DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging ), nullptr, this ); } bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::TransferDataToWindow() { if( m_shape == nullptr ) return false; m_thickness.SetValue( m_shape->GetWidth() ); m_filledCtrl->SetValue( m_shape->IsFilled() ); // Populates the list of corners int extra_rows = m_currPoints.size() - m_gridCornersList->GetNumberRows(); if( extra_rows > 0 ) { m_gridCornersList->AppendRows( extra_rows ); } else if( extra_rows < 0 ) { extra_rows = -extra_rows; m_gridCornersList->DeleteRows( 0, extra_rows ); } // enter others corner coordinates for( unsigned row = 0; row < m_currPoints.size(); ++row ) { // Row label is "Corner x" m_gridCornersList->SetRowLabelValue( row, wxString::Format( _( "Corner %d" ), row+1 ) ); m_gridCornersList->SetUnitValue( row, 0, m_currPoints[row].x ); m_gridCornersList->SetUnitValue( row, 1, m_currPoints[row].y ); } return true; } bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::TransferDataFromWindow() { if( !Validate() ) return false; m_shape->SetPolyPoints( m_currPoints ); STROKE_PARAMS stroke = m_shape->GetStroke(); stroke.SetWidth( m_thickness.GetIntValue() ); m_shape->SetStroke( stroke ); m_shape->SetFilled( m_filledCtrl->GetValue() ); return true; } bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::Validate() { // Don't remove redundant corners while user is editing corner list return doValidate( false ); } bool DIALOG_PAD_PRIMITIVE_POLY_PROPS::doValidate( bool aRemoveRedundantCorners ) { if( !m_gridCornersList->CommitPendingChanges() ) return false; if( m_currPoints.size() < 3 ) { m_warningText->SetLabel( _("Polygon must have at least 3 corners" ) ); m_warningText->Show( true ); m_warningIcon->Show( true ); return false; } bool valid = true; SHAPE_LINE_CHAIN polyline( m_currPoints, true ); // Remove redundant corners: polyline.Simplify(); if( polyline.PointCount() < 3 ) { m_warningText->SetLabel( _( "Polygon must have at least 3 corners after simplification" ) ); valid = false; } if( valid && polyline.SelfIntersecting() ) { m_warningText->SetLabel( _( "Polygon can not be self-intersecting" ) ); valid = false; } m_warningIcon->Show( !valid ); m_warningText->Show( !valid ); if( aRemoveRedundantCorners ) { if( polyline.PointCount() != (int) m_currPoints.size() ) { // Happens after simplification m_currPoints.clear(); for( const VECTOR2I& pt : polyline.CPoints() ) m_currPoints.emplace_back( pt ); m_warningIcon->Show( true ); m_warningText->Show( true ); m_warningText->SetLabel( _( "Note: redundant corners removed" ) ); } } return valid; } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::OnButtonAdd( wxCommandEvent& event ) { if( !m_gridCornersList->CommitPendingChanges() ) return; // Insert a new corner after the currently selected: wxArrayInt selections = m_gridCornersList->GetSelectedRows(); int row = -1; if( m_gridCornersList->GetNumberRows() == 0 ) row = 0; else if( selections.size() > 0 ) row = selections[ selections.size() - 1 ] + 1; else row = m_gridCornersList->GetGridCursorRow() + 1; if( row < 0 ) { wxMessageBox( _( "Select a corner to add the new corner after." ) ); return; } if( m_currPoints.size() == 0 || row >= (int) m_currPoints.size() ) m_currPoints.emplace_back( 0, 0 ); else m_currPoints.insert( m_currPoints.begin() + row, VECTOR2I( 0, 0 ) ); Validate(); TransferDataToWindow(); m_gridCornersList->ForceRefresh(); // Select the new row m_gridCornersList->SelectRow( row, false ); m_panelPoly->Refresh(); } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::OnButtonDelete( wxCommandEvent& event ) { if( !m_gridCornersList->CommitPendingChanges() ) return; wxArrayInt selections = m_gridCornersList->GetSelectedRows(); if( m_gridCornersList->GetNumberRows() == 0 ) return; if( selections.size() == 0 && m_gridCornersList->GetGridCursorRow() >= 0 ) selections.push_back( m_gridCornersList->GetGridCursorRow() ); if( selections.size() == 0 ) { wxMessageBox( _( "Select a corner to delete." ) ); return; } // remove corners: std::sort( selections.begin(), selections.end() ); for( int ii = selections.size()-1; ii >= 0 ; --ii ) m_currPoints.erase( m_currPoints.begin() + selections[ii] ); Validate(); TransferDataToWindow(); m_gridCornersList->ForceRefresh(); // select the row previous to the last deleted row m_gridCornersList->SelectRow( std::max( 0, selections[ 0 ] - 1 ) ); m_panelPoly->Refresh(); } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onPaintPolyPanel( wxPaintEvent& event ) { wxPaintDC dc( m_panelPoly ); wxSize dc_size = dc.GetSize(); dc.SetDeviceOrigin( dc_size.x / 2, dc_size.y / 2 ); // Calculate a suitable scale to fit the available draw area int minsize( pcbIUScale.mmToIU( 0.5 ) ); for( unsigned ii = 0; ii < m_currPoints.size(); ++ii ) { minsize = std::max( minsize, std::abs( m_currPoints[ii].x ) ); minsize = std::max( minsize, std::abs( m_currPoints[ii].y ) ); } // The draw origin is the center of the window. // Therefore the window size is twice the minsize just calculated minsize *= 2; minsize += m_thickness.GetIntValue(); // Give a margin double scale = std::min( double( dc_size.x ) / minsize, double( dc_size.y ) / minsize ) * 0.9; GRResetPenAndBrush( &dc ); // Draw X and Y axis. This is particularly useful to show the // reference position of basic shape // Axis are drawn before the polygon to avoid masking segments on axis GRLine( &dc, -dc_size.x, 0, dc_size.x, 0, 0, LIGHTBLUE ); // X axis GRLine( &dc, 0, -dc_size.y, 0, dc_size.y, 0, LIGHTBLUE ); // Y axis // Draw polygon. // The selected edge(s) are shown in selectcolor, the others in normalcolor. EDA_COLOR_T normalcolor = WHITE; EDA_COLOR_T selectcolor = RED; for( unsigned ii = 0; ii < m_currPoints.size(); ++ii ) { EDA_COLOR_T color = normalcolor; if( m_gridCornersList->IsInSelection (ii, 0) || m_gridCornersList->IsInSelection (ii, 1) || m_gridCornersList->GetGridCursorRow() == (int)ii ) color = selectcolor; unsigned jj = ii + 1; if( jj >= m_currPoints.size() ) jj = 0; GRLine( &dc, m_currPoints[ii].x * scale, m_currPoints[ii].y * scale, m_currPoints[jj].x * scale, m_currPoints[jj].y * scale, m_thickness.GetValue() * scale, color ); } event.Skip(); } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onPolyPanelResize( wxSizeEvent& event ) { m_panelPoly->Refresh(); event.Skip(); } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onGridSelect( wxGridRangeSelectEvent& event ) { m_panelPoly->Refresh(); } void DIALOG_PAD_PRIMITIVE_POLY_PROPS::onCellChanging( wxGridEvent& event ) { int row = event.GetRow(); int col = event.GetCol(); CallAfter( [this, row, col]() { if( col == 0 ) // Set the X value m_currPoints[row].x = m_gridCornersList->GetUnitValue( row, col ); else // Set the Y value m_currPoints[row].y = m_gridCornersList->GetUnitValue( row, col ); Validate(); m_panelPoly->Refresh(); } ); } DIALOG_PAD_PRIMITIVES_TRANSFORM::DIALOG_PAD_PRIMITIVES_TRANSFORM( wxWindow* aParent, PCB_BASE_FRAME* aFrame, std::vector>& aList, bool aShowDuplicate ) : DIALOG_PAD_PRIMITIVES_TRANSFORM_BASE( aParent ), m_list( aList ), m_vectorX( aFrame, m_xLabel, m_xCtrl, m_xUnits ), m_vectorY( aFrame, m_yLabel, m_yCtrl, m_yUnits ), m_rotation( aFrame, m_rotationLabel, m_rotationCtrl, m_rotationUnits ) { m_rotation.SetUnits( EDA_UNITS::DEGREES ); if( !aShowDuplicate ) // means no duplicate transform { m_staticTextDupCnt->Show( false ); m_spinCtrlDuplicateCount->Show( false ); } SetupStandardButtons(); GetSizer()->SetSizeHints( this ); } void DIALOG_PAD_PRIMITIVES_TRANSFORM::Transform( std::vector>* aList, int aDuplicateCount ) { VECTOR2I move_vect( m_vectorX.GetValue(), m_vectorY.GetValue() ); EDA_ANGLE rotation = m_rotation.GetAngleValue(); double scale = EDA_UNIT_UTILS::UI::DoubleValueFromString( m_scaleCtrl->GetValue() ); // Avoid too small / too large scale, which could create issues: if( scale < 0.01 ) scale = 0.01; if( scale > 100.0 ) scale = 100.0; // Transform shapes // shapes are scaled, then moved then rotated. // if aList != NULL, the initial shape will be duplicated, and transform // applied to the duplicated shape VECTOR2I currMoveVect = move_vect; EDA_ANGLE curr_rotation = rotation; do { for( unsigned idx = 0; idx < m_list.size(); ++idx ) { std::shared_ptr shape; if( aList == nullptr ) { shape = m_list[idx]; } else { aList->emplace_back( std::make_shared( *m_list[idx] ) ); shape = aList->back(); } // Transform parameters common to all shape types (some can be unused) STROKE_PARAMS stroke = shape->GetStroke(); stroke.SetWidth( KiROUND( shape->GetWidth() * scale ) ); shape->SetStroke( stroke ); shape->Move( currMoveVect ); shape->Scale( scale ); shape->Rotate( VECTOR2I( 0, 0 ), curr_rotation ); } // Prepare new transform on duplication: // Each new item is rotated (or moved) by the transform from the last duplication curr_rotation += rotation; currMoveVect += move_vect; } while( aList && --aDuplicateCount > 0 ); }