mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-04 01:44:13 +02:00
Merge pull request #1212 from ispyisail/feature/sketch-fillet
Add a fillet tool to round the corner between two drawn lines
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 ¢re, 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"));
|
||||
}
|
||||
@@ -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
|
||||
@@ -43,6 +43,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"
|
||||
@@ -1148,6 +1149,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"));
|
||||
@@ -1161,6 +1163,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"));
|
||||
@@ -1174,6 +1177,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);
|
||||
@@ -1183,6 +1187,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
|
||||
@@ -2146,6 +2151,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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user