소스 검색

feat: 增加鼠标画线实时预览

main
suyu 3 주 전
부모
커밋
200e75a536
5개의 변경된 파일364개의 추가작업 그리고 23개의 파일을 삭제
  1. +19
    -0
      app/src/services/logic_editor_service.cpp
  2. +173
    -22
      app/src/ui/logic_editor_widget.cpp
  3. +7
    -1
      app/src/ui/logic_editor_widget.h
  4. +10
    -0
      app/tests/logic_editor_service_tests.cpp
  5. +155
    -0
      app/tests/runtime_panel_controller_tests.cpp

+ 19
- 0
app/src/services/logic_editor_service.cpp 파일 보기

@@ -1559,6 +1559,25 @@ LogicEditorResult LogicEditorService::setWireCells(
}
}

const auto should_change = [this, &logic_id, connected](
const std::pair<std::string, int> &position)
{
const LadderCell *cell = findCell(
logic_id, position.first, position.second);
return cell != nullptr
&& cell->kind != LadderCellKind::Node
&& (connected ? cell->kind != LadderCellKind::Wire
: cell->kind != LadderCellKind::Gap);
};
if (!std::any_of(cells.cbegin(), cells.cend(), should_change))
{
return {
true,
LogicEditorError::None,
connected ? "横线已经存在" : "目标位置没有横线",
cells.front().first};
}

HistoryState before = captureState();
const bool modified_before = project_service_.isModified();
Project &project = project_service_.editProject();


+ 173
- 22
app/src/ui/logic_editor_widget.cpp 파일 보기

