From c73d555fe23601b49658bf834dd497f60e2ff093 Mon Sep 17 00:00:00 2001 From: Andrzej Wolski Date: Mon, 14 Jul 2025 02:24:34 +0200 Subject: [PATCH] ADDED: Lasso selection in pcbnew Adds a lasso or freeform selection tool to KiCad in addition to standard rectangular selection. Adds supporting HitTest routines Fixes: https://gitlab.com/kicad/code/kicad/-/issues/1977 --- common/eda_shape.cpp | 9 + common/marker_base.cpp | 11 + common/preview_items/selection_area.cpp | 74 ++- common/tool/actions.cpp | 46 ++ eeschema/tools/sch_selection_tool.cpp | 2 + include/eda_item.h | 13 + include/eda_shape.h | 1 + include/marker_base.h | 5 + include/preview_items/selection_area.h | 14 +- include/tool/actions.h | 10 + include/tool/selection_tool.h | 10 + include/tool/tool_event.h | 5 + libs/kimath/include/geometry/geometry_utils.h | 42 +- .../include/geometry/shape_line_chain.h | 1 + libs/kimath/src/geometry/geometry_utils.cpp | 103 +++- libs/kimath/src/geometry/shape_line_chain.cpp | 12 + pagelayout_editor/tools/pl_selection_tool.cpp | 2 + pcbnew/footprint.cpp | 44 ++ pcbnew/footprint.h | 2 + pcbnew/footprint_edit_frame.cpp | 7 + pcbnew/generators/pcb_tuning_pattern.cpp | 6 + pcbnew/menubar_footprint_editor.cpp | 16 +- pcbnew/menubar_pcb_editor.cpp | 17 +- pcbnew/pad.cpp | 19 + pcbnew/pad.h | 2 +- pcbnew/pcb_dimension.cpp | 28 + pcbnew/pcb_dimension.h | 6 +- pcbnew/pcb_edit_frame.cpp | 7 + pcbnew/pcb_group.cpp | 7 + pcbnew/pcb_group.h | 3 + pcbnew/pcb_marker.h | 8 + pcbnew/pcb_reference_image.cpp | 6 + pcbnew/pcb_reference_image.h | 1 + pcbnew/pcb_shape.h | 5 + pcbnew/pcb_table.cpp | 7 + pcbnew/pcb_table.h | 2 + pcbnew/pcb_text.cpp | 12 + pcbnew/pcb_text.h | 6 + pcbnew/pcb_textbox.cpp | 8 + pcbnew/pcb_textbox.h | 2 + pcbnew/pcb_track.cpp | 6 + pcbnew/pcb_track.h | 1 + pcbnew/toolbars_footprint_editor.cpp | 7 +- pcbnew/toolbars_pcb_editor.cpp | 5 + pcbnew/tools/pcb_actions.cpp | 2 +- pcbnew/tools/pcb_selection_tool.cpp | 531 +++++++++++++----- pcbnew/tools/pcb_selection_tool.h | 65 ++- pcbnew/zone.cpp | 46 ++ pcbnew/zone.h | 7 +- qa/qa_utils/mocks.cpp | 5 +- 50 files changed, 1069 insertions(+), 187 deletions(-) diff --git a/common/eda_shape.cpp b/common/eda_shape.cpp index 3e73c7e54b..48bd54d31b 100644 --- a/common/eda_shape.cpp +++ b/common/eda_shape.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include // for KiROUND #include @@ -1587,6 +1588,14 @@ bool EDA_SHAPE::hitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) co } +bool EDA_SHAPE::hitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + SHAPE_COMPOUND shape( MakeEffectiveShapes() ); + + return KIGEOM::ShapeHitTest( aPoly, shape, aContained ); +} + + std::vector EDA_SHAPE::GetRectCorners() const { std::vector pts; diff --git a/common/marker_base.cpp b/common/marker_base.cpp index 50c33c104d..1d967ff3f8 100644 --- a/common/marker_base.cpp +++ b/common/marker_base.cpp @@ -34,6 +34,7 @@ #include "marker_base.h" #include #include +#include #include "dialogs/dialog_display_html_text_base.h" @@ -114,6 +115,16 @@ bool MARKER_BASE::HitTestMarker( const BOX2I& aRect, bool aContained, int aAccur } +bool MARKER_BASE::HitTestMarker( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + SHAPE_LINE_CHAIN shape; + ShapeToPolygon( shape ); + shape.Move( m_Pos ); + + return KIGEOM::ShapeHitTest( aPoly, shape, aContained ); +} + + void MARKER_BASE::ShapeToPolygon( SHAPE_LINE_CHAIN& aPolygon, int aScale ) const { if( aScale < 0 ) diff --git a/common/preview_items/selection_area.cpp b/common/preview_items/selection_area.cpp index 095432c525..3fe4fcd9c2 100644 --- a/common/preview_items/selection_area.cpp +++ b/common/preview_items/selection_area.cpp @@ -66,7 +66,8 @@ static const SELECTION_COLORS selectionColorScheme[2] = { SELECTION_AREA::SELECTION_AREA() : m_additive( false ), m_subtractive( false ), - m_exclusiveOr( false ) + m_exclusiveOr( false ), + m_mode( SELECTION_MODE::INSIDE_RECTANGLE ) { } @@ -76,9 +77,23 @@ const BOX2I SELECTION_AREA::ViewBBox() const { BOX2I tmp; - tmp.SetOrigin( m_origin ); - tmp.SetEnd( m_end ); + switch( m_mode ) + { + default: + case SELECTION_MODE::INSIDE_RECTANGLE: + case SELECTION_MODE::TOUCHING_RECTANGLE: + tmp.SetOrigin( m_origin ); + tmp.SetEnd( m_end ); + break; + case SELECTION_MODE::INSIDE_LASSO: + case SELECTION_MODE::TOUCHING_LASSO: + case SELECTION_MODE::TOUCHING_PATH: + tmp = m_shape_poly.BBox(); + break; + } + tmp.Normalize(); + return tmp; } @@ -91,10 +106,9 @@ void SELECTION_AREA::ViewDraw( int aLayer, KIGFX::VIEW* aView ) const const SELECTION_COLORS& scheme = settings->IsBackgroundDark() ? selectionColorScheme[0] : selectionColorScheme[1]; - // Set the fill of the selection rectangle - // based on the selection mode + // Set the colors of the selection shape based on the selection mode if( m_additive ) - gal.SetFillColor( scheme.additive ); + gal.SetFillColor( scheme.additive ); else if( m_subtractive ) gal.SetFillColor( scheme.subtract ); else if( m_exclusiveOr ) @@ -102,24 +116,44 @@ void SELECTION_AREA::ViewDraw( int aLayer, KIGFX::VIEW* aView ) const else gal.SetFillColor( scheme.normal ); - gal.SetIsStroke( true ); - gal.SetIsFill( true ); + if( m_mode == SELECTION_MODE::INSIDE_RECTANGLE || m_mode == SELECTION_MODE::INSIDE_LASSO ) + gal.SetStrokeColor( scheme.outline_l2r ); + else + gal.SetStrokeColor( scheme.outline_r2l ); + auto drawSelectionShape = + [&]() + { + switch( m_mode ) + { + default: + case SELECTION_MODE::INSIDE_RECTANGLE: + case SELECTION_MODE::TOUCHING_RECTANGLE: + gal.DrawRectangle( m_origin, m_end ); + break; + case SELECTION_MODE::INSIDE_LASSO: + case SELECTION_MODE::TOUCHING_LASSO: + if( m_shape_poly.PointCount() > 1 ) + gal.DrawPolygon( m_shape_poly ); + break; + case SELECTION_MODE::TOUCHING_PATH: + if( m_shape_poly.PointCount() > 0 ) + gal.DrawPolyline( m_shape_poly ); + break; + } + }; + + gal.SetIsStroke( true ); + gal.SetIsFill( false ); // force 1-pixel-wide line gal.SetLineWidth( 0.0 ); - - // Set the stroke color to indicate window or crossing selection - bool windowSelection = ( m_origin.x <= m_end.x ) ? true : false; - - if( aView->IsMirroredX() ) - windowSelection = !windowSelection; - - gal.SetStrokeColor( windowSelection ? scheme.outline_l2r : scheme.outline_r2l ); - gal.SetIsFill( false ); - gal.DrawRectangle( m_origin, m_end ); - gal.SetIsFill( true ); + drawSelectionShape(); // draw the fill as the second object so that Z test will not clamp // the single-pixel-wide rectangle sides - gal.DrawRectangle( m_origin, m_end ); + if( m_mode != SELECTION_MODE::TOUCHING_PATH ) + { + gal.SetIsFill( true ); + drawSelectionShape(); + } } diff --git a/common/tool/actions.cpp b/common/tool/actions.cpp index e06652d0af..723c27d625 100644 --- a/common/tool/actions.cpp +++ b/common/tool/actions.cpp @@ -30,6 +30,7 @@ #include #include #include +#include // Actions, being statically-defined, require specialized I18N handling. We continue to // use the _() macro so that string harvesting by the I18N framework doesn't have to be @@ -346,6 +347,51 @@ TOOL_ACTION ACTIONS::paste( TOOL_ACTION_ARGS() .Flags( AF_NONE ) .UIId( wxID_PASTE ) ); +TOOL_ACTION ACTIONS::selectInsideRectangle( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectInsideRectangle" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Inside Rectangle" ) ) + .Tooltip( _( "Select all items fully contained within the rectangular area" ) ) + .Parameter( SELECTION_MODE::INSIDE_RECTANGLE ) ); + +TOOL_ACTION ACTIONS::selectTouchingRectangle( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectTouchingRectangle" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Touching Rectangle" ) ) + .Tooltip( _( "Select all items that touch or intersect the rectangular area" ) ) + .Parameter( SELECTION_MODE::TOUCHING_RECTANGLE ) ); + +TOOL_ACTION ACTIONS::selectInsideLasso( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectInsideLasso" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Inside Lasso" ) ) + .Tooltip( _( "Select all items fully contained within the lasso area" ) ) + .Icon( BITMAPS::add_graphical_polygon ) // TODO: add proper icon + .Parameter( SELECTION_MODE::INSIDE_LASSO ) ); + +TOOL_ACTION ACTIONS::selectTouchingLasso( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectTouchingLasso" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Touching Lasso" ) ) + .Tooltip( _( "Select all items that touch or intersect the lasso area" ) ) + .Icon( BITMAPS::add_dashed_line ) // TODO: add proper icon + .Parameter( SELECTION_MODE::TOUCHING_LASSO ) ); + +TOOL_ACTION ACTIONS::selectAutoLasso( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectAutoLasso" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Lasso" ) ) + .Tooltip( _( "Select all items fully contained within or touching the lasso area, depending on the drawing direction" ) ) + .Icon( BITMAPS::opt_show_polygon ) ); // TODO: add proper icon + +TOOL_ACTION ACTIONS::selectTouchingPath( TOOL_ACTION_ARGS() + .Name( "common.Interactive.selectTouchingPath" ) + .Scope( AS_GLOBAL ) + .FriendlyName( _( "Touching Path" ) ) + .Tooltip( _( "Select all items that touch or intersect the drawn path" ) ) + .Icon( BITMAPS::add_line ) // TODO: add proper icon + .Parameter( SELECTION_MODE::TOUCHING_PATH ) ); + TOOL_ACTION ACTIONS::selectAll( TOOL_ACTION_ARGS() .Name( "common.Interactive.selectAll" ) .Scope( AS_GLOBAL ) diff --git a/eeschema/tools/sch_selection_tool.cpp b/eeschema/tools/sch_selection_tool.cpp index 3f37268901..18c2613dea 100644 --- a/eeschema/tools/sch_selection_tool.cpp +++ b/eeschema/tools/sch_selection_tool.cpp @@ -2148,6 +2148,8 @@ bool SCH_SELECTION_TOOL::selectMultiple() area.SetAdditive( m_drag_additive ); area.SetSubtractive( m_drag_subtractive ); area.SetExclusiveOr( false ); + area.SetMode( isGreedy ? SELECTION_MODE::TOUCHING_RECTANGLE + : SELECTION_MODE::INSIDE_RECTANGLE ); view->SetVisible( &area, true ); view->Update( &area ); diff --git a/include/eda_item.h b/include/eda_item.h index 0c1902c9ea..86e0dc4a9d 100644 --- a/include/eda_item.h +++ b/include/eda_item.h @@ -29,6 +29,7 @@ #include +#include #include #include #include @@ -247,6 +248,18 @@ public: return false; // derived classes should override this function } + /** + * Test if \a aPoly intersects this item. + * + * @param aPoly A reference to a #SHAPE_LINE_CHAIN object containing the polygon or polyline to test. + * @param aContained Set to true to test for containment instead of an intersection. + * @return True if \a aPoly contains or intersects the item. + */ + virtual bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const + { + return false; // derived classes should override this function + } + /** * Return the orthogonal bounding box of this object for display purposes. * diff --git a/include/eda_shape.h b/include/eda_shape.h index 458a9d66af..6f245109d5 100644 --- a/include/eda_shape.h +++ b/include/eda_shape.h @@ -451,6 +451,7 @@ protected: bool hitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const; bool hitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const; + bool hitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const; const std::vector buildBezierToSegmentsPointsList( int aMaxError ) const; diff --git a/include/marker_base.h b/include/marker_base.h index b74c418a8f..51fee6290d 100644 --- a/include/marker_base.h +++ b/include/marker_base.h @@ -119,6 +119,11 @@ public: */ bool HitTestMarker( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const; + /** + * Test if the given #SHAPE_LINE_CHAIN intersects or contains the bounds of this object. + */ + bool HitTestMarker( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const; + /** * Return the orthogonal, bounding box of this object for display purposes. * diff --git a/include/preview_items/selection_area.h b/include/preview_items/selection_area.h index 850e82a134..863d509289 100644 --- a/include/preview_items/selection_area.h +++ b/include/preview_items/selection_area.h @@ -28,7 +28,8 @@ #define PREVIEW_ITEMS_SELECTION_AREA_H #include - +#include +#include namespace KIGFX { @@ -82,6 +83,12 @@ public: void SetSubtractive( bool aSubtractive ) { m_subtractive = aSubtractive; } void SetExclusiveOr( bool aExclusiveOr ) { m_exclusiveOr = aExclusiveOr; } + void SetMode( SELECTION_MODE aMode ) { m_mode = aMode; } + SELECTION_MODE GetMode() const { return m_mode; } + + void SetPoly( SHAPE_LINE_CHAIN& aPoly ) { m_shape_poly = aPoly; } + SHAPE_LINE_CHAIN& GetPoly() { return m_shape_poly; } + void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override final; private: @@ -90,7 +97,10 @@ private: bool m_subtractive; bool m_exclusiveOr; - VECTOR2I m_origin, m_end; + SELECTION_MODE m_mode; + + VECTOR2I m_origin, m_end; // Used for box selection + SHAPE_LINE_CHAIN m_shape_poly; // Used for lasso selection }; } // PREVIEW diff --git a/include/tool/actions.h b/include/tool/actions.h index 39700aa533..a5c70510da 100644 --- a/include/tool/actions.h +++ b/include/tool/actions.h @@ -213,6 +213,16 @@ public: /// Select a single item under the cursor position static TOOL_ACTION selectionCursor; + /// Run a box selection tool in a fixed mode + static TOOL_ACTION selectInsideRectangle; + static TOOL_ACTION selectTouchingRectangle; + + /// Run a lasso selection tool + static TOOL_ACTION selectInsideLasso; + static TOOL_ACTION selectTouchingLasso; + static TOOL_ACTION selectAutoLasso; + static TOOL_ACTION selectTouchingPath; + /// Clear the current selection static TOOL_ACTION selectionClear; diff --git a/include/tool/selection_tool.h b/include/tool/selection_tool.h index 0ee65958c8..1575b8a3fd 100644 --- a/include/tool/selection_tool.h +++ b/include/tool/selection_tool.h @@ -34,6 +34,16 @@ class COLLECTOR; class KIID; +enum class SELECTION_MODE +{ + INSIDE_RECTANGLE, + TOUCHING_RECTANGLE, + INSIDE_LASSO, + TOUCHING_LASSO, + TOUCHING_PATH +}; + + class SELECTION_TOOL : public TOOL_INTERACTIVE, public wxEvtHandler { public: diff --git a/include/tool/tool_event.h b/include/tool/tool_event.h index 40afefc914..cb41733c20 100644 --- a/include/tool/tool_event.h +++ b/include/tool/tool_event.h @@ -522,6 +522,11 @@ public: m_param = aParam; } + bool HasParameter() const + { + return m_param.has_value(); + } + std::optional GetCommandId() const { return m_commandId; diff --git a/libs/kimath/include/geometry/geometry_utils.h b/libs/kimath/include/geometry/geometry_utils.h index 6334957871..882a8d987b 100644 --- a/libs/kimath/include/geometry/geometry_utils.h +++ b/libs/kimath/include/geometry/geometry_utils.h @@ -34,6 +34,10 @@ #include // for abs #include #include +#include +#include +#include +#include /** * @return the number of segments to approximate a arc by segments @@ -226,4 +230,40 @@ bool BoxHitTest( const VECTOR2I& aHitPoint, const BOX2I& aHittee, int aAccuracy * @param aAccuracy - The accuracy of the hit test. */ bool BoxHitTest( const BOX2I& aHitter, const BOX2I& aHittee, bool aHitteeContained, int aAccuracy ); -}; // namespace KIGEOM \ No newline at end of file + +/** + * Perform a shape-to-box hit test. + * + * @param aHitter - The selection shape that is either hitting or containing the hittee. + * @param aHittee - The box that is either being hit or contained by the hitter + * (this is possibly an object's bounding box). + * @param aHitteeContained - True if the hittee is tested for total containment, + * false if it is tested for intersection. + */ +bool BoxHitTest( const SHAPE_LINE_CHAIN& aHitter, const BOX2I& aHittee, bool aHitteeContained ); + +/** + * Perform a shape-to-box hit test with rotated box. + * + * @param aHitter - The selection shape that is either hitting or containing the hittee. + * @param aHittee - The box that is either being hit or contained by the hitter + * (this is possibly an object's bounding box). + * @param aHitteeRotation - The rotation of the hittee box. + * @param aHitteeRotationCenter - The center of the hittee box rotation. + * @param aHitteeContained - True if the hittee is tested for total containment, + * false if it is tested for intersection. + */ +bool BoxHitTest( const SHAPE_LINE_CHAIN& aHitter, const BOX2I& aHittee, const EDA_ANGLE& aHitteeRotation, + const VECTOR2I& aHitteeRotationCenter, bool aHitteeContained ); + +/** + * Perform a shape-to-shape hit test. + * + * @param aHitter - The selection shape that is either hitting or containing the hittee. + * @param aHittee - The shape that is either being hit or contained by the hitter + * (this is possibly an object's bounding box). + * @param aHitteeContained - True if the hittee is tested for total containment, + * false if it is tested for intersection. + */ +bool ShapeHitTest( const SHAPE_LINE_CHAIN& aHitter, const SHAPE& aHittee, bool aHitteeContained ); +}; // namespace KIGEOM diff --git a/libs/kimath/include/geometry/shape_line_chain.h b/libs/kimath/include/geometry/shape_line_chain.h index af13ad0704..2851c83f43 100644 --- a/libs/kimath/include/geometry/shape_line_chain.h +++ b/libs/kimath/include/geometry/shape_line_chain.h @@ -660,6 +660,7 @@ public: VECTOR2I m_origin; }; + bool Intersects( const SEG& aSeg) const; bool Intersects( const SHAPE_LINE_CHAIN& aChain ) const; /** diff --git a/libs/kimath/src/geometry/geometry_utils.cpp b/libs/kimath/src/geometry/geometry_utils.cpp index 04106697e7..b8a4ecee23 100644 --- a/libs/kimath/src/geometry/geometry_utils.cpp +++ b/libs/kimath/src/geometry/geometry_utils.cpp @@ -215,4 +215,105 @@ bool KIGEOM::BoxHitTest( const BOX2I& aHitter, const BOX2I& aHittee, bool aHitte return hitter.Contains( aHittee ); return hitter.Intersects( aHittee ); -} \ No newline at end of file +} + + +bool KIGEOM::BoxHitTest( const SHAPE_LINE_CHAIN& aHitter, const BOX2I& aHittee, bool aHitteeContained ) +{ + SHAPE_RECT bbox( aHittee ); + + return KIGEOM::ShapeHitTest( aHitter, bbox, aHitteeContained ); +} + + +bool KIGEOM::BoxHitTest( const SHAPE_LINE_CHAIN& aHitter, const BOX2I& aHittee, const EDA_ANGLE& aHitteeRotation, + const VECTOR2I& aHitteeRotationCenter, bool aHitteeContained ) +{ + // Optimization: use SHAPE_RECT collision test if possible + if( aHitteeRotation.IsZero() ) + { + return KIGEOM::BoxHitTest( aHitter, aHittee, aHitteeContained ); + } + else if( aHitteeRotation.IsCardinal() ) + { + BOX2I box = aHittee.GetBoundingBoxRotated( aHitteeRotationCenter, aHitteeRotation ); + return KIGEOM::BoxHitTest( aHitter, box, aHitteeContained ); + } + + // Non-cardinal angle: convert to simple polygon and rotate + const std::vector corners = + { + aHittee.GetOrigin(), + VECTOR2I( aHittee.GetRight(), aHittee.GetTop() ), + aHittee.GetEnd(), + VECTOR2I( aHittee.GetLeft(), aHittee.GetBottom() ) + }; + + SHAPE_SIMPLE shape( corners ); + shape.Rotate( aHitteeRotation, aHitteeRotationCenter ); + + return KIGEOM::ShapeHitTest( aHitter, shape, aHitteeContained ); +} + + +bool KIGEOM::ShapeHitTest( const SHAPE_LINE_CHAIN& aHitter, const SHAPE& aHittee, bool aHitteeContained ) +{ + // Check if the selection polygon collides with any of the hittee's subshapes. + auto collidesAny = + [&]() + { + return aHittee.Collide( &aHitter ); + }; + + // Check if the selection polygon collides with all of the hittee's subshapes. + auto collidesAll = + [&]() + { + if( const auto compoundHittee = dynamic_cast( &aHittee ) ) + { + // If the hittee is a compound shape, all subshapes must collide. + return std::ranges::all_of( + compoundHittee->Shapes(), + [&]( const SHAPE* subshape ) + { + return subshape && subshape->Collide( &aHitter ); + } ); + } + else + { + // If the hittee is a simple shape, we can check it directly. + return aHittee.Collide( &aHitter ); + } + }; + + // Check if the selection polygon outline collides with the hittee's shape. + auto intersectsAny = + [&]() + { + const int count = aHitter.SegmentCount(); + + for( int i = 0; i < count; ++i ) + { + if( aHittee.Collide( aHitter.CSegment( i ) ) ) + return true; + } + + return false; + }; + + if( aHitter.IsClosed() ) + { + if( aHitteeContained ) + // Containing polygon - all of the subshapes must collide with the selection polygon, + // but none of them can intersect its outline. + return collidesAll() && !intersectsAny(); + else + // Touching polygon - any of the subshapes should collide with the selection polygon. + return collidesAny(); + } + else + { + // Touching (poly)line - any of the subshapes should intersect the selection polyline. + return intersectsAny(); + } +} diff --git a/libs/kimath/src/geometry/shape_line_chain.cpp b/libs/kimath/src/geometry/shape_line_chain.cpp index 4335ff8f45..e876b6b30c 100644 --- a/libs/kimath/src/geometry/shape_line_chain.cpp +++ b/libs/kimath/src/geometry/shape_line_chain.cpp @@ -1745,6 +1745,18 @@ int SHAPE_LINE_CHAIN::Intersect( const SEG& aSeg, INTERSECTIONS& aIp ) const } +bool SHAPE_LINE_CHAIN::Intersects( const SEG& aSeg ) const +{ + for( int s = 0; s < SegmentCount(); s++ ) + { + if( CSegment( s ).Intersects( aSeg ) ) + return true; + } + + return false; +} + + static inline void addIntersection( SHAPE_LINE_CHAIN::INTERSECTIONS& aIps, int aPc, const SHAPE_LINE_CHAIN::INTERSECTION& aP ) { diff --git a/pagelayout_editor/tools/pl_selection_tool.cpp b/pagelayout_editor/tools/pl_selection_tool.cpp index fdd5fee207..1fd1c769f4 100644 --- a/pagelayout_editor/tools/pl_selection_tool.cpp +++ b/pagelayout_editor/tools/pl_selection_tool.cpp @@ -378,6 +378,8 @@ bool PL_SELECTION_TOOL::selectMultiple() area.SetAdditive( m_drag_additive ); area.SetSubtractive( m_drag_subtractive ); area.SetExclusiveOr( false ); + area.SetMode( windowSelection ? SELECTION_MODE::INSIDE_RECTANGLE + : SELECTION_MODE::TOUCHING_RECTANGLE ); view->SetVisible( &area, true ); view->Update( &area ); diff --git a/pcbnew/footprint.cpp b/pcbnew/footprint.cpp index 79de31b353..429fcbcbae 100644 --- a/pcbnew/footprint.cpp +++ b/pcbnew/footprint.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -1961,6 +1962,49 @@ bool FOOTPRINT::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) co } +bool FOOTPRINT::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + using std::ranges::all_of; + using std::ranges::any_of; + + // If there are no pads, zones, or drawings, test footprint text instead. + if( m_pads.empty() && m_zones.empty() && m_drawings.empty() ) + return KIGEOM::BoxHitTest( aPoly, GetBoundingBox( true ), aContained ); + + auto hitTest = + [&]( const auto* aItem ) + { + return aItem && aItem->HitTest( aPoly, aContained ); + }; + + // Filter out text items from the drawings, since they are selectable on their own, + // and we don't want to select the whole footprint when text is hit. TextBox items are NOT + // selectable on their own, so they are not excluded here. + auto drawings = m_drawings | std::views::filter( []( const auto* aItem ) + { + return aItem && aItem->Type() != PCB_TEXT_T; + } ); + + // Test pads, zones and drawings with text excluded. PCB fields are also selectable + // on their own, so they don't get tested. Groups are not hit-tested, only their members. + // Bitmaps aren't selectable since they aren't displayed. + if( aContained ) + { + // All items must be contained in the selection poly. + return all_of( drawings, hitTest ) + && all_of( m_pads, hitTest ) + && all_of( m_zones, hitTest ); + } + else + { + // Any item intersecting the selection poly is sufficient. + return any_of( drawings, hitTest ) + || any_of( m_pads, hitTest ) + || any_of( m_zones, hitTest ); + } +} + + PAD* FOOTPRINT::FindPadByNumber( const wxString& aPadNumber, PAD* aSearchAfterMe ) const { bool can_select = aSearchAfterMe ? false : true; diff --git a/pcbnew/footprint.h b/pcbnew/footprint.h index 90848a8e21..0446e1184c 100644 --- a/pcbnew/footprint.h +++ b/pcbnew/footprint.h @@ -607,6 +607,8 @@ public: bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; + /** * Test if the point hits one or more of the footprint elements on a given layer. * diff --git a/pcbnew/footprint_edit_frame.cpp b/pcbnew/footprint_edit_frame.cpp index 0c76e03cf8..6290efa401 100644 --- a/pcbnew/footprint_edit_frame.cpp +++ b/pcbnew/footprint_edit_frame.cpp @@ -1181,6 +1181,7 @@ void FOOTPRINT_EDIT_FRAME::setupTools() m_toolManager->RegisterTool( new COMMON_CONTROL ); m_toolManager->RegisterTool( new COMMON_TOOLS ); m_toolManager->RegisterTool( new PCB_SELECTION_TOOL ); + m_toolManager->RegisterTool( new PCB_LASSO_SELECTION_TOOL ); m_toolManager->RegisterTool( new ZOOM_TOOL ); m_toolManager->RegisterTool( new EDIT_TOOL ); m_toolManager->RegisterTool( new PCB_EDIT_TABLE_TOOL ); @@ -1316,6 +1317,12 @@ void FOOTPRINT_EDIT_FRAME::setupUIConditions() mgr->SetConditions( ACTIONS::pasteSpecial, ENABLE( SELECTION_CONDITIONS::Idle && cond.NoActiveTool() ) ); mgr->SetConditions( ACTIONS::doDelete, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::duplicate, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectInsideRectangle, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingRectangle,ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectInsideLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectAutoLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingPath, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::selectAll, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::unselectAll, ENABLE( cond.HasItems() ) ); diff --git a/pcbnew/generators/pcb_tuning_pattern.cpp b/pcbnew/generators/pcb_tuning_pattern.cpp index fd4fe8482c..84d5b1e807 100644 --- a/pcbnew/generators/pcb_tuning_pattern.cpp +++ b/pcbnew/generators/pcb_tuning_pattern.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -384,6 +385,11 @@ public: return sel.Intersects( GetBoundingBox() ); } + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override + { + return KIGEOM::ShapeHitTest( aPoly, getOutline(), aContained ); + } + const BOX2I ViewBBox() const override { return GetBoundingBox(); } EDA_ITEM* Clone() const override { return new PCB_TUNING_PATTERN( *this ); } diff --git a/pcbnew/menubar_footprint_editor.cpp b/pcbnew/menubar_footprint_editor.cpp index 75cb7b1699..14500be7c8 100644 --- a/pcbnew/menubar_footprint_editor.cpp +++ b/pcbnew/menubar_footprint_editor.cpp @@ -106,8 +106,22 @@ void FOOTPRINT_EDIT_FRAME::doReCreateMenuBar() editMenu->Add( ACTIONS::doDelete ); editMenu->Add( ACTIONS::duplicate ); + editMenu->AppendSeparator(); - editMenu->Add( ACTIONS::selectAll ); + + // Select Submenu + ACTION_MENU* selectSubMenu = new ACTION_MENU( false, selTool ); + selectSubMenu->SetTitle( _( "&Select" ) ); + selectSubMenu->Add( ACTIONS::selectInsideRectangle ); + selectSubMenu->Add( ACTIONS::selectTouchingRectangle ); + selectSubMenu->Add( ACTIONS::selectInsideLasso ); + selectSubMenu->Add( ACTIONS::selectTouchingLasso ); + selectSubMenu->Add( ACTIONS::selectTouchingPath ); + selectSubMenu->AppendSeparator(); + selectSubMenu->Add( ACTIONS::selectAll ); + selectSubMenu->Add( ACTIONS::unselectAll ); + + editMenu->Add( selectSubMenu ); editMenu->AppendSeparator(); editMenu->Add( PCB_ACTIONS::editTextAndGraphics ); diff --git a/pcbnew/menubar_pcb_editor.cpp b/pcbnew/menubar_pcb_editor.cpp index 6a7ed82021..a4824af130 100644 --- a/pcbnew/menubar_pcb_editor.cpp +++ b/pcbnew/menubar_pcb_editor.cpp @@ -179,8 +179,21 @@ void PCB_EDIT_FRAME::doReCreateMenuBar() editMenu->Add( ACTIONS::doDelete ); editMenu->AppendSeparator(); - editMenu->Add( ACTIONS::selectAll ); - editMenu->Add( ACTIONS::unselectAll ); + + // Select Submenu + ACTION_MENU* selectSubMenu = new ACTION_MENU( false, selTool ); + selectSubMenu->SetTitle( _( "&Select" ) ); + + selectSubMenu->Add( ACTIONS::selectInsideRectangle ); + selectSubMenu->Add( ACTIONS::selectTouchingRectangle ); + selectSubMenu->Add( ACTIONS::selectInsideLasso ); + selectSubMenu->Add( ACTIONS::selectTouchingLasso ); + selectSubMenu->Add( ACTIONS::selectTouchingPath ); + selectSubMenu->AppendSeparator(); + selectSubMenu->Add( ACTIONS::selectAll ); + selectSubMenu->Add( ACTIONS::unselectAll ); + + editMenu->Add( selectSubMenu ); editMenu->AppendSeparator(); editMenu->Add( ACTIONS::find ); diff --git a/pcbnew/pad.cpp b/pcbnew/pad.cpp index fa36d90d9c..1478e0a905 100644 --- a/pcbnew/pad.cpp +++ b/pcbnew/pad.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -1555,6 +1556,24 @@ bool PAD::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) const } +bool PAD::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + SHAPE_COMPOUND effectiveShape; + + // Add padstack shapes + Padstack().ForEachUniqueLayer( + [&]( PCB_LAYER_ID aLayer ) + { + effectiveShape.AddShape( GetEffectiveShape( aLayer ) ); + } ); + + // Add hole shape + effectiveShape.AddShape( GetEffectiveHoleShape() ); + + return KIGEOM::ShapeHitTest( aPoly, effectiveShape, aContained ); +} + + int PAD::Compare( const PAD* aPadRef, const PAD* aPadCmp ) { int diff; diff --git a/pcbnew/pad.h b/pcbnew/pad.h index 319c97fe5c..bd3076e6b7 100644 --- a/pcbnew/pad.h +++ b/pcbnew/pad.h @@ -823,7 +823,7 @@ public: bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override; bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; - + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; /** * Recombines the pad with other graphical shapes in the footprint diff --git a/pcbnew/pcb_dimension.cpp b/pcbnew/pcb_dimension.cpp index d0bb1001c1..fb1f4491b6 100644 --- a/pcbnew/pcb_dimension.cpp +++ b/pcbnew/pcb_dimension.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include #include @@ -725,6 +727,22 @@ bool PCB_DIMENSION_BASE::HitTest( const BOX2I& aRect, bool aContained, int aAccu } +bool PCB_DIMENSION_BASE::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + // Note: Can't use GetEffectiveShape() because we want text as BoundingBox, not as graphics. + SHAPE_COMPOUND effShape; + + // Add shapes + for( const std::shared_ptr& shape : GetShapes() ) + effShape.AddShape( shape ); + + if( aContained ) + return TextHitTest( aPoly, aContained ) && KIGEOM::ShapeHitTest( aPoly, effShape, aContained ); + else + return TextHitTest( aPoly, aContained ) || KIGEOM::ShapeHitTest( aPoly, effShape, aContained ); +} + + const BOX2I PCB_DIMENSION_BASE::GetBoundingBox() const { BOX2I bBox; @@ -1745,6 +1763,16 @@ const BOX2I PCB_DIM_CENTER::ViewBBox() const } +void PCB_DIM_CENTER::updateText() +{ + // Even if PCB_DIM_CENTER has no text, we still need to update its text position + // so GetTextPos() users get a valid value. Required at least for lasso hit-testing. + SetTextPos( m_start ); + + PCB_DIMENSION_BASE::updateText(); +} + + void PCB_DIM_CENTER::updateGeometry() { if( m_busy ) // Skeep reentrance that happens sometimes after calling updateText() diff --git a/pcbnew/pcb_dimension.h b/pcbnew/pcb_dimension.h index df38d8a6a2..1a14b17441 100644 --- a/pcbnew/pcb_dimension.h +++ b/pcbnew/pcb_dimension.h @@ -295,11 +295,12 @@ public: bool HitTest( const VECTOR2I& aPosition, int aAccuracy ) const override; bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; const BOX2I GetBoundingBox() const override; - std::shared_ptr GetEffectiveShape( PCB_LAYER_ID aLayer, - FLASHING aFlash = FLASHING::DEFAULT ) const override; + std::shared_ptr GetEffectiveShape( PCB_LAYER_ID aLayer = UNDEFINED_LAYER, + FLASHING aFlash = FLASHING::DEFAULT ) const override; wxString GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const override; @@ -719,6 +720,7 @@ public: protected: virtual void swapData( BOARD_ITEM* aImage ) override; + void updateText() override; void updateGeometry() override; }; diff --git a/pcbnew/pcb_edit_frame.cpp b/pcbnew/pcb_edit_frame.cpp index 5624fbb9ab..884a35e530 100644 --- a/pcbnew/pcb_edit_frame.cpp +++ b/pcbnew/pcb_edit_frame.cpp @@ -731,6 +731,7 @@ void PCB_EDIT_FRAME::setupTools() m_toolManager->RegisterTool( new COMMON_CONTROL ); m_toolManager->RegisterTool( new COMMON_TOOLS ); m_toolManager->RegisterTool( new PCB_SELECTION_TOOL ); + m_toolManager->RegisterTool( new PCB_LASSO_SELECTION_TOOL ); m_toolManager->RegisterTool( new ZOOM_TOOL ); m_toolManager->RegisterTool( new PCB_PICKER_TOOL ); m_toolManager->RegisterTool( new ROUTER_TOOL ); @@ -832,6 +833,12 @@ void PCB_EDIT_FRAME::setupUIConditions() mgr->SetConditions( ACTIONS::copy, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::paste, ENABLE( SELECTION_CONDITIONS::Idle && cond.NoActiveTool() ) ); mgr->SetConditions( ACTIONS::pasteSpecial, ENABLE( SELECTION_CONDITIONS::Idle && cond.NoActiveTool() ) ); + mgr->SetConditions( ACTIONS::selectInsideRectangle, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingRectangle, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectInsideLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectAutoLasso, ENABLE( cond.HasItems() ) ); + mgr->SetConditions( ACTIONS::selectTouchingPath, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::selectAll, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::unselectAll, ENABLE( cond.HasItems() ) ); mgr->SetConditions( ACTIONS::doDelete, ENABLE( cond.HasItems() ) ); diff --git a/pcbnew/pcb_group.cpp b/pcbnew/pcb_group.cpp index 0b4fb7e13c..8fbcc215f3 100644 --- a/pcbnew/pcb_group.cpp +++ b/pcbnew/pcb_group.cpp @@ -256,6 +256,13 @@ bool PCB_GROUP::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) co } +bool PCB_GROUP::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + // Groups are selected by promoting a selection of one of their children + return false; +} + + const BOX2I PCB_GROUP::GetBoundingBox() const { BOX2I bbox; diff --git a/pcbnew/pcb_group.h b/pcbnew/pcb_group.h index 21531a0401..23536c21fa 100644 --- a/pcbnew/pcb_group.h +++ b/pcbnew/pcb_group.h @@ -142,6 +142,9 @@ public: /// @copydoc EDA_ITEM::HitTest bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + /// @copydoc EDA_ITEM::HitTest + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; + /// @copydoc EDA_ITEM::GetBoundingBox const BOX2I GetBoundingBox() const override; diff --git a/pcbnew/pcb_marker.h b/pcbnew/pcb_marker.h index 8d80b96465..c172bfdeed 100644 --- a/pcbnew/pcb_marker.h +++ b/pcbnew/pcb_marker.h @@ -90,6 +90,14 @@ public: return HitTestMarker( aRect, aContained, aAccuracy ); } + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override + { + if( GetMarkerType() == MARKER_RATSNEST ) + return false; + + return HitTestMarker( aPoly, aContained ); + } + EDA_ITEM* Clone() const override { return new PCB_MARKER( *this ); diff --git a/pcbnew/pcb_reference_image.cpp b/pcbnew/pcb_reference_image.cpp index 75eab7d6e4..564283191a 100644 --- a/pcbnew/pcb_reference_image.cpp +++ b/pcbnew/pcb_reference_image.cpp @@ -203,6 +203,12 @@ bool PCB_REFERENCE_IMAGE::HitTest( const BOX2I& aRect, bool aContained, int aAcc } +bool PCB_REFERENCE_IMAGE::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + return KIGEOM::BoxHitTest( aPoly, GetBoundingBox(), aContained ); +} + + BITMAPS PCB_REFERENCE_IMAGE::GetMenuImage() const { return BITMAPS::image; diff --git a/pcbnew/pcb_reference_image.h b/pcbnew/pcb_reference_image.h index 91fa95436a..f911d0575d 100644 --- a/pcbnew/pcb_reference_image.h +++ b/pcbnew/pcb_reference_image.h @@ -100,6 +100,7 @@ public: bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override; bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; EDA_ITEM* Clone() const override; diff --git a/pcbnew/pcb_shape.h b/pcbnew/pcb_shape.h index 54e702b2c7..b0988ecf02 100644 --- a/pcbnew/pcb_shape.h +++ b/pcbnew/pcb_shape.h @@ -130,6 +130,11 @@ public: return hitTest( aRect, aContained, aAccuracy ); } + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override + { + return hitTest( aPoly, aContained ); + } + void Normalize() override; /** diff --git a/pcbnew/pcb_table.cpp b/pcbnew/pcb_table.cpp index 97cb1c040f..26534e7895 100644 --- a/pcbnew/pcb_table.cpp +++ b/pcbnew/pcb_table.cpp @@ -28,6 +28,7 @@ #include #include #include +#include PCB_TABLE::PCB_TABLE( BOARD_ITEM* aParent, int aLineWidth ) : @@ -491,6 +492,12 @@ bool PCB_TABLE::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) co } +bool PCB_TABLE::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + return KIGEOM::ShapeHitTest( aPoly, *GetEffectiveShape(), aContained ); +} + + void PCB_TABLE::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector& aList ) { // Don't use GetShownText() here; we want to show the user the variable references diff --git a/pcbnew/pcb_table.h b/pcbnew/pcb_table.h index f13af424c3..b4bb88ba05 100644 --- a/pcbnew/pcb_table.h +++ b/pcbnew/pcb_table.h @@ -238,6 +238,8 @@ public: bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; + EDA_ITEM* Clone() const override { return new PCB_TABLE( *this ); diff --git a/pcbnew/pcb_text.cpp b/pcbnew/pcb_text.cpp index fb17886fc7..3373d5581e 100644 --- a/pcbnew/pcb_text.cpp +++ b/pcbnew/pcb_text.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -381,6 +382,17 @@ bool PCB_TEXT::TextHitTest( const BOX2I& aRect, bool aContains, int aAccuracy ) } +bool PCB_TEXT::TextHitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + BOX2I rect = GetTextBox( nullptr ); + + if( IsKnockout() ) + rect.Inflate( getKnockoutMargin() ); + + return KIGEOM::BoxHitTest( aPoly, rect, GetDrawRotation(), GetDrawPos(), aContained ); +} + + void PCB_TEXT::Rotate( const VECTOR2I& aRotCentre, const EDA_ANGLE& aAngle ) { VECTOR2I pt = GetTextPos(); diff --git a/pcbnew/pcb_text.h b/pcbnew/pcb_text.h index a856a58847..d3b15f596b 100644 --- a/pcbnew/pcb_text.h +++ b/pcbnew/pcb_text.h @@ -106,6 +106,7 @@ public: bool TextHitTest( const VECTOR2I& aPoint, int aAccuracy = 0 ) const override; bool TextHitTest( const BOX2I& aRect, bool aContains, int aAccuracy = 0 ) const override; + bool TextHitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const; bool HitTest( const VECTOR2I& aPosition, int aAccuracy ) const override { @@ -117,6 +118,11 @@ public: return TextHitTest( aRect, aContained, aAccuracy ); } + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override + { + return TextHitTest( aPoly, aContained ); + } + wxString GetClass() const override { return wxT( "PCB_TEXT" ); diff --git a/pcbnew/pcb_textbox.cpp b/pcbnew/pcb_textbox.cpp index f0e2cf8c93..0319f27ae8 100644 --- a/pcbnew/pcb_textbox.cpp +++ b/pcbnew/pcb_textbox.cpp @@ -33,6 +33,8 @@ #include #include #include +#include +#include #include #include #include @@ -583,6 +585,12 @@ bool PCB_TEXTBOX::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) } +bool PCB_TEXTBOX::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + return PCB_SHAPE::HitTest( aPoly, aContained ); +} + + wxString PCB_TEXTBOX::GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const { return wxString::Format( _( "PCB Text Box '%s' on %s" ), diff --git a/pcbnew/pcb_textbox.h b/pcbnew/pcb_textbox.h index 68be0bcadd..f885a2d6a8 100644 --- a/pcbnew/pcb_textbox.h +++ b/pcbnew/pcb_textbox.h @@ -114,6 +114,8 @@ public: bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; + wxString GetClass() const override { return wxT( "PCB_TEXTBOX" ); diff --git a/pcbnew/pcb_track.cpp b/pcbnew/pcb_track.cpp index 2d64aee005..5896658041 100644 --- a/pcbnew/pcb_track.cpp +++ b/pcbnew/pcb_track.cpp @@ -2085,6 +2085,12 @@ bool PCB_VIA::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) cons } +bool PCB_TRACK::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + return KIGEOM::ShapeHitTest( aPoly, *GetEffectiveShape(), aContained ); +} + + wxString PCB_TRACK::GetItemDescription( UNITS_PROVIDER* aUnitsProvider, bool aFull ) const { return wxString::Format( Type() == PCB_ARC_T ? _("Track (arc) %s on %s, length %s" ) diff --git a/pcbnew/pcb_track.h b/pcbnew/pcb_track.h index c6e9c1e9a6..e036e1cfef 100644 --- a/pcbnew/pcb_track.h +++ b/pcbnew/pcb_track.h @@ -246,6 +246,7 @@ public: bool HitTest( const VECTOR2I& aPosition, int aAccuracy = 0 ) const override; bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override; + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const override; bool ApproxCollinear( const PCB_TRACK& aTrack ); diff --git a/pcbnew/toolbars_footprint_editor.cpp b/pcbnew/toolbars_footprint_editor.cpp index ed69c01560..65071d5f84 100644 --- a/pcbnew/toolbars_footprint_editor.cpp +++ b/pcbnew/toolbars_footprint_editor.cpp @@ -83,7 +83,12 @@ std::optional FOOTPRINT_EDIT_TOOLBAR_SETTINGS::DefaultToo break; case TOOLBAR_LOC::RIGHT: - config.AppendAction( ACTIONS::selectionTool ); + config.AppendAction( ACTIONS::selectionTool ) + .AppendGroup( TOOLBAR_GROUP_CONFIG( _( "Lasso selection tools" ) ) + .AddAction( ACTIONS::selectAutoLasso ) + .AddAction( ACTIONS::selectInsideLasso ) + .AddAction( ACTIONS::selectTouchingLasso ) + .AddAction( ACTIONS::selectTouchingPath ) ); config.AppendSeparator() .AppendAction( PCB_ACTIONS::placePad ) diff --git a/pcbnew/toolbars_pcb_editor.cpp b/pcbnew/toolbars_pcb_editor.cpp index 3e51d2809a..951897c2ae 100644 --- a/pcbnew/toolbars_pcb_editor.cpp +++ b/pcbnew/toolbars_pcb_editor.cpp @@ -188,6 +188,11 @@ std::optional PCB_EDIT_TOOLBAR_SETTINGS::DefaultToolbarCo case TOOLBAR_LOC::RIGHT: config.AppendAction( ACTIONS::selectionTool ) + .AppendGroup( TOOLBAR_GROUP_CONFIG( _( "Lasso selection tools" ) ) + .AddAction( ACTIONS::selectAutoLasso ) + .AddAction( ACTIONS::selectInsideLasso ) + .AddAction( ACTIONS::selectTouchingLasso ) + .AddAction( ACTIONS::selectTouchingPath ) ) .AppendAction( PCB_ACTIONS::localRatsnestTool ); config.AppendSeparator() diff --git a/pcbnew/tools/pcb_actions.cpp b/pcbnew/tools/pcb_actions.cpp index 10818685fc..30012d7dfb 100644 --- a/pcbnew/tools/pcb_actions.cpp +++ b/pcbnew/tools/pcb_actions.cpp @@ -422,7 +422,7 @@ TOOL_ACTION PCB_ACTIONS::magneticSnapToggle( TOOL_ACTION_ARGS() TOOL_ACTION PCB_ACTIONS::deleteLastPoint( TOOL_ACTION_ARGS() .Name( "pcbnew.InteractiveDrawing.deleteLastPoint" ) - .Scope( AS_CONTEXT ) + .Scope( AS_GLOBAL ) .DefaultHotkey( WXK_BACK ) .FriendlyName( _( "Delete Last Point" ) ) .Tooltip( _( "Delete the last point added to the current item" ) ) diff --git a/pcbnew/tools/pcb_selection_tool.cpp b/pcbnew/tools/pcb_selection_tool.cpp index 5dffaaa07b..ff5ee6eef2 100644 --- a/pcbnew/tools/pcb_selection_tool.cpp +++ b/pcbnew/tools/pcb_selection_tool.cpp @@ -54,7 +54,6 @@ using namespace std::placeholders; #include #include #include -#include #include #include #include @@ -68,6 +67,7 @@ using namespace std::placeholders; #include #include #include +#include #include #include #include @@ -452,11 +452,11 @@ int PCB_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) } else if( hasModifier() || dragAction == MOUSE_DRAG_ACTION::SELECT ) { - selectMultiple(); + SelectRectArea( aEvent ); } else if( m_selection.Empty() && dragAction != MOUSE_DRAG_ACTION::DRAG_ANY ) { - selectMultiple(); + SelectRectArea( aEvent ); } else { @@ -485,7 +485,7 @@ int PCB_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) aCollector.Remove( item ); }; - // See if we can drag before falling back to selectMultiple() + // See if we can drag before falling back to SelectRectArea() bool doDrag = false; if( evt->HasPosition() ) @@ -522,7 +522,7 @@ int PCB_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent ) else { // Otherwise drag a selection box - selectMultiple(); + SelectRectArea( aEvent ); } } } @@ -946,6 +946,19 @@ const TOOL_ACTION* allowedActions[] = { &ACTIONS::panUp, &ACTIONS::panD &ACTIONS::zoomFitObjects, nullptr }; +static void passEvent( TOOL_EVENT* const aEvent, const TOOL_ACTION* const aAllowedActions[] ) +{ + for( int i = 0; aAllowedActions[i]; ++i ) + { + if( aEvent->IsAction( aAllowedActions[i] ) ) + { + aEvent->SetPassEvent(); + break; + } + } +} + + bool PCB_SELECTION_TOOL::selectTableCells( PCB_TABLE* aTable ) { bool cancelled = false; // Was the tool canceled while it was running? @@ -1030,14 +1043,7 @@ bool PCB_SELECTION_TOOL::selectTableCells( PCB_TABLE* aTable ) else { // Allow some actions for navigation - for( int i = 0; allowedActions[i]; ++i ) - { - if( evt->IsAction( allowedActions[i] ) ) - { - evt->SetPassEvent(); - break; - } - } + passEvent( evt, allowedActions ); } } @@ -1052,31 +1058,46 @@ bool PCB_SELECTION_TOOL::selectTableCells( PCB_TABLE* aTable ) } -bool PCB_SELECTION_TOOL::selectMultiple() +int PCB_SELECTION_TOOL::SelectRectArea( const TOOL_EVENT& aEvent ) { bool cancelled = false; // Was the tool canceled while it was running? m_multiple = true; // Multiple selection mode is active - KIGFX::VIEW* view = getView(); + KIGFX::VIEW* view = getView(); + bool fixedMode = false; + SELECTION_MODE selectionMode = SELECTION_MODE::INSIDE_RECTANGLE; + + if( aEvent.HasParameter() ) + { + fixedMode = true; + selectionMode = aEvent.Parameter(); + } KIGFX::PREVIEW::SELECTION_AREA area; view->Add( &area ); - bool anyAdded = false; - bool anySubtracted = false; - while( TOOL_EVENT* evt = Wait() ) { - /* Selection mode depends on direction of drag-selection: - * Left > Right : Select objects that are fully enclosed by selection - * Right > Left : Select objects that are crossed by selection - */ - bool greedySelection = area.GetEnd().x < area.GetOrigin().x; + if( !fixedMode ) + { + int width = area.GetEnd().x - area.GetOrigin().x; - if( view->IsMirroredX() ) - greedySelection = !greedySelection; + /* Selection mode depends on direction of drag-selection: + * Left > Right : Select objects that are fully enclosed by selection + * Right > Left : Select objects that are crossed by selection + */ + bool touchingSelection = width >= 0 ? false : true; - m_frame->GetCanvas()->SetCurrentCursor( !greedySelection ? KICURSOR::SELECT_WINDOW - : KICURSOR::SELECT_LASSO ); + if( view->IsMirroredX() ) + touchingSelection = !touchingSelection; + + selectionMode = touchingSelection ? SELECTION_MODE::TOUCHING_RECTANGLE + : SELECTION_MODE::INSIDE_RECTANGLE; + } + + if( selectionMode == SELECTION_MODE::INSIDE_RECTANGLE ) + m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SELECT_WINDOW ); + else + m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SELECT_LASSO ); if( evt->IsCancelInteractive() || evt->IsActivate() ) { @@ -1090,8 +1111,8 @@ bool PCB_SELECTION_TOOL::selectMultiple() { if( m_selection.GetSize() > 0 ) { - anySubtracted = true; ClearSelection( true /*quiet mode*/ ); + m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); } } @@ -1101,6 +1122,7 @@ bool PCB_SELECTION_TOOL::selectMultiple() area.SetAdditive( m_drag_additive ); area.SetSubtractive( m_drag_subtractive ); area.SetExclusiveOr( false ); + area.SetMode( selectionMode ); view->SetVisible( &area, true ); view->Update( &area ); @@ -1114,133 +1136,20 @@ bool PCB_SELECTION_TOOL::selectMultiple() // End drawing the selection box view->SetVisible( &area, false ); - std::vector candidates; - BOX2I selectionRect = area.ViewBBox(); - view->Query( selectionRect, candidates ); // Get the list of nearby items + SelectMultiple( area, m_subtractive, m_exclusive_or ); - selectionRect.Normalize(); - - GENERAL_COLLECTOR collector; - GENERAL_COLLECTOR padsCollector; - std::set group_items; - - for( PCB_GROUP* group : board()->Groups() ) - { - // The currently entered group does not get limited - if( m_enteredGroup == group ) - continue; - - std::unordered_set& newset = group->GetItems(); - - // If we are not greedy and have selected the whole group, add just one item - // to allow it to be promoted to the group later - if( !greedySelection && selectionRect.Contains( group->GetBoundingBox() ) - && newset.size() ) - { - for( EDA_ITEM* group_item : newset ) - { - if( !group_item->IsBOARD_ITEM() ) - continue; - - if( Selectable( static_cast( group_item ) ) ) - collector.Append( *newset.begin() ); - } - } - - for( EDA_ITEM* group_item : newset ) - group_items.emplace( group_item ); - } - - for( const auto& [item, layer] : candidates ) - { - if( !item->IsBOARD_ITEM() ) - continue; - - BOARD_ITEM* boardItem = static_cast( item ); - - if( Selectable( boardItem ) && boardItem->HitTest( selectionRect, !greedySelection ) - && ( greedySelection || !group_items.count( boardItem ) ) ) - { - if( boardItem->Type() == PCB_PAD_T && !m_isFootprintEditor ) - padsCollector.Append( boardItem ); - else - collector.Append( boardItem ); - } - } - - // Apply the stateful filter - FilterCollectedItems( collector, true ); - - FilterCollectorForHierarchy( collector, true ); - - // If we selected nothing but pads, allow them to be selected - if( collector.GetCount() == 0 ) - { - collector = padsCollector; - FilterCollectedItems( collector, true ); - FilterCollectorForHierarchy( collector, true ); - } - - // Sort the filtered selection by rows and columns to have a nice default - // for tools that can use it. - std::sort( collector.begin(), collector.end(), - []( EDA_ITEM* a, EDA_ITEM* b ) - { - VECTOR2I aPos = a->GetPosition(); - VECTOR2I bPos = b->GetPosition(); - - if( aPos.y == bPos.y ) - return aPos.x < bPos.x; - - return aPos.y < bPos.y; - } ); - - for( EDA_ITEM* i : collector ) - { - if( !i->IsBOARD_ITEM() ) - continue; - - BOARD_ITEM* item = static_cast( i ); - - if( m_subtractive || ( m_exclusive_or && item->IsSelected() ) ) - { - unselect( item ); - anySubtracted = true; - } - else - { - select( item ); - anyAdded = true; - } - } - - m_selection.SetIsHover( false ); - - // Inform other potentially interested tools - if( anyAdded ) - m_toolMgr->ProcessEvent( EVENTS::SelectedEvent ); - else if( anySubtracted ) - m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); - - break; // Stop waiting for events + break; // Stop waiting for events } // Allow some actions for navigation - for( int i = 0; allowedActions[i]; ++i ) - { - if( evt->IsAction( allowedActions[i] ) ) - { - evt->SetPassEvent(); - break; - } - } + passEvent( evt, allowedActions ); } getViewControls()->SetAutoPan( false ); // Stop drawing the selection box view->Remove( &area ); - m_multiple = false; // Multiple selection mode is inactive + m_multiple = false; // Multiple selection mode is inactive if( !cancelled ) m_selection.ClearReferencePoint(); @@ -1251,6 +1160,141 @@ bool PCB_SELECTION_TOOL::selectMultiple() } +void PCB_SELECTION_TOOL::SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, bool aSubtractive, + bool aExclusiveOr ) +{ + KIGFX::VIEW* view = getView(); + + bool anyAdded = false; + bool anySubtracted = false; + + SELECTION_MODE selectionMode = aArea.GetMode(); + bool containedMode = ( selectionMode == SELECTION_MODE::INSIDE_RECTANGLE + || selectionMode == SELECTION_MODE::INSIDE_LASSO ) ? true : false; + bool boxMode = ( selectionMode == SELECTION_MODE::INSIDE_RECTANGLE + || selectionMode == SELECTION_MODE::TOUCHING_RECTANGLE ) ? true : false; + + std::vector candidates; + BOX2I selectionBox = aArea.ViewBBox(); + view->Query( selectionBox, candidates ); // Get the list of nearby items + + GENERAL_COLLECTOR collector; + GENERAL_COLLECTOR padsCollector; + std::set group_items; + + for( PCB_GROUP* group : board()->Groups() ) + { + // The currently entered group does not get limited + if( m_enteredGroup == group ) + continue; + + std::unordered_set& newset = group->GetItems(); + + auto boxContained = + [&]( const BOX2I& aBox ) + { + return boxMode ? selectionBox.Contains( aBox ) + : KIGEOM::BoxHitTest( aArea.GetPoly(), aBox, true ); + }; + + // If we are not greedy and have selected the whole group, add just one item + // to allow it to be promoted to the group later + if( containedMode && boxContained( group->GetBoundingBox() ) && newset.size() ) + { + for( EDA_ITEM* group_item : newset ) + { + if( !group_item->IsBOARD_ITEM() ) + continue; + + if( Selectable( static_cast( group_item ) ) ) + collector.Append( *newset.begin() ); + } + } + + for( EDA_ITEM* group_item : newset ) + group_items.emplace( group_item ); + } + + auto hitTest = + [&]( const EDA_ITEM* aItem ) + { + return boxMode ? aItem->HitTest( selectionBox, containedMode ) + : aItem->HitTest( aArea.GetPoly(), containedMode ); + }; + + for( const auto& [item, layer] : candidates ) + { + if( !item->IsBOARD_ITEM() ) + continue; + + BOARD_ITEM* boardItem = static_cast( item ); + + if( Selectable( boardItem ) && hitTest( boardItem ) + && ( !containedMode || !group_items.count( boardItem ) ) ) + { + if( boardItem->Type() == PCB_PAD_T && !m_isFootprintEditor ) + padsCollector.Append( boardItem ); + else + collector.Append( boardItem ); + } + } + + // Apply the stateful filter + FilterCollectedItems( collector, true ); + + FilterCollectorForHierarchy( collector, true ); + + // If we selected nothing but pads, allow them to be selected + if( collector.GetCount() == 0 ) + { + collector = padsCollector; + FilterCollectedItems( collector, true ); + FilterCollectorForHierarchy( collector, true ); + } + + // Sort the filtered selection by rows and columns to have a nice default + // for tools that can use it. + std::sort( collector.begin(), collector.end(), + []( EDA_ITEM* a, EDA_ITEM* b ) + { + VECTOR2I aPos = a->GetPosition(); + VECTOR2I bPos = b->GetPosition(); + + if( aPos.y == bPos.y ) + return aPos.x < bPos.x; + + return aPos.y < bPos.y; + } ); + + for( EDA_ITEM* i : collector ) + { + if( !i->IsBOARD_ITEM() ) + continue; + + BOARD_ITEM* item = static_cast( i ); + + if( aSubtractive || ( aExclusiveOr && item->IsSelected() ) ) + { + unselect( item ); + anySubtracted = true; + } + else + { + select( item ); + anyAdded = true; + } + } + + m_selection.SetIsHover( false ); + + // Inform other potentially interested tools + if( anyAdded ) + m_toolMgr->ProcessEvent( EVENTS::SelectedEvent ); + else if( anySubtracted ) + m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); +} + + int PCB_SELECTION_TOOL::disambiguateCursor( const TOOL_EVENT& aEvent ) { wxMouseState keyboardState = wxGetMouseState(); @@ -4205,8 +4249,205 @@ void PCB_SELECTION_TOOL::setTransitions() Go( &PCB_SELECTION_TOOL::SelectRows, ACTIONS::selectRows.MakeEvent() ); Go( &PCB_SELECTION_TOOL::SelectTable, ACTIONS::selectTable.MakeEvent() ); + Go( &PCB_SELECTION_TOOL::SelectRectArea, ACTIONS::selectInsideRectangle.MakeEvent() ); + Go( &PCB_SELECTION_TOOL::SelectRectArea, ACTIONS::selectTouchingRectangle.MakeEvent() ); Go( &PCB_SELECTION_TOOL::SelectAll, ACTIONS::selectAll.MakeEvent() ); Go( &PCB_SELECTION_TOOL::UnselectAll, ACTIONS::unselectAll.MakeEvent() ); Go( &PCB_SELECTION_TOOL::disambiguateCursor, EVENTS::DisambiguatePoint ); } + + +PCB_LASSO_SELECTION_TOOL::PCB_LASSO_SELECTION_TOOL() : + PCB_TOOL_BASE( "common.InteractiveLassoSelection" ) +{ +} + + +PCB_LASSO_SELECTION_TOOL::~PCB_LASSO_SELECTION_TOOL() +{ +} + + +bool PCB_LASSO_SELECTION_TOOL::Init() +{ + return true; +} + + +void PCB_LASSO_SELECTION_TOOL::Reset( RESET_REASON aReason ) +{ +} + + +int PCB_LASSO_SELECTION_TOOL::SelectPolyArea( const TOOL_EVENT& aEvent ) +{ + bool cancelled = false; // Was the tool canceled while it was running? + bool fixedMode = false; + bool additive = false; + bool subtractive = false; + bool exclusiveOr = false; + PCB_SELECTION_TOOL* selectionTool = m_toolMgr->GetTool(); + SELECTION_MODE selectionMode = SELECTION_MODE::TOUCHING_LASSO; + + auto updateModifiersAndCursor = + [&]( wxTimerEvent& aEvent ) + { + KICURSOR cursor; + wxMouseState keyboardState = wxGetMouseState(); + + subtractive = keyboardState.ControlDown() && keyboardState.ShiftDown(); + additive = !keyboardState.ControlDown() && keyboardState.ShiftDown(); + exclusiveOr = keyboardState.ControlDown() && !keyboardState.ShiftDown(); + + if( additive ) + cursor = KICURSOR::ADD; + else if( subtractive ) + cursor = KICURSOR::SUBTRACT; + else if( exclusiveOr ) + cursor = KICURSOR::XOR ; + else if( selectionMode == SELECTION_MODE::INSIDE_LASSO ) + cursor = KICURSOR::SELECT_WINDOW; + else + cursor = KICURSOR::SELECT_LASSO; + + frame()->GetCanvas()->SetCurrentCursor( cursor ); + }; + + // No events are sent for modifier keys, so we need to poll them using a timer. + wxTimer timer; + timer.Bind( wxEVT_TIMER, updateModifiersAndCursor ); + timer.Start( 100 ); + + if( aEvent.HasParameter() ) + { + fixedMode = true; + selectionMode = aEvent.Parameter(); + } + + SHAPE_LINE_CHAIN points; + + if( selectionMode != SELECTION_MODE::TOUCHING_PATH ) + points.SetClosed( true ); + + KIGFX::PREVIEW::SELECTION_AREA area; + view()->Add( &area ); + view()->SetVisible( &area, true ); + + controls()->SetAutoPan( true ); + + frame()->PushTool( aEvent ); + Activate(); + + while( TOOL_EVENT* evt = Wait() ) + { + if( !fixedMode ) + { + // Auto Mode: The selection mode depends on the drawing direction of the selection shape: + // - Clockwise: Contained selection + // - Counterclockwise: Touching selection + double shapeArea = area.GetPoly().Area( false ); + bool isClockwise = shapeArea > 0 ? true : false; + + // Flip the selection mode if the view is mirrored, but only if the area is non-zero. + // A zero area means the selection shape is a line, so the mode should always be "touching". + if( view()->IsMirroredX() && shapeArea != 0 ) + isClockwise = !isClockwise; + + selectionMode = isClockwise ? SELECTION_MODE::INSIDE_LASSO + : SELECTION_MODE::TOUCHING_LASSO; + } + + if( evt->IsCancelInteractive() || evt->IsActivate() ) + { + // Cancel the selection + cancelled = true; + evt->SetPassEvent( false ); + + break; + } + else if( evt->IsDrag( BUT_LEFT ) // Lasso selection + || evt->IsClick( BUT_LEFT ) // Polygon selection + || evt->IsAction( &ACTIONS::cursorClick ) ) // Return key + { + // Add a point to the selection shape + points.Append( evt->Position() ); + } + else if( evt->IsDblClick( BUT_LEFT ) + || evt->IsAction( &ACTIONS::cursorDblClick ) // End key + || evt->IsAction( &ACTIONS::finishInteractive ) ) + { + // Finish the selection + area.GetPoly().GenerateBBoxCache(); + + selectionTool->SelectMultiple( area, subtractive, exclusiveOr ); + + evt->SetPassEvent( false ); + + break; + } + else if( evt->IsAction( &PCB_ACTIONS::deleteLastPoint ) + || evt->IsAction( &ACTIONS::doDelete ) + || evt->IsAction( &ACTIONS::undo ) ) + { + // Delete the last point in the selection shape + if( points.GetPointCount() > 0 ) + { + controls()->SetCursorPosition( points.CLastPoint() ); + points.Remove( points.GetPointCount() - 1 ); + } + } + else + { + // Allow some actions for navigation + passEvent( evt, allowedActions ); + } + + if( points.PointCount() > 0 ) + { + // Clear existing selection if not in add/sub/xor mode + if( !additive && !subtractive && !exclusiveOr ) + { + if( !selectionTool->GetSelection().Empty() ) + { + selectionTool->ClearSelection( true /*quiet mode*/ ); + m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent ); + } + } + } + + // Draw selection shape + area.SetPoly( points ); + area.GetPoly().Append( m_toolMgr->GetMousePosition() ); + + area.SetAdditive( additive ); + area.SetSubtractive( subtractive ); + area.SetExclusiveOr( exclusiveOr ); + area.SetMode( selectionMode ); + + view()->Update( &area ); + } + + frame()->PopTool( aEvent ); + + controls()->SetAutoPan( false ); + view()->SetVisible( &area, false ); + view()->Remove( &area ); // Stop drawing the selection shape + frame()->GetCanvas()->SetCurrentCursor( KICURSOR::ARROW ); // Reset cursor to default + + if( !cancelled ) + selectionTool->GetSelection().ClearReferencePoint(); + + m_toolMgr->ProcessEvent( EVENTS::UninhibitSelectionEditing ); + + return cancelled; +} + + +void PCB_LASSO_SELECTION_TOOL::setTransitions() +{ + Go( &PCB_LASSO_SELECTION_TOOL::SelectPolyArea, ACTIONS::selectInsideLasso.MakeEvent() ); + Go( &PCB_LASSO_SELECTION_TOOL::SelectPolyArea, ACTIONS::selectTouchingLasso.MakeEvent() ); + Go( &PCB_LASSO_SELECTION_TOOL::SelectPolyArea, ACTIONS::selectAutoLasso.MakeEvent() ); + Go( &PCB_LASSO_SELECTION_TOOL::SelectPolyArea, ACTIONS::selectTouchingPath.MakeEvent() ); +} diff --git a/pcbnew/tools/pcb_selection_tool.h b/pcbnew/tools/pcb_selection_tool.h index 1a74484491..81e86e1c3a 100644 --- a/pcbnew/tools/pcb_selection_tool.h +++ b/pcbnew/tools/pcb_selection_tool.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +120,19 @@ public: ///< Unselect all items on the board int UnselectAll( const TOOL_EVENT& aEvent ); + /** + * Handles drawing a selection box that allows multiple items to be selected simultaneously. + * + * @return true if the operation was canceled (i.e. a CancelEvent was received). + */ + int SelectRectArea( const TOOL_EVENT& aEvent ); + + /** + * Selects multiple PCB items within a specified area. + */ + void SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, bool aSubtractive = false, + bool aExclusiveOr = false ); + /** * Take necessary actions to mark an item as found. * @@ -295,13 +309,6 @@ private: bool selectCursor( bool aForceSelect = false, CLIENT_SELECTION_FILTER aClientFilter = nullptr ); - /** - * Handle drawing a selection box that allows one to select many items at the same time. - * - * @return true if the function was canceled (i.e. CancelEvent was received). - */ - bool selectMultiple(); - bool selectTableCells( PCB_TABLE* aTable ); /** @@ -475,4 +482,48 @@ private: std::unique_ptr m_priv; }; +/** + * The PCB_LASSO_SELECTION_TOOL is a tool that allows the user to select multiple items + * by drawing a polygon, lasso or polyline on the PCB. + * It is used for selecting items in a more flexible way than the standard rectangle selection. + */ +class PCB_LASSO_SELECTION_TOOL : public PCB_TOOL_BASE +{ +public: + PCB_LASSO_SELECTION_TOOL(); + ~PCB_LASSO_SELECTION_TOOL(); + + /// @copydoc TOOL_BASE::Init() + bool Init() override; + + /// @copydoc TOOL_BASE::Reset() + void Reset( RESET_REASON aReason ) override; + + ///< Set up handlers for various events. + void setTransitions() override; + + /** + * Handles drawing a selection polygon (lasso) or polyline (path) that allows multiple items + * to be selectedsimultaneously. + * @return true if the operation was canceled (i.e. a CancelEvent was received). + */ + int SelectPolyArea( const TOOL_EVENT& aEvent ); + +protected: + KIGFX::PCB_VIEW* view() const + { + return static_cast( getView() ); + } + + KIGFX::VIEW_CONTROLS* controls() const + { + return getViewControls(); + } + + PCB_BASE_FRAME* frame() const + { + return getEditFrame(); + } +}; + #endif /* PCB_SELECTION_TOOL_H */ diff --git a/pcbnew/zone.cpp b/pcbnew/zone.cpp index c3847b338b..3db309f437 100644 --- a/pcbnew/zone.cpp +++ b/pcbnew/zone.cpp @@ -765,6 +765,52 @@ bool ZONE::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) const } +bool ZONE::HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const +{ + if( aContained ) + { + auto outlineIntersectingSelection = + [&]() + { + for( auto segment = m_Poly->IterateSegments(); segment; segment++ ) + { + if( aPoly.Intersects( *segment ) ) + return true; + } + + return false; + }; + + // In the case of contained selection, all vertices of the zone outline must be inside + // the selection polygon, so we can check only the first vertex. + auto vertexInsideSelection = + [&]() + { + return aPoly.PointInside( m_Poly->CVertex( 0 ) ); + }; + + return vertexInsideSelection() && !outlineIntersectingSelection(); + } + else + { + // Touching selection - check if any segment of the zone contours collides with the + // selection shape. + for( auto segment = m_Poly->IterateSegmentsWithHoles(); segment; segment++ ) + { + if( aPoly.PointInside( ( *segment ).A ) ) + return true; + + if( aPoly.Intersects( *segment ) ) + return true; + + // Note: aPoly.Collide() could be used instead of two test above, but it is 3x slower. + } + + return false; + } +} + + std::optional ZONE::GetLocalClearance() const { return m_isRuleArea ? 0 : m_ZoneClearance; diff --git a/pcbnew/zone.h b/pcbnew/zone.h index 550be40141..f0462824af 100644 --- a/pcbnew/zone.h +++ b/pcbnew/zone.h @@ -448,10 +448,15 @@ public: SHAPE_POLY_SET::VERTEX_INDEX* aCornerHit = nullptr ) const; /** - * @copydoc BOARD_ITEM::HitTest(const BOX2I& aRect, bool aContained, int aAccuracy) const + * @copydoc EDA_ITEM::HitTest(const BOX2I& aRect, bool aContained, int aAccuracy) const */ bool HitTest( const BOX2I& aRect, bool aContained = true, int aAccuracy = 0 ) const override; + /** + * @copydoc EDA_ITEM::HitTest(const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const + */ + bool HitTest( const SHAPE_LINE_CHAIN& aPoly, bool aContained ) const; + /** * Removes the zone filling. * diff --git a/qa/qa_utils/mocks.cpp b/qa/qa_utils/mocks.cpp index 8033a257f5..8f2041bda6 100644 --- a/qa/qa_utils/mocks.cpp +++ b/qa/qa_utils/mocks.cpp @@ -38,6 +38,7 @@ #include #include #include +#include FP_LIB_TABLE GFootprintTable; @@ -262,9 +263,9 @@ bool PCB_SELECTION_TOOL::selectCursor( bool aForceSelect, CLIENT_SELECTION_FILTE } -bool PCB_SELECTION_TOOL::selectMultiple() +void PCB_SELECTION_TOOL::SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, bool aSubtractive, + bool aExclusiveOr ) { - return false; }