基础的图形绘制完成

This commit is contained in:
codeboss 2024-07-31 10:12:34 +08:00
parent f86b7bf460
commit 0538a059d9
8 changed files with 154 additions and 78 deletions

View File

@ -6,6 +6,7 @@
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="f609c0f2-cd0d-4eea-87f1-8caf02d3f04f" name="Changes" comment=""> <list default="true" id="f609c0f2-cd0d-4eea-87f1-8caf02d3f04f" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/DataType.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/DataType.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" afterDir="false" />
</list> </list>
@ -35,29 +36,29 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;Python.CompareViews.executor&quot;: &quot;Run&quot;, "Python.CompareViews.executor": "Run",
&quot;Python.CompareWindow.executor&quot;: &quot;Run&quot;, "Python.CompareWindow.executor": "Run",
&quot;Python.DAGGraph (1).executor&quot;: &quot;Run&quot;, "Python.DAGGraph (1).executor": "Run",
&quot;Python.DAGGraph.executor&quot;: &quot;Run&quot;, "Python.DAGGraph.executor": "Run",
&quot;Python.DAGLayout (1).executor&quot;: &quot;Run&quot;, "Python.DAGLayout (1).executor": "Run",
&quot;Python.DAGLayout.executor&quot;: &quot;Run&quot;, "Python.DAGLayout.executor": "Run",
&quot;Python.DAGPresent.executor&quot;: &quot;Run&quot;, "Python.DAGPresent.executor": "Run",
&quot;Python.MergeView.executor&quot;: &quot;Run&quot;, "Python.MergeView.executor": "Run",
&quot;Python.MileStone.executor&quot;: &quot;Run&quot;, "Python.MileStone.executor": "Run",
&quot;Python.NovelManage.executor&quot;: &quot;Debug&quot;, "Python.NovelManage.executor": "Debug",
&quot;Python.ReferView.executor&quot;: &quot;Run&quot;, "Python.ReferView.executor": "Run",
&quot;Python.StoryMap.executor&quot;: &quot;Run&quot;, "Python.StoryMap.executor": "Run",
&quot;Python.ast_load.executor&quot;: &quot;Debug&quot;, "Python.ast_load.executor": "Debug",
&quot;Python.entry.executor&quot;: &quot;Run&quot;, "Python.entry.executor": "Run",
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;, "RunOnceActivity.OpenProjectViewOnStart": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;git-widget-placeholder&quot;: &quot;master&quot;, "git-widget-placeholder": "master",
&quot;last_opened_file_path&quot;: &quot;D:/Projects/Python/StoryCheckTools&quot;, "last_opened_file_path": "D:/Projects/Python/StoryCheckTools",
&quot;settings.editor.selected.configurable&quot;: &quot;reference.settings.ide.settings.new.ui&quot; "settings.editor.selected.configurable": "reference.settings.ide.settings.new.ui"
} }
}</component> }]]></component>
<component name="RunManager" selected="Python.DAGPresent"> <component name="RunManager" selected="Python.DAGPresent">
<configuration name="CompareViews" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="CompareViews" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" /> <module name="StoryTools" />

View File

@ -12,16 +12,15 @@ class Pos:
class Point: class Point:
def __init__(self, name:str, pos: Pos = Pos()): def __init__(self, name:str):
self.point_name = name self.point_name = name
self.pos = pos
pass pass
def name(self) -> str: def name(self) -> str:
return self.point_name return self.point_name
def make_copy(self) -> 'Point': def make_copy(self) -> 'Point':
return Point(self.point_name, self.pos.make_copy()) return Point(self.point_name)
class Line: class Line:

View File

