diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 58cfe2b..c563925 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,8 +5,14 @@ + + + - + + + + - { - "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.ast_load.executor": "Debug", - "Python.entry.executor": "Run", - "RunOnceActivity.OpenProjectViewOnStart": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "git-widget-placeholder": "master", - "last_opened_file_path": "D:/Projects/Python/StoryCheckTools", - "settings.editor.selected.configurable": "reference.settings.ide.settings.new.ui" + - - - - +}]]> + + + - - - - + + + + + + + + + + - - - + + + - - - - diff --git a/entry.py b/entry.py index e6055c3..9ca187c 100644 --- a/entry.py +++ b/entry.py @@ -1,9 +1 @@ -from parse.StoryMap import XAST_ParseTool, storyline_list2map -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) \ No newline at end of file +print(__file__) \ No newline at end of file diff --git a/frame/ContentView.py b/frame/ContentView.py index 8e92d2a..330e7dc 100644 --- a/frame/ContentView.py +++ b/frame/ContentView.py @@ -2,7 +2,7 @@ import sys from typing import Dict, List from PyQt5.QtCore import QPoint -from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout from PyQt5.QtWidgets import QMenu from graph.DataType import Arrow, Point @@ -17,13 +17,15 @@ class FragmentPoint(Point): pass -class ContentWindow(QMainWindow): +class StorylinesView(QWidget): def __init__(self, parent): - QMainWindow.__init__(self, parent) + QWidget.__init__(self, parent) + layout = QVBoxLayout(self) self.fragment_view = DAGActiveView(self) - self.setCentralWidget(self.fragment_view) - self.fragment_view.nodes_clicked.connect(self.print_node_list) - self.present_graph: Dict[str, StoryMap] = None + layout.setContentsMargins(0,0,0,0) + layout.addWidget(self.fragment_view) + self.fragment_view.nodes_clicked.connect(self.highlisth_node_path) + self.present_graph: Dict[str, StoryMap] = {} pass pass @@ -56,7 +58,7 @@ class ContentWindow(QMainWindow): self.fragment_view.update_with_edges(arrows) pass - def print_node_list(self, xpos, ypos, list): + def highlisth_node_path(self, xpos, ypos, list): if len(list) == 0: return @@ -90,12 +92,12 @@ class ContentWindow(QMainWindow): pass if len(story_list) == 1: - self.print_node_list(xpos, ypos, [("node", story_list[0])]) + self.highlisth_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.print_node_list(xpos, ypos, [("node", story_name)]) + return lambda : self.highlisth_node_path(xpos, ypos, [("node", story_name)]) menu.addAction(f"story/{story}", trigger(story)) pass @@ -111,9 +113,15 @@ class ContentWindow(QMainWindow): pass +class ArticleRefsView(QWidget): + def __init__(self, parent): + QWidget.__init__(self, parent) + pass + + if __name__ == "__main__": app = QApplication(sys.argv) - view = ContentWindow(None) + view = StorylinesView(None) view.show() tool = XAST_ParseTool(global_ast_path) diff --git a/graph/DataType.py b/graph/DataType.py index 100a964..792bf0a 100644 --- a/graph/DataType.py +++ b/graph/DataType.py @@ -1,3 +1,4 @@ +import math from typing import List @@ -7,6 +8,44 @@ class Pos: self.y_pos = y 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': return Pos(self.x_pos, self.y_pos) @@ -23,6 +62,16 @@ class Point: 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: def __init__(self, p0: Point, p1: Point): self.point_set = [p0, p1] @@ -46,5 +95,3 @@ class Arrow(Line): def end_point(self): return self.point_set[1] - pass - diff --git a/graph/__pycache__/DataType.cpython-312.pyc b/graph/__pycache__/DataType.cpython-312.pyc index 20f65d0..da53a9f 100644 Binary files a/graph/__pycache__/DataType.cpython-312.pyc and b/graph/__pycache__/DataType.cpython-312.pyc differ diff --git a/graph/directed_acyclic_graph/DAGPresent.py b/graph/directed_acyclic_graph/DAGPresent.py index aad6521..ba5bcc3 100644 --- a/graph/directed_acyclic_graph/DAGPresent.py +++ b/graph/directed_acyclic_graph/DAGPresent.py @@ -422,18 +422,19 @@ class DAGActiveView(QGraphicsView): def mousePressEvent(self, event: QMouseEvent): QGraphicsView.mousePressEvent(self, event) - gitems = self.items(event.pos()) - noderef_names = [] - for gnode in gitems: - if gnode.node_key_bind[0].startswith("node"): - noderef_names.append(gnode.node_key_bind) + if event.button() == Qt.MouseButton.LeftButton: + gitems = self.items(event.pos()) + noderef_names = [] + for gnode in gitems: + if gnode.node_key_bind[0].startswith("node"): + noderef_names.append(gnode.node_key_bind) + pass 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 + if __name__ == "__main__": app = QApplication(sys.argv) view = DAGActiveView(None) diff --git a/graph/directed_acyclic_graph/__pycache__/DAGPresent.cpython-312.pyc b/graph/directed_acyclic_graph/__pycache__/DAGPresent.cpython-312.pyc index c960f85..5f20c03 100644 Binary files a/graph/directed_acyclic_graph/__pycache__/DAGPresent.cpython-312.pyc and b/graph/directed_acyclic_graph/__pycache__/DAGPresent.cpython-312.pyc differ diff --git a/graph/undirected_graph/UDGLayout.py b/graph/undirected_graph/UDGLayout.py new file mode 100644 index 0000000..8221bd0 --- /dev/null +++ b/graph/undirected_graph/UDGLayout.py @@ -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 diff --git a/graph/undirected_graph/UDGPresent.py b/graph/undirected_graph/UDGPresent.py new file mode 100644 index 0000000..b1bcd1f --- /dev/null +++ b/graph/undirected_graph/UDGPresent.py @@ -0,0 +1,285 @@ +import sys +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 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") + + +class PresentNode(QGraphicsItem, GraphNode): + def __init__(self, name: str, font: QFont, parent): + QGraphicsItem.__init__(self, parent) + self.node_name = name + self.__is_highlight_mark = False + self.__font_bind = font + self.__sibling_list = [] + 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 + + +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): + 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) + 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.__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() + + 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] + + 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) + 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] + connection = ConnectionNode(node_one, node_two, None) + self.scene().addItem(connection) + connection.relayout_exec() + 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.node_type() == PresentNodeType.PresentNode: + vnode: PresentNode = item + self.node_clicked.emit(vnode.node_name) + print(vnode.node_name) + pass + pass + 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() \ No newline at end of file diff --git a/graph/undirected_graph/__init__.py b/graph/undirected_graph/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graph/undirected_graph/test.py b/graph/undirected_graph/test.py new file mode 100644 index 0000000..836347e --- /dev/null +++ b/graph/undirected_graph/test.py @@ -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() \ No newline at end of file