Add a sketch fillet: round the corner between two drawn lines

"Ajouter un congé" on the drawing toolbar, in Édition > Ajouter and in
the command search. Click two lines one after the other, or once where
they meet, and a small dialog asks for the radius (remembered for the
next one); Enter makes the fillet. The tool stays on for the next
corner; Escape or a right-click ends it.

The two lines are trimmed -- or extended, when they did not reach each
other -- to the points where the arc touches them, and an arc is added
between them. For each line the part kept is the one on the side of
the corner where it was clicked; a click on the corner keeps the
longer part of each. Parallel or nearly aligned lines, and a radius
too big for the lines, are refused with a status-bar message.

The arc is an ordinary Ellipse shape with a start and end angle, as
the Arc tool and the ellipse's arc handles save it, so nothing new is
written to the file. It is added by its own small undo command rather
than AddGraphicsObjectCommand, whose redo() calls setPos(), and
QetShapeItem::setPos() snaps the outline's corner to the grid, which
would pull the arc off the lines. One undo step undoes the fillet.

New icon draw-fillet, light and dark, registered like the Align icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-02 07:08:18 +13:00
parent b6495b51a6
commit 4b501521a5
13 changed files with 519 additions and 0 deletions
+2
View File
@@ -352,6 +352,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/diagramevent/diagrameventaddimage.h
${QET_DIR}/sources/diagramevent/diagrameventaddshape.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddshape.h
${QET_DIR}/sources/diagramevent/diagrameventfillet.cpp
${QET_DIR}/sources/diagramevent/diagrameventfillet.h
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 22 22" width="22" height="22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 10 4 H 4 V 10" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:2" d="M 19 4 H 11 A 7 7 0 0 0 4 11 V 19" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 546 B