@ -32,7 +32,6 @@ class DAGLayerHelper:
ins.input_count = self.input_count ins.input_count = self.input_count
ins.next_points = temp_ps ins.next_points = temp_ps
ins.layer_v = self.layer_v ins.layer_v = self.layer_v
ins.sort_v = self.sort_v
return ins return ins
@ -53,10 +52,10 @@ class DAGOrderHelper:
def is_fake_node(self) -> bool: def is_fake_node(self) -> bool:
return self.layer_bind is None return self.layer_bind is None
def get_previous_sibling_layer_nodes(self): def get_upstream_nodes(self):
return self.__prev_layer_nodes return self.__prev_layer_nodes
def append_previous_sibling_layer_node(self, node: 'DAGOrderHelper'): def append_upstream_node(self, node: 'DAGOrderHelper'):
self.__prev_layer_nodes.append(node) self.__prev_layer_nodes.append(node)
pass pass
@ -202,7 +201,7 @@ class DAGGraph:
for idx in range(1, len(node_links)): for idx in range(1, len(node_links)):
start_point = node_links[idx-1] start_point = node_links[idx-1]
end_point = node_links[idx] end_point = node_links[idx]
end_point.append_previous_sibling_layer_node(start_point) end_point.append_upstream_node(start_point)
pass pass
temp_array.extend(node_links[1:len(node_links)-1]) temp_array.extend(node_links[1:len(node_links)-1])
@ -233,7 +232,7 @@ class DAGGraph:
elif layer_index > 0: elif layer_index > 0:
# 计算排序系数 # 计算排序系数
for target_node in target_nodes_within_layer: for target_node in target_nodes_within_layer:
prev_sorts = list(map(lambda n:n.sort_number, target_node.get_previous_sibling_layer_nodes())) prev_sorts = list(map(lambda n:n.sort_number, target_node.get_upstream_nodes()))
target_node.sort_number = sum(prev_sorts)/len(prev_sorts) target_node.sort_number = sum(prev_sorts)/len(prev_sorts)
pass pass

View File

