Add in new tool folder & first tool, TagEditor
This commit is contained in:
@@ -33,6 +33,7 @@ include(toml)
|
||||
add_subdirectory(IDHAN)
|
||||
add_subdirectory(IDHANServer)
|
||||
add_subdirectory(IDHANClient)
|
||||
add_subdirectory(tools/TagEditor)
|
||||
add_subdirectory(docs)
|
||||
|
||||
|
||||
|
||||
11
tools/TagEditor/CMakeLists.txt
Normal file
11
tools/TagEditor/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Network Concurrent Widgets)
|
||||
AddFGLExecutable(TagEditor ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
|
||||
file(GLOB_RECURSE UI_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.ui")
|
||||
target_sources(TagEditor PRIVATE ${UI_FILES})
|
||||
|
||||
target_link_libraries(TagEditor PUBLIC Qt6::Core Qt6::Widgets Qt6::Concurrent IDHANClient)
|
||||
74
tools/TagEditor/src/GraphWidget.cpp
Normal file
74
tools/TagEditor/src/GraphWidget.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
#include "GraphWidget.hpp"
|
||||
|
||||
#include <qgraphicsitem.h>
|
||||
|
||||
#include "TagNode.hpp"
|
||||
|
||||
GraphWidget::GraphWidget( QWidget* parent ) : QGraphicsView( parent )
|
||||
{
|
||||
QGraphicsScene* scene { new QGraphicsScene( this ) };
|
||||
scene->setItemIndexMethod( QGraphicsScene::NoIndex );
|
||||
scene->setSceneRect( -200, -200, 400, 400 );
|
||||
setScene( scene );
|
||||
setCacheMode( CacheBackground );
|
||||
setViewportUpdateMode( BoundingRectViewportUpdate );
|
||||
setRenderHint( QPainter::Antialiasing );
|
||||
setTransformationAnchor( AnchorUnderMouse );
|
||||
setDragMode( ScrollHandDrag );
|
||||
}
|
||||
|
||||
void GraphWidget::itemMoved()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
if ( !timer.isActive() )
|
||||
{
|
||||
timer.start( 1000ms / 25, this );
|
||||
}
|
||||
}
|
||||
|
||||
void GraphWidget::setRootNode( TagNode* node )
|
||||
{
|
||||
m_tag_nodes.clear();
|
||||
scene()->clear();
|
||||
|
||||
scene()->addItem( node );
|
||||
m_tag_nodes.insert_or_assign( node->tagID(), node );
|
||||
|
||||
node->spawnRelationships();
|
||||
}
|
||||
|
||||
void GraphWidget::addRelationship( TagNode* left, TagNode* right, TagRelationship::RelationshipType relationship )
|
||||
{
|
||||
this->scene()->addItem( new TagRelationship( left, right, relationship ) );
|
||||
|
||||
itemMoved();
|
||||
}
|
||||
|
||||
void GraphWidget::drawBackground( QPainter* painter, const QRectF& rect )
|
||||
{
|
||||
QGraphicsView::drawBackground( painter, rect );
|
||||
}
|
||||
|
||||
void GraphWidget::timerEvent( [[maybe_unused]] QTimerEvent* event )
|
||||
{
|
||||
const QList< QGraphicsItem* > items { scene()->items() };
|
||||
for ( QGraphicsItem* item : items )
|
||||
{
|
||||
TagNode* node { qgraphicsitem_cast< TagNode* >( item ) };
|
||||
|
||||
if ( node ) node->calculateForces();
|
||||
}
|
||||
|
||||
bool items_moved { false };
|
||||
|
||||
for ( const auto item : items )
|
||||
{
|
||||
TagNode* node { qgraphicsitem_cast< TagNode* >( item ) };
|
||||
if ( node ) items_moved |= node->advancePosition();
|
||||
}
|
||||
|
||||
if ( !items_moved ) timer.stop();
|
||||
}
|
||||
43
tools/TagEditor/src/GraphWidget.hpp
Normal file
43
tools/TagEditor/src/GraphWidget.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
#pragma once
|
||||
#include <QGraphicsView>
|
||||
#include <QWidget>
|
||||
|
||||
#include <qbasictimer.h>
|
||||
|
||||
#include "IDHANTypes.hpp"
|
||||
#include "TagRelationship.hpp"
|
||||
|
||||
class TagNode;
|
||||
|
||||
class GraphWidget final : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
GraphWidget( QWidget* parent = nullptr );
|
||||
|
||||
void itemMoved();
|
||||
|
||||
std::unordered_map< idhan::TagID, TagNode* > m_tag_nodes {};
|
||||
QBasicTimer timer;
|
||||
|
||||
public slots:
|
||||
// void zoomIn();
|
||||
// void zoomOut();
|
||||
|
||||
public:
|
||||
|
||||
void setRootNode( TagNode* node );
|
||||
void addRelationship( TagNode* left, TagNode* right, TagRelationship::RelationshipType relationship );
|
||||
|
||||
protected:
|
||||
|
||||
void drawBackground( QPainter* painter, const QRectF& rect ) override;
|
||||
|
||||
private slots:
|
||||
void timerEvent( QTimerEvent* event ) override;
|
||||
};
|
||||
346
tools/TagEditor/src/TagNode.cpp
Normal file
346
tools/TagEditor/src/TagNode.cpp
Normal file
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
#include "TagNode.hpp"
|
||||
|
||||
#include <moc_TagNode.cpp>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "TagRelationship.hpp"
|
||||
|
||||
TagNode::TagNode( GraphWidget* widget, const idhan::TagID tag_id ) :
|
||||
m_id( tag_id ),
|
||||
tag_text( "loading..." ),
|
||||
graph( widget )
|
||||
{
|
||||
FGL_ASSERT( graph, "Graph was nullptr!" );
|
||||
|
||||
setFlag( ItemIsMovable );
|
||||
setFlag( ItemSendsGeometryChanges );
|
||||
setCacheMode( DeviceCoordinateCache );
|
||||
setZValue( -1 );
|
||||
|
||||
connect( this, &TagNode::triggerSpawnRelated, this, &TagNode::getRelatedInfo );
|
||||
connect( this, &TagNode::triggerGatherName, this, &TagNode::gatherName );
|
||||
connect(
|
||||
&m_relationshipinfo_watcher, &decltype( m_relationshipinfo_watcher )::finished, this, &TagNode::spawnRelated );
|
||||
|
||||
// spawnRelationships();
|
||||
emit triggerGatherName();
|
||||
}
|
||||
|
||||
void TagNode::spawnRelationships()
|
||||
{
|
||||
if ( m_relationships_spawned ) return;
|
||||
m_relationships_spawned = true;
|
||||
emit triggerSpawnRelated();
|
||||
}
|
||||
|
||||
void TagNode::addRelationship( TagRelationship* relationship )
|
||||
{
|
||||
m_relationships.push_back( relationship );
|
||||
}
|
||||
|
||||
double modifier( const double x, const double a )
|
||||
{
|
||||
constexpr double clamp { 50.0 };
|
||||
constexpr double div { 30.0 };
|
||||
return std::clamp< double >( std::pow( x - a, 5.0 ) / div, -clamp, clamp );
|
||||
}
|
||||
|
||||
void TagNode::calculateForces()
|
||||
{
|
||||
QVector2D force_direction { 0, 0 };
|
||||
qreal largest_dist { 0.0f };
|
||||
|
||||
FGL_ASSERT( graph, "Graph was nullptr!" );
|
||||
|
||||
const QList< QGraphicsItem* > items = graph->scene()->items();
|
||||
|
||||
const auto cur_pos { this->pos() };
|
||||
if ( std::isnan( cur_pos.x() ) || std::isnan( cur_pos.y() ) )
|
||||
{
|
||||
new_pos = { 0.0f, 0.0f };
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
for ( const QGraphicsItem* item : items )
|
||||
{
|
||||
const TagRelationship* relationship { qgraphicsitem_cast< const TagRelationship* >( item ) };
|
||||
if ( !relationship ) continue;
|
||||
|
||||
// the left node is, children, older_siblings, and aliased_tags
|
||||
if ( relationship->leftNode() != this ) continue;
|
||||
|
||||
// Children are pulled toward parents,
|
||||
// Older siblings are pulled toward younger siblings (TOOD: Reverse this)
|
||||
// Aliased tags are pulled closer to their alias
|
||||
|
||||
const TagNode* other { relationship->rightNode() };
|
||||
}
|
||||
*/
|
||||
|
||||
// Push nodes away from each other
|
||||
for ( const QGraphicsItem* item : items )
|
||||
{
|
||||
const TagNode* other = qgraphicsitem_cast< const TagNode* >( item );
|
||||
if ( !other || other == this ) continue;
|
||||
|
||||
const QVector2D vec_to_target { other->pos() - pos() };
|
||||
QVector2D vec_to_target_normalized { vec_to_target.normalized() };
|
||||
qreal distance = vec_to_target.length();
|
||||
|
||||
bool has_relationship { false };
|
||||
for ( const auto& relationship : std::as_const( m_relationships ) )
|
||||
{
|
||||
if ( relationship->rightNode() == other )
|
||||
{
|
||||
has_relationship = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We have a relationship with this node, So we should only apply relationship weights.
|
||||
if ( has_relationship ) continue;
|
||||
|
||||
if ( std::isnan( distance ) || std::isinf( distance ) ) continue;
|
||||
|
||||
if ( distance < 1.0f )
|
||||
{
|
||||
// If the nodes are right on top of eachother, move them.
|
||||
new_pos = pos() + QPointF( rand() % 50, rand() % 50 );
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr double range { 80 }; // leeway around the target
|
||||
constexpr double target { 300 }; // target
|
||||
constexpr double offset { target / range };
|
||||
|
||||
//remap the distance from [-200,200] (-offset,offset) to [-1,1]
|
||||
const double normalized_distance = ( distance / range );
|
||||
|
||||
const auto mod { modifier( normalized_distance, offset ) };
|
||||
|
||||
if ( mod > 0.0f ) continue;
|
||||
|
||||
vec_to_target_normalized *= ( mod );
|
||||
|
||||
force_direction += vec_to_target_normalized;
|
||||
}
|
||||
/*
|
||||
// Add constant force pulling toward center
|
||||
constexpr double center_pull { 0.5 };
|
||||
QVector2D to_center { -pos() };
|
||||
if ( to_center.length() > 1.0 )
|
||||
{
|
||||
to_center.normalize();
|
||||
force_direction += to_center * center_pull;
|
||||
}
|
||||
*/
|
||||
|
||||
for ( const QGraphicsItem* item : items )
|
||||
{
|
||||
const TagRelationship* relationship { qgraphicsitem_cast< const TagRelationship* >( item ) };
|
||||
if ( !relationship ) continue;
|
||||
|
||||
// the left node is, children, older_siblings, and aliased_tags
|
||||
if ( relationship->leftNode() != this ) continue;
|
||||
|
||||
// Children are pulled toward parents,
|
||||
// Older siblings are pulled toward younger siblings (TOOD: Reverse this)
|
||||
// Aliased tags are pulled closer to their alias
|
||||
|
||||
const TagNode* other { relationship->rightNode() };
|
||||
|
||||
QVector2D target_vector { other->pos() - pos() };
|
||||
const auto distance { target_vector.length() };
|
||||
|
||||
target_vector.normalize();
|
||||
|
||||
constexpr double range { 80 }; // leeway around the target
|
||||
constexpr double target { 300 }; // target
|
||||
constexpr double offset { target / range };
|
||||
const double normalized_distance { distance / range };
|
||||
|
||||
const auto mod { modifier( normalized_distance, offset ) };
|
||||
|
||||
// Pull toward parent, older sibling, alias
|
||||
force_direction += target_vector * mod;
|
||||
}
|
||||
|
||||
auto xvel { force_direction.x() };
|
||||
auto yvel { force_direction.y() };
|
||||
|
||||
// Limit maximum velocity
|
||||
// if ( qAbs( xvel ) < 0.01 ) xvel = 0;
|
||||
// if ( qAbs( yvel ) < 0.01 ) yvel = 0;
|
||||
|
||||
new_pos = pos() + QPointF( xvel, yvel );
|
||||
}
|
||||
|
||||
bool TagNode::advancePosition()
|
||||
{
|
||||
if ( new_pos == pos() ) return false;
|
||||
|
||||
setPos( new_pos );
|
||||
return true;
|
||||
}
|
||||
|
||||
const QFont FONT { "Arial", 12 };
|
||||
|
||||
QRectF TagNode::boundingRect() const
|
||||
{
|
||||
// The bounding rect for a tag node should be the text of the tag
|
||||
const QFontMetrics metrics( FONT );
|
||||
QRectF bounds { metrics.boundingRect( tag_text ) };
|
||||
|
||||
// Add some padding around the text
|
||||
bounds.adjust( -10, -10, 10, 10 );
|
||||
return bounds;
|
||||
}
|
||||
|
||||
QPainterPath TagNode::shape() const
|
||||
{
|
||||
return QGraphicsItem::shape();
|
||||
}
|
||||
|
||||
void TagNode::paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget )
|
||||
{
|
||||
QRectF bounds = boundingRect();
|
||||
|
||||
// Draw rectangle
|
||||
painter->setPen( QPen( Qt::black, 2 ) );
|
||||
painter->setBrush( QBrush( Qt::white ) );
|
||||
painter->drawRect( bounds );
|
||||
|
||||
// Draw text
|
||||
painter->setPen( Qt::black );
|
||||
painter->setFont( FONT );
|
||||
painter->drawText( bounds, Qt::AlignCenter, tag_text );
|
||||
}
|
||||
|
||||
TagNode* checkNodes( std::unordered_map< idhan::TagID, TagNode* >& nodes, const idhan::TagID& id, GraphWidget* graph )
|
||||
{
|
||||
if ( auto itter = nodes.find( id ); itter != nodes.end() ) return itter->second;
|
||||
|
||||
// Node doesn't exist.
|
||||
idhan::logging::info( "Spawning node {}", id );
|
||||
TagNode* other { new TagNode( graph, id ) };
|
||||
nodes.insert_or_assign( id, other );
|
||||
graph->scene()->addItem( other );
|
||||
|
||||
return other;
|
||||
}
|
||||
|
||||
void TagNode::spawnRelated()
|
||||
{
|
||||
const auto& future { m_relationshipinfo_watcher.future() };
|
||||
|
||||
if ( future.isFinished() )
|
||||
{
|
||||
const idhan::TagRelationshipInfo& info { future.result() };
|
||||
|
||||
const auto& [ aliased, aliases, parents, children, older_siblings, younger_siblings ] = info;
|
||||
|
||||
auto& existing_nodes { graph->m_tag_nodes };
|
||||
|
||||
for ( const auto& aliased_id : aliased )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, aliased_id, graph ) };
|
||||
graph->addRelationship( node, this, TagRelationship::Aliased );
|
||||
node->spawnRelationships();
|
||||
}
|
||||
|
||||
for ( const auto& alias : aliases )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, alias, graph ) };
|
||||
graph->addRelationship( this, node, TagRelationship::Aliased );
|
||||
node->spawnRelationships();
|
||||
}
|
||||
|
||||
/*
|
||||
for ( const auto& parent_id : parents )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, parent_id, graph ) };
|
||||
graph->addRelationship( node, this, TagRelationship::ParentChild );
|
||||
// node->spawnRelationships();
|
||||
}
|
||||
|
||||
for ( const auto& child_id : children )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, child_id, graph ) };
|
||||
graph->addRelationship( this, node, TagRelationship::ParentChild );
|
||||
// node->spawnRelationships();
|
||||
}
|
||||
|
||||
for ( const auto& older_sibling_id : older_siblings )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, older_sibling_id, graph ) };
|
||||
graph->addRelationship( node, this, TagRelationship::Sibling );
|
||||
// node->spawnRelationships();
|
||||
}
|
||||
|
||||
for ( const auto& younger_sibling_id : younger_siblings )
|
||||
{
|
||||
auto* node { checkNodes( existing_nodes, younger_sibling_id, graph ) };
|
||||
graph->addRelationship( this, node, TagRelationship::Sibling );
|
||||
// node->spawnRelationships();
|
||||
}
|
||||
*/
|
||||
|
||||
// add node to graph
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error( "Huh?" );
|
||||
}
|
||||
}
|
||||
|
||||
void TagNode::getRelatedInfo()
|
||||
{
|
||||
const auto m_domain { 5 };
|
||||
auto future { idhan::IDHANClient::instance().getTagRelationships( m_id, m_domain ) };
|
||||
|
||||
m_relationshipinfo_watcher.setFuture( future );
|
||||
}
|
||||
|
||||
void TagNode::gatherName()
|
||||
{
|
||||
m_taginfo_watcher.setFuture( idhan::IDHANClient::instance().getTagInfo( m_id ) );
|
||||
|
||||
connect(
|
||||
&m_taginfo_watcher,
|
||||
&decltype( m_taginfo_watcher )::finished,
|
||||
this,
|
||||
&TagNode::gotName,
|
||||
Qt::SingleShotConnection );
|
||||
}
|
||||
|
||||
void TagNode::gotName()
|
||||
{
|
||||
const auto result { m_taginfo_watcher.future().result() };
|
||||
this->tag_text = QString::number( m_id ) + ": " + result.toQString();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QVariant TagNode::itemChange( GraphicsItemChange change, const QVariant& value )
|
||||
{
|
||||
switch ( change )
|
||||
{
|
||||
case ItemPositionHasChanged:
|
||||
{
|
||||
for ( const auto relationship : std::as_const( m_relationships ) )
|
||||
{
|
||||
relationship->adjust();
|
||||
}
|
||||
graph->itemMoved();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return QGraphicsItem::itemChange( change, value );
|
||||
}
|
||||
78
tools/TagEditor/src/TagNode.hpp
Normal file
78
tools/TagEditor/src/TagNode.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <QFutureWatcher>
|
||||
|
||||
#include "GraphWidget.hpp"
|
||||
#include "TagRelationship.hpp"
|
||||
#include "idhan/IDHANClient.hpp"
|
||||
|
||||
class TagNode final : public QObject, public QGraphicsItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES( QGraphicsItem )
|
||||
|
||||
idhan::TagID m_id;
|
||||
QString tag_text;
|
||||
QList< TagRelationship* > m_relationships {};
|
||||
QFutureWatcher< idhan::TagRelationshipInfo > m_relationshipinfo_watcher;
|
||||
QFutureWatcher< idhan::TagInfo > m_taginfo_watcher;
|
||||
bool m_relationships_spawned { false };
|
||||
|
||||
std::vector< TagNode* > left_siblings {};
|
||||
std::vector< TagNode* > right_siblings {};
|
||||
|
||||
std::vector< TagNode* > children {};
|
||||
std::vector< TagNode* > parents {};
|
||||
|
||||
std::vector< TagNode* > aliased_tags {};
|
||||
std::vector< TagNode* > ideal_tags {};
|
||||
|
||||
public:
|
||||
|
||||
TagNode( GraphWidget* widget, idhan::TagID tag_id );
|
||||
void spawnRelationships();
|
||||
|
||||
void addRelationship( TagRelationship* relationship );
|
||||
|
||||
QList< TagRelationship* > edges() const { return m_relationships; }
|
||||
|
||||
enum
|
||||
{
|
||||
Type = UserType + 1
|
||||
};
|
||||
|
||||
int type() const override { return Type; }
|
||||
|
||||
void calculateForces();
|
||||
bool advancePosition();
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
QPainterPath shape() const override;
|
||||
void paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget ) override;
|
||||
|
||||
virtual ~TagNode() {}
|
||||
|
||||
idhan::TagID tagID() const { return m_id; }
|
||||
|
||||
signals:
|
||||
void triggerSpawnRelated();
|
||||
void triggerGatherName();
|
||||
|
||||
private slots:
|
||||
void spawnRelated();
|
||||
void getRelatedInfo();
|
||||
void gatherName();
|
||||
void gotName();
|
||||
|
||||
protected:
|
||||
|
||||
QVariant itemChange( GraphicsItemChange change, const QVariant& value ) override;
|
||||
|
||||
private:
|
||||
|
||||
QPointF new_pos;
|
||||
GraphWidget* graph;
|
||||
};
|
||||
151
tools/TagEditor/src/TagRelationship.cpp
Normal file
151
tools/TagEditor/src/TagRelationship.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// Created by kj16609 on 5/3/25.
|
||||
//
|
||||
#include "TagRelationship.hpp"
|
||||
|
||||
#include "TagNode.hpp"
|
||||
|
||||
TagRelationship::TagRelationship( TagNode* left, TagNode* right, const RelationshipType relationship ) :
|
||||
m_relationship( relationship ),
|
||||
m_left( left ),
|
||||
m_right( right )
|
||||
{
|
||||
setZValue( -1 ); // Draw behind nodes
|
||||
m_left->addRelationship( this );
|
||||
m_right->addRelationship( this );
|
||||
adjust();
|
||||
}
|
||||
|
||||
QPointF intersects( const QRectF& rect, const QLineF& line )
|
||||
{
|
||||
const auto top_left { rect.topLeft() };
|
||||
const auto bottom_right { rect.bottomRight() };
|
||||
|
||||
const auto top_right { QPointF( bottom_right.x(), top_left.y() ) };
|
||||
const auto bottom_left { QPointF( top_left.x(), bottom_right.y() ) };
|
||||
|
||||
const QLineF top_line { top_left, top_right };
|
||||
const QLineF bottom_line { bottom_left, bottom_right };
|
||||
const QLineF left_line { top_left, bottom_left };
|
||||
const QLineF right_line { top_right, bottom_right };
|
||||
|
||||
QPointF closest_intersection { line.p2() };
|
||||
QPointF intersection { line.p2() };
|
||||
|
||||
const auto isCloser = [ &line, &closest_intersection ]( QPointF point )
|
||||
{
|
||||
const QLineF line_to_point { line.p1(), point };
|
||||
const QLineF line_to_prev_point { line.p1(), closest_intersection };
|
||||
|
||||
return line_to_point.length() < line_to_prev_point.length();
|
||||
};
|
||||
|
||||
if ( line.intersects( top_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) )
|
||||
closest_intersection = intersection;
|
||||
|
||||
if ( line.intersects( bottom_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) )
|
||||
closest_intersection = intersection;
|
||||
|
||||
if ( line.intersects( left_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) )
|
||||
closest_intersection = intersection;
|
||||
|
||||
if ( line.intersects( right_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) )
|
||||
closest_intersection = intersection;
|
||||
|
||||
return closest_intersection;
|
||||
}
|
||||
|
||||
void TagRelationship::adjust()
|
||||
{
|
||||
const QLineF line { mapFromItem( m_left, 0, 0 ), mapFromItem( m_right, 0, 0 ) };
|
||||
const qreal length { line.length() };
|
||||
|
||||
const auto left_bounds { m_left->sceneBoundingRect() };
|
||||
const auto right_bounds { m_right->sceneBoundingRect() };
|
||||
|
||||
prepareGeometryChange();
|
||||
|
||||
if ( length > qreal( 20.0 ) )
|
||||
{
|
||||
QPointF edge_offset { ( line.dx() * 10 ) / length, ( line.dy() * 10 ) / length };
|
||||
// source_point = line.p1() + edge_offset;
|
||||
// target_point = line.p2() - edge_offset;
|
||||
|
||||
// source_point = intersects( left_bounds, line );
|
||||
source_point = intersects( left_bounds, line );
|
||||
target_point = intersects( right_bounds, line );
|
||||
}
|
||||
else
|
||||
{
|
||||
source_point = line.p1();
|
||||
target_point = line.p1();
|
||||
}
|
||||
}
|
||||
|
||||
QRectF TagRelationship::boundingRect() const
|
||||
{
|
||||
constexpr qreal pen_width { 1.0 };
|
||||
const qreal extra { ( pen_width + arrow_size ) / 2.0f };
|
||||
|
||||
return QRectF( source_point, QSizeF( target_point.x() - source_point.x(), target_point.y() - source_point.y() ) )
|
||||
.normalized()
|
||||
.adjusted( -extra, -extra, extra, extra );
|
||||
}
|
||||
|
||||
void TagRelationship::paint( QPainter* painter, const QStyleOptionGraphicsItem*, QWidget* )
|
||||
{
|
||||
static constexpr qreal arrow_size { 10.0 };
|
||||
static constexpr qreal node_offset { 10.0 };
|
||||
QLineF line { source_point, target_point };
|
||||
if ( qFuzzyCompare( line.length(), qreal( 0.0 ) ) ) return;
|
||||
|
||||
const QPen alias_pen { Qt::white, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin };
|
||||
const QPen parent_pen { Qt::blue, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin };
|
||||
const QPen sibling_pen { Qt::red, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin };
|
||||
const QPen outline_pen { Qt::black, 3, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin };
|
||||
|
||||
// Calculate offset target point
|
||||
const double angle { std::atan2( -line.dy(), line.dx() ) };
|
||||
const QPointF offset_target { target_point
|
||||
- QPointF( std::cos( angle ) * node_offset, -std::sin( angle ) * node_offset ) };
|
||||
QLineF offset_line { source_point, offset_target };
|
||||
|
||||
// Draw black outline
|
||||
painter->setPen( outline_pen );
|
||||
painter->drawLine( offset_line );
|
||||
|
||||
switch ( m_relationship )
|
||||
{
|
||||
case ParentChild:
|
||||
painter->setPen( parent_pen );
|
||||
break;
|
||||
case Sibling:
|
||||
painter->setPen( sibling_pen );
|
||||
break;
|
||||
default:
|
||||
[[fallthrough]]
|
||||
case Aliased:
|
||||
painter->setPen( alias_pen );
|
||||
break;
|
||||
}
|
||||
|
||||
painter->drawLine( offset_line );
|
||||
|
||||
QPointF sourceArrowP1 { source_point
|
||||
+ QPointF( sin( angle + M_PI / 3 ) * arrow_size, cos( angle + M_PI / 3 ) * arrow_size ) };
|
||||
QPointF sourceArrowP2 {
|
||||
source_point
|
||||
+ QPointF( sin( angle + M_PI - M_PI / 3 ) * arrow_size, cos( angle + M_PI - M_PI / 3 ) * arrow_size )
|
||||
};
|
||||
|
||||
QPointF destArrowP1 { target_point
|
||||
+ QPointF( sin( angle - M_PI / 3 ) * arrow_size, cos( angle - M_PI / 3 ) * arrow_size ) };
|
||||
QPointF destArrowP2 {
|
||||
target_point
|
||||
+ QPointF( sin( angle - M_PI + M_PI / 3 ) * arrow_size, cos( angle - M_PI + M_PI / 3 ) * arrow_size )
|
||||
};
|
||||
|
||||
painter->setBrush( Qt::black );
|
||||
// painter->drawPolygon( QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2 );
|
||||
painter->drawPolygon( QPolygonF() << line.p2() << destArrowP1 << destArrowP2 );
|
||||
}
|
||||
68
tools/TagEditor/src/TagRelationship.hpp
Normal file
68
tools/TagEditor/src/TagRelationship.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Created by kj16609 on 5/3/25.
|
||||
//
|
||||
#pragma once
|
||||
#include <QGraphicsItem>
|
||||
|
||||
class TagNode;
|
||||
|
||||
class TagRelationship : public QObject, public QGraphicsItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES( QGraphicsItem )
|
||||
|
||||
public:
|
||||
|
||||
enum class RelationshipType
|
||||
{
|
||||
ParentChild,
|
||||
Sibling,
|
||||
Aliased
|
||||
} m_relationship;
|
||||
|
||||
using enum RelationshipType;
|
||||
|
||||
private:
|
||||
|
||||
// Parent/OlderSibling/AliasedTag
|
||||
union
|
||||
{
|
||||
TagNode* m_left;
|
||||
TagNode* child;
|
||||
TagNode* older_sibling;
|
||||
TagNode* aliased_tag;
|
||||
};
|
||||
|
||||
// Child/YoungerSibling/IdealTag
|
||||
union
|
||||
{
|
||||
TagNode* m_right;
|
||||
TagNode* parent;
|
||||
TagNode* younger_sibling;
|
||||
TagNode* ideal_tag;
|
||||
};
|
||||
|
||||
QPointF source_point;
|
||||
|
||||
QPointF target_point;
|
||||
qreal arrow_size { 10 };
|
||||
|
||||
public:
|
||||
|
||||
TagRelationship( TagNode* left, TagNode* right, RelationshipType relationship );
|
||||
|
||||
TagNode* leftNode() const { return m_left; }
|
||||
|
||||
TagNode* rightNode() const { return m_right; }
|
||||
|
||||
void adjust();
|
||||
QRectF boundingRect() const override;
|
||||
void paint( QPainter* painter, const QStyleOptionGraphicsItem*, QWidget* ) override;
|
||||
|
||||
enum
|
||||
{
|
||||
Type = UserType + 2
|
||||
};
|
||||
|
||||
int type() const override { return Type; }
|
||||
};
|
||||
18
tools/TagEditor/src/main.cpp
Normal file
18
tools/TagEditor/src/main.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include "ui/MainWindow.hpp"
|
||||
|
||||
int main( int argc, char** argv )
|
||||
{
|
||||
QApplication app { argc, argv };
|
||||
|
||||
MainWindow window;
|
||||
|
||||
window.show();
|
||||
|
||||
app.exec();
|
||||
}
|
||||
89
tools/TagEditor/src/ui/MainWindow.cpp
Normal file
89
tools/TagEditor/src/ui/MainWindow.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
// You may need to build the project (run Qt uic code generator) to get "ui_MainWindow.h" resolved
|
||||
|
||||
#include "MainWindow.hpp"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QListView>
|
||||
#include <QStringListModel>
|
||||
|
||||
#include "TagNode.hpp"
|
||||
#include "ui_MainWindow.h"
|
||||
|
||||
MainWindow::MainWindow( QWidget* parent ) : QMainWindow( parent ), ui( new Ui::MainWindow )
|
||||
{
|
||||
ui->setupUi( this );
|
||||
|
||||
// TagNode* node { new TagNode( ui->graphicsView ) };
|
||||
|
||||
// ui->graphicsView->scene()->addItem( node );
|
||||
ui->tagSearch->setCompleter( &m_completer );
|
||||
m_completer.setCaseSensitivity( Qt::CaseInsensitive );
|
||||
m_completer.setCompletionMode( QCompleter::UnfilteredPopupCompletion );
|
||||
m_completer.setModel( m_model.get() );
|
||||
|
||||
connect(
|
||||
&m_autocomplete_watcher,
|
||||
&QFutureWatcher< std::vector< std::pair< idhan::TagID, std::string > > >::finished,
|
||||
this,
|
||||
&MainWindow::autocompleteFinished );
|
||||
|
||||
connect(
|
||||
&m_completer, QOverload< const QString& >::of( &QCompleter::activated ), ui->tagSearch, &QLineEdit::setText );
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void MainWindow::on_tagSearch_textChanged( const QString& text )
|
||||
{
|
||||
const auto tags { m_client.autocompleteTag( text ) };
|
||||
|
||||
m_autocomplete_watcher.setFuture( tags );
|
||||
|
||||
idhan::logging::debug( "Searching for tags matching \"{}\"", text.toStdString() );
|
||||
}
|
||||
|
||||
void MainWindow::on_tagSearch_returnPressed()
|
||||
{
|
||||
const auto tag_name { ui->tagSearch->text() };
|
||||
|
||||
idhan::logging::debug( "Searching for tag: {}", tag_name.toStdString() );
|
||||
|
||||
const auto itter { tag_map.find( tag_name.toStdString() ) };
|
||||
|
||||
if ( itter != tag_map.end() )
|
||||
{
|
||||
auto* root { new TagNode( ui->graphicsView, itter->second ) };
|
||||
|
||||
ui->graphicsView->setRootNode( root );
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->tagSearch->setText( "" );
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::autocompleteFinished()
|
||||
{
|
||||
idhan::logging::debug( "Autocomplete finished" );
|
||||
|
||||
QStringList list {};
|
||||
|
||||
const auto& future { m_autocomplete_watcher.future() };
|
||||
|
||||
for ( const auto& tag : future.result() )
|
||||
{
|
||||
const auto [ tag_id, string ] = tag;
|
||||
list.append( QString::fromStdString( string ) );
|
||||
tag_map.emplace( string, tag_id );
|
||||
}
|
||||
|
||||
idhan::logging::debug( "Got {} tags", future.result().size() );
|
||||
|
||||
m_model->setStringList( list );
|
||||
}
|
||||
54
tools/TagEditor/src/ui/MainWindow.hpp
Normal file
54
tools/TagEditor/src/ui/MainWindow.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Created by kj16609 on 5/2/25.
|
||||
//
|
||||
#ifndef MAINWINDOW_HPP
|
||||
#define MAINWINDOW_HPP
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QFutureWatcher>
|
||||
#include <QMainWindow>
|
||||
#include <QStringListModel>
|
||||
|
||||
#include "NET_CONSTANTS.hpp"
|
||||
#include "idhan/IDHANClient.hpp"
|
||||
|
||||
class QStringListModel;
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class MainWindow;
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow final : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
idhan::IDHANClientConfig m_client_config { "localhost", idhan::IDHAN_DEFAULT_PORT, "Tag Editor", false };
|
||||
|
||||
idhan::IDHANClient m_client { m_client_config };
|
||||
|
||||
QCompleter m_completer { this };
|
||||
std::unordered_map< std::string, idhan::TagID > tag_map {};
|
||||
std::unique_ptr< QStringListModel > m_model { new QStringListModel() };
|
||||
|
||||
QFutureWatcher< std::vector< std::pair< idhan::TagID, std::string > > > m_autocomplete_watcher {};
|
||||
|
||||
explicit MainWindow( QWidget* parent = nullptr );
|
||||
~MainWindow() override;
|
||||
|
||||
public slots:
|
||||
void on_tagSearch_textChanged( const QString& text );
|
||||
void on_tagSearch_returnPressed();
|
||||
void autocompleteFinished();
|
||||
|
||||
private:
|
||||
|
||||
Ui::MainWindow* ui;
|
||||
};
|
||||
|
||||
#endif //MAINWINDOW_HPP
|
||||
47
tools/TagEditor/src/ui/MainWindow.ui
Normal file
47
tools/TagEditor/src/ui/MainWindow.ui
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1486</width>
|
||||
<height>947</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="tagSearch"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="GraphWidget" name="graphicsView"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1486</width>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GraphWidget</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>GraphWidget.hpp</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user