How to implement a tool
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!