@ -1,9 +1,9 @@
from graph.directed_acyclic_graph.DAGLayout import DAGGraph, DAGOrderHelper from graph.directed_acyclic_graph.DAGLayout import DAGGraph, DAGOrderHelper
from graph.DataType import Arrow, Point from graph.DataType import Arrow, Point
from typing import List from typing import List, Dict
from PyQt5.QtWidgets import QWidget, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem from PyQt5.QtWidgets import QWidget, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
from PyQt5.QtCore import QRectF, Qt, QPointF from PyQt5.QtCore import QRectF, Qt, QPointF
from PyQt5.QtGui import QFont, QBrush, QPen, QPainter from PyQt5.QtGui import QFont, QBrush, QPen, QPainter, QPainterPath
import sys import sys
from enum import Enum from enum import Enum
from abc import abstractmethod from abc import abstractmethod
@ -15,17 +15,27 @@ class GraphNode:
raise NotImplementedError("boundingRect") raise NotImplementedError("boundingRect")
@abstractmethod @abstractmethod
def pos(self)->QPointF: def pos(self) -> QPointF:
raise NotImplementedError("pos") raise NotImplementedError("pos")
class StoryNodeType(Enum):
"""
故事节点类型
"""
StoryStartNode = 0,
FragmentNode = 1,
class StoryFragmentNode(QGraphicsItem, GraphNode): class StoryFragmentNode(QGraphicsItem, GraphNode):
""" """
故事情节名称节点 故事情节名称节点
""" """
def __init__(self, name:str, font: QFont, parent):
def __init__(self, name: str, type: StoryNodeType, font: QFont, parent):
QGraphicsItem.__init__(self, parent) QGraphicsItem.__init__(self, parent)
self.node_type_within_story = type
self.fragment_name = name self.fragment_name = name
self.font_bind = font self.font_bind = font
self.data_bind: object = None self.data_bind: object = None
@ -33,14 +43,20 @@ class StoryFragmentNode(QGraphicsItem, GraphNode):
def boundingRect(self) -> QRectF: def boundingRect(self) -> QRectF:
size = self.font_bind.pixelSize() size = self.font_bind.pixelSize()
return QRectF(0, 0, size * len(self.fragment_name) + 10, size + 10) if self.node_type_within_story == StoryNodeType.FragmentNode:
return QRectF(0, 0, size * len(self.fragment_name) + 10, size + 10)
else:
return QRectF(0, 0, size * len(self.fragment_name) + 10, size + 10)
def paint(self, painter, option, widget = ...): def paint(self, painter, option, widget=...):
outline = self.boundingRect() outline = self.boundingRect()
text_rect = QRectF(outline.x()+5, outline.y()+5, outline.width()-10, outline.height() - 10) text_rect = QRectF(outline.x() + 5, outline.y() + 5, outline.width() - 10, outline.height() - 10)
painter.save() painter.save()
painter.fillRect(outline, QBrush(Qt.gray)) if self.node_type_within_story == StoryNodeType.FragmentNode:
painter.fillRect(outline, QBrush(Qt.gray))
else:
painter.fillRect(outline, Qt.green)
painter.drawRect(outline) painter.drawRect(outline)
painter.setFont(self.font_bind) painter.setFont(self.font_bind)
painter.drawText(text_rect, self.fragment_name) painter.drawText(text_rect, self.fragment_name)
@ -48,7 +64,6 @@ class StoryFragmentNode(QGraphicsItem, GraphNode):
pass pass
# 添加自定义功能函数 # 添加自定义功能函数
def attach_data(self, data_inst: object): def attach_data(self, data_inst: object):
self.data_bind = data_inst self.data_bind = data_inst
@ -64,9 +79,11 @@ class PenetrateNode(QGraphicsItem, GraphNode):
""" """
贯穿连线节点 贯穿连线节点
""" """
def __init__(self, width:float, parent):
def __init__(self, width: float, parent):
QGraphicsItem.__init__(self, parent) QGraphicsItem.__init__(self, parent)
self.width_store = width self.width_store = width
self.data_bind: object = None
pass pass
def resize(self, width: float): def resize(self, width: float):
@ -77,7 +94,7 @@ class PenetrateNode(QGraphicsItem, GraphNode):
def boundingRect(self): def boundingRect(self):
return QRectF(0, 0, self.width_store, 8) return QRectF(0, 0, self.width_store, 8)
def paint(self, painter, option, widget = ...): def paint(self, painter, option, widget=...):
outline = self.boundingRect() outline = self.boundingRect()
painter.save() painter.save()
@ -85,27 +102,40 @@ class PenetrateNode(QGraphicsItem, GraphNode):
painter.restore() painter.restore()
pass pass
# 添加自定义功能函数
def attach_data(self, data_inst: object):
self.data_bind = data_inst
pass
class CurveTransition(QGraphicsItem): def get_data(self) -> object:
return self.data_bind
class TransitionCurve(QGraphicsItem):
def __init__(self, start: GraphNode, end: GraphNode, parent): def __init__(self, start: GraphNode, end: GraphNode, parent):
QGraphicsItem.__init__(self, parent) QGraphicsItem.__init__(self, parent)
self.start_node = start self.start_node = start
self.end_node = end self.end_node = end
self.outline = QRectF() self.outline = QRectF()
self.data_bind: object = None
pass pass
def layout_refresh(self): def layout_refresh(self):
self.setPos(self.start_node.boundingRect().topRight()) gpos = self.start_node.pos() + self.start_node.boundingRect().topRight()
self.setPos(gpos)
orect = self.start_node.boundingRect()
erect = self.end_node.boundingRect()
self.outline = QRectF(0, 0, self.outline = QRectF(0, 0,
self.end_node.pos().x() - self.start_node.pos().x() - self.start_node.boundingRect().width(), self.end_node.pos().x() - self.start_node.pos().x() - orect.width(),
self.end_node.pos().y() + self.end_node.boundingRect().height() - self.start_node.pos().y()) self.end_node.pos().y() + erect.height() - self.start_node.pos().y())
pass pass
def boundingRect(self): def boundingRect(self):
return self.outline return self.outline
pass pass
def paint(self, painter, option, widget = ...): def paint(self, painter, option, widget=...):
outline = self.outline outline = self.outline
painter.save() painter.save()
@ -113,16 +143,31 @@ class CurveTransition(QGraphicsItem):
start_rect = self.start_node.boundingRect() start_rect = self.start_node.boundingRect()
end_rect = self.end_node.boundingRect() end_rect = self.end_node.boundingRect()
start_pos = QPointF(0, start_rect.height()/2) start_pos = QPointF(0, start_rect.height() / 2)
end_pos = QPointF(outline.width(), outline.height() - end_rect.height()/2) control_pos0 = start_pos + QPointF(outline.width()/3, 0)
end_pos = QPointF(outline.width(), outline.height() - end_rect.height() / 2)
control_pos1 = end_pos + QPointF(-outline.width()/3, 0)
npen = QPen(Qt.black) npen = QPen(Qt.black)
npen.setWidthF(4) npen.setWidthF(4)
painter.setPen(npen) painter.setPen(npen)
painter.drawLine(start_pos, end_pos)
path0 = QPainterPath()
path0.moveTo(start_pos)
path0.cubicTo(control_pos0, control_pos1, end_pos)
painter.drawPath(path0)
painter.restore() painter.restore()
pass pass
# 添加自定义功能函数
def attach_data(self, data_inst: object):
self.data_bind = data_inst
pass
def get_data(self) -> object:
return self.data_bind
class Direction(Enum): class Direction(Enum):
RankLR = 0, RankLR = 0,
@ -133,24 +178,16 @@ class DAGActiveView(QGraphicsView):
def __init__(self, orie: Direction, parent): def __init__(self, orie: Direction, parent):
QGraphicsView.__init__(self, parent) QGraphicsView.__init__(self, parent)
self.transition_with = 200 self.layer_span = 200
self.node_span = 50
self.orientation = orie self.orientation = orie
self.scene_bind = QGraphicsScene(self) self.scene_bind = QGraphicsScene(self)
self.setScene(self.scene_bind) self.setScene(self.scene_bind)
font_global = QFont() font = QFont()
font_global.setPixelSize(20) font.setPixelSize(20)
self.setFont(font)
one = StoryFragmentNode("实例节点:示例情节", font_global, None)
self.scene_bind.addItem(one)
two = PenetrateNode(200, None)
two.setPos(QPointF(500, 400))
self.scene_bind.addItem(two)
connectx = CurveTransition(one, two, None)
self.scene_bind.addItem(connectx)
connectx.layout_refresh()
pass pass
@ -160,39 +197,68 @@ class DAGActiveView(QGraphicsView):
tools.graph_layout() tools.graph_layout()
total_nodes = tools.nodes_with_layout total_nodes = tools.nodes_with_layout
previous_node_end = 0
previous_graphics_nodes = []
# 迭代呈现层
for layer_idx in range(0, tools.max_layer_count): for layer_idx in range(0, tools.max_layer_count):
current_level_nodes: List[DAGOrderHelper] = list(filter(lambda n:n.layer_number == layer_idx, total_nodes)) current_level_nodes: List[DAGOrderHelper] = list(filter(lambda n: n.layer_number == layer_idx, total_nodes))
current_level_nodes.sort(key=lambda n:n.sort_number) current_level_nodes.sort(key=lambda n: n.sort_number)
# 构建当前层节点 # 构建当前层节点
ypos_acc = 0 ypos_acc = 0
all_nodes = [] all_graphics_nodes = []
for node in current_level_nodes: for node in current_level_nodes:
if node.is_fake_node(): if node.is_fake_node():
node = PenetrateNode(20, None) curr_gnode = PenetrateNode(20, None)
ypos_acc += node.boundingRect().height() curr_gnode.setPos(previous_node_end, ypos_acc)
all_nodes.append(node) curr_gnode.attach_data(node)
self.scene_bind.addItem(node) ypos_acc += curr_gnode.boundingRect().height()
all_graphics_nodes.append(curr_gnode)
self.scene_bind.addItem(curr_gnode)
else: else:
node = StoryFragmentNode(node.layer_bind.bind_point().point_name, self.font(), None) if layer_idx == 0:
ypos_acc += node.boundingRect().height() curr_gnode = StoryFragmentNode(node.layer_bind.bind_point().point_name, StoryNodeType.StoryStartNode,
all_nodes.append(node) self.font(), None)
self.scene_bind.addItem(node) else:
curr_gnode = StoryFragmentNode(node.layer_bind.bind_point().point_name, StoryNodeType.FragmentNode,
self.font(), None)
curr_gnode.attach_data(node)
curr_gnode.setPos(previous_node_end, ypos_acc)
ypos_acc += curr_gnode.boundingRect().height()
all_graphics_nodes.append(curr_gnode)
self.scene_bind.addItem(curr_gnode)
pass pass
ypos_acc += self.node_span
pass pass
# 调整同层节点宽度
width_max = 0 width_max = 0
for n in all_nodes: for n in all_graphics_nodes:
width_max = max(width_max, n.boundingRect().width()) width_max = max(width_max, n.boundingRect().width())
pass pass
for n in all_graphics_nodes:
for n in all_nodes:
if n is PenetrateNode: if n is PenetrateNode:
n.resize(width_max) n.resize(width_max)
pass pass
pass pass
pass
previous_node_end += width_max + self.layer_span
if len(previous_graphics_nodes) > 0:
for curr_gnode in all_graphics_nodes:
sort_helper: DAGOrderHelper = curr_gnode.get_data()
for prev_gnode in previous_graphics_nodes:
if prev_gnode.get_data() in sort_helper.get_upstream_nodes():
line_cmbn = TransitionCurve(prev_gnode, curr_gnode, None)
self.scene_bind.addItem(line_cmbn)
line_cmbn.layout_refresh()
pass
pass
pass
pass
previous_graphics_nodes = all_graphics_nodes
pass
pass pass
@ -201,4 +267,15 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
view = DAGActiveView(Direction.RankLR, None) view = DAGActiveView(Direction.RankLR, None)
view.show() view.show()
arrows = [
Arrow(Point('a'), Point('b')),
Arrow(Point('a'), Point('c')),
Arrow(Point('c'), Point('d')),
Arrow(Point('a'), Point('d')),
Arrow(Point('c'), Point('e')),
Arrow(Point('c'), Point('f')),
]
view.update_with_edges(arrows)
app.exec() app.exec()