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