章节节点引用视图构建完成

This commit is contained in:
codeboss 2024-08-02 17:07:41 +08:00
parent d4bf6b186c
commit 9b59b0a458
7 changed files with 286 additions and 98 deletions

View File

@ -5,14 +5,10 @@
</component>
<component name="ChangeListManager">
<list default="true" id="f609c0f2-cd0d-4eea-87f1-8caf02d3f04f" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/UDGLayout.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/graph/undirected_graph/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/entry.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frame/ContentView.py" beforeDir="false" afterPath="$PROJECT_DIR$/frame/ContentView.py" 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/DAGPresent.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGPresent.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/parse/StoryMap.py" beforeDir="false" afterPath="$PROJECT_DIR$/parse/StoryMap.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -40,40 +36,40 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python.CompareViews.executor": "Run",
"Python.CompareWindow.executor": "Run",
"Python.ContentView.executor": "Run",
"Python.DAGGraph (1).executor": "Run",
"Python.DAGGraph.executor": "Run",
"Python.DAGLayout (1).executor": "Run",
"Python.DAGLayout.executor": "Run",
"Python.DAGPresent.executor": "Run",
"Python.MergeView.executor": "Run",
"Python.MileStone.executor": "Run",
"Python.NovelManage.executor": "Debug",
"Python.ReferView.executor": "Run",
"Python.StoryMap.executor": "Run",
"Python.UDGLayout.executor": "Run",
"Python.UDGPresent.executor": "Run",
"Python.ast_load.executor": "Debug",
"Python.entry.executor": "Run",
"Python.test.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/Projects/Python/StoryTools",
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Python.CompareViews.executor&quot;: &quot;Run&quot;,
&quot;Python.CompareWindow.executor&quot;: &quot;Run&quot;,
&quot;Python.ContentView.executor&quot;: &quot;Debug&quot;,
&quot;Python.DAGGraph (1).executor&quot;: &quot;Run&quot;,
&quot;Python.DAGGraph.executor&quot;: &quot;Run&quot;,
&quot;Python.DAGLayout (1).executor&quot;: &quot;Run&quot;,
&quot;Python.DAGLayout.executor&quot;: &quot;Run&quot;,
&quot;Python.DAGPresent.executor&quot;: &quot;Run&quot;,
&quot;Python.MergeView.executor&quot;: &quot;Run&quot;,
&quot;Python.MileStone.executor&quot;: &quot;Run&quot;,
&quot;Python.NovelManage.executor&quot;: &quot;Debug&quot;,
&quot;Python.ReferView.executor&quot;: &quot;Run&quot;,
&quot;Python.StoryMap.executor&quot;: &quot;Run&quot;,
&quot;Python.UDGLayout.executor&quot;: &quot;Run&quot;,
&quot;Python.UDGPresent.executor&quot;: &quot;Run&quot;,
&quot;Python.ast_load.executor&quot;: &quot;Debug&quot;,
&quot;Python.entry.executor&quot;: &quot;Run&quot;,
&quot;Python.test.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Projects/Python/StoryTools&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;debugger.dataViews&quot;
}
}]]></component>
}</component>
<component name="PyDebuggerOptionsProvider">
<option name="mySaveCallSignatures" value="true" />
<option name="mySupportGeventDebugging" value="true" />
<option name="myDropIntoDebuggerOnFailedTests" value="true" />
<option name="myPyQtBackend" value="pyqt5" />
</component>
<component name="RunManager" selected="Python.UDGPresent">
<component name="RunManager" selected="Python.ContentView">
<configuration name="ContentView" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
@ -193,8 +189,8 @@
</list>
<recent_temporary>
<list>
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.ContentView" />
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.test" />
<item itemvalue="Python.UDGLayout" />
<item itemvalue="Python.DAGPresent" />
@ -222,4 +218,15 @@
<component name="UnknownFeatures">
<option featureType="com.intellij.fileTypeFactory" implementationName="*.bat" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py</url>
<line>239</line>
<option name="timeStamp" value="2" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
</project>

