The interactive manipulator (imp) in Horizon EDA is the editor for anything that exists in 2D space, to be more precise:

  • Symbols
  • Schematics
  • Padstacks
  • Packages
  • Boards

The user interaction mostly revolves around tools that allow the user to do one particular action such as moving or deleting objects or a global operation not specific to an object such as modifying the board’s stackup.

For the purpose of this post, we’ll implement a tool that changes on edge of a polygon to an arc.

Add the tool to the action catalog

First of all, we’ll have to add our new tool to the ToolID enum in tool_id.hpp :

enum class ToolID {
     ...
     POLYGON_TO_LINE_LOOP,
+    POLYGON_EDGE_TO_ARC
 };

With that in place, we can add it to the action catalog to give it a human-readable name.

const std::map<std::pair<ActionID, ToolID>, ActionCatalogItem> action_catalog =
 {
         ...
         {{ActionID::TOOL, ToolID::POLYGON_TO_LINE_LOOP},
          {"Polygon to line loop", ActionGroup::GRAPHICS, ActionCatalogItem::AVAILABLE_EVERYWHERE,
           ActionCatalogItem::FLAGS_DEFAULT}},
+
+        {{ActionID::TOOL, ToolID::POLYGON_EDGE_TO_ARC},
+         {"Polygon edge to arc", ActionGroup::GRAPHICS, ActionCatalogItem::AVAILABLE_EVERYWHERE,
+          ActionCatalogItem::FLAGS_DEFAULT}},
 }; 

ActionGroup::GRAPHICS will make our new tool show up in the graphics group in the spacebar menu and the the key sequences preferences. ActionCatalogItem::AVAILABLE_EVERYWHERE specifies that the tool is valid for all editor types (symbols, etc., see above). The key sequences preferences editor also uses this to allow for key bindings specific to an editor type. The flags declared by ActionCatalogItem::FLAGS_DEFAULT could be used to hide the tool from various places such as the context menu or the key sequences preferences, but aren’t needed in our case.

We’ll also have to add our new tool to the tool LUT so there’s a machine-readable name for our tool:

const LutEnumStr<ToolID> tool_lut = {
         ...
         TOOL_LUT_ITEM(POLYGON_TO_LINE_LOOP),
+        TOOL_LUT_ITEM(POLYGON_EDGE_TO_ARC),
 };

Implement the tool

Now that we’ve told the imp about our tool, we can go about actually implementing it, starting with the header in core/tool_polygon_edge_to_arc.hpp:

#pragma once
#include "core.hpp"

namespace horizon {

class ToolPolygonEdgeToArc : public ToolBase {
public:
    ToolPolygonEdgeToArc(Core *c, ToolID tid);
    ToolResponse begin(const ToolArgs &args) override;
    ToolResponse update(const ToolArgs &args) override;
    bool can_begin() override;
    bool is_specific() override
    {
        return true;
    }

private:
    std::pair<class Polygon *, size_t> get_polygon_edge();
    Polygon *poly = nullptr;
    size_t vertex_index = 0;
    Polygon::Vertex *vertex = nullptr;
};
} // namespace horizon

Tools have to inherit from ToolBase to be a tool in the first place. Some tools share code by inheriting from helper classes such as ToolHelperMove.

Make the tool creatable

To map our tool’s class ToolPolygonEdgeToArc to its ID POLYGON_EDGE_TO_ARC, we’ll add this to the Core::create_tool method:

 ...
 #include "tool_polygon_to_line_loop.hpp"
