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