Skip to content

Commit e4caa9f

Browse files
authored
Merge pull request solvcon#931 from tigercosmos/worktree-canvas-painter-circle
Add the painter toolbox to draw a circle on the pilot's 2D canvas
2 parents 695be1d + 86b99da commit e4caa9f

13 files changed

Lines changed: 689 additions & 7 deletions

File tree

cpp/solvcon/pilot/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ cmake_minimum_required(VERSION 4.0.1)
66
set(SOLVCON_PILOT_PYMODHEADERS
77
${CMAKE_CURRENT_SOURCE_DIR}/R3DWidget.hpp
88
${CMAKE_CURRENT_SOURCE_DIR}/R2DWidget.hpp
9+
${CMAKE_CURRENT_SOURCE_DIR}/DrawTool.hpp
910
${CMAKE_CURRENT_SOURCE_DIR}/RWorldRenderer2d.hpp
1011
${CMAKE_CURRENT_SOURCE_DIR}/RWorld.hpp
1112
${CMAKE_CURRENT_SOURCE_DIR}/RManager.hpp
@@ -22,6 +23,7 @@ set(SOLVCON_PILOT_PYMODHEADERS
2223
set(SOLVCON_PILOT_PYMODSOURCES
2324
${CMAKE_CURRENT_SOURCE_DIR}/R3DWidget.cpp
2425
${CMAKE_CURRENT_SOURCE_DIR}/R2DWidget.cpp
26+
${CMAKE_CURRENT_SOURCE_DIR}/DrawTool.cpp
2527
${CMAKE_CURRENT_SOURCE_DIR}/RWorldRenderer2d.cpp
2628
${CMAKE_CURRENT_SOURCE_DIR}/RWorld.cpp
2729
${CMAKE_CURRENT_SOURCE_DIR}/RManager.cpp

cpp/solvcon/pilot/DrawTool.cpp

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright (c) 2026, solvcon team <contact@solvcon.net>
3+
* BSD 3-Clause License, see COPYING
4+
*/
5+
6+
#include <solvcon/pilot/DrawTool.hpp>
7+
8+
#include <cmath>
9+
#include <stdexcept>
10+
11+
#include <QColor>
12+
#include <QPainter>
13+
#include <QPen>
14+
#include <QPointF>
15+
16+
namespace solvcon
17+
{
18+
19+
void DrawToolBase::paint_preview(
20+
QPainter & painter,
21+
ViewTransform2dFp64 const & view,
22+
std::span<DrawPoint const> points) const
23+
{
24+
QPen pen(QColor(240, 200, 120));
25+
pen.setCosmetic(true);
26+
pen.setWidthF(1.5);
27+
pen.setStyle(Qt::DashLine);
28+
painter.setPen(pen);
29+
painter.setBrush(Qt::NoBrush);
30+
paint_outline(painter, view, points);
31+
}
32+
33+
namespace
34+
{
35+
36+
/// The default navigation tool: a left-button drag pans and zooms the view instead of drawing.
37+
class PanTool : public DrawToolBase
38+
{
39+
40+
public:
41+
42+
static constexpr char const * NAME = "pan";
43+
44+
std::string name() const override { return NAME; }
45+
46+
bool can_draw_shape() const override { return false; }
47+
48+
void commit(WorldFp64 &, std::span<DrawPoint const>) const override {}
49+
50+
protected:
51+
52+
void paint_outline(QPainter &, ViewTransform2dFp64 const &, std::span<DrawPoint const>) const override {}
53+
54+
}; /* end class PanTool */
55+
56+
/// Circle defined by two gesture points: the center and a point on the rim.
57+
class CircleTool : public DrawToolBase
58+
{
59+
60+
public:
61+
62+
static constexpr char const * NAME = "circle";
63+
64+
std::string name() const override { return NAME; }
65+
66+
bool can_draw_shape() const override { return true; }
67+
68+
void commit(WorldFp64 & world, std::span<DrawPoint const> points) const override
69+
{
70+
if (points.size() < 2)
71+
{
72+
throw std::invalid_argument("CircleTool::commit: need at least two points");
73+
}
74+
DrawPoint const & center = points.front();
75+
DrawPoint const & rim = points.back();
76+
world.add_circle(center.x, center.y, std::hypot(rim.x - center.x, rim.y - center.y));
77+
}
78+
79+
protected:
80+
81+
void paint_outline(QPainter & painter,
82+
ViewTransform2dFp64 const & view,
83+
std::span<DrawPoint const> points) const override
84+
{
85+
if (points.size() < 2)
86+
{
87+
throw std::invalid_argument("CircleTool::paint_outline: need at least two points");
88+
}
89+
DrawPoint const & center = points.front();
90+
DrawPoint const & rim = points.back();
91+
double const radius = std::hypot(rim.x - center.x, rim.y - center.y);
92+
if (radius <= 0.0)
93+
{
94+
throw std::invalid_argument("CircleTool::paint_outline: radius must be positive");
95+
}
96+
double center_x = 0.0;
97+
double center_y = 0.0;
98+
view.screen_from_world(center.x, center.y, center_x, center_y);
99+
double const radius_px = view.zoom() * radius;
100+
painter.drawEllipse(QPointF(center_x, center_y), radius_px, radius_px);
101+
}
102+
103+
}; /* end class CircleTool */
104+
105+
/// The tool registry. The first entry is the default tool a fresh canvas
106+
/// starts with. Add a new shape by writing a `DrawToolBase` subclass above
107+
/// and appending one entry here.
108+
struct ToolEntry
109+
{
110+
char const * name;
111+
std::unique_ptr<DrawToolBase> (*make)();
112+
};
113+
114+
ToolEntry const TOOL_TABLE[] = {
115+
{PanTool::NAME, []() -> std::unique_ptr<DrawToolBase>
116+
{ return std::make_unique<PanTool>(); }},
117+
{CircleTool::NAME, []() -> std::unique_ptr<DrawToolBase>
118+
{ return std::make_unique<CircleTool>(); }},
119+
};
120+
121+
} /* end anonymous namespace */
122+
123+
std::vector<std::string> const & draw_tool_names()
124+
{
125+
static std::vector<std::string> const names = []
126+
{
127+
std::vector<std::string> out;
128+
for (ToolEntry const & entry : TOOL_TABLE)
129+
{
130+
out.emplace_back(entry.name);
131+
}
132+
return out;
133+
}();
134+
return names;
135+
}
136+
137+
std::string const & default_draw_tool_name()
138+
{
139+
static_assert(std::size(TOOL_TABLE) > 0, "TOOL_TABLE must have at least one entry");
140+
return draw_tool_names().front();
141+
}
142+
143+
std::unique_ptr<DrawToolBase> make_draw_tool(std::string const & name)
144+
{
145+
for (ToolEntry const & entry : TOOL_TABLE)
146+
{
147+
if (name == entry.name)
148+
{
149+
return entry.make();
150+
}
151+
}
152+
throw std::invalid_argument("make_draw_tool: unknown draw tool '" + name + "'");
153+
}
154+
155+
bool is_draw_tool(std::string const & name)
156+
{
157+
for (ToolEntry const & entry : TOOL_TABLE)
158+
{
159+
if (name == entry.name)
160+
{
161+
return true;
162+
}
163+
}
164+
return false;
165+
}
166+
167+
} /* end namespace solvcon */
168+
169+
// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4:

cpp/solvcon/pilot/DrawTool.hpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#pragma once
2+
3+
/*
4+
* Copyright (c) 2026, solvcon team <contact@solvcon.net>
5+
* BSD 3-Clause License, see COPYING
6+
*/
7+
8+
#include <solvcon/pilot/common_detail.hpp> // Must be the first include.
9+
10+
#include <solvcon/universe/ViewTransform2d.hpp>
11+
#include <solvcon/universe/World.hpp>
12+
13+
#include <memory>
14+
#include <span>
15+
#include <string>
16+
#include <vector>
17+
18+
class QPainter;
19+
20+
namespace solvcon
21+
{
22+
23+
/// A point in world coordinates -- the unit a drawing gesture is built from.
24+
struct DrawPoint
25+
{
26+
double x;
27+
double y;
28+
}; /* end struct DrawPoint */
29+
30+
/// Abstract base class for a 2D canvas drawing tool.
31+
class DrawToolBase
32+
{
33+
34+
public:
35+
36+
virtual ~DrawToolBase() = default;
37+
38+
/// Stable tool name shared with the Python binding and the toolbox.
39+
virtual std::string name() const = 0;
40+
41+
/// Wether this tool draws a shape or just navigates the view.
42+
virtual bool can_draw_shape() const = 0;
43+
44+
/// Paint the rubber-band preview of the gesture `points`. Sets the
45+
/// shared preview pen, then defers to `paint_outline`.
46+
void paint_preview(QPainter & painter, ViewTransform2dFp64 const & view, std::span<DrawPoint const> points) const;
47+
48+
/// Commit the shape described by the gesture `points` into `world`.
49+
virtual void commit(WorldFp64 & world, std::span<DrawPoint const> points) const = 0;
50+
51+
protected:
52+
53+
/// Draw the shape's outline for `points`, with the preview pen already
54+
/// set on `painter` by `paint_preview`.
55+
virtual void paint_outline(QPainter & painter,
56+
ViewTransform2dFp64 const & view,
57+
std::span<DrawPoint const> points) const = 0;
58+
59+
}; /* end class DrawToolBase */
60+
61+
/// Get the names of all registered tools, in Painter-toolbox order. The
62+
/// first entry is the default tool (see `default_draw_tool_name`).
63+
std::vector<std::string> const & draw_tool_names();
64+
65+
/// Name of the default tool a fresh canvas starts with: the first
66+
/// registered tool, which is the pan navigation tool.
67+
std::string const & default_draw_tool_name();
68+
69+
/// Build the tool registered under `name`.
70+
/// @return A unique pointer to the tool, never null for a valid name.
71+
/// @throw std::invalid_argument for an unknown name.
72+
std::unique_ptr<DrawToolBase> make_draw_tool(std::string const & name);
73+
74+
/// True if `name` is a registered tool R2DWidget accepts. Lets callers
75+
/// validate a name without building a tool.
76+
bool is_draw_tool(std::string const & name);
77+
78+
} /* end namespace solvcon */
79+
80+
// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4:

0 commit comments

Comments
 (0)