KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_re_rule_loader.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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "drc_re_rule_loader.h"
21
22#include <base_units.h>
23#include <reporter.h>
25#include <drc/drc_rule_parser.h>
27#include <wx/ffile.h>
28
29#include <tuple>
30#include <utility>
31
46
47
51
52
53double DRC_RULE_LOADER::toMM( int aValue )
54{
55 return aValue / 1000000.0;
56}
57
58
59double DRC_RULE_LOADER::toPS( int aValue )
60{
61 return aValue / pcbIUScale.IU_PER_PS;
62}
63
64
66{
67 for( const DRC_CONSTRAINT& constraint : aRule.m_Constraints )
68 {
69 if( constraint.m_Type == aType )
70 return &constraint;
71 }
72
73 return nullptr;
74}
75
76
77static bool isSymmetricMinOptMax( const DRC_CONSTRAINT* aConstraint )
78{
79 if( !aConstraint )
80 return true;
81
82 const auto& value = aConstraint->GetValue();
83
84 if( !value.HasMin() || !value.HasOpt() || !value.HasMax() )
85 return true;
86
87 return ( value.Opt() - value.Min() ) == ( value.Max() - value.Opt() );
88}
89
90
91// The DRC lexer treats a line whose first non-blank character is # as a comment
92static bool isOnCommentLine( const wxString& aContent, size_t aPos )
93{
94 size_t lineStart = aPos == 0 ? wxString::npos : aContent.rfind( '\n', aPos - 1 );
95
96 for( size_t i = ( lineStart == wxString::npos ) ? 0 : lineStart + 1; i <= aPos; ++i )
97 {
98 if( !wxIsspace( aContent[i] ) )
99 return aContent[i] == '#';
100 }
101
102 return false;
103}
104
105
106// Offset of the paren closing the expression opened at aOpen, or npos when unbalanced
107static size_t findClosingParen( const wxString& aContent, size_t aOpen )
108{
109 int parenCount = 0;
110 bool inString = false;
111 bool escaped = false;
112
113 for( size_t i = aOpen; i < aContent.length(); ++i )
114 {
115 wxUniChar c = aContent[i];
116
117 if( escaped )
118 {
119 escaped = false;
120 continue;
121 }
122
123 if( c == '\\' )
124 {
125 escaped = true;
126 continue;
127 }
128
129 if( c == '"' )
130 {
131 inString = !inString;
132 continue;
133 }
134
135 if( inString )
136 continue;
137
138 if( c == '#' && isOnCommentLine( aContent, i ) )
139 {
140 i = aContent.find( '\n', i );
141
142 if( i == wxString::npos )
143 break;
144 }
145 else if( c == '(' )
146 {
147 parenCount++;
148 }
149 else if( c == ')' )
150 {
151 if( --parenCount == 0 )
152 return i;
153 }
154 }
155
156 return wxString::npos;
157}
158
159
160// Span of the next top-level expression at or after aFrom, skipping comments
161static std::pair<size_t, size_t> findNextExpression( const wxString& aContent, size_t aFrom )
162{
163 for( size_t i = aFrom; i < aContent.length(); ++i )
164 {
165 if( aContent[i] == '#' && isOnCommentLine( aContent, i ) )
166 {
167 i = aContent.find( '\n', i );
168
169 if( i == wxString::npos )
170 break;
171 }
172 else if( aContent[i] == '(' )
173 {
174 size_t close = findClosingParen( aContent, i );
175
176 if( close == wxString::npos )
177 break;
178
179 return { i, close + 1 };
180 }
181 }
182
183 return { wxString::npos, wxString::npos };
184}
185
186
187static bool isVersionExpression( const wxString& aContent, size_t aOpen )
188{
189 size_t keyword = aOpen + 1;
190
191 while( keyword < aContent.length() )
192 {
193 if( aContent[keyword] == '#' && isOnCommentLine( aContent, keyword ) )
194 keyword = aContent.find( '\n', keyword );
195 else if( wxIsspace( aContent[keyword] ) )
196 keyword++;
197 else
198 break;
199 }
200
201 return keyword != wxString::npos && aContent.compare( keyword, 7, wxS( "version" ) ) == 0;
202}
203
204
205// Version clauses may repeat anywhere, so the next rule is the next expression that is not one
206static std::pair<size_t, size_t> findNextRule( const wxString& aContent, size_t aFrom )
207{
208 std::pair<size_t, size_t> span = findNextExpression( aContent, aFrom );
209
210 while( span.first != wxString::npos && isVersionExpression( aContent, span.first ) )
211 span = findNextExpression( aContent, span.second );
212
213 return span;
214}
215
216
217static std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> makeCustomRuleData( const DRC_RULE& aRule )
218{
219 auto customData = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
220 customData->SetRuleName( aRule.m_Name );
221 return customData;
222}
223
224
225wxString DRC_RULE_LOADER::ExtractRuleBody( const wxString& aOriginalText )
226{
227 // Comment lines live in the comment field, not in the body.
228 wxArrayString kept;
229
230 for( const wxString& line : wxSplit( aOriginalText, '\n', '\0' ) )
231 {
232 wxString trimmed = line;
233 trimmed.Trim( false );
234
235 if( !trimmed.StartsWith( wxS( "#" ) ) )
236 kept.Add( line );
237 }
238
239 wxString text = wxJoin( kept, '\n', '\0' );
240
241 int ruleKeyword = text.Find( wxS( "rule " ) );
242 if( ruleKeyword == wxNOT_FOUND )
243 return aOriginalText;
244
245 int bodyStart = text.find( '(', ruleKeyword + 5 );
246 if( bodyStart == (int) wxString::npos )
247 return aOriginalText;
248
249 wxString body = text.Mid( bodyStart );
250 body.Trim( true );
251
252 if( body.EndsWith( wxS( ")" ) ) )
253 body = body.Left( body.Length() - 1 );
254 body.Trim( true );
255
256 return body;
257}
258
259
260wxString DRC_RULE_LOADER::ExtractRuleComment( const wxString& aOriginalText )
261{
262 wxString comment;
263 wxArrayString lines = wxSplit( aOriginalText, '\n', '\0' );
264
265 for( const wxString& line : lines )
266 {
267 wxString trimmed = line;
268 trimmed.Trim( false );
269
270 if( trimmed.StartsWith( wxS( "#" ) ) )
271 {
272 wxString commentLine = trimmed.Mid( 1 );
273 commentLine.Trim( false );
274
275 if( !comment.IsEmpty() )
276 comment += wxS( "\n" );
277
278 comment += commentLine;
279 }
280 }
281
282 return comment;
283}
284
285
286wxString DRC_RULE_LOADER::cleanStrippedCondition( const wxString& aCondition )
287{
288 wxString cleaned = aCondition;
289
290 // Strip empty parentheses left over from removing conditions
291 wxString prev;
292 do
293 {
294 prev = cleaned;
295 cleaned.Replace( wxS( "()" ), wxS( "" ) );
296 } while( cleaned != prev );
297
298 cleaned.Replace( wxS( "&& &&" ), wxS( "&&" ) );
299 cleaned.Replace( wxS( "|| ||" ), wxS( "||" ) );
300 cleaned.Trim( true ).Trim( false );
301 if( cleaned.StartsWith( wxS( "&&" ) ) )
302 cleaned = cleaned.Mid( 2 ).Trim( false );
303 if( cleaned.EndsWith( wxS( "&&" ) ) )
304 cleaned = cleaned.Left( cleaned.Length() - 2 ).Trim( true );
305 if( cleaned.StartsWith( wxS( "||" ) ) )
306 cleaned = cleaned.Mid( 2 ).Trim( false );
307 if( cleaned.EndsWith( wxS( "||" ) ) )
308 cleaned = cleaned.Left( cleaned.Length() - 2 ).Trim( true );
309
310 return cleaned;
311}
312
313
314std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>
316 const DRC_RULE& aRule,
317 const std::set<DRC_CONSTRAINT_T>& aClaimedConstraints )
318{
319 switch( aPanel )
320 {
321 case VIA_STYLE:
322 {
323 auto data = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>();
324 data->SetRuleName( aRule.m_Name );
325 data->SetConstraintCode( "via_style" );
326
327 const DRC_CONSTRAINT* viaDia = findConstraint( aRule, VIA_DIAMETER_CONSTRAINT );
328 const DRC_CONSTRAINT* holeSize = findConstraint( aRule, HOLE_SIZE_CONSTRAINT );
329
330 if( viaDia )
331 {
332 data->SetMinViaDiameter( toMM( viaDia->GetValue().Min() ) );
333 data->SetMaxViaDiameter( toMM( viaDia->GetValue().Max() ) );
334
335 if( viaDia->GetValue().HasOpt() )
336 data->SetOptViaDiameter( toMM( viaDia->GetValue().Opt() ) );
337 }
338
339 if( holeSize )
340 {
341 data->SetMinViaHoleSize( toMM( holeSize->GetValue().Min() ) );
342 data->SetMaxViaHoleSize( toMM( holeSize->GetValue().Max() ) );
343
344 if( holeSize->GetValue().HasOpt() )
345 data->SetOptViaHoleSize( toMM( holeSize->GetValue().Opt() ) );
346 }
347
348 if( aRule.m_Condition )
349 {
350 wxString expr = aRule.m_Condition->GetExpression();
351
352 if( expr.Contains( wxS( "'Micro'" ) ) )
353 data->SetViaType( VIA_STYLE_TYPE::MICRO );
354 else if( expr.Contains( wxS( "'Through'" ) ) )
355 data->SetViaType( VIA_STYLE_TYPE::THROUGH );
356 else if( expr.Contains( wxS( "'Blind'" ) ) )
357 data->SetViaType( VIA_STYLE_TYPE::BLIND );
358 else if( expr.Contains( wxS( "'Buried'" ) ) )
359 data->SetViaType( VIA_STYLE_TYPE::BURIED );
360
361 // Strip the via type condition so it doesn't duplicate on save
362 wxString cleanedCondition = expr;
363 cleanedCondition.Replace( wxS( "A.Via_Type == 'Micro'" ), wxS( "" ) );
364 cleanedCondition.Replace( wxS( "A.Via_Type == 'Through'" ), wxS( "" ) );
365 cleanedCondition.Replace( wxS( "A.Via_Type == 'Blind'" ), wxS( "" ) );
366 cleanedCondition.Replace( wxS( "A.Via_Type == 'Buried'" ), wxS( "" ) );
367
368 data->SetRuleCondition( cleanStrippedCondition( cleanedCondition ) );
369 }
370
371 return data;
372 }
373
375 {
376 const DRC_CONSTRAINT* trackWidth = findConstraint( aRule, TRACK_WIDTH_CONSTRAINT );
377 const DRC_CONSTRAINT* diffGap = findConstraint( aRule, DIFF_PAIR_GAP_CONSTRAINT );
378 const DRC_CONSTRAINT* uncoupled = findConstraint( aRule, MAX_UNCOUPLED_CONSTRAINT );
379
380 if( !isSymmetricMinOptMax( trackWidth ) || !isSymmetricMinOptMax( diffGap ) )
381 {
382 return makeCustomRuleData( aRule );
383 }
384
385 auto data = std::make_shared<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>();
386 data->SetRuleName( aRule.m_Name );
387 data->SetConstraintCode( "diff_pair_gap" );
388
389 if( trackWidth )
390 {
391 double opt = toMM( trackWidth->GetValue().PinnedOpt() );
392 double min = toMM( trackWidth->GetValue().Min() );
393
394 data->SetOptWidth( opt );
395 data->SetWidthTolerance( opt - min );
396 }
397
398 if( diffGap )
399 {
400 double opt = toMM( diffGap->GetValue().PinnedOpt() );
401 double min = toMM( diffGap->GetValue().Min() );
402
403 data->SetOptGap( opt );
404 data->SetGapTolerance( opt - min );
405 }
406
407 if( uncoupled )
408 {
409 data->SetMaxUncoupledLength( toMM( uncoupled->GetValue().Max() ) );
410 }
411
412 return data;
413 }
414
416 {
417 auto data = std::make_shared<DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA>();
418 data->SetRuleName( aRule.m_Name );
419 data->SetConstraintCode( "text_height" );
420
421 const DRC_CONSTRAINT* textHeight = findConstraint( aRule, TEXT_HEIGHT_CONSTRAINT );
422 const DRC_CONSTRAINT* textThickness = findConstraint( aRule, TEXT_THICKNESS_CONSTRAINT );
423
424 if( textHeight )
425 data->SetMinTextHeight( toMM( textHeight->GetValue().Min() ) );
426
427 if( textThickness )
428 data->SetMinTextThickness( toMM( textThickness->GetValue().Min() ) );
429
430 return data;
431 }
432
433 case ROUTING_WIDTH:
434 {
435 const DRC_CONSTRAINT* trackWidth = findConstraint( aRule, TRACK_WIDTH_CONSTRAINT );
436
437 if( !isSymmetricMinOptMax( trackWidth ) )
438 return makeCustomRuleData( aRule );
439
440 auto data = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>();
441 data->SetRuleName( aRule.m_Name );
442 data->SetConstraintCode( "track_width" );
443
444 if( trackWidth )
445 {
446 double opt = toMM( trackWidth->GetValue().PinnedOpt() );
447 double min = toMM( trackWidth->GetValue().Min() );
448
449 data->SetOptWidth( opt );
450 data->SetWidthTolerance( opt - min );
451 }
452
453 return data;
454 }
455
456 case ABSOLUTE_LENGTH:
457 {
458 const DRC_CONSTRAINT* length = findConstraint( aRule, LENGTH_CONSTRAINT );
459
460 if( !isSymmetricMinOptMax( length ) )
461 return makeCustomRuleData( aRule );
462
463 auto data = std::make_shared<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>();
464 data->SetRuleName( aRule.m_Name );
465 data->SetConstraintCode( "length" );
466
467 if( length )
468 {
469 bool timeDomain = length->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN );
470
471 auto convert = [&]( int aValue )
472 {
473 return timeDomain ? toPS( aValue ) : toMM( aValue );
474 };
475
476 double min = convert( length->GetValue().Min() );
477 double max = convert( length->GetValue().Max() );
478
479 // A rule without an optimum gets the window center, so saving keeps its min and max.
480 double opt = length->GetValue().HasOpt() ? convert( length->GetValue().PinnedOpt() ) : ( min + max ) / 2.0;
481
482 data->SetTimeDomain( timeDomain );
483 data->SetOptimumLength( opt );
484 data->SetTolerance( ( max - min ) / 2.0 );
485 }
486
487 return data;
488 }
489
491 {
492 const DRC_CONSTRAINT* length = findConstraint( aRule, LENGTH_CONSTRAINT );
493
494 if( !isSymmetricMinOptMax( length ) )
495 return makeCustomRuleData( aRule );
496
497 auto data = std::make_shared<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>();
498 data->SetRuleName( aRule.m_Name );
499 data->SetConstraintCode( "length" );
500
501 if( length )
502 {
503 double minMM = toMM( length->GetValue().Min() );
504 double optMM = toMM( length->GetValue().PinnedOpt() );
505 double maxMM = toMM( length->GetValue().Max() );
506
507 data->SetOptimumLength( optMM );
508 data->SetTolerance( ( maxMM - minMM ) / 2.0 );
509 }
510
511 const DRC_CONSTRAINT* skew = findConstraint( aRule, SKEW_CONSTRAINT );
512
513 if( skew )
514 {
515 data->SetMaxSkew( toMM( skew->GetValue().Max() ) );
516 data->SetWithinDiffPairs( skew->GetOption( DRC_CONSTRAINT::OPTIONS::SKEW_WITHIN_DIFF_PAIRS ) );
517 }
518
519 return data;
520 }
521
522 case PERMITTED_LAYERS:
523 {
524 auto data = std::make_shared<DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA>();
525 data->SetRuleName( aRule.m_Name );
526
527 const DRC_CONSTRAINT* constraint = findConstraint( aRule, ASSERTION_CONSTRAINT );
528
529 if( constraint && constraint->m_Test )
530 {
531 wxString expr = constraint->m_Test->GetExpression();
532
533 data->SetTopLayerEnabled( expr.Contains( wxS( "F.Cu" ) ) );
534 data->SetBottomLayerEnabled( expr.Contains( wxS( "B.Cu" ) ) );
535
536 // Check for layer references the panel can't represent
537 wxString remaining = expr;
538 remaining.Replace( wxS( "A.Layer == 'F.Cu'" ), wxS( "" ) );
539 remaining.Replace( wxS( "A.Layer == 'B.Cu'" ), wxS( "" ) );
540
541 if( remaining.Contains( wxS( "A.Layer" ) ) )
542 {
543 // Inner or non-standard layers — fall back to custom rule
544 auto customData = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
545 customData->SetRuleName( aRule.m_Name );
546 return customData;
547 }
548 }
549
550 return data;
551 }
552
554 {
555 auto data = std::make_shared<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>();
556 data->SetRuleName( aRule.m_Name );
557 data->SetConstraintCode( wxS( "allowed_orientation" ) );
558
559 const DRC_CONSTRAINT* constraint = findConstraint( aRule, ASSERTION_CONSTRAINT );
560
561 if( constraint && constraint->m_Test )
562 {
563 wxString expr = constraint->m_Test->GetExpression();
564
565 data->SetIsZeroDegreesAllowed( expr.Contains( wxS( "== 0 deg" ) ) );
566 data->SetIsNinetyDegreesAllowed( expr.Contains( wxS( "== 90 deg" ) ) );
567 data->SetIsOneEightyDegreesAllowed( expr.Contains( wxS( "== 180 deg" ) ) );
568 data->SetIsTwoSeventyDegreesAllowed( expr.Contains( wxS( "== 270 deg" ) ) );
569
570 if( data->GetIsZeroDegreesAllowed() && data->GetIsNinetyDegreesAllowed()
571 && data->GetIsOneEightyDegreesAllowed() && data->GetIsTwoSeventyDegreesAllowed() )
572 {
573 data->SetIsAllDegreesAllowed( true );
574 return data;
575 }
576
577 if( data->GetIsZeroDegreesAllowed() || data->GetIsNinetyDegreesAllowed()
578 || data->GetIsOneEightyDegreesAllowed() || data->GetIsTwoSeventyDegreesAllowed() )
579 {
580 return data;
581 }
582
583 // Non-standard angles cannot be represented by the
584 // orientation panel, fall back to custom rule
585 auto customData = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
586 customData->SetRuleName( aRule.m_Name );
587 return customData;
588 }
589 else
590 {
591 data->SetIsAllDegreesAllowed( true );
592 }
593
594 return data;
595 }
596
597 case VIAS_UNDER_SMD:
598 {
599 auto data = std::make_shared<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>();
600 data->SetRuleName( aRule.m_Name );
601 data->SetConstraintCode( wxS( "disallow_via" ) );
602
603 const DRC_CONSTRAINT* constraint = findConstraint( aRule, DISALLOW_CONSTRAINT );
604
605 if( constraint )
606 {
607 data->SetDisallowThroughVias( ( constraint->m_DisallowFlags & DRC_DISALLOW_THROUGH_VIAS ) != 0 );
608 data->SetDisallowMicroVias( ( constraint->m_DisallowFlags & DRC_DISALLOW_MICRO_VIAS ) != 0 );
609 data->SetDisallowBlindVias( ( constraint->m_DisallowFlags & DRC_DISALLOW_BLIND_VIAS ) != 0 );
610 data->SetDisallowBuriedVias( ( constraint->m_DisallowFlags & DRC_DISALLOW_BURIED_VIAS ) != 0 );
611 }
612
613 return data;
614 }
615
616 case CUSTOM_RULE:
617 {
618 auto data = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
619 data->SetRuleName( aRule.m_Name );
620 return data;
621 }
622
623 default:
624 {
625 // For numeric input types, create a generic numeric constraint data
627 {
629 data->SetRuleName( aRule.m_Name );
630
631 wxString code = DRC_RULE_EDITOR_UTILS::GetConstraintCode( aPanel );
632 data->SetConstraintCode( code );
633
634 // Find the first matching constraint from the claimed set
635 for( DRC_CONSTRAINT_T type : aClaimedConstraints )
636 {
637 const DRC_CONSTRAINT* constraint = findConstraint( aRule, type );
638
639 if( constraint )
640 {
642 data->SetNumericInputValue( constraint->GetValue().Max() );
643 else if( type == MICROVIA_ASPECT_RATIO_CONSTRAINT )
644 data->SetNumericInputValue( constraint->GetValue().Max() / 1000.0 );
645 else if( type == MIN_RESOLVED_SPOKES_CONSTRAINT )
646 data->SetNumericInputValue( constraint->GetValue().Min() );
647 else
648 data->SetNumericInputValue( toMM( constraint->GetValue().Min() ) );
649
650 break;
651 }
652 }
653
654 return data;
655 }
656
657 // Fallback to custom rule
658 auto data = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
659 data->SetRuleName( aRule.m_Name );
660 return data;
661 }
662 }
663}
664
665
666std::vector<DRC_RE_LOADED_PANEL_ENTRY> DRC_RULE_LOADER::LoadRule( const DRC_RULE& aRule,
667 const wxString& aOriginalText )
668{
669 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
670
671 // Get condition expression if present
672 wxString condition;
673
674 if( aRule.m_Condition )
675 condition = aRule.m_Condition->GetExpression();
676
677 // Only the absolute length panel can hold time domain values.
678 bool fitsStructuredPanels = true;
679
680 for( const DRC_CONSTRAINT& constraint : aRule.m_Constraints )
681 {
683 && !( constraint.m_Type == LENGTH_CONSTRAINT && aRule.m_Constraints.size() == 1 ) )
684 {
685 fitsStructuredPanels = false;
686 break;
687 }
688 }
689
690 // Match the rule to panels
691 std::vector<DRC_PANEL_MATCH> matches;
692
693 if( fitsStructuredPanels )
694 matches = m_matcher.MatchRule( aRule );
695
696 for( DRC_PANEL_MATCH& match : matches )
697 {
698 if( match.panelType == PERMITTED_LAYERS && match.claimedConstraints.count( ASSERTION_CONSTRAINT ) )
699 {
700 const DRC_CONSTRAINT* assertion = findConstraint( aRule, ASSERTION_CONSTRAINT );
701
702 if( assertion && assertion->m_Test )
703 {
704 wxString expr = assertion->m_Test->GetExpression();
705
706 if( expr.Contains( wxS( "Orientation" ) ) && !expr.Contains( wxS( "Layer" ) ) )
707 match.panelType = ALLOWED_ORIENTATION;
708 }
709 }
710
711 if( match.panelType == SILK_TO_SILK_CLEARANCE && match.claimedConstraints.count( SILK_CLEARANCE_CONSTRAINT ) )
712 {
713 if( !condition.IsEmpty()
714 && ( condition.Contains( wxS( "L == 'F.Mask'" ) ) || condition.Contains( wxS( "L == 'B.Mask'" ) ) ) )
715 {
716 match.panelType = SILK_TO_SOLDERMASK_CLEARANCE;
717 }
718 else if( !condition.IsEmpty() && !condition.Contains( wxS( "L == 'F.SilkS'" ) )
719 && !condition.Contains( wxS( "L == 'B.SilkS'" ) ) )
720 {
721 match.panelType = CUSTOM_RULE;
722 }
723 }
724
725 auto constraintData = createConstraintData( match.panelType, aRule, match.claimedConstraints );
726
727 if( !constraintData )
728 continue;
729
730 // If createConstraintData returned a custom rule fallback (e.g. non-standard
731 // orientation angles), update the panel type to match the actual data type
732 auto customFallback = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( constraintData );
733
734 if( customFallback && match.panelType != CUSTOM_RULE )
735 {
736 match.panelType = CUSTOM_RULE;
737 }
738
739 if( match.panelType == SILK_TO_SOLDERMASK_CLEARANCE )
740 {
741 wxString cleanedCondition = condition;
742
743 // New format: L == 'F.Mask' || L == 'B.Mask'
744 bool hasBothSides = condition.Contains( wxS( "L == 'F.Mask' || L == 'B.Mask'" ) );
745
746 if( hasBothSides )
747 {
748 constraintData->SetLayers( { F_SilkS, B_SilkS } );
749 constraintData->SetLayerSource( wxS( "" ) );
750
751 cleanedCondition.Replace( wxS( "L == 'F.Mask' || L == 'B.Mask'" ), wxS( "" ) );
752 }
753 else
754 {
755 bool isFront = condition.Contains( wxS( "F.Mask" ) );
756 PCB_LAYER_ID layer = isFront ? F_SilkS : B_SilkS;
757 constraintData->SetLayers( { layer } );
758 constraintData->SetLayerSource( isFront ? wxS( "F.SilkS" ) : wxS( "B.SilkS" ) );
759
760 cleanedCondition.Replace( wxS( "L == 'F.Mask'" ), wxS( "" ) );
761 cleanedCondition.Replace( wxS( "L == 'B.Mask'" ), wxS( "" ) );
762 }
763
764 constraintData->SetRuleCondition( cleanStrippedCondition( cleanedCondition ) );
765 }
766
767 if( match.panelType == SILK_TO_SILK_CLEARANCE )
768 {
769 wxString cleanedCondition = condition;
770
771 bool hasBothSides = condition.Contains( wxS( "L == 'F.SilkS' || L == 'B.SilkS'" ) );
772
773 if( hasBothSides )
774 {
775 constraintData->SetLayers( { F_SilkS, B_SilkS } );
776 constraintData->SetLayerSource( wxS( "" ) );
777
778 cleanedCondition.Replace( wxS( "L == 'F.SilkS' || L == 'B.SilkS'" ), wxS( "" ) );
779 }
780 else if( condition.Contains( wxS( "L == 'F.SilkS'" ) ) || condition.Contains( wxS( "L == 'B.SilkS'" ) ) )
781 {
782 bool isFront = condition.Contains( wxS( "F.SilkS" ) );
783 PCB_LAYER_ID layer = isFront ? F_SilkS : B_SilkS;
784 constraintData->SetLayers( { layer } );
785 constraintData->SetLayerSource( isFront ? wxS( "F.SilkS" ) : wxS( "B.SilkS" ) );
786
787 cleanedCondition.Replace( wxS( "L == 'F.SilkS'" ), wxS( "" ) );
788 cleanedCondition.Replace( wxS( "L == 'B.SilkS'" ), wxS( "" ) );
789 }
790
791 constraintData->SetRuleCondition( cleanStrippedCondition( cleanedCondition ) );
792 }
793
794 if( match.panelType == VIAS_UNDER_SMD )
795 {
796 wxString cleanedCondition = condition;
797
798 cleanedCondition.Replace( wxS( "A.Pad_Type == 'SMD'" ), wxS( "" ) );
799 cleanedCondition.Replace( wxS( "B.Pad_Type == 'SMD'" ), wxS( "" ) );
800
801 constraintData->SetRuleCondition( cleanStrippedCondition( cleanedCondition ) );
802 }
803
804 if( match.panelType == CUSTOM_RULE && customFallback )
805 {
806 customFallback->SetRuleText( ExtractRuleBody( aOriginalText ) );
807 }
808
809 if( match.panelType != VIA_STYLE && match.panelType != SILK_TO_SOLDERMASK_CLEARANCE
810 && match.panelType != SILK_TO_SILK_CLEARANCE && match.panelType != VIAS_UNDER_SMD )
811 constraintData->SetRuleCondition( condition );
812
813 DRC_RE_LOADED_PANEL_ENTRY entry( match.panelType, constraintData, aRule.m_Name, condition, aRule.m_Severity,
814 aRule.m_LayerCondition );
815
816 // Preserve original layer source text for round-trip fidelity
817 wxString source = aRule.m_LayerSource;
818 if( source.StartsWith( wxS( "'" ) ) && source.EndsWith( wxS( "'" ) ) )
819 source = source.Mid( 1, source.Length() - 2 );
820 entry.layerSource = source;
821
822 wxString comment = ExtractRuleComment( aOriginalText );
823 if( !comment.IsEmpty() )
824 constraintData->SetComment( comment );
825
826 // Store original text only for the first entry to avoid duplication issues
827 if( entries.empty() )
828 entry.originalRuleText = aOriginalText;
829
830 entries.push_back( std::move( entry ) );
831 }
832
833 // If no matches, create a custom rule entry
834 if( entries.empty() )
835 {
836 auto customData = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>();
837 customData->SetRuleName( aRule.m_Name );
838 customData->SetRuleCondition( condition );
839
840 wxString comment = ExtractRuleComment( aOriginalText );
841 if( !comment.IsEmpty() )
842 customData->SetComment( comment );
843
844 customData->SetRuleText( ExtractRuleBody( aOriginalText ) );
845
846 DRC_RE_LOADED_PANEL_ENTRY entry( CUSTOM_RULE, customData, aRule.m_Name, condition,
847 aRule.m_Severity, aRule.m_LayerCondition );
848 wxString source = aRule.m_LayerSource;
849 if( source.StartsWith( wxS( "'" ) ) && source.EndsWith( wxS( "'" ) ) )
850 source = source.Mid( 1, source.Length() - 2 );
851 entry.layerSource = source;
852 entry.originalRuleText = aOriginalText;
853 entries.push_back( std::move( entry ) );
854 }
855
856 for( DRC_RE_LOADED_PANEL_ENTRY& entry : entries )
857 entry.originalEntryCount = static_cast<int>( entries.size() );
858
859 return entries;
860}
861
862
863std::vector<DRC_RE_LOADED_PANEL_ENTRY> DRC_RULE_LOADER::LoadFromString( const wxString& aRulesText )
864{
865 std::vector<DRC_RE_LOADED_PANEL_ENTRY> allEntries;
866 std::vector<std::shared_ptr<DRC_RULE>> parsedRules;
867
869
870 wxString rulesText = aRulesText;
871 auto [start, end] = findNextExpression( rulesText, 0 );
872
873 if( start == wxString::npos || !isVersionExpression( rulesText, start ) )
874 {
875 rulesText.Prepend( "(version 2)\n" );
876 std::tie( start, end ) = findNextExpression( rulesText, 0 );
877 }
878
879 try
880 {
881 DRC_RULES_PARSER parser( rulesText, "Rule Loader" );
882 parser.Parse( parsedRules, nullptr );
883 }
884 catch( const IO_ERROR& )
885 {
886 return allEntries;
887 }
888
889 // The parser accepted the text, so its non-version expressions are the rules in parse order
890 // and everything between them is kept verbatim for the saver
891 size_t pos = end;
892 m_fileTrivia.header = rulesText.Left( pos );
893
894 int ruleIndex = 0;
895
896 for( const auto& rule : parsedRules )
897 {
898 wxString originalText;
899 wxString leadingTrivia;
900
901 std::tie( start, end ) = findNextRule( rulesText, pos );
902
903 if( start != wxString::npos )
904 {
905 originalText = rulesText.Mid( start, end - start );
906 leadingTrivia = rulesText.Mid( pos, start - pos );
907 pos = end;
908 }
909
910 m_fileTrivia.leadingTrivia.push_back( leadingTrivia );
911
912 std::vector<DRC_RE_LOADED_PANEL_ENTRY> ruleEntries = LoadRule( *rule, originalText );
913
914 for( auto& entry : ruleEntries )
915 {
916 entry.sourceRule = ruleIndex;
917 allEntries.push_back( std::move( entry ) );
918 }
919
920 ruleIndex++;
921 }
922
923 m_fileTrivia.trailer = rulesText.Mid( pos );
924
925 return allEntries;
926}
927
928
929wxString DRC_RULE_LOADER::ExtractRuleText( const wxString& aContent, const wxString& aRuleName )
930{
931 // Search for the rule by name, handling both quoted and unquoted names.
932 // The quoted form includes the closing quote as a boundary so partial
933 // matches like "Clearance" vs "Clearance for BGA" are not possible.
934 // The unquoted form needs an explicit boundary check.
935 wxString quotedSearch = wxString::Format( wxS( "(rule \"%s\"" ), aRuleName );
936 wxString unquotedSearch = wxString::Format( wxS( "(rule %s" ), aRuleName );
937
938 size_t startPos = aContent.find( quotedSearch );
939
940 if( startPos == wxString::npos )
941 {
942 size_t pos = 0;
943
944 while( ( pos = aContent.find( unquotedSearch, pos ) ) != wxString::npos )
945 {
946 size_t afterMatch = pos + unquotedSearch.length();
947
948 if( afterMatch >= aContent.length()
949 || aContent[afterMatch] == ')'
950 || aContent[afterMatch] == ' '
951 || aContent[afterMatch] == '\n'
952 || aContent[afterMatch] == '\r'
953 || aContent[afterMatch] == '\t' )
954 {
955 startPos = pos;
956 break;
957 }
958
959 pos = afterMatch;
960 }
961 }
962
963 if( startPos == wxString::npos )
964 return wxEmptyString;
965
966 size_t endPos = findClosingParen( aContent, startPos );
967
968 if( endPos == wxString::npos )
969 return wxEmptyString;
970
971 return aContent.Mid( startPos, endPos - startPos + 1 );
972}
973
974
975std::vector<DRC_RE_LOADED_PANEL_ENTRY> DRC_RULE_LOADER::LoadFile( const wxString& aPath )
976{
977 std::vector<DRC_RE_LOADED_PANEL_ENTRY> allEntries;
978
980
981 wxFFile file( aPath, "r" );
982
983 if( !file.IsOpened() )
984 return allEntries;
985
986 wxString content;
987 file.ReadAll( &content );
988 file.Close();
989
990 return LoadFromString( content );
991}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
DRC_RULE_CONDITION * m_Test
Definition drc_rule.h:282
int m_DisallowFlags
Definition drc_rule.h:280
const MINOPTMAX< int > & GetValue() const
Definition drc_rule.h:204
DRC_CONSTRAINT_T m_Type
Definition drc_rule.h:278
bool GetOption(OPTIONS option) const
Definition drc_rule.h:268
void Parse(std::vector< std::shared_ptr< DRC_RULE > > &aRules, REPORTER *aReporter)
wxString GetExpression() const
static bool IsNumericInputType(const DRC_RULE_EDITOR_CONSTRAINT_NAME &aConstraintType)
static wxString GetConstraintCode(DRC_RULE_EDITOR_CONSTRAINT_NAME aConstraintType)
Translate a rule tree node type into the keyword used by the rules file for that constraint.
static std::shared_ptr< DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA > CreateNumericConstraintData(DRC_RULE_EDITOR_CONSTRAINT_NAME aType)
double toMM(int aValue)
Convert internal units (nanometers) to millimeters.
DRC_RE_FILE_TRIVIA m_fileTrivia
DRC_PANEL_MATCHER m_matcher
std::vector< DRC_RE_LOADED_PANEL_ENTRY > LoadRule(const DRC_RULE &aRule, const wxString &aOriginalText)
Load a single DRC_RULE and convert it to panel entries.
double toPS(int aValue)
Convert internal time units to picoseconds.
std::vector< DRC_RE_LOADED_PANEL_ENTRY > LoadFromString(const wxString &aRulesText)
Load rules from a text string.
const DRC_CONSTRAINT * findConstraint(const DRC_RULE &aRule, DRC_CONSTRAINT_T aType)
Find a constraint of a specific type in a rule.
static wxString ExtractRuleComment(const wxString &aOriginalText)
Extract comment lines from a rule.
static wxString ExtractRuleBody(const wxString &aOriginalText)
Extract the body of a rule from its original text, stripping the (rule "name" ...) wrapper.
static wxString ExtractRuleText(const wxString &aContent, const wxString &aRuleName)
Extract the complete original text of a rule from file content.
std::vector< DRC_RE_LOADED_PANEL_ENTRY > LoadFile(const wxString &aPath)
Load all rules from a .kicad_dru file.
wxString cleanStrippedCondition(const wxString &aCondition)
Clean up a condition string after auto-generated tokens have been removed.
std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > createConstraintData(DRC_RULE_EDITOR_CONSTRAINT_NAME aPanel, const DRC_RULE &aRule, const std::set< DRC_CONSTRAINT_T > &aClaimedConstraints)
Create the appropriate constraint data object for a panel type.
SEVERITY m_Severity
Definition drc_rule.h:161
DRC_RULE_CONDITION * m_Condition
Definition drc_rule.h:159
LSET m_LayerCondition
Definition drc_rule.h:158
std::vector< DRC_CONSTRAINT > m_Constraints
Definition drc_rule.h:160
wxString m_Name
Definition drc_rule.h:155
wxString m_LayerSource
Definition drc_rule.h:157
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
T Min() const
Definition minoptmax.h:29
T PinnedOpt() const
Definition minoptmax.h:32
T Max() const
Definition minoptmax.h:30
T Opt() const
Definition minoptmax.h:31
bool HasOpt() const
Definition minoptmax.h:39
static std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > makeCustomRuleData(const DRC_RULE &aRule)
static std::pair< size_t, size_t > findNextExpression(const wxString &aContent, size_t aFrom)
static bool isOnCommentLine(const wxString &aContent, size_t aPos)
static size_t findClosingParen(const wxString &aContent, size_t aOpen)
static bool isVersionExpression(const wxString &aContent, size_t aOpen)
static std::pair< size_t, size_t > findNextRule(const wxString &aContent, size_t aFrom)
static bool isSymmetricMinOptMax(const DRC_CONSTRAINT *aConstraint)
@ DRC_DISALLOW_BURIED_VIAS
Definition drc_rule.h:100
@ DRC_DISALLOW_BLIND_VIAS
Definition drc_rule.h:99
@ DRC_DISALLOW_THROUGH_VIAS
Definition drc_rule.h:97
@ DRC_DISALLOW_MICRO_VIAS
Definition drc_rule.h:98
DRC_CONSTRAINT_T
Definition drc_rule.h:49
@ VIA_DIAMETER_CONSTRAINT
Definition drc_rule.h:72
@ DIFF_PAIR_GAP_CONSTRAINT
Definition drc_rule.h:78
@ DISALLOW_CONSTRAINT
Definition drc_rule.h:71
@ TRACK_WIDTH_CONSTRAINT
Definition drc_rule.h:61
@ SILK_CLEARANCE_CONSTRAINT
Definition drc_rule.h:58
@ MIN_RESOLVED_SPOKES_CONSTRAINT
Definition drc_rule.h:67
@ TEXT_THICKNESS_CONSTRAINT
Definition drc_rule.h:60
@ LENGTH_CONSTRAINT
Definition drc_rule.h:73
@ VIA_COUNT_CONSTRAINT
Definition drc_rule.h:81
@ MICROVIA_ASPECT_RATIO_CONSTRAINT
Definition drc_rule.h:91
@ MAX_UNCOUPLED_CONSTRAINT
Definition drc_rule.h:79
@ ASSERTION_CONSTRAINT
Definition drc_rule.h:84
@ SKEW_CONSTRAINT
Definition drc_rule.h:77
@ MICROVIA_STACK_DEPTH_CONSTRAINT
Definition drc_rule.h:90
@ HOLE_SIZE_CONSTRAINT
Definition drc_rule.h:56
@ TEXT_HEIGHT_CONSTRAINT
Definition drc_rule.h:59
DRC_RULE_EDITOR_CONSTRAINT_NAME
@ ALLOWED_ORIENTATION
@ SILK_TO_SILK_CLEARANCE
@ ROUTING_DIFF_PAIR
@ SILK_TO_SOLDERMASK_CLEARANCE
@ ABSOLUTE_LENGTH
@ PERMITTED_LAYERS
@ MINIMUM_TEXT_HEIGHT_AND_THICKNESS
@ ROUTING_WIDTH
@ MATCHED_LENGTH_DIFF_PAIR
std::string source
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_SilkS
Definition layer_ids.h:96
@ B_SilkS
Definition layer_ids.h:97
Result of matching a panel to constraints.
The text of a rules file that lies outside every rule, kept so a save can reproduce it.
Represents a rule loaded from a .kicad_dru file and mapped to a panel.
wxString originalRuleText
wxString layerSource
Original layer text: "inner", "outer", or layer name.
VECTOR2I end