KiCad PCB EDA Suite
Loading...
Searching...
No Matches
construction_manager.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU 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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <chrono>
27
28#include <wx/timer.h>
29
30#include <advanced_config.h>
31#include <hash.h>
32
33
45template <typename T>
47{
48public:
49 using ACTIVATION_CALLBACK = std::function<void( T&& )>;
50
51 ACTIVATION_HELPER( std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback ) :
52 m_timeout( aTimeout ),
53 m_callback( std::move( aCallback ) )
54 {
55 m_timer.Bind( wxEVT_TIMER, &ACTIVATION_HELPER::onTimerExpiry, this );
56 }
57
59 {
60 // Hold the lock while shutting down to prevent a propoal being accepted
61 // while state is being destroyed.
62 std::unique_lock<std::mutex> lock( m_mutex );
63 m_timer.Stop();
64 m_timer.Unbind( wxEVT_TIMER, &ACTIVATION_HELPER::onTimerExpiry, this );
65
66 // Should be redundant to inhibiting timer callbacks, but make it explicit.
68 }
69
70 void ProposeActivation( T&& aProposal, std::size_t aProposalTag, bool aAcceptImmediately )
71 {
72 std::unique_lock<std::mutex> lock( m_mutex );
73
74 if( m_lastAcceptedProposalTag.has_value() && aProposalTag == *m_lastAcceptedProposalTag )
75 {
76 // This proposal was accepted last time
77 // (could be made optional if we want to allow re-accepting the same proposal)
78 return;
79 }
80
81 if( m_pendingProposalTag.has_value() && aProposalTag == *m_pendingProposalTag )
82 {
83 // This proposal is already pending
84 return;
85 }
86
87 m_pendingProposalTag = aProposalTag;
88 m_lastProposal = std::move( aProposal );
89
90 if( aAcceptImmediately )
91 {
92 // Synchonously accept the proposal
93 lock.unlock();
95 }
96 else
97 {
98 m_timer.Start( m_timeout.count(), wxTIMER_ONE_SHOT );
99 }
100 }
101
103 {
104 std::lock_guard<std::mutex> lock( m_mutex );
105 m_pendingProposalTag.reset();
106 m_timer.Stop();
107 }
108
109private:
113 void onTimerExpiry( wxTimerEvent& aEvent )
114 {
116 }
117
119 {
120 std::unique_lock<std::mutex> lock( m_mutex );
121
123 {
125 m_pendingProposalTag.reset();
126
127 // Move out from the locked variable
128 T proposalToAccept = std::move( m_lastProposal );
129 lock.unlock();
130
131 // Call the callback (outside the lock)
132 // This is all in the UI thread now, so it won't be concurrent
133 m_callback( std::move( proposalToAccept ) );
134 }
135 }
136
137 mutable std::mutex m_mutex;
138
140 std::chrono::milliseconds m_timeout;
141
143 std::optional<std::size_t> m_pendingProposalTag;
144
146 std::optional<std::size_t> m_lastAcceptedProposalTag;
147
150
153
154 wxTimer m_timer;
155};
156
157
159{
162};
163
164
166 m_viewHandler( aHelper )
167{
168 const std::chrono::milliseconds acceptanceTimeout(
169 ADVANCED_CFG::GetCfg().m_ExtensionSnapTimeoutMs );
170
171 m_activationHelper = std::make_unique<ACTIVATION_HELPER<std::unique_ptr<PENDING_BATCH>>>(
172 acceptanceTimeout,
173 [this]( std::unique_ptr<PENDING_BATCH>&& aAccepted )
174 {
175 // This shouldn't be possible (probably indicates a race in destruction of something)
176 // but at least avoid blowing up acceptConstructionItems.
177 wxCHECK_MSG( aAccepted != nullptr, void(), "Null proposal accepted" );
178
179 acceptConstructionItems( std::move( aAccepted ) );
180 } );
181}
182
183
185{
186}
187
188
192static std::size_t
194 bool aIsPersistent )
195{
196 std::size_t hash = hash_val( aIsPersistent );
197
198 for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& item : aBatch )
199 {
200 hash_combine( hash, item.Source, item.Item );
201 }
202 return hash;
203}
204
205
207 std::unique_ptr<CONSTRUCTION_ITEM_BATCH> aBatch, bool aIsPersistent )
208{
209 if( aBatch->empty() )
210 {
211 // There's no point in proposing an empty batch
212 // It would just clear existing construction items for nothing new
213 return;
214 }
215
216 bool acceptImmediately = false;
217
218 {
219 std::lock_guard<std::mutex> lock( m_batchesMutex );
220
221 if( aIsPersistent )
222 {
223 acceptImmediately = true;
224 }
225 else
226 {
227 // If the batch is temporary, we can accept it immediately if there's room
228 acceptImmediately = m_temporaryConstructionBatches.size() < getMaxTemporaryBatches();
229 }
230 }
231
232 auto pendingBatch =
233 std::make_unique<PENDING_BATCH>( PENDING_BATCH{ std::move( *aBatch ), aIsPersistent } );
234 const std::size_t hash = HashConstructionBatchSources( pendingBatch->Batch, aIsPersistent );
235
236 // Immediate or not, propose the batch via the activation helper as this handles duplicates
237 m_activationHelper->ProposeActivation( std::move( pendingBatch ), hash, acceptImmediately );
238}
239
240
242{
243 m_activationHelper->CancelProposal();
244}
245
246
248{
249 // We only keep up to one previous temporary batch and the current one
250 // we could make this a setting if we want to keep more, but it gets cluttered
251 return 2;
252}
253
254
255void CONSTRUCTION_MANAGER::acceptConstructionItems( std::unique_ptr<PENDING_BATCH> aAcceptedBatch )
256{
257 const auto getInvolved = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd )
258 {
259 for( const CONSTRUCTION_ITEM& item : aBatchToAdd )
260 {
261 // Only show the item if it's not already involved
262 // (avoid double-drawing the same item)
263 if( m_involvedItems.count( item.Item ) == 0 )
264 {
265 m_involvedItems.insert( item.Item );
266 }
267 }
268 };
269
270 // Copies for use outside the lock
271 std::vector<CONSTRUCTION_ITEM_BATCH> persistentBatches, temporaryBatches;
272 {
273 std::lock_guard<std::mutex> lock( m_batchesMutex );
274
275 if( aAcceptedBatch->IsPersistent )
276 {
277 // We only keep one previous persistent batch for the moment
278 m_persistentConstructionBatch = std::move( aAcceptedBatch->Batch );
279 }
280 else
281 {
282 bool anyNewItems = false;
283 for( CONSTRUCTION_ITEM& item : aAcceptedBatch->Batch )
284 {
285 if( m_involvedItems.count( item.Item ) == 0 )
286 {
287 anyNewItems = true;
288 break;
289 }
290 }
291
292 // If there are no new items involved, don't bother adding the batch
293 if( !anyNewItems )
294 {
295 return;
296 }
297
299 {
301 }
302
303 m_temporaryConstructionBatches.emplace_back( std::move( aAcceptedBatch->Batch ) );
304 }
305
306 m_involvedItems.clear();
307
308 // Copy the batches for use outside the lock
310 {
311 getInvolved( *m_persistentConstructionBatch );
312 persistentBatches.push_back( *m_persistentConstructionBatch );
313 }
314
316 {
317 getInvolved( batch );
318 temporaryBatches.push_back( batch );
319 }
320 }
321
323 geom.ClearDrawables();
324
325 const auto addDrawables =
326 [&]( const std::vector<CONSTRUCTION_ITEM_BATCH>& aBatches, bool aIsPersistent )
327 {
328 for( const CONSTRUCTION_ITEM_BATCH& batch : aBatches )
329 {
330 for( const CONSTRUCTION_ITEM& item : batch )
331 {
332 for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable : item.Constructions )
333 {
334 geom.AddDrawable( drawable, aIsPersistent );
335 }
336 }
337 }
338 };
339
340 addDrawables( persistentBatches, true );
341 addDrawables( temporaryBatches, false );
342
344}
345
346
347bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
348{
349 for( EDA_ITEM* item : aItems )
350 {
351 // Null items (i.e. construction items) are always considered involved
352 if( item && m_involvedItems.count( item ) == 0 )
353 {
354 return false;
355 }
356 }
357
358 return true;
359}
360
361
363 std::vector<CONSTRUCTION_ITEM_BATCH>& aToExtend ) const
364{
365 std::lock_guard<std::mutex> lock( m_batchesMutex );
367 {
368 aToExtend.push_back( *m_persistentConstructionBatch );
369 }
370
372 {
373 aToExtend.push_back( batch );
374 }
375}
376
377
379{
380 std::lock_guard<std::mutex> lock( m_batchesMutex );
382}
383
384
386 m_viewHandler( aViewHandler )
387{
388}
389
390
392{
393 // Setting the origin clears the snap line as the end point is no longer valid
395 m_snapLineOrigin = aOrigin;
396}
397
398
400{
401 if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
402 {
403 m_snapLineEnd = aSnapEnd;
404
405 if( m_snapLineEnd )
407 else
409
411 }
412}
413
414
416{
417 m_snapLineOrigin.reset();
418 m_snapLineEnd.reset();
421}
422
423
425{
426 if( m_snapLineOrigin.has_value() )
427 {
428 if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
429 {
430 SetSnapLineEnd( aAnchorPos );
431 }
432 else
433 {
434 // Snapped to something that is not the snap line origin, so
435 // this anchor is now the new snap line origin
436 SetSnapLineOrigin( aAnchorPos );
437 }
438 }
439 else
440 {
441 // If there's no snap line, start one
442 SetSnapLineOrigin( aAnchorPos );
443 }
444}
445
446
454static bool pointHasEscapedSnapLineX( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
455 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
456{
457 if( std::abs( aCursor.x - aSnapLineOrigin.x ) < aEscapeRange )
458 {
459 return false;
460 }
461 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin ) + EDA_ANGLE( 90, DEGREES_T );
462 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
463}
464
465
469static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
470 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
471{
472 if( std::abs( aCursor.y - aSnapLineOrigin.y ) < aEscapeRange )
473 {
474 return false;
475 }
476 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin );
477 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
478}
479
480
482 const VECTOR2I& aNearestGrid,
483 std::optional<int> aDistToNearest,
484 int aSnapRange ) const
485{
486 // return std::nullopt;
487 if( m_snapLineOrigin )
488 {
489 bool snapLine = false;
490 VECTOR2I bestSnapPoint = aNearestGrid;
491
492 // If there's no snap anchor, or it's too far away, prefer the grid
493 const bool gridBetterThanNearest = !aDistToNearest || *aDistToNearest > aSnapRange;
494
495 // The escape range is how far you go before the snap line is de-activated.
496 // Make this a bit more forgiving than the snap range, as you can easily cancel
497 // deliberately with a mouse move.
498 // These are both a bit arbitrary, and can be adjusted as preferred
499 const int escapeRange = 2 * aSnapRange;
500 const EDA_ANGLE longRangeEscapeAngle( 4, DEGREES_T );
501
502 const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
503 longRangeEscapeAngle );
504 const bool escapedY = pointHasEscapedSnapLineY( aCursor, *m_snapLineOrigin, escapeRange,
505 longRangeEscapeAngle );
506
509 if( !escapedX && gridBetterThanNearest )
510 {
511 bestSnapPoint.x = m_snapLineOrigin->x;
512 snapLine = true;
513 }
514
515 if( !escapedY && gridBetterThanNearest )
516 {
517 bestSnapPoint.y = m_snapLineOrigin->y;
518 snapLine = true;
519 }
520
521 if( snapLine )
522 {
523 return bestSnapPoint;
524 }
525 }
526
527 return std::nullopt;
528}
529
530
532 CONSTRUCTION_VIEW_HANDLER( aHelper ), m_snapLineManager( *this ),
533 m_constructionManager( *this )
534{
535}
536
537
539{
540 if( m_updateCallback )
541 {
542 bool showAnything = m_constructionManager.HasActiveConstruction()
544
545 m_updateCallback( showAnything );
546 }
547}
548
549
550std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
552{
553 std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> batches;
554
556
557 if( const OPT_VECTOR2I& snapLineOrigin = m_snapLineManager.GetSnapLineOrigin();
558 snapLineOrigin.has_value() )
559 {
561
563 batch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
565 nullptr,
566 {},
567 } );
568
569 // One horizontal and one vertical infinite line from the snap point
570 snapPointItem.Constructions.push_back(
571 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 100000, 0 ) } );
572 snapPointItem.Constructions.push_back(
573 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 0, 100000 ) } );
574
575 batches.push_back( std::move( batch ) );
576 }
577
578 return batches;
579}
A helper class to manage the activation of a "proposal" after a timeout.
void onTimerExpiry(wxTimerEvent &aEvent)
Timer expiry callback in the UI thread.
ACTIVATION_HELPER(std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback)
std::optional< std::size_t > m_lastAcceptedProposalTag
The last proposal that was accepted.
ACTIVATION_CALLBACK m_callback
Callback to call when the proposal is accepted.
std::chrono::milliseconds m_timeout
Activation timeout in milliseconds.
void ProposeActivation(T &&aProposal, std::size_t aProposalTag, bool aAcceptImmediately)
std::optional< std::size_t > m_pendingProposalTag
The last proposal tag that was made.
T m_lastProposal
The most recently-proposed item.
std::function< void(T &&)> ACTIVATION_CALLBACK
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
void GetConstructionItems(std::vector< CONSTRUCTION_ITEM_BATCH > &aToExtend) const
Get the list of additional geometry items that should be considered.
void ProposeConstructionItems(std::unique_ptr< CONSTRUCTION_ITEM_BATCH > aBatch, bool aIsPersistent)
Add a batch of construction items to the helper.
CONSTRUCTION_VIEW_HANDLER & m_viewHandler
CONSTRUCTION_MANAGER(CONSTRUCTION_VIEW_HANDLER &aViewHandler)
void CancelProposal()
Cancel outstanding proposals for new geometry.
std::deque< CONSTRUCTION_ITEM_BATCH > m_temporaryConstructionBatches
Temporary construction items are added and removed as needed.
std::vector< CONSTRUCTION_ITEM > CONSTRUCTION_ITEM_BATCH
std::optional< CONSTRUCTION_ITEM_BATCH > m_persistentConstructionBatch
Within one "operation", there is one set of construction items that are "persistent",...
std::unique_ptr< ACTIVATION_HELPER< std::unique_ptr< PENDING_BATCH > > > m_activationHelper
unsigned getMaxTemporaryBatches() const
How many batches of temporary construction items can be active at once.
std::mutex m_batchesMutex
Protects the persistent and temporary construction batches.
bool InvolvesAllGivenRealItems(const std::vector< EDA_ITEM * > &aItems) const
Check if all 'real' (non-null = constructed) the items in the batch are in the list of items currentl...
void acceptConstructionItems(std::unique_ptr< PENDING_BATCH > aAcceptedBatchHash)
std::set< EDA_ITEM * > m_involvedItems
Set of all items for which construction geometry has been added.
Interface wrapper for the construction geometry preview with a callback to signal the view owner that...
virtual void updateView()=0
KIGFX::CONSTRUCTION_GEOM & GetViewItem()
EDA_ANGLE Normalize90()
Definition: eda_angle.h:252
A base class for most all the KiCad significant classes used in schematics and boards.
Definition: eda_item.h:96
Shows construction geometry for things like line extensions, arc centers, etc.
void AddDrawable(const DRAWABLE &aItem, bool aIsPersistent)
std::variant< SEG, LINE, HALF_LINE, CIRCLE, SHAPE_ARC, VECTOR2I > DRAWABLE
void SetSnapLine(const SEG &aLine)
Definition: line.h:36
Definition: seg.h:42
void SetSnappedAnchor(const VECTOR2I &aAnchorPos)
Inform this manager that an anchor snap has been made.
OPT_VECTOR2I GetNearestSnapLinePoint(const VECTOR2I &aCursor, const VECTOR2I &aNearestGrid, std::optional< int > aDistToNearest, int snapRange) const
If the snap line is active, return the best snap point that is closest to the cursor.
CONSTRUCTION_VIEW_HANDLER & m_viewHandler
void ClearSnapLine()
Clear the snap line origin and end points.
bool HasCompleteSnapLine() const
SNAP_LINE_MANAGER(CONSTRUCTION_VIEW_HANDLER &aViewHandler)
void SetSnapLineOrigin(const VECTOR2I &aOrigin)
The snap point is a special point that is located at the last point the cursor snapped to.
void SetSnapLineEnd(const OPT_VECTOR2I &aSnapPoint)
Set the end point of the snap line.
const OPT_VECTOR2I & GetSnapLineOrigin() const
OPT_VECTOR2I m_snapLineOrigin
GFX_UPDATE_CALLBACK m_updateCallback
CONSTRUCTION_MANAGER m_constructionManager
std::vector< CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH > GetConstructionItems() const
Get a list of all the active construction geometry, computed from the combined state of the snap line...
void updateView() override
SNAP_LINE_MANAGER m_snapLineManager
SNAP_MANAGER(KIGFX::CONSTRUCTION_GEOM &aHelper)
static std::size_t HashConstructionBatchSources(const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH &aBatch, bool aIsPersistent)
Construct a hash based on the sources of the items in the batch.
static bool pointHasEscapedSnapLineX(const VECTOR2I &aCursor, const VECTOR2I &aSnapLineOrigin, int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle)
Check if the cursor has moved far enough away from the snap line origin to escape snapping in the X d...
static bool pointHasEscapedSnapLineY(const VECTOR2I &aCursor, const VECTOR2I &aSnapLineOrigin, int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle)
As above, but for the Y direction.
@ DEGREES_T
Definition: eda_angle.h:31
static constexpr void hash_combine(std::size_t &seed)
This is a dummy function to take the final case of hash_combine below.
Definition: hash.h:32
static constexpr std::size_t hash_val(const Types &... args)
Definition: hash.h:51
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition: eda_angle.h:393
std::optional< VECTOR2I > OPT_VECTOR2I
Definition: seg.h:39
Items to be used for the construction of "virtual" anchors, for example, when snapping to a point inv...
std::vector< KIGFX::CONSTRUCTION_GEOM::DRAWABLE > Constructions
VECTOR2< int32_t > VECTOR2I
Definition: vector2d.h:695