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