@@ -690,6 +690,7 @@ LogicEditorWidget::LogicEditorWidget(
void LogicEditorWidget::setLogicId(const std::string &logic_id)
{
const bool logic_changed = logic_id_ != logic_id;
clearGesture();
logic_id_ = logic_id;
if (logic_changed)
{
@@ -792,6 +793,7 @@ void LogicEditorWidget::setLogicId(const std::string &logic_id)

void LogicEditorWidget::setEditingEnabled(bool enabled)
{
clearGesture();
editing_enabled_ = enabled;
if (!enabled)
{
@@ -806,6 +808,7 @@ void LogicEditorWidget::setEditingEnabled(bool enabled)

void LogicEditorWidget::setMouseWireMode(MouseWireMode mode)
{
clearGesture();
mouse_wire_mode_ = editing_enabled_ ? mode : MouseWireMode::Select;
selection_pressed_ = false;
selection_dragging_ = false;
@@ -1707,14 +1710,21 @@ void LogicEditorWidget::beginGesture(const Hit &hit)
gesture_active_ = true;
gesture_origin_ = hit;
gesture_current_ = hit;
gesture_scene_position_valid_ = false;
viewport()->update();
}

void LogicEditorWidget::updateGesture(const Hit &hit)
void LogicEditorWidget::updateGesture(
const Hit &hit, const QPointF &scene_position)
{
if (gesture_active_ && !hit.rungId.empty())
if (!gesture_active_)
{
gesture_current_ = hit;
return;
}
gesture_current_ = hit;
gesture_scene_position_ = scene_position;
gesture_scene_position_valid_ = true;
viewport()->update();
}

void LogicEditorWidget::finishGesture(const Hit &hit)
@@ -1723,57 +1733,62 @@ void LogicEditorWidget::finishGesture(const Hit &hit)
{
return;
}
const Hit origin = gesture_origin_;
gesture_active_ = false;
if (gesture_origin_.rungId.empty() || hit.rungId.empty())
gesture_origin_ = {};
gesture_current_ = {};
gesture_scene_position_valid_ = false;
viewport()->update();
if (origin.rungId.empty() || hit.rungId.empty())
{
return;
}
LogicEditorResult result;
const bool connected = mouse_wire_mode_ == MouseWireMode::Draw;
if ((gesture_origin_.vertical || gesture_origin_.boundary)
if ((origin.vertical || origin.boundary)
&& (hit.vertical || hit.boundary))
{
if (!connected && gesture_origin_.vertical
&& gesture_origin_.objectId == hit.objectId)
if (!connected && origin.vertical
&& origin.objectId == hit.objectId)
{
result = editor_service_.removeVerticalConnections(
logic_id_, {gesture_origin_.objectId});
logic_id_, {origin.objectId});
}
else if (gesture_origin_.column != hit.column)
else if (origin.column != hit.column)
{
result = {false, LogicEditorError::InvalidOperation,
"竖线拖动必须保持在同一列边界", {}};
}
else
{
std::string first_rung = gesture_origin_.rungId;
std::string first_rung = origin.rungId;
std::string last_rung = hit.rungId;
if (last_rung.empty() && !gesture_origin_.lowerRungId.empty())
if (last_rung.empty() && !origin.lowerRungId.empty())
{
last_rung = gesture_origin_.lowerRungId;
last_rung = origin.lowerRungId;
}
result = editor_service_.setVerticalConnectionRange(
logic_id_, first_rung, last_rung,
gesture_origin_.column, connected);
origin.column, connected);
}
}
else if (gesture_origin_.vertical || hit.vertical)
else if (origin.vertical || hit.vertical)
{
result = {false, LogicEditorError::InvalidOperation,
"请从网格边界开始竖向拖动", {}};
}
else if (gesture_origin_.column == hit.column
&& gesture_origin_.rungId != hit.rungId)
else if (origin.column == hit.column
&& origin.rungId != hit.rungId)
{
result = editor_service_.setVerticalConnectionRange(
logic_id_, gesture_origin_.rungId, hit.rungId,
gesture_origin_.column, connected);
logic_id_, origin.rungId, hit.rungId,
origin.column, connected);
}
else if (gesture_origin_.rungId == hit.rungId)
else if (origin.rungId == hit.rungId)
{
result = editor_service_.setHorizontalWireRange(
logic_id_, gesture_origin_.rungId,
gesture_origin_.column, hit.column, connected);
logic_id_, origin.rungId,
origin.column, hit.column, connected);
}
else
{
@@ -1795,6 +1810,134 @@ void LogicEditorWidget::finishGesture(const Hit &hit)
}
}

void LogicEditorWidget::clearGesture()
{
if (!gesture_active_ && !gesture_scene_position_valid_)
{
return;
}
gesture_active_ = false;
gesture_origin_ = {};
gesture_current_ = {};
gesture_scene_position_ = {};
gesture_scene_position_valid_ = false;
viewport()->update();
}

void LogicEditorWidget::drawGesturePreview(QPainter *painter) const
{
if (!gesture_active_ || painter == nullptr
|| gesture_origin_.rungId.empty())
{
return;
}

enum class PreviewKind { Invalid, Horizontal, Vertical };
PreviewKind kind = PreviewKind::Invalid;
bool valid = false;
const Hit &origin = gesture_origin_;
const Hit &current = gesture_current_;
const bool connected = mouse_wire_mode_ == MouseWireMode::Draw;

if (!current.rungId.empty())
{
if ((origin.vertical || origin.boundary)
&& (current.vertical || current.boundary))
{
kind = PreviewKind::Vertical;
valid = origin.column == current.column
&& (origin.rungId != current.rungId
|| (!connected && origin.vertical
&& origin.objectId == current.objectId));
}
else if (origin.vertical || current.vertical)
{
kind = PreviewKind::Invalid;
}
else if (origin.column == current.column
&& origin.rungId != current.rungId)
{
kind = PreviewKind::Vertical;
valid = origin.column >= 0
&& origin.column <= ProjectLimits::kMaximumConditionColumns;
}
else if (origin.rungId == current.rungId)
{
kind = PreviewKind::Horizontal;
valid = origin.column >= 0 && current.column >= 0
&& origin.column < ProjectLimits::kMaximumConditionColumns
&& current.column < ProjectLimits::kMaximumConditionColumns;
}
}

QColor preview_color = valid
? connected ? QColor(QStringLiteral("#277da1"))
: QColor(QStringLiteral("#c56a1a"))
: kFaultColor;
preview_color.setAlpha(220);
QPen preview_pen(preview_color, valid ? 2.8 : 2.4, Qt::DashLine);
preview_pen.setCosmetic(true);
preview_pen.setCapStyle(Qt::SquareCap);
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(preview_pen);
painter->setBrush(Qt::NoBrush);

if (valid && kind == PreviewKind::Horizontal)
{
const RowLayout *layout = layoutForRung(origin.rungId);
if (layout != nullptr)
{
const int first = std::min(origin.column, current.column);
const int last = std::max(origin.column, current.column);
for (int column = first; column <= last; ++column)
{
const LadderCell *cell = editor_service_.findCell(
logic_id_, origin.rungId, column);
if (cell == nullptr || cell->kind == LadderCellKind::Node
|| (mouse_wire_mode_ == MouseWireMode::Erase
&& cell->kind != LadderCellKind::Wire))
{
continue;
}
const qreal left = kLeftBus
+ static_cast<qreal>(column) * kCellWidth;
painter->drawLine(
QPointF(left, layout->centerY),
QPointF(left + kCellWidth, layout->centerY));
}
}
}
else if (valid && kind == PreviewKind::Vertical)
{
const RowLayout *first = layoutForRung(origin.rungId);
const RowLayout *last = layoutForRung(current.rungId);
if (first != nullptr && last != nullptr)
{
const qreal x = kLeftBus
+ static_cast<qreal>(origin.column) * kCellWidth;
painter->drawLine(
QPointF(x, first->centerY),
QPointF(x, last->centerY));
}
}
else
{
const RowLayout *layout = layoutForRung(origin.rungId);
if (layout != nullptr && gesture_scene_position_valid_)
{
const qreal origin_x = kLeftBus
+ (origin.vertical || origin.boundary
? static_cast<qreal>(origin.column) * kCellWidth
: (static_cast<qreal>(origin.column) + 0.5) * kCellWidth);
painter->drawLine(
QPointF(origin_x, layout->centerY),
gesture_scene_position_);
}
}
painter->restore();
}

void LogicEditorWidget::showCommandEditor(const Hit &hit)
{
if (!editing_enabled_ || hit.rungId.empty()
@@ -2199,7 +2342,8 @@ void LogicEditorWidget::mouseMoveEvent(QMouseEvent *event)
}
else
{
updateGesture(hitAt(mapToScene(event->pos())));
const QPointF scene_position = mapToScene(event->pos());
updateGesture(hitAt(scene_position), scene_position);
}
event->accept();
}
@@ -2242,6 +2386,13 @@ void LogicEditorWidget::mouseDoubleClickEvent(QMouseEvent *event)
event->accept();
}