View File

@ -1,13 +1,16 @@
import sys
import sys, os
from typing import Dict, List
from PyQt5.QtCore import QPoint
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
from PyQt5.QtWidgets import QMenu
from PyQt5.QtCore import QPoint, Qt
from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QSlider, QDoubleSpinBox
from PyQt5.QtWidgets import QMenu, QHBoxLayout
from graph.DataType import Arrow, Point
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from graph.DataType import Arrow, Point, Line
from graph.directed_acyclic_graph.DAGPresent import DAGActiveView
from parse.StoryMap import StoryMap, XAST_ParseTool
from graph.undirected_graph.UDGPresent import UDGPresent
from parse.StoryMap import StoryMap, XAST_ParseTool, ArticleSlice
from parse.ast_load import global_ast_path
@ -24,7 +27,7 @@ class StorylinesView(QWidget):
self.fragment_view = DAGActiveView(self)
layout.setContentsMargins(0,0,0,0)
layout.addWidget(self.fragment_view)
self.fragment_view.nodes_clicked.connect(self.highlisth_node_path)
self.fragment_view.nodes_clicked.connect(self.highlight_node_path)
self.present_graph: Dict[str, StoryMap] = {}
pass
pass
@ -58,7 +61,7 @@ class StorylinesView(QWidget):
self.fragment_view.update_with_edges(arrows)
pass
def highlisth_node_path(self, xpos, ypos, list):
def highlight_node_path(self, xpos, ypos, list):
if len(list) == 0:
return
@ -92,12 +95,12 @@ class StorylinesView(QWidget):
pass
if len(story_list) == 1:
self.highlisth_node_path(xpos, ypos, [("node", story_list[0])])
self.highlight_node_path(xpos, ypos, [("node", story_list[0])])
elif len(story_list) > 1:
menu = QMenu()
for story in story_list:
def trigger(story_name:str):
return lambda : self.highlisth_node_path(xpos, ypos, [("node", story_name)])
return lambda : self.highlight_node_path(xpos, ypos, [("node", story_name)])
menu.addAction(f"story/{story}", trigger(story))
pass
@ -116,16 +119,70 @@ class StorylinesView(QWidget):
class ArticleRefsView(QWidget):
def __init__(self, parent):
QWidget.__init__(self, parent)
self.refer_view = UDGPresent(self)
layout = QGridLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.refer_view)
self.ppu_slider = QSlider(Qt.Orientation.Horizontal, self)
self.scala_slider = QSlider(Qt.Orientation.Vertical, self)
self.scala_slider.setRange(0, 1000)
self.scala_slider.setValue(100)
layout.addWidget(self.scala_slider, 0, 1)
slide_panel = QWidget(self)
layout.addWidget(slide_panel, 1, 0, 1, 2)
bottom_layout = QHBoxLayout(slide_panel)
bottom_layout.setSpacing(0)
bottom_layout.setContentsMargins(0, 0, 0, 0)
bottom_layout.addWidget(self.ppu_slider)
self.ppu_max = QDoubleSpinBox(self)
self.ppu_max.setRange(0, 2**31)
self.ppu_max.setValue(10000)
self.ppu_slider.setRange(0, 10000)
self.ppu_slider.setValue(int(self.refer_view.pixel_per_unit))
bottom_layout.addWidget(self.ppu_max)
self.refer_view.node_clicked.connect(self.highlight_sibling_nodes)
def scala_view(times:float):
tm = QTransform()
tm.scale(times, times)
return tm
self.scala_slider.valueChanged.connect(lambda iproc: self.refer_view.setTransform(scala_view(iproc/100.0)))
self.ppu_slider.valueChanged.connect(lambda ppu:self.refer_view.refresh_with_ppu(ppu))
self.ppu_slider.sliderReleased.connect(lambda : self.refer_view.update_scene_rect())
pass
def present_volumes_graph(self, ref_graph: List[ArticleSlice]):
node_edges = []
for line in ref_graph:
for target in line.refer_target:
node_edges.append(Line(Point(line.article_fullname), Point(target)))
pass
pass
self.refer_view.rebuild_from_edges(node_edges)
pass
def highlight_sibling_nodes(self, nodename: str):
self.refer_view.highlight_sibling_nodes(nodename)
pass
if __name__ == "__main__":
app = QApplication(sys.argv)
view = StorylinesView(None)
#view = StorylinesView(None)
view = ArticleRefsView(None)
view.show()
tool = XAST_ParseTool(global_ast_path)
view.present_stories_graph(tool.get_story_graph())
# view.present_stories_graph(tool.get_story_graph())
view.present_volumes_graph(tool.get_article_nodes())
# view.fragment_view.highlight_graph_link(["血脉的源头", "血脉的源头&待续"])

