KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_diff_scene_helpers.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 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * 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/gpl-3.0.html
19 * or you may search the http://www.gnu.org website for the version 3 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 <boost/test/unit_test.hpp>
25
30
31#include <algorithm>
32
33
34using namespace KICAD_DIFF;
35
36
37BOOST_AUTO_TEST_SUITE( DiffSceneHelpers )
38
39
40// CategoryFor / PAINT_ORDER coverage ----------------------------------------
41
42BOOST_AUTO_TEST_CASE( CategoryForMapsAllChangeKinds )
43{
44 BOOST_CHECK( CategoryFor( CHANGE_KIND::ADDED ) == CATEGORY::ADDED );
45 BOOST_CHECK( CategoryFor( CHANGE_KIND::REMOVED ) == CATEGORY::REMOVED );
46 BOOST_CHECK( CategoryFor( CHANGE_KIND::MODIFIED ) == CATEGORY::MODIFIED );
47 BOOST_CHECK( CategoryFor( CHANGE_KIND::COLLISION ) == CATEGORY::CONFLICT );
48 BOOST_CHECK( CategoryFor( CHANGE_KIND::DUPLICATE_UUID ) == CATEGORY::CONFLICT );
49}
50
51
52BOOST_AUTO_TEST_CASE( PaintOrderIsExhaustiveAndUnique )
53{
55
56 std::set<CATEGORY> seen( PAINT_ORDER.begin(), PAINT_ORDER.end() );
57 BOOST_CHECK_EQUAL( seen.size(), CATEGORY_COUNT );
58}
59
60
61// ShapesFor ------------------------------------------------------------------
62
63BOOST_AUTO_TEST_CASE( ShapesForReturnsCorrectListPerCategory )
64{
65 DIFF_SCENE scene;
66
67 SCENE_SHAPE added;
68 added.label = wxS( "a" );
69 scene.addedShapes.push_back( added );
70 SCENE_SHAPE removed;
71 removed.label = wxS( "r" );
72 scene.removedShapes.push_back( removed );
73 SCENE_SHAPE modified;
74 modified.label = wxS( "m" );
75 scene.modifiedShapes.push_back( modified );
76 SCENE_SHAPE conflict;
77 conflict.label = wxS( "c" );
78 scene.conflictShapes.push_back( conflict );
79
80 BOOST_CHECK( ShapesFor( scene, CATEGORY::ADDED ).at( 0 ).label == wxS( "a" ) );
81 BOOST_CHECK( ShapesFor( scene, CATEGORY::REMOVED ).at( 0 ).label == wxS( "r" ) );
82 BOOST_CHECK( ShapesFor( scene, CATEGORY::MODIFIED ).at( 0 ).label == wxS( "m" ) );
83 BOOST_CHECK( ShapesFor( scene, CATEGORY::CONFLICT ).at( 0 ).label == wxS( "c" ) );
84}
85
86
87// MakeBBoxOutline ------------------------------------------------------------
88
89BOOST_AUTO_TEST_CASE( MakeBBoxOutlineDegenerateBoxIsEmpty )
90{
92 BOOST_CHECK( poly.outline.empty() );
93}
94
95
96BOOST_AUTO_TEST_CASE( MakeBBoxOutlineFourCornersInOrder )
97{
98 const BOX2I box( VECTOR2I( 10, 20 ), VECTOR2I( 100, 50 ) );
99 const KIGFX::COLOR4D color( 0.1, 0.2, 0.3, 1.0 );
100
101 DOCUMENT_POLYGON poly = MakeBBoxOutline( box, color, /*lineWidth*/ 3 );
102
103 BOOST_CHECK_EQUAL( poly.outline.size(), 4 );
104 BOOST_CHECK( poly.outline[0] == box.GetOrigin() );
105 BOOST_CHECK( poly.outline[2] == box.GetEnd() );
106 BOOST_CHECK_EQUAL( poly.lineWidth, 3 );
107 BOOST_CHECK( !poly.filled );
108 BOOST_CHECK( poly.color == color );
109}
110
111
112// Shared presentation helpers -----------------------------------------------
113
114BOOST_AUTO_TEST_CASE( ChangeDisplayLabelIncludesRefdesWhenPresent )
115{
116 ITEM_CHANGE change;
117 change.typeName = wxS( "FOOTPRINT" );
118 change.refdes = wxS( "U7" );
119
120 BOOST_CHECK( ChangeDisplayLabel( change ) == wxS( "FOOTPRINT [U7]" ) );
121}
122
123
124BOOST_AUTO_TEST_CASE( ChangeDisplayLabelFallsBackToTypeName )
125{
126 ITEM_CHANGE change;
127 change.typeName = wxS( "ZONE" );
128
129 BOOST_CHECK( ChangeDisplayLabel( change ) == wxS( "ZONE" ) );
130}
131
132
133BOOST_AUTO_TEST_CASE( IsRoutingNetChangeRequiresRoutingTypeAndRefdes )
134{
135 ITEM_CHANGE track;
136 track.typeName = wxS( "PCB_TRACK" );
137 track.refdes = wxS( "GND" );
138 BOOST_CHECK( IsRoutingNetChange( track ) );
139
141 pad.typeName = wxS( "PAD" );
142 pad.refdes = wxS( "GND" );
143 BOOST_CHECK( !IsRoutingNetChange( pad ) );
144
145 ITEM_CHANGE noNet;
146 noNet.typeName = wxS( "PCB_VIA" );
147 BOOST_CHECK( !IsRoutingNetChange( noNet ) );
148}
149
150
151BOOST_AUTO_TEST_CASE( AppendGeometryMovesEveryPrimitiveKind )
152{
155
156 src.segments.push_back( DOCUMENT_SEGMENT{} );
157 src.polygons.push_back( DOCUMENT_POLYGON{} );
158 src.circles.push_back( DOCUMENT_CIRCLE{} );
159
160 AppendGeometry( dst, std::move( src ) );
161
162 BOOST_CHECK_EQUAL( dst.segments.size(), 1u );
163 BOOST_CHECK_EQUAL( dst.polygons.size(), 1u );
164 BOOST_CHECK_EQUAL( dst.circles.size(), 1u );
165}
166
167
168// HighlightedBBox -----------------------------------------------------------
169
170namespace
171{
172
173SCENE_SHAPE makeShape( const wxString& aId, const BOX2I& aBox )
174{
175 SCENE_SHAPE s;
176 s.changeId = KIID_PATH( aId );
177 s.bbox = aBox;
178 return s;
179}
180
181} // namespace
182
183
184BOOST_AUTO_TEST_CASE( HighlightedBBoxFindsAcrossCategories )
185{
186 DIFF_SCENE scene;
187 scene.addedShapes.push_back( makeShape( wxS( "/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ),
188 BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) ) );
189 scene.modifiedShapes.push_back( makeShape( wxS( "/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" ),
190 BOX2I( VECTOR2I( 100, 100 ), VECTOR2I( 10, 10 ) ) ) );
191
192 std::array<bool, CATEGORY_COUNT> allVisible{ { true, true, true, true } };
193
194 auto found = HighlightedBBox( scene, KIID_PATH( wxS( "/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" ) ), allVisible );
195
196 BOOST_REQUIRE( found.has_value() );
197 BOOST_CHECK( found->Contains( VECTOR2I( 105, 105 ) ) );
198}
199
200
201BOOST_AUTO_TEST_CASE( HighlightedBBoxIgnoresHiddenCategory )
202{
203 DIFF_SCENE scene;
204 scene.addedShapes.push_back( makeShape( wxS( "/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ),
205 BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) ) );
206
207 // ADDED hidden, others visible.
208 std::array<bool, CATEGORY_COUNT> visible{ { false, true, true, true } };
209
210 auto found = HighlightedBBox( scene, KIID_PATH( wxS( "/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ) ), visible );
211
212 BOOST_CHECK( !found.has_value() );
213}
214
215
216BOOST_AUTO_TEST_CASE( HighlightedBBoxUnionsMultipleMatches )
217{
218 DIFF_SCENE scene;
219
220 // Same changeId on two shapes — e.g. a DUPLICATE_UUID reported twice with
221 // bboxes at different locations. The helper should union them.
222 const wxString id = wxS( "/cccccccc-cccc-cccc-cccc-cccccccccccc" );
223 scene.conflictShapes.push_back( makeShape( id, BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) ) );
224 scene.conflictShapes.push_back( makeShape( id, BOX2I( VECTOR2I( 100, 100 ), VECTOR2I( 10, 10 ) ) ) );
225
226 std::array<bool, CATEGORY_COUNT> allVisible{ { true, true, true, true } };
227 auto found = HighlightedBBox( scene, KIID_PATH( id ), allVisible );
228
229 BOOST_REQUIRE( found.has_value() );
230 // Union must contain both centers.
231 BOOST_CHECK( found->Contains( VECTOR2I( 5, 5 ) ) );
232 BOOST_CHECK( found->Contains( VECTOR2I( 105, 105 ) ) );
233}
234
235
236// CollectChangeBBoxes -------------------------------------------------------
237
238BOOST_AUTO_TEST_CASE( CollectChangeBBoxesFlatList )
239{
240 DOCUMENT_DIFF diff;
241
242 ITEM_CHANGE c1;
243 c1.id = KIID_PATH( wxS( "/11111111-1111-1111-1111-111111111111" ) );
244 c1.bbox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) );
245
246 ITEM_CHANGE c2;
247 c2.id = KIID_PATH( wxS( "/22222222-2222-2222-2222-222222222222" ) );
248 c2.bbox = BOX2I( VECTOR2I( 50, 50 ), VECTOR2I( 5, 5 ) );
249
250 // Degenerate bbox — should be skipped.
251 ITEM_CHANGE c3;
252 c3.id = KIID_PATH( wxS( "/33333333-3333-3333-3333-333333333333" ) );
253 c3.bbox = BOX2I();
254
255 diff.changes = { c1, c2, c3 };
256
257 std::map<KIID_PATH, BOX2I> out;
258 CollectChangeBBoxes( diff, out );
259
260 BOOST_CHECK_EQUAL( out.size(), 2 );
261 BOOST_CHECK( out.count( c1.id ) == 1 );
262 BOOST_CHECK( out.count( c2.id ) == 1 );
263 BOOST_CHECK( out.count( c3.id ) == 0 );
264}
265
266
267BOOST_AUTO_TEST_CASE( CollectChangeBBoxesRecursesIntoChildren )
268{
269 DOCUMENT_DIFF diff;
270
271 ITEM_CHANGE parent;
272 parent.id = KIID_PATH( wxS( "/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ) );
273 parent.bbox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) );
274
275 ITEM_CHANGE child;
276 child.id = KIID_PATH( wxS( "/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" ) );
277 child.bbox = BOX2I( VECTOR2I( 10, 10 ), VECTOR2I( 5, 5 ) );
278
279 parent.children = { child };
280 diff.changes = { parent };
281
282 std::map<KIID_PATH, BOX2I> out;
283 CollectChangeBBoxes( diff, out );
284
285 BOOST_CHECK_EQUAL( out.size(), 2 );
286 BOOST_CHECK( out.count( parent.id ) == 1 );
287 BOOST_CHECK( out.count( child.id ) == 1 );
288}
289
290
291BOOST_AUTO_TEST_CASE( CollectChangeBBoxesPreservesExistingEntries )
292{
293 DOCUMENT_DIFF diff;
294
295 ITEM_CHANGE c;
296 c.id = KIID_PATH( wxS( "/11111111-1111-1111-1111-111111111111" ) );
297 c.bbox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) );
298 diff.changes = { c };
299
300 std::map<KIID_PATH, BOX2I> out;
301 out.emplace( c.id, BOX2I( VECTOR2I( 999, 999 ), VECTOR2I( 1, 1 ) ) );
302
303 CollectChangeBBoxes( diff, out );
304
305 // emplace is no-op on existing key — the pre-existing entry wins.
306 BOOST_CHECK( out.at( c.id ).GetOrigin() == VECTOR2I( 999, 999 ) );
307}
308
309
310// ExpandBBoxToGeometry ------------------------------------------------------
311
312BOOST_AUTO_TEST_CASE( ExpandBBoxToGeometryNoOpOnEmptyGeometry )
313{
314 DIFF_SCENE scene;
315 scene.documentBBox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) );
316
317 const BOX2I before = scene.documentBBox;
318 ExpandBBoxToGeometry( scene );
319
320 BOOST_CHECK( scene.documentBBox.GetOrigin() == before.GetOrigin() );
321 BOOST_CHECK( scene.documentBBox.GetEnd() == before.GetEnd() );
322}
323
324
325BOOST_AUTO_TEST_CASE( ExpandBBoxToGeometryIncludesSegmentExtent )
326{
327 DIFF_SCENE scene;
328 scene.documentBBox = BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) );
329
331 seg.start = VECTOR2I( -50, -50 );
332 seg.end = VECTOR2I( 200, 200 );
333 seg.width = 0;
334 scene.referenceGeometry.segments.push_back( seg );
335
336 ExpandBBoxToGeometry( scene );
337
338 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( -50, -50 ) ) );
339 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( 200, 200 ) ) );
340}
341
342
343BOOST_AUTO_TEST_CASE( ExpandBBoxToGeometryInflatesByStrokeWidth )
344{
345 // Thick segment from (0,0) to (0,0) — the stroke width is the entire
346 // extent. ExpandBBoxToGeometry must inflate by width/2 so the bbox
347 // covers the visible stroke.
348 DIFF_SCENE scene;
349
351 seg.start = VECTOR2I( 0, 0 );
352 seg.end = VECTOR2I( 0, 0 );
353 seg.width = 40;
354 scene.referenceGeometry.segments.push_back( seg );
355
356 ExpandBBoxToGeometry( scene );
357
358 BOOST_CHECK_GE( scene.documentBBox.GetWidth(), 40 );
359 BOOST_CHECK_GE( scene.documentBBox.GetHeight(), 40 );
360}
361
362
363BOOST_AUTO_TEST_CASE( ExpandBBoxToGeometryIncludesCircleExtent )
364{
365 DIFF_SCENE scene;
366
368 c.center = VECTOR2I( 100, 100 );
369 c.radius = 50;
370 c.lineWidth = 0;
371 scene.comparisonGeometry.circles.push_back( c );
372
373 ExpandBBoxToGeometry( scene );
374
375 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( 50, 100 ) ) );
376 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( 150, 100 ) ) );
377 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( 100, 50 ) ) );
378 BOOST_CHECK( scene.documentBBox.Contains( VECTOR2I( 100, 150 ) ) );
379}
380
381
382// ItemRes string round-trip -------------------------------------------------
383
384BOOST_AUTO_TEST_CASE( ItemResStringRoundTrip )
385{
388 {
389 const std::string s = ItemResToString( kind );
390 const ITEM_RES out = ItemResFromString( s );
391 BOOST_CHECK( out == kind );
392 }
393}
394
395
396BOOST_AUTO_TEST_CASE( ItemResFromStringRejectsUnknown )
397{
398 BOOST_CHECK_THROW( ItemResFromString( "TAKE_OURS" ), std::invalid_argument );
399 BOOST_CHECK_THROW( ItemResFromString( "" ), std::invalid_argument );
400 BOOST_CHECK_THROW( ItemResFromString( "bogus" ), std::invalid_argument );
401}
402
403
404// PropRes string round-trip -------------------------------------------------
405
406BOOST_AUTO_TEST_CASE( PropResStringRoundTrip )
407{
409 {
410 const std::string s = PropResToString( kind );
411 const PROP_RES out = PropResFromString( s );
412 BOOST_CHECK( out == kind );
413 }
414}
415
416
417BOOST_AUTO_TEST_CASE( PropResStringLiterals )
418{
419 // Pin the literal spellings — paired serializer/parser regressions
420 // that flipped both ends to a new spelling would still round-trip,
421 // but downstream tools that read the wire format directly would
422 // break. The PROPERTY_RESOLUTION JSON consumers (kicad-cli mergetool
423 // scripted resolutions in particular) depend on these strings.
424 BOOST_CHECK_EQUAL( std::string( PropResToString( PROP_RES::OURS ) ), "ours" );
425 BOOST_CHECK_EQUAL( std::string( PropResToString( PROP_RES::THEIRS ) ), "theirs" );
426 BOOST_CHECK_EQUAL( std::string( PropResToString( PROP_RES::ANCESTOR ) ), "ancestor" );
427 BOOST_CHECK_EQUAL( std::string( PropResToString( PROP_RES::CUSTOM ) ), "custom" );
428}
429
430
431BOOST_AUTO_TEST_CASE( PropResFromStringRejectsUnknown )
432{
433 BOOST_CHECK_THROW( PropResFromString( "OURS" ), std::invalid_argument );
434 BOOST_CHECK_THROW( PropResFromString( "" ), std::invalid_argument );
435 BOOST_CHECK_THROW( PropResFromString( "bogus" ), std::invalid_argument );
436}
437
438
439// BBoxFromGeometry coverage -------------------------------------------------
440
441BOOST_AUTO_TEST_CASE( BBoxFromGeometry_EmptyReturnsNullopt )
442{
444 BOOST_CHECK( !BBoxFromGeometry( g ).has_value() );
445}
446
447
448BOOST_AUTO_TEST_CASE( BBoxFromGeometry_SegmentInflatedByHalfStroke )
449{
452 s.start = VECTOR2I( 0, 0 );
453 s.end = VECTOR2I( 100, 0 );
454 s.width = 20;
455 g.segments.push_back( s );
456
457 const auto bbox = BBoxFromGeometry( g );
458 BOOST_REQUIRE( bbox.has_value() );
459 // Each endpoint contributes [-10,-10]..[+10,+10] around itself.
460 BOOST_CHECK_EQUAL( bbox->GetLeft(), -10 );
461 BOOST_CHECK_EQUAL( bbox->GetRight(), 110 );
462 BOOST_CHECK_EQUAL( bbox->GetTop(), -10 );
463 BOOST_CHECK_EQUAL( bbox->GetBottom(), 10 );
464}
465
466
467BOOST_AUTO_TEST_CASE( BBoxFromGeometry_NegativeStrokeBecomesHairline )
468{
469 // width < 0 takes the EffectivePlotWidth path (substitutes the renderer's
470 // hairline) so the bbox includes the half-hairline inflation the renderer
471 // actually paints.
474 s.start = VECTOR2I( 50, 50 );
475 s.end = VECTOR2I( 50, 50 );
476 s.width = -100;
477 g.segments.push_back( s );
478
479 const int halfHairline = PLOT_HAIRLINE_IU / 2;
480 const auto bbox = BBoxFromGeometry( g );
481 BOOST_REQUIRE( bbox.has_value() );
482 BOOST_CHECK_EQUAL( bbox->GetLeft(), 50 - halfHairline );
483 BOOST_CHECK_EQUAL( bbox->GetRight(), 50 + halfHairline );
484}
485
486
487BOOST_AUTO_TEST_CASE( BBoxFromGeometry_PolygonInflatedByHalfLineWidth )
488{
491 p.outline = { VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ), VECTOR2I( 100, 50 ) };
492 p.lineWidth = 6;
493 g.polygons.push_back( p );
494
495 const auto bbox = BBoxFromGeometry( g );
496 BOOST_REQUIRE( bbox.has_value() );
497 BOOST_CHECK_EQUAL( bbox->GetLeft(), -3 );
498 BOOST_CHECK_EQUAL( bbox->GetRight(), 103 );
499 BOOST_CHECK_EQUAL( bbox->GetTop(), -3 );
500 BOOST_CHECK_EQUAL( bbox->GetBottom(), 53 );
501}
502
503
504BOOST_AUTO_TEST_CASE( BBoxFromGeometry_CircleAccountsForRadiusPlusHalfStroke )
505{
508 c.center = VECTOR2I( 100, 200 );
509 c.radius = 30;
510 c.lineWidth = 4;
511 g.circles.push_back( c );
512
513 const auto bbox = BBoxFromGeometry( g );
514 BOOST_REQUIRE( bbox.has_value() );
515 // reach = radius (30) + half stroke (2) = 32.
516 BOOST_CHECK_EQUAL( bbox->GetLeft(), 68 );
517 BOOST_CHECK_EQUAL( bbox->GetRight(), 132 );
518 BOOST_CHECK_EQUAL( bbox->GetTop(), 168 );
519 BOOST_CHECK_EQUAL( bbox->GetBottom(), 232 );
520}
521
522
523BOOST_AUTO_TEST_CASE( BBoxFromGeometry_UnionsAcrossAllShapeKinds )
524{
525 // Each shape declares width=0 — but BBoxFromGeometry mirrors the
526 // renderer's PLOT_HAIRLINE_IU substitution so inflation is hairline/2 = 5
527 // on each side. The extents below account for that.
529
531 s.start = VECTOR2I( -1000, -1000 );
532 s.end = VECTOR2I( -900, -900 );
533 s.width = 0;
534 g.segments.push_back( s );
535
537 p.outline = { VECTOR2I( 500, 500 ), VECTOR2I( 600, 500 ), VECTOR2I( 600, 600 ) };
538 p.lineWidth = 0;
539 g.polygons.push_back( p );
540
542 c.center = VECTOR2I( 0, 1000 );
543 c.radius = 50;
544 c.lineWidth = 0;
545 g.circles.push_back( c );
546
547 const int halfHairline = PLOT_HAIRLINE_IU / 2;
548 const auto bbox = BBoxFromGeometry( g );
549 BOOST_REQUIRE( bbox.has_value() );
550 BOOST_CHECK_EQUAL( bbox->GetLeft(), -1000 - halfHairline );
551 BOOST_CHECK_EQUAL( bbox->GetRight(), 600 + halfHairline );
552 BOOST_CHECK_EQUAL( bbox->GetTop(), -1000 - halfHairline );
553 BOOST_CHECK_EQUAL( bbox->GetBottom(), 1050 + halfHairline );
554}
555
556
557BOOST_AUTO_TEST_CASE( BBoxFromGeometry_WidthZeroSegmentMatchesRendererHairline )
558{
559 // The bug this commit fixes: a width-0 horizontal segment previously
560 // produced a zero-height bbox, but the renderer draws it at
561 // PLOT_HAIRLINE_IU pixels. Now the bbox includes half the hairline above
562 // and below so the rendered pixels actually fit inside the bbox.
565 s.start = VECTOR2I( 0, 100 );
566 s.end = VECTOR2I( 200, 100 );
567 s.width = 0;
568 g.segments.push_back( s );
569
570 const auto bbox = BBoxFromGeometry( g );
571 BOOST_REQUIRE( bbox.has_value() );
572 BOOST_CHECK_EQUAL( bbox->GetHeight(), PLOT_HAIRLINE_IU );
573 BOOST_CHECK_EQUAL( bbox->GetWidth(), 200 + PLOT_HAIRLINE_IU );
574}
575
576
577BOOST_AUTO_TEST_CASE( BBoxFromGeometry_SinglePointPolygonSkipped )
578{
579 // A single-point outline draws nothing in any renderer (headless plotter
580 // requires >=3, GAL requires >=2), so the bbox skips it.
583 p.outline = { VECTOR2I( 999, 999 ) };
584 p.lineWidth = 0;
585 g.polygons.push_back( p );
586
587 BOOST_CHECK( !BBoxFromGeometry( g ).has_value() );
588}
589
590
591BOOST_AUTO_TEST_CASE( BBoxFromGeometry_TwoPointPolygonIncluded )
592{
593 // The GAL overlay (diff_renderer_gal.cpp) draws two-point outlines as
594 // open segments — the bbox must include them so the interactive zoom-
595 // to-fit doesn't crop the rendered shape.
598 p.outline = { VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ) };
599 p.lineWidth = 0;
600 g.polygons.push_back( p );
601
602 const auto bbox = BBoxFromGeometry( g );
603 BOOST_REQUIRE( bbox.has_value() );
604 BOOST_CHECK_EQUAL( bbox->GetLeft(), 0 - PLOT_HAIRLINE_IU / 2 );
605 BOOST_CHECK_EQUAL( bbox->GetRight(), 100 + PLOT_HAIRLINE_IU / 2 );
606}
607
608
609// Geometry layer filtering ---------------------------------------------------
610
611BOOST_AUTO_TEST_CASE( FilterGeometryByVisibleLayersKeepsLayerlessShapes )
612{
614
615 DOCUMENT_SEGMENT layerless;
616 layerless.start = VECTOR2I( 0, 0 );
617 layerless.end = VECTOR2I( 100, 0 );
618 g.segments.push_back( layerless );
619
620 DOCUMENT_SEGMENT front;
621 front.start = VECTOR2I( 0, 10 );
622 front.end = VECTOR2I( 100, 10 );
623 front.layers = LSET( { F_Cu } );
624 g.segments.push_back( front );
625
627
628 BOOST_REQUIRE_EQUAL( filtered.segments.size(), 1u );
629 BOOST_CHECK( filtered.segments[0].layers.none() );
630}
631
632
633BOOST_AUTO_TEST_CASE( FilterGeometryByVisibleLayersMatchesAnyLayer )
634{
636
637 DOCUMENT_POLYGON frontBack;
638 frontBack.outline = { VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ), VECTOR2I( 100, 100 ) };
639 frontBack.layers = LSET( { F_Cu, B_Cu } );
640 g.polygons.push_back( frontBack );
641
642 DOCUMENT_CIRCLE silk;
643 silk.center = VECTOR2I( 50, 50 );
644 silk.radius = 10;
645 silk.layers = LSET( { F_SilkS } );
646 g.circles.push_back( silk );
647
649
650 BOOST_REQUIRE_EQUAL( filtered.polygons.size(), 1u );
651 BOOST_CHECK( ( filtered.polygons[0].layers & LSET( { B_Cu } ) ).any() );
652 BOOST_CHECK( filtered.circles.empty() );
653}
654
655
656// EffectivePlotWidth coverage -----------------------------------------------
657
658BOOST_AUTO_TEST_CASE( EffectivePlotWidth_PositivePassThrough )
659{
661 BOOST_CHECK_EQUAL( EffectivePlotWidth( 1000 ), 1000 );
662}
663
664
671
672
673// Compile-time guarantee that the substituted hairline width is genuinely
674// positive — SVG treats width=0 as "no stroke", so the contract documented
675// in diff_renderer_plotter.h would silently break if a future edit zeroed
676// PLOT_HAIRLINE_IU.
677static_assert( PLOT_HAIRLINE_IU > 0, "hairline width must be positive" );
678
679
680// ThemeColorFor coverage -----------------------------------------------------
681
682BOOST_AUTO_TEST_CASE( ThemeColorFor_MapsEveryCategory )
683{
684 DIFF_COLOR_THEME theme;
685 // Override defaults with distinct sentinel colors so an inverted mapping
686 // (e.g. returning theme.added for CATEGORY::REMOVED) trips the test.
687 theme.added = KIGFX::COLOR4D( 1.0, 0.0, 0.0, 1.0 );
688 theme.removed = KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 );
689 theme.modified = KIGFX::COLOR4D( 0.0, 0.0, 1.0, 1.0 );
690 theme.conflict = KIGFX::COLOR4D( 1.0, 1.0, 0.0, 1.0 );
691
692 BOOST_CHECK( ThemeColorFor( theme, CATEGORY::ADDED ) == theme.added );
693 BOOST_CHECK( ThemeColorFor( theme, CATEGORY::REMOVED ) == theme.removed );
694 BOOST_CHECK( ThemeColorFor( theme, CATEGORY::MODIFIED ) == theme.modified );
695 BOOST_CHECK( ThemeColorFor( theme, CATEGORY::CONFLICT ) == theme.conflict );
696}
697
698
699// BuildScene coverage --------------------------------------------------------
700
701namespace
702{
703
704ITEM_CHANGE MakeChangeWithBBox( const wxString& aType, CHANGE_KIND aKind, const BOX2I& aBBox )
705{
706 ITEM_CHANGE c;
707 c.typeName = aType;
708 c.kind = aKind;
709 c.id = KIID_PATH( wxS( "/" ) + KIID().AsString() );
710 c.bbox = aBBox;
711 return c;
712}
713
714} // namespace
715
716
717BOOST_AUTO_TEST_CASE( BuildScene_EmptyDiffProducesEmptyScene )
718{
719 DOCUMENT_DIFF diff;
720 DIFF_COLOR_THEME theme;
721
722 DIFF_SCENE scene = BuildScene( diff, theme );
723
724 BOOST_CHECK( scene.addedShapes.empty() );
725 BOOST_CHECK( scene.removedShapes.empty() );
726 BOOST_CHECK( scene.modifiedShapes.empty() );
727 BOOST_CHECK( scene.conflictShapes.empty() );
730}
731
732
733BOOST_AUTO_TEST_CASE( BuildScene_RoutesByChangeKind )
734{
735 DOCUMENT_DIFF diff;
736 diff.changes.push_back(
737 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) ) );
738 diff.changes.push_back(
739 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::REMOVED, BOX2I( VECTOR2I( 20, 0 ), VECTOR2I( 10, 10 ) ) ) );
740 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::MODIFIED,
741 BOX2I( VECTOR2I( 40, 0 ), VECTOR2I( 10, 10 ) ) ) );
742 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::COLLISION,
743 BOX2I( VECTOR2I( 60, 0 ), VECTOR2I( 10, 10 ) ) ) );
744 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::DUPLICATE_UUID,
745 BOX2I( VECTOR2I( 80, 0 ), VECTOR2I( 10, 10 ) ) ) );
746
747 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
748
749 BOOST_CHECK_EQUAL( scene.addedShapes.size(), 1u );
750 BOOST_CHECK_EQUAL( scene.removedShapes.size(), 1u );
751 BOOST_CHECK_EQUAL( scene.modifiedShapes.size(), 1u );
752 // COLLISION and DUPLICATE_UUID both map to CONFLICT.
753 BOOST_CHECK_EQUAL( scene.conflictShapes.size(), 2u );
754}
755
756
757BOOST_AUTO_TEST_CASE( BuildScene_AppliesThemeColorsPerCategory )
758{
759 // Use distinct sentinel colors on every category so an inverted mapping
760 // (e.g. returning theme.added for CATEGORY::REMOVED) trips even though
761 // both sides are valid COLOR4D values. Default-constructed COLOR4D
762 // values are identical, so the previous version of this test would have
763 // passed even with the dispatch entirely broken.
764 DIFF_COLOR_THEME theme;
765 theme.added = KIGFX::COLOR4D( 1.0, 0.0, 0.0, 1.0 );
766 theme.removed = KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 );
767 theme.modified = KIGFX::COLOR4D( 0.0, 0.0, 1.0, 1.0 );
768 theme.conflict = KIGFX::COLOR4D( 1.0, 1.0, 0.0, 1.0 );
769
770 DOCUMENT_DIFF diff;
771 diff.changes.push_back(
772 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) ) );
773 diff.changes.push_back(
774 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::REMOVED, BOX2I( VECTOR2I( 20, 0 ), VECTOR2I( 10, 10 ) ) ) );
775 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::MODIFIED,
776 BOX2I( VECTOR2I( 40, 0 ), VECTOR2I( 10, 10 ) ) ) );
777 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::COLLISION,
778 BOX2I( VECTOR2I( 60, 0 ), VECTOR2I( 10, 10 ) ) ) );
779
780 DIFF_SCENE scene = BuildScene( diff, theme );
781
782 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
783 BOOST_REQUIRE_EQUAL( scene.removedShapes.size(), 1u );
784 BOOST_REQUIRE_EQUAL( scene.modifiedShapes.size(), 1u );
785 BOOST_REQUIRE_EQUAL( scene.conflictShapes.size(), 1u );
786 BOOST_CHECK( scene.addedShapes[0].color == theme.added );
787 BOOST_CHECK( scene.removedShapes[0].color == theme.removed );
788 BOOST_CHECK( scene.modifiedShapes[0].color == theme.modified );
789 BOOST_CHECK( scene.conflictShapes[0].color == theme.conflict );
790}
791
792
793BOOST_AUTO_TEST_CASE( BuildScene_SkipsItemsWithDegenerateBBox )
794{
795 // BuildScene calls bboxValid which requires BOTH dimensions > 0 — a
796 // zero-area bbox at the origin or elsewhere should be skipped, and so
797 // should bboxes with only one zero dimension (a horizontal or vertical
798 // sliver). The (0,0) origin case alone would pass even if the predicate
799 // only checked the origin field, so test all three shapes.
800 DOCUMENT_DIFF diff;
801 diff.changes.push_back(
802 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 50, 50 ), VECTOR2I( 0, 0 ) ) ) );
803 diff.changes.push_back(
804 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 100, 100 ), VECTOR2I( 0, 50 ) ) ) );
805 diff.changes.push_back(
806 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 200, 200 ), VECTOR2I( 50, 0 ) ) ) );
807
808 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
809
810 BOOST_CHECK( scene.addedShapes.empty() );
813}
814
815
816BOOST_AUTO_TEST_CASE( BuildScene_DocumentBBoxIsUnionOfValidChangeBBoxes )
817{
818 DOCUMENT_DIFF diff;
819 diff.changes.push_back(
820 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ) );
821 diff.changes.push_back( MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::REMOVED,
822 BOX2I( VECTOR2I( 200, 200 ), VECTOR2I( 50, 50 ) ) ) );
823
824 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
825
830}
831
832
833BOOST_AUTO_TEST_CASE( BuildScene_NestedChildrenContributeShapes )
834{
835 // A footprint change with nested pad children: each child must produce
836 // its own SCENE_SHAPE with the child's changeId — per-pad highlight in
837 // the dialog uses changeId to map the click target to the source item.
838 // Counts alone could be satisfied by emitting duplicate parent shapes.
839 DOCUMENT_DIFF diff;
840 ITEM_CHANGE fp = MakeChangeWithBBox( wxS( "FOOTPRINT" ), CHANGE_KIND::MODIFIED,
841 BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) );
842 fp.children.push_back( MakeChangeWithBBox( wxS( "PAD" ), CHANGE_KIND::MODIFIED,
843 BOX2I( VECTOR2I( 10, 10 ), VECTOR2I( 20, 20 ) ) ) );
844 fp.children.push_back(
845 MakeChangeWithBBox( wxS( "PAD" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 60, 60 ), VECTOR2I( 20, 20 ) ) ) );
846
847 // Snapshot the ids before moving the parent into the diff.
848 const KIID_PATH parentId = fp.id;
849 const KIID_PATH modPadId = fp.children[0].id;
850 const KIID_PATH addPadId = fp.children[1].id;
851
852 diff.changes.push_back( std::move( fp ) );
853
854 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
855
856 BOOST_REQUIRE_EQUAL( scene.modifiedShapes.size(), 2u );
857 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
858
859 auto hasId = []( const std::vector<SCENE_SHAPE>& aShapes, const KIID_PATH& aId )
860 {
861 return std::any_of( aShapes.begin(), aShapes.end(),
862 [&aId]( const SCENE_SHAPE& aShape )
863 {
864 return aShape.changeId == aId;
865 } );
866 };
867
868 BOOST_CHECK( hasId( scene.modifiedShapes, parentId ) );
869 BOOST_CHECK( hasId( scene.modifiedShapes, modPadId ) );
870 BOOST_CHECK( hasId( scene.addedShapes, addPadId ) );
871}
872
873
874BOOST_AUTO_TEST_CASE( BuildScene_CollapsesSameNetRoutingChanges )
875{
876 DOCUMENT_DIFF diff;
877
878 ITEM_CHANGE track = MakeChangeWithBBox( wxS( "PCB_TRACK" ), CHANGE_KIND::MODIFIED,
879 BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) );
880 track.refdes = wxS( "Net-(U1-Pad1)" );
881
882 ITEM_CHANGE via = MakeChangeWithBBox( wxS( "PCB_VIA" ), CHANGE_KIND::MODIFIED,
883 BOX2I( VECTOR2I( 20, 20 ), VECTOR2I( 10, 10 ) ) );
884 via.refdes = wxS( "Net-(U1-Pad1)" );
885
886 diff.changes.push_back( std::move( track ) );
887 diff.changes.push_back( std::move( via ) );
888
889 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
890
891 BOOST_REQUIRE_EQUAL( scene.modifiedShapes.size(), 1u );
892 BOOST_CHECK( scene.modifiedShapes[0].label == wxS( "NET [Net-(U1-Pad1)]" ) );
893 BOOST_CHECK_EQUAL( scene.modifiedShapes[0].bbox.GetLeft(), 0 );
894 BOOST_CHECK_EQUAL( scene.modifiedShapes[0].bbox.GetTop(), 0 );
895 BOOST_CHECK_EQUAL( scene.modifiedShapes[0].bbox.GetRight(), 30 );
896 BOOST_CHECK_EQUAL( scene.modifiedShapes[0].bbox.GetBottom(), 30 );
897}
898
899
900BOOST_AUTO_TEST_CASE( BuildScene_LabelIncludesRefdesWhenPresent )
901{
902 DOCUMENT_DIFF diff;
903 ITEM_CHANGE c = MakeChangeWithBBox( wxS( "FOOTPRINT" ), CHANGE_KIND::MODIFIED,
904 BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) );
905 c.refdes = wxS( "U7" );
906 diff.changes.push_back( std::move( c ) );
907
908 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
909
910 BOOST_REQUIRE_EQUAL( scene.modifiedShapes.size(), 1u );
911 // Pin the exact format ("%s [%s]" with typeName + refdes) so a label
912 // refactor that flips field order or drops the brackets fails the test
913 // explicitly. Substring matches would still pass on "U7FOOTPRINT" or
914 // "[FOOTPRINT/U7]" which both break the dialog's display contract.
915 BOOST_CHECK( scene.modifiedShapes[0].label == wxS( "FOOTPRINT [U7]" ) );
916}
917
918
919BOOST_AUTO_TEST_CASE( BuildScene_LabelIsTypeNameWhenRefdesAbsent )
920{
921 DOCUMENT_DIFF diff;
922 ITEM_CHANGE c =
923 MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::ADDED, BOX2I( VECTOR2I( 0, 0 ), VECTOR2I( 10, 10 ) ) );
924 diff.changes.push_back( std::move( c ) );
925
926 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
927
928 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
929 BOOST_CHECK( scene.addedShapes[0].label == wxS( "ZONE" ) );
930}
931
932
933static DIFF_VALUE RectPolygonSet( const BOX2I& aBox )
934{
936 ps.push_back( { { aBox.GetOrigin(),
937 { aBox.GetEnd().x, aBox.GetOrigin().y },
938 aBox.GetEnd(),
939 { aBox.GetOrigin().x, aBox.GetEnd().y } } } );
940 return DIFF_VALUE::FromPolygonSet( std::move( ps ) );
941}
942
943
944// A grown ZONE whose old outline is fully inside the new one produces an
945// added ring and nothing removed. The delta region must replace the
946// whole-zone MODIFIED bbox rectangle.
947BOOST_AUTO_TEST_CASE( BuildScene_ZoneOutlineDeltaEmitsAddedRingNotBBox )
948{
949 BOX2I before( VECTOR2I( 10, 10 ), VECTOR2I( 80, 80 ) ); // 10,10 .. 90,90
950 BOX2I after( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ); // 0,0 .. 100,100
951
952 ITEM_CHANGE c = MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::MODIFIED, after );
953
955 d.name = wxS( "Outline" );
956 d.before = RectPolygonSet( before );
957 d.after = RectPolygonSet( after );
958 c.properties.push_back( std::move( d ) );
959
960 DOCUMENT_DIFF diff;
961 diff.changes.push_back( std::move( c ) );
962
963 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
964
965 BOOST_CHECK_EQUAL( scene.modifiedShapes.size(), 0u );
966 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
967 BOOST_CHECK( !scene.addedShapes[0].polygons.empty() );
968 BOOST_CHECK_EQUAL( scene.removedShapes.size(), 0u );
969}
970
971
972// A shifted ZONE outline leaves a removed strip on one side and an added
973// strip on the other. Both shapes carry real polygon geometry.
974BOOST_AUTO_TEST_CASE( BuildScene_ZoneOutlineDeltaEmitsBothAddedAndRemoved )
975{
976 BOX2I before( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ); // 0,0 .. 100,100
977 BOX2I after( VECTOR2I( 50, 0 ), VECTOR2I( 100, 100 ) ); // 50,0 .. 150,100
978
979 ITEM_CHANGE c = MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::MODIFIED, after );
980
982 d.name = wxS( "Outline" );
983 d.before = RectPolygonSet( before );
984 d.after = RectPolygonSet( after );
985 c.properties.push_back( std::move( d ) );
986
987 DOCUMENT_DIFF diff;
988 diff.changes.push_back( std::move( c ) );
989
990 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
991
992 BOOST_CHECK_EQUAL( scene.modifiedShapes.size(), 0u );
993 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
994 BOOST_CHECK( !scene.addedShapes[0].polygons.empty() );
995 BOOST_REQUIRE_EQUAL( scene.removedShapes.size(), 1u );
996 BOOST_CHECK( !scene.removedShapes[0].polygons.empty() );
997}
998
999
1000// A "Filled Area (<layer>)" delta drives the same boolean-subtract path as
1001// "Outline"; the StartsWith predicate must accept it.
1002BOOST_AUTO_TEST_CASE( BuildScene_ZoneFilledAreaDeltaAlsoEmitsRegions )
1003{
1004 BOX2I before( VECTOR2I( 10, 10 ), VECTOR2I( 80, 80 ) );
1005 BOX2I after( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) );
1006
1007 ITEM_CHANGE c = MakeChangeWithBBox( wxS( "ZONE" ), CHANGE_KIND::MODIFIED, after );
1008
1010 d.name = wxS( "Filled Area (F.Cu)" );
1011 d.before = RectPolygonSet( before );
1012 d.after = RectPolygonSet( after );
1013 c.properties.push_back( std::move( d ) );
1014
1015 DOCUMENT_DIFF diff;
1016 diff.changes.push_back( std::move( c ) );
1017
1018 DIFF_SCENE scene = BuildScene( diff, DIFF_COLOR_THEME{} );
1019
1020 BOOST_CHECK_EQUAL( scene.modifiedShapes.size(), 0u );
1021 BOOST_REQUIRE_EQUAL( scene.addedShapes.size(), 1u );
1022 BOOST_CHECK( !scene.addedShapes[0].polygons.empty() );
1023}
1024
1025
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
constexpr const Vec GetEnd() const
Definition box2.h:208
constexpr size_type GetWidth() const
Definition box2.h:210
constexpr size_type GetHeight() const
Definition box2.h:211
constexpr coord_type GetLeft() const
Definition box2.h:224
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:164
constexpr const Vec & GetOrigin() const
Definition box2.h:206
constexpr coord_type GetRight() const
Definition box2.h:213
constexpr coord_type GetTop() const
Definition box2.h:225
constexpr coord_type GetBottom() const
Definition box2.h:218
A typed sum value used to carry the before/after of any single property.
std::vector< std::vector< std::vector< VECTOR2I > > > PolygonSet
static DIFF_VALUE FromPolygonSet(PolygonSet aValue)
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
static const COLOR4D WHITE
Definition color4d.h:401
Definition kiid.h:44
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
@ B_Cu
Definition layer_ids.h:61
@ F_SilkS
Definition layer_ids.h:96
@ F_Cu
Definition layer_ids.h:60
CHANGE_KIND
Coarse classification of a single item-level change between two documents.
constexpr std::size_t CATEGORY_COUNT
Definition diff_scene.h:75
wxString ChangeDisplayLabel(const ITEM_CHANGE &aChange)
User-facing item label used consistently by scene tooltips and change tree entries.
DIFF_SCENE BuildScene(const DOCUMENT_DIFF &aDiff, const DIFF_COLOR_THEME &aTheme)
Build a DIFF_SCENE from a DOCUMENT_DIFF, populating the shape lists and computing the union bbox.
DOCUMENT_POLYGON MakeBBoxOutline(const BOX2I &aBBox, const KIGFX::COLOR4D &aColor, int aLineWidth)
Build a DOCUMENT_POLYGON outlining a bounding box.
ITEM_RES
Resolution kind for a whole item.
constexpr int EffectivePlotWidth(int aWidth)
Return aWidth if positive, otherwise PLOT_HAIRLINE_IU.
const char * PropResToString(PROP_RES aRes)
Canonical lower-case spellings for PROP_RES used inside the JSON serialization of PROPERTY_RESOLUTION...
PROP_RES PropResFromString(const std::string &aStr)
constexpr int PLOT_HAIRLINE_IU
Minimum stroke width the headless plotter will use when a primitive declares a non-positive line widt...
void CollectChangeBBoxes(const DOCUMENT_DIFF &aDiff, std::map< KIID_PATH, BOX2I > &aOut)
Walk a DOCUMENT_DIFF and populate a (KIID_PATH → BOX2I) map with each changed item's bbox,...
KIGFX::COLOR4D ThemeColorFor(const DIFF_COLOR_THEME &aTheme, CATEGORY aCategory)
Map a CATEGORY to its color in a DIFF_COLOR_THEME.
std::optional< BOX2I > HighlightedBBox(const DIFF_SCENE &aScene, const KIID_PATH &aChangeId, const std::array< bool, CATEGORY_COUNT > &aCategoryVisible)
Union bbox of every visible SCENE_SHAPE whose changeId matches aChangeId.
void AppendGeometry(DOCUMENT_GEOMETRY &aDst, DOCUMENT_GEOMETRY &&aSrc)
Move all primitives from aSrc into aDst.
ITEM_RES ItemResFromString(const std::string &aStr)
PROP_RES
Resolution kind for a single property of a single item.
void ExpandBBoxToGeometry(DIFF_SCENE &aScene)
Grow the scene's documentBBox to also include the extent of any background geometry.
bool IsRoutingNetChange(const ITEM_CHANGE &aChange)
Presentation predicate for PCB routing changes that should be displayed as one net-level entry/shape.
const char * ItemResToString(ITEM_RES aRes)
Canonical snake_case spellings used in MERGE_PLAN JSON serialization (take_ours / take_theirs / take_...
const std::vector< SCENE_SHAPE > & ShapesFor(const DIFF_SCENE &aScene, CATEGORY aCategory)
Read-only access to a DIFF_SCENE's shape list for a given category.
constexpr std::array< CATEGORY, 4 > PAINT_ORDER
Paint order.
Definition diff_scene.h:67
std::optional< BOX2I > BBoxFromGeometry(const DOCUMENT_GEOMETRY &aGeometry)
Compute the tight bounding box of a DOCUMENT_GEOMETRY, inflating each primitive by half its stroke so...
CATEGORY CategoryFor(CHANGE_KIND aKind)
Map a CHANGE_KIND to the visual category it belongs to.
DOCUMENT_GEOMETRY FilterGeometryByVisibleLayers(const DOCUMENT_GEOMETRY &aGeometry, const LSET &aVisibleLayers)
Copy geometry primitives whose layer set intersects aVisibleLayers.
_OUT_STRING AsString(const std::string &aString)
Definition sexpr.h:131
std::vector< SCENE_SHAPE > modifiedShapes
Definition diff_scene.h:232
DOCUMENT_GEOMETRY referenceGeometry
Background geometry from the two source documents.
Definition diff_scene.h:239
std::vector< SCENE_SHAPE > conflictShapes
Definition diff_scene.h:233
std::vector< SCENE_SHAPE > addedShapes
Definition diff_scene.h:230
DOCUMENT_GEOMETRY comparisonGeometry
Definition diff_scene.h:240
std::vector< SCENE_SHAPE > removedShapes
Definition diff_scene.h:231
Filled or stroked circle.
Definition diff_scene.h:146
The full set of changes between two parsed documents of one type.
std::vector< ITEM_CHANGE > changes
Aggregate of background geometry extracted from one source document.
Definition diff_scene.h:163
std::vector< DOCUMENT_SEGMENT > segments
Definition diff_scene.h:164
std::vector< DOCUMENT_POLYGON > polygons
Definition diff_scene.h:165
std::vector< DOCUMENT_CIRCLE > circles
Definition diff_scene.h:166
Closed polygon outline from a source document.
Definition diff_scene.h:133
std::vector< VECTOR2I > outline
Definition diff_scene.h:134
Stroked line segment from one of the source documents.
Definition diff_scene.h:117
One change record on a single item.
std::vector< PROPERTY_DELTA > properties
std::optional< wxString > refdes
std::vector< ITEM_CHANGE > children
Single (name, before, after) triple for one mutated property on an item.
Shared rendering model consumed by both the GAL renderer (interactive widget) and the plotter rendere...
Definition diff_scene.h:90
KIID_PATH changeId
Stable identifier of the ITEM_CHANGE that produced this shape.
Definition diff_scene.h:99
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(CategoryForMapsAllChangeKinds)
static DIFF_VALUE RectPolygonSet(const BOX2I &aBox)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683