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 for( const auto& [prefix, data] : m_prefixData )
311 {
312 if( !first )
313 result << ",";
314 first = false;
315
316 std::string escapedPrefix = escapeForSerialization( prefix );
317
318 // Separate numbers from prefix-only entries
319 std::vector<int> numbers;
320 bool hasPrefix = false;
321
322 for( int num : data.m_usedNumbers )
323 {
324 if( num > 0 )
325 numbers.push_back( num );
326 else if( num == 0 )
327 hasPrefix = true;
328 }
329
330 if( numbers.empty() && !hasPrefix )
331 continue; // No data for this prefix
332
333 // Create ranges for numbered entries
334 std::vector<std::pair<int, int>> ranges;
335
336 if( !numbers.empty() )
337 {
338 int start = numbers[0];
339 int end = numbers[0];
340
341 for( size_t i = 1; i < numbers.size(); ++i )
342 {
343 if( numbers[i] == end + 1 )
344 {
345 end = numbers[i];
346 }
347 else
348 {
349 ranges.push_back( { start, end } );
350 start = end = numbers[i];
351 }
352 }
353 ranges.push_back( { start, end } );
354 }
355
356 bool firstRange = true;
357 for( const auto& [start, end] : ranges )
358 {
359 if( !firstRange )
360 result << ",";
361 firstRange = false;
362
363 result << escapedPrefix;
364 if( start == end )
365 {
366 result << start;
367 }
368 else
369 {
370 result << start << "-" << end;
371 }
372 }
373
374 // Add prefix-only entry if it exists
375 if( hasPrefix )
376 {
377 if( !firstRange )
378 result << ",";
379 result << escapedPrefix;
380 }
381 }
382
383 return result.str();
384}
385
386bool REFDES_TRACKER::Deserialize( const std::string& aData )
387{
388 std::unique_lock<std::mutex> lock;
389
390 if( m_threadSafe )
391 lock = std::unique_lock<std::mutex>( m_mutex );
392
393 clearImpl();
394
395 if( aData.empty() )
396 return true;
397
398 auto parts = splitString( aData, ',' );
399
400 // A malformed project file must fail Deserialize cleanly rather than throw,
401 // since QA builds install wxAssertThrower around the calling load path.
402 auto parsePositiveInt = []( const std::ssub_match& aMatch, int& aOut ) -> bool
403 {
404 const char* first = std::to_address( aMatch.first );
405 const char* last = std::to_address( aMatch.second );
406 int value = 0;
407 auto [ptr, ec] = std::from_chars( first, last, value );
408
409 if( ec != std::errc() || ptr != last || value <= 0 )
410 return false;
411
412 aOut = value;
413 return true;
414 };
415
416 // Prefix may contain any non-digit characters, including '#' for power flags
417 // and embedded digits (e.g. "U1U2"), so anchor on the final non-digit before
418 // the trailing digit run. Hoisted out of the loop because std::regex
419 // construction dominates the per-part match cost.
420 const std::regex rangePattern( R"(^(.*\D)(\d+)-(\d+)$)" );
421 const std::regex numberedPattern( R"(^(.*\D)(\d+)$)" );
422 const std::regex prefixOnlyPattern( R"(^(.+)$)" );
423
424 for( const std::string& part : parts )
425 {
426 std::string unescaped = unescapeFromSerialization( part );
427 std::smatch match;
428
429 if( std::regex_match( unescaped, match, rangePattern ) )
430 {
431 std::string prefix = match[1].str();
432 int start = 0;
433 int end = 0;
434
435 if( !parsePositiveInt( match[2], start ) || !parsePositiveInt( match[3], end ) )
436 {
437 clearImpl();
438 return false;
439 }
440
441 for( int i = start; i <= end; ++i )
442 insertImpl( prefix + std::to_string( i ) );
443 }
444 else if( std::regex_match( unescaped, match, numberedPattern ) )
445 {
446 std::string prefix = match[1].str();
447 int number = 0;
448
449 if( !parsePositiveInt( match[2], number ) )
450 {
451 clearImpl();
452 return false;
453 }
454
455 insertImpl( prefix + std::to_string( number ) );
456 }
457 else if( std::regex_match( unescaped, match, prefixOnlyPattern ) )
458 {
459 std::string prefix = match[1].str();
460
461 insertImpl( prefix );
462 }
463 else
464 {
465 // Invalid format
466 clearImpl();
467 return false;
468 }
469 }
470
471 return true;
472}
473
475{
476 std::unique_lock<std::mutex> lock;
477
478 if( m_threadSafe )
479 lock = std::unique_lock<std::mutex>( m_mutex );
480
481 clearImpl();
482}
483
484
486{
487 m_prefixData.clear();
488 m_allRefDes.clear();
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 return m_allRefDes.size();
499}
500
501std::string REFDES_TRACKER::escapeForSerialization( const std::string& aStr ) const
502{
503 std::string result;
504 result.reserve( aStr.length() * 2 ); // Reserve space to avoid frequent reallocations
505
506 for( char c : aStr )
507 {
508 if( c == '\\' || c == ',' || c == '-' )
509 result += '\\';
510 result += c;
511 }
512 return result;
513}
514
515std::string REFDES_TRACKER::unescapeFromSerialization( const std::string& aStr ) const
516{
517 std::string result;
518 result.reserve( aStr.length() );
519
520 bool escaped = false;
521 for( char c : aStr )
522 {
523 if( escaped )
524 {
525 result += c;
526 escaped = false;
527 }
528 else if( c == '\\' )
529 {
530 escaped = true;
531 }
532 else
533 {
534 result += c;
535 }
536 }
537 return result;
538}
539
540std::vector<std::string> REFDES_TRACKER::splitString( const std::string& aStr, char aDelimiter ) const
541{
542 std::vector<std::string> result;
543 std::string current;
544 bool escaped = false;
545
546 for( char c : aStr )
547 {
548 if( escaped )
549 {
550 current += c;
551 escaped = false;
552 }
553 else if( c == '\\' )
554 {
555 escaped = true;
556 current += c;
557 }
558 else if( c == aDelimiter )
559 {
560 result.push_back( current );
561 current.clear();
562 }
563 else
564 {
565 current += c;
566 }
567 }
568
569 if( !current.empty() )
570 result.push_back( current );
571
572 return result;
573}
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.