← Back to team overview

kicad-developers team mailing list archive

[PATCH] Enhanced arc tool in pcbnew

 

Hi,

This is something I've bene toying with for a little while: an
enhanced overlay and construction method for arcs in Pcbnew.

The flashiest part is the new overlay, which is similar to the ruler
tool in concept and feel and uses many of the same utility functions.

New features for the tool are ability to "go back" with backspace and
snap angles with the Ctrl key.

It introduces a couple of new classes, which, as members of
common/preview_items, can theoretically be used from other GAL
canvases for implementing similar tools.

I tried to genericise the event loop code, but failed to make it much
clearer, so I put it back: it's still a custom event loop in
DRAWING_TOOL::drawArc.

Cheers,

John
From cfdbb19b17c8b151ea3ecb06c459312c4834f8a2 Mon Sep 17 00:00:00 2001
From: John Beard <john.j.beard@xxxxxxxxx>
Date: Mon, 27 Feb 2017 04:08:45 +0800
Subject: [PATCH] Enhance Pcbnew arc construction

This adds a richer overlay to the arc-by-three-points tool in Pcbnew,
including more guide-lines and a live display of radius and angle.

Also included is ability to go back to the previous step (if setting end
angle, you can go back to setting start point, etc) using Backspace, and
Ctrl snaps the start/end angles to 45 degree multiples.

This adds new classes

* MULTISTEP_GEOM_MANAGER: represents a generic "geometry manager" that
  builds up some geometrical construction based on a sequence of points.
  Used by:
* ARC_GEOM_MANAGER: handles the logical flow of constructing an
  arc by centre-point, set radius, set angle. This moves the logic out
  of the Pcbnew DRAWING_TOOL event loop in drawArc().
* ARC_ASSISTANT: graphical overlay to communicate current arc shape to
  the user during the drawing process
---
 common/CMakeLists.txt                          |   2 +
 common/preview_items/arc_assistant.cpp         | 198 +++++++++++++++++++++++++
 common/preview_items/arc_geom_manager.cpp      | 164 ++++++++++++++++++++
 include/preview_items/arc_assistant.h          |  74 +++++++++
 include/preview_items/arc_geom_manager.h       | 144 ++++++++++++++++++
 include/preview_items/multistep_geom_manager.h | 195 ++++++++++++++++++++++++
 pcbnew/tools/drawing_tool.cpp                  | 187 +++++++++++------------
 7 files changed, 864 insertions(+), 100 deletions(-)
 create mode 100644 common/preview_items/arc_assistant.cpp
 create mode 100644 common/preview_items/arc_geom_manager.cpp
 create mode 100644 include/preview_items/arc_assistant.h
 create mode 100644 include/preview_items/arc_geom_manager.h
 create mode 100644 include/preview_items/multistep_geom_manager.h

diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 3d095027c..fed29fba0 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -185,6 +185,8 @@ set( COMMON_PAGE_LAYOUT_SRCS
     )
 
 set( COMMON_PREVIEW_ITEMS_SRCS
+    preview_items/arc_assistant.cpp
+    preview_items/arc_geom_manager.cpp
     preview_items/preview_utils.cpp
     preview_items/ruler_item.cpp
     preview_items/simple_overlay_item.cpp
diff --git a/common/preview_items/arc_assistant.cpp b/common/preview_items/arc_assistant.cpp
new file mode 100644
index 000000000..1e3504b7d
--- /dev/null
+++ b/common/preview_items/arc_assistant.cpp
@@ -0,0 +1,198 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2017 Kicad Developers, see AUTHORS.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
+ */
+
+#include <preview_items/arc_assistant.h>
+
+#include <preview_items/preview_utils.h>
+#include <gal/graphics_abstraction_layer.h>
+#include <view/view.h>
+
+#include <common.h>
+#include <base_units.h>
+
+using namespace KIGFX::PREVIEW;
+
+ARC_ASSISTANT::ARC_ASSISTANT( const ARC_GEOM_MANAGER& aManager ) :
+    EDA_ITEM( NOT_USED ),
+    m_constructMan( aManager )
+{
+}
+
+
+const BOX2I ARC_ASSISTANT::ViewBBox() const
+{
+    BOX2I tmp;
+
+    // no bounding box when no graphic shown
+    if( m_constructMan.IsReset() )
+        return tmp;
+
+    // just enclose the whle circular area
+    auto    origin  = m_constructMan.GetOrigin();
+    auto    radius  = m_constructMan.GetRadius();
+    VECTOR2D rVec( radius, radius );
+
+    tmp.SetOrigin( origin + rVec );
+    tmp.SetEnd( origin - rVec );
+    tmp.Normalize();
+    return tmp;
+}
+
+
+/**
+ * Get deci-degrees from radians, normalised to +/- 360.
+ *
+ * The normalisation is such that a negative angle will stay
+ * negative.
+ */
+double getNormDeciDegFromRad( double aRadians )
+{
+    double degs = RAD2DECIDEG( aRadians );
+
+    // normalise to +/- 360
+    while( degs < -3600.0 )
+        degs += 3600.0;
+
+    while( degs > 3600.0 )
+        degs -= 3600.0;
+
+    return degs;
+}
+
+
+static const double ANGLE_EPSILON = 1e-9;
+
+double angleIsSpecial( double aRadians )
+{
+    return std::fabs( std::remainder( aRadians, M_PI_4 ) ) < ANGLE_EPSILON;
+}
+
+
+static void drawLineWithHilight( KIGFX::GAL& aGal,
+        const VECTOR2I& aStart, const VECTOR2I& aEnd, bool aDim )
+{
+    const auto vec = aEnd - aStart;
+    COLOR4D strokeColor = PreviewOverlayDefaultColor();
+
+    if( angleIsSpecial( vec.Angle() ) )
+        strokeColor = PreviewOverlaySpecialAngleColor();
+
+    aGal.SetStrokeColor( strokeColor.WithAlpha( PreviewOverlayDeemphAlpha( aDim ) ) );
+    aGal.DrawLine( aStart, aEnd );
+}
+
+
+static void drawArcWithHilight( KIGFX::GAL& aGal,
+        const VECTOR2I& aOrigin, double aRad, double aStartAngle,
+        double aEndAngle )
+{
+    COLOR4D color = PreviewOverlayDefaultColor();
+
+    if( angleIsSpecial( aStartAngle - aEndAngle ) )
+        color = PreviewOverlaySpecialAngleColor();
+
+    aGal.SetStrokeColor( color );
+    aGal.SetFillColor( color.WithAlpha( 0.2 ) );
+
+    // draw the angle reference arc
+    aGal.DrawArc( aOrigin, aRad, -aStartAngle, -aEndAngle );
+}
+
+
+void ARC_ASSISTANT::ViewDraw( int aLayer, KIGFX::VIEW* aView ) const
+{
+    auto& gal = *aView->GetGAL();
+
+    // not in a position to draw anything
+    if( m_constructMan.IsReset() )
+        return;
+
+    gal.SetLineWidth( 1.0 );
+    gal.SetIsStroke( true );
+    gal.SetIsFill( true );
+
+    // constant text size on screen
+    SetConstantGlyphHeight( gal, 12.0 );
+
+    // angle reference arc size
+    const double innerRad = 12.0 / gal.GetWorldScale();
+
+    const auto origin = m_constructMan.GetOrigin();
+
+    // draw first radius line
+    bool dimFirstLine = m_constructMan.GetStep() > ARC_GEOM_MANAGER::SET_START;
+
+    drawLineWithHilight( gal, origin, m_constructMan.GetStartRadiusEnd(), dimFirstLine );
+
+    std::vector<wxString> cursorStrings;
+
+    if( m_constructMan.GetStep() == ARC_GEOM_MANAGER::SET_START )
+    {
+        // haven't started the angle selection phase yet
+
+        auto initAngle = m_constructMan.GetStartAngle();
+
+        const auto angleRefLineEnd = m_constructMan.GetOrigin() + VECTOR2D( innerRad * 1.5, 0.0 );
+
+        gal.SetStrokeColor( PreviewOverlayDefaultColor() );
+        gal.DrawLine( origin, angleRefLineEnd );
+
+        // draw the angle reference arc
+        drawArcWithHilight( gal, origin, innerRad, initAngle, 0.0 );
+
+        double degs = getNormDeciDegFromRad( initAngle );
+
+        cursorStrings.push_back( DimensionLabel( "r", m_constructMan.GetRadius(), g_UserUnit ) );
+        cursorStrings.push_back( DimensionLabel( "θ", degs, DEGREES ) );
+    }
+    else
+    {
+        drawLineWithHilight( gal, origin, m_constructMan.GetEndRadiusEnd(), false );
+
+        auto    start = m_constructMan.GetStartAngle();
+        auto    subtended = m_constructMan.GetSubtended();
+
+        drawArcWithHilight( gal, origin, innerRad, start, start + subtended );
+
+        double  subtendedDeg    = getNormDeciDegFromRad( subtended );
+        double  endAngleDeg     = getNormDeciDegFromRad( start + subtended );
+
+        // draw dimmed extender line to cursor
+        drawLineWithHilight( gal, origin, m_constructMan.GetLastPoint(), true );
+
+        cursorStrings.push_back( DimensionLabel( "Δθ", subtendedDeg, DEGREES ) );
+        cursorStrings.push_back( DimensionLabel( "θ", endAngleDeg, DEGREES ) );
+    }
+
+    // FIXME: spaces choke OpenGL lp:1668455
+    for( auto& str : cursorStrings )
+    {
+        str.erase( std::remove( str.begin(), str.end(), ' ' ),
+                str.end() );
+    }
+
+    // place the text next to cursor, on opposite side from radius
+    DrawTextNextToCursor( gal, m_constructMan.GetLastPoint(),
+            origin - m_constructMan.GetLastPoint(),
+            cursorStrings );
+}
diff --git a/common/preview_items/arc_geom_manager.cpp b/common/preview_items/arc_geom_manager.cpp
new file mode 100644
index 000000000..49888f90f
--- /dev/null
+++ b/common/preview_items/arc_geom_manager.cpp
@@ -0,0 +1,164 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2017 Kicad Developers, see AUTHORS.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
+ */
+
+#include <preview_items/arc_geom_manager.h>
+
+#include <common.h> // KiROUND
+
+using namespace KIGFX::PREVIEW;
+
+
+///> Snap an angle to the nearest 45 degrees
+static double snapAngle( double aAngle )
+{
+    return KiROUND( aAngle / M_PI_4 ) * M_PI_4;
+}
+
+
+bool ARC_GEOM_MANAGER::acceptPoint( const VECTOR2I& aPt )
+{
+    switch( getStep() )
+    {
+    case SET_ORIGIN:
+        return setOrigin( aPt );
+    case SET_START:
+        return setStart( aPt );
+    case SET_ANGLE:
+        return setEnd( aPt );
+    case COMPLETE:
+        break;
+    }
+
+    return false;
+}
+
+
+void ARC_GEOM_MANAGER::SetClockwise( bool aCw )
+{
+    m_clockwise = aCw;
+    setGeometryChanged();
+}
+
+
+void ARC_GEOM_MANAGER::ToggleClockwise()
+{
+    m_clockwise = !m_clockwise;
+    setGeometryChanged();
+}
+
+
+VECTOR2I ARC_GEOM_MANAGER::GetOrigin() const
+{
+    return m_origin;
+}
+
+
+VECTOR2I ARC_GEOM_MANAGER::GetStartRadiusEnd() const
+{
+    return m_origin + VECTOR2I( m_radius, 0 ).Rotate( m_startAngle );
+}
+
+
+VECTOR2I ARC_GEOM_MANAGER::GetEndRadiusEnd() const
+{
+    return m_origin + VECTOR2I( m_radius, 0 ).Rotate( m_endAngle );
+}
+
+
+double ARC_GEOM_MANAGER::GetRadius() const
+{
+    return m_radius;
+}
+
+
+double ARC_GEOM_MANAGER::GetStartAngle() const
+{
+    double angle = m_startAngle;
+
+    if( m_clockwise )
+        angle -= 2 * M_PI;
+
+    return -angle;
+}
+
+
+double ARC_GEOM_MANAGER::GetSubtended() const
+{
+    double angle = m_endAngle - m_startAngle;
+
+    if( m_endAngle <= m_startAngle )
+        angle += 2 * M_PI;
+
+    if( m_clockwise )
+        angle -= 2 * M_PI;
+
+    return -angle;
+}
+
+
+bool ARC_GEOM_MANAGER::setOrigin( const VECTOR2I& aOrigin )
+{
+    m_origin    = aOrigin;
+    m_startAngle = 0.0;
+    m_endAngle = 0.0;
+
+    return true;
+}
+
+
+bool ARC_GEOM_MANAGER::setStart( const VECTOR2I& aEnd )
+{
+    const auto radVec = aEnd - m_origin;
+
+    m_radius = radVec.EuclideanNorm();
+    m_startAngle = radVec.Angle();
+
+    if( m_angleSnap )
+        m_startAngle = snapAngle( m_startAngle );
+
+    // normalise into 0-2Pi
+    while( m_startAngle < 0 )
+        m_startAngle += M_PI * 2;
+
+    m_endAngle = m_startAngle;
+
+    return m_radius != 0.0;
+}
+
+
+bool ARC_GEOM_MANAGER::setEnd( const VECTOR2I& aCursor )
+{
+    const auto radVec = aCursor - m_origin;
+
+    m_endAngle = radVec.Angle();
+
+    if( m_angleSnap )
+        m_endAngle = snapAngle( m_endAngle );
+
+    // normalise into 0-2Pi
+    while( m_endAngle < 0 )
+        m_endAngle += M_PI * 2;
+
+    // if the end is the same as the start, this is a bad point
+    return m_endAngle != m_startAngle;
+}
diff --git a/include/preview_items/arc_assistant.h b/include/preview_items/arc_assistant.h
new file mode 100644
index 000000000..9c3c411ae
--- /dev/null
+++ b/include/preview_items/arc_assistant.h
@@ -0,0 +1,74 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2017 Kicad Developers, see AUTHORS.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
+ */
+
+#ifndef PREVIEW_ITEMS_ARC_ASSISTANT_H
+#define PREVIEW_ITEMS_ARC_ASSISTANT_H
+
+#include <base_struct.h>
+#include <preview_items/arc_geom_manager.h>
+
+namespace KIGFX {
+namespace PREVIEW {
+/**
+ * Class SELECTION_AREA
+ *
+ * Represents an assitant draw when interactively drawing an
+ * arc on a canvas.
+ */
+class ARC_ASSISTANT : public EDA_ITEM
+{
+public:
+
+    ARC_ASSISTANT( const ARC_GEOM_MANAGER& aManager );
+
+    const BOX2I ViewBBox() const override;
+
+    /**
+     * Draw the assistance (with reference to the contstruction manager
+     */
+    void ViewDraw( int aLayer, KIGFX::VIEW* aView ) const override final;
+
+#if defined(DEBUG)
+    void Show( int x, std::ostream& st ) const override
+    {
+    }
+
+#endif
+
+    /**
+     * Get class name
+     * @return  string "ARC_ASSISTANT"
+     */
+    wxString GetClass() const override
+    {
+        return "ARC_ASSISTANT";
+    }
+
+private:
+
+    const ARC_GEOM_MANAGER& m_constructMan;
+};
+}       // PREVIEW
+}       // KIGFX
+
+#endif  // PREVIEW_ITEMS_ARC_ASSISTANT_H
diff --git a/include/preview_items/arc_geom_manager.h b/include/preview_items/arc_geom_manager.h
new file mode 100644
index 000000000..4c39fd1c4
--- /dev/null
+++ b/include/preview_items/arc_geom_manager.h
@@ -0,0 +1,144 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2017 Kicad Developers, see AUTHORS.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
+ */
+
+#ifndef PREVIEW_ITEMS_ARC_GEOMETRY_MANAGER_H
+#define PREVIEW_ITEMS_ARC_GEOMETRY_MANAGER_H
+
+#include <preview_items/multistep_geom_manager.h>
+
+#include <common.h>
+
+namespace KIGFX {
+namespace PREVIEW {
+
+
+/**
+ * Class ARC_GEOM_MANAGER
+ *
+ * A class to manage the construction of a circular arc though
+ * sequential setting of critical points: centre, arc start
+ * and arc end. The manager is driven by setting cursor points, which
+ * update the geometry, and optionally advance the manager state.
+ *
+ * Interfaces are provided to return both arc geometry (can be used
+ * to set up real arcs on PCBs, for example) as well as important
+ * control points for informational overlays.
+ */
+class ARC_GEOM_MANAGER: public MULTISTEP_GEOM_MANAGER
+{
+public:
+    ARC_GEOM_MANAGER()
+    {}
+
+    enum ARC_STEPS
+    {
+        SET_ORIGIN = 0,     ///> Waiting to lock in origin point
+        SET_START,          ///> Waiting to lock in the arc start point
+        SET_ANGLE,          ///> Waiting to lock in the arc end point
+        COMPLETE
+    };
+
+
+    int getMaxStep() const override
+    {
+        return COMPLETE;
+    }
+
+    /**
+     * Get the current step the mananger is on (useful when drawing
+     * something depends on the current state)
+     */
+    ARC_STEPS GetStep() const
+    {
+        return static_cast<ARC_STEPS>( getStep() );
+    }
+
+    bool acceptPoint( const VECTOR2I& aPt ) override;
+
+    ///> The the arc to be clockwise from start
+    void SetClockwise( bool aCw );
+
+    ///> Reverse the current are direction
+    void ToggleClockwise();
+
+    ///> Set angle snapping (for the next point)
+    void SetAngleSnap( bool aSnap )
+    {
+        m_angleSnap = aSnap;
+    }
+
+    /*
+     * Geometry query interface - used by clients of the manager
+     */
+
+    ///> Get the centre point of the arc (valid when state > SET_ORIGIN)
+    VECTOR2I GetOrigin() const;
+
+    ///> Get the coordinates of the arc start
+    VECTOR2I GetStartRadiusEnd() const;
+
+    ///> Get the coordinates of the arc end point
+    VECTOR2I GetEndRadiusEnd() const;
+
+    ///> Get the radius of the arc (valid if step >= SET_START)
+    double GetRadius() const;
+
+    ///> Get the angle of the vector leading to the start point (valid if step >= SET_START)
+    double GetStartAngle() const;
+
+    ///> Get the angle of the vector leading to the end point (valid if step >= SET_ANGLE)
+    double GetSubtended() const;
+
+private:
+
+    /*
+     * Point acceptor functions
+     */
+
+    ///> Set the centre point of the arc
+    bool setOrigin( const VECTOR2I& aOrigin );
+
+    ///> Set the end of the first radius line (arc start)
+    bool setStart( const VECTOR2I& aEnd );
+
+    ///> Set a point of the second radius line (collinear with arc end)
+    bool setEnd( const VECTOR2I& aCursor );
+
+    /*
+     * Arc geometry
+     */
+    bool m_clockwise  = true;
+    VECTOR2I m_origin;
+    double m_radius = 0.0;
+    double m_startAngle = 0.0;
+    double m_endAngle = 0.0;
+
+    /*
+     * construction parameters
+     */
+    bool m_angleSnap;
+};
+}       // PREVIEW
+}       // KIGFX
+
+#endif  // PREVIEW_ITEMS_ARC_GEOMETRY_MANAGER_H
diff --git a/include/preview_items/multistep_geom_manager.h b/include/preview_items/multistep_geom_manager.h
new file mode 100644
index 000000000..83f73f6e1
--- /dev/null
+++ b/include/preview_items/multistep_geom_manager.h
@@ -0,0 +1,195 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2017 Kicad Developers, see AUTHORS.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
+ */
+
+#ifndef PREVIEW_ITEMS_MULTISTEP_GEOMETRY_MANAGER_H
+#define PREVIEW_ITEMS_MULTISTEP_GEOMETRY_MANAGER_H
+
+#include <math/vector2d.h>
+#include <algorithm>
+
+namespace KIGFX {
+namespace PREVIEW {
+
+/**
+ * A geometry manager that works by accepting a sequence of points
+ * and advancing though stages of geometric construction with each
+ * point "locked in".
+ *
+ * For example, constructing an arc might go:
+ *    - Set origin
+ *    - Set start point
+ *    - Set end point
+ *
+ * The exact steps and how the points translate to geometry depends on
+ * the subclass.
+ */
+class MULTISTEP_GEOM_MANAGER
+{
+public:
+    virtual ~MULTISTEP_GEOM_MANAGER()
+    {}
+
+    /**
+     * Add a point to the construction manager
+     *
+     * @param aPt the new point
+     * @param aLockIn whether to "lock in" the point, and move the
+     * geometry manager to the next (or previous) step. False to
+     * update geomtry and not affect manager state
+     */
+    void AddPoint( const VECTOR2I& aPt, bool aLockIn )
+    {
+        // hold onto the raw point separately to the managed geometry
+        m_lastPoint = aPt;
+
+        // update the geometry
+        bool accepted = acceptPoint( aPt );
+
+        // advance or regress the manager
+        if( aLockIn )
+            performStep( accepted );
+
+        setGeometryChanged();
+    }
+
+    /**
+     * Undo the last point, and move the manager back to the previous
+     * step
+     */
+    void RemoveLastPoint()
+    {
+        performStep( false );
+
+        // process the last point again, but in the previous step mode
+        // it doesn't matter if accepted or not, as long as the geometry
+        // is regenerated if needed
+        acceptPoint( GetLastPoint() );
+        setGeometryChanged();
+    }
+
+    /**
+     * @return true if the manager is in the initial state
+     */
+    bool IsReset() const
+    {
+        return m_step == 0;
+    }
+
+    /**
+     * Reset the manager to the initial state
+     */
+    void Reset()
+    {
+        m_step = 0;
+        setGeometryChanged();
+    }
+
+    /**
+     * @return true if the manager reached the final state
+     */
+    bool IsComplete() const
+    {
+        return m_step == getMaxStep();
+    }
+
+    /**
+     * Gets the last point added (locked in or not). This can
+     * be useful when drawing previews, as the point given isn't always
+     * what gets locked into the geometry, if that step doesn't have
+     * full degrees of freedom
+     */
+    VECTOR2I GetLastPoint() const
+    {
+        return m_lastPoint;
+    }
+
+    /**
+     * @return true if the geoemtry has changed, eg such that a client
+     * should redraw
+     */
+    bool HasGeometryChanged() const
+    {
+        return m_changed;
+    }
+
+    /**
+     * Clear the geometry changed flag, call after the client code has
+     * updated everything as needed.
+     */
+    void ClearGeometryChanged()
+    {
+        m_changed = false;
+    }
+
+protected:
+
+    ///> Mark the geometry as changed for clients to notice
+    void setGeometryChanged()
+    {
+        m_changed = true;
+    }
+
+    ///> Get the current stage of the manager
+    int getStep() const
+    {
+        return m_step;
+    }
+
+private:
+
+    ///> Function that accepts a point for a stage, or rejects it
+    ///> to return to the previous stage
+    virtual bool acceptPoint( const VECTOR2I& aPt ) = 0;
+
+    /**
+     * The highest step this manager has - used to recognise completion
+     * and to clamp the step as it advances.
+     */
+    virtual int getMaxStep() const = 0;
+
+    ///> Moves the manager forward or backward through the stages
+    void performStep( bool aForward )
+    {
+        m_step += aForward ? 1 : -1;
+
+        // clamp to allowed values
+        m_step = std::min( std::max( m_step, 0 ), getMaxStep() );
+    }
+
+    ///> Has the gemotry changed such that a client should redraw?
+    bool m_changed = false;
+
+    ///> The last (raw) point added, which is usually the cursor position
+    VECTOR2I m_lastPoint;
+
+    /**
+     * The current manager step, from 0 to some highest number that
+     * depends on the manager
+     */
+    int m_step = 0;
+};
+
+}       // PREVIEW
+}       // KIGFX
+
+#endif  // PREVIEW_ITEMS_MULTIPOINT_GEOMETRY_MANAGER_H
diff --git a/pcbnew/tools/drawing_tool.cpp b/pcbnew/tools/drawing_tool.cpp
index 90ab2b0f8..9038e7f69 100644
--- a/pcbnew/tools/drawing_tool.cpp
+++ b/pcbnew/tools/drawing_tool.cpp
@@ -47,6 +47,8 @@
 #include <bitmaps.h>
 #include <hotkeys.h>
 
+#include <preview_items/arc_assistant.h>
+
 #include <class_board.h>
 #include <class_edge_mod.h>
 #include <class_pcb_text.h>
@@ -119,6 +121,16 @@ TOOL_ACTION PCB_ACTIONS::arcPosture( "pcbnew.InteractiveDrawing.arcPosture",
         AS_CONTEXT, TOOL_ACTION::LegacyHotKey( HK_SWITCH_TRACK_POSTURE ),
         _( "Switch Arc Posture" ), _( "Switch the arc posture" ) );
 
+/*
+ * Contextual actions
+ */
+
+static TOOL_ACTION deleteLastPoint( "pcbnew.InteractiveDrawing.deleteLastPoint",
+        AS_CONTEXT, WXK_BACK,
+        _( "Delete Last Point" ), _( "Delete the last point added to the current item" ),
+        undo_xpm );
+
+
 DRAWING_TOOL::DRAWING_TOOL() :
     PCB_TOOL( "pcbnew.InteractiveDrawing" ),
     m_view( nullptr ), m_controls( nullptr ),
@@ -140,10 +152,18 @@ bool DRAWING_TOOL::Init()
         return m_mode != MODE::NONE;
     };
 
