[board] basic drag-n-drop block reservation, blocks must be adjacent (for now)

Dieser Commit ist enthalten in:
Reinder Feenstra 2026-02-11 18:32:05 +01:00
Ursprung a79dfbb5d3
Commit d3738607f5
10 geänderte Dateien mit 318 neuen und 28 gelöschten Zeilen

Datei anzeigen

@ -1,7 +1,6 @@
/**
* client/src/board/boardareawidget.cpp
*
* This file is part of the traintastic source code.
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2020-2025 Reinder Feenstra
*
@ -27,6 +26,7 @@
#include <QtMath>
#include <QApplication>
#include <QToolTip>
#include <QDrag>
#include <traintastic/locale/locale.hpp>
#include "boardwidget.hpp"
#include "getboardcolorscheme.hpp"
@ -367,6 +367,47 @@ TileLocation BoardAreaWidget::pointToTileLocation(const QPoint& p)
return TileLocation{static_cast<int16_t>(p.x() / pxPerTile + boardLeft()), static_cast<int16_t>(p.y() / pxPerTile + boardTop())};
}
QRect BoardAreaWidget::tileRect(int x, int y, int width, int height) const
{
const int pxPerTile = getTileSize() - 1;
return QRect(
(x - boardLeft()) * pxPerTile,
(y - boardTop()) * pxPerTile,
width * pxPerTile,
height * pxPerTile);
}
QRect BoardAreaWidget::tileRect(const Object& tile) const
{
return tileRect(
tile.getPropertyValueInt("x", 0),
tile.getPropertyValueInt("y", 0),
tile.getPropertyValueInt("width", 1),
tile.getPropertyValueInt("heigth", 1));
}
BlockTrainDirection BoardAreaWidget::getBlockTrainDirection(const Object& tile, const QPoint& point) const
{
const auto blockRect = tileRect(tile);
if(blockRect.contains(point))
{
const auto r = tile.getPropertyValueEnum<TileRotate>("rotate", TileRotate::Deg0);
if(r == TileRotate::Deg0)
{
return (point.y() - blockRect.y()) >= (blockRect.height() / 2)
? BlockTrainDirection::TowardsA
: BlockTrainDirection::TowardsB;
}
if(r == TileRotate::Deg90)
{
return (point.x() - blockRect.x()) >= (blockRect.width() / 2)
? BlockTrainDirection::TowardsB
: BlockTrainDirection::TowardsA;
}
}
return BlockTrainDirection::Unknown;
}
QString BoardAreaWidget::getTileToolTip(const TileLocation& l) const
{
const auto tileId = m_board->getTileId(l);
@ -521,6 +562,7 @@ void BoardAreaWidget::mousePressEvent(QMouseEvent* event)
if(event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = true;
m_dragStartPosition = event->pos();
m_mouseLeftButtonPressedTileLocation = pointToTileLocation(event->pos());
}
else if(event->button() == Qt::RightButton)
@ -532,13 +574,23 @@ void BoardAreaWidget::mousePressEvent(QMouseEvent* event)
void BoardAreaWidget::mouseReleaseEvent(QMouseEvent* event)
{
qDebug() << "mouseReleaseEvent";
if(m_mouseLeftButtonPressed && event->button() == Qt::LeftButton)
{
m_mouseLeftButtonPressed = false;
TileLocation tl = pointToTileLocation(event->pos());
if(m_mouseLeftButtonPressedTileLocation == tl) // click
emit tileClicked(tl.x, tl.y);
if(!m_dragStarted)
{
TileLocation tl = pointToTileLocation(event->pos());
if(m_mouseLeftButtonPressedTileLocation == tl) // click
{
emit tileClicked(tl.x, tl.y);
}
}
else
{
m_dragStarted = false;
}
}
else if(m_mouseRightButtonPressed && event->button() == Qt::RightButton)
{
@ -550,6 +602,26 @@ void BoardAreaWidget::mouseReleaseEvent(QMouseEvent* event)
void BoardAreaWidget::mouseMoveEvent(QMouseEvent* event)
{
if(m_dragStarted)
{
return;
}
if(!m_dragStarted && (event->buttons() & Qt::LeftButton) && (event->pos() - m_dragStartPosition).manhattanLength() >= QApplication::startDragDistance())
{
const TileLocation l = pointToTileLocation(m_dragStartPosition);
if(auto tile = m_board->getTileObject(l); tile && m_board->getTileId(l) == TileId::RailBlock)
{
if(const auto direction = getBlockTrainDirection(*tile, m_dragStartPosition); direction != BlockTrainDirection::Unknown) [[likely]]
{
m_dragStarted = true;
auto* drag = new QDrag(this);
drag->setMimeData(new BlockReservePathMimeData(tile->getPropertyValueString("id"), direction));
drag->exec(Qt::LinkAction);
}
}
}
if(hasMouseTracking())
{
const TileLocation tl = pointToTileLocation(event->pos());
@ -868,7 +940,8 @@ void BoardAreaWidget::paintEvent(QPaintEvent* event)
void BoardAreaWidget::dragEnterEvent(QDragEnterEvent *event)
{
if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType))
if(event->mimeData()->hasFormat(BlockReservePathMimeData::mimeType) ||
event->mimeData()->hasFormat(AssignTrainMimeData::mimeType))
{
m_dragMoveTileLocation = TileLocation::invalid;
event->acceptProposedAction();
@ -886,6 +959,12 @@ void BoardAreaWidget::dragMoveEvent(QDragMoveEvent* event)
if(m_dragMoveTileLocation != l)
{
m_dragMoveTileLocation = l;
if(event->mimeData()->hasFormat(BlockReservePathMimeData::mimeType) &&
m_board->getTileId(l) == TileId::RailBlock)
{
// FIXME: block drag start block
return event->accept();
}
if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType) &&
m_board->getTileId(l) == TileId::RailBlock)
{
@ -898,14 +977,46 @@ void BoardAreaWidget::dragMoveEvent(QDragMoveEvent* event)
void BoardAreaWidget::dropEvent(QDropEvent* event)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
const TileLocation l = pointToTileLocation(event->pos());
const auto pos = event->pos();
#else
const TileLocation l = pointToTileLocation(event->position().toPoint());
const auto pos = event->position().toPoint();
#endif
const TileLocation l = pointToTileLocation(pos);
switch(m_board->getTileId(l))
if(m_board->getTileId(l) == TileId::RailBlock)
{
case TileId::RailBlock:
if(event->mimeData()->hasFormat(BlockReservePathMimeData::mimeType))
{
m_mouseLeftButtonPressed = false;
m_dragStarted = false;
if(auto* blockReservePath = dynamic_cast<const BlockReservePathMimeData*>(event->mimeData()))
{
if(auto tile = m_board->getTileObject(l); tile && m_board->getTileId(l) == TileId::RailBlock) [[likely]]
{
if(const auto toDirection = getBlockTrainDirection(*tile, pos); toDirection != BlockTrainDirection::Unknown) [[likely]]
{
const auto [fromBlock, fromDirection] = blockReservePath->values();
const auto toBlock = tile->getPropertyValueString("id");
qDebug() << fromBlock << static_cast<int>(fromDirection) << toBlock << static_cast<int>(toDirection);
(void)callMethodR<bool>(
*m_board->connection(),
"world.train_path_finder.reserve",
[](const bool& /*success*/, std::optional<const Error> /*err*/)
{
},
fromBlock,
fromDirection,
toBlock,
toDirection);
}
}
}
}
else if(event->mimeData()->hasFormat(AssignTrainMimeData::mimeType))
{
if(auto* assignTrain = dynamic_cast<const AssignTrainMimeData*>(event->mimeData()))
{
if(auto tile = std::dynamic_pointer_cast<BlockRailTile>(m_board->getTileObject(l)))
@ -917,10 +1028,7 @@ void BoardAreaWidget::dropEvent(QDropEvent* event)
}
}
}
break;
default:
break;
}
}
event->ignore();
}

