KiCad PCB EDA Suite
Loading...
Searching...
No Matches
refdes_tracker.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 * KiCad is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * KiCad 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 KiCad. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <regex>
21#include <algorithm>
22#include <cctype>
23#include <charconv>
24#include <iostream>
25
26#include <sch_reference_list.h>
27
28#include "refdes_tracker.h"
29
30REFDES_TRACKER::REFDES_TRACKER( bool aThreadSafe ) :
31 m_threadSafe( aThreadSafe ), m_reuseRefDes( true )
32{
33}
34
35bool REFDES_TRACKER::Insert( const std::string& aRefDes )
36{
37 std::unique_lock<std::mutex> lock;
38
39 if( m_threadSafe )
40 lock = std::unique_lock<std::mutex>( m_mutex );
41
42 return insertImpl( aRefDes );
43}
44
45bool REFDES_TRACKER::insertImpl( const std::string& aRefDes )
46{
47 if( m_allRefDes.find( aRefDes ) != m_allRefDes.end() )
48 return false;
49
50 auto [prefix, number] = parseRefDes( aRefDes );
51
52 m_allRefDes.insert( aRefDes );
53
54 // Insert the number and update caches
55 return insertNumber( prefix, number );
56}
57
58bool REFDES_TRACKER::insertNumber( const std::string& aPrefix, int aNumber )
59{
60 PREFIX_DATA& data = m_prefixData[aPrefix];
61
62 if( data.m_usedNumbers.find( aNumber ) != data.m_usedNumbers.end() )
63 return false;
64
65 data.m_usedNumbers.insert( aNumber );
66
67 if( aNumber > 0 )
68 updateCacheOnInsert( data, aNumber );
69
70 return true;
71}
72
73
74bool REFDES_TRACKER::containsImpl( const std::string& aRefDes ) const
75{
76 return m_allRefDes.contains( aRefDes );
77}
78
79bool REFDES_TRACKER::Contains( const std::string& aRefDes ) const
80{
81 std::unique_lock<std::mutex> lock;
82
83 if( m_threadSafe )
84 lock = std::unique_lock<std::mutex>( m_mutex );
85
86 return containsImpl( aRefDes );
87}
88
89
91 const std::map<int, std::vector<SCH_REFERENCE>>& aRefNumberMap,
92 const std::vector<int>& aRequiredUnits,
93 int aMinValue )
94{
95 std::unique_lock<std::mutex> lock;
96
97 if( m_threadSafe )
98 lock = std::unique_lock<std::mutex>( m_mutex );
99
100 // Filter out negative unit numbers
101 std::vector<int> validUnits;
102 std::copy_if( aRequiredUnits.begin(), aRequiredUnits.end(),
103 std::back_inserter( validUnits ),
104 []( int unit ) { return unit >= 0; } );
105
106 int candidate = aMinValue;
107
108 while( true )
109 {
110 // Check if this candidate number is currently in use
111 auto mapIt = aRefNumberMap.find( candidate );
112
113 if( mapIt == aRefNumberMap.end() )
114 {
115 // Not currently in use - check if it was previously used
116 std::string candidateRefDes = aRef.GetRef().ToStdString() + std::to_string( candidate );
117
118 if( m_reuseRefDes || !containsImpl( candidateRefDes ) )
119 {
120 // Completely unused - this is our answer
121 insertNumber( aRef.GetRefStr(), candidate );
122 m_allRefDes.insert( candidateRefDes );
123 return candidate;
124 }
125 else
126 {
127 // Previously used but no longer active - skip to next candidate
128 candidate++;
129 continue;
130 }
131 }
132 else
133 {
134 // Currently in use - check if required units are available
135 if( validUnits.empty() )
136 {
137 // Need completely unused reference, but this one is in use
138 candidate++;
139 continue;
140 }
141
142 // Check if required units are available
143 bool unitsAvailable;
145 {
146 // Use external units checker if available
147 unitsAvailable = m_externalUnitsChecker( aRef, mapIt->second, validUnits );
148 }
149 else
150 {
151 // Use default implementation
152 unitsAvailable = areUnitsAvailable( aRef, mapIt->second, validUnits );
153 }
154
155 if( unitsAvailable )
156 {
157 // All required units are available - this is our answer
158 // Note: Don't insert into tracker since reference is already in use
159 return candidate;
160 }
161 else
162 {
163 // Some required units are not available - try next candidate
164 candidate++;
165 continue;
166 }
167 }
168 }
169}
170
171
173 const std::vector<SCH_REFERENCE>& aRefVector,
174 const std::vector<int>& aRequiredUnits ) const
175{
176 for( const int& unit : aRequiredUnits )
177 {
178 for( const SCH_REFERENCE& ref : aRefVector )
179 {
180 // If we have a different library or different value,
181 // we cannot share a reference designator. Also, if the unit matches,
182 // the reference designator + unit is already in use.
183 if( ref.CompareLibName( aRef ) != 0
184 || ref.CompareValue( aRef ) != 0
185 || ref.GetUnit() == unit )
186 {
187 return false; // Conflict found
188 }
189 }
190 }
191
192 return true; // All required units are available
193}
194
195
196std::pair<std::string, int> REFDES_TRACKER::parseRefDes( const std::string& aRefDes ) const
197{
198 if( aRefDes.empty() )
199 return { "", 0 };
200
201 // Split on the trailing run of digits so any non-digit prefix (including '#'
202 // used by power and flag symbols) is preserved as the map key.
203 size_t pos = aRefDes.size();
204
205 while( pos > 0 && std::isdigit( static_cast<unsigned char>( aRefDes[pos - 1] ) ) )
206 pos--;
207
208 if( pos == 0 )
209 return { aRefDes, 0 };
210
211 if( pos == aRefDes.size() )
212 return { aRefDes, 0 };
213
214 int number = 0;
215 const char* first = aRefDes.data() + pos;
216 const char* last = aRefDes.data() + aRefDes.size();
217 auto [ptr, ec] = std::from_chars( first, last, number );
218
219 if( ec != std::errc() || ptr != last )
220 return { aRefDes, 0 };
221
222 return { aRefDes.substr( 0, pos ), number };
223}
224
226{
227 if( aData.m_cacheValid )
228 return;
229
230 // Find the first gap in the sequence starting from 1
231 int candidate = 1;
232 for( int used : aData.m_usedNumbers )
233 {
234 if( used <= 0 )
235 continue; // Skip non-positive numbers (like our 0 marker)
236 if( used == candidate )
237 {
238 candidate++;
239 }
240 else if( used > candidate )
241 {
242 break; // Found a gap
243 }
244 }
245
246 aData.m_baseNext = candidate;
247 aData.m_cacheValid = true;
248}
249
250void REFDES_TRACKER::updateCacheOnInsert( PREFIX_DATA& aData, int aInsertedNumber ) const
251{
252 // Update base next cache if it's valid and affected
253 if( aData.m_cacheValid )
254 {
255 if( aInsertedNumber == aData.m_baseNext )
256 {
257 // The base next was just used, find the new next
258 int candidate = aData.m_baseNext + 1;
259 while( aData.m_usedNumbers.find( candidate ) != aData.m_usedNumbers.end() )
260 {
261 candidate++;
262 }
263 aData.m_baseNext = candidate;
264 }
265 // If aInsertedNumber > m_baseNext, base cache is still valid
266 // If aInsertedNumber < m_baseNext, base cache is still valid
267 }
268
269 for( auto cacheIt = aData.m_nextCache.begin(); cacheIt != aData.m_nextCache.end(); ++cacheIt )
270 {
271 int cachedNext = cacheIt->second;
272
273 if( aInsertedNumber == cachedNext )
274 {
275 // This cached value was just used, need to update it
276 int candidate = cachedNext + 1;
277
278 while( aData.m_usedNumbers.contains( candidate ) )
279 candidate++;
280
281 cacheIt->second = candidate;
282 }
283 }
284}
285
286int REFDES_TRACKER::findNextAvailable( const PREFIX_DATA& aData, int aMinValue ) const
287{
288 if( auto cacheIt = aData.m_nextCache.find( aMinValue ); cacheIt != aData.m_nextCache.end() )
289 return cacheIt->second;
290
291 updateBaseNext( const_cast<PREFIX_DATA&>( aData ) );
292
293 int candidate;
294
295 if( aMinValue <= 1 )
296 {
297 candidate = aData.m_baseNext;
298 }
299 else
300 {
301 // Start search from aMinValue
302 candidate = aMinValue;
303
304 while( aData.m_usedNumbers.find( candidate ) != aData.m_usedNumbers.end() )
305 candidate++;
306 }
307
308 // Cache the result
309 aData.m_nextCache[aMinValue] = candidate;
310
311 return candidate;
312}
313
314std::string REFDES_TRACKER::Serialize() const
315{
316 std::unique_lock<std::mutex> lock;
317 if( m_threadSafe )
318 lock = std::unique_lock<std::mutex>( m_mutex );
319
320 std::ostringstream result;
321 bool first = true;
322
323 for( const auto& [prefix, data] : m_prefixData )
324 {
325 if( !first )
326 result << ",";
327 first = false;
328
329 std::string escapedPrefix = escapeForSerialization( prefix );
330
331 // Separate numbers from prefix-only entries
332 std::vector<int> numbers;
333 bool hasPrefix = false;
334
335 for( int num : data.m_usedNumbers )
336 {
337 if( num > 0 )
338 numbers.push_back( num );
339 else if( num == 0 )
340 hasPrefix = true;
341 }
342
343 if( numbers.empty() && !hasPrefix )
344 continue; // No data for this prefix
345
346 // Create ranges for numbered entries
347 std::vector<std::pair<int, int>> ranges;
348
349 if( !numbers.empty() )
350 {
351 int start = numbers[0];
352 int end = numbers[0];
353
354 for( size_t i = 1; i < numbers.size(); ++i )
355 {
356 if( numbers[i] == end + 1 )
357 {
358 end = numbers[i];
359 }
360 else
361 {
362 ranges.push_back( { start, end } );
363 start = end = numbers[i];
364 }
365 }
366 ranges.push_back( { start, end } );
367 }
368
369 bool firstRange = true;
370 for( const auto& [start, end] : ranges )
371 {
372 if( !firstRange )
373 result << ",";
374 firstRange = false;
375
376 result << escapedPrefix;
377 if( start == end )
378 {
379 result << start;
380 }
381 else
382 {
383 result << start << "-" << end;
384 }
385 }
386
387 // Add prefix-only entry if it exists
388 if( hasPrefix )
389 {
390 if( !firstRange )
391 result << ",";
392 result << escapedPrefix;
393 }
394 }
395
396 return result.str();
397}
398
399bool REFDES_TRACKER::Deserialize( const std::string& aData )
400{
401 std::unique_lock<std::mutex> lock;
402
403 if( m_threadSafe )
404 lock = std::unique_lock<std::mutex>( m_mutex );
405
406 clearImpl();
407
408 if( aData.empty() )
409 return true;
410
411 auto parts = splitString( aData, ',' );
412
413 // A malformed project file must fail Deserialize cleanly rather than throw,
414 // since QA builds install wxAssertThrower around the calling load path.
415 auto parsePositiveInt = []( const std::ssub_match& aMatch, int& aOut ) -> bool
416 {
417 const char* first = std::to_address( aMatch.first );
418 const char* last = std::to_address( aMatch.second );
419 int value = 0;
420 auto [ptr, ec] = std::from_chars( first, last, value );
421
422 if( ec != std::errc() || ptr != last || value <= 0 )
423 return false;
424
425 aOut = value;
426 return true;
427 };
428
429 // Prefix may contain any non-digit characters, including '#' for power flags
430 // and embedded digits (e.g. "U1U2"), so anchor on the final non-digit before
431 // the trailing digit run. Hoisted out of the loop because std::regex
432 // construction dominates the per-part match cost.
433 const std::regex rangePattern( R"(^(.*\D)(\d+)-(\d+)$)" );
434 const std::regex numberedPattern( R"(^(.*\D)(\d+)$)" );
435 const std::regex prefixOnlyPattern( R"(^(.+)$)" );
436
437 for( const std::string& part : parts )
438 {
439 std::string unescaped = unescapeFromSerialization( part );
440 std::smatch match;
441
442 if( std::regex_match( unescaped, match, rangePattern ) )
443 {
444 std::string prefix = match[1].str();
445 int start = 0;
446 int end = 0;
447
448 if( !parsePositiveInt( match[2], start ) || !parsePositiveInt( match[3], end ) )
449 {
450 clearImpl();
451 return false;
452 }
453
454 for( int i = start; i <= end; ++i )
455 insertImpl( prefix + std::to_string( i ) );
456 }
457 else if( std::regex_match( unescaped, match, numberedPattern ) )
458 {
459 std::string prefix = match[1].str();
460 int number = 0;
461
462 if( !parsePositiveInt( match[2], number ) )
463 {
464 clearImpl();
465 return false;
466 }
467
468 insertImpl( prefix + std::to_string( number ) );
469 }
470 else if( std::regex_match( unescaped, match, prefixOnlyPattern ) )
471 {
472 std::string prefix = match[1].str();
473
474 insertImpl( prefix );
475 }
476 else
477 {
478 // Invalid format
479 clearImpl();
480 return false;
481 }
482 }
483
484 return true;
485}
486
488{
489 std::unique_lock<std::mutex> lock;
490
491 if( m_threadSafe )
492 lock = std::unique_lock<std::mutex>( m_mutex );
493
494 clearImpl();
495}
496
497
499{
500 m_prefixData.clear();
501 m_allRefDes.clear();
502}
503
505{
506 std::unique_lock<std::mutex> lock;
507
508 if( m_threadSafe )
509 lock = std::unique_lock<std::mutex>( m_mutex );
510
511 return m_allRefDes.size();
512}
513
514std::string REFDES_TRACKER::escapeForSerialization( const std::string& aStr ) const
515{
516 std::string result;
517 result.reserve( aStr.length() * 2 ); // Reserve space to avoid frequent reallocations
518
519 for( char c : aStr )
520 {
521 if( c == '\\' || c == ',' || c == '-' )
522 result += '\\';
523 result += c;
524 }
525 return result;
526}
527
528std::string REFDES_TRACKER::unescapeFromSerialization( const std::string& aStr ) const
529{
530 std::string result;
531 result.reserve( aStr.length() );
532
533 bool escaped = false;
534 for( char c : aStr )
535 {
536 if( escaped )
537 {
538 result += c;
539 escaped = false;
540 }
541 else if( c == '\\' )
542 {
543 escaped = true;
544 }
545 else
546 {
547 result += c;
548 }
549 }
550 return result;
551}
552
553std::vector<std::string> REFDES_TRACKER::splitString( const std::string& aStr, char aDelimiter ) const
554{
555 std::vector<std::string> result;
556 std::string current;
557 bool escaped = false;
558
559 for( char c : aStr )
560 {
561 if( escaped )
562 {
563 current += c;
564 escaped = false;
565 }
566 else if( c == '\\' )
567 {
568 escaped = true;
569 current += c;
570 }
571 else if( c == aDelimiter )
572 {
573 result.push_back( current );
574 current.clear();
575 }
576 else
577 {
578 current += c;
579 }
580 }
581
582 if( !current.empty() )
583 result.push_back( current );
584
585 return result;
586}
587
588
590{
591 std::unique_lock<std::mutex> lock;
592
593 if( m_threadSafe )
594 lock = std::unique_lock<std::mutex>( m_mutex );
595
596 m_externalUnitsChecker = aChecker;
597}
598
599
601{
602 std::unique_lock<std::mutex> lock;
603
604 if( m_threadSafe )
605 lock = std::unique_lock<std::mutex>( m_mutex );
606
607 m_externalUnitsChecker = nullptr;
608}
void updateBaseNext(PREFIX_DATA &aData) const
std::string escapeForSerialization(const std::string &aStr) const
Escape special characters for serialization.
bool insertNumber(const std::string &aPrefix, int aNumber)
Insert a number for a specific prefix, updating internal structures.
void ClearUnitsChecker()
Clear the external units checker, reverting to default behavior.
bool Deserialize(const std::string &aData)
Deserialize tracker data from string representation.
std::vector< std::string > splitString(const std::string &aStr, char aDelimiter) const
Split string by delimiter, handling escaped characters.
bool m_reuseRefDes
If true, allows reusing existing reference designators.
std::mutex m_mutex
Mutex for thread safety.
int GetNextRefDesForUnits(const SCH_REFERENCE &aRef, const std::map< int, std::vector< SCH_REFERENCE > > &aRefNumberMap, const std::vector< int > &aRequiredUnits, int aMinValue)
Get the next available reference designator number for multi-unit symbols.
std::unordered_set< std::string > m_allRefDes
bool Insert(const std::string &aRefDes)
Insert a reference designator into the tracker.
void clearImpl()
Clear all internal data structures without locking.
size_t Size() const
Get the total count of stored reference designators.
std::string unescapeFromSerialization(const std::string &aStr) const
Unescape special characters from serialization.
bool insertImpl(const std::string &aRefDes)
Internal implementation of Insert without locking.
int findNextAvailable(const PREFIX_DATA &aData, int aMinValue) const
Find next available number for a prefix starting from a minimum value.
REFDES_TRACKER(bool aThreadSafe=false)
Constructor.
bool areUnitsAvailable(const SCH_REFERENCE &aRef, const std::vector< SCH_REFERENCE > &aRefVector, const std::vector< int > &aRequiredUnits) const
Check if all required units are available for a given reference number.
void SetUnitsChecker(const UNITS_CHECKER_FUNC< SCH_REFERENCE > &aChecker)
Set an external units checker function for SCH_REFERENCE objects.
void updateCacheOnInsert(PREFIX_DATA &aData, int aInsertedNumber) const
Update cached next available values when a number is inserted.
std::string Serialize() const
Serialize the tracker data to a compact string representation.
std::unordered_map< std::string, PREFIX_DATA > m_prefixData
Map from prefix to its tracking data.
bool m_threadSafe
True if thread safety is enabled.
bool Contains(const std::string &aRefDes) const
Check if a reference designator exists in the tracker.
std::pair< std::string, int > parseRefDes(const std::string &aRefDes) const
Parse a reference designator into prefix and numerical suffix.
bool containsImpl(const std::string &aRefDes) const
Check if a reference designator exists in the tracker without locking.
UNITS_CHECKER_FUNC< SCH_REFERENCE > m_externalUnitsChecker
External units checker function (optional)
void Clear()
Clear all stored reference designators.
A helper to define a symbol's reference designator in a schematic.
wxString GetRef() const
const char * GetRefStr() const
std::function< bool(const T &aTestRef, const std::vector< T > &aExistingRefs, const std::vector< int > &aRequiredUnits)> UNITS_CHECKER_FUNC
Function type for external units availability checking.
Data structure for tracking used numbers and caching next available values.
std::set< int > m_usedNumbers
Sorted set of used numbers for this prefix.
bool m_cacheValid
True if m_baseNext cache is valid.
int m_baseNext
Next available from 1 (cached)
std::map< int, int > m_nextCache
Cache of next available number for given min values.
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.