+28
View File
@@ -29,6 +29,7 @@
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
<file alias="themes/qet/scalable/align-horizontal-center.svg">scalable/align-horizontal-center.svg</file>
<file alias="themes/qet/scalable/align-horizontal-left.svg">scalable/align-horizontal-left.svg</file>
<file alias="themes/qet/scalable/draw-fillet.svg">scalable/draw-fillet.svg</file>
<file alias="themes/qet/scalable/align-horizontal-right.svg">scalable/align-horizontal-right.svg</file>
<file alias="themes/qet/scalable/align-vertical-bottom.svg">scalable/align-vertical-bottom.svg</file>
<file alias="themes/qet/scalable/align-vertical-center.svg">scalable/align-vertical-center.svg</file>
@@ -61,17 +62,29 @@
<file alias="themes/qet/64x64@2/align-horizontal-center.svg">scalable/16/align-horizontal-center.svg</file>
<file alias="themes/qet/128x128@2/align-horizontal-center.svg">scalable/16/align-horizontal-center.svg</file>
<file alias="themes/qet/16x16/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/16x16/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/22x22/align-horizontal-left.svg">generated/scalable-22/align-horizontal-left.svg</file>
<file alias="themes/qet/22x22/draw-fillet.svg">generated/scalable-22/draw-fillet.svg</file>
<file alias="themes/qet/32x32/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/32x32/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/48x48/align-horizontal-left.svg">scalable/align-horizontal-left.svg</file>
<file alias="themes/qet/48x48/draw-fillet.svg">scalable/draw-fillet.svg</file>
<file alias="themes/qet/64x64/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/64x64/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/128x128/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/128x128/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/16x16@2/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/16x16@2/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/22x22@2/align-horizontal-left.svg">generated/scalable-22/align-horizontal-left.svg</file>
<file alias="themes/qet/22x22@2/draw-fillet.svg">generated/scalable-22/draw-fillet.svg</file>
<file alias="themes/qet/32x32@2/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/32x32@2/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/48x48@2/align-horizontal-left.svg">scalable/align-horizontal-left.svg</file>
<file alias="themes/qet/48x48@2/draw-fillet.svg">scalable/draw-fillet.svg</file>
<file alias="themes/qet/64x64@2/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/64x64@2/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/128x128@2/align-horizontal-left.svg">scalable/16/align-horizontal-left.svg</file>
<file alias="themes/qet/128x128@2/draw-fillet.svg">scalable/16/draw-fillet.svg</file>
<file alias="themes/qet/16x16/align-horizontal-right.svg">scalable/16/align-horizontal-right.svg</file>
<file alias="themes/qet/22x22/align-horizontal-right.svg">generated/scalable-22/align-horizontal-right.svg</file>
<file alias="themes/qet/32x32/align-horizontal-right.svg">scalable/16/align-horizontal-right.svg</file>
@@ -2372,6 +2385,7 @@
<file>themes/qet-dark/128x128/settings.png</file>
<file>themes/qet-dark/scalable/align-horizontal-center.svg</file>
<file>themes/qet-dark/scalable/align-horizontal-left.svg</file>
<file>themes/qet-dark/scalable/draw-fillet.svg</file>
<file>themes/qet-dark/scalable/align-horizontal-right.svg</file>
<file>themes/qet-dark/scalable/align-vertical-bottom.svg</file>
<file>themes/qet-dark/scalable/align-vertical-center.svg</file>
@@ -2394,7 +2408,9 @@
<file>themes/qet-dark/scalable-16/align-horizontal-center.svg</file>
<file>themes/qet-dark/scalable-22/align-horizontal-center.svg</file>
<file>themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file>themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file>themes/qet-dark/scalable-22/align-horizontal-left.svg</file>
<file>themes/qet-dark/scalable-22/draw-fillet.svg</file>
<file>themes/qet-dark/scalable-16/align-horizontal-right.svg</file>
<file>themes/qet-dark/scalable-22/align-horizontal-right.svg</file>
<file>themes/qet-dark/scalable-16/align-vertical-bottom.svg</file>
@@ -2990,17 +3006,29 @@
<file alias="themes/qet-dark/64x64@2/align-horizontal-center.svg">themes/qet-dark/scalable-16/align-horizontal-center.svg</file>
<file alias="themes/qet-dark/128x128@2/align-horizontal-center.svg">themes/qet-dark/scalable-16/align-horizontal-center.svg</file>
<file alias="themes/qet-dark/16x16/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/16x16/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/22x22/align-horizontal-left.svg">themes/qet-dark/scalable-22/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/22x22/draw-fillet.svg">themes/qet-dark/scalable-22/draw-fillet.svg</file>
<file alias="themes/qet-dark/32x32/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/32x32/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/48x48/align-horizontal-left.svg">themes/qet-dark/scalable/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/48x48/draw-fillet.svg">themes/qet-dark/scalable/draw-fillet.svg</file>
<file alias="themes/qet-dark/64x64/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/64x64/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/128x128/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/128x128/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/16x16@2/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/16x16@2/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/22x22@2/align-horizontal-left.svg">themes/qet-dark/scalable-22/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/22x22@2/draw-fillet.svg">themes/qet-dark/scalable-22/draw-fillet.svg</file>
<file alias="themes/qet-dark/32x32@2/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/32x32@2/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/48x48@2/align-horizontal-left.svg">themes/qet-dark/scalable/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/48x48@2/draw-fillet.svg">themes/qet-dark/scalable/draw-fillet.svg</file>
<file alias="themes/qet-dark/64x64@2/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/64x64@2/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/128x128@2/align-horizontal-left.svg">themes/qet-dark/scalable-16/align-horizontal-left.svg</file>
<file alias="themes/qet-dark/128x128@2/draw-fillet.svg">themes/qet-dark/scalable-16/draw-fillet.svg</file>
<file alias="themes/qet-dark/16x16/align-horizontal-right.svg">themes/qet-dark/scalable-16/align-horizontal-right.svg</file>
<file alias="themes/qet-dark/22x22/align-horizontal-right.svg">themes/qet-dark/scalable-22/align-horizontal-right.svg</file>
<file alias="themes/qet-dark/32x32/align-horizontal-right.svg">themes/qet-dark/scalable-16/align-horizontal-right.svg</file>
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1" d="M 7.5 2.5 H 2.5 V 7.5" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 14 2.5 H 8 A 5.5 5.5 0 0 0 2.5 8 V 14" class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 10 4 H 4 V 10" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:2" d="M 19 4 H 11 A 7 7 0 0 0 4 11 V 19" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1" d="M 7.5 2.5 H 2.5 V 7.5" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 14 2.5 H 8 A 5.5 5.5 0 0 0 2.5 8 V 14" class="ColorScheme-Text"/>
</svg>