View File

@ -1,4 +1,4 @@
import sys
import sys, os
from abc import abstractmethod
from enum import Enum
from typing import List, Dict, Tuple
@ -7,7 +7,9 @@ import networkx as nx
from PyQt5.QtCore import QPointF, QRectF, Qt, pyqtSignal
from PyQt5.QtGui import QFont, QPainterPath, QPen, QPainter, QMouseEvent
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsView, QApplication, QGraphicsScene
from PyQt5.QtWidgets import QSplitter, QHBoxLayout
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from graph.DataType import Point, Line
@ -36,15 +38,20 @@ class GraphNode:
@abstractmethod
def pos(self) -> QPointF:
raise NotImplementedError("pos")
@abstractmethod
def relayout_exec(self, ppu: float = 1):
raise NotImplementedError("relayout_exec")
class PresentNode(QGraphicsItem, GraphNode):
def __init__(self, name: str, font: QFont, parent):
def __init__(self, name: str, font: QFont, prim_x:float, prim_y:float, parent):
QGraphicsItem.__init__(self, parent)
self.node_name = name
self.__is_highlight_mark = False
self.__font_bind = font
self.__sibling_list = []
self.__prim_pos: Tuple[float, float] = (prim_x, prim_y)
self.setZValue(10)
pass
@ -85,7 +92,7 @@ class PresentNode(QGraphicsItem, GraphNode):
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.drawRect(outline)
#painter.drawRect(outline)
if self.__is_highlight_mark:
brush = Qt.red
@ -102,6 +109,13 @@ class PresentNode(QGraphicsItem, GraphNode):
painter.restore()
pass
def relayout_exec(self, ppu: float = 1):
pos_x = ppu * self.__prim_pos[0]
pos_y = ppu * self.__prim_pos[1]
self.setPos(pos_x, pos_y)
self.update()
pass
class ConnectionNode(QGraphicsItem, GraphNode):
def __init__(self, p0: GraphNode, p1: GraphNode, parent):
@ -124,7 +138,7 @@ class ConnectionNode(QGraphicsItem, GraphNode):
return self.__highlight_mark
pass
def relayout_exec(self):
def relayout_exec(self, ppi: float = 1):
start_pos = self.__point0.pos()
end_pos = self.__point1.pos()
@ -135,6 +149,7 @@ class ConnectionNode(QGraphicsItem, GraphNode):
self.setPos(QPointF(start_x, start_y))
self.__outline = QRectF(0, 0, end_x - start_x, end_y - start_y)
self.update()
pass
def boundingRect(self):
@ -176,6 +191,8 @@ class UDGPresent(QGraphicsView):
def __init__(self, parent):
QGraphicsView.__init__(self, parent)
self.pixel_per_unit = 5000
self.__highlight_nodes: List[GraphNode] = []
self.node_set: Dict[str, GraphNode] = {}
self.__layout_graph = nx.Graph()
@ -190,69 +207,38 @@ class UDGPresent(QGraphicsView):
def rebuild_from_edges(self, line_set: List[Line]):
self.node_set.clear()
edge_set: Dict[str, Tuple[GraphNode, GraphNode]] = {}
for line in line_set:
start_node = line.points()[0]
if start_node.point_name not in self.node_set:
self.node_set[start_node.point_name] = PresentNode(start_node.point_name, self.font(), None)
pass
self.__layout_graph.add_node(start_node.point_name)
end_node = line.points()[1]
if start_node.point_name == end_node.point_name:
continue
if end_node.point_name not in self.node_set:
self.node_set[end_node.point_name] = PresentNode(end_node.point_name, self.font(), None)
pass
self.__layout_graph.add_node(end_node.point_name)
self.__layout_graph.add_edge(start_node.point_name, end_node.point_name)
start_force_point: PresentNode = self.node_set[start_node.point_name]
other_force_point: PresentNode = self.node_set[end_node.point_name]
if other_force_point not in start_force_point.sibling_nodes():
start_force_point.sibling_append(other_force_point)
if start_force_point not in other_force_point.sibling_nodes():
other_force_point.sibling_append(start_force_point)
pass
pos_map = nx.spring_layout(self.__layout_graph)
scala_value:float = 0
for name in pos_map:
primitive_pos = pos_map[name]
target_gnode: PresentNode = self.node_set[name]
for node_name in pos_map:
node_prim_pos = pos_map[node_name]
targetx_node = PresentNode(node_name, self.font(), node_prim_pos[0], node_prim_pos[1], None)
self.node_set[node_name] = targetx_node
sibling_nodes = target_gnode.sibling_nodes()
for sib in sibling_nodes:
sib_primitive_pos = pos_map[sib.node_name]
prim_x_span = primitive_pos[0] - sib_primitive_pos[0]
prim_y_span = primitive_pos[1] - sib_primitive_pos[1]
target_rect = target_gnode.boundingRect()
scala_value = max(scala_value, target_rect.width()/prim_x_span)
scala_value = max(scala_value, target_rect.height()/prim_y_span)
pass
pass
for name in pos_map:
primitive_pos = pos_map[name]
target_gnode: PresentNode = self.node_set[name]
target_gnode.setPos(primitive_pos[0] * scala_value, primitive_pos[1] * scala_value)
self.__scene_bind.addItem(target_gnode)
self.scene().addItem(targetx_node)
targetx_node.relayout_exec(self.pixel_per_unit)
pass
for edge in nx.edges(self.__layout_graph):
edge_start = edge[0]
edge_end = edge[1]
node_one = self.node_set[edge_start]
node_two = self.node_set[edge_end]
node_one: PresentNode = self.node_set[edge_start]
node_two: PresentNode = self.node_set[edge_end]
node_one.sibling_append(node_two)
node_two.sibling_append(node_one)
connection = ConnectionNode(node_one, node_two, None)
self.scene().addItem(connection)
connection.relayout_exec()
connection.relayout_exec(self.pixel_per_unit)
self.node_set[f"conn::{edge_start}&{edge_end}"] = connection
pass
pass
@ -260,7 +246,7 @@ class UDGPresent(QGraphicsView):
QGraphicsView.mousePressEvent(self, event)
if event.button() == Qt.MouseButton.LeftButton:
item: GraphNode = self.itemAt(event.pos())
if item.node_type() == PresentNodeType.PresentNode:
if item is not None and item.node_type() == PresentNodeType.PresentNode:
vnode: PresentNode = item
self.node_clicked.emit(vnode.node_name)
print(vnode.node_name)
@ -268,6 +254,74 @@ class UDGPresent(QGraphicsView):
pass
pass
def refresh_with_ppu(self, ppu: float):
self.pixel_per_unit = ppu
for node in self.node_set.values():
if node.node_type() == PresentNodeType.PresentNode:
node.relayout_exec(ppu)
pass
pass
for node in self.node_set.values():
if node.node_type() == PresentNodeType.ConnectionNode:
node.relayout_exec(ppu)
pass
pass
self.scene().update()
self.update()
pass
def update_scene_rect(self):
minx = 2*64
miny = 2*64
maxx = -2**64
maxy = -2**64
for item in self.node_set.values():
minx = min(item.pos().x(), minx)
miny = min(item.pos().y(), miny)
maxx = max(item.pos().x() + item.boundingRect().width(), maxx)
maxy = max(item.pos().y() + item.boundingRect().height(), maxy)
pass
self.scene().setSceneRect(minx, miny, maxx - minx, maxy - miny)
pass
def highlight_sibling_nodes(self, center_node: str):
for node in self.__highlight_nodes:
node.highlight(False)
pass
self.__highlight_nodes.clear()
target_node: PresentNode = self.node_set[center_node]
if target_node is None:
return
target_node.highlight(True)
self.__highlight_nodes.append(target_node)
for sib_node in target_node.sibling_nodes():
sib_node.highlight(True)
self.__highlight_nodes.append(sib_node)
sib_name = sib_node.node_name
constr1 = f"conn::{center_node}&{sib_name}"
constr2 = f"conn::{sib_name}&{center_node}"
if constr1 in self.node_set:
self.node_set[constr1].highlight(True)
self.__highlight_nodes.append(self.node_set[constr1])
elif constr2 in self.node_set:
self.node_set[constr2].highlight(True)
self.__highlight_nodes.append(self.node_set[constr2])
pass
pass
self.scene().update()
self.update()
pass
if __name__ == "__main__":
app = QApplication(sys.argv)

