KiCad PCB EDA Suite
Loading...
Searching...
No Matches
graphic_edit_tool.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <board_commit.h>
23#include <board.h>
25#include <gal/painter.h>
26#include <geometry/circle.h>
27#include <geometry/seg.h>
28#include <pcb_shape.h>
30#include <render_settings.h>
31#include <tool/actions.h>
32#include <tool/tool_manager.h>
34#include <tools/graphic_trim.h>
35#include <tools/hover_picker.h>
36#include <tools/pcb_actions.h>
39
40#include <algorithm>
41#include <cmath>
42#include <limits>
43
46static double outlineProximity( const BOARD_ITEM& aItem, const VECTOR2I& aPointer )
47{
48 const PCB_SHAPE* shape = GraphicEditShape( &aItem );
49
50 if( !shape )
51 return std::numeric_limits<double>::infinity();
52
53 switch( shape->GetShape() )
54 {
56 return SEG( shape->GetStart(), shape->GetEnd() ).Distance( aPointer );
57
58 case SHAPE_T::CIRCLE:
59 return std::abs( shape->GetCenter().Distance( aPointer ) - (double) shape->GetRadius() );
60
61 case SHAPE_T::ARC:
62 {
63 SHAPE_ARC arc = GraphicEditArc( *shape );
64
65 return arc.NearestPoint( aPointer ).Distance( aPointer );
66 }
67
69 {
70 std::vector<VECTOR2I> corners = shape->GetRectCorners();
71 double nearest = std::numeric_limits<double>::infinity();
72
73 for( size_t i = 0; i < corners.size(); i++ )
74 {
75 SEG side( corners[i], corners[( i + 1 ) % corners.size()] );
76 nearest = std::min( nearest, (double) side.Distance( aPointer ) );
77 }
78
79 return nearest;
80 }
81
82 default:
83 return std::numeric_limits<double>::infinity();
84 }
85}
86
87
88static void applyGeometry( PCB_SHAPE& aShape, const GRAPHIC_EDIT_GEOMETRY& aGeometry )
89{
90 if( aShape.GetShape() != aGeometry.m_Shape )
91 {
92 // A shape that has been opened up has no inside left to fill.
93 aShape.SetShape( aGeometry.m_Shape );
95 }
96
97 if( aGeometry.m_Shape == SHAPE_T::ARC )
98 {
99 aShape.SetArcGeometry( aGeometry.m_Start, aGeometry.m_Mid, aGeometry.m_End );
100 }
101 else
102 {
103 aShape.SetStart( aGeometry.m_Start );
104 aShape.SetEnd( aGeometry.m_End );
105 }
106}
107
108
110{
111 if( aGeometry.m_Shape == SHAPE_T::ARC )
112 return SHAPE_ARC( aGeometry.m_Start, aGeometry.m_Mid, aGeometry.m_End, 0 );
113
114 // A circle carries its centre in m_Start and a point on the rim in m_End.
115 if( aGeometry.m_Shape == SHAPE_T::CIRCLE )
116 return CIRCLE( aGeometry.m_Start, aGeometry.m_Start.Distance( aGeometry.m_End ) );
117
118 return SEG( aGeometry.m_Start, aGeometry.m_End );
119}
120
121
123static wxString refusalMessage( GRAPHIC_EDIT_REFUSAL aRefusal, const wxString& aNoResult )
124{
125 switch( aRefusal )
126 {
128 return _( "More than one shape meets this one here." );
130 return _( "That shape is too small or too near a full circle to work with." );
132 return _( "That shape is locked." );
133 default:
134 return aNoResult;
135 }
136}
137
138
140 PCB_TOOL_BASE( "pcbnew.GraphicEdit" )
141{
142}
143
144
146{
148
149 // Menu entries live in EDIT_TOOL's Shape Modification submenu.
150
151 return true;
152}
153
154
156{
157 return runInteractive( aEvent,
158 { .m_NoResult = _( "There is nothing to reach in that direction." ),
159 .m_CommitDescription = _( "Extend Line or Arc" ),
160 .m_Accepts = IsGraphicExtendSource,
161 .m_PreviewLayer = LAYER_AUX_ITEMS,
163 .m_Plan = GRAPHIC_EXTEND_PLANNER::Plan } );
164}
165
166
168{
169 return runInteractive( aEvent,
170 { .m_NoResult = _( "Nothing crosses the shape here." ),
171 .m_CommitDescription = _( "Trim Shape" ),
172 .m_Accepts = IsGraphicTrimSource,
173 .m_SuppressedSnaps = { SNAP_CANDIDATE_SUBTYPE::INTERSECTION,
175 .m_PreviewLayer = LAYER_DRC_ERROR,
176 .m_QueryBounds = nullptr,
177 .m_Plan = GRAPHIC_TRIM_PLANNER::Plan } );
178}
179
180
181int GRAPHIC_EDIT_TOOL::runInteractive( const TOOL_EVENT& aEvent, const OPERATION& aOperation )
182{
183 PCB_PICKER_TOOL* picker = m_toolMgr->GetTool<PCB_PICKER_TOOL>();
185 HOVER_PICKER hover( m_toolMgr );
186 bool done = false;
187
188 // Nothing is picked up front, so a leftover selection would only be a distraction.
190
191 // Only bounds the extension ray. A snapshot is enough if the board grows.
192 const BOX2I worldBounds = board()->GetBoundingBox();
193
194 // A locked shape is still hovered. The planner refuses it by name, which tells the user
195 // more than nothing lighting up.
196 auto hoveredSource =
197 [&]( const VECTOR2I& aPointer ) -> PCB_SHAPE*
198 {
199 auto accepts =
200 [&]( BOARD_ITEM& aItem )
201 {
202 const PCB_SHAPE* shape = GraphicEditShape( &aItem );
203
204 return shape && aOperation.m_Accepts( *shape );
205 };
206
207 return static_cast<PCB_SHAPE*>( hover.Pick( aPointer, accepts, outlineProximity ) );
208 };
209
210 auto collectBoundaries =
211 [&]( const BOX2I& aQueryBounds, const PCB_SHAPE& aSource )
212 {
213 std::set<const BOARD_ITEM*> seen;
214 std::vector<const BOARD_ITEM*> boundaries;
215 const PCB_LAYER_ID layer = aSource.GetLayer();
216
217 // Selectable() is asked for visibility only, because we do want to include footprint graphics,
218 // even though the board editor will not consider them selectable. Similarly, we also ignore
219 // the currently entered group (if any). Selectable() is the costliest test, so it goes last.
220 view()->Query( aQueryBounds,
221 [&]( KIGFX::VIEW_ITEM* aViewItem )
222 {
223 if( !aViewItem->IsBOARD_ITEM() )
224 return true;
225
226 BOARD_ITEM* item = static_cast<BOARD_ITEM*>( aViewItem );
227
228 if( item != &aSource
229 && item->GetLayer() == layer
230 && GraphicEditShape( item )
231 && view()->IsVisible( item )
232 && m_selectionTool->Selectable( item, true /* checkVisibilityOnly */ )
233 && seen.insert( item ).second )
234 {
235 boundaries.push_back( item );
236 }
237
238 return true;
239 } );
240
241 return boundaries;
242 };
243
244 auto resetPreview =
245 [&]()
246 {
247 hover.ClearBrightening();
248 preview.ClearDrawables();
249 view()->Update( &preview );
250 };
251
252 // Returns the source it planned against, so the click handler can commit to the same one.
253 auto updatePreview =
254 [&]( const VECTOR2I& aPointer, GRAPHIC_EDIT_RESULT& aResult ) -> PCB_SHAPE*
255 {
256 PCB_SHAPE* source = hoveredSource( aPointer );
257
258 preview.ClearDrawables();
259
260 if( !source )
261 {
262 aResult = {};
263 hover.ClearBrightening();
264 view()->Update( &preview );
265 return nullptr;
266 }
267
268 BOX2I queryBounds = source->GetBoundingBox();
269
270 if( aOperation.m_QueryBounds )
271 {
272 BOX2I rayBounds = worldBounds;
273
274 rayBounds.Merge( queryBounds );
275 queryBounds = aOperation.m_QueryBounds( *source, aPointer, rayBounds );
276 }
277
278 if( !queryBounds.IsValid() )
279 {
280 aResult = {};
281 hover.BrightenOnly( { source } );
282 view()->Update( &preview );
283 return source;
284 }
285
286 std::vector<const BOARD_ITEM*> candidates = collectBoundaries( queryBounds, *source );
287
288 aResult = aOperation.m_Plan( *source, aPointer, candidates );
289
290 // The shape under the pointer is worth marking whether or not it can be edited here.
291 std::vector<BOARD_ITEM*> lit{ source };
292
293 if( aResult )
294 {
295 const std::vector<GRAPHIC_EDIT_GEOMETRY>& shown = aResult.m_Preview.empty() ? aResult.m_Geometry
296 : aResult.m_Preview;
297
298 for( const GRAPHIC_EDIT_GEOMETRY& geometry : shown )
299 preview.AddDrawable( drawable( geometry ), false, 4 );
300
301 for( const BOARD_ITEM* boundary : aResult.m_Boundaries )
302 lit.push_back( const_cast<BOARD_ITEM*>( boundary ) );
303 }
304
305 hover.BrightenOnly( lit );
306 view()->Update( &preview );
307 return source;
308 };
309
311 KIGFX::COLOR4D previewColor = settings->GetLayerColor( aOperation.m_PreviewLayer );
312
313 // The borrowed colour is not always a good choice. Compare with the background.
314 if( view()->GetGAL()->GetClearColor().Distance( previewColor ) < 0.5 )
315 previewColor.Invert();
316
317 preview.SetColor( previewColor );
318
319 Activate();
320
321 // The selection tool arms its disambiguation on button-down unless a tool owns the stack.
322 // Without this the committing click also selects, which cancels the picker.
323 frame()->PushTool( aEvent );
324
325 view()->Add( &preview );
326 picker->SetCursor( KICURSOR::BULLSEYE );
327
328 // Snapping on is what makes the picker honour the modifiers that turn it off again, so
329 // <shift> and <ctrl> give a finer aim among crowded items.
330 picker->SetSnapping( true );
331
332 // Nothing here follows an extension, so the lines the snap system draws are just noise.
333 picker->SetConstructionGeometry( false );
334 picker->SetSuppressedSnaps( aOperation.m_SuppressedSnaps );
335 picker->ClearHandlers();
336 picker->SetMotionHandler(
337 [&]( const VECTOR2D& aPointer )
338 {
340
341 updatePreview( aPointer, result );
342 } );
343 picker->SetClickHandler(
344 [&]( const VECTOR2D& aPointer )
345 {
347 PCB_SHAPE* source = updatePreview( aPointer, result );
348
349 if( !source )
350 return true;
351
352 if( !result )
353 {
354 frame()->ShowInfoBarError( refusalMessage( result.m_Refusal, aOperation.m_NoResult ) );
355 return true;
356 }
357
358 // The commit may free the source, so let go of it first.
359 hover.ClearBrightening();
360 preview.ClearDrawables();
361 view()->Update( &preview );
362
363 BOARD_COMMIT commit( this );
364
365 if( result.m_Geometry.empty() )
366 {
367 commit.Remove( source );
368 }
369 else
370 {
371 std::vector<PCB_SHAPE*> pieces;
372
373 // Duplicate off the untouched source, before its own geometry is replaced.
374 for( size_t i = 1; i < result.m_Geometry.size(); i++ )
375 {
376 PCB_SHAPE* piece = static_cast<PCB_SHAPE*>( source->Duplicate( true, &commit ) );
377
378 piece->ClearSelected();
379 pieces.push_back( piece );
380 }
381
382 commit.Modify( source );
383 applyGeometry( *source, result.m_Geometry.front() );
384
385 for( size_t i = 0; i < pieces.size(); i++ )
386 {
387 applyGeometry( *pieces[i], result.m_Geometry[i + 1] );
388 commit.Add( pieces[i] );
389 }
390 }
391
392 commit.Push( aOperation.m_CommitDescription );
393
394 updatePreview( aPointer, result );
395 return true;
396 } );
397 picker->SetCancelHandler( resetPreview );
398 picker->SetFinalizeHandler(
399 [&]( const int& )
400 {
401 resetPreview();
402 done = true;
403 } );
404 m_toolMgr->RunAction( ACTIONS::pickerSubTool );
405
406 while( !done )
407 {
408 TOOL_EVENT* event = Wait();
409
410 if( !event )
411 break;
412
413 // Nothing is cached across events. Model changes and undo need no handling. The next
414 // motion queries the board as it stands.
415 event->SetPassEvent();
416 }
417
418 picker->ClearHandlers();
419 resetPreview();
420 view()->Remove( &preview );
421 frame()->PopTool( aEvent );
422 return 0;
423}
424
425
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
static TOOL_ACTION pickerSubTool
Definition actions.h:250
static TOOL_ACTION selectionClear
Clear the current selection.
Definition actions.h:220
virtual void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
virtual BOARD_ITEM * Duplicate(bool addToParentGroup, BOARD_COMMIT *aCommit=nullptr) const
Create a copy of this BOARD_ITEM.
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
Definition board.h:1265
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:653
constexpr bool IsValid() const
Definition box2.h:914
COMMIT & Remove(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Remove a new item from the model.
Definition commit.h:86
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
Definition commit.h:102
COMMIT & Add(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Add a new item to the model.
Definition commit.h:74
void ClearSelected()
Definition eda_item.h:153
int GetRadius() const
SHAPE_T GetShape() const
Definition eda_shape.h:175
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:325
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:275
std::vector< VECTOR2I > GetRectCorners() const
void SetFillMode(FILL_T aFill)
void setTransitions() override
This method is meant to be overridden in order to specify handlers for events.
int Trim(const TOOL_EVENT &aEvent)
PCB_SELECTION_TOOL * m_selectionTool
bool Init() override
Init() is called once upon a registration of the tool.
int Extend(const TOOL_EVENT &aEvent)
int runInteractive(const TOOL_EVENT &aEvent, const OPERATION &aOperation)
The board item under the pointer, and the highlight that follows it.
void BrightenOnly(const std::vector< BOARD_ITEM * > &aItems)
Light exactly these, leaving what is already lit alone.
void ClearBrightening()
Items go back by id, so a commit that replaced one still puts the original back.
BOARD_ITEM * Pick(const VECTOR2I &aPointer, const ACCEPTS &aAccepts, const PROXIMITY &aProximity=nullptr) const
The item a click would take, or null.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
COLOR4D & Invert()
Makes the color inverted, alpha remains the same.
Definition color4d.h:239
Shows construction geometry for things like line extensions, arc centers, etc.
std::variant< SEG, LINE, HALF_LINE, CIRCLE, SHAPE_ARC, VECTOR2I > DRAWABLE
virtual RENDER_SETTINGS * GetSettings()=0
Return a pointer to current settings that are going to be used when drawing items.
virtual void Update(const VIEW_ITEM *aItem, int aUpdateFlags) const override
For dynamic VIEWs, inform the associated VIEW that the graphical representation of this item has chan...
Definition pcb_view.cpp:87
virtual void Add(VIEW_ITEM *aItem, int aDrawPriority=-1) override
Add a VIEW_ITEM to the view.
Definition pcb_view.cpp:53
virtual void Remove(VIEW_ITEM *aItem) override
Remove a VIEW_ITEM from the view.
Definition pcb_view.cpp:70
Container for all the knowledge about how graphical objects are drawn on any output surface/device.
const COLOR4D & GetLayerColor(int aLayer) const
Return the color used to draw a layer.
An abstract base class for deriving all objects that can be added to a VIEW.
Definition view_item.h:82
bool IsBOARD_ITEM() const
Definition view_item.h:98
int Query(const BOX2I &aRect, std::vector< LAYER_ITEM_PAIR > &aResult) const
Find all visible items that touch or are within the rectangle aRect.
Definition view.cpp:505
PAINTER * GetPainter() const
Return the painter object used by the view for drawing #VIEW_ITEMS.
Definition view.h:225
static TOOL_ACTION extendGraphic
Extend one graphical line or arc to the nearest boundary.
static TOOL_ACTION trimGraphic
Trim a section from one graphical line or arc.
Generic tool for picking an item.
void SetSuppressedSnaps(std::set< SNAP_CANDIDATE_SUBTYPE > aSubtypes)
Snap kinds this run never wants.
void SetConstructionGeometry(bool aEnable)
Whether the snap system may draw its explanatory geometry over the board this run.
void ClearHandlers()
Handlers only.
The selection tool: currently supports:
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
VECTOR2I GetCenter() const override
This defaults to the center of the bounding box if not overridden.
Definition pcb_shape.h:78
void SetShape(SHAPE_T aShape) override
Definition pcb_shape.h:207
void SetEnd(const VECTOR2I &aEnd) override
void SetArcGeometry(const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd)
void SetStart(const VECTOR2I &aStart) override
T * frame() const
KIGFX::PCB_VIEW * view() const
PCB_TOOL_BASE(TOOL_ID aId, const std::string &aName)
Constructor.
BOARD * board() const
void SetMotionHandler(MOTION_HANDLER aHandler)
Set a handler for mouse motion.
Definition picker_tool.h:92
void SetClickHandler(CLICK_HANDLER aHandler)
Set a handler for mouse click event.
Definition picker_tool.h:81
void SetSnapping(bool aSnap)
Definition picker_tool.h:65
void SetCursor(KICURSOR aCursor)
Definition picker_tool.h:63
void SetCancelHandler(CANCEL_HANDLER aHandler)
Set a handler for cancel events (ESC or context-menu Cancel).
void SetFinalizeHandler(FINALIZE_HANDLER aHandler)
Set a handler for the finalize event.
Definition seg.h:38
int Distance(const SEG &aSeg) const
Compute minimum Euclidean distance to segment aSeg.
Definition seg.cpp:709
VECTOR2I NearestPoint(const VECTOR2I &aP) const
TOOL_MANAGER * m_toolMgr
Definition tool_base.h:220
Generic, UI-independent tool event.
Definition tool_event.h:167
void Go(int(T::*aStateFunc)(const TOOL_EVENT &), const TOOL_EVENT_LIST &aConditions=TOOL_EVENT(TC_ANY, TA_ANY))
Define which state (aStateFunc) to go when a certain event arrives (aConditions).
TOOL_EVENT * Wait(const TOOL_EVENT_LIST &aEventList=TOOL_EVENT(TC_ANY, TA_ANY))
Suspend execution of the tool until an event specified in aEventList arrives.
void Activate()
Run the tool.
double Distance(const VECTOR2< extended_type > &aVector) const
Compute the distance between two vectors.
Definition vector2d.h:549
@ BULLSEYE
Definition cursors.h:54
#define _(s)
@ NO_FILL
Definition eda_fill.h:30
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
SHAPE_ARC GraphicEditArc(const PCB_SHAPE &aShape)
bool IsGraphicTrimSource(const PCB_SHAPE &aShape)
Trim also takes the closed shapes, which it opens up.
bool IsGraphicExtendSource(const PCB_SHAPE &aShape)
Extend needs two ends to work with.
const PCB_SHAPE * GraphicEditShape(const EDA_ITEM *aItem)
Any graphical shape a planner or a boundary search might use. Anything else gives null.
GRAPHIC_EDIT_REFUSAL
Why a planner refused. Each one gets its own message.
static double outlineProximity(const BOARD_ITEM &aItem, const VECTOR2I &aPointer)
How far aPointer is from the shape's outline.
static KIGFX::CONSTRUCTION_GEOM::DRAWABLE drawable(const GRAPHIC_EDIT_GEOMETRY &aGeometry)
static void applyGeometry(PCB_SHAPE &aShape, const GRAPHIC_EDIT_GEOMETRY &aGeometry)
static wxString refusalMessage(GRAPHIC_EDIT_REFUSAL aRefusal, const wxString &aNoResult)
aNoResult carries wording only the operation can supply.
@ LAYER_AUX_ITEMS
Auxiliary items (guides, rule, etc).
Definition layer_ids.h:279
@ LAYER_DRC_ERROR
Layer for DRC markers with #SEVERITY_ERROR.
Definition layer_ids.h:273
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
BOX2I QueryBounds(const BOARD_ITEM &aSource, GRAPHIC_ENDPOINT aEndpoint, const BOX2I &aWorldBounds)
GRAPHIC_EDIT_RESULT Plan(const BOARD_ITEM &aSource, GRAPHIC_ENDPOINT aEndpoint, const std::vector< const BOARD_ITEM * > &aBoundaries)
GRAPHIC_EDIT_RESULT Plan(const BOARD_ITEM &aSource, const VECTOR2I &aPointer, const std::vector< const BOARD_ITEM * > &aBoundaries)
Plan removal of the part of aSource under aPointer.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
SHAPE_T m_Shape
A result need not be the same kind as the source.
VECTOR2I m_Mid
Arcs only. Everything else leaves it default.
What one edit operation gives the shared interactive loop.
std::set< SNAP_CANDIDATE_SUBTYPE > m_SuppressedSnaps
Snap kinds this operation cannot use.
bool(* m_Accepts)(const PCB_SHAPE &aShape)
Which shapes may be the source. Anything else is not worth hovering.
GRAPHIC_EDIT_RESULT(* m_Plan)(const BOARD_ITEM &aSource, const VECTOR2I &aPointer, const std::vector< const BOARD_ITEM * > &aBoundaries)
BOX2I(* m_QueryBounds)(const BOARD_ITEM &aSource, const VECTOR2I &aPointer, const BOX2I &aWorldBounds)
Null bounds the search by the source's own extent, which only cuts what it crosses.
int m_PreviewLayer
The colour role the preview borrows. Trim marks a removal, extend an addition.
wxString result
Test unit parsing edge cases and error handling.
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682