chartflow 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {chartflow-0.1.4 → chartflow-0.1.5}/PKG-INFO +1 -1
  2. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/chart.py +2 -1
  3. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_node_base.py +29 -2
  4. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_node_interface.py +3 -1
  5. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_node_ports.py +49 -30
  6. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/connection_item.py +1877 -1636
  7. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/enums.py +1 -0
  8. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/node_canvas.py +22 -0
  9. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/node_item.py +59 -3
  10. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/popmenu.py +27 -2
  11. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/port_item.py +1 -1
  12. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/test_qnode.py +283 -3
  13. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow.egg-info/PKG-INFO +1 -1
  14. {chartflow-0.1.4 → chartflow-0.1.5}/setup.py +1 -1
  15. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/__init__.py +0 -0
  16. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/_verify_importexport.py +0 -0
  17. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/__init__.py +0 -0
  18. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/code_editor.py +0 -0
  19. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/code_folder.py +0 -0
  20. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/completer.py +0 -0
  21. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/context_analyzer.py +0 -0
  22. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/demo.py +0 -0
  23. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/line_number_area.py +0 -0
  24. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/minimap.py +0 -0
  25. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/python_editor.py +0 -0
  26. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/qcompletion.py +0 -0
  27. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/syntax_highlighter.py +0 -0
  28. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/edit/test_editor.py +0 -0
  29. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/__init__.py +0 -0
  30. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/_demo.py +0 -0
  31. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/editor.py +0 -0
  32. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/flow.py +0 -0
  33. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/qchart.py +0 -0
  34. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/test/__init__.py +0 -0
  35. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/test/test_chart.py +0 -0
  36. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/flow/test_decorator_sync.py +0 -0
  37. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/__init__.py +0 -0
  38. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/decorators.py +0 -0
  39. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/demo.py +0 -0
  40. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/demo_pyqt6.py +0 -0
  41. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/demo_tree.py +0 -0
  42. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/logic.py +0 -0
  43. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/meta.py +0 -0
  44. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/scheduler.py +0 -0
  45. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/stage.py +0 -0
  46. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/stdio.py +0 -0
  47. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/__init__.py +0 -0
  48. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_decorators.py +0 -0
  49. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_events.py +0 -0
  50. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_integration.py +0 -0
  51. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_logic.py +0 -0
  52. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_meta.py +0 -0
  53. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_runtime_param.py +0 -0
  54. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_scheduler.py +0 -0
  55. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test/test_stage.py +0 -0
  56. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test_behavior_events.py +0 -0
  57. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test_nested_parallel.py +0 -0
  58. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/test_stage_spec.py +0 -0
  59. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/fsm/utils.py +0 -0
  60. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/__init__.py +0 -0
  61. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_connection_interface.py +0 -0
  62. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_node_interaction.py +0 -0
  63. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_node_markers.py +0 -0
  64. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/_port_interface.py +0 -0
  65. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/arrow_item.py +0 -0
  66. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/color_utils.py +0 -0
  67. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/demo.py +0 -0
  68. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/demo_arrow_drag.py +0 -0
  69. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/demo_decorator.py +0 -0
  70. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/demo_ports.py +0 -0
  71. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/example.py +0 -0
  72. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/layout_utils.py +0 -0
  73. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/main_window.py +0 -0
  74. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/menu_bar.py +0 -0
  75. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/qconnection_editor.py +0 -0
  76. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/qnode_editor.py +0 -0
  77. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/selection_rect.py +0 -0
  78. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/style_panel.py +0 -0
  79. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/node/styles.py +0 -0
  80. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/showcase.py +0 -0
  81. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow/theme.py +0 -0
  82. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow.egg-info/SOURCES.txt +0 -0
  83. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow.egg-info/dependency_links.txt +0 -0
  84. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow.egg-info/requires.txt +0 -0
  85. {chartflow-0.1.4 → chartflow-0.1.5}/chartflow.egg-info/top_level.txt +0 -0
  86. {chartflow-0.1.4 → chartflow-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartflow
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Self-use. with fsm. node. chart. edit. Used for fsm and run, structure graph. use pyqt to show and interact
5
5
  Author-email: 2229066748@qq.com
6
6
  Maintainer: Eagle'sBaby
@@ -958,7 +958,8 @@ class Chart(IChart):
958
958
  shape_map = {
959
959
  "ellipse": NodeShape.ELLIPSE,
960
960
  "rectangle": NodeShape.RECTANGLE,
961
- "diamond": NodeShape.DIAMOND
961
+ "diamond": NodeShape.DIAMOND,
962
+ "hexagon": NodeShape.HEXAGON
962
963
  }
963
964
 
964
965
  # Create node mapping
@@ -398,7 +398,10 @@ class _NodeCore(QGraphicsEllipseItem):
398
398
 
399
399
  - 名称需容纳在内部区域(INNER_RATIO 以内); 有 hit 时额外预留
400
400
  (dot 深度 + 间距 + hit 文本宽度), 避免 hit 与名称重叠。
401
- - 矩形且有端口时: 宽度由名称(+hit)决定, 高度由最多端口的一侧决定(可长宽不等)。
401
+ - 矩形且有端口时: 宽度由名称(+hit)决定, 高度由最多端口的一侧决定
402
+ (可长宽不等, 但宽不短于高——顶/底边不短于左右边)。
403
+ - 六边形: 平顶/平底边长(=rw)须不短于斜边长 hypot(rw/2, rh),
404
+ 即 rw >= rh * 2/√3, 按此加宽(名称短时正方形不满足该约束)。
402
405
  - 其余(椭圆/菱形/无端口矩形): 保持方形。
403
406
  """
404
407
  tw, th = self._nameTextSize()
@@ -411,6 +414,8 @@ class _NodeCore(QGraphicsEllipseItem):
411
414
  port_extent = self._portInwardExtent()
412
415
  rw = max(rw_name, name_half + port_extent + NAME_INNER_PAD, self._min_radius)
413
416
  rh = max(rh_name, self._halfHeightForPorts(rw), self._min_radius)
417
+ # 矩形: 顶/底边(宽)不短于左右边(高); 名称短/端口多导致超高时加宽
418
+ rw = max(rw, rh)
414
419
  return rw, rh
415
420
 
416
421
  # 方形
@@ -421,6 +426,10 @@ class _NodeCore(QGraphicsEllipseItem):
421
426
  R = max(R, self._circlePortRadius())
422
427
  else:
423
428
  R = max(R, self._halfHeightForPorts(R))
429
+ if self._shape == NodeShape.HEXAGON:
430
+ # 六边形: 顶/底边 >= 斜边 -> rw >= rh * 2/√3; 加宽而非压扁,
431
+ # 以保留端口/名称的纵向空间
432
+ return R * 2.0 / math.sqrt(3.0), R
424
433
  return R, R
425
434
 
426
435
  def _setSize(self, rw: float, rh: float) -> None:
@@ -429,6 +438,12 @@ class _NodeCore(QGraphicsEllipseItem):
429
438
  # 圆形节点固定为标准圆(宽高相等), 保证端口沿弧线布局正确
430
439
  side = max(rw, rh)
431
440
  rw = rh = side
441
+ elif self._shape == NodeShape.HEXAGON:
442
+ # 六边形: 顶/底边 >= 斜边 -> rw >= rh * 2/√3(兜底, 含自定义半径路径)
443
+ rw = max(rw, rh * 2.0 / math.sqrt(3.0))
444
+ elif self._shape == NodeShape.RECTANGLE:
445
+ # 矩形: 顶/底边(宽)不短于左右边(高)(兜底)
446
+ rw = max(rw, rh)
432
447
  self._rw = rw
433
448
  self._rh = rh
434
449
  self._radius = rw # 兼容椭圆/菱形及旧代码
@@ -475,6 +490,7 @@ class _NodeCore(QGraphicsEllipseItem):
475
490
  - ELLIPSE: 圆(半径 + 外环)
476
491
  - RECTANGLE: 圆角矩形(外扩外环)
477
492
  - DIAMOND: 菱形(四顶点外扩外环)
493
+ - HEXAGON: 六边形(六顶点外扩外环)
478
494
  """
479
495
  rect = self.rect()
480
496
  m = self._hitShapeMargin()
@@ -488,7 +504,7 @@ class _NodeCore(QGraphicsEllipseItem):
488
504
  # 圆角半径与 paint() 一致(基于原始 rect, 否则圆角位置错位)
489
505
  corner = min(rect.width(), rect.height()) * 0.2
490
506
  path.addRoundedRect(outer, corner, corner)
491
- else: # DIAMOND
507
+ elif self._shape == NodeShape.DIAMOND:
492
508
  c = rect.center()
493
509
  hw = rect.width() / 2.0 + m
494
510
  hh = rect.height() / 2.0 + m
@@ -497,6 +513,17 @@ class _NodeCore(QGraphicsEllipseItem):
497
513
  path.lineTo(c.x(), c.y() + hh)
498
514
  path.lineTo(c.x() - hw, c.y())
499
515
  path.closeSubpath()
516
+ else: # HEXAGON
517
+ c = rect.center()
518
+ hw = rect.width() / 2.0 + m
519
+ hh = rect.height() / 2.0 + m
520
+ path.moveTo(c.x() - hw / 2.0, c.y() - hh)
521
+ path.lineTo(c.x() + hw / 2.0, c.y() - hh)
522
+ path.lineTo(c.x() + hw, c.y())
523
+ path.lineTo(c.x() + hw / 2.0, c.y() + hh)
524
+ path.lineTo(c.x() - hw / 2.0, c.y() + hh)
525
+ path.lineTo(c.x() - hw, c.y())
526
+ path.closeSubpath()
500
527
  return path
501
528
 
502
529
  def _centerText(self) -> None:
@@ -78,6 +78,7 @@ class INodeItem(Protocol):
78
78
  - ``ELLIPSE`` 椭圆/圆形(强制为宽高相等的标准圆)
79
79
  - ``RECTANGLE`` 圆角矩形
80
80
  - ``DIAMOND`` 菱形
81
+ - ``HEXAGON`` 六边形(平顶/平底, 左右尖角)
81
82
 
82
83
  赋值后若未设置自定义半径, 会按新形状重算尺寸; 切换到圆形时
83
84
  端口会从轴向排布切换为沿侧弧径向排布。
@@ -396,7 +397,8 @@ class INodeItem(Protocol):
396
397
  """计算从节点中心指向 ``target_pos`` 方向、落在节点外轮廓上的交点。
397
398
 
398
399
  依据当前 :attr:`nodeShape` 采用不同算法: 圆形用三角函数、
399
- 圆角矩形先求外接矩形交点再修正圆角、菱形用 L1 范数缩放。
400
+ 圆角矩形先求外接矩形交点再修正圆角、菱形用 L1 范数缩放、
401
+ 六边形取平顶边与斜边两组约束的较小射线交点。
400
402
 
401
403
  Args:
402
404
  target_pos: 目标位置(场景坐标)。
@@ -213,21 +213,22 @@ class _NodePortMixin:
213
213
  # 端口布局
214
214
  # ------------------------------------------------------------------ #
215
215
  def _layoutPorts(self) -> None:
216
- """分布端口: 圆形沿侧弧径向; 菱形沿左/右斜边贴合排布; 矩形沿边缘轴向。"""
216
+ """分布端口: 圆形沿侧弧径向; 菱形/六边形沿左/右斜边贴合排布; 矩形沿边缘轴向。"""
217
217
  is_circle = self._shape == NodeShape.ELLIPSE
218
- is_diamond = self._shape == NodeShape.DIAMOND
218
+ # 菱形与六边形(左右均为两条斜边)共用斜边贴合布局
219
+ is_slant = self._shape in (NodeShape.DIAMOND, NodeShape.HEXAGON)
219
220
  for side in ("left", "right"):
220
221
  for it in self._port_items[side]:
221
- # 互斥的排布模式: _radial(圆形) / _diamond(菱形) / 皆 False(矩形)
222
+ # 互斥的排布模式: _radial(圆形) / _diamond(斜边) / 皆 False(矩形)
222
223
  if it._radial != is_circle:
223
224
  it._radial = is_circle
224
225
  it.update()
225
- if it._diamond != is_diamond:
226
- it._diamond = is_diamond
226
+ if it._diamond != is_slant:
227
+ it._diamond = is_slant
227
228
  it.update()
228
229
  if is_circle:
229
230
  self._layoutPortsOnCircle()
230
- elif is_diamond:
231
+ elif is_slant:
231
232
  self._layoutPortsOnDiamond()
232
233
  else:
233
234
  self._layoutPortsAxisAligned()
@@ -277,16 +278,29 @@ class _NodePortMixin:
277
278
  it.setPos(QPointF(x, y))
278
279
  it.update()
279
280
 
281
+ def _slantInsetX(self) -> float:
282
+ """斜边上端点(顶/底侧)相对中心 x 的内缩量。
283
+
284
+ 菱形: 0(斜边直达中线上的顶/底尖角);
285
+ 六边形: half_w / 2(斜边从侧尖角连到平顶/平底边的端点)。
286
+ """
287
+ if self._shape == NodeShape.HEXAGON:
288
+ return self.rect().width() / 4.0
289
+ return 0.0
290
+
280
291
  def _layoutPortsOnDiamond(self) -> None:
281
- """菱形: 端口沿左/右两条斜边贴合排布, dot 旋转使底边贴合斜边、
292
+ """菱形/六边形: 端口沿左/右两条斜边贴合排布, dot 旋转使底边贴合斜边、
282
293
  apex 沿斜边内法线(指向节点内部), 出射方向沿斜边外法线。
283
294
 
284
295
  每侧(right/left)对应两条斜边(上、下), 总弧长 2L(L=单条斜边长)。
285
296
  端口沿弧长以 PORT_MIN_GAP 紧凑排布, 整组围绕侧顶点(右顶点/左顶点,
286
297
  即赤道)居中。**所有四个顶点(top/side/bottom 及对侧顶点)附近都预留
287
- 顶点间隙**, 故端口自然落在斜边中段, 不压在任何菱角上。
298
+ 顶点间隙**, 故端口自然落在斜边中段, 不压在任何尖角上。
299
+
300
+ 斜边端点由 _slantInsetX() 参数化: 菱形斜边连到中线的顶/底尖角,
301
+ 六边形斜边连到平顶/平底边的端点(x=±hw/2), 布局算法完全一致。
288
302
 
289
- 注意: 菱形斜边的内法线是**该斜边的常量法线**(垂直于斜边), 而非"指向
303
+ 注意: 斜边的内法线是**该斜边的常量法线**(垂直于斜边), 而非"指向
290
304
  圆心"的方向 —— 二者仅在斜边中点重合。dot 底边须平行于斜边, 故
291
305
  _orient_angle 必须取斜边内法线方向(使旋转后底边贴合斜边)。
292
306
  """
@@ -296,20 +310,22 @@ class _NodePortMixin:
296
310
  hh = rect.height() / 2.0
297
311
  if hw <= 0 or hh <= 0:
298
312
  return
299
- L = math.hypot(hw, hh) # 单条斜边长
300
- # 斜边内法线(指向节点内部, 垂直于斜边)。菱形四条斜边, 内法线方向固定。
301
- # 右上(TR: top->right) 斜边方向 (hw, hh), 内法线 (-hh, hw)
302
- # 右下(RB: right->bottom) 方向 (-hw, hh), 内法线 (-hh, -hw)
303
- # 左下(BL: bottom->left) 方向 (-hw, -hh), 内法线 (hh, -hw)
304
- # 左上(LT: left->top) 方向 (hw, -hh), 内法线 (hh, hw)
313
+ kx = self._slantInsetX()
314
+ sw = hw - kx # 斜边的水平跨度(菱形=hw, 六边形=hw/2)
315
+ L = math.hypot(sw, hh) # 单条斜边长
316
+ # 斜边内法线(指向节点内部, 垂直于斜边)。四条斜边, 内法线方向固定。
317
+ # 右上(TR: top->right) 斜边方向 (sw, hh), 内法线 (-hh, sw)
318
+ # 右下(RB: right->bottom) 方向 (-sw, hh), 内法线 (-hh, -sw)
319
+ # 左下(BL: bottom->left) 方向 (-sw, -hh), 内法线 (hh, -sw)
320
+ # 左上(LT: left->top) 方向 (sw, -hh), 内法线 (hh, sw)
305
321
  def inward_normal(ix, iy):
306
322
  nl = math.hypot(ix, iy)
307
323
  return (ix / nl, iy / nl) if nl > 1e-6 else (0.0, 0.0)
308
324
 
309
- NORM_TR = inward_normal(-hh, hw) # 右上斜边内法线
310
- NORM_RB = inward_normal(-hh, -hw) # 右下斜边内法线
311
- NORM_BL = inward_normal(hh, -hw) # 左下斜边内法线
312
- NORM_LT = inward_normal(hh, hw) # 左上斜边内法线
325
+ NORM_TR = inward_normal(-hh, sw) # 右上斜边内法线
326
+ NORM_RB = inward_normal(-hh, -sw) # 右下斜边内法线
327
+ NORM_BL = inward_normal(hh, -sw) # 左下斜边内法线
328
+ NORM_LT = inward_normal(hh, sw) # 左上斜边内法线
313
329
 
314
330
  # 四个顶点处的弧长预留(像素, 沿弧长方向): 避免端口压在任何菱角上。
315
331
  # top/bottom 顶点处一条斜边交汇, side 顶点处两条斜边交汇, 均需避让。
@@ -366,30 +382,31 @@ class _NodePortMixin:
366
382
  s_values.append(L + vertex_gap + off_c) # 下半中段内
367
383
  for it, s in zip(items, s_values):
368
384
  # s ∈ [0, 2L]; 上半斜边 s∈[0,L](top→侧顶点), 下半 s∈[L,2L](侧顶点→bottom)
385
+ # 斜边端点: 侧尖角(±hw,0) <-> 顶/底端点(±kx,∓hh); kx=0 即菱形
369
386
  if side == "right":
370
387
  if s <= L:
371
- # 上半(TR): top(0,-hh) -> right(+hw,0), t=s/L
388
+ # 上半(TR): (kx,-hh) -> right(+hw,0), t=s/L
372
389
  t = max(0.0, min(1.0, s / L))
373
- px = cx + hw * t
390
+ px = cx + kx + sw * t
374
391
  py = cy + (-hh) + hh * t
375
392
  nx, ny = NORM_TR
376
393
  else:
377
- # 下半(RB): right(+hw,0) -> bottom(0,+hh), t=(s-L)/L
394
+ # 下半(RB): right(+hw,0) -> (kx,+hh), t=(s-L)/L
378
395
  t = max(0.0, min(1.0, (s - L) / L))
379
- px = cx + hw + (-hw) * t
396
+ px = cx + hw - sw * t
380
397
  py = cy + hh * t
381
398
  nx, ny = NORM_RB
382
399
  else: # left
383
400
  if s <= L:
384
- # 上半(LT): top(0,-hh) -> left(-hw,0), t=s/L
401
+ # 上半(LT): (-kx,-hh) -> left(-hw,0), t=s/L
385
402
  t = max(0.0, min(1.0, s / L))
386
- px = cx + (-hw) * t
403
+ px = cx - kx - sw * t
387
404
  py = cy + (-hh) + hh * t
388
405
  nx, ny = NORM_LT
389
406
  else:
390
- # 下半(BL): left(-hw,0) -> bottom(0,+hh), t=(s-L)/L
407
+ # 下半(BL): left(-hw,0) -> (-kx,+hh), t=(s-L)/L
391
408
  t = max(0.0, min(1.0, (s - L) / L))
392
- px = cx + (-hw) + hw * t
409
+ px = cx - hw + sw * t
393
410
  py = cy + hh * t
394
411
  nx, ny = NORM_BL
395
412
  it._orient_angle = math.degrees(math.atan2(ny, nx))
@@ -397,15 +414,15 @@ class _NodePortMixin:
397
414
  it.update()
398
415
 
399
416
  def _diamondSideBias(self) -> float:
400
- """菱形每条斜边上端口组的"逻辑中点"参数(0=远端顶点, 1=侧顶点)。
417
+ """菱形/六边形每条斜边上端口组的"逻辑中点"参数(0=远端顶点, 1=侧顶点)。
401
418
 
402
419
  取 0.7: 端口组偏向侧顶点(左右两角), 体现"左右两侧"的视觉聚类,
403
- 同时仍处于斜边中段(避开顶点预留区), 不会压在任何菱角上。
420
+ 同时仍处于斜边中段(避开顶点预留区), 不会压在任何尖角上。
404
421
  """
405
422
  return 0.7
406
423
 
407
424
  def _diamondVertexGap(self) -> float:
408
- """菱形每个顶点处沿弧长预留的间隙(像素)。
425
+ """菱形/六边形每个顶点处沿弧长预留的间隙(像素)。
409
426
 
410
427
  四个顶点(top/底/左/右)处斜边交汇, 法线方向不连续, 端口压在顶点上
411
428
  视觉突兀。预留固定像素间隙使端口落在斜边中段。取 10px: 对常见尺寸
@@ -422,6 +439,8 @@ class _NodePortMixin:
422
439
  return corner + 4.0
423
440
  if self._shape == NodeShape.DIAMOND:
424
441
  return half_h * 0.30
442
+ if self._shape == NodeShape.HEXAGON:
443
+ return half_h * 0.25
425
444
  # ELLIPSE
426
445
  return half_h * 0.22
427
446