KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pin_stacked_layout.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
24
26
27#include <sch_pin.h>
28#include <lib_symbol.h>
29#include <pin_layout_cache.h>
30#include <transform.h>
31#include <sch_io/sch_io_mgr.h>
32
33#include <wx/log.h>
34#include <boost/test/unit_test.hpp>
35
36BOOST_AUTO_TEST_SUITE( PinStackedLayout )
37
38
42{
43 auto symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestResistor" ) );
44
45 // Set pin name offset to 0 so names are positioned outside (like numbers)
46 symbol->SetPinNameOffset( 0 );
47
48 // Create first pin with stacked numbers [1-5]
49 auto pin1 = std::make_unique<SCH_PIN>( symbol.get() );
50 pin1->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( 250 ) ) ); // top pin
51 pin1->SetOrientation( PIN_ORIENTATION::PIN_DOWN );
52 pin1->SetLength( schIUScale.MilsToIU( 50 ) );
53 pin1->SetNumber( wxT( "[1-5]" ) );
54 pin1->SetName( wxT( "A" ) ); // Short name
56 pin1->SetUnit( 1 );
57
58 // Create second pin with stacked numbers [6,7,9-11]
59 auto pin2 = std::make_unique<SCH_PIN>( symbol.get() );
60 pin2->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( -340 ) ) ); // bottom pin
61 pin2->SetOrientation( PIN_ORIENTATION::PIN_UP );
62 pin2->SetLength( schIUScale.MilsToIU( 50 ) );
63 pin2->SetNumber( wxT( "[6,7,9-11]" ) );
64 pin2->SetName( wxT( "B" ) ); // Short name
65 pin2->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
66 pin2->SetUnit( 1 );
67
68 // Add pins to symbol
69 symbol->AddDrawItem( pin1.release() );
70 symbol->AddDrawItem( pin2.release() );
71
72 return symbol;
73}
74
78static VECTOR2I getPinLineEnd( const SCH_PIN* pin, const TRANSFORM& transform )
79{
80 VECTOR2I start = pin->GetPosition();
81 VECTOR2I end = start;
82
83 int length = pin->GetLength();
84
85 switch( pin->PinDrawOrient( transform ) )
86 {
88 end.y += length;
89 break;
91 end.y -= length;
92 break;
94 end.x -= length;
95 break;
97 end.x += length;
98 break;
100 default:
101 break;
102 }
103
104 return end;
105}
106
110BOOST_AUTO_TEST_CASE( PinNumbersNoOverlapAllRotations )
111{
112 // Create test symbol
113 std::unique_ptr<LIB_SYMBOL> symbol = createTestResistorSymbol();
114 BOOST_REQUIRE( symbol );
115
116 // Get the pins
117 std::vector<SCH_PIN*> pins;
118
119 for( SCH_ITEM& item : symbol->GetDrawItems() )
120 {
121 if( item.Type() == SCH_PIN_T )
122 pins.push_back( static_cast<SCH_PIN*>( &item ) );
123 }
124
125 BOOST_REQUIRE_EQUAL( pins.size(), 2 );
126
127 // Test rotations: 0°, 90°, 180°, 270°
128 std::vector<TRANSFORM> rotations = {
129 TRANSFORM( 1, 0, 0, 1 ), // 0° (identity)
130 TRANSFORM( 0, -1, 1, 0 ), // 90° CCW
131 TRANSFORM( -1, 0, 0, -1 ), // 180°
132 TRANSFORM( 0, 1, -1, 0 ) // 270° CCW (90° CW)
133 };
134
135 std::vector<wxString> rotationNames = { wxT("0°"), wxT("90°"), wxT("180°"), wxT("270°") };
136
137 for( size_t r = 0; r < rotations.size(); r++ )
138 {
139 const TRANSFORM& transform = rotations[r];
140 const wxString& rotName = rotationNames[r];
141
142 // Set global transform for this test
143 TRANSFORM oldTransform = DefaultTransform;
144 DefaultTransform = transform;
145
146 for( size_t p = 0; p < pins.size(); p++ )
147 {
148 SCH_PIN* pin = pins[p];
149
150 // Create layout cache for this pin
151 PIN_LAYOUT_CACHE cache( *pin );
152
153 // Production geometry, font metrics and all; no character-count estimate can stand in
154 // for it across rotations
155 OPT_BOX2I numberBox = cache.GetPinNumberBBox();
156
157 BOOST_REQUIRE_MESSAGE( numberBox.has_value(),
158 "Pin '" << pin->GetNumber() << "' has no number box at rotation " << rotName );
159
160 VECTOR2I pinStart = pin->GetPosition();
161 VECTOR2I pinEnd = getPinLineEnd( pin, transform );
162
163 BOOST_CHECK_MESSAGE( !numberBox->Intersects( pinStart, pinEnd ),
164 "Pin number '" << pin->GetNumber() << "' overlaps pin geometry at rotation "
165 << rotName );
166
167 // Control: the same box grown over the pin line must be reported as overlapping,
168 // otherwise the non-overlap result above says nothing
169 BOX2I grown = *numberBox;
170 grown.Inflate( pin->GetLength() * 2 );
171
172 BOOST_CHECK_MESSAGE( grown.Intersects( pinStart, pinEnd ),
173 "Inflated number box for '" << pin->GetNumber()
174 << "' should reach the pin line at rotation " << rotName );
175 }
176
177 // Restore original transform
178 DefaultTransform = oldTransform;
179 }
180}
181
186BOOST_AUTO_TEST_CASE( PinTextConsistentSidePlacement )
187{
188 // Create test symbol with both types of pins
189 std::unique_ptr<LIB_SYMBOL> symbol = createTestResistorSymbol();
190 BOOST_REQUIRE( symbol );
191
192 // Get the pins - one will be multiline formatted, one will not
193 std::vector<SCH_PIN*> pins;
194
195 for( SCH_ITEM& item : symbol->GetDrawItems() )
196 {
197 if( item.Type() == SCH_PIN_T )
198 pins.push_back( static_cast<SCH_PIN*>( &item ) );
199 }
200
201 BOOST_REQUIRE_EQUAL( pins.size(), 2 );
202
203 // Test rotations
204 std::vector<TRANSFORM> rotations = {
205 TRANSFORM( 1, 0, 0, 1 ), // 0° (identity)
206 TRANSFORM( 0, -1, 1, 0 ), // 90° CCW
207 TRANSFORM( -1, 0, 0, -1 ), // 180°
208 TRANSFORM( 0, 1, -1, 0 ) // 270° CCW (90° CW)
209 };
210
211 std::vector<wxString> rotationNames = { wxT("0°"), wxT("90°"), wxT("180°"), wxT("270°") };
212
213 for( size_t r = 0; r < rotations.size(); r++ )
214 {
215 const TRANSFORM& transform = rotations[r];
216 const wxString& rotName = rotationNames[r];
217
218 // Set global transform for this test
219 TRANSFORM oldTransform = DefaultTransform;
220 DefaultTransform = transform;
221
222 // For each rotation, collect pin number and name positions relative to pin center
223 struct PinTextInfo {
224 VECTOR2I pinPos;
225 VECTOR2I numberPos;
226 VECTOR2I namePos;
227 wxString pinNumber;
228 bool isMultiline;
229 };
230
231 std::vector<PinTextInfo> pinInfos;
232
233 for( SCH_PIN* pin : pins )
234 {
235 PinTextInfo info;
236 info.pinPos = pin->GetPosition();
237 info.pinNumber = pin->GetNumber();
238
239 // Create layout cache for this pin
240 PIN_LAYOUT_CACHE cache( *pin );
241
242 // Get number position (shadow width 0 for testing)
243 std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> numberInfoOpt = cache.GetPinNumberInfo( 0 );
244
245 if( numberInfoOpt.has_value() )
246 {
247 const PIN_LAYOUT_CACHE::TEXT_INFO& numberInfo = numberInfoOpt.value();
248 info.numberPos = numberInfo.m_TextPosition;
249 info.isMultiline = numberInfo.m_Text.Contains( '\n' );
250 }
251
252 // Get name position
253 std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> nameInfoOpt = cache.GetPinNameInfo( 0 );
254
255 if( nameInfoOpt.has_value() )
256 {
257 const PIN_LAYOUT_CACHE::TEXT_INFO& nameInfo = nameInfoOpt.value();
258 info.namePos = nameInfo.m_TextPosition;
259 }
260
261 pinInfos.push_back( info );
262
263 wxLogTrace( "KICAD_PINS", "Rotation %s, Pin %s: pos=(%d,%d) numberPos=(%d,%d) namePos=(%d,%d) multiline=%s",
264 rotName, info.pinNumber,
265 info.pinPos.x, info.pinPos.y,
266 info.numberPos.x, info.numberPos.y,
267 info.namePos.x, info.namePos.y,
268 info.isMultiline ? wxT("YES") : wxT("NO") );
269 }
270
271 BOOST_REQUIRE_EQUAL( pinInfos.size(), 2 );
272
273 // New semantics:
274 // * Vertical pins (UP/DOWN): numbers and names must be LEFT (x < pin.x)
275 // * Horizontal pins (LEFT/RIGHT): numbers/names must be ABOVE (y < pin.y)
276 PIN_ORIENTATION orient = pins[0]->PinDrawOrient( DefaultTransform );
277
278 if( orient == PIN_ORIENTATION::PIN_UP || orient == PIN_ORIENTATION::PIN_DOWN )
279 {
280 for( const PinTextInfo& inf : pinInfos )
281 {
282 BOOST_CHECK_MESSAGE( inf.numberPos.x > inf.pinPos.x,
283 "At rotation " << rotName << ", number for pin " << inf.pinNumber << " not right of vertical pin." );
284 BOOST_CHECK_MESSAGE( inf.namePos.x < inf.pinPos.x,
285 "At rotation " << rotName << ", name for pin " << inf.pinNumber << " not left of vertical pin." );
286 }
287 }
288 else if( orient == PIN_ORIENTATION::PIN_LEFT || orient == PIN_ORIENTATION::PIN_RIGHT )
289 {
290 for( const PinTextInfo& inf : pinInfos )
291 {
292 BOOST_CHECK_MESSAGE( inf.numberPos.y > inf.pinPos.y,
293 "At rotation " << rotName << ", number for pin " << inf.pinNumber << " not below horizontal pin." );
294 BOOST_CHECK_MESSAGE( inf.namePos.y < inf.pinPos.y,
295 "At rotation " << rotName << ", name for pin " << inf.pinNumber << " not above horizontal pin." );
296 }
297 }
298
299 // Restore original transform
300 DefaultTransform = oldTransform;
301 }
302}
303
304// Distance from a pin to the near edge of a box, measured perpendicular to the pin.
305static int nearEdgeClearance( const SCH_PIN* aPin, const TRANSFORM& aTransform, const BOX2I& aBox )
306{
307 VECTOR2I pinPos = aPin->GetPosition();
308 PIN_ORIENTATION orient = aPin->PinDrawOrient( aTransform );
309
310 if( orient == PIN_ORIENTATION::PIN_LEFT || orient == PIN_ORIENTATION::PIN_RIGHT )
311 return std::min( std::abs( aBox.GetTop() - pinPos.y ), std::abs( aBox.GetBottom() - pinPos.y ) );
312
313 return std::min( std::abs( aBox.GetLeft() - pinPos.x ), std::abs( aBox.GetRight() - pinPos.x ) );
314}
315
316// Number and name keep the same clearance from the pin for every pin and every
317// rotation, whatever the text height. Measured from the real text bounding boxes.
318BOOST_AUTO_TEST_CASE( PinTextSameBottomCoordinate )
319{
320 std::unique_ptr<LIB_SYMBOL> symbol = createTestResistorSymbol();
321 BOOST_REQUIRE( symbol );
322
323 std::vector<SCH_PIN*> pins;
324
325 for( SCH_ITEM& item : symbol->GetDrawItems() )
326 {
327 if( item.Type() == SCH_PIN_T )
328 pins.push_back( static_cast<SCH_PIN*>( &item ) );
329 }
330
331 BOOST_REQUIRE_EQUAL( pins.size(), 2 );
332
333 const std::vector<TRANSFORM> rotations = {
334 TRANSFORM( 1, 0, 0, 1 ), // 0
335 TRANSFORM( 0, -1, 1, 0 ), // 90
336 TRANSFORM( -1, 0, 0, -1 ), // 180
337 TRANSFORM( 0, 1, -1, 0 ) // 270
338 };
339
340 const std::vector<wxString> rotationNames = { wxT( "0" ), wxT( "90" ), wxT( "180" ), wxT( "270" ) };
341
342 const int tolerance = 100;
343 int numberRef = -1;
344 int nameRef = -1;
345
346 for( size_t r = 0; r < rotations.size(); r++ )
347 {
348 TRANSFORM oldTransform = DefaultTransform;
349 DefaultTransform = rotations[r];
350
351 for( SCH_PIN* pin : pins )
352 {
353 PIN_LAYOUT_CACHE cache( *pin );
354
355 OPT_BOX2I numberBox = cache.GetPinNumberBBox();
356 OPT_BOX2I nameBox = cache.GetPinNameBBox();
357
358 BOOST_REQUIRE_MESSAGE( numberBox.has_value() && nameBox.has_value(),
359 "Missing text box for pin " << pin->GetNumber() << " at rotation "
360 << rotationNames[r] );
361
362 int numberClearance = nearEdgeClearance( pin, rotations[r], *numberBox );
363 int nameClearance = nearEdgeClearance( pin, rotations[r], *nameBox );
364
365 if( numberRef < 0 )
366 numberRef = numberClearance;
367
368 if( nameRef < 0 )
369 nameRef = nameClearance;
370
371 BOOST_CHECK_MESSAGE( std::abs( numberClearance - numberRef ) <= tolerance,
372 "Pin " << pin->GetNumber() << " number clearance " << numberClearance
373 << " at rotation " << rotationNames[r] << " differs from " << numberRef
374 << " (tolerance " << tolerance << ")" );
375
376 BOOST_CHECK_MESSAGE( std::abs( nameClearance - nameRef ) <= tolerance,
377 "Pin " << pin->GetNumber() << " name clearance " << nameClearance << " at rotation "
378 << rotationNames[r] << " differs from " << nameRef << " (tolerance "
379 << tolerance << ")" );
380 }
381
382 DefaultTransform = oldTransform;
383 }
384}
385
386// Symbol that shows only pin numbers (names hidden).
387static std::unique_ptr<LIB_SYMBOL> createNumberOnlySymbol()
388{
389 auto symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestNumberOnly" ) );
390
391 symbol->SetShowPinNames( false );
392 symbol->SetShowPinNumbers( true );
393
394 // A plain number and a stacked number.
395 auto pin1 = std::make_unique<SCH_PIN>( symbol.get() );
396 pin1->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( 250 ) ) );
397 pin1->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
398 pin1->SetLength( schIUScale.MilsToIU( 100 ) );
399 pin1->SetNumber( wxT( "12" ) );
401 pin1->SetUnit( 1 );
402
403 auto pin2 = std::make_unique<SCH_PIN>( symbol.get() );
404 pin2->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( -250 ) ) );
405 pin2->SetOrientation( PIN_ORIENTATION::PIN_LEFT );
406 pin2->SetLength( schIUScale.MilsToIU( 50 ) );
407 pin2->SetNumber( wxT( "[6,7,9-11]" ) );
408 pin2->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
409 pin2->SetUnit( 1 );
410
411 symbol->AddDrawItem( pin1.release() );
412 symbol->AddDrawItem( pin2.release() );
413
414 return symbol;
415}
416
417// Perpendicular distance from the pin to its number centre, at 0/90/180/270 deg.
418static std::vector<int> collectNumberGapsPerRotation( SCH_PIN* aPin )
419{
420 const std::vector<TRANSFORM> rotations = {
421 TRANSFORM( 1, 0, 0, 1 ), // 0
422 TRANSFORM( 0, -1, 1, 0 ), // 90
423 TRANSFORM( -1, 0, 0, -1 ), // 180
424 TRANSFORM( 0, 1, -1, 0 ) // 270
425 };
426
427 std::vector<int> gaps;
428
429 for( const TRANSFORM& transform : rotations )
430 {
431 TRANSFORM oldTransform = DefaultTransform;
432 DefaultTransform = transform;
433
434 PIN_LAYOUT_CACHE cache( *aPin );
435 std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> numberInfo = cache.GetPinNumberInfo( 0 );
436
437 BOOST_REQUIRE_MESSAGE( numberInfo.has_value(), "Missing pin number info for pin " << aPin->GetNumber() );
438
439 VECTOR2I pinPos = aPin->GetPosition();
440 PIN_ORIENTATION orient = aPin->PinDrawOrient( transform );
441 int gap;
442
443 if( orient == PIN_ORIENTATION::PIN_LEFT || orient == PIN_ORIENTATION::PIN_RIGHT )
444 gap = std::abs( numberInfo->m_TextPosition.y - pinPos.y ); // horizontal pin
445 else
446 gap = std::abs( numberInfo->m_TextPosition.x - pinPos.x ); // vertical pin
447
448 gaps.push_back( gap );
449
450 DefaultTransform = oldTransform;
451 }
452
453 return gaps;
454}
455
456// Issue 21778: the number-to-pin gap must not change when the symbol is rotated.
457// Here names and numbers are both shown (name offset 0).
458BOOST_AUTO_TEST_CASE( PinNumberGapConstantAcrossRotations )
459{
460 std::unique_ptr<LIB_SYMBOL> symbol = createTestResistorSymbol();
461 BOOST_REQUIRE( symbol );
462
463 std::vector<SCH_PIN*> pins;
464
465 for( SCH_ITEM& item : symbol->GetDrawItems() )
466 {
467 if( item.Type() == SCH_PIN_T )
468 pins.push_back( static_cast<SCH_PIN*>( &item ) );
469 }
470
471 BOOST_REQUIRE_EQUAL( pins.size(), 2 );
472
473 // Gap is orientation independent, allow a couple of units for rounding.
474 const int tolerance = 2;
475
476 for( SCH_PIN* pin : pins )
477 {
478 std::vector<int> gaps = collectNumberGapsPerRotation( pin );
479
480 int minGap = *std::min_element( gaps.begin(), gaps.end() );
481 int maxGap = *std::max_element( gaps.begin(), gaps.end() );
482
483 BOOST_CHECK_MESSAGE( ( maxGap - minGap ) <= tolerance,
484 "Pin " << pin->GetNumber() << " number gap changes with rotation. "
485 << "0=" << gaps[0] << " 90=" << gaps[1] << " 180=" << gaps[2] << " 270=" << gaps[3]
486 << " (tolerance " << tolerance << ")" );
487 }
488}
489
490// Issue 21778, number-only branch (pin names hidden): gap must stay constant too.
491BOOST_AUTO_TEST_CASE( PinNumberOnlyGapConstantAcrossRotations )
492{
493 std::unique_ptr<LIB_SYMBOL> symbol = createNumberOnlySymbol();
494 BOOST_REQUIRE( symbol );
495
496 std::vector<SCH_PIN*> pins;
497
498 for( SCH_ITEM& item : symbol->GetDrawItems() )
499 {
500 if( item.Type() == SCH_PIN_T )
501 pins.push_back( static_cast<SCH_PIN*>( &item ) );
502 }
503
504 BOOST_REQUIRE_EQUAL( pins.size(), 2 );
505
506 const int tolerance = 2;
507
508 for( SCH_PIN* pin : pins )
509 {
510 std::vector<int> gaps = collectNumberGapsPerRotation( pin );
511
512 int minGap = *std::min_element( gaps.begin(), gaps.end() );
513 int maxGap = *std::max_element( gaps.begin(), gaps.end() );
514
515 BOOST_CHECK_MESSAGE( ( maxGap - minGap ) <= tolerance,
516 "Pin " << pin->GetNumber() << " number gap changes with rotation. "
517 << "0=" << gaps[0] << " 90=" << gaps[1] << " 180=" << gaps[2] << " 270=" << gaps[3]
518 << " (tolerance " << tolerance << ")" );
519 }
520}
521
522// Symbol for issue 24894: number-only display, one bottom-edge vertical pin with a
523// stacked number that wraps to a block, one horizontal pin with the same, and a
524// horizontal pin with a plain single-line number.
525static std::unique_ptr<LIB_SYMBOL> createMirrorTestSymbol()
526{
527 auto symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestMirror" ) );
528
529 symbol->SetShowPinNames( false );
530 symbol->SetShowPinNumbers( true );
531
532 auto vertPin = std::make_unique<SCH_PIN>( symbol.get() );
533 vertPin->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( 250 ) ) );
534 vertPin->SetOrientation( PIN_ORIENTATION::PIN_UP );
535 vertPin->SetLength( schIUScale.MilsToIU( 50 ) );
536 vertPin->SetNumber( wxT( "[8,11,16,19]" ) );
537 vertPin->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
538 vertPin->SetUnit( 1 );
539
540 auto horizPin = std::make_unique<SCH_PIN>( symbol.get() );
541 horizPin->SetPosition( VECTOR2I( schIUScale.MilsToIU( -250 ), 0 ) );
542 horizPin->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
543 horizPin->SetLength( schIUScale.MilsToIU( 50 ) );
544 horizPin->SetNumber( wxT( "[1,2,3,4]" ) );
545 horizPin->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
546 horizPin->SetUnit( 1 );
547
548 auto plainPin = std::make_unique<SCH_PIN>( symbol.get() );
549 plainPin->SetPosition( VECTOR2I( schIUScale.MilsToIU( 250 ), 0 ) );
550 plainPin->SetOrientation( PIN_ORIENTATION::PIN_LEFT );
551 plainPin->SetLength( schIUScale.MilsToIU( 100 ) );
552 plainPin->SetNumber( wxT( "12" ) );
553 plainPin->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
554 plainPin->SetUnit( 1 );
555
556 symbol->AddDrawItem( vertPin.release() );
557 symbol->AddDrawItem( horizPin.release() );
558 symbol->AddDrawItem( plainPin.release() );
559
560 return symbol;
561}
562
563
565{
566 TRANSFORM oldTransform = DefaultTransform;
567 DefaultTransform = aTransform;
568
569 PIN_LAYOUT_CACHE cache( *aPin );
570 std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> info = cache.GetPinNumberInfo( 0 );
571
572 DefaultTransform = oldTransform;
573
574 BOOST_REQUIRE_MESSAGE( info.has_value(), "Missing pin number info for pin " << aPin->GetNumber() );
575 return *info;
576}
577
578
579// Truth table for the side-flip test used by the painter and the plotter.
580BOOST_AUTO_TEST_CASE( StackedTextSideFlippedTransforms )
581{
582 auto symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestFlip" ) );
583
584 auto pin = std::make_unique<SCH_PIN>( symbol.get() );
585 SCH_PIN* pinPtr = pin.get();
586 symbol->AddDrawItem( pin.release() );
587
588 const TRANSFORM identity( 1, 0, 0, 1 );
589 const TRANSFORM mirrorY( -1, 0, 0, 1 ); // left-right flip
590 const TRANSFORM mirrorX( 1, 0, 0, -1 ); // top-bottom flip
591 const TRANSFORM rot180( -1, 0, 0, -1 );
592
593 struct FLIP_CASE
594 {
595 PIN_ORIENTATION orient;
596 TRANSFORM transform;
597 bool expected;
598 };
599
600 const std::vector<FLIP_CASE> cases = {
601 { PIN_ORIENTATION::PIN_RIGHT, identity, false },
602 { PIN_ORIENTATION::PIN_LEFT, identity, false },
603 { PIN_ORIENTATION::PIN_UP, identity, false },
604 { PIN_ORIENTATION::PIN_DOWN, identity, false },
605
606 // Left-right flip moves the block side of vertical pins only
607 { PIN_ORIENTATION::PIN_RIGHT, mirrorY, false },
608 { PIN_ORIENTATION::PIN_LEFT, mirrorY, false },
609 { PIN_ORIENTATION::PIN_UP, mirrorY, true },
610 { PIN_ORIENTATION::PIN_DOWN, mirrorY, true },
611
612 // Top-bottom flip moves the block side of horizontal pins only
613 { PIN_ORIENTATION::PIN_RIGHT, mirrorX, true },
614 { PIN_ORIENTATION::PIN_LEFT, mirrorX, true },
615 { PIN_ORIENTATION::PIN_UP, mirrorX, false },
616 { PIN_ORIENTATION::PIN_DOWN, mirrorX, false },
617
618 { PIN_ORIENTATION::PIN_RIGHT, rot180, true },
619 { PIN_ORIENTATION::PIN_LEFT, rot180, true },
620 { PIN_ORIENTATION::PIN_UP, rot180, true },
621 { PIN_ORIENTATION::PIN_DOWN, rot180, true },
622 };
623
624 for( const FLIP_CASE& c : cases )
625 {
626 pinPtr->SetOrientation( c.orient );
627
628 BOOST_CHECK_MESSAGE( pinPtr->StackedTextSideFlipped( c.transform ) == c.expected,
629 "Orientation " << (int) c.orient << " transform (" << c.transform.x1 << ","
630 << c.transform.y1 << "," << c.transform.x2 << "," << c.transform.y2
631 << ") expected " << c.expected );
632 }
633}
634
635
636// Issue 24894: a stacked multi-line number block must move to the other side of the pin
637// when the symbol is mirrored, so it stays clear of the neighbouring pins. Single-line
638// numbers keep the classic fixed side.
639BOOST_AUTO_TEST_CASE( StackedBlockFollowsMirror )
640{
641 std::unique_ptr<LIB_SYMBOL> symbol = createMirrorTestSymbol();
642 BOOST_REQUIRE( symbol );
643
644 SCH_PIN* vertPin = nullptr;
645 SCH_PIN* horizPin = nullptr;
646 SCH_PIN* plainPin = nullptr;
647
648 for( SCH_ITEM& item : symbol->GetDrawItems() )
649 {
650 if( item.Type() != SCH_PIN_T )
651 continue;
652
653 SCH_PIN* pin = static_cast<SCH_PIN*>( &item );
654
655 if( pin->GetNumber() == wxT( "[8,11,16,19]" ) )
656 vertPin = pin;
657 else if( pin->GetNumber() == wxT( "[1,2,3,4]" ) )
658 horizPin = pin;
659 else if( pin->GetNumber() == wxT( "12" ) )
660 plainPin = pin;
661 }
662
663 BOOST_REQUIRE( vertPin && horizPin && plainPin );
664
665 const TRANSFORM identity( 1, 0, 0, 1 );
666 const TRANSFORM mirrorY( -1, 0, 0, 1 );
667 const TRANSFORM mirrorX( 1, 0, 0, -1 );
668
669 // Vertical pin: block left of the pin, right when mirrored left-right
670 {
671 PIN_LAYOUT_CACHE::TEXT_INFO base = numberInfoWithTransform( vertPin, identity );
672 PIN_LAYOUT_CACHE::TEXT_INFO mirrored = numberInfoWithTransform( vertPin, mirrorY );
673
674 BOOST_REQUIRE_MESSAGE( base.m_Text.Contains( '\n' ), "Stacked number did not wrap to a block" );
675
676 int baseOffset = base.m_TextPosition.x - vertPin->GetPosition().x;
677 int mirroredOffset = mirrored.m_TextPosition.x - vertPin->GetPosition().x;
678
679 BOOST_CHECK_MESSAGE( baseOffset < 0, "Unmirrored block not left of the vertical pin" );
680 BOOST_CHECK_MESSAGE( mirroredOffset > 0, "Mirrored block not right of the vertical pin" );
681 BOOST_CHECK_EQUAL( baseOffset, -mirroredOffset );
682 }
683
684 // Horizontal pin: block above the pin, below when mirrored top-bottom
685 {
686 PIN_LAYOUT_CACHE::TEXT_INFO base = numberInfoWithTransform( horizPin, identity );
687 PIN_LAYOUT_CACHE::TEXT_INFO mirrored = numberInfoWithTransform( horizPin, mirrorX );
688
689 BOOST_REQUIRE_MESSAGE( base.m_Text.Contains( '\n' ), "Stacked number did not wrap to a block" );
690
691 int baseOffset = base.m_TextPosition.y - horizPin->GetPosition().y;
692 int mirroredOffset = mirrored.m_TextPosition.y - horizPin->GetPosition().y;
693
694 BOOST_CHECK_MESSAGE( baseOffset < 0, "Unmirrored block not above the horizontal pin" );
695 BOOST_CHECK_MESSAGE( mirroredOffset > 0, "Mirrored block not below the horizontal pin" );
696 BOOST_CHECK_EQUAL( baseOffset, -mirroredOffset );
697 }
698
699 // Single-line number keeps the classic side under both mirrors
700 {
701 PIN_LAYOUT_CACHE::TEXT_INFO base = numberInfoWithTransform( plainPin, identity );
702 PIN_LAYOUT_CACHE::TEXT_INFO mirrored = numberInfoWithTransform( plainPin, mirrorX );
703
704 BOOST_REQUIRE_MESSAGE( !base.m_Text.Contains( '\n' ), "Plain number unexpectedly wrapped" );
705
707 BOOST_CHECK_MESSAGE( mirrored.m_TextPosition.y < plainPin->GetPosition().y,
708 "Single-line number moved off the classic side" );
709 }
710
711 // Painter hint: a pre-transformed temp pin flips via the flag alone
712 {
713 PIN_LAYOUT_CACHE::TEXT_INFO base = numberInfoWithTransform( vertPin, identity );
714
715 vertPin->SetFlipStackedTextSide( true );
716 PIN_LAYOUT_CACHE::TEXT_INFO flagged = numberInfoWithTransform( vertPin, identity );
717 vertPin->SetFlipStackedTextSide( false );
718
720 -( flagged.m_TextPosition.x - vertPin->GetPosition().x ) );
721 }
722}
723
constexpr EDA_IU_SCALE schIUScale
Definition base_units.h:123
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
std::optional< BOX2I > OPT_BOX2I
Definition box2.h:922
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:554
constexpr coord_type GetLeft() const
Definition box2.h:224
constexpr coord_type GetRight() const
Definition box2.h:213
constexpr coord_type GetTop() const
Definition box2.h:225
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:307
constexpr coord_type GetBottom() const
Definition box2.h:218
Define a library symbol object.
Definition lib_symbol.h:114
A pin layout helper is a class that manages the layout of the parts of a pin on a schematic symbol:
OPT_BOX2I GetPinNumberBBox()
Get the bounding box of the pin number, if there is one.
std::optional< TEXT_INFO > GetPinNameInfo(int aShadowWidth)
Get the text info for the pin name.
std::optional< TEXT_INFO > GetPinNumberInfo(int aShadowWidth)
OPT_BOX2I GetPinNameBBox()
Get the bounding box of the pin name, if there is one.
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:162
PIN_ORIENTATION PinDrawOrient(const TRANSFORM &aTransform) const
Return the pin real orientation (PIN_UP, PIN_DOWN, PIN_RIGHT, PIN_LEFT), according to its orientation...
Definition sch_pin.cpp:1399
void SetOrientation(PIN_ORIENTATION aOrientation)
Definition sch_pin.h:99
void SetFlipStackedTextSide(bool aFlip)
Definition sch_pin.h:407
VECTOR2I GetPosition() const override
Definition sch_pin.cpp:350
bool StackedTextSideFlipped(const TRANSFORM &aTransform) const
Return true when aTransform maps the side the stacked number block is drawn on (above horizontal pins...
Definition sch_pin.cpp:1434
const wxString & GetNumber() const
Definition sch_pin.h:130
for transforming drawing coordinates for a wxDC device context.
Definition transform.h:42
STL class.
TRANSFORM DefaultTransform
Definition transform.cpp:28
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ PT_PASSIVE
pin for passive symbols: must be connected, and can be connected to any pin.
Definition pin_type.h:39
PIN_ORIENTATION
The symbol library pin object orientations.
Definition pin_type.h:101
@ PIN_UP
The pin extends upwards from the connection point: Probably on the bottom side of the symbol.
Definition pin_type.h:123
@ PIN_RIGHT
The pin extends rightwards from the connection point.
Definition pin_type.h:107
@ PIN_LEFT
The pin extends leftwards from the connection point: Probably on the right side of the symbol.
Definition pin_type.h:114
@ PIN_DOWN
The pin extends downwards from the connection: Probably on the top side of the symbol.
Definition pin_type.h:131
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
KIBIS_PIN * pin1
KIBIS_PIN * pin
VECTOR3I expected(15, 30, 45)
static int nearEdgeClearance(const SCH_PIN *aPin, const TRANSFORM &aTransform, const BOX2I &aBox)
static std::unique_ptr< LIB_SYMBOL > createNumberOnlySymbol()
static VECTOR2I getPinLineEnd(const SCH_PIN *pin, const TRANSFORM &transform)
Get pin geometry (line segment from connection point to pin end)
static std::unique_ptr< LIB_SYMBOL > createTestResistorSymbol()
Create a test symbol with stacked pin numbers for rotation testing.
static std::unique_ptr< LIB_SYMBOL > createMirrorTestSymbol()
static PIN_LAYOUT_CACHE::TEXT_INFO numberInfoWithTransform(SCH_PIN *aPin, const TRANSFORM &aTransform)
static std::vector< int > collectNumberGapsPerRotation(SCH_PIN *aPin)
BOOST_AUTO_TEST_CASE(PinNumbersNoOverlapAllRotations)
Test that pin numbers don't overlap with pin geometry across all rotations.
VECTOR2I end
BOOST_CHECK_EQUAL(result, "25.4")
@ SCH_PIN_T
Definition typeinfo.h:150
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683