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 if( areUnitsAvailable( aRef, mapIt->second, validUnits ) )
143 {
144 // All required units are available - this is our answer
145 // Note: Don't insert into tracker since reference is already in use
146 return candidate;
147 }
148 else
149 {
150 // Some required units are not available - try next candidate
151 candidate++;
152 continue;
153 }
154 }
155 }
156}
157
158
160 const std::vector<SCH_REFERENCE>& aRefVector,
161 const std::vector<int>& aRequiredUnits ) const
162{
163 for( const int& unit : aRequiredUnits )
164 {
165 for( const SCH_REFERENCE& ref : aRefVector )
166 {
167 // If we have a different library or different value,
168 // we cannot share a reference designator. Also, if the unit matches,
169 // the reference designator + unit is already in use.
170 if( ref.CompareLibName( aRef ) != 0
171 || ref.CompareValue( aRef ) != 0
172 || ref.GetUnit() == unit )
173 {
174 return false; // Conflict found
175 }
176 }
177 }
178
179 return true; // All required units are available
180}
181
182
183std::pair<std::string, int> REFDES_TRACKER::parseRefDes( const std::string& aRefDes ) const
184{
185 if( aRefDes.empty() )
186 return { "", 0 };
187
188 // Split on the trailing run of digits so any non-digit prefix (including '#'
189 // used by power and flag symbols) is preserved as the map key.
190 size_t pos = aRefDes.size();
191
192 while( pos > 0 && std::isdigit( static_cast<unsigned char>( aRefDes[pos - 1] ) ) )
193 pos--;
194
195 if( pos == 0 )
196 return { aRefDes, 0 };
197
198 if( pos == aRefDes.size() )
199 return { aRefDes, 0 };
200
201 int number = 0;
202 const char* first = aRefDes.data() + pos;
203 const char* last = aRefDes.data() + aRefDes.size();
204 auto [ptr, ec] = std::from_chars( first, last, number );
205
206 if( ec != std::errc() || ptr != last )
207 return { aRefDes, 0 };
208
209 return { aRefDes.substr( 0, pos ), number };
210}
211
213{
214 if( aData.m_cacheValid )
215 return;
216
217 // Find the first gap in the sequence starting from 1
218 int candidate = 1;
219 for( int used : aData.m_usedNumbers )
220 {
221 if( used <= 0 )
222 continue; // Skip non-positive numbers (like our 0 marker)
223 if( used == candidate )
224 {
225 candidate++;
226 }
227 else if( used > candidate )
228 {
229 break; // Found a gap
230 }
231 }
232
233 aData.m_baseNext = candidate;
234 aData.m_cacheValid = true;
235}
236
237void REFDES_TRACKER::updateCacheOnInsert( PREFIX_DATA& aData, int aInsertedNumber ) const
238{
239 // Update base next cache if it's valid and affected
240 if( aData.m_cacheValid )
241 {
242 if( aInsertedNumber == aData.m_baseNext )
243 {
244 // The base next was just used, find the new next
245 int candidate = aData.m_baseNext + 1;
246 while( aData.m_usedNumbers.find( candidate ) != aData.m_usedNumbers.end() )
247 {
248 candidate++;
249 }
250 aData.m_baseNext = candidate;
251 }
252 // If aInsertedNumber > m_baseNext, base cache is still valid
253 // If aInsertedNumber < m_baseNext, base cache is still valid
254 }
255
256 for( auto cacheIt = aData.m_nextCache.begin(); cacheIt != aData.m_nextCache.end(); ++cacheIt )
257 {
258 int cachedNext = cacheIt->second;
259
260 if( aInsertedNumber == cachedNext )
261 {
262 // This cached value was just used, need to update it
263 int candidate = cachedNext + 1;
264
265 while( aData.m_usedNumbers.contains( candidate ) )
266 candidate++;
267
268 cacheIt->second = candidate;
269 }
270 }
271}
272
273int REFDES_TRACKER::findNextAvailable( const PREFIX_DATA& aData, int aMinValue ) const
274{
275 if( auto cacheIt = aData.m_nextCache.find( aMinValue ); cacheIt != aData.m_nextCache.end() )
276 return cacheIt->second;
277
278 updateBaseNext( const_cast<PREFIX_DATA&>( aData ) );
279
280 int candidate;
281
282 if( aMinValue <= 1 )
283 {
284 candidate = aData.m_baseNext;
285 }
286 else
287 {
288 // Start search from aMinValue
289 candidate = aMinValue;
290
291 while( aData.m_usedNumbers.find( candidate ) != aData.m_usedNumbers.end() )
292 candidate++;
293 }
294
295 // Cache the result
296 aData.m_nextCache[aMinValue] = candidate;
297
298 return candidate;
299}
300
301std::string REFDES_TRACKER::Serialize() const
302{
303 std::unique_lock<std::mutex> lock;
304 if( m_threadSafe )
305 lock = std::unique_lock<std::mutex>( m_mutex );
306
307 std::ostringstream result;
308 bool first = true;
309
310 using PREFIX_ENTRY = std::pair<const std::string, PREFIX_DATA>;
311
312 std::vector<const PREFIX_ENTRY*> entries;
313 entries.reserve( m_prefixData.size() );
314
315 for( const auto& entry : m_prefixData )
316 entries.push_back( &entry );
317
318 std::sort( entries.begin(), entries.end(),
319 []( const auto* aLeft, const auto* aRight )
320 {
321 return aLeft->first < aRight->first;
322 } );
323
324 for( const auto* entry : entries )
325 {
326 const std::string& prefix = entry->first;
327 const PREFIX_DATA& data = entry->second;
328
329 if( !first )
330 result << ",";
331 first = false;
332
333 std::string escapedPrefix = escapeForSerialization( prefix );
334
335 // Separate numbers from prefix-only entries
336 std::vector<int> numbers;
337 bool hasPrefix = false;
338
339 for( int num : data.m_usedNumbers )
340 {
341 if( num > 0 )
342 numbers.push_back( num );
343 else if( num == 0 )
344 hasPrefix = true;
345 }
346
347 if( numbers.empty() && !hasPrefix )
348 continue; // No data for this prefix
349
350 // Create ranges for numbered entries
351 std::vector<std::pair<int, int>> ranges;
352
353 if( !numbers.empty() )
354 {
355 int start = numbers[0];
356 int end = numbers[0];
357
358 for( size_t i = 1; i < numbers.size(); ++i )
359 {
360 if( numbers[i] == end + 1 )
361 {
362 end = numbers[i];
363 }
364 else
365 {
366 ranges.push_back( { start, end } );
367 start = end = numbers[i];
368 }
369 }
370 ranges.push_back( { start, end } );
371 }
372
373 bool firstRange = true;
374 for( const auto& [start, end] : ranges )
375 {
376 if( !firstRange )
377 result << ",";
378 firstRange = false;
379
380 result << escapedPrefix;
381 if( start == end )
382 {
383 result << start;
384 }
385 else
386 {
387 result << start << "-" << end;
388 }
389 }
390
391 // Add prefix-only entry if it exists
392 if( hasPrefix )
393 {
394 if( !firstRange )
395 result << ",";
396 result << escapedPrefix;
397 }
398 }
399
400 return result.str();
401}
402
403bool REFDES_TRACKER::Deserialize( const std::string& aData )
404{
405 std::unique_lock<std::mutex> lock;
406
407 if( m_threadSafe )
408 lock = std::unique_lock<std::mutex>( m_mutex );
409
410 clearImpl();
411
412 if( aData.empty() )
413 return true;
414
415 auto parts = splitString( aData, ',' );
416
417 // A malformed project file must fail Deserialize cleanly rather than throw,
418 // since QA builds install wxAssertThrower around the calling load path.
419 auto parsePositiveInt = []( const std::ssub_match& aMatch, int& aOut ) -> bool
420 {
421 const char* first = std::to_address( aMatch.first );
422 const char* last = std::to_address( aMatch.second );
423 int value = 0;
424 auto [ptr, ec] = std::from_chars( first, last, value );
425
426 if( ec != std::errc() || ptr != last || value <= 0 )
427 return false;
428
429 aOut = value;
430 return true;
431 };
432
433 // Prefix may contain any non-digit characters, including '#' for power flags
434 // and embedded digits (e.g. "U1U2"), so anchor on the final non-digit before
435 // the trailing digit run. Hoisted out of the loop because std::regex
436 // construction dominates the per-part match cost.
437 const std::regex rangePattern( R"(^(.*\D)(\d+)-(\d+)$)" );
438 const std::regex numberedPattern( R"(^(.*\D)(\d+)$)" );
439 const std::regex prefixOnlyPattern( R"(^(.+)$)" );
440
441 for( const std::string& part : parts )
442 {
443 std::string unescaped = unescapeFromSerialization( part );
444 std::smatch match;
445
446 if( std::regex_match( unescaped, match, rangePattern ) )
447 {
448 std::string prefix = match[1].str();
449 int start = 0;
450 int end = 0;
451
452 if( !parsePositiveInt( match[2], start ) || !parsePositiveInt( match[3], end ) )
453 {
454 clearImpl();
455 return false;
456 }
457
458 for( int i = start; i <= end; ++i )
459 insertImpl( prefix + std::to_string( i ) );
460 }
461 else if( std::regex_match( unescaped, match, numberedPattern ) )
462 {
463 std::string prefix = match[1].str();
464 int number = 0;
465
466 if( !parsePositiveInt( match[2], number ) )
467 {
468 clearImpl();
469 return false;
470 }
471
472 insertImpl( prefix + std::to_string( number ) );
473 }
474 else if( std::regex_match( unescaped, match, prefixOnlyPattern ) )
475 {
476 std::string prefix = match[1].str();
477
478 insertImpl( prefix );
479 }
480 else
481 {
482 // Invalid format
483 clearImpl();
484 return false;
485 }
486 }
487
488 return true;
489}
490
492{
493 std::unique_lock<std::mutex> lock;
494
495 if( m_threadSafe )
496 lock = std::unique_lock<std::mutex>( m_mutex );
497
498 clearImpl();
499}
500
501
503{
504 m_prefixData.clear();
505 m_allRefDes.clear();
506}
507
509{
510 std::unique_lock<std::mutex> lock;
511
512 if( m_threadSafe )
513 lock = std::unique_lock<std::mutex>( m_mutex );
514
515 return m_allRefDes.size();
516}
517
518std::string REFDES_TRACKER::escapeForSerialization( const std::string& aStr ) const
519{
520 std::string result;
521 result.reserve( aStr.length() * 2 ); // Reserve space to avoid frequent reallocations
522
523 for( char c : aStr )
524 {
525 if( c == '\\' || c == ',' || c == '-' )
526 result += '\\';
527 result += c;
528 }
529 return result;
530}
531
532std::string REFDES_TRACKER::unescapeFromSerialization( const std::string& aStr ) const
533{
534 std::string result;
535 result.reserve( aStr.length() );
536
537 bool escaped = false;
538 for( char c : aStr )
539 {
540 if( escaped )
541 {
542 result += c;
543 escaped = false;
544 }
545 else if( c == '\\' )
546 {
547 escaped = true;
548 }
549 else
550 {
551 result += c;
552 }
553 }
554 return result;
555}
556
557std::vector<std::string> REFDES_TRACKER::splitString( const std::string& aStr, char aDelimiter ) const
558{
559 std::vector<std::string> result;
560 std::string current;
561 bool escaped = false;
562
563 for( char c : aStr )
564 {
565 if( escaped )
566 {
567 current += c;
568 escaped = false;
569 }
570 else if( c == '\\' )
571 {
572 escaped = true;
573 current += c;
574 }
575 else if( c == aDelimiter )
576 {
577 result.push_back( current );
578 current.clear();
579 }
580 else
581 {
582 current += c;
583 }
584 }
585
586 if( !current.empty() )
587 result.push_back( current );
588
589 return result;
590}
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.
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 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.
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
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.