KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_re_rule_saver.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_saver.h"
21
22#include <board.h>
23#include <kiplatform/io.h>
24#include <lset.h>
25#include <string_utils.h>
26
27#include <drc_rules_lexer.h>
28
31
32
33namespace
34{
35
36wxString formatLayerClause( const wxString& aLayerSource )
37{
38 if( aLayerSource.IsEmpty() )
39 return wxEmptyString;
40
41 if( aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer )
42 || aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) )
43 return wxString::Format( wxS( "(layer %s)" ), aLayerSource );
44
45 return wxString::Format( wxS( "(layer \"%s\")" ), EscapeString( aLayerSource, CTX_QUOTED_STR ) );
46}
47
48}
49
50
54
55
56bool DRC_RULE_SAVER::SaveFile( const wxString& aPath,
57 const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
58 const BOARD* aBoard )
59{
60 wxString content = GenerateRulesText( aEntries, aBoard );
61 std::string utf8 = std::string( content.mb_str( wxConvUTF8 ) );
62
63 return KIPLATFORM::IO::AtomicWriteFile( aPath, utf8.data(), utf8.size() );
64}
65
66
67wxString DRC_RULE_SAVER::GenerateRulesText( const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
68 const BOARD* aBoard )
69{
70 wxString result = "(version 2)\n";
71
72 // Group entries by (ruleName, condition, layerSource) for merging same-rule constraints.
73 // Including the layer source prevents rules with different layer scopes from being
74 // incorrectly merged (e.g. separate "outer" and "inner" rules must remain distinct).
75 using GroupKey = std::tuple<wxString, wxString, wxString>;
76
77 std::vector<std::pair<GroupKey, std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>>>
78 groupedEntries;
79 std::map<GroupKey, size_t> groupIndex;
80
81 for( const DRC_RE_LOADED_PANEL_ENTRY& entry : aEntries )
82 {
83 auto key = std::make_tuple( entry.ruleName, entry.condition, entry.layerSource );
84 auto it = groupIndex.find( key );
85
86 if( it == groupIndex.end() )
87 {
88 groupIndex[key] = groupedEntries.size();
89 groupedEntries.push_back( { key, { &entry } } );
90 }
91 else
92 {
93 groupedEntries[it->second].second.push_back( &entry );
94 }
95 }
96
97 // Generate rule text for each group
98 for( const auto& [key, entries] : groupedEntries )
99 {
100 wxString ruleText;
101
102 if( entries.size() == 1 )
103 {
104 // Single entry, no merge needed
105 ruleText = generateRuleText( *entries[0], aBoard );
106 }
107 else
108 {
109 // Multiple entries with same name/condition need merging
110 ruleText = generateMergedRuleText( entries, aBoard );
111 }
112
113 if( !ruleText.IsEmpty() )
114 result += ruleText + "\n";
115 }
116
117 return result;
118}
119
120
122 const BOARD* aBoard )
123{
124 // Round-trip preservation: return original text if not edited
125 if( !aEntry.wasEdited && !aEntry.originalRuleText.IsEmpty() )
126 return aEntry.originalRuleText;
127
128 // Otherwise, regenerate from panel data
129 if( !aEntry.constraintData )
130 return wxEmptyString;
131
132 wxString ruleText = aEntry.constraintData->GetGeneratedRule();
133
134 if( ruleText.IsEmpty() || aEntry.panelType == SILK_TO_SOLDERMASK_CLEARANCE
135 || aEntry.panelType == SILK_TO_SILK_CLEARANCE )
136 {
138 ctx.ruleName = aEntry.ruleName;
139 ctx.conditionExpression = aEntry.condition;
141 ctx.comment = aEntry.constraintData->GetComment();
142
144 {
145 wxString silkCond;
146
147 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
148 silkCond = wxS( "L == 'F.Mask'" );
149 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
150 silkCond = wxS( "L == 'B.Mask'" );
151 else
152 silkCond = wxS( "L == 'F.Mask' || L == 'B.Mask'" );
153
154 if( !ctx.conditionExpression.IsEmpty() )
155 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
156 else
157 ctx.conditionExpression = silkCond;
158 }
159 else if( aEntry.panelType == SILK_TO_SILK_CLEARANCE )
160 {
161 wxString silkCond;
162
163 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
164 silkCond = wxS( "L == 'F.SilkS'" );
165 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
166 silkCond = wxS( "L == 'B.SilkS'" );
167 else
168 silkCond = wxS( "L == 'F.SilkS' || L == 'B.SilkS'" );
169
170 if( !ctx.conditionExpression.IsEmpty() )
171 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
172 else
173 ctx.conditionExpression = silkCond;
174 }
175 else if( aBoard )
176 {
177 if( !aEntry.layerSource.IsEmpty() )
178 ctx.layerClause = formatLayerClause( aEntry.layerSource );
179 else
180 ctx.layerClause = generateLayerClause( aEntry.layerCondition, aBoard );
181 }
182
183 ruleText = aEntry.constraintData->GenerateRule( ctx );
184 }
185
186 // If severity is specified and not default, we need to inject it
187 // The GenerateRule method should handle this, but we verify here
189 {
190 wxString severityClause = generateSeverityClause( aEntry.severity );
191
192 if( !severityClause.IsEmpty() && !ruleText.Contains( "(severity" ) )
193 {
194 // Insert severity clause before the closing paren
195 size_t lastParen = ruleText.rfind( ')' );
196
197 if( lastParen != wxString::npos )
198 {
199 ruleText = ruleText.Left( lastParen ) + "\n\t" + severityClause + ")";
200 }
201 }
202 }
203
204 return ruleText;
205}
206
207
208wxString DRC_RULE_SAVER::generateLayerClause( const LSET& aLayers, const BOARD* aBoard )
209{
210 if( !aBoard || !aLayers.any() )
211 return wxEmptyString;
212
213 if( ( aLayers & LSET::AllCuMask() ) == LSET::ExternalCuMask() )
214 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer ) );
215
216 if( ( aLayers & LSET::AllCuMask() ) == LSET::InternalCuMask() )
217 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) );
218
219 // The parser only accepts a single layer name, so emit the first matching layer.
220 // Multi-layer conditions should use "outer" or "inner" keywords above.
221 for( PCB_LAYER_ID layer : aLayers.Seq() )
222 return wxString::Format( wxS( "(layer \"%s\")" ), aBoard->GetLayerName( layer ) );
223
224 return wxEmptyString;
225}
226
227
229{
230 switch( aSeverity )
231 {
232 case RPT_SEVERITY_IGNORE: return "(severity ignore)";
233 case RPT_SEVERITY_WARNING: return "(severity warning)";
234 case RPT_SEVERITY_ERROR: return "(severity error)";
235 case RPT_SEVERITY_EXCLUSION: return "(severity exclusion)";
236 default: return wxEmptyString;
237 }
238}
239
240
242 const std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>& aEntries,
243 const BOARD* aBoard )
244{
245 if( aEntries.empty() )
246 return wxEmptyString;
247
248 // Check if all entries are unedited and the first one has original text
249 // If so, we can use round-trip preservation
250 bool allUnedited = true;
251
252 for( const auto* entry : aEntries )
253 {
254 if( entry->wasEdited )
255 {
256 allUnedited = false;
257 break;
258 }
259 }
260
261 if( allUnedited && !aEntries[0]->originalRuleText.IsEmpty() )
262 return aEntries[0]->originalRuleText;
263
264 // Otherwise, merge constraint clauses from all entries
265 const DRC_RE_LOADED_PANEL_ENTRY* firstEntry = aEntries[0];
266
268 ctx.ruleName = firstEntry->ruleName;
269 ctx.conditionExpression = firstEntry->condition;
270
271 // Generate layer clause from first entry with layer info (all entries in a
272 // merged group share the same layerSource because it's part of the grouping key)
273 for( const auto* entry : aEntries )
274 {
275 if( entry->layerCondition.any() && aBoard )
276 {
277 if( !entry->layerSource.IsEmpty() )
278 ctx.layerClause = formatLayerClause( entry->layerSource );
279 else
280 ctx.layerClause = generateLayerClause( entry->layerCondition, aBoard );
281
282 break;
283 }
284 }
285
286 // Collect all constraint clauses from all entries
287 std::vector<wxString> allClauses;
288
289 for( const auto* entry : aEntries )
290 {
291 if( entry->constraintData )
292 {
294 entryCtx.ruleName = entry->ruleName;
295 entryCtx.conditionExpression = entry->condition;
296 entryCtx.constraintCode = entry->constraintData->GetConstraintCode();
297
298 auto clauses = entry->constraintData->GetConstraintClauses( entryCtx );
299
300 for( const wxString& clause : clauses )
301 {
302 if( !clause.IsEmpty() )
303 allClauses.push_back( clause );
304 }
305 }
306 }
307
308 // Build the merged rule
309 wxString rule;
310 rule << wxS( "(rule " ) << DRC_RE_BASE_CONSTRAINT_DATA::formatRuleName( ctx.ruleName )
311 << wxS( "\n" );
312
313 if( !ctx.layerClause.IsEmpty() )
314 rule << wxS( "\t" ) << ctx.layerClause << wxS( "\n" );
315
316 for( const wxString& clause : allClauses )
317 rule << wxS( "\t" ) << clause << wxS( "\n" );
318
319 if( !ctx.conditionExpression.IsEmpty() )
320 {
321 rule << wxS( "\t(condition \"" )
322 << EscapeString( ctx.conditionExpression, CTX_QUOTED_STR ) << wxS( "\")\n" );
323 }
324
325 // Add severity if any entry has non-default severity
326 for( const auto* entry : aEntries )
327 {
328 if( entry->severity != RPT_SEVERITY_UNDEFINED && entry->severity != RPT_SEVERITY_ERROR )
329 {
330 rule << wxS( "\t" ) << generateSeverityClause( entry->severity ) << wxS( "\n" );
331 break;
332 }
333 }
334
335 rule << wxS( ")" );
336
337 return rule;
338}
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:793
static wxString formatRuleName(const wxString &aRuleName)
Format a rule name for use in S-expression output while preserving the exact name the user chose.
virtual wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext)
bool SaveFile(const wxString &aPath, const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr)
Save all panel entries to a file.
wxString generateLayerClause(const LSET &aLayers, const BOARD *aBoard)
Generate a layer clause from an LSET.
wxString generateRuleText(const DRC_RE_LOADED_PANEL_ENTRY &aEntry, const BOARD *aBoard)
Generate the rule text for a single panel entry.
wxString GenerateRulesText(const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr)
Generate rule text from panel entries.
wxString generateMergedRuleText(const std::vector< const DRC_RE_LOADED_PANEL_ENTRY * > &aEntries, const BOARD *aBoard)
Generate a merged rule text from multiple panel entries with the same name/condition.
wxString generateSeverityClause(SEVERITY aSeverity)
Generate a severity clause.
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & ExternalCuMask()
Return a mask holding the Front and Bottom layers.
Definition lset.cpp:630
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:309
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
static const LSET & InternalCuMask()
Return a complete set of internal copper layers which is all Cu layers except F_Cu and B_Cu.
Definition lset.cpp:573
wxString GetComment()
Get the comment associated with the rule.
@ SILK_TO_SILK_CLEARANCE
@ SILK_TO_SOLDERMASK_CLEARANCE
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
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
SEVERITY
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_UNDEFINED
@ RPT_SEVERITY_EXCLUSION
@ RPT_SEVERITY_IGNORE
wxString EscapeString(const wxString &aSource, ESCAPE_CONTEXT aContext)
The Escape/Unescape routines use HTML-entity-reference-style encoding to handle characters which are:...
@ CTX_QUOTED_STR
Represents a rule loaded from a .kicad_dru file and mapped to a panel.
wxString ruleName
wxString originalRuleText
wxString condition
bool wasEdited
wxString layerSource
Original layer text: "inner", "outer", or layer name.
LSET layerCondition
std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > constraintData
DRC_RULE_EDITOR_CONSTRAINT_NAME panelType
SEVERITY severity
wxString result
Test unit parsing edge cases and error handling.