+#include "tool_polygon_edge_to_arc.hpp"
 ...
 std::unique_ptr<ToolBase> Core::create_tool(ToolID tool_id)
 {
     ...
     case ToolID::POLYGON_TO_LINE_LOOP:
         return std::make_unique<ToolPolygonToLineLoop>(this, tool_id);
 
+    case ToolID::POLYGON_EDGE_TO_ARC:
+        return std::make_unique<ToolPolygonEdgeToArc>(this, tool_id);
+ 

Lifecycle

Before diving into the actual implementation, we’ll need to understand the lifecycle of a tool.

Testing if a tool is valid

Before the user invokes a tool, various parts of the imp such as the context menu or the spacebar menu need figure out if our tool makes sense to be invoked in the current situation or not. To do so, each tool that’s available for the editor type is constructed and its can_begin method is called. If it returns true, the tool shows up in the context or spacebar menu. There’s also the is_specific method that tells if a tool is specific to the current selection (e.g. delete) or operates independently (e.g. edit stackup). Only specific tools show up in the context menu to reduce clutter.

Invoking a tool

Once the users has invoked a tool, the imp tells the core to construct it and calls the tool’s begin method. The begin method is the place to do things that need to be done once during the tool’s lifecycle. For some tools that don’t require interaction, the tool’s lifecyle ends in begin by returning ToolResponse::end().

While the tool is active, it receives all user input, i.e. key presses, mouse movement and clicks in it’s update method. Once the tool determines the user is done editing, it returns ToolResponse::end() and the imp and core destroy the tool and do the necessary cleanup.

Actually implement the tool

With all of the boilerplate in place we can finally start writing code that does something! The source code for our tool goes in core/tool_polygon_edge_to_arc.cpp and we need to add it to the SRC_IMP variable in the Makefile.

ToolPolygonEdgeToArc::ToolPolygonEdgeToArc(Core *c, ToolID tid) : ToolBase(c, tid)
{
}

The constructor can usually be left empty, any initialisation has to take in place since the constructor will also be called when just checking if the tool can do something useful.

can_begin

bool ToolPolygonEdgeToArc::can_begin()
{
    if (!core.r->has_object_type(ObjectType::POLYGON))
        return false;
    return get_polygon_edge().first;
}

std::pair<Polygon *, size_t> ToolPolygonEdgeToArc::get_polygon_edge()
{
    for (const auto &it : core.r->selection) {
        if (it.type == ObjectType::POLYGON_EDGE) {
            return {core.r->get_polygon(it.uuid), it.vertex};
        }
    }
    return {nullptr, 0};
} 

The Core is the part of the interactive manipulator that holds the document (symbol, schematic, board, etc.) and provides a unified interface to object types common to more than one document type such as polygons or lines. For each document type, there’s a
subclass of the Core. A tool gets access to the Core by means of the Cores convince class that holds pointers to every possible subclass with only the generic one r and the pointer for the current document type being non-null. This way we can write core.c instead of the more lengthy dynamic_cast<CoreSchematic*>(core). As our tool is intended to be used on every document type that supports polygons, we only need to consider core.r.

The first thing can_begin needs to check, is if the current document type supports polygons. If the tool would only make sense for boards, we’d first check if core.b is not null to ensure the document is a board and not something else.

The next thing to check is if the user had selected a polygon edge by looking for a SelectableRef with type == ObjectType::POLYGON_EDGE in the core’s selection. The core receives the selection from the canvas the moment a tool is invoked by the user. As we’ll need to find the polygon edge from the selection as well when actually using the tool, this code is factored out into the get_polygon_edge method. After having found a polygon edge, we get the corresponding Polygon from the core by the uuid from the SelectableRef.

begin

ToolResponse ToolPolygonEdgeToArc::begin(const ToolArgs &args)
{
    std::tie(poly, vertex_index) = get_polygon_edge();
    vertex = &poly->vertices.at(vertex_index);
    vertex->type = Polygon::Vertex::Type::ARC;
    vertex->arc_center = args.coords;
    imp->tool_bar_set_tip("<b>LMB:</b>place arc center <b>RMB:</b>cancel <b>e:</b>flip arc");
    return ToolResponse();
}

Since we now have the Polygon and the vertex we want to work with, we can start modifying it. The first thing to do is to set the type of the selected vertex to be an arc and set the arc’s center to the current cursor position. It’s important to do this in begin as if we’d only do it in update, the user would see an unexpected jump when first moving the mouse after activating the tool. The begin method also is the right place to set the tool bar tip visible at the bottom of the canvas to tell the user about ways of interacting with the tool.

update

ToolResponse ToolPolygonEdgeToArc::update(const ToolArgs &args)
{
    if (args.type == ToolEventType::MOVE) {
        vertex->arc_center = args.coords;
    }
    else if (args.type == ToolEventType::KEY) {
        if (args.key == GDK_KEY_e) {
            vertex->arc_reverse = !vertex->arc_reverse;
        }
    }
    else if (args.type == ToolEventType::CLICK) {
        if (args.button == 1) {
            core.r->commit();
            return ToolResponse::end();
        }
        else if (args.button == 3) {
            core.r->revert();
            return ToolResponse::end();
        }
    }
    return ToolResponse();
}

The update method is where real action happens: Every time the user does something, this method gets called and has to update the document accordingly. The imp will take care of updating the canvas so that the user can see what they’re doing.

For mouse move events (ToolEventType::MOVE) we just set the arc’s center position to the cursor position. If we were to spend more time on this, we’d set it along the perpendicular bisector of the original edge so that the start and end radii are the same.

Since arcs can go either clockwise or counterclockwise from start to end, we use the e key to allow the user to flip the arc’s direction. For simplicity, the key member of ToolArgs uses the key symbols defined by the Gdk header gdkkeysyms.h.

For our tool, we use mouse click events to either commit or revert the user’s modifications to the document. The button member of ToolArgs uses the same semantics as Gdk with 1 being the left and 3 being the right mouse button. It’s important to either call Core::commit() or Core::revert() before ending the tool by returning ToolResponse::end() for the undo/redo history to work as intended.

Closing words

The tool implemented in this post is a rather simple one, but should help to explain the concepts used in more complex tools. Happy coding!