void LogicEditorWidget::drawForeground(
QPainter *painter, const QRectF &rect)
{
QGraphicsView::drawForeground(painter, rect);
drawGesturePreview(painter);
}

void LogicEditorWidget::resizeEvent(QResizeEvent *event)
{
QGraphicsView::resizeEvent(event);


+ 7
- 1
app/src/ui/logic_editor_widget.h 파일 보기

@@ -17,6 +17,7 @@ class QCompleter;
class QEvent;
class QLineEdit;
class QMouseEvent;
class QPainter;
class QRubberBand;
class QResizeEvent;

@@ -76,6 +77,7 @@ protected:
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void drawForeground(QPainter *painter, const QRectF &rect) override;
void resizeEvent(QResizeEvent *event) override;
void scrollContentsBy(int dx, int dy) override;
bool eventFilter(QObject *watched, QEvent *event) override;
@@ -108,8 +110,10 @@ private:
std::string currentRungId() const;
Hit hitAt(const QPointF &scene_position) const;
void beginGesture(const Hit &hit);
void updateGesture(const Hit &hit);
void updateGesture(const Hit &hit, const QPointF &scene_position);
void finishGesture(const Hit &hit);
void clearGesture();
void drawGesturePreview(QPainter *painter) const;
void showCommandEditor(const Hit &hit);
void commitCommandInput();
void cancelCommandInput();
@@ -162,6 +166,8 @@ private:
bool gesture_active_ = false;
Hit gesture_origin_;
Hit gesture_current_;
QPointF gesture_scene_position_;
bool gesture_scene_position_valid_ = false;
QLineEdit *command_editor_ = nullptr;
QCompleter *command_completer_ = nullptr;
LogicCommandTarget command_target_;


+ 10
- 0
app/tests/logic_editor_service_tests.cpp 파일 보기

@@ -99,6 +99,16 @@ void testContinuousGridAndIndependentHorizontalWires()
"horizontal wires must be persisted independently per cell");
}

fixture.editor.clearHistory();
fixture.projects.restoreModifiedState(false);
const LogicEditorResult duplicate = fixture.editor.setHorizontalWireRange(
fixture.logicId, rung_id, 2, 5, true);
require(
duplicate.succeeded
&& !fixture.projects.isModified()
&& !fixture.editor.canUndo(),
"repeating an existing horizontal wire range must not dirty the project or history");

const LogicEditorResult node = fixture.editor.setConditionAtColumn(
fixture.logicId, rung_id, 3, contact(3), true);
require(node.succeeded, "a wire cell must accept a condition node");


+ 155
- 0
app/tests/runtime_panel_controller_tests.cpp 파일 보기

@@ -685,6 +685,160 @@ void testLadderLayoutAndDragDeletion()
"one undo must restore every object removed by the drag selection");
}

