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 (C) 2024 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#include <condition_variable>
28#include <thread>
29
30#include <advanced_config.h>
31#include <hash.h>
32
43template <typename T>
45{
46public:
47 using ACTIVATION_CALLBACK = std::function<void( T&& )>;
48
49 ACTIVATION_HELPER( std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback ) :
50 m_timeout( aTimeout ), m_callback( std::move( aCallback ) ), m_stop( false ),
52 {
53 }
54
56 {
57 // Stop the delay thread and wait for it
58 {
59 std::lock_guard<std::mutex> lock( m_mutex );
60 m_stop = true;
61 m_cv.notify_all();
62 }
63
64 if( m_thread.joinable() )
65 {
66 m_thread.join();
67 }
68 }
69
70 void ProposeActivation( T&& aProposal, std::size_t aProposalTag )
71 {
72 std::lock_guard<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 m_proposalDeadline = std::chrono::steady_clock::now() + m_timeout;
90 m_cv.notify_all();
91 }
92
94 {
95 std::lock_guard<std::mutex> lock( m_mutex );
97 m_cv.notify_all();
98 }
99
101 {
102 while( !m_stop )
103 {
104 std::unique_lock<std::mutex> lock( m_mutex );
105 if( !m_stop && !m_pendingProposalTag.has_value() )
106 {
107 // No active proposal - wait for one (unlocks while waiting)
108 m_cv.wait( lock );
109 }
110
111 if( !m_stop && m_pendingProposalTag.has_value() )
112 {
113 // Active proposal - wait for timeout
114 auto now = std::chrono::steady_clock::now();
115
116 if( m_cv.wait_for( lock, m_proposalDeadline - now ) == std::cv_status::timeout )
117 {
118 // See if the timeout was extended for a new proposal
119 now = std::chrono::steady_clock::now();
120 if( now < m_proposalDeadline )
121 {
122 // Extended - wait for the new deadline
123 continue;
124 }
125
126 // See if there is still a proposal to accept
127 // (could have been cancelled in the meantime)
129 {
131 m_pendingProposalTag.reset();
132
133 T proposalToAccept = std::move( m_lastProposal );
134 lock.unlock();
135 // Call the callback (outside the lock)
136 m_callback( std::move( proposalToAccept ) );
137 }
138 }
139 }
140 }
141 }
142
143private:
144 mutable std::mutex m_mutex;
145
146 // Activation timeout in milliseconds
147 std::chrono::milliseconds m_timeout;
148
149 std::chrono::time_point<std::chrono::steady_clock> m_proposalDeadline;
150
152 std::optional<std::size_t> m_pendingProposalTag;
153
155 std::optional<std::size_t> m_lastAcceptedProposalTag;
156
157 // The most recently-proposed item
159
162 std::condition_variable m_cv;
163 std::atomic<bool> m_stop;
164 // The thread must be constructed last, as it starts running immediately
165 std::thread m_thread;
166};
167
168
170{
173};
174
175
177 m_viewHandler( aHelper )
178{
179 const std::chrono::milliseconds acceptanceTimeout(
180 ADVANCED_CFG::GetCfg().m_ExtensionSnapTimeoutMs );
181
182 m_activationHelper = std::make_unique<ACTIVATION_HELPER<std::unique_ptr<PENDING_BATCH>>>(
183 acceptanceTimeout,
184 [this]( std::unique_ptr<PENDING_BATCH>&& aAccepted )
185 {
186 acceptConstructionItems( std::move( aAccepted ) );
187 } );
188}
189
190
192{
193}
194
195
199static std::size_t
201 bool aIsPersistent )
202{
203 std::size_t hash = hash_val( aIsPersistent );
204
205 for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& item : aBatch )
206 {
207 hash_combine( hash, item.Source, item.Item );
208 }
209 return hash;
210}
211
212
214 std::unique_ptr<CONSTRUCTION_ITEM_BATCH> aBatch, bool aIsPersistent )
215{
216 if( aBatch->empty() )
217 {
218 // There's no point in proposing an empty batch
219 // It would just clear existing construction items for nothing new
220 return;
221 }
222
223 const std::size_t hash = HashConstructionBatchSources( *aBatch, aIsPersistent );
224
225 m_activationHelper->ProposeActivation(
226 std::make_unique<PENDING_BATCH>( PENDING_BATCH{ std::move( *aBatch ), aIsPersistent } ),
227 hash );
228}
229
230
232{
233 m_activationHelper->CancelProposal();
234}
235
236
237void CONSTRUCTION_MANAGER::acceptConstructionItems( std::unique_ptr<PENDING_BATCH> aAcceptedBatch )
238{
239 const auto getInvolved = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd )
240 {
241 for( const CONSTRUCTION_ITEM& item : aBatchToAdd )
242 {
243 // Only show the item if it's not already involved
244 // (avoid double-drawing the same item)
245 if( m_involvedItems.count( item.Item ) == 0 )
246 {
247 m_involvedItems.insert( item.Item );
248 }
249 }
250 };
251
252 // Copies for use outside the lock
253 std::vector<CONSTRUCTION_ITEM_BATCH> persistentBatches, temporaryBatches;
254 {
255 std::lock_guard<std::mutex> lock( m_batchesMutex );
256
257 if( aAcceptedBatch->IsPersistent )
258 {
259 // We only keep one previous persistent batch for the moment
260 m_persistentConstructionBatch = std::move( aAcceptedBatch->Batch );
261 }
262 else
263 {
264 bool anyNewItems = false;
265 for( CONSTRUCTION_ITEM& item : aAcceptedBatch->Batch )
266 {
267 if( m_involvedItems.count( item.Item ) == 0 )
268 {
269 anyNewItems = true;
270 break;
271 }
272 }
273
274 // If there are no new items involved, don't bother adding the batch
275 if( !anyNewItems )
276 {
277 return;
278 }
279
280 // We only keep up to one previous temporary batch and the current one
281 // we could make this a setting if we want to keep more, but it gets cluttered
282 const int maxTempItems = 2;
283
284 while( m_temporaryConstructionBatches.size() >= maxTempItems )
285 {
287 }
288
289 m_temporaryConstructionBatches.emplace_back( std::move( aAcceptedBatch->Batch ) );
290 }
291
292 m_involvedItems.clear();
293
294 // Copy the batches for use outside the lock
296 {
297 getInvolved( *m_persistentConstructionBatch );
298 persistentBatches.push_back( *m_persistentConstructionBatch );
299 }
300
302 {
303 getInvolved( batch );
304 temporaryBatches.push_back( batch );
305 }
306 }
307
309 geom.ClearDrawables();
310
311 const auto addDrawables =
312 [&]( const std::vector<CONSTRUCTION_ITEM_BATCH>& aBatches, bool aIsPersistent )
313 {
314 for( const CONSTRUCTION_ITEM_BATCH& batch : aBatches )
315 {
316 for( const CONSTRUCTION_ITEM& item : batch )
317 {
318 for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable : item.Constructions )
319 {
320 geom.AddDrawable( drawable, aIsPersistent );
321 }
322 }
323 }
324 };
325
326 addDrawables( persistentBatches, true );
327 addDrawables( temporaryBatches, false );
328
330}
331
332bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
333{
334 for( EDA_ITEM* item : aItems )
335 {
336 // Null items (i.e. construction items) are always considered involved
337 if( item && m_involvedItems.count( item ) == 0 )
338 {
339 return false;
340 }
341 }
342
343 return true;
344}
345
347 std::vector<CONSTRUCTION_ITEM_BATCH>& aToExtend ) const
348{
349 std::lock_guard<std::mutex> lock( m_batchesMutex );
351 {
352 aToExtend.push_back( *m_persistentConstructionBatch );
353 }
354
356 {
357 aToExtend.push_back( batch );
358 }
359}
360
362{
363 std::lock_guard<std::mutex> lock( m_batchesMutex );
365}
366
368 m_viewHandler( aViewHandler )
369{
370}
371
373{
374 // Setting the origin clears the snap line as the end point is no longer valid
376 m_snapLineOrigin = aOrigin;
377}
378
380{
381 if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
382 {
383 m_snapLineEnd = aSnapEnd;
384
385 if( m_snapLineEnd )
387 else
389
391 }
392}
393
395{
396 m_snapLineOrigin.reset();
397 m_snapLineEnd.reset();
400}
401
403{
404 if( m_snapLineOrigin.has_value() )
405 {
406 if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
407 {
408 SetSnapLineEnd( aAnchorPos );
409 }
410 else
411 {
412 // Snapped to something that is not the snap line origin, so
413 // this anchor is now the new snap line origin
414 SetSnapLineOrigin( aAnchorPos );
415 }
416 }
417 else
418 {
419 // If there's no snap line, start one
420 SetSnapLineOrigin( aAnchorPos );
421 }
422}
423
424
432static bool pointHasEscapedSnapLineX( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
433 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
434{
435 if( std::abs( aCursor.x - aSnapLineOrigin.x ) < aEscapeRange )
436 {
437 return false;
438 }
439 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin ) + EDA_ANGLE( 90, DEGREES_T );
440 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
441}
442
446static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
447 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
448{
449 if( std::abs( aCursor.y - aSnapLineOrigin.y ) < aEscapeRange )
450 {
451 return false;
452 }
453 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin );
454 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
455}
456
457
459 const VECTOR2I& aNearestGrid,
460 std::optional<int> aDistToNearest,
461 int aSnapRange ) const
462{
463 // return std::nullopt;
464 if( m_snapLineOrigin )
465 {
466 bool snapLine = false;
467 VECTOR2I bestSnapPoint = aNearestGrid;
468
469 // If there's no snap anchor, or it's too far away, prefer the grid
470 const bool gridBetterThanNearest = !aDistToNearest || *aDistToNearest > aSnapRange;
471
472 // The escape range is how far you go before the snap line is de-activated.
473 // Make this a bit more forgiving than the snap range, as you can easily cancel
474 // deliberately with a mouse move.
475 // These are both a bit arbitrary, and can be adjusted as preferred
476 const int escapeRange = 2 * aSnapRange;
477 const EDA_ANGLE longRangeEscapeAngle( 4, DEGREES_T );
478
479 const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
480 longRangeEscapeAngle );
481 const bool escapedY = pointHasEscapedSnapLineY( aCursor, *m_snapLineOrigin, escapeRange,
482 longRangeEscapeAngle );
483
486 if( !escapedX && gridBetterThanNearest )
487 {
488 bestSnapPoint.x = m_snapLineOrigin->x;
489 snapLine = true;
490 }
491
492 if( !escapedY && gridBetterThanNearest )
493 {
494 bestSnapPoint.y = m_snapLineOrigin->y;
495 snapLine = true;
496 }
497
498 if( snapLine )
499 {
500 return bestSnapPoint;
501 }
502 }
503
504 return std::nullopt;
505}
506
507
509 CONSTRUCTION_VIEW_HANDLER( aHelper ), m_snapLineManager( *this ),
510 m_constructionManager( *this )
511{
512}
513
514
516{
517 if( m_updateCallback )
518 {
519 bool showAnything = m_constructionManager.HasActiveConstruction()
521
522 m_updateCallback( showAnything );
523 }
524}
525
526
527std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
529{
530 std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> batches;
531
533
534 if( const OPT_VECTOR2I& snapLineOrigin = m_snapLineManager.GetSnapLineOrigin();
535 snapLineOrigin.has_value() )
536 {
538
540 batch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
542 nullptr,
543 {},
544 } );
545
546 // One horizontal and one vertical infinite line from the snap point
547 snapPointItem.Constructions.push_back(
548 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 100000, 0 ) } );
549 snapPointItem.Constructions.push_back(
550 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 0, 100000 ) } );
551
552 batches.push_back( std::move( batch ) );
553 }
554
555 return batches;
556}
A helper class to manage the activation of a "proposal" after a timeout.
std::atomic< bool > m_stop
ACTIVATION_HELPER(std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback)
std::optional< std::size_t > m_lastAcceptedProposalTag
ACTIVATION_CALLBACK m_callback
std::chrono::milliseconds m_timeout
void ProposeActivation(T &&aProposal, std::size_t aProposalTag)
std::chrono::time_point< std::chrono::steady_clock > m_proposalDeadline
The last proposal tag that was made.
std::condition_variable m_cv
std::optional< std::size_t > m_pendingProposalTag
The last proposal that was accepted.
T m_lastProposal
Callback to call when the proposal is accepted.
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
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
std::vector< CONSTRUCTION_ITEM > CONSTRUCTION_ITEM_BATCH
std::optional< CONSTRUCTION_ITEM_BATCH > m_persistentConstructionBatch
std::unique_ptr< ACTIVATION_HELPER< std::unique_ptr< PENDING_BATCH > > > m_activationHelper
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
Interface wrapper for the construction geometry preview, with a callback to signal the view owner tha...
virtual void updateView()=0
KIGFX::CONSTRUCTION_GEOM & GetViewItem()
EDA_ANGLE Normalize90()
Definition: eda_angle.h:249
A base class for most all the KiCad significant classes used in schematics and boards.
Definition: eda_item.h:89
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:390
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:691