Compare commits

...

2 Commits

Author SHA1 Message Date
codeboss 9b59b0a458 章节节点引用视图构建完成 2024-08-02 17:07:41 +08:00
codeboss d4bf6b186c 基础的无向图呈现 2024-08-02 12:02:24 +08:00
15 changed files with 819 additions and 95 deletions

View File

@ -6,7 +6,9 @@
<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/directed_acyclic_graph/DAGLayout.py" beforeDir="false" afterPath="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.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/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> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -38,7 +40,7 @@
&quot;keyToString&quot;: { &quot;keyToString&quot;: {
&quot;Python.CompareViews.executor&quot;: &quot;Run&quot;, &quot;Python.CompareViews.executor&quot;: &quot;Run&quot;,
&quot;Python.CompareWindow.executor&quot;: &quot;Run&quot;, &quot;Python.CompareWindow.executor&quot;: &quot;Run&quot;,
&quot;Python.ContentView.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 (1).executor&quot;: &quot;Run&quot;,
&quot;Python.DAGGraph.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 (1).executor&quot;: &quot;Run&quot;,
@ -49,38 +51,25 @@
&quot;Python.NovelManage.executor&quot;: &quot;Debug&quot;, &quot;Python.NovelManage.executor&quot;: &quot;Debug&quot;,
&quot;Python.ReferView.executor&quot;: &quot;Run&quot;, &quot;Python.ReferView.executor&quot;: &quot;Run&quot;,
&quot;Python.StoryMap.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.ast_load.executor&quot;: &quot;Debug&quot;,
&quot;Python.entry.executor&quot;: &quot;Run&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.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;, &quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Projects/Python/StoryCheckTools&quot;, &quot;last_opened_file_path&quot;: &quot;D:/Projects/Python/StoryTools&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;reference.settings.ide.settings.new.ui&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.ContentView"> <component name="RunManager" selected="Python.ContentView">
<configuration name="CompareViews" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/frame" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/frame/CompareViews.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="ContentView" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="ContentView" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" /> <module name="StoryTools" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
@ -103,28 +92,6 @@
<option name="INPUT_FILE" value="" /> <option name="INPUT_FILE" value="" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="DAGLayout" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/directed_acyclic_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/directed_acyclic_graph/DAGLayout.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="DAGPresent" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="DAGPresent" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" /> <module name="StoryTools" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
@ -147,7 +114,7 @@
<option name="INPUT_FILE" value="" /> <option name="INPUT_FILE" value="" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="NovelManage" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="UDGLayout" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" /> <module name="StoryTools" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
@ -156,12 +123,56 @@
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
</envs> </envs>
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/manage" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/manage/NovelManage.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/UDGLayout.py" />
<option name="PARAMETERS" value="wnss -cmp" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="UDGPresent" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/UDGPresent.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="StoryTools" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/graph/undirected_graph" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/graph/undirected_graph/test.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" /> <option name="MODULE_MODE" value="false" />
@ -170,26 +181,26 @@
<method v="2" /> <method v="2" />
</configuration> </configuration>
<list> <list>
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.test" />
<item itemvalue="Python.UDGLayout" />
<item itemvalue="Python.ContentView" /> <item itemvalue="Python.ContentView" />
<item itemvalue="Python.DAGLayout" />
<item itemvalue="Python.DAGPresent" /> <item itemvalue="Python.DAGPresent" />
<item itemvalue="Python.CompareViews" />
<item itemvalue="Python.NovelManage" />
</list> </list>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Python.ContentView" /> <item itemvalue="Python.ContentView" />
<item itemvalue="Python.UDGPresent" />
<item itemvalue="Python.test" />
<item itemvalue="Python.UDGLayout" />
<item itemvalue="Python.DAGPresent" /> <item itemvalue="Python.DAGPresent" />
<item itemvalue="Python.DAGLayout" />
<item itemvalue="Python.CompareViews" />
<item itemvalue="Python.NovelManage" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" /> <option value="bundled-python-sdk-975db3bf15a3-31b6be0877a2-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.18034.82" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@ -207,4 +218,15 @@
<component name="UnknownFeatures"> <component name="UnknownFeatures">
<option featureType="com.intellij.fileTypeFactory" implementationName="*.bat" /> <option featureType="com.intellij.fileTypeFactory" implementationName="*.bat" />
</component> </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> </project>

View File

@ -1,9 +1 @@
from parse.StoryMap import XAST_ParseTool, storyline_list2map print(__file__)
from parse.ast_load import global_ast_path
astx = XAST_ParseTool(global_ast_path)
storys = astx.story_list
storys_map = storyline_list2map(storys)
astx.storylines_plait(storys_map)
print(storys_map)

View File

@ -1,13 +1,16 @@
import sys import sys, os
from typing import Dict, List from typing import Dict, List
from PyQt5.QtCore import QPoint from PyQt5.QtCore import QPoint, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QMenu 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 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 from parse.ast_load import global_ast_path
@ -17,13 +20,15 @@ class FragmentPoint(Point):
pass pass
class ContentWindow(QMainWindow): class StorylinesView(QWidget):
def __init__(self, parent): def __init__(self, parent):
QMainWindow.__init__(self, parent) QWidget.__init__(self, parent)
layout = QVBoxLayout(self)
self.fragment_view = DAGActiveView(self) self.fragment_view = DAGActiveView(self)
self.setCentralWidget(self.fragment_view) layout.setContentsMargins(0,0,0,0)
self.fragment_view.nodes_clicked.connect(self.print_node_list) layout.addWidget(self.fragment_view)
self.present_graph: Dict[str, StoryMap] = None self.fragment_view.nodes_clicked.connect(self.highlight_node_path)
self.present_graph: Dict[str, StoryMap] = {}
pass pass
pass pass
@ -56,7 +61,7 @@ class ContentWindow(QMainWindow):
self.fragment_view.update_with_edges(arrows) self.fragment_view.update_with_edges(arrows)
pass pass
def print_node_list(self, xpos, ypos, list): def highlight_node_path(self, xpos, ypos, list):
if len(list) == 0: if len(list) == 0:
return return
@ -90,12 +95,12 @@ class ContentWindow(QMainWindow):
pass pass
if len(story_list) == 1: if len(story_list) == 1:
self.print_node_list(xpos, ypos, [("node", story_list[0])]) self.highlight_node_path(xpos, ypos, [("node", story_list[0])])
elif len(story_list) > 1: elif len(story_list) > 1:
menu = QMenu() menu = QMenu()
for story in story_list: for story in story_list:
def trigger(story_name:str): def trigger(story_name:str):
return lambda : self.print_node_list(xpos, ypos, [("node", story_name)]) return lambda : self.highlight_node_path(xpos, ypos, [("node", story_name)])
menu.addAction(f"story/{story}", trigger(story)) menu.addAction(f"story/{story}", trigger(story))
pass pass
@ -111,13 +116,73 @@ class ContentWindow(QMainWindow):
pass pass
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__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
view = ContentWindow(None) #view = StorylinesView(None)
view = ArticleRefsView(None)
view.show() view.show()
tool = XAST_ParseTool(global_ast_path) 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.fragment_view.highlight_graph_link(["血脉的源头", "血脉的源头&待续"])

View File

@ -1,3 +1,4 @@
import math
from typing import List from typing import List
@ -7,6 +8,44 @@ class Pos:
self.y_pos = y self.y_pos = y
pass pass
def __add__(self, other: 'Pos') -> 'Pos':
return Pos(self.x_pos + other.x_pos, self.y_pos + other.y_pos)
def __sub__(self, other: 'Pos') -> 'Pos':
return Pos(self.x_pos - other.x_pos, self.y_pos - other.y_pos)
def __iadd__(self, other: 'Pos') -> 'Pos':
self.x_pos += other.x_pos
self.y_pos += other.y_pos
return self
def __isub__(self, other: 'Pos') -> 'Pos':
self.x_pos -= other.x_pos
self.y_pos -= other.y_pos
return self
def __mul__(self, t: float) -> 'Pos':
return Pos(self.x_pos * t, self.y_pos * t)
def __imul__(self, t: float) -> 'Pos':
self.x_pos *= t
self.y_pos *= t
return self
def vec_length(self) -> float:
return math.sqrt(self.x_pos**2 + self.y_pos**2)
def normalized(self) -> 'Pos':
dist = self.vec_length()
if dist == 0:
return Pos(0, 0)
return self * (1/dist)
def to_text(self) -> str:
return f"pos<{self.__hash__()}>{{{self.x_pos}, {self.y_pos}}}"
def make_copy(self) -> 'Pos': def make_copy(self) -> 'Pos':
return Pos(self.x_pos, self.y_pos) return Pos(self.x_pos, self.y_pos)
@ -23,6 +62,16 @@ class Point:
return Point(self.point_name) return Point(self.point_name)
class PositionalPoint(Point, Pos):
def __init__(self, name: str, x: float, y: float):
Point.__init__(self, name)
Pos.__init__(self, x, y)
pass
def make_copy(self) -> 'PositionalPoint':
return PositionalPoint(self.name(), self.x_pos, self.y_pos)
class Line: class Line:
def __init__(self, p0: Point, p1: Point): def __init__(self, p0: Point, p1: Point):
self.point_set = [p0, p1] self.point_set = [p0, p1]
@ -46,5 +95,3 @@ class Arrow(Line):
def end_point(self): def end_point(self):
return self.point_set[1] return self.point_set[1]
pass

View File

@ -422,6 +422,7 @@ class DAGActiveView(QGraphicsView):
def mousePressEvent(self, event: QMouseEvent): def mousePressEvent(self, event: QMouseEvent):
QGraphicsView.mousePressEvent(self, event) QGraphicsView.mousePressEvent(self, event)
if event.button() == Qt.MouseButton.LeftButton:
gitems = self.items(event.pos()) gitems = self.items(event.pos())
noderef_names = [] noderef_names = []
for gnode in gitems: for gnode in gitems:
@ -431,9 +432,9 @@ class DAGActiveView(QGraphicsView):
pass pass
self.nodes_clicked.emit(event.pos().x(), event.pos().y(), noderef_names[0:1]) self.nodes_clicked.emit(event.pos().x(), event.pos().y(), noderef_names[0:1])
pass pass
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
view = DAGActiveView(None) view = DAGActiveView(None)

View File

@ -0,0 +1,167 @@
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
from graph.DataType import PositionalPoint, Line, Pos, Point
from typing import List, Dict
import random as rd
class ForceCalcHelper:
"""
力导向计算辅助节点
"""
def __init__(self, init_v: PositionalPoint):
self.bind_point = init_v
self.sibling_nodes: Dict[str, 'ForceCalcHelper'] = {}
pass
def bind_point_name(self) -> str:
return self.bind_point.point_name
def get_sibling_nodes(self) -> Dict[str, 'ForceCalcHelper']:
return self.sibling_nodes
pass
def sibling_append(self, node: 'ForceCalcHelper'):
self.sibling_nodes[node.bind_point_name()] = node
pass
class UDGGraph:
force2accx_rates = 1
def __init__(self):
self.random_gen = rd.Random()
self.node_set: Dict[str, ForceCalcHelper] = {}
pass
def rebuild_from_edges(self, line_set: List[Line]):
self.node_set.clear()
for line in line_set:
start_node = line.points()[0]
if start_node.point_name not in self.node_set:
pos_node = PositionalPoint(start_node.point_name, 0, 0)
self.node_set[start_node.point_name] = ForceCalcHelper(pos_node)
pass
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:
pos_node = PositionalPoint(end_node.point_name, 0, 0)
self.node_set[end_node.point_name] = ForceCalcHelper(pos_node)
pass
start_force_point = self.node_set[start_node.point_name]
other_force_point = self.node_set[end_node.point_name]
start_force_point.sibling_append(other_force_point)
other_force_point.sibling_append(start_force_point)
pass
pass
def __eject_with_item(self, curr: ForceCalcHelper, node_set:Dict[str, ForceCalcHelper]) -> Pos:
init_value = Pos(0, 0)
for node in node_set.values():
if curr.bind_point_name() == node.bind_point_name():
continue
coord_span = curr.current_pos() - node.current_pos()
distance = coord_span.vec_length()
force_scalar = ForceCalcHelper.eject_k / (distance**2)
force_vec = coord_span.normalized() * force_scalar
init_value += force_vec
pass
return init_value
def __attract_with_item_sibs(self, curr: ForceCalcHelper) -> Pos:
init_value = Pos(0, 0)
for node in curr.get_sibling_nodes().values():
coord_span = curr.current_pos() - node.current_pos()
distance = coord_span.vec_length()
force_scalar = distance * ForceCalcHelper.attract_k
force_vec = coord_span.normalized() * force_scalar
init_value -= force_vec
pass
return init_value
def __calculate_item_force(self, curr: ForceCalcHelper, node_set:Dict[str, ForceCalcHelper]) -> float:
"""
计算指定节点对整个数据图节点的合力
:param times: 次数初始迭代有特殊处理
:param curr: 当前节点
:param node_set: 所有节点
:return: 合力标量
"""
eject_vec2 = self.__eject_with_item(curr, node_set)
attract_vec2 = self.__attract_with_item_sibs(curr)
curr.force_with_direction = eject_vec2 + attract_vec2
# 阻尼计算 f=fxG(fxg=1,m=1)
fr = ForceCalcHelper.damping_k * 10
if curr.force_with_direction.vec_length() > fr:
curr.force_with_direction -= curr.force_with_direction.normalized() * fr
pass
else:
curr.force_with_direction = Pos()
pass
return curr.force_with_direction.vec_length()
def __item_position_adjust(self, curr: ForceCalcHelper) -> None:
if curr.force_with_direction.vec_length() == 0:
return
vec_speed = curr.force_with_direction.normalized()
curr.move_by(vec_speed)
pass
def graph_layout(self):
for curr in self.node_set.values():
random_pos = Pos(self.random_gen.random() * 100, self.random_gen.random() * 100)
curr.move_by(random_pos)
pass
for idx in range(0, 10):
for curr in self.node_set.values():
self.__calculate_item_force(curr, self.node_set)
pass
for curr in self.node_set.values():
self.__item_position_adjust(curr)
pass
pass
pass
def visible_positon_set(self) -> List[PositionalPoint]:
retvs = []
for node in self.node_set.values():
retvs.append(node.bind_point)
pass
return retvs
if __name__ == "__main__":
list_in = [
Line(Point("a"), Point("b")),
Line(Point("a"), Point("c")),
Line(Point("a"), Point("d")),
Line(Point("a"), Point("e")),
Line(Point("d"), Point("e")),
Line(Point("f"), Point("c")),
]
graph = UDGGraph()
graph.rebuild_from_edges(list_in)
graph.graph_layout()
for p in graph.visible_positon_set():
print(f"node:{p.name()}<{p.x_pos},{p.y_pos}>")
pass
pass

View File

@ -0,0 +1,339 @@
import sys, os
from abc import abstractmethod
from enum import Enum
from typing import List, Dict, Tuple
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
class PresentNodeType(Enum):
PresentNode = 0,
ConnectionNode = 1,
class GraphNode:
@abstractmethod
def node_type(self) -> PresentNodeType:
raise NotImplementedError("node_type")
@abstractmethod
def highlight(self, flag: bool):
raise NotImplementedError("highlight")
@abstractmethod
def is_highlighted(self) -> bool:
raise NotImplementedError("is_highlighted")
@abstractmethod
def boundingRect(self) -> QRectF:
raise NotImplementedError("boundingRect")
@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, 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
def sibling_append(self, node: 'PresentNode'):
if node not in self.__sibling_list:
self.__sibling_list.append(node)
pass
pass
def sibling_nodes(self) -> List['PresentNode']:
return self.__sibling_list
def node_type(self) -> PresentNodeType:
return PresentNodeType.PresentNode
def highlight(self, flag: bool):
self.__is_highlight_mark = flag
pass
def is_highlighted(self) -> bool:
return self.__is_highlight_mark
pass
def boundingRect(self) -> QRectF:
width_x = self.__font_bind.pixelSize() * (len(self.node_name)+1)
height_x = self.__font_bind.pixelSize()
return QRectF(0, 0, width_x + 10, height_x + 10)
pass
def paint(self, painter, option, widget = ...):
outline = self.boundingRect()
path_icon = QPainterPath()
path_icon.lineTo(outline.height()/2 - 5, outline.height() -5)
path_icon.lineTo(outline.height()/2, outline.height()/2)
path_icon.lineTo(outline.height() - 5, outline.height()/2 - 5)
path_icon.lineTo(0, 0)
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
#painter.drawRect(outline)
if self.__is_highlight_mark:
brush = Qt.red
painter.setPen(Qt.red)
else:
brush = Qt.black
painter.setPen(Qt.black)
pass
painter.fillPath(path_icon, brush)
painter.translate(outline.height(), 5)
painter.drawText(outline, self.node_name)
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):
QGraphicsItem.__init__(self, parent)
self.__highlight_mark = False
self.__point0 = p0
self.__point1 = p1
self.__outline = QRectF()
self.setZValue(1)
pass
def node_type(self) -> PresentNodeType:
return PresentNodeType.ConnectionNode
def highlight(self, flag: bool):
self.__highlight_mark = flag
pass
def is_highlighted(self) -> bool:
return self.__highlight_mark
pass
def relayout_exec(self, ppi: float = 1):
start_pos = self.__point0.pos()
end_pos = self.__point1.pos()
start_x = min(start_pos.x(), end_pos.x())
start_y = min(start_pos.y(), end_pos.y())
end_x = max(start_pos.x(), end_pos.x())
end_y = max(start_pos.y(), end_pos.y())
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):
return self.__outline
pass
def paint(self, painter, option, widget = ...):
start_pos = self.__point0.pos()
end_pos = self.__point1.pos()
outline = self.boundingRect()
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
if self.__highlight_mark:
pen = QPen(Qt.red)
else:
pen = QPen(Qt.lightGray)
pen.setWidthF(3)
painter.setPen(pen)
if start_pos.y() < end_pos.y():
if start_pos.x() < end_pos.x():
painter.drawLine(outline.topLeft(), outline.bottomRight())
else:
painter.drawLine(outline.topRight(), outline.bottomLeft())
else:
if start_pos.x() < end_pos.x():
painter.drawLine(outline.bottomLeft(), outline.topRight())
else:
painter.drawLine(outline.topLeft(), outline.bottomRight())
painter.restore()
pass
class UDGPresent(QGraphicsView):
node_clicked = pyqtSignal(str)
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()
self.__scene_bind = QGraphicsScene(self)
self.setScene(self.__scene_bind)
font = QFont()
font.setPixelSize(20)
self.setFont(font)
pass
def rebuild_from_edges(self, line_set: List[Line]):
self.node_set.clear()
for line in line_set:
start_node = line.points()[0]
self.__layout_graph.add_node(start_node.point_name)
end_node = line.points()[1]
self.__layout_graph.add_node(end_node.point_name)
self.__layout_graph.add_edge(start_node.point_name, end_node.point_name)
pass
pos_map = nx.spring_layout(self.__layout_graph)
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
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: 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(self.pixel_per_unit)
self.node_set[f"conn::{edge_start}&{edge_end}"] = connection
pass
pass
def mousePressEvent(self, event: QMouseEvent):
QGraphicsView.mousePressEvent(self, event)
if event.button() == Qt.MouseButton.LeftButton:
item: GraphNode = self.itemAt(event.pos())
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)
pass
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 = UDGPresent(None)
view.show()
list_in = [
Line(Point('a中古'), Point('b')),
Line(Point('a中古'), Point('c')),
Line(Point('a中古'), Point('d')),
]
view.rebuild_from_edges(list_in)
app.exec()