void testWireGesturePreviewKeepsAtomicCommit()
{
TestProjectStorage storage;
ProjectService project_service(storage);
LogicEditorService editor(project_service);
const std::string logic_id = editor.ensureDefaultLogic().id;
const std::string first = editor.addRung(logic_id).id;
const std::string second = editor.addRung(logic_id).id;
editor.clearHistory();
project_service.restoreModifiedState(false);

LogicEditorWidget widget(editor);
widget.setLogicId(logic_id);
widget.setMouseWireMode(LogicEditorWidget::MouseWireMode::Draw);
widget.resize(1200, 360);
widget.show();
QCoreApplication::processEvents(QEventLoop::AllEvents);

constexpr qreal left_bus = 60.0;
constexpr qreal cell_width = 96.0;
constexpr qreal first_row_center = 91.0;
constexpr qreal second_row_center = 197.0;
const QPoint first_cell = widget.mapFromScene(QPointF(
left_bus + cell_width / 2.0, first_row_center));
const QPoint fourth_cell = widget.mapFromScene(QPointF(
left_bus + 3.0 * cell_width + cell_width / 2.0,
first_row_center));
const QPoint invalid_target = widget.mapFromScene(QPointF(
left_bus + 3.0 * cell_width + cell_width / 2.0,
second_row_center));
const auto press = [&widget](const QPoint &point)
{
QMouseEvent event(
QEvent::MouseButtonPress,
QPointF(point),
Qt::LeftButton,
Qt::LeftButton,
Qt::NoModifier);
QApplication::sendEvent(widget.viewport(), &event);
};
const auto move = [&widget](const QPoint &point)
{
QMouseEvent event(
QEvent::MouseMove,
QPointF(point),
Qt::NoButton,
Qt::LeftButton,
Qt::NoModifier);
QApplication::sendEvent(widget.viewport(), &event);
QCoreApplication::processEvents(QEventLoop::AllEvents);
};
const auto release = [&widget](const QPoint &point)
{
QMouseEvent event(
QEvent::MouseButtonRelease,
QPointF(point),
Qt::LeftButton,
Qt::NoButton,
Qt::NoModifier);
QApplication::sendEvent(widget.viewport(), &event);
QCoreApplication::processEvents(QEventLoop::AllEvents);
};
const auto countPreviewPixels = [&widget](bool invalid)
{
const QImage image = widget.viewport()->grab().toImage();
int count = 0;
for (int y = 0; y < image.height(); ++y)
{
for (int x = 0; x < image.width(); ++x)
{
const QColor color = image.pixelColor(x, y);
const bool matches = invalid
? color.red() > color.green() + 45
&& color.red() > color.blue() + 25
: color.blue() > color.red() + 50
&& color.blue() > color.green() + 20;
count += matches ? 1 : 0;
}
}
return count;
};

QString error_message;
QObject::connect(
&widget,
&LogicEditorWidget::editorError,
[&error_message](const QString &message) { error_message = message; });

press(first_cell);
move(fourth_cell);
require(countPreviewPixels(false) > 20,
"a legal drag must paint a visible temporary wire preview");
const LadderRung *first_rung = editor.findRung(logic_id, first);
require(
std::all_of(
first_rung->cells.cbegin(),
first_rung->cells.cend(),
[](const LadderCell &cell)
{
return cell.kind == LadderCellKind::Gap;
})
&& !project_service.isModified()
&& !editor.canUndo(),
"the temporary preview must not modify the project or history");

move(invalid_target);
require(countPreviewPixels(true) > 20,
"an invalid drag direction must switch the preview to an error color");
release(invalid_target);
first_rung = editor.findRung(logic_id, first);
require(
!error_message.isEmpty()
&& std::all_of(
first_rung->cells.cbegin(),
first_rung->cells.cend(),
[](const LadderCell &cell)
{
return cell.kind == LadderCellKind::Gap;
})
&& editor.findRung(logic_id, second) != nullptr
&& !project_service.isModified()
&& !editor.canUndo(),
"releasing an invalid gesture must discard its complete preview");

error_message.clear();
press(first_cell);
move(fourth_cell);
require(!project_service.isModified() && !editor.canUndo(),
"a legal gesture must remain temporary until mouse release");
release(fourth_cell);
first_rung = editor.findRung(logic_id, first);
require(
error_message.isEmpty()
&& first_rung->cells[0].kind == LadderCellKind::Wire
&& first_rung->cells[1].kind == LadderCellKind::Wire
&& first_rung->cells[2].kind == LadderCellKind::Wire
&& first_rung->cells[3].kind == LadderCellKind::Wire
&& project_service.isModified()
&& editor.canUndo(),
"releasing a legal gesture must commit its complete wire range once");
require(editor.undo().succeeded && !editor.canUndo(),
"one undo must remove the complete committed gesture");
first_rung = editor.findRung(logic_id, first);
require(
std::all_of(
first_rung->cells.cbegin(),
first_rung->cells.cend(),
[](const LadderCell &cell)
{
return cell.kind == LadderCellKind::Gap;
}),
"undo must restore every cell changed by the gesture");
}

void testCursorAdvanceAndInlineCommandInput()
{
TestProjectStorage storage;
@@ -1077,6 +1231,7 @@ int main(int argc, char *argv[])
testQueuedOfflineTraceIsIgnoredAfterReturningToEditing();
testLogicEditorGridSelectionAndDeletion();
testLadderLayoutAndDragDeletion();
testWireGesturePreviewKeepsAtomicCommit();
testCursorAdvanceAndInlineCommandInput();
testVerticalWireShortcutAdvancesDownward();
testSegmentLevelTraceProjection();


불러오는 중...
취소
저장