diff --git a/LEDMapper.spec b/LEDMapper.spec
index c882afb..ad89e27 100644
--- a/LEDMapper.spec
+++ b/LEDMapper.spec
@@ -1,12 +1,24 @@
# -*- mode: python ; coding: utf-8 -*-
+from pathlib import Path
+
+
+spec_dir = Path(SPECPATH)
+
a = Analysis(
- ['main.py'],
- pathex=['.'],
+ [str(spec_dir / 'main.py')],
+ pathex=[str(spec_dir), str(spec_dir.parent)],
binaries=[],
datas=[],
- hiddenimports=[],
+ hiddenimports=[
+ 'canvas',
+ 'icons',
+ 'main_window',
+ 'models',
+ 'panel_mapping_generator',
+ 'unfolding',
+ ],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
@@ -35,4 +47,5 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
+ icon=str(spec_dir / 'assets' / 'ledmapper.ico'),
)
diff --git a/assets/ledmapper.ico b/assets/ledmapper.ico
new file mode 100644
index 0000000..37acc48
Binary files /dev/null and b/assets/ledmapper.ico differ
diff --git a/canvas.py b/canvas.py
index 0f6a3c2..c5e00ae 100644
--- a/canvas.py
+++ b/canvas.py
@@ -529,15 +529,21 @@ class LedCanvas(QGraphicsView):
inner_alpha = 0.9 if is_active else 0.25
bridge_width = 1.9 if is_active else 1.0
bridge_alpha = 0.65 if is_active else 0.18
+ arrow_width = 1.8 if is_active else 1.0
+ arrow_alpha = 0.95 if is_active else 0.28
inner_path = QPainterPath()
+ inner_arrows = QPainterPath()
for block in line.blocks:
ordered_items = [self.items_by_id[point_id] for point_id in block.point_ids if point_id in self.items_by_id]
for first, second in zip(ordered_items, ordered_items[1:]):
self._append_connection(inner_path, first.pos(), second.pos())
+ self._append_arrow_marker(inner_arrows, first.pos(), second.pos())
self._add_connection_path(inner_path, line.color, inner_width, inner_alpha)
+ self._add_connection_path(inner_arrows, line.color, arrow_width, arrow_alpha, z_value=2)
previous_block_end = None
bridge_path = QPainterPath()
+ bridge_arrows = QPainterPath()
for block in line.blocks:
if not block.point_ids:
continue
@@ -545,22 +551,48 @@ class LedCanvas(QGraphicsView):
block_end = self.items_by_id.get(block.point_ids[-1])
if block_start is not None and previous_block_end is not None:
self._append_connection(bridge_path, previous_block_end.pos(), block_start.pos())
+ self._append_arrow_marker(bridge_arrows, previous_block_end.pos(), block_start.pos())
if block_end is not None:
previous_block_end = block_end
self._add_connection_path(bridge_path, line.color, bridge_width, bridge_alpha)
+ self._add_connection_path(bridge_arrows, line.color, arrow_width, bridge_alpha, z_value=2)
@staticmethod
def _append_connection(path: QPainterPath, start: QPointF, end: QPointF) -> None:
path.moveTo(start)
path.lineTo(end)
- def _add_connection_path(self, path: QPainterPath, color: str, width: float, alpha: float) -> None:
+ @staticmethod
+ def _append_arrow_marker(path: QPainterPath, start: QPointF, end: QPointF) -> None:
+ dx = end.x() - start.x()
+ dy = end.y() - start.y()
+ length = math.hypot(dx, dy)
+ if length < 8.0:
+ return
+ unit_x = dx / length
+ unit_y = dy / length
+ mid_x = start.x() + dx * 0.55
+ mid_y = start.y() + dy * 0.55
+ arrow_length = min(7.0, length * 0.28)
+ arrow_width = arrow_length * 0.55
+ base_x = mid_x - unit_x * arrow_length
+ base_y = mid_y - unit_y * arrow_length
+ perp_x = -unit_y
+ perp_y = unit_x
+ left = QPointF(base_x + perp_x * arrow_width, base_y + perp_y * arrow_width)
+ right = QPointF(base_x - perp_x * arrow_width, base_y - perp_y * arrow_width)
+ tip = QPointF(mid_x, mid_y)
+ path.moveTo(left)
+ path.lineTo(tip)
+ path.lineTo(right)
+
+ def _add_connection_path(self, path: QPainterPath, color: str, width: float, alpha: float, *, z_value: float = 1) -> None:
if path.isEmpty():
return
pen_color = QColor(color)
pen_color.setAlphaF(alpha)
item = self.scene().addPath(path, QPen(pen_color, width))
- item.setZValue(1)
+ item.setZValue(z_value)
self.connection_items.append(item)
def _selection_has_assignment(self) -> bool:
diff --git a/icons.py b/icons.py
index b46bf18..31d74ff 100644
--- a/icons.py
+++ b/icons.py
@@ -14,6 +14,7 @@ LUCIDE_PATHS = {
"file-plus": '',
"file-code": '',
"folder-open": '',
+ "git-branch": '',
"grid-2x2": '',
"layers": '',
"list-plus": '',
diff --git a/main.py b/main.py
index 520ef9d..4b4fe2f 100644
--- a/main.py
+++ b/main.py
@@ -1,15 +1,28 @@
from __future__ import annotations
import sys
+from pathlib import Path
from PySide6.QtWidgets import QApplication
-from main_window import MainWindow
+APP_DIR = Path(__file__).resolve().parent
+if str(APP_DIR) not in sys.path:
+ sys.path.insert(0, str(APP_DIR))
+
+try:
+ from .icons import lucide_icon
+ from .main_window import MainWindow
+except ImportError:
+ from icons import lucide_icon
+ from main_window import MainWindow
def main() -> int:
app = QApplication(sys.argv)
+ app_icon = lucide_icon("git-branch", "#e5e7eb", 256)
+ app.setWindowIcon(app_icon)
window = MainWindow()
+ window.setWindowIcon(app_icon)
window.show()
return app.exec()
diff --git a/qa_smoke.py b/qa_smoke.py
index 8d043e6..52d0cf5 100644
--- a/qa_smoke.py
+++ b/qa_smoke.py
@@ -81,6 +81,7 @@ def main() -> None:
test_canvas_rebuild_can_preserve_user_zoom,
test_grid_toggle_adds_grid_items,
test_canvas_connects_new_block_to_previous_non_empty_block,
+ test_canvas_draws_direction_arrows_on_connections,
test_canvas_rebuild_drops_deleted_selection_items,
test_canvas_refresh_does_not_accumulate_connection_items,
test_main_window_has_classic_menu_bar,
@@ -767,6 +768,32 @@ def test_canvas_connects_new_block_to_previous_non_empty_block() -> None:
assert bridge_found
+def test_canvas_draws_direction_arrows_on_connections() -> None:
+ if LedCanvas is None or QApplication is None:
+ return
+ app = QApplication.instance() or QApplication([])
+ with tempfile.TemporaryDirectory() as temp_dir:
+ path = Path(temp_dir) / "arrows.json"
+ points = [
+ {"id": 0, "x": 0.0, "y": 0.0, "row": 0, "col": 0},
+ {"id": 1, "x": 34.0, "y": 0.0, "row": 0, "col": 1},
+ {"id": 2, "x": 68.0, "y": 0.0, "row": 0, "col": 2},
+ ]
+ path.write_text(json.dumps({"circles": points}), encoding="utf-8")
+ doc = MappingDocument()
+ doc.load(path)
+ line = doc.add_line()
+ doc.add_block(line.id, [0, 1, 2])
+
+ canvas = LedCanvas()
+ canvas.set_document(doc)
+ canvas.set_active_line(line.id)
+
+ arrow_paths = [item.path() for item in canvas.connection_items if item.zValue() == 2]
+ assert arrow_paths
+ assert sum(path.elementCount() for path in arrow_paths) >= 6
+
+
def test_canvas_rebuild_drops_deleted_selection_items() -> None:
if LedCanvas is None or QApplication is None:
return
@@ -963,6 +990,7 @@ def test_lucide_icon_names_are_declared() -> None:
"file-plus",
"file-code",
"folder-open",
+ "git-branch",
"grid-2x2",
"layers",
"paintbrush",
@@ -1356,6 +1384,7 @@ def test_main_window_restores_tree_state_after_refresh_error() -> None:
def test_package_imports_work_from_project_root() -> None:
+ importlib.import_module("app.main")
importlib.import_module("app.canvas")
importlib.import_module("app.main_window")
diff --git a/requirements.txt b/requirements.txt
index da33986..0c6237f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-PySide6>=6.7
+PySide6==6.11.1
+pyinstaller==6.20.0