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
58 void ProposeActivation( T&& aProposal, std::size_t aProposalTag, bool aAcceptImmediately )
59 {
60 std::unique_lock<std::mutex> lock( m_mutex );
61
62 if( m_lastAcceptedProposalTag.has_value() && aProposalTag == *m_lastAcceptedProposalTag )
63 {
64 // This proposal was accepted last time
65 // (could be made optional if we want to allow re-accepting the same proposal)
66 return;
67 }
68
69 if( m_pendingProposalTag.has_value() && aProposalTag == *m_pendingProposalTag )
70 {
71 // This proposal is already pending
72 return;
73 }
74
75 m_pendingProposalTag = aProposalTag;
76 m_lastProposal = std::move( aProposal );
77
78 if( aAcceptImmediately )
79 {
80 // Synchonously accept the proposal
81 lock.unlock();
83 }
84 else
85 {
86 m_timer.Start( m_timeout.count(), wxTIMER_ONE_SHOT );
87 }
88 }
89
91 {
92 std::lock_guard<std::mutex> lock( m_mutex );
94 m_timer.Stop();
95 }
96
97private:
101 void onTimerExpiry( wxTimerEvent& aEvent )
102 {
104 }
105
107 {
108 std::unique_lock<std::mutex> lock( m_mutex );
109
111 {
113 m_pendingProposalTag.reset();
114
115 // Move out from the locked variable
116 T proposalToAccept = std::move( m_lastProposal );
117 lock.unlock();
118
119 // Call the callback (outside the lock)
120 // This is all in the UI thread now, so it won't be concurrent
121 m_callback( std::move( proposalToAccept ) );
122 }
123 }
124
125 mutable std::mutex m_mutex;
126
128 std::chrono::milliseconds m_timeout;
129
131 std::optional<std::size_t> m_pendingProposalTag;
132
134 std::optional<std::size_t> m_lastAcceptedProposalTag;
135
138
141
142 wxTimer m_timer;
143};
144
145
147{
150};
151
152
154 m_viewHandler( aHelper )
155{
156 const std::chrono::milliseconds acceptanceTimeout(
157 ADVANCED_CFG::GetCfg().m_ExtensionSnapTimeoutMs );
158
159 m_activationHelper = std::make_unique<ACTIVATION_HELPER<std::unique_ptr<PENDING_BATCH>>>(
160 acceptanceTimeout,
161 [this]( std::unique_ptr<PENDING_BATCH>&& aAccepted )
162 {
163 acceptConstructionItems( std::move( aAccepted ) );
164 } );
165}
166
167
169{
170}
171
172
176static std::size_t
178 bool aIsPersistent )
179{
180 std::size_t hash = hash_val( aIsPersistent );
181
182 for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& item : aBatch )
183 {
184 hash_combine( hash, item.Source, item.Item );
185 }
186 return hash;
187}
188
189
191 std::unique_ptr<CONSTRUCTION_ITEM_BATCH> aBatch, bool aIsPersistent )
192{
193 if( aBatch->empty() )
194 {
195 // There's no point in proposing an empty batch
196 // It would just clear existing construction items for nothing new
197 return;
198 }
199
200 bool acceptImmediately = false;
201
202 {
203 std::lock_guard<std::mutex> lock( m_batchesMutex );
204
205 if( aIsPersistent )
206 {
207 acceptImmediately = true;
208 }
209 else
210 {
211 // If the batch is temporary, we can accept it immediately if there's room
212 acceptImmediately = m_temporaryConstructionBatches.size() < getMaxTemporaryBatches();
213 }
214 }
215
216 auto pendingBatch =
217 std::make_unique<PENDING_BATCH>( PENDING_BATCH{ std::move( *aBatch ), aIsPersistent } );
218 const std::size_t hash = HashConstructionBatchSources( pendingBatch->Batch, aIsPersistent );
219
220 // Immediate or not, propose the batch via the activation helper as this handles duplicates
221 m_activationHelper->ProposeActivation( std::move( pendingBatch ), hash, acceptImmediately );
222}
223
224
226{
227 m_activationHelper->CancelProposal();
228}
229
230
232{
233 // We only keep up to one previous temporary batch and the current one
234 // we could make this a setting if we want to keep more, but it gets cluttered
235 return 2;
236}
237
238
239void CONSTRUCTION_MANAGER::acceptConstructionItems( std::unique_ptr<PENDING_BATCH> aAcceptedBatch )
240{
241 const auto getInvolved = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd )
242 {
243 for( const CONSTRUCTION_ITEM& item : aBatchToAdd )
244 {
245 // Only show the item if it's not already involved
246 // (avoid double-drawing the same item)
247 if( m_involvedItems.count( item.Item ) == 0 )
248 {
249 m_involvedItems.insert( item.Item );
250 }
251 }
252 };
253
254 // Copies for use outside the lock
255 std::vector<CONSTRUCTION_ITEM_BATCH> persistentBatches, temporaryBatches;
256 {
257 std::lock_guard<std::mutex> lock( m_batchesMutex );
258
259 if( aAcceptedBatch->IsPersistent )
260 {
261 // We only keep one previous persistent batch for the moment
262 m_persistentConstructionBatch = std::move( aAcceptedBatch->Batch );
263 }
264 else
265 {
266 bool anyNewItems = false;
267 for( CONSTRUCTION_ITEM& item : aAcceptedBatch->Batch )
268 {
269 if( m_involvedItems.count( item.Item ) == 0 )
270 {
271 anyNewItems = true;
272 break;
273 }
274 }
275
276 // If there are no new items involved, don't bother adding the batch
277 if( !anyNewItems )
278 {
279 return;
280 }
281
283 {
285 }
286
287 m_temporaryConstructionBatches.emplace_back( std::move( aAcceptedBatch->Batch ) );
288 }
289
290 m_involvedItems.clear();
291
292 // Copy the batches for use outside the lock
294 {
295 getInvolved( *m_persistentConstructionBatch );
296 persistentBatches.push_back( *m_persistentConstructionBatch );
297 }
298
300 {
301 getInvolved( batch );
302 temporaryBatches.push_back( batch );
303 }
304 }
305
307 geom.ClearDrawables();
308
309 const auto addDrawables =
310 [&]( const std::vector<CONSTRUCTION_ITEM_BATCH>& aBatches, bool aIsPersistent )
311 {
312 for( const CONSTRUCTION_ITEM_BATCH& batch : aBatches )
313 {
314 for( const CONSTRUCTION_ITEM& item : batch )
315 {
316 for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable : item.Constructions )
317 {
318 geom.AddDrawable( drawable, aIsPersistent );
319 }
320 }
321 }
322 };
323
324 addDrawables( persistentBatches, true );
325 addDrawables( temporaryBatches, false );
326
328}
329
330
331bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
332{
333 for( EDA_ITEM* item : aItems )
334 {
335 // Null items (i.e. construction items) are always considered involved
336 if( item && m_involvedItems.count( item ) == 0 )
337 {
338 return false;
339 }
340 }
341
342 return true;
343}
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
361
363{
364 std::lock_guard<std::mutex> lock( m_batchesMutex );
366}
367
368
370 m_viewHandler( aViewHandler )
371{
372}
373
374
376{
377 // Setting the origin clears the snap line as the end point is no longer valid
379 m_snapLineOrigin = aOrigin;
380}
381
382
384{
385 if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
386 {
387 m_snapLineEnd = aSnapEnd;
388
389 if( m_snapLineEnd )
391 else
393
395 }
396}
397
398
400{
401 m_snapLineOrigin.reset();
402 m_snapLineEnd.reset();
405}
406
407
409{
410 if( m_snapLineOrigin.has_value() )
411 {
412 if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
413 {
414 SetSnapLineEnd( aAnchorPos );
415 }
416 else
417 {
418 // Snapped to something that is not the snap line origin, so
419 // this anchor is now the new snap line origin
420 SetSnapLineOrigin( aAnchorPos );
421 }
422 }
423 else
424 {
425 // If there's no snap line, start one
426 SetSnapLineOrigin( aAnchorPos );
427 }
428}
429
430
438static bool pointHasEscapedSnapLineX( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
439 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
440{
441 if( std::abs( aCursor.x - aSnapLineOrigin.x ) < aEscapeRange )
442 {
443 return false;
444 }
445 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin ) + EDA_ANGLE( 90, DEGREES_T );
446 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
447}
448
449
453static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
454 int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
455{
456 if( std::abs( aCursor.y - aSnapLineOrigin.y ) < aEscapeRange )
457 {
458 return false;
459 }
460 EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin );
461 return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
462}
463
464
466 const VECTOR2I& aNearestGrid,
467 std::optional<int> aDistToNearest,
468 int aSnapRange ) const
469{
470 // return std::nullopt;
471 if( m_snapLineOrigin )
472 {
473 bool snapLine = false;
474 VECTOR2I bestSnapPoint = aNearestGrid;
475
476 // If there's no snap anchor, or it's too far away, prefer the grid
477 const bool gridBetterThanNearest = !aDistToNearest || *aDistToNearest > aSnapRange;
478
479 // The escape range is how far you go before the snap line is de-activated.
480 // Make this a bit more forgiving than the snap range, as you can easily cancel
481 // deliberately with a mouse move.
482 // These are both a bit arbitrary, and can be adjusted as preferred
483 const int escapeRange = 2 * aSnapRange;
484 const EDA_ANGLE longRangeEscapeAngle( 4, DEGREES_T );
485
486 const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
487 longRangeEscapeAngle );
488 const bool escapedY = pointHasEscapedSnapLineY( aCursor, *m_snapLineOrigin, escapeRange,
489 longRangeEscapeAngle );
490
493 if( !escapedX && gridBetterThanNearest )
494 {
495 bestSnapPoint.x = m_snapLineOrigin->x;
496 snapLine = true;
497 }
498
499 if( !escapedY && gridBetterThanNearest )
500 {
501 bestSnapPoint.y = m_snapLineOrigin->y;
502 snapLine = true;
503 }
504
505 if( snapLine )
506 {
507 return bestSnapPoint;
508 }
509 }
510
511 return std::nullopt;
512}
513
514
516 CONSTRUCTION_VIEW_HANDLER( aHelper ), m_snapLineManager( *this ),
517 m_constructionManager( *this )
518{
519}
520
521
523{
524 if( m_updateCallback )
525 {
526 bool showAnything = m_constructionManager.HasActiveConstruction()
528
529 m_updateCallback( showAnything );
530 }
531}
532
533
534std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
536{
537 std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> batches;
538
540
541 if( const OPT_VECTOR2I& snapLineOrigin = m_snapLineManager.GetSnapLineOrigin();
542 snapLineOrigin.has_value() )
543 {
545
547 batch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
549 nullptr,
550 {},
551 } );
552
553 // One horizontal and one vertical infinite line from the snap point
554 snapPointItem.Constructions.push_back(
555 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 100000, 0 ) } );
556 snapPointItem.Constructions.push_back(
557 LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 0, 100000 ) } );
558
559 batches.push_back( std::move( batch ) );
560 }
561
562 return batches;
563}
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: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:695