+    auto canUndoPoint = [ this ] ( const SELECTION& aSel ) {
+        return m_mode == MODE::ARC;
+    };
+
     auto& ctxMenu = m_menu.GetMenu();
 
     // cancel current toool goes in main context menu at the top if present
     ctxMenu.AddItem( ACTIONS::cancelInteractive, activeToolFunctor, 1000 );
+
+    // some interactive drawing tools can undo the last point
+    ctxMenu.AddItem( deleteLastPoint, canUndoPoint, 1000 );
+
     ctxMenu.AddSeparator( activeToolFunctor, 1000 );
 
     // Drawing type-specific options will be added by the PCB control tool
@@ -1039,138 +1059,98 @@ bool DRAWING_TOOL::drawSegment( int aShape, DRAWSEGMENT*& aGraphic,
 }
 
 
+/**
+ * Update an arc DRAWSEGMENT from the current state
+ * of an Arc Geoemetry Manager
+ */
+static void updateArcFromConstructionMgr(
+        const KIGFX::PREVIEW::ARC_GEOM_MANAGER& aMgr,
+        DRAWSEGMENT& aArc )
+{
+    auto vec = aMgr.GetOrigin();
+
+    aArc.SetCenter( { vec.x, vec.y } );
+
+    vec = aMgr.GetStartRadiusEnd();
+    aArc.SetArcStart( { vec.x, vec.y } );
+
+    aArc.SetAngle( RAD2DECIDEG( -aMgr.GetSubtended() ) );
+}
+
+
 bool DRAWING_TOOL::drawArc( DRAWSEGMENT*& aGraphic )
 {
-    bool clockwise = true;      // drawing direction of the arc
-    double startAngle = 0.0f;   // angle of the first arc line
-    VECTOR2I cursorPos = m_controls->GetCursorPosition();
+    m_toolMgr->RunAction( PCB_ACTIONS::selectionClear, true );
+
+    // Arc geometric construction manager
+    KIGFX::PREVIEW::ARC_GEOM_MANAGER arcManager;
 
-    // Line from the arc center to its origin, to visualize its radius
-    DRAWSEGMENT helperLine;
-    helperLine.SetShape( S_SEGMENT );
-    helperLine.SetLayer( Dwgs_User );
-    helperLine.SetWidth( 1 );
+    // Arc drawing assistant overlay
+    KIGFX::PREVIEW::ARC_ASSISTANT arcAsst( arcManager );
 
     // Add a VIEW_GROUP that serves as a preview for the new item
     SELECTION preview;
     m_view->Add( &preview );
+    m_view->Add( &arcAsst );
 
-    m_toolMgr->RunAction( PCB_ACTIONS::selectionClear, true );
     m_controls->ShowCursor( true );
     m_controls->SetSnapping( true );
 
     Activate();
 
-    enum ARC_STEPS
-    {
-        SET_ORIGIN = 0,
-        SET_END,
-        SET_ANGLE,
-        FINISHED
-    };
-    int step = SET_ORIGIN;
+    bool firstPoint = false;
 
     // Main loop: keep receiving events
     while( OPT_TOOL_EVENT evt = Wait() )
     {
-        cursorPos = m_controls->GetCursorPosition();
+        const VECTOR2I cursorPos = m_controls->GetCursorPosition();
 
-        if( TOOL_EVT_UTILS::IsCancelInteractive( *evt ) )
-        {
-            preview.Clear();
-            delete aGraphic;
-            aGraphic = NULL;
-            break;
-        }
-        else if( evt->IsClick( BUT_RIGHT ) )
-        {
-            m_menu.ShowContextMenu();
-        }
-        else if( evt->IsClick( BUT_LEFT ) )
+        if( evt->IsClick( BUT_LEFT ) )
         {
-            switch( step )
-            {
-            case SET_ORIGIN:
+            if( !firstPoint )
             {
+                m_controls->SetAutoPan( true );
+                m_controls->CaptureCursor( true );
+
                 LAYER_ID layer = getDrawingLayer();
 
                 // Init the new item attributes
+                // (non-geometric, those are handled by the manager)
                 aGraphic->SetShape( S_ARC );
-                aGraphic->SetAngle( 0.0 );
                 aGraphic->SetWidth( m_lineWidth );
-                aGraphic->SetCenter( wxPoint( cursorPos.x, cursorPos.y ) );
                 aGraphic->SetLayer( layer );
 
-                helperLine.SetStart( aGraphic->GetCenter() );
-                helperLine.SetEnd( aGraphic->GetCenter() );
-
                 preview.Add( aGraphic );
-                preview.Add( &helperLine );
-
-                m_controls->SetAutoPan( true );
-                m_controls->CaptureCursor( true );
-            }
-            break;
-
-            case SET_END:
-            {
-                if( wxPoint( cursorPos.x, cursorPos.y ) != aGraphic->GetCenter() )
-                {
-                    VECTOR2D startLine( aGraphic->GetArcStart() - aGraphic->GetCenter() );
-                    startAngle = startLine.Angle();
-                    aGraphic->SetArcStart( wxPoint( cursorPos.x, cursorPos.y ) );
-                }
-                else
-                    --step;     // one another chance to draw a proper arc
+                firstPoint = true;
             }
-            break;
 
-            case SET_ANGLE:
-            {
-                if( wxPoint( cursorPos.x, cursorPos.y ) != aGraphic->GetArcStart() && aGraphic->GetAngle() != 0 )
-                {
-                    assert( aGraphic->GetArcStart() != aGraphic->GetArcEnd() );
-                    assert( aGraphic->GetWidth() > 0 );
-
-                    preview.Remove( aGraphic );
-                    preview.Remove( &helperLine );
-                }
-                else
-                    --step;     // one another chance to draw a proper arc
-            }
-            break;
-            }
+            arcManager.AddPoint( cursorPos, true );
+        }
 
-            if( ++step == FINISHED )
-                break;
+        else if( evt->IsAction( &deleteLastPoint ) )
+        {
+            arcManager.RemoveLastPoint();
         }
 
         else if( evt->IsMotion() )
         {
-            switch( step )
-            {
-            case SET_END:
-                helperLine.SetEnd( wxPoint( cursorPos.x, cursorPos.y ) );
-                aGraphic->SetArcStart( wxPoint( cursorPos.x, cursorPos.y ) );
-                break;
-
-            case SET_ANGLE:
-            {
-                VECTOR2D endLine( wxPoint( cursorPos.x, cursorPos.y ) - aGraphic->GetCenter() );
-                double newAngle = RAD2DECIDEG( endLine.Angle() - startAngle );
+            // set angle snap
+            arcManager.SetAngleSnap( evt->Modifier( MD_CTRL ) );
 
-                // Adjust the new angle to (counter)clockwise setting
-                if( clockwise && newAngle < 0.0 )
-                    newAngle += 3600.0;
-                else if( !clockwise && newAngle > 0.0 )
-                    newAngle -= 3600.0;
+            // update, but don't step the manager state
+            arcManager.AddPoint( cursorPos, false );
+        }
 
-                aGraphic->SetAngle( newAngle );
-            }
+        else if( TOOL_EVT_UTILS::IsCancelInteractive( *evt ) )
+        {
+            preview.Clear();
+            delete aGraphic;
+            aGraphic = nullptr;
             break;
-            }
-
-            m_view->Update( &preview );
+        }
+        else if( evt->IsClick( BUT_RIGHT ) )
+        {
+            m_menu.ShowContextMenu();
         }
 
         else if( evt->IsAction( &PCB_ACTIONS::incWidth ) )
@@ -1189,19 +1169,26 @@ bool DRAWING_TOOL::drawArc( DRAWSEGMENT*& aGraphic )
 
         else if( evt->IsAction( &PCB_ACTIONS::arcPosture ) )
         {
-            if( clockwise )
-                aGraphic->SetAngle( aGraphic->GetAngle() - 3600.0 );
-            else
-                aGraphic->SetAngle( aGraphic->GetAngle() + 3600.0 );
+            arcManager.ToggleClockwise();
+        }
 
-            clockwise = !clockwise;
+        if( arcManager.IsComplete() )
+        {
+            break;
+        }
+        else if( arcManager.HasGeometryChanged() )
+        {
+            updateArcFromConstructionMgr( arcManager, *aGraphic );
             m_view->Update( &preview );
+            m_view->Update( &arcAsst );
         }
     }
 
+    preview.Remove( aGraphic );
+    m_view->Remove( &arcAsst );
     m_view->Remove( &preview );
 
-    return ( step > SET_ORIGIN );
+    return !arcManager.IsReset();
 }
 
 
-- 
2.12.0

Attachment: arcassist.mp4
Description: video/mp4


Follow ups