Datei anzeigen

@ -1,9 +1,8 @@
/**
* client/src/board/boardareawidget.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2025 Reinder Feenstra
* Copyright (C) 2020-2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -42,6 +41,7 @@
class BoardWidget;
class BlockHighlight;
class Board;
enum class BlockTrainDirection : uint8_t;
class BoardAreaWidget : public QWidget
{
@ -79,6 +79,9 @@ class BoardAreaWidget : public QWidget
bool m_mouseRightButtonPressed;
QPoint m_mouseRightButtonPressedPoint;
QPoint m_dragStartPosition;
bool m_dragStarted = false;
MouseMoveAction m_mouseMoveAction;
TileId m_mouseMoveTileId;
TileLocation m_mouseMoveTileLocation;
@ -107,6 +110,9 @@ class BoardAreaWidget : public QWidget
bool getNXButtonEnabled(const TileLocation& l) const;
bool getNXButtonPressed(const TileLocation& l) const;
TileLocation pointToTileLocation(const QPoint& p);
QRect tileRect(int x, int y, int width, int height) const;
QRect tileRect(const Object& tile) const;
BlockTrainDirection getBlockTrainDirection(const Object& tile, const QPoint& point) const;
QString getTileToolTip(const TileLocation& l) const;
bool event(QEvent* event) final;

Datei anzeigen

@ -1,9 +1,8 @@
/**
* client/src/misc/mimedata.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2024 Reinder Feenstra
* Copyright (C) 2024-2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -24,6 +23,31 @@
#define TRAINTASTIC_CLIENT_MISC_MIMEDATA_HPP
#include <QMimeData>
#include <QIODevice>
#include <traintastic/enum/blocktraindirection.hpp>
class BlockReservePathMimeData : public QMimeData
{
public:
inline static const auto mimeType = QLatin1String("application/vnd.traintastic.block_reserve_path");
explicit BlockReservePathMimeData(const QString& blockId, BlockTrainDirection direction)
{
QByteArray payload;
QDataStream out(&payload, QIODevice::WriteOnly);
out << blockId << static_cast<std::underlying_type_t<BlockTrainDirection>>(direction);
setData(mimeType, payload);
}
inline std::tuple<QString, BlockTrainDirection> values() const
{
QString blockId;
std::underlying_type_t<BlockTrainDirection> directionRaw;
QDataStream in(data(mimeType));
in >> blockId >> directionRaw;
return std::make_tuple(blockId, static_cast<BlockTrainDirection>(directionRaw));
}
};
class AssignTrainMimeData : public QMimeData
{

Datei anzeigen

@ -67,6 +67,7 @@ file(GLOB SOURCES
"src/board/map/*.cpp"
"src/board/nx/*.hpp"
"src/board/nx/*.cpp"
"src/board/pathfinder/*.cpp"
"src/board/tile/*.hpp"
"src/board/tile/*.cpp"
"src/board/tile/hidden/*.cpp"

Datei anzeigen

@ -0,0 +1,36 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_BOARD_PATHFINDER_PATHFINDER_HPP
#define TRAINTASTIC_SERVER_BOARD_PATHFINDER_PATHFINDER_HPP
#include "../../core/subobject.hpp"
class PathFinder : public SubObject
{
protected:
PathFinder(Object& parent_, std::string_view parentPropertyName)
: SubObject(parent_, parentPropertyName)
{
}
};
#endif

Datei anzeigen

@ -0,0 +1,63 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "trainpathfinder.hpp"
#include <traintastic/enum/blocktraindirection.hpp>
#include "../map/blockpath.hpp"
#include "../../core/method.tpp"
#include "../../board/tile/rail/blockrailtile.hpp"
#include "../../train/trainblockstatus.hpp"
TrainPathFinder::TrainPathFinder(Object& parent_, std::string_view parentPropertyName)
: PathFinder(parent_, parentPropertyName)
, reserve{*this, "reserve", MethodFlags::ScriptCallable,
[](const std::shared_ptr<BlockRailTile>& fromBlock, BlockTrainDirection fromDirection, const std::shared_ptr<BlockRailTile>& toBlock, BlockTrainDirection toDirection)
{
if(!fromBlock || fromBlock->trains.empty() || !isKnown(fromDirection) || !toBlock || !isKnown(toDirection)) [[unlikely]]
{
return false;
}
const auto fromSide = (fromDirection == BlockTrainDirection::TowardsA) ? BlockSide::A : BlockSide::B;
const auto toSide = (toDirection == BlockTrainDirection::TowardsB) ? BlockSide::A : BlockSide::B;
// FIXME: for now only support block to block (direct path only)
for(const auto& path : fromBlock->paths())
{
if(path->fromSide() == fromSide && path->toBlock() == toBlock && path->toSide() == toSide)
{
const auto train =
(fromDirection == BlockTrainDirection::TowardsB)
? fromBlock->trains.back()->train.value()
: fromBlock->trains.front()->train.value();
return path->reserve(train);
}
}
return false;
}}
{
m_interfaceItems.add(reserve);
}

Datei anzeigen

@ -0,0 +1,41 @@
/**
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* Copyright (C) 2026 Reinder Feenstra
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_SERVER_BOARD_PATHFINDER_TRAINPATHFINDER_HPP
#define TRAINTASTIC_SERVER_BOARD_PATHFINDER_TRAINPATHFINDER_HPP
#include "pathfinder.hpp"
#include "../../core/method.hpp"
class BlockRailTile;
enum class BlockTrainDirection : uint8_t;
class TrainPathFinder : public PathFinder
{
CLASS_ID("path_finder.train")
public:
Method<bool(const std::shared_ptr<BlockRailTile>&, BlockTrainDirection, const std::shared_ptr<BlockRailTile>&, BlockTrainDirection)> reserve;
TrainPathFinder(Object& parent_, std::string_view parentPropertyName);
};
#endif

Datei anzeigen

@ -63,6 +63,7 @@
#include "../board/list/blockrailtilelist.hpp"
#include "../board/list/linkrailtilelist.hpp"
#include "../board/nx/nxmanager.hpp"
#include "../board/pathfinder/trainpathfinder.hpp"
#include "../board/tile/rail/nxbuttonrailtile.hpp"
#include "../zone/zone.hpp"
@ -143,6 +144,7 @@ void World::init(World& world)
world.blockRailTiles.setValueInternal(std::make_shared<BlockRailTileList>(world, world.blockRailTiles.name()));
world.linkRailTiles.setValueInternal(std::make_shared<LinkRailTileList>(world, world.linkRailTiles.name()));
world.nxManager.setValueInternal(std::make_shared<NXManager>(world, world.nxManager.name()));
world.trainPathFinder.setValueInternal(std::make_shared<TrainPathFinder>(world, world.trainPathFinder.name()));
world.simulationStatus.setValueInternal(std::make_shared<SimulationStatus>(world, world.simulationStatus.name()));
}
@ -202,6 +204,7 @@ World::World(Private /*unused*/) :
blockRailTiles{this, "block_rail_tiles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
linkRailTiles{this, "link_rail_tiles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
nxManager{this, "nx_manager", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
trainPathFinder{this, "train_path_finder", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
statuses(*this, "statuses", {}, PropertyFlags::ReadOnly | PropertyFlags::Store),
hardwareThrottles{this, "hardware_throttles", 0, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::NoScript},
state{this, "state", WorldState(), PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
@ -435,6 +438,8 @@ World::World(Private /*unused*/) :
m_interfaceItems.add(linkRailTiles);
Attributes::addObjectEditor(nxManager, false);
m_interfaceItems.add(nxManager);
Attributes::addObjectEditor(trainPathFinder, false);
m_interfaceItems.add(trainPathFinder);
Attributes::addObjectEditor(statuses, false);
m_interfaceItems.add(statuses);

Datei anzeigen

@ -56,6 +56,7 @@ class ZoneList;
class BlockRailTileList;
class LinkRailTileList;
class NXManager;
class TrainPathFinder;
class Clock;
class ThrottleList;
class TrainList;
@ -150,6 +151,7 @@ class World : public Object
ObjectProperty<BlockRailTileList> blockRailTiles;
ObjectProperty<LinkRailTileList> linkRailTiles;
ObjectProperty<NXManager> nxManager;
ObjectProperty<TrainPathFinder> trainPathFinder;
ObjectVectorProperty<Status> statuses;
Property<uint32_t> hardwareThrottles; //<! number of connected hardware throttles

Datei anzeigen

@ -1,9 +1,8 @@
/**
* shared/src/traintastic/enum/blocktraindirection.hpp
* This file is part of Traintastic,
* see <https://github.com/traintastic/traintastic>.
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2023 Reinder Feenstra
* Copyright (C) 2023-2026 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -56,4 +55,9 @@ constexpr BlockTrainDirection operator !(BlockTrainDirection value)
return (value == BlockTrainDirection::TowardsA) ? BlockTrainDirection::TowardsB : BlockTrainDirection::TowardsA;
}
constexpr bool isKnown(BlockTrainDirection value)
{
return (value == BlockTrainDirection::TowardsA) || (value == BlockTrainDirection::TowardsB);
}
#endif