View File

View File

@ -0,0 +1,21 @@
import sys, os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
import networkx as nx
import matplotlib.pyplot as plt
g = nx.Graph()
g.add_node('a')
g.add_node('b')
g.add_node('c')
g.add_node('d')
g.add_edge('a', 'b')
g.add_edge('b','c')
g.add_edge('b','d')
posx = nx.spring_layout(g)
print(posx)
for n in posx:
print(n)
nx.draw(g, posx)
plt.show()

View File

@ -27,6 +27,24 @@ class FragmentSlice(EmptyNode):
pass 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: class StoryMap:
def __init__(self, name: str): def __init__(self, name: str):
self.story_name = name self.story_name = name
@ -44,7 +62,7 @@ class StoryMap:
self.slice_list.append(node) self.slice_list.append(node)
pass pass
def get_fragment_defined(self, name: str) -> FragmentSlice: def get_fragment_defined(self, name: str) -> FragmentSlice|None:
for fit in self.slice_list: for fit in self.slice_list:
if fit.is_define_node[0]: if fit.is_define_node[0]:
if fit.is_define_node[1] == name: if fit.is_define_node[1] == name:
@ -140,6 +158,58 @@ class XAST_ParseTool:
self.storylines_plait(story_dict) self.storylines_plait(story_dict)
return 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 pass