/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2015 Chris Pavlina * Copyright (C) 2015, 2019 KiCad Developers, see change_log.txt for contributors. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ /****************************************************************************** * Field autoplacer: Tries to find an optimal place for component fields, and * places them there. There are two modes: "auto"-autoplace, and "manual" autoplace. * Auto mode is for when the process is run automatically, like when rotating parts, * and it avoids doing things that would be helpful for the final positioning but * annoying if they happened without permission. * Short description of the process: * * 1. Compute the dimensions of the fields' bounding box ::ComputeFBoxSize * 2. Determine which side the fields will go on. ::choose_side_for_fields * 1. Sort the four sides in preference order, * depending on the component's shape and * orientation ::get_preferred_sides * 2. If in manual mode, sift out the sides that would * cause fields to overlap other items ::get_colliding_sides * 3. If any remaining sides have zero pins there, * choose the highest zero-pin side according to * preference order. * 4. If all sides have pins, choose the side with the * fewest pins. * 3. Compute the position of the fields' bounding box ::field_box_placement * 4. In manual mode, shift the box vertically if possible * to fit fields between adjacent wires ::fit_fields_between_wires * 5. Move all fields to their final positions * 1. Re-justify fields if options allow that ::justify_field * 2. Round to a 50-mil grid coordinate if desired */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define FIELD_PADDING Mils2iu( 10 ) // arbitrarily chosen for aesthetics #define FIELD_PADDING_ALIGNED Mils2iu( 18 ) // aligns 50 mil text to a 100 mil grid #define WIRE_V_SPACING Mils2iu( 100 ) #define HPADDING Mils2iu( 25 ) #define VPADDING Mils2iu( 25 ) /** * Round up/down to the nearest multiple of n */ template T round_n( const T& value, const T& n, bool aRoundUp ) { if( value % n ) return n * (value / n + (aRoundUp ? 1 : 0)); else return value; } /** * Convert an integer to a horizontal justification; neg=L zero=C pos=R */ EDA_TEXT_HJUSTIFY_T TO_HJUSTIFY( int x ) { return static_cast( x ); } class AUTOPLACER { SCH_SCREEN* m_screen; SCH_COMPONENT* m_component; std::vector m_fields; std::vector m_colliders; EDA_RECT m_comp_bbox; wxSize m_fbox_size; bool m_allow_rejustify, m_align_to_grid; bool m_power_symbol; public: typedef wxPoint SIDE; static const SIDE SIDE_TOP, SIDE_BOTTOM, SIDE_LEFT, SIDE_RIGHT; enum COLLISION { COLLIDE_NONE, COLLIDE_OBJECTS, COLLIDE_H_WIRES }; struct SIDE_AND_NPINS { SIDE side; unsigned pins; }; struct SIDE_AND_COLL { SIDE side; COLLISION collision; }; AUTOPLACER( SCH_COMPONENT* aComponent, SCH_SCREEN* aScreen ) :m_screen( aScreen ), m_component( aComponent ) { m_component->GetFields( m_fields, /* aVisibleOnly */ true ); Kiface().KifaceSettings()->Read( AutoplaceJustifyEntry, &m_allow_rejustify, true ); Kiface().KifaceSettings()->Read( AutoplaceAlignEntry, &m_align_to_grid, false ); m_comp_bbox = m_component->GetBodyBoundingBox(); m_fbox_size = ComputeFBoxSize( /* aDynamic */ true ); m_power_symbol = ! m_component->IsInNetlist(); if( aScreen ) get_possible_colliders( m_colliders ); } /** * Do the actual autoplacement. * @param aManual - if true, use extra heuristics for smarter placement when manually * called up. */ void DoAutoplace( bool aManual ) { bool force_wire_spacing = false; SIDE field_side = choose_side_for_fields( aManual ); wxPoint fbox_pos = field_box_placement( field_side ); EDA_RECT field_box( fbox_pos, m_fbox_size ); if( aManual ) force_wire_spacing = fit_fields_between_wires( &field_box, field_side ); // Move the fields int last_y_coord = field_box.GetTop(); for( unsigned field_idx = 0; field_idx < m_fields.size(); ++field_idx ) { SCH_FIELD* field = m_fields[field_idx]; if( m_allow_rejustify ) justify_field( field, field_side ); wxPoint pos( field_horiz_placement( field, field_box ), field_vert_placement( field, field_box, &last_y_coord, !force_wire_spacing ) ); if( m_align_to_grid ) { pos.x = round_n( pos.x, Mils2iu( 50 ), field_side.x >= 0 ); pos.y = round_n( pos.y, Mils2iu( 50 ), field_side.y == 1 ); } field->SetPosition( pos ); } } protected: /** * Compute and return the size of the fields' bounding box. * @param aDynamic - if true, use dynamic spacing */ wxSize ComputeFBoxSize( bool aDynamic ) { int max_field_width = 0; int total_height = 0; for( SCH_FIELD* field : m_fields ) { int field_width; int field_height; if( m_component->GetTransform().y1 ) { field->SetTextAngle( TEXT_ANGLE_VERT ); } else { field->SetTextAngle( TEXT_ANGLE_HORIZ ); } field_width = field->GetBoundingBox().GetWidth(); field_height = field->GetBoundingBox().GetHeight(); max_field_width = std::max( max_field_width, field_width ); if( aDynamic ) total_height += field_height + get_field_padding(); else total_height += WIRE_V_SPACING; } return wxSize( max_field_width, total_height ); } /** * Return the side that a pin is on. */ SIDE get_pin_side( LIB_PIN* aPin ) { int pin_orient = aPin->PinDrawOrient( m_component->GetTransform() ); switch( pin_orient ) { case PIN_RIGHT: return SIDE_LEFT; case PIN_LEFT: return SIDE_RIGHT; case PIN_UP: return SIDE_BOTTOM; case PIN_DOWN: return SIDE_TOP; default: wxFAIL_MSG( "Invalid pin orientation" ); return SIDE_LEFT; } } /** * Count the number of pins on a side of the component. */ unsigned pins_on_side( SIDE aSide ) { unsigned pin_count = 0; std::vector pins; m_component->GetPins( pins ); for( LIB_PIN* each_pin : pins ) { if( !each_pin->IsVisible() && !m_power_symbol ) continue; if( get_pin_side( each_pin ) == aSide ) ++pin_count; } return pin_count; } /** * Populate a list of all drawing items that *may* collide with the fields. That is, * all drawing items, including other fields, that are not the current component or * its own fields. */ void get_possible_colliders( std::vector& aItems ) { wxCHECK_RET( m_screen, "get_possible_colliders() with null m_screen" ); for( auto item : m_screen->Items().Overlapping( m_component->GetBoundingBox() ) ) { if( SCH_COMPONENT* comp = dynamic_cast( item ) ) { if( comp == m_component ) continue; std::vector fields; comp->GetFields( fields, /* aVisibleOnly */ true ); for( SCH_FIELD* field : fields ) aItems.push_back( field ); } aItems.push_back( item ); } } /** * Filter a list of possible colliders to include only those that actually collide * with a given rectangle. Returns the new vector. */ std::vector filtered_colliders( const EDA_RECT& aRect ) { std::vector filtered; for( SCH_ITEM* item : m_colliders ) { EDA_RECT item_box; if( SCH_COMPONENT* item_comp = dynamic_cast( item ) ) item_box = item_comp->GetBodyBoundingBox(); else item_box = item->GetBoundingBox(); if( item_box.Intersects( aRect ) ) filtered.push_back( item ); } return filtered; } /** * Return a list with the preferred field sides for the component, in * decreasing order of preference. */ std::vector get_preferred_sides() { SIDE_AND_NPINS sides_init[] = { { SIDE_RIGHT, pins_on_side( SIDE_RIGHT ) }, { SIDE_TOP, pins_on_side( SIDE_TOP ) }, { SIDE_LEFT, pins_on_side( SIDE_LEFT ) }, { SIDE_BOTTOM, pins_on_side( SIDE_BOTTOM ) }, }; std::vector sides( sides_init, sides_init + arrayDim( sides_init ) ); int orient = m_component->GetOrientation(); int orient_angle = orient & 0xff; // enum is a bitmask bool h_mirrored = ( ( orient & CMP_MIRROR_X ) && ( orient_angle == CMP_ORIENT_0 || orient_angle == CMP_ORIENT_180 ) ); double w = double( m_comp_bbox.GetWidth() ); double h = double( m_comp_bbox.GetHeight() ); // The preferred-sides heuristics are a bit magical. These were determined mostly // by trial and error. if( m_power_symbol ) { // For power symbols, we generally want the label at the top first. switch( orient_angle ) { case CMP_ORIENT_0: std::swap( sides[0], sides[1] ); std::swap( sides[1], sides[3] ); // TOP, BOTTOM, RIGHT, LEFT break; case CMP_ORIENT_90: std::swap( sides[0], sides[2] ); std::swap( sides[1], sides[2] ); // LEFT, RIGHT, TOP, BOTTOM break; case CMP_ORIENT_180: std::swap( sides[0], sides[3] ); // BOTTOM, TOP, LEFT, RIGHT break; case CMP_ORIENT_270: std::swap( sides[1], sides[2] ); // RIGHT, LEFT, TOP, BOTTOM break; } } else { // If the component is horizontally mirrored, swap left and right if( h_mirrored ) { std::swap( sides[0], sides[2] ); } // If the component is very long or is a power symbol, swap H and V if( w/h > 3.0 ) { std::swap( sides[0], sides[1] ); std::swap( sides[1], sides[3] ); } } return sides; } /** * Return a list of the sides where a field set would collide with another item. */ std::vector get_colliding_sides() { SIDE sides_init[] = { SIDE_RIGHT, SIDE_TOP, SIDE_LEFT, SIDE_BOTTOM }; std::vector sides( sides_init, sides_init + arrayDim( sides_init ) ); std::vector colliding; // Iterate over all sides and find the ones that collide for( SIDE side : sides ) { EDA_RECT box( field_box_placement( side ), m_fbox_size ); COLLISION collision = COLLIDE_NONE; for( SCH_ITEM* collider : filtered_colliders( box ) ) { SCH_LINE* line = dynamic_cast( collider ); if( line && !side.x ) { wxPoint start = line->GetStartPoint(), end = line->GetEndPoint(); if( start.y == end.y && collision != COLLIDE_OBJECTS ) collision = COLLIDE_H_WIRES; else collision = COLLIDE_OBJECTS; } else collision = COLLIDE_OBJECTS; } if( collision != COLLIDE_NONE ) colliding.push_back( { side, collision } ); } return colliding; } /** * Choose a side for the fields, filtered on only one side collision type. * Removes the sides matching the filter from the list. */ SIDE_AND_NPINS choose_side_filtered( std::vector& aSides, const std::vector& aCollidingSides, COLLISION aCollision, SIDE_AND_NPINS aLastSelection) { SIDE_AND_NPINS sel = aLastSelection; std::vector::iterator it = aSides.begin(); while( it != aSides.end() ) { bool collide = false; for( SIDE_AND_COLL collision : aCollidingSides ) { if( collision.side == it->side && collision.collision == aCollision ) collide = true; } if( !collide ) ++it; else { if( it->pins <= sel.pins ) { sel.pins = it->pins; sel.side = it->side; } it = aSides.erase( it ); } } return sel; } /** * Look where a component's pins are to pick a side to put the fields on * @param aAvoidCollisions - if true, pick last the sides where the label will collide * with other items. */ SIDE choose_side_for_fields( bool aAvoidCollisions ) { std::vector sides = get_preferred_sides(); std::reverse( sides.begin(), sides.end() ); SIDE_AND_NPINS side = { wxPoint( 1, 0 ), UINT_MAX }; if( aAvoidCollisions ) { std::vector colliding_sides = get_colliding_sides(); side = choose_side_filtered( sides, colliding_sides, COLLIDE_OBJECTS, side ); side = choose_side_filtered( sides, colliding_sides, COLLIDE_H_WIRES, side ); } for( SIDE_AND_NPINS& each_side : sides | boost::adaptors::reversed ) { if( !each_side.pins ) return each_side.side; } for( SIDE_AND_NPINS& each_side : sides ) { if( each_side.pins <= side.pins ) { side.pins = each_side.pins; side.side = each_side.side; } } return side.side; } /** * Set the justification of a field based on the side it's supposed to be on, taking * into account whether the field will be displayed with flipped justification due to * mirroring. */ void justify_field( SCH_FIELD* aField, SIDE aFieldSide ) { // Justification is set twice to allow IsHorizJustifyFlipped() to work correctly. aField->SetHorizJustify( TO_HJUSTIFY( -aFieldSide.x ) ); aField->SetHorizJustify( TO_HJUSTIFY( -aFieldSide.x * ( aField->IsHorizJustifyFlipped() ? -1 : 1 ) ) ); aField->SetVertJustify( GR_TEXT_VJUSTIFY_CENTER ); } /** * Return the position of the field bounding box. */ wxPoint field_box_placement( SIDE aFieldSide ) { wxPoint fbox_center = m_comp_bbox.Centre(); int offs_x = ( m_comp_bbox.GetWidth() + m_fbox_size.GetWidth() ) / 2 + HPADDING; int offs_y = ( m_comp_bbox.GetHeight() + m_fbox_size.GetHeight() ) / 2 + VPADDING; fbox_center.x += aFieldSide.x * offs_x; fbox_center.y += aFieldSide.y * offs_y; wxPoint fbox_pos( fbox_center.x - m_fbox_size.GetWidth() / 2, fbox_center.y - m_fbox_size.GetHeight() / 2 ); return fbox_pos; } /** * Shift a field box up or down a bit to make the fields fit between some wires. * Returns true if a shift was made. */ bool fit_fields_between_wires( EDA_RECT* aBox, SIDE aSide ) { if( aSide != SIDE_TOP && aSide != SIDE_BOTTOM ) return false; std::vector colliders = filtered_colliders( *aBox ); if( colliders.empty() ) return false; // Find the offset of the wires for proper positioning int offset = 0; for( SCH_ITEM* item : colliders ) { SCH_LINE* line = dynamic_cast( item ); if( !line ) return false; wxPoint start = line->GetStartPoint(), end = line->GetEndPoint(); if( start.y != end.y ) return false; int this_offset = (3 * WIRE_V_SPACING / 2) - ( start.y % WIRE_V_SPACING ); if( offset == 0 ) offset = this_offset; else if( offset != this_offset ) return false; } // At this point we are recomputing the field box size. Do not // return false after this point. m_fbox_size = ComputeFBoxSize( /* aDynamic */ false ); wxPoint pos = aBox->GetPosition(); // Remove the existing padding to get a bit more space to work with if( aSide == SIDE_BOTTOM ) { pos.y = m_comp_bbox.GetBottom() - get_field_padding(); } else { pos.y = m_comp_bbox.GetTop() - m_fbox_size.y + get_field_padding(); } pos.y = round_n( pos.y, WIRE_V_SPACING, aSide == SIDE_BOTTOM ); aBox->SetOrigin( pos ); return true; } /** * Place a field horizontally, taking into account the field width and justification. * * @param aField - the field to place. * @param aFieldBox - box in which fields will be placed * * @return Correct field horizontal position */ int field_horiz_placement( SCH_FIELD *aField, const EDA_RECT &aFieldBox ) { int field_hjust; int field_xcoord; if( aField->IsHorizJustifyFlipped() ) field_hjust = -aField->GetHorizJustify(); else field_hjust = aField->GetHorizJustify(); switch( field_hjust ) { case GR_TEXT_HJUSTIFY_LEFT: field_xcoord = aFieldBox.GetLeft(); break; case GR_TEXT_HJUSTIFY_CENTER: field_xcoord = aFieldBox.Centre().x; break; case GR_TEXT_HJUSTIFY_RIGHT: field_xcoord = aFieldBox.GetRight(); break; default: wxFAIL_MSG( "Unexpected value for SCH_FIELD::GetHorizJustify()" ); field_xcoord = aFieldBox.Centre().x; // Most are centered } return field_xcoord; } /** * Place a field vertically. Because field vertical placements accumulate, * this takes a pointer to a vertical position accumulator. * * @param aField - the field to place. * @param aFieldBox - box in which fields will be placed. * @param aPosAccum - pointer to a position accumulator * @param aDynamic - use dynamic spacing * * @return Correct field vertical position */ int field_vert_placement( SCH_FIELD *aField, const EDA_RECT &aFieldBox, int *aPosAccum, bool aDynamic ) { int field_height; int padding; if( aDynamic ) { field_height = aField->GetBoundingBox().GetHeight(); padding = get_field_padding(); } else { field_height = WIRE_V_SPACING / 2; padding = WIRE_V_SPACING / 2; } int placement = *aPosAccum + padding / 2 + field_height / 2; *aPosAccum += padding + field_height; return placement; } /** * Return the desired padding between fields. */ int get_field_padding() { if( m_align_to_grid ) return FIELD_PADDING_ALIGNED; else return FIELD_PADDING; } }; const AUTOPLACER::SIDE AUTOPLACER::SIDE_TOP( 0, -1 ); const AUTOPLACER::SIDE AUTOPLACER::SIDE_BOTTOM( 0, 1 ); const AUTOPLACER::SIDE AUTOPLACER::SIDE_LEFT( -1, 0 ); const AUTOPLACER::SIDE AUTOPLACER::SIDE_RIGHT( 1, 0 ); void SCH_COMPONENT::AutoplaceFields( SCH_SCREEN* aScreen, bool aManual ) { if( aManual ) wxASSERT_MSG( aScreen, "A SCH_SCREEN pointer must be given for manual autoplacement" ); AUTOPLACER autoplacer( this, aScreen ); autoplacer.DoAutoplace( aManual ); m_fieldsAutoplaced = ( aManual? AUTOPLACED_MANUAL : AUTOPLACED_AUTO ); }