View File

@ -27,6 +27,24 @@ class FragmentSlice(EmptyNode):
pass
class ArticleSlice(EmptyNode):
def __init__(self, a_name: str, v_name: str):
self.article_name = a_name
self.volume_blongs = v_name
self.article_fullname = f"{v_name}@{a_name}"
self.refer_target: List[str] = [] # 引用切面
self.text_sections: List[str] = [] # 文本段落
pass
def get_from_memory(self) -> str:
return "\n".join(self.text_sections)
def set_to_memory(self, content: str):
self.text_sections = content.split("\n")
pass
class StoryMap:
def __init__(self, name: str):
self.story_name = name
@ -44,7 +62,7 @@ class StoryMap:
self.slice_list.append(node)
pass
def get_fragment_defined(self, name: str) -> FragmentSlice:
def get_fragment_defined(self, name: str) -> FragmentSlice|None:
for fit in self.slice_list:
if fit.is_define_node[0]:
if fit.is_define_node[1] == name:
@ -140,6 +158,58 @@ class XAST_ParseTool:
self.storylines_plait(story_dict)
return story_dict
def get_article_nodes(self) -> List[ArticleSlice]:
retvalues = []
hangout_nodes = ArticleSlice("悬空节点", "")
retvalues.append(hangout_nodes)
fragments = self.__get_all_fragment_names()
volumes = self.dom_tree.getElementsByTagName("volume")
for vnode in volumes:
child_articles = self.__volume_node_parse(vnode)
retvalues.extend(child_articles)
for child in child_articles:
for refn in child.refer_target:
if refn in fragments:
fragments.remove(refn)
pass
pass
pass
pass
hangout_nodes.refer_target.extend(fragments)
return retvalues
def __get_all_fragment_names(self) -> List[str]:
values = []
frags = self.dom_tree.getElementsByTagName("fragment")
for frag in frags:
story: mdom.Element = frag.parentNode
values.append(f"{story.getAttribute("name")}&{frag.getAttribute("name")}")
pass
return values
def __volume_node_parse(self, vnode: mdom.Element)-> List[ArticleSlice]:
retvalues = []
vname = vnode.getAttribute("name")
articles = vnode.getElementsByTagName("article")
for anode in articles:
aname = anode.getAttribute("name")
node_inst = ArticleSlice(aname, vname)
retvalues.append(node_inst)
refsnode = anode.getElementsByTagName("refer")
for refnode in refsnode:
ref_story = refnode.getAttribute("story")
ref_fragment = refnode.getAttribute("fragment")
node_inst.refer_target.append(f"{ref_story}&{ref_fragment}")
pass
pass
return retvalues
pass