diff --git a/CMakeLists.txt b/CMakeLists.txt index af99ea1e2b..55b9924940 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,9 @@ project( kicad ) include( GNUInstallDirs ) include( CMakeDependentOption ) +# Output compile_commands.json for various LSP and other users +set( CMAKE_EXPORT_COMPILE_COMMANDS ON ) + # Path to local CMake modules. set( CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMakeModules ) diff --git a/eeschema/sch_line.h b/eeschema/sch_line.h index fcffd4af06..14451aaa09 100644 --- a/eeschema/sch_line.h +++ b/eeschema/sch_line.h @@ -87,6 +87,36 @@ public: int GetAngleFrom( const VECTOR2I& aPoint ) const; int GetReverseAngleFrom( const VECTOR2I& aPoint ) const; + /** + * Gets the angle between the start and end lines. + * + * @return Line angle in radians. + */ + inline EDA_ANGLE Angle() const + { + return ( EDA_ANGLE( (VECTOR2I) m_end - (VECTOR2I) m_start ) ); + } + + /** + * Saves the current line angle. Useful when dragging a line and its important to + * be able to restart the line from length 0 in the correct direction. + */ + inline void StoreAngle() { m_storedAngle = Angle(); } + + /** + * Returns the angle stored by StoreAngle() + * + * @return Stored angle in radians. + */ + inline EDA_ANGLE GetStoredAngle() const { return m_storedAngle; } + + /** + * Checks if line is orthogonal (to the grid). + * + * @return True if orthogonal, false if not or the line is zero length. + */ + inline bool IsOrthogonal() const { return Angle().IsCardinal(); } + bool IsNull() const { return m_start == m_end; } VECTOR2I GetStartPoint() const { return m_start; } @@ -282,6 +312,7 @@ private: bool m_endIsDangling; ///< True if end point is not connected. VECTOR2I m_start; ///< Line start point VECTOR2I m_end; ///< Line end point + EDA_ANGLE m_storedAngle; ///< Stored angle STROKE_PARAMS m_stroke; ///< Line stroke properties. // If real-time connectivity gets disabled (due to being too slow on a particular diff --git a/eeschema/sch_painter.cpp b/eeschema/sch_painter.cpp index 50c2a3921f..cf90d05185 100644 --- a/eeschema/sch_painter.cpp +++ b/eeschema/sch_painter.cpp @@ -1530,14 +1530,20 @@ void SCH_PAINTER::draw( const SCH_LINE *aLine, int aLayer ) { if( aLine->IsStartDangling() && aLine->IsWire() ) { - drawDanglingSymbol( aLine->GetStartPoint(), color, getLineWidth( aLine, drawingShadows ), - drawingShadows, aLine->IsBrightened() ); + COLOR4D danglingColor = + ( drawingShadows && !aLine->HasFlag( STARTPOINT ) ) ? color.Inverted() : color; + drawDanglingSymbol( aLine->GetStartPoint(), danglingColor, + getLineWidth( aLine, drawingShadows ), drawingShadows, + aLine->IsBrightened() ); } if( aLine->IsEndDangling() && aLine->IsWire() ) { - drawDanglingSymbol( aLine->GetEndPoint(), color, getLineWidth( aLine, drawingShadows ), - drawingShadows, aLine->IsBrightened() ); + COLOR4D danglingColor = + ( drawingShadows && !aLine->HasFlag( ENDPOINT ) ) ? color.Inverted() : color; + drawDanglingSymbol( aLine->GetEndPoint(), danglingColor, + getLineWidth( aLine, drawingShadows ), drawingShadows, + aLine->IsBrightened() ); } if( drawingDangling ) diff --git a/eeschema/tools/ee_selection_tool.cpp b/eeschema/tools/ee_selection_tool.cpp index 888f54bdc5..c30105ff7e 100644 --- a/eeschema/tools/ee_selection_tool.cpp +++ b/eeschema/tools/ee_selection_tool.cpp @@ -1291,7 +1291,25 @@ bool EE_SELECTION_TOOL::selectMultiple() else { select( aItem ); - aItem->SetFlags( STARTPOINT | ENDPOINT ); + + // Lines can have just one end selected + if( aItem->Type() == SCH_LINE_T ) + { + SCH_LINE* line = static_cast( aItem ); + + line->ClearFlags( STARTPOINT | ENDPOINT ); + + if( selectionRect.Contains( line->GetStartPoint() ) ) + line->SetFlags( STARTPOINT ); + + if( selectionRect.Contains( line->GetEndPoint() ) ) + line->SetFlags( ENDPOINT ); + + // If no ends were selected, select whole line (both ends) + if( !line->HasFlag( STARTPOINT ) && !line->HasFlag( ENDPOINT ) ) + line->SetFlags( STARTPOINT | ENDPOINT ); + } + anyAdded = true; } }; diff --git a/eeschema/tools/sch_editor_control.cpp b/eeschema/tools/sch_editor_control.cpp index 37ffbf49a5..8ceceed485 100644 --- a/eeschema/tools/sch_editor_control.cpp +++ b/eeschema/tools/sch_editor_control.cpp @@ -1842,6 +1842,11 @@ int SCH_EDITOR_CONTROL::Paste( const TOOL_EVENT& aEvent ) } } + // Lines need both ends selected for a move after paste so the whole + // line moves + if( item->Type() == SCH_LINE_T ) + item->SetFlags( STARTPOINT | ENDPOINT ); + item->SetFlags( IS_NEW | IS_PASTED | IS_MOVING ); m_frame->AddItemToScreenAndUndoList( m_frame->GetScreen(), (SCH_ITEM*) item, i > 0 ); diff --git a/eeschema/tools/sch_move_tool.cpp b/eeschema/tools/sch_move_tool.cpp index bb648c4202..8084aa32ed 100644 --- a/eeschema/tools/sch_move_tool.cpp +++ b/eeschema/tools/sch_move_tool.cpp @@ -22,6 +22,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ +#include #include #include #include @@ -183,6 +184,7 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) m_dragAdditions.clear(); m_specialCaseLabels.clear(); internalPoints.clear(); + clearNewDragLines(); for( SCH_ITEM* it : m_frame->GetScreen()->Items() ) { @@ -190,9 +192,6 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) if( !it->IsSelected() ) it->ClearFlags( STARTPOINT | ENDPOINT ); - - if( !selection.IsHover() && it->IsSelected() ) - it->SetFlags( STARTPOINT | ENDPOINT ); } if( m_isDrag ) @@ -220,6 +219,23 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) m_dragAdditions.push_back( item->m_Uuid ); m_selectionTool->AddItemToSel( item, QUIET_MODE ); } + + // Pre-cache all connections of our selected objects so we can keep track of what + // they were originally connected to as we drag them around + for( EDA_ITEM* edaItem : selection ) + { + SCH_ITEM* schItem = static_cast( edaItem ); + + if( schItem->Type() == SCH_LINE_T ) + { + SCH_LINE* line = static_cast( schItem ); + //Also store the original angle of the line, is needed later to decide + //which segment to extend when they've become zero length + line->StoreAngle(); + for( VECTOR2I point : line->GetConnectionPoints() ) + getConnectedItems( line, point, m_lineConnectionCache[line] ); + } + } } else { @@ -360,14 +376,300 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) m_moveOffset += delta; prevPos = m_cursor; - for( EDA_ITEM* item : selection ) + // Used for tracking how far off a drag end should have its 90 degree elbow added + int xBendCount = 1; + int yBendCount = 1; + + // Split the move into X and Y moves so we can correctly drag orthogonal lines + for( VECTOR2I splitDelta : { VECTOR2I( delta.x, 0 ), VECTOR2I( 0, delta.y ) } ) { - // Don't double move pins, fields, etc. - if( item->GetParent() && item->GetParent()->IsSelected() ) + // Skip non-moves + if( splitDelta == VECTOR2I( 0, 0 ) ) continue; - moveItem( item, delta ); - updateItem( item, false ); + for( EDA_ITEM* item : selection.GetItemsSortedByTypeAndXY() ) + { + // Don't double move pins, fields, etc. + if( item->GetParent() && item->GetParent()->IsSelected() ) + continue; + + SCH_LINE* line = + item->Type() == SCH_LINE_T ? static_cast( item ) : nullptr; + + //Only partially selected drag lines in orthogonal line mode need special handling + if( m_isDrag && cfg->m_Drawing.hv_lines_only && line + && ( line->HasFlag( STARTPOINT ) != line->HasFlag( ENDPOINT ) ) ) + { + // If the move is not the same angle as this move, then we need to do something + // special with the unselected end to maintain orthogonality. Either drag some + // connected line that is the same angle as the move or add two lines to make + // a 90 degree connection + if( !( EDA_ANGLE( splitDelta ).IsParallelTo( line->Angle() ) ) + || ( line->GetLength() == 0 ) ) + { + VECTOR2I unselectedEnd = line->HasFlag( STARTPOINT ) + ? line->GetEndPoint() + : line->GetStartPoint(); + VECTOR2I selectedEnd = line->HasFlag( STARTPOINT ) + ? line->GetStartPoint() + : line->GetEndPoint(); + + // Look for pre-existing lines we can drag with us instead of creating new ones + bool foundAttachment = false; + bool foundJunction = false; + SCH_LINE* foundLine = nullptr; + for( EDA_ITEM* cItem : m_lineConnectionCache[line] ) + { + foundAttachment = true; + + // If the move is the same angle as a connected line, + // we can shrink/extend that line endpoint + if( cItem->Type() == SCH_LINE_T ) + { + SCH_LINE* cLine = static_cast( cItem ); + + // A matching angle on a non-zero-length line means lengthen/shorten will work + if( ( EDA_ANGLE( splitDelta ).IsParallelTo( cLine->Angle() ) ) + && cLine->GetLength() != 0 ) + foundLine = cLine; + + // Zero length lines are lines that this algorithm has shortened to 0 so they + // also work but we should prefer using a segment with length and angle matching + // when we can (otherwise the zero length line will draw overlapping segments on them) + if( foundLine == nullptr && cLine->GetLength() == 0 ) + foundLine = cLine; + } + else if( cItem->Type() == SCH_JUNCTION_T ) + foundJunction = true; + } + + // Ok... what if our original line is length zero from moving in its direction, + // and the last added segment of the 90 bend we are connected to is zero from moving + // it its direction after it was added? + // + // If we are moving in original direction, we should lengthen the original + // drag wire. Otherwise we should lengthen the new wire. + bool preferOriginalLine = false; + + if( foundLine && ( foundLine->GetLength() == 0 ) + && ( line->GetLength() == 0 ) + && ( EDA_ANGLE( splitDelta ) + .IsParallelTo( line->GetStoredAngle() ) ) ) + { + preferOriginalLine = true; + } + // If we have found an attachment, but not a line, we want to check if it's + // a junction. These are special-cased and get a single line added instead of a + // 90-degree bend. + else if( !foundLine && foundJunction ) + { + // Create a new wire ending at the unselected end + foundLine = new SCH_LINE( unselectedEnd, line->GetLayer() ); + foundLine->SetFlags( IS_NEW ); + m_frame->AddToScreen( foundLine, m_frame->GetScreen() ); + m_newDragLines.insert( foundLine ); + + // We just broke off of the existing items, so replace all of them with our new + // end connection. + m_lineConnectionCache[foundLine] = m_lineConnectionCache[line]; + m_lineConnectionCache[line].clear(); + m_lineConnectionCache[line].emplace_back( foundLine ); + } + + // We want to drag our found line if it's in the same angle as the move or zero length, + // but if the original drag line is also zero and the same original angle we should extend + // that one first + if( foundLine && !preferOriginalLine ) + { + // Move the connected line found oriented in the direction of our move. + // + // Make sure we grab the right endpoint, it's not always STARTPOINT since + // the user can draw a box of lines. We need to only move one though, + // and preferably the start point, in case we have a zero length line + // that we are extending (we want the foundLine start point to be attached + // to the unselected end of our drag line). + // + // Also, new lines are added already so they'll be in the undo list, skip + // adding them. + if( !foundLine->HasFlag( IS_CHANGED ) + && !foundLine->HasFlag( IS_NEW ) ) + saveCopyInUndoList( (SCH_ITEM*) foundLine, UNDO_REDO::CHANGED, + true ); + + if( foundLine->GetStartPoint() == unselectedEnd ) + foundLine->MoveStart( splitDelta ); + else if( foundLine->GetEndPoint() == unselectedEnd ) + foundLine->MoveEnd( splitDelta ); + + updateItem( foundLine, true ); + + + SCH_LINE* bendLine = nullptr; + + if( ( m_lineConnectionCache.count( foundLine ) == 1 ) + && ( m_lineConnectionCache[foundLine][0]->Type() + == SCH_LINE_T ) ) + { + bendLine = static_cast( + m_lineConnectionCache[foundLine][0] ); + } + + // Remerge segments we've created if this is a segment that we've added + // whose only other connection is also an added segment + // + // bendLine is first added segment at the original attachment point, + // foundLine is the orthogonal line between bendLine and this line + if( foundLine->HasFlag( IS_NEW ) && ( foundLine->GetLength() == 0 ) + && ( bendLine != nullptr ) && bendLine->HasFlag( IS_NEW ) ) + { + if( line->HasFlag( STARTPOINT ) ) + line->SetEndPoint( bendLine->GetEndPoint() ); + else + line->SetStartPoint( bendLine->GetEndPoint() ); + + // Update our cache of the connected items. + + // First, re-attach our drag labels to the original line being re-merged. + for( auto possibleLabel : m_lineConnectionCache[bendLine] ) + { + switch( possibleLabel->Type() ) + { + case SCH_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: + { + SCH_LABEL* label = + static_cast( possibleLabel ); + if( m_specialCaseLabels.count( label ) ) + m_specialCaseLabels[label].attachedLine = line; + break; + } + default: break; + } + } + + m_lineConnectionCache[line] = m_lineConnectionCache[bendLine]; + m_lineConnectionCache[bendLine].clear(); + m_lineConnectionCache[foundLine].clear(); + + + m_frame->RemoveFromScreen( bendLine, m_frame->GetScreen() ); + m_frame->RemoveFromScreen( foundLine, m_frame->GetScreen() ); + + m_newDragLines.erase( bendLine ); + m_newDragLines.erase( foundLine ); + + delete bendLine; + delete foundLine; + } + //Ok, move the unselected end of our item + else + { + if( line->HasFlag( STARTPOINT ) ) + line->MoveEnd( splitDelta ); + else + line->MoveStart( splitDelta ); + } + + updateItem( line, true ); + } + else if( line->GetLength() == 0 ) + { + // We didn't find another line to shorten/lengthen, (or we did but it's also zero) + // so now is a good time to use our existing zero-length original line + } + // Either no line was at the "right" angle, or this was a junction, pin, sheet, etc. + // We need to add segments to keep the soon-to-move unselected end connected to these + // items. + // + // To keep our drag selections all the same, we'll move our unselected end point and + // then put wires between it and its original endpoint. + else if( foundAttachment && line->IsOrthogonal() ) + { + // The bend counter handles a group of wires all needing their offset one + // grid movement further out from each other to not overlap. + // The absolute value stuff finds the direction of the line and hence + // the the bend increment on that axis + unsigned int xMoveBit = splitDelta.x != 0; + unsigned int yMoveBit = splitDelta.y != 0; + int xLength = abs( unselectedEnd.x - selectedEnd.x ); + int yLength = abs( unselectedEnd.y - selectedEnd.y ); + int xMove = ( xLength - ( xBendCount * grid.GetGrid().x ) ) + * sign( selectedEnd.x - unselectedEnd.x ); + int yMove = ( yLength - ( yBendCount * grid.GetGrid().y ) ) + * sign( selectedEnd.y - unselectedEnd.y ); + + // Create a new wire ending at the unselected end, we'll + // move the new wire's start point to the unselected end + SCH_LINE* a = new SCH_LINE( unselectedEnd, line->GetLayer() ); + a->MoveStart( VECTOR2I( xMove, yMove ) ); + a->SetFlags( IS_NEW ); + m_frame->AddToScreen( a, m_frame->GetScreen() ); + m_newDragLines.insert( a ); + + SCH_LINE* b = new SCH_LINE( a->GetStartPoint(), line->GetLayer() ); + b->MoveStart( VECTOR2I( splitDelta.x, splitDelta.y ) ); + b->SetFlags( IS_NEW | STARTPOINT ); + m_frame->AddToScreen( b, m_frame->GetScreen() ); + m_newDragLines.insert( b ); + + xBendCount += yMoveBit; + yBendCount += xMoveBit; + + // Ok move the unselected end of our item + if( line->HasFlag( STARTPOINT ) ) + line->MoveEnd( + VECTOR2I( splitDelta.x ? splitDelta.x : xMove, + splitDelta.y ? splitDelta.y : yMove ) ); + else + line->MoveStart( + VECTOR2I( splitDelta.x ? splitDelta.x : xMove, + splitDelta.y ? splitDelta.y : yMove ) ); + + updateItem( line, true ); + + // Update our cache of the connected items. + // First, attach our drag labels to the line left behind. + for( auto possibleLabel : m_lineConnectionCache[line] ) + { + switch( possibleLabel->Type() ) + { + case SCH_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: + { + SCH_LABEL* label = static_cast( possibleLabel ); + if( m_specialCaseLabels.count( label ) ) + m_specialCaseLabels[label].attachedLine = a; + break; + } + default: break; + } + } + + // We just broke off of the existing items, so replace all of them with our new + // end connection. + m_lineConnectionCache[a] = m_lineConnectionCache[line]; + m_lineConnectionCache[b].emplace_back( a ); + m_lineConnectionCache[line].clear(); + m_lineConnectionCache[line].emplace_back( b ); + } + // Original line has no attachments, just move the unselected end + else if( !foundAttachment ) + { + if( line->HasFlag( STARTPOINT ) ) + line->MoveEnd( splitDelta ); + else + line->MoveStart( splitDelta ); + } + } + } + + // Move all other items normally, including the selected end of + // partially selected lines + moveItem( item, splitDelta ); + updateItem( item, false ); + } } if( selection.HasReferencePoint() ) @@ -375,6 +677,7 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) m_toolMgr->PostEvent( EVENTS::SelectedItemsMoved ); } + //------------------------------------------------------------------------ // Handle cancel // @@ -400,6 +703,8 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) restore_state = true; } + clearNewDragLines(); + break; } //------------------------------------------------------------------------ @@ -471,6 +776,9 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) } while( ( evt = Wait() ) ); //Should be assignment not equality test + // Save whatever new bend lines survived the drag + commitNewDragLines(); + controls->ForceCursorPosition( false ); controls->ShowCursor( false ); controls->SetAutoPan( false ); @@ -519,20 +827,131 @@ int SCH_MOVE_TOOL::Main( const TOOL_EVENT& aEvent ) m_selectionTool->RebuildSelection(); // Schematic cleanup might have merged lines, etc. m_dragAdditions.clear(); + m_lineConnectionCache.clear(); m_moveInProgress = false; m_frame->PopTool( tool ); return 0; } +void SCH_MOVE_TOOL::getConnectedItems( SCH_ITEM* aOriginalItem, const VECTOR2I& aPoint, + EDA_ITEMS& aList ) +{ + EE_RTREE& items = m_frame->GetScreen()->Items(); + EE_RTREE::EE_TYPE itemsOverlapping = items.Overlapping( aOriginalItem->GetBoundingBox() ); + + // If you're connected to a junction, you're only connected to the junction + // (unless you are the junction) + for( SCH_ITEM* item : itemsOverlapping ) + { + if( item != aOriginalItem && item->Type() == SCH_JUNCTION_T && item->IsConnected( aPoint ) ) + { + aList.push_back( item ); + return; + } + } + + for( SCH_ITEM* test : itemsOverlapping ) + { + if( test == aOriginalItem || !test->CanConnect( aOriginalItem ) ) + continue; + + switch( test->Type() ) + { + case SCH_LINE_T: + { + SCH_LINE* line = static_cast( test ); + + //When getting lines for the connection cache, it's important that + //we only add items at the unselected end, since that is the only + //end that is handled specially. Fully selected lines, and the selected + //end of a partially selected line, are moved around normally and + //don't care about their connections. + if( ( line->HasFlag( STARTPOINT ) && aPoint == line->GetStartPoint() ) + || ( line->HasFlag( ENDPOINT ) && aPoint == line->GetEndPoint() ) ) + continue; + + if( test->IsConnected( aPoint ) ) + aList.push_back( test ); + + // Labels can connect to a wire (or bus) anywhere along the length + switch( aOriginalItem->Type() ) + { + case SCH_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: + if( static_cast( test )->HitTest( + static_cast( aOriginalItem )->GetTextPos(), 1 ) ) + aList.push_back( test ); + break; + default: break; + } + + break; + } + case SCH_SHEET_T: + case SCH_SYMBOL_T: + case SCH_JUNCTION_T: + case SCH_NO_CONNECT_T: + if( test->IsConnected( aPoint ) ) + aList.push_back( test ); + + break; + + case SCH_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: + // Labels can connect to a wire (or bus) anywhere along the length + if( aOriginalItem->Type() == SCH_LINE_T && test->CanConnect( aOriginalItem ) ) + { + SCH_TEXT* label = static_cast( test ); + SCH_LINE* line = static_cast( aOriginalItem ); + + if( line->HitTest( label->GetTextPos(), 1 ) ) + aList.push_back( label ); + } + break; + case SCH_BUS_WIRE_ENTRY_T: + case SCH_BUS_BUS_ENTRY_T: + if( aOriginalItem->Type() == SCH_LINE_T && test->CanConnect( aOriginalItem ) ) + { + SCH_TEXT* label = static_cast( test ); + SCH_LINE* line = static_cast( aOriginalItem ); + + if( line->HitTest( aPoint, 1 ) ) + aList.push_back( label ); + } + break; + + default: break; + } + } +} + + void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR2I& aPoint, EDA_ITEMS& aList ) { EE_RTREE& items = m_frame->GetScreen()->Items(); - EE_RTREE::EE_TYPE itemsOverlapping = items.Overlapping( aOriginalItem->GetBoundingBox() ); + EE_RTREE::EE_TYPE itemsOverlappingRTree = items.Overlapping( aOriginalItem->GetBoundingBox() ); + std::vector itemsConnectable; bool ptHasUnselectedJunction = false; + SCH_LINE* newWire = nullptr; - for( SCH_ITEM* item : itemsOverlapping ) + for( SCH_ITEM* item : itemsOverlappingRTree ) + { + // Skip ourselves, skip already selected items (but not lines, they need both ends tested) + // and skip unconnectable items + if( item == aOriginalItem || ( item->Type() != SCH_LINE_T && item->IsSelected() ) + || !item->CanConnect( aOriginalItem ) ) + { + continue; + } + + itemsConnectable.push_back( item ); + } + + for( SCH_ITEM* item : itemsConnectable ) { if( item->Type() == SCH_JUNCTION_T && item->IsConnected( aPoint ) && !item->IsSelected() ) { @@ -541,11 +960,8 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR } } - for( SCH_ITEM* test : itemsOverlapping ) + for( SCH_ITEM* test : itemsConnectable ) { - if( test == aOriginalItem || test->IsSelected() || !test->CanConnect( aOriginalItem ) ) - continue; - KICAD_T testType = test->Type(); switch( testType ) @@ -561,29 +977,76 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR if( line->GetStartPoint() == aPoint ) { - if( !line->HasFlag(TEMP_SELECTED ) ) - aList.push_back( line ); + // It's possible to manually select one end of a line and get a drag + // connected other end, so we set the flag and then early exit the loop + // later if the other drag items like labels attached to the line have + // already been grabbed during the partial selection process. + line->SetFlags( STARTPOINT ); - line->SetFlags(STARTPOINT | TEMP_SELECTED ); + if( line->HasFlag( SELECTED ) || line->HasFlag( TEMP_SELECTED ) ) + continue; + else + { + line->SetFlags( TEMP_SELECTED ); + aList.push_back( line ); + } } else if( line->GetEndPoint() == aPoint ) { - if( !line->HasFlag(TEMP_SELECTED ) ) - aList.push_back( line ); + line->SetFlags( ENDPOINT ); - line->SetFlags(ENDPOINT | TEMP_SELECTED ); + if( line->HasFlag( SELECTED ) || line->HasFlag( TEMP_SELECTED ) ) + continue; + else + { + line->SetFlags( TEMP_SELECTED ); + aList.push_back( line ); + } } else { + switch( aOriginalItem->Type() ) + { + // These items can connect anywhere along a line + case SCH_BUS_BUS_ENTRY_T: + case SCH_BUS_WIRE_ENTRY_T: + case SCH_LABEL_T: + case SCH_HIER_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_DIRECTIVE_LABEL_T: + if( line->HitTest( aPoint, 1 ) ) + { + // Add a new line so we have something to drag + newWire = new SCH_LINE( aPoint, line->GetLayer() ); + newWire->SetFlags( IS_NEW ); + m_frame->AddToScreen( newWire, m_frame->GetScreen() ); + + newWire->SetFlags( TEMP_SELECTED | STARTPOINT ); + aList.push_back( newWire ); + + //We need to add a connection reference here because the normal + //algorithm won't find a new line with a point in the middle of an + //existing line + m_lineConnectionCache[newWire] = { line }; + } + break; + default: break; + } + break; } // Since only one end is going to move, the movement vector of any labels attached // to it is scaled by the proportion of the line length the label is from the moving // end. - for( SCH_ITEM* item : itemsOverlapping ) + for( SCH_ITEM* item : items.Overlapping( line->GetBoundingBox() ) ) { - if( item->Type() == SCH_LABEL_T || item->Type() == SCH_DIRECTIVE_LABEL_T ) + switch( item->Type() ) + { + case SCH_LABEL_T: + case SCH_HIER_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_DIRECTIVE_LABEL_T: { SCH_LABEL_BASE* label = static_cast( item ); @@ -603,6 +1066,10 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR info.originalLabelPos = label->GetPosition(); m_specialCaseLabels[label] = info; } + + break; + } + default: break; } } @@ -612,12 +1079,10 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR case SCH_SHEET_T: case SCH_SYMBOL_T: case SCH_JUNCTION_T: - if( test->IsConnected( aPoint ) ) + if( test->IsConnected( aPoint ) && !newWire ) { // Add a new wire between the symbol or junction and the selected item so // the selected item can be dragged. - SCH_LINE* newWire = nullptr; - if( test->GetLayer() == LAYER_BUS_JUNCTION || aOriginalItem->GetLayer() == LAYER_BUS ) { @@ -662,15 +1127,27 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR if( line->HitTest( label->GetTextPos(), 1 ) ) { - label->SetFlags( TEMP_SELECTED ); - aList.push_back( label ); - - if( oneEndFixed ) + if( ( !line->HasFlag( STARTPOINT ) + && label->GetPosition() == line->GetStartPoint() ) + || ( !line->HasFlag( ENDPOINT ) + && label->GetPosition() == line->GetEndPoint() ) ) { - SPECIAL_CASE_LABEL_INFO info; - info.attachedLine = line; - info.originalLabelPos = label->GetPosition(); - m_specialCaseLabels[ label ] = info; + //If we have a line selected at only one end, don't grab labels + //connected directly to the unselected endpoint + break; + } + else + { + label->SetFlags( TEMP_SELECTED ); + aList.push_back( label ); + + if( oneEndFixed ) + { + SPECIAL_CASE_LABEL_INFO info; + info.attachedLine = line; + info.originalLabelPos = label->GetPosition(); + m_specialCaseLabels[label] = info; + } } } } @@ -687,11 +1164,12 @@ void SCH_MOVE_TOOL::getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR if( aOriginalItem->Type() == SCH_LINE_T && test->CanConnect( aOriginalItem ) ) { SCH_LINE* line = static_cast( aOriginalItem ); - bool oneEndFixed = !line->HasFlag( STARTPOINT ) || !line->HasFlag( ENDPOINT ); - if( oneEndFixed ) + if( ( !line->HasFlag( STARTPOINT ) && test->IsConnected( line->GetStartPoint() ) ) + || ( !line->HasFlag( ENDPOINT ) && test->IsConnected( line->GetEndPoint() ) ) ) { - // This is only going to end in tears, so don't go there + //If we have a line selected at only one end, don't grab bus entries + //connected directly to the unselected endpoint continue; } @@ -776,6 +1254,8 @@ void SCH_MOVE_TOOL::moveItem( EDA_ITEM* aItem, const VECTOR2I& aDelta ) } case SCH_LABEL_T: case SCH_DIRECTIVE_LABEL_T: + case SCH_GLOBAL_LABEL_T: + case SCH_HIER_LABEL_T: { SCH_LABEL_BASE* label = static_cast( aItem ); @@ -937,3 +1417,25 @@ void SCH_MOVE_TOOL::setTransitions() Go( &SCH_MOVE_TOOL::Main, EE_ACTIONS::drag.MakeEvent() ); Go( &SCH_MOVE_TOOL::AlignElements, EE_ACTIONS::alignToGrid.MakeEvent() ); } + + +void SCH_MOVE_TOOL::commitNewDragLines() +{ + for( auto newLine : m_newDragLines ) + saveCopyInUndoList( newLine, UNDO_REDO::NEWITEM, true ); + + m_newDragLines.clear(); +} + + +void SCH_MOVE_TOOL::clearNewDragLines() +{ + // Remove new bend lines added during the drag + for( auto newLine : m_newDragLines ) + { + m_frame->RemoveFromScreen( newLine, m_frame->GetScreen() ); + delete newLine; + } + + m_newDragLines.clear(); +} diff --git a/eeschema/tools/sch_move_tool.h b/eeschema/tools/sch_move_tool.h index 908873bfda..d215745a04 100644 --- a/eeschema/tools/sch_move_tool.h +++ b/eeschema/tools/sch_move_tool.h @@ -68,11 +68,18 @@ private: ///< Find additional items for a drag operation. ///< Connected items with no wire are included (as there is no wire to adjust for the drag). ///< Connected wires are included with any un-connected ends flagged (STARTPOINT or ENDPOINT). + void getConnectedItems( SCH_ITEM* aOriginalItem, const VECTOR2I& aPoint, EDA_ITEMS& aList ); void getConnectedDragItems( SCH_ITEM* aOriginalItem, const VECTOR2I& aPoint, EDA_ITEMS& aList ); ///< Set up handlers for various events. void setTransitions() override; + ///< Saves the new drag lines to the undo list + void commitNewDragLines(); + + ///< Clears the new drag lines and removes them from the screen + void clearNewDragLines(); + private: ///< Flag determining if anything is being dragged right now bool m_moveInProgress; @@ -80,6 +87,10 @@ private: ///< Items (such as wires) which were added to the selection for a drag std::vector m_dragAdditions; + ///< Cache of the line's original connections before dragging started + std::map m_lineConnectionCache; + ///< Lines added at bend points dynamically during the move + std::unordered_set m_newDragLines; ///< Used for chaining commands VECTOR2I m_moveOffset; diff --git a/include/eda_item.h b/include/eda_item.h index e1743b1921..07d1d9e708 100644 --- a/include/eda_item.h +++ b/include/eda_item.h @@ -157,15 +157,14 @@ public: EDA_ITEM_FLAGS GetEditFlags() const { constexpr int mask = ( IS_NEW | IS_PASTED | IS_MOVING | IS_RESIZING | IS_DRAGGING - | IS_WIRE_IMAGE | STRUCT_DELETED ); + | IS_CHANGED | IS_WIRE_IMAGE | STRUCT_DELETED ); return m_flags & mask; } void ClearTempFlags() { - ClearFlags( STARTPOINT | ENDPOINT | CANDIDATE | TEMP_SELECTED | IS_LINKED | SKIP_STRUCT | - DO_NOT_DRAW ); + ClearFlags( CANDIDATE | TEMP_SELECTED | IS_LINKED | SKIP_STRUCT | DO_NOT_DRAW ); } void ClearEditFlags() diff --git a/include/tool/selection.h b/include/tool/selection.h index c9516812f6..f380cbeb75 100644 --- a/include/tool/selection.h +++ b/include/tool/selection.h @@ -31,6 +31,7 @@ #include #include #include +#include #include class EDA_ITEM; @@ -111,6 +112,37 @@ public: return m_items; } + /** + * Returns a copy of this selection of items sorted by their X then Y position. + * + * @return Vector of sorted items + */ + const std::vector GetItemsSortedByTypeAndXY() const + { + std::vector sorted_items = + std::vector( m_items.begin(), m_items.end() ); + + std::sort( sorted_items.begin(), sorted_items.end(), [&]( EDA_ITEM* a, EDA_ITEM* b ) { + if( a->Type() == b->Type() ) + { + if( a->GetPosition().x == b->GetPosition().x ) + { + // Ensure deterministic sort + if( a->GetPosition().y == b->GetPosition().y ) + return a->m_Uuid < b->m_Uuid; + + return a->GetPosition().y < b->GetPosition().y; + } + else + return a->GetPosition().x < b->GetPosition().x; + } + else + return a->Type() < b->Type(); + } ); + + return sorted_items; + } + /// Returns the center point of the selection area bounding box. virtual VECTOR2I GetCenter() const; diff --git a/libs/kimath/include/geometry/eda_angle.h b/libs/kimath/include/geometry/eda_angle.h index a437acade6..1bca8e91bf 100644 --- a/libs/kimath/include/geometry/eda_angle.h +++ b/libs/kimath/include/geometry/eda_angle.h @@ -192,6 +192,23 @@ public: return m_value == 90.0 || m_value == 270.0; } + bool IsParallelTo( EDA_ANGLE aAngle ) const + { + EDA_ANGLE thisNormalized = *this; + + // Normalize90 is inclusive on both ends [-90, +90] + // but we need it to be (-90, +90] for this test to work + thisNormalized.Normalize90(); + if( thisNormalized.AsDegrees() == -90.0 ) + thisNormalized = EDA_ANGLE( 90.0, DEGREES_T ); + + aAngle.Normalize90(); + if( aAngle.AsDegrees() == -90.0 ) + aAngle = EDA_ANGLE( 90.0, DEGREES_T ); + + return ( thisNormalized.AsDegrees() == aAngle.AsDegrees() ); + } + EDA_ANGLE Invert() const { return EDA_ANGLE( -AsDegrees(), DEGREES_T );