After

Width:  |  Height:  |  Size: 518 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 22 22" width="22" height="22">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 10 4 H 4 V 10" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:2" d="M 19 4 H 11 A 7 7 0 0 0 4 11 V 19" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 10 4 H 4 V 10" class="ColorScheme-Text"/>
<path style="fill:none;stroke:currentColor;stroke-width:2" d="M 19 4 H 11 A 7 7 0 0 0 4 11 V 19" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 546 B

+343
View File
@@ -0,0 +1,343 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech 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.
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventfillet.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../qgimanager.h"
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QInputDialog>
#include <QSettings>
#include <QStatusBar>
#include <QTimer>
#include <QUndoCommand>
#include <QtMath>
#include <algorithm>
#include <cmath>
namespace {
const char *radiusSettingKey = "diagrameditor/fillet_radius";
/**
Adds the fillet's arc to the diagram exactly where it was computed.
AddGraphicsObjectCommand is not used: its redo() calls setPos(),
and QetShapeItem::setPos() snaps the outline's corner to the grid,
which would pull the arc off the two lines it has to touch.
*/
class AddFilletArcCommand : public QUndoCommand
{
public:
AddFilletArcCommand(QetShapeItem *arc, Diagram *diagram, QUndoCommand *parent) :
QUndoCommand(parent), m_arc(arc), m_diagram(diagram)
{ m_diagram->qgiManager().manage(m_arc); }
~AddFilletArcCommand() override
{ m_diagram->qgiManager().release(m_arc); }
void redo() override { m_diagram->addItem(m_arc); }
void undo() override { m_diagram->removeItem(m_arc); }
private:
QetShapeItem *m_arc;
Diagram *m_diagram;
};
qreal length(const QPointF &v) { return std::hypot(v.x(), v.y()); }
qreal dot(const QPointF &a, const QPointF &b) { return a.x() * b.x() + a.y() * b.y(); }
qreal distanceToSegment(const QPointF &p, const QLineF &segment)
{
const QPointF d = segment.p2() - segment.p1();
const qreal len2 = dot(d, d);
if (len2 <= 0)
return length(p - segment.p1());
const qreal t = qBound(qreal(0), dot(p - segment.p1(), d) / len2, qreal(1));
return length(p - (segment.p1() + t * d));
}
QLineF sceneLine(const QetShapeItem *line)
{
return QLineF(line->mapToScene(line->line().p1()), line->mapToScene(line->line().p2()));
}
//Angle of a point on a circle, in the convention QPainterPath::arcTo()
//and QetShapeItem use: degrees, anticlockwise from three o'clock.
qreal arcAngle(const QPointF &centre, const QPointF &p)
{
return qRadiansToDegrees(std::atan2(-(p.y() - centre.y()), p.x() - centre.x()));
}
//Same test as QetShapeItem::setStartAngle()/setEndAngle() use to snap
//an arc back to a full ellipse.
bool nearlyFullTurn(qreal span)
{
qreal wrapped = std::fmod(span, 360.0);
if (wrapped < 0) wrapped += 360.0;
return wrapped < 5.0 || wrapped > 355.0;
}
} // namespace
DiagramEventFillet::DiagramEventFillet(Diagram *diagram) :
DiagramEventInterface(diagram)
{
m_running = true;
//Deferred for the same reason as DiagramEventAddShape's hint:
//the previous tool's destructor clears the status bar after
//this constructor returns.
QTimer::singleShot(0, this, [this]() { showDefaultHint(); });
}
DiagramEventFillet::~DiagramEventFillet()
{
clearFirstPick();
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
}
void DiagramEventFillet::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
event->setAccepted(true);
if (Q_UNLIKELY(m_diagram->isReadOnly()))
return;
if (event->button() == Qt::RightButton)
{
if (m_first_line) {
clearFirstPick();
showDefaultHint();
} else {
m_running = false;
emit finish();
}
return;
}
if (event->button() != Qt::LeftButton)
return;
const QPointF pos = event->scenePos();
const QList<QetShapeItem *> lines = linesNear(pos);
if (lines.isEmpty()) {
showHint(tr("Aucune ligne ici : cliquez sur une ligne dessinée sur le folio"));
return;
}
if (!m_first_line)
{
//One click where two lines meet picks both of them
if (lines.size() >= 2) {
filletLines({lines.at(0), pos}, {lines.at(1), pos});
return;
}
m_first = {lines.first(), pos};
m_first_line = lines.first();
m_diagram->clearSelection();
m_first_line->setSelected(true);
showHint(tr("Cliquez sur la deuxième ligne ; clic droit : annuler"));
return;
}
const auto other = std::find_if(lines.cbegin(), lines.cend(),
[this](QetShapeItem *line) { return line != m_first_line; });
if (other == lines.cend()) {
showHint(tr("Cliquez sur une autre ligne ; clic droit : annuler"));
return;
}
const Pick first = m_first;
clearFirstPick();
filletLines(first, {*other, pos});
}
/**
@brief DiagramEventFillet::linesNear
@return the Line shapes passing within a few screen pixels of
scenePos, nearest first. Only the items under the cursor are looked
at; the diagram is not walked.
*/
QList<QetShapeItem *> DiagramEventFillet::linesNear(const QPointF &scenePos) const
{
qreal scale = 1;
if (!m_diagram->views().isEmpty())
scale = m_diagram->views().constFirst()->transform().m11();
const qreal tolerance = 6.0 / qMax(scale, qreal(0.01));
QList<QPair<qreal, QetShapeItem *>> found;
const QRectF area(scenePos - QPointF(tolerance, tolerance), QSizeF(2 * tolerance, 2 * tolerance));
for (QGraphicsItem *item : m_diagram->items(area))
{
if (item->type() != QetShapeItem::Type)
continue;
auto *shape = static_cast<QetShapeItem *>(item);
if (shape->shapeType() != QetShapeItem::Line)
continue;
const qreal distance = distanceToSegment(scenePos, sceneLine(shape));
if (distance <= tolerance)
found.append({distance, shape});
}
std::sort(found.begin(), found.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
QList<QetShapeItem *> lines;
for (const auto &f : found)
lines.append(f.second);
return lines;
}
/**
@brief DiagramEventFillet::filletLines
Rounds the corner between the two picked lines. For each line, the
part kept is the one on the side of the corner where it was clicked;
a click on the corner itself keeps the longer part. The other end of
each line moves to the point where the arc touches it -- shortening
the line, or lengthening it when the two lines did not reach each
other. One undo step undoes the whole fillet.
*/
void DiagramEventFillet::filletLines(const Pick &a, const Pick &b)
{
const QLineF s1 = sceneLine(a.line);
const QLineF s2 = sceneLine(b.line);
QPointF corner;
if (s1.intersects(s2, &corner) == QLineF::NoIntersection) {
showHint(tr("Ces deux lignes sont parallèles : pas de congé possible"));
return;
}
qreal scale = 1;
if (!m_diagram->views().isEmpty())
scale = m_diagram->views().constFirst()->transform().m11();
const qreal cornerTolerance = 6.0 / qMax(scale, qreal(0.01));
auto keptEnd = [&](const QLineF &segment, const QPointF &pick) -> int {
const QPointF ends[2] = {segment.p1(), segment.p2()};
const QPointF towardPick = pick - corner;
const bool onCorner = length(towardPick) < cornerTolerance;
int best = -1;
for (int pass = 0; pass < 2 && best < 0; ++pass) {
qreal bestLength = -1;
for (int i = 0; i < 2; ++i) {
const QPointF v = ends[i] - corner;
if (pass == 0 && !onCorner && dot(v, towardPick) <= 0)
continue;
if (length(v) > bestLength) { best = i; bestLength = length(v); }
}
}
return best;
};
const int keep1 = keptEnd(s1, a.pos);
const int keep2 = keptEnd(s2, b.pos);
const QPointF e1 = keep1 == 0 ? s1.p1() : s1.p2();
const QPointF e2 = keep2 == 0 ? s2.p1() : s2.p2();
const qreal len1 = length(e1 - corner);
const qreal len2 = length(e2 - corner);
if (len1 < 1e-6 || len2 < 1e-6) {
showHint(tr("Une des lignes s'arrête au coin : rien à arrondir de ce côté"));
return;
}
const QPointF u1 = (e1 - corner) / len1;
const QPointF u2 = (e2 - corner) / len2;
const qreal theta = std::acos(qBound(qreal(-1), dot(u1, u2), qreal(1)));
if (qRadiansToDegrees(theta) > 174.0) {
showHint(tr("Ces deux lignes sont presque alignées : pas de congé possible"));
return;
}
QSettings settings;
bool ok = false;
QWidget *parent = m_diagram->views().isEmpty() ? nullptr : m_diagram->views().constFirst();
const qreal radius = QInputDialog::getDouble(parent, tr("Congé"), tr("Rayon :"),
settings.value(radiusSettingKey, 20.0).toDouble(), 0.1, 100000.0, 1, &ok);
if (!ok || !m_diagram)
return;
settings.setValue(radiusSettingKey, radius);
const qreal tangentDistance = radius / std::tan(theta / 2);
if (tangentDistance > len1 + 1e-6 || tangentDistance > len2 + 1e-6) {
showHint(tr("Rayon trop grand pour ces lignes (%1 au plus)")
.arg(std::min(len1, len2) * std::tan(theta / 2), 0, 'f', 1));
return;
}
const QPointF t1 = corner + u1 * tangentDistance;
const QPointF t2 = corner + u2 * tangentDistance;
const QPointF bisector = (u1 + u2) / length(u1 + u2);
const QPointF centre = corner + bisector * (radius / std::sin(theta / 2));
//Each line keeps its picked end and moves its other end to the
//tangent point; p1/p2 keep their roles, mapped back into the
//line's own coordinates (it may be rotated or skewed).
auto newLine = [](QetShapeItem *line, int keep, const QPointF &kept, const QPointF &tangent) {
const QPointF p1 = keep == 0 ? kept : tangent;
const QPointF p2 = keep == 0 ? tangent : kept;
return QLineF(line->mapFromScene(p1), line->mapFromScene(p2));
};
auto *arc = new QetShapeItem(centre - QPointF(radius, radius),
centre + QPointF(radius, radius), QetShapeItem::Ellipse);
arc->setPen(a.line->pen());
qreal start = std::fmod(arcAngle(centre, t1), 360.0);
if (start < 0) start += 360.0;
const qreal span = std::fmod(arcAngle(centre, t2) - start + 540.0, 360.0) - 180.0;
//Set the two angles in an order whose intermediate state is not
//close to a full turn, which the setters would snap to an ellipse.
if (!nearlyFullTurn(360.0 - start)) {
arc->setStartAngle(start);
arc->setEndAngle(start + span);
} else {
arc->setEndAngle(start + span);
arc->setStartAngle(start);
}
auto *undo = new QUndoCommand(tr("Ajouter un congé"));
new QPropertyUndoCommand(a.line, "line", a.line->line(), newLine(a.line, keep1, e1, t1), undo);
new QPropertyUndoCommand(b.line, "line", b.line->line(), newLine(b.line, keep2, e2, t2), undo);
new AddFilletArcCommand(arc, m_diagram, undo);
m_diagram->undoStack().push(undo);
showDefaultHint();
}
void DiagramEventFillet::clearFirstPick()
{
if (m_first_line)
m_first_line->setSelected(false);
m_first_line.clear();
m_first = Pick();
}
void DiagramEventFillet::showHint(const QString &text) const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(text);
}
void DiagramEventFillet::showDefaultHint() const
{
showHint(tr("Congé : cliquez sur deux lignes, ou une fois là où elles se rejoignent ; "
"Échap ou clic droit : terminer"));
}
+62
View File
@@ -0,0 +1,62 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech 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.
QElectroTech 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 QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMEVENTFILLET_H
#define DIAGRAMEVENTFILLET_H
#include "diagrameventinterface.h"
#include <QPointer>
#include <QPointF>
class QetShapeItem;
/**
@brief The DiagramEventFillet class
The "add a fillet" tool: rounds the corner between two drawn lines,
the way a sketch fillet does in a CAD program. Click the two lines
one after the other, or once where they meet; a dialog asks for the
radius. The lines are trimmed (or extended) to the points where the
arc touches them, and an arc is added between them -- an ordinary
Ellipse shape with a start and end angle, so nothing new is saved.
*/
class DiagramEventFillet : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventFillet(Diagram *diagram);
~DiagramEventFillet() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
private:
struct Pick {
QetShapeItem *line = nullptr;
QPointF pos; // where the line was clicked, in scene coordinates
};
QList<QetShapeItem *> linesNear(const QPointF &scenePos) const;
void filletLines(const Pick &a, const Pick &b);
void clearFirstPick();
void showHint(const QString &text) const;
void showDefaultHint() const;
Pick m_first;
QPointer<QetShapeItem> m_first_line; // m_first.line, guarded: the line can be deleted between the two clicks
};
#endif // DIAGRAMEVENTFILLET_H
+7
View File
@@ -38,6 +38,7 @@
#endif
#include "diagramevent/diagrameventaddshape.h"
#include "diagramevent/diagrameventaddpath.h"
#include "diagramevent/diagrameventfillet.h"
#include "diagramevent/diagrameventaddtext.h"
#include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h"
@@ -1063,6 +1064,7 @@ void QETDiagramEditor::setUpActions()
QAction *add_arc = m_add_item_actions_group.addAction(QET::Icons::PartArc, tr("Ajouter un arc"));
QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne"));
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe"));
QAction *add_fillet = m_add_item_actions_group.addAction(QET::Icons::DrawFillet, tr("Ajouter un congé"));
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes"));
add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel"));
@@ -1076,6 +1078,7 @@ void QETDiagramEditor::setUpActions()
add_arc ->setStatusTip(tr("Ajoute un arc sur le folio actuel"));
add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel"));
add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel"));
add_fillet ->setStatusTip(tr("Arrondit le coin entre deux lignes du folio actuel"));
add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel"));
add_text ->setData(QStringLiteral("text"));
@@ -1089,6 +1092,7 @@ void QETDiagramEditor::setUpActions()
add_arc ->setData(QStringLiteral("arc"));
add_polyline ->setData(QStringLiteral("polyline"));
add_path ->setData(QStringLiteral("path"));
add_fillet ->setData(QStringLiteral("fillet"));
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
add_text->setCheckable(true);
@@ -1098,6 +1102,7 @@ void QETDiagramEditor::setUpActions()
add_arc->setCheckable(true);
add_polyline->setCheckable(true);
add_path->setCheckable(true);
add_fillet->setCheckable(true);
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
//No default key, but an id: they can then be found by the command
@@ -2049,6 +2054,8 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
{
diagram_event = new DiagramEventAddPath (d);
}
else if (value == "fillet")
diagram_event = new DiagramEventFillet (d);
else if (value == "image")
{
DiagramEventAddImage *deai = new DiagramEventAddImage(d);
+2
View File
@@ -140,6 +140,7 @@ namespace QET {
QIcon Orientations;
QIcon PartArc;
QIcon PartBezier;
QIcon DrawFillet;
QIcon PartCircle;
QIcon PartEllipse;
QIcon PartLine;
@@ -540,6 +541,7 @@ void QET::Icons::initIcons()
Orientations = QIcon::fromTheme("orientations");
PartArc = QIcon::fromTheme("arc");
PartBezier = QIcon::fromTheme("draw-bezier-curves");
DrawFillet = QIcon::fromTheme("draw-fillet");
PartCircle = QIcon::fromTheme("circle");
PartEllipse = QIcon::fromTheme("ellipse");
PartLine = QIcon::fromTheme("line");
+1
View File
@@ -148,6 +148,7 @@ namespace QET {
extern QIcon Orientations;
extern QIcon PartArc;
extern QIcon PartBezier;
extern QIcon DrawFillet;
extern QIcon PartCircle;
extern QIcon PartEllipse;
extern QIcon PartLine;