Pcbnew: add drc test for texts on copper layer (only in full drc test, not in on line drc), from a patch sent by Simon Schumann
Add EDA_TEXT::TransformTextShapeToSegmentList function to export a list of segments used to draw/plot the text.
This commit is contained in:
parent
adbf343fef
commit
c8d69f19c8
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
#include <eda_text.h>
|
#include <eda_text.h>
|
||||||
#include <drawtxt.h>
|
#include <drawtxt.h>
|
||||||
|
#include <macros.h>
|
||||||
#include <trigo.h> // RotatePoint
|
#include <trigo.h> // RotatePoint
|
||||||
#include <class_drawpanel.h> // EDA_DRAW_PANEL
|
#include <class_drawpanel.h> // EDA_DRAW_PANEL
|
||||||
|
|
||||||
|
@ -447,3 +448,55 @@ void EDA_TEXT::Format( OUTPUTFORMATTER* aFormatter, int aNestLevel, int aControl
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert the text shape to a list of segment
|
||||||
|
// each segment is stored as 2 wxPoints: its starting point and its ending point
|
||||||
|
// we are using DrawGraphicText to create the segments.
|
||||||
|
// and therefore a call-back function is needed
|
||||||
|
static std::vector<wxPoint>* s_cornerBuffer;
|
||||||
|
|
||||||
|
// This is a call back function, used by DrawGraphicText to put each segment in buffer
|
||||||
|
static void addTextSegmToBuffer( int x0, int y0, int xf, int yf )
|
||||||
|
{
|
||||||
|
s_cornerBuffer->push_back( wxPoint( x0, y0 ) );
|
||||||
|
s_cornerBuffer->push_back( wxPoint( xf, yf ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
void EDA_TEXT::TransformTextShapeToSegmentList( std::vector<wxPoint>& aCornerBuffer ) const
|
||||||
|
{
|
||||||
|
wxSize size = GetSize();
|
||||||
|
|
||||||
|
if( IsMirrored() )
|
||||||
|
NEGATE( size.x );
|
||||||
|
|
||||||
|
s_cornerBuffer = &aCornerBuffer;
|
||||||
|
EDA_COLOR_T color = BLACK; // not actually used, but needed by DrawGraphicText
|
||||||
|
|
||||||
|
if( IsMultilineAllowed() )
|
||||||
|
{
|
||||||
|
wxArrayString* list = wxStringSplit( GetText(), '\n' );
|
||||||
|
std::vector<wxPoint> positions;
|
||||||
|
positions.reserve( list->Count() );
|
||||||
|
GetPositionsOfLinesOfMultilineText( positions, list->Count() );
|
||||||
|
|
||||||
|
for( unsigned ii = 0; ii < list->Count(); ii++ )
|
||||||
|
{
|
||||||
|
wxString txt = list->Item( ii );
|
||||||
|
DrawGraphicText( NULL, NULL, positions[ii], color,
|
||||||
|
txt, GetOrientation(), size,
|
||||||
|
GetHorizJustify(), GetVertJustify(),
|
||||||
|
GetThickness(), IsItalic(),
|
||||||
|
true, addTextSegmToBuffer );
|
||||||
|
}
|
||||||
|
|
||||||
|
delete list;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawGraphicText( NULL, NULL, GetTextPosition(), color,
|
||||||
|
GetText(), GetOrientation(), size,
|
||||||
|
GetHorizJustify(), GetVertJustify(),
|
||||||
|
GetThickness(), IsItalic(),
|
||||||
|
true, addTextSegmToBuffer );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -202,6 +202,14 @@ public:
|
||||||
GR_DRAWMODE aDrawMode, EDA_DRAW_MODE_T aDisplay_mode = LINE,
|
GR_DRAWMODE aDrawMode, EDA_DRAW_MODE_T aDisplay_mode = LINE,
|
||||||
EDA_COLOR_T aAnchor_color = UNSPECIFIED_COLOR );
|
EDA_COLOR_T aAnchor_color = UNSPECIFIED_COLOR );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the text shape to a list of segment
|
||||||
|
* each segment is stored as 2 wxPoints: the starting point and the ending point
|
||||||
|
* there are therefore 2*n points
|
||||||
|
* @param aCornerBuffer = a buffer to store the polygon
|
||||||
|
*/
|
||||||
|
void TransformTextShapeToSegmentList( std::vector<wxPoint>& aCornerBuffer ) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function TextHitTest
|
* Function TextHitTest
|
||||||
* Test if \a aPoint is within the bounds of this object.
|
* Test if \a aPoint is within the bounds of this object.
|
||||||
|
|
|
@ -309,11 +309,11 @@ void ZONE_CONTAINER::TransformSolidAreasShapesToPolygonSet(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function TransformBoundingBoxWithClearanceToPolygon
|
* Function TransformBoundingBoxWithClearanceToPolygon
|
||||||
* Convert the text bonding box to a rectangular polygon
|
* Convert the text bounding box to a rectangular polygon
|
||||||
* Used in filling zones calculations
|
* Used in filling zones calculations
|
||||||
* Circles and arcs are approximated by segments
|
* Circles and arcs are approximated by segments
|
||||||
* @param aCornerBuffer = a buffer to store the polygon
|
* @param aCornerBuffer = a buffer to store the polygon
|
||||||
* @param aClearanceValue = the clearance around the pad
|
* @param aClearanceValue = the clearance around the text bounding box
|
||||||
*/
|
*/
|
||||||
void TEXTE_PCB::TransformBoundingBoxWithClearanceToPolygon(
|
void TEXTE_PCB::TransformBoundingBoxWithClearanceToPolygon(
|
||||||
CPOLYGONS_LIST& aCornerBuffer,
|
CPOLYGONS_LIST& aCornerBuffer,
|
||||||
|
|
|
@ -111,6 +111,15 @@ wxString DRC_ITEM::GetErrorText() const
|
||||||
case DRCE_PAD_INSIDE_KEEPOUT:
|
case DRCE_PAD_INSIDE_KEEPOUT:
|
||||||
return wxString( _("Pad inside a keepout area"));
|
return wxString( _("Pad inside a keepout area"));
|
||||||
|
|
||||||
|
case DRCE_VIA_INSIDE_TEXT:
|
||||||
|
return wxString( _("Via inside a text"));
|
||||||
|
|
||||||
|
case DRCE_TRACK_INSIDE_TEXT:
|
||||||
|
return wxString( _("Track inside a text"));
|
||||||
|
|
||||||
|
case DRCE_PAD_INSIDE_TEXT:
|
||||||
|
return wxString( _("Pad inside a text"));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
wxString msg;
|
wxString msg;
|
||||||
|
|
154
pcbnew/drc.cpp
154
pcbnew/drc.cpp
|
@ -2,9 +2,9 @@
|
||||||
/*
|
/*
|
||||||
* This program source code file is part of KiCad, a free EDA CAD application.
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2004-2007 Jean-Pierre Charras, jean-pierre.charras@gipsa-lab.inpg.fr
|
* Copyright (C) 2004-2014 Jean-Pierre Charras, jp.charras at wanadoo.fr
|
||||||
* Copyright (C) 2007 Dick Hollenbeck, dick@softplc.com
|
* Copyright (C) 2014 Dick Hollenbeck, dick@softplc.com
|
||||||
* Copyright (C) 2007 KiCad Developers, see change_log.txt for contributors.
|
* Copyright (C) 2014 KiCad Developers, see change_log.txt for contributors.
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -24,9 +24,9 @@
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/****************************/
|
/**
|
||||||
/* DRC control */
|
* @file drc.cpp
|
||||||
/****************************/
|
*/
|
||||||
|
|
||||||
#include <fctsys.h>
|
#include <fctsys.h>
|
||||||
#include <wxPcbStruct.h>
|
#include <wxPcbStruct.h>
|
||||||
|
@ -38,8 +38,10 @@
|
||||||
#include <class_track.h>
|
#include <class_track.h>
|
||||||
#include <class_pad.h>
|
#include <class_pad.h>
|
||||||
#include <class_zone.h>
|
#include <class_zone.h>
|
||||||
|
#include <class_pcb_text.h>
|
||||||
#include <class_draw_panel_gal.h>
|
#include <class_draw_panel_gal.h>
|
||||||
#include <view/view.h>
|
#include <view/view.h>
|
||||||
|
#include <geometry/seg.h>
|
||||||
|
|
||||||
#include <pcbnew.h>
|
#include <pcbnew.h>
|
||||||
#include <drc_stuff.h>
|
#include <drc_stuff.h>
|
||||||
|
@ -261,6 +263,15 @@ void DRC::RunTests( wxTextCtrl* aMessages )
|
||||||
testKeepoutAreas();
|
testKeepoutAreas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find and gather vias, tracks, pads inside text boxes.
|
||||||
|
if( aMessages )
|
||||||
|
{
|
||||||
|
aMessages->AppendText( _( "Test texts...\n" ) );
|
||||||
|
wxSafeYield();
|
||||||
|
}
|
||||||
|
|
||||||
|
testTexts();
|
||||||
|
|
||||||
// update the m_ui listboxes
|
// update the m_ui listboxes
|
||||||
updatePointers();
|
updatePointers();
|
||||||
|
|
||||||
|
@ -628,6 +639,137 @@ void DRC::testKeepoutAreas()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DRC::testTexts()
|
||||||
|
{
|
||||||
|
std::vector<wxPoint> textShape; // a buffer to store the text shape (set of segments)
|
||||||
|
std::vector<D_PAD*> padList = m_pcb->GetPads();
|
||||||
|
|
||||||
|
// Test text areas for vias, tracks and pads inside text areas
|
||||||
|
for( BOARD_ITEM* item = m_pcb->m_Drawings; item; item = item->Next() )
|
||||||
|
{
|
||||||
|
// Drc test only items on copper layers
|
||||||
|
if( ! IsCopperLayer( item->GetLayer() ) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// only texts on copper layers are tested
|
||||||
|
if( item->Type() != PCB_TEXT_T )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
textShape.clear();
|
||||||
|
|
||||||
|
// So far the bounding box makes up the text-area
|
||||||
|
TEXTE_PCB* text = (TEXTE_PCB*) item;
|
||||||
|
text->TransformTextShapeToSegmentList( textShape );
|
||||||
|
|
||||||
|
if( textShape.size() == 0 ) // Should not happen (empty text?)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for( TRACK* track = m_pcb->m_Track; track != NULL; track = track->Next() )
|
||||||
|
{
|
||||||
|
if( ! track->IsOnLayer( item->GetLayer() ) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Test the distance between each segment and the current track/via
|
||||||
|
int min_dist = ( track->GetWidth() + text->GetThickness() ) /2 +
|
||||||
|
track->GetClearance(NULL);
|
||||||
|
|
||||||
|
if( track->Type() == PCB_TRACE_T )
|
||||||
|
{
|
||||||
|
SEG segref( track->GetStart(), track->GetEnd() );
|
||||||
|
|
||||||
|
// Error condition: Distance between text segment and track segment is
|
||||||
|
// smaller than the clearance of the segment
|
||||||
|
for( unsigned jj = 0; jj < textShape.size(); jj += 2 )
|
||||||
|
{
|
||||||
|
SEG segtest( textShape[jj], textShape[jj+1] );
|
||||||
|
int dist = segref.Distance( segtest );
|
||||||
|
|
||||||
|
if( dist < min_dist )
|
||||||
|
{
|
||||||
|
m_currentMarker = fillMarker( track, text,
|
||||||
|
DRCE_TRACK_INSIDE_TEXT,
|
||||||
|
m_currentMarker );
|
||||||
|
m_pcb->Add( m_currentMarker );
|
||||||
|
m_mainWindow->GetGalCanvas()->GetView()->Add( m_currentMarker );
|
||||||
|
m_currentMarker = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( track->Type() == PCB_VIA_T )
|
||||||
|
{
|
||||||
|
// Error condition: Distance between text segment and via is
|
||||||
|
// smaller than the clearance of the via
|
||||||
|
for( unsigned jj = 0; jj < textShape.size(); jj += 2 )
|
||||||
|
{
|
||||||
|
SEG segtest( textShape[jj], textShape[jj+1] );
|
||||||
|
|
||||||
|
if( segtest.PointCloserThan( track->GetPosition(), min_dist ) )
|
||||||
|
{
|
||||||
|
m_currentMarker = fillMarker( track, text,
|
||||||
|
DRCE_VIA_INSIDE_TEXT, m_currentMarker );
|
||||||
|
m_pcb->Add( m_currentMarker );
|
||||||
|
m_mainWindow->GetGalCanvas()->GetView()->Add( m_currentMarker );
|
||||||
|
m_currentMarker = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test pads
|
||||||
|
for( unsigned ii = 0; ii < padList.size(); ii++ )
|
||||||
|
{
|
||||||
|
D_PAD* pad = padList[ii];
|
||||||
|
|
||||||
|
if( ! pad->IsOnLayer( item->GetLayer() ) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
wxPoint shape_pos = pad->ShapePos();
|
||||||
|
|
||||||
|
for( unsigned jj = 0; jj < textShape.size(); jj += 2 )
|
||||||
|
{
|
||||||
|
SEG segtest( textShape[jj], textShape[jj+1] );
|
||||||
|
/* In order to make some calculations more easier or faster,
|
||||||
|
* pads and tracks coordinates will be made relative
|
||||||
|
* to the segment origin
|
||||||
|
*/
|
||||||
|
wxPoint origin = textShape[jj]; // origin will be the origin of other coordinates
|
||||||
|
m_segmEnd = textShape[jj+1] - origin;
|
||||||
|
wxPoint delta = m_segmEnd;
|
||||||
|
m_segmAngle = 0;
|
||||||
|
|
||||||
|
// for a non horizontal or vertical segment Compute the segment angle
|
||||||
|
// in tenths of degrees and its length
|
||||||
|
if( delta.x || delta.y ) // delta.x == delta.y == 0 for vias
|
||||||
|
{
|
||||||
|
// Compute the segment angle in 0,1 degrees
|
||||||
|
m_segmAngle = ArcTangente( delta.y, delta.x );
|
||||||
|
|
||||||
|
// Compute the segment length: we build an equivalent rotated segment,
|
||||||
|
// this segment is horizontal, therefore dx = length
|
||||||
|
RotatePoint( &delta, m_segmAngle ); // delta.x = length, delta.y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
m_segmLength = delta.x;
|
||||||
|
m_padToTestPos = shape_pos - origin;
|
||||||
|
|
||||||
|
if( !checkClearanceSegmToPad( pad, text->GetThickness(),
|
||||||
|
pad->GetClearance(NULL) ) )
|
||||||
|
{
|
||||||
|
m_currentMarker = fillMarker( pad, text,
|
||||||
|
DRCE_PAD_INSIDE_TEXT, m_currentMarker );
|
||||||
|
m_pcb->Add( m_currentMarker );
|
||||||
|
m_mainWindow->GetGalCanvas()->GetView()->Add( m_currentMarker );
|
||||||
|
m_currentMarker = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool DRC::doTrackKeepoutDrc( TRACK* aRefSeg )
|
bool DRC::doTrackKeepoutDrc( TRACK* aRefSeg )
|
||||||
{
|
{
|
||||||
// Test keepout areas for vias, tracks and pads inside keepout areas
|
// Test keepout areas for vias, tracks and pads inside keepout areas
|
||||||
|
|
|
@ -41,7 +41,9 @@
|
||||||
#include <class_pad.h>
|
#include <class_pad.h>
|
||||||
#include <class_track.h>
|
#include <class_track.h>
|
||||||
#include <class_zone.h>
|
#include <class_zone.h>
|
||||||
|
#include <class_zone.h>
|
||||||
#include <class_marker_pcb.h>
|
#include <class_marker_pcb.h>
|
||||||
|
#include <class_pcb_text.h>
|
||||||
|
|
||||||
|
|
||||||
MARKER_PCB* DRC::fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorCode,
|
MARKER_PCB* DRC::fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorCode,
|
||||||
|
@ -84,6 +86,11 @@ MARKER_PCB* DRC::fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorC
|
||||||
if( dToEnd < dToStart )
|
if( dToEnd < dToStart )
|
||||||
position = endPos;
|
position = endPos;
|
||||||
}
|
}
|
||||||
|
else if( aItem->Type() == PCB_TEXT_T )
|
||||||
|
{
|
||||||
|
position = aTrack->GetPosition();
|
||||||
|
posB = ((TEXTE_PCB*) aItem)->GetPosition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
position = aTrack->GetPosition();
|
position = aTrack->GetPosition();
|
||||||
|
@ -118,13 +125,33 @@ MARKER_PCB* DRC::fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
MARKER_PCB* DRC::fillMarker( D_PAD* aPad, D_PAD* bPad, int aErrorCode, MARKER_PCB* fillMe )
|
MARKER_PCB* DRC::fillMarker( D_PAD* aPad, BOARD_ITEM* aItem, int aErrorCode, MARKER_PCB* fillMe )
|
||||||
{
|
{
|
||||||
wxString textA = aPad->GetSelectMenuText();
|
wxString textA = aPad->GetSelectMenuText();
|
||||||
wxString textB = bPad->GetSelectMenuText();
|
wxString textB;
|
||||||
|
|
||||||
wxPoint posA = aPad->GetPosition();
|
wxPoint posA = aPad->GetPosition();
|
||||||
wxPoint posB = bPad->GetPosition();
|
wxPoint posB;
|
||||||
|
|
||||||
|
if( aItem )
|
||||||
|
{
|
||||||
|
textB = aItem->GetSelectMenuText();
|
||||||
|
|
||||||
|
switch( aItem->Type() )
|
||||||
|
{
|
||||||
|
case PCB_PAD_T:
|
||||||
|
posB = ((D_PAD*)aItem)->GetPosition();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PCB_TEXT_T:
|
||||||
|
posB = ((TEXTE_PCB*)aItem)->GetPosition();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
wxLogDebug( wxT("fillMarker: unsupported item") );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if( fillMe )
|
if( fillMe )
|
||||||
{
|
{
|
||||||
|
|
|
@ -75,6 +75,9 @@
|
||||||
#define DRCE_VIA_INSIDE_KEEPOUT 36 ///< Via in inside a keepout area
|
#define DRCE_VIA_INSIDE_KEEPOUT 36 ///< Via in inside a keepout area
|
||||||
#define DRCE_TRACK_INSIDE_KEEPOUT 37 ///< Track in inside a keepout area
|
#define DRCE_TRACK_INSIDE_KEEPOUT 37 ///< Track in inside a keepout area
|
||||||
#define DRCE_PAD_INSIDE_KEEPOUT 38 ///< Pad in inside a keepout area
|
#define DRCE_PAD_INSIDE_KEEPOUT 38 ///< Pad in inside a keepout area
|
||||||
|
#define DRCE_VIA_INSIDE_TEXT 39 ///< Via in inside a text area
|
||||||
|
#define DRCE_TRACK_INSIDE_TEXT 40 ///< Track in inside a text area
|
||||||
|
#define DRCE_PAD_INSIDE_TEXT 41 ///< Pad in inside a text area
|
||||||
|
|
||||||
|
|
||||||
class EDA_DRAW_PANEL;
|
class EDA_DRAW_PANEL;
|
||||||
|
@ -221,7 +224,7 @@ private:
|
||||||
*/
|
*/
|
||||||
MARKER_PCB* fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorCode, MARKER_PCB* fillMe );
|
MARKER_PCB* fillMarker( const TRACK* aTrack, BOARD_ITEM* aItem, int aErrorCode, MARKER_PCB* fillMe );
|
||||||
|
|
||||||
MARKER_PCB* fillMarker( D_PAD* aPad, D_PAD* bPad, int aErrorCode, MARKER_PCB* fillMe );
|
MARKER_PCB* fillMarker( D_PAD* aPad, BOARD_ITEM* aItem, int aErrorCode, MARKER_PCB* fillMe );
|
||||||
|
|
||||||
MARKER_PCB* fillMarker( ZONE_CONTAINER* aArea, int aErrorCode, MARKER_PCB* fillMe );
|
MARKER_PCB* fillMarker( ZONE_CONTAINER* aArea, int aErrorCode, MARKER_PCB* fillMe );
|
||||||
|
|
||||||
|
@ -281,6 +284,8 @@ private:
|
||||||
|
|
||||||
void testKeepoutAreas();
|
void testKeepoutAreas();
|
||||||
|
|
||||||
|
void testTexts();
|
||||||
|
|
||||||
//-----<single "item" tests>-----------------------------------------
|
//-----<single "item" tests>-----------------------------------------
|
||||||
|
|
||||||
bool doNetClass( boost::shared_ptr<NETCLASS> aNetClass, wxString& msg );
|
bool doNetClass( boost::shared_ptr<NETCLASS> aNetClass, wxString& msg );
|
||||||
|
|
|
@ -399,7 +399,7 @@ int GetClearanceBetweenSegments( int x1i, int y1i, int x1f, int y1f, int w1,
|
||||||
double dist;
|
double dist;
|
||||||
TestForIntersectionOfStraightLineSegments( x1i, y1i, x1f, y1f,
|
TestForIntersectionOfStraightLineSegments( x1i, y1i, x1f, y1f,
|
||||||
x2i, y2i, x2f, y2f, &xx, &yy, &dist );
|
x2i, y2i, x2f, y2f, &xx, &yy, &dist );
|
||||||
int d = KiROUND( dist - (w1 + w2) / 2 );
|
int d = KiROUND( dist ) - ((w1 + w2) / 2);
|
||||||
if( d < 0 )
|
if( d < 0 )
|
||||||
d = 0;
|
d = 0;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue