From d4bf6b186c7245f7a84c73130edc91c1f788a66e Mon Sep 17 00:00:00 2001 From: codeboss <2422523675@qq.com> Date: Fri, 2 Aug 2024 12:02:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=9A=84=E6=97=A0=E5=90=91?= =?UTF-8?q?=E5=9B=BE=E5=91=88=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 175 ++++++----- entry.py | 10 +- frame/ContentView.py | 28 +- graph/DataType.py | 51 +++- graph/__pycache__/DataType.cpython-312.pyc | Bin 3033 -> 6208 bytes graph/directed_acyclic_graph/DAGPresent.py | 17 +- .../__pycache__/DAGPresent.cpython-312.pyc | Bin 23959 -> 24107 bytes graph/undirected_graph/UDGLayout.py | 167 ++++++++++ graph/undirected_graph/UDGPresent.py | 285 ++++++++++++++++++ graph/undirected_graph/__init__.py | 0 graph/undirected_graph/test.py | 21 ++ 11 files changed, 645 insertions(+), 109 deletions(-) create mode 100644 graph/undirected_graph/UDGLayout.py create mode 100644 graph/undirected_graph/UDGPresent.py create mode 100644 graph/undirected_graph/__init__.py create mode 100644 graph/undirected_graph/test.py 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 20f65d0867f2a5073277b56d3a8908ed51eded70..da53a9f799765a1149d1c783a2f864f89d108aeb 100644 GIT binary patch literal 6208 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 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 c960f8570e0c6176ac3aea41c65a7d8a438319d6..5f20c03c108be6c0e7e8d26478c47c0a053ba1f9 100644 GIT binary patch delta 414 zcmbQfn{o9XM!wU$yj%=G;1ISZjmvH$-@!1(Uy~=A8! zoOxFb!(;<}5tbUpEPk+@X}C0_z~sPi$;nA_!jo&m`58qgKMWLRX2}wmT*xmqd2cxP zn>mQEG9qYguYu z$t|{|(vp(=ydr*(EVnO6)(I?fi_0f9tpviJyf9)8x(-y4$hpBPwpMJKS{5SF+u ztaedYZ9(BhVa@BpCKrWGHiTUfwz(mqcwI!}qKL){(d(Kv7d36Jh}hl`k+>ls@_`em z^8*h9ui$kaxr;n<*Ln1nFY+iaa9!@X&~vf(a^Ho%i~To*Zjam;xjA}!?8ey5@dt7* pgoIuQ3%}qNafK)H#^hI#8jLENg`?z|80{w;$1G&bntV0p5C91?ksSa4 delta 325 zcmV-L0lNOHyaAWH0S?Oz4GI7N007@!s%Ei0u@1pj0i=_WH%A0~0cr=6aaT0~2$O+Q z69)kq0001L3zNZDA(JsL69jw&Y6z2Y4;lvn00{t$J_M7%4;PcbI}`$Z0h6yL9g|`n z8v=X*ldmQnlj2tslYkKjlet(Y0SlAiSQnEXS(|^W0001h0Ne-_*9axi2qmCos72HW zGT;#vml}~8m_?sSnMs>V(-A0$2G|%Hhz-~k8}a}I2@!}z*bfxf4s*bETx2?Pld@C*P64c7=7(FhvX z2p=xd2p^yYrwO46qYI}Ep$(%Cuobr%u^F=)w;iz^vmd}M&`?p(Qd7`ERMZGn;FEq@ XC;=g}r&=EZ0X&n(T%iGIlWASTp+{`Z 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