diff --git a/mapping_renderer.py b/mapping_renderer.py index 0993de4..a21752c 100644 --- a/mapping_renderer.py +++ b/mapping_renderer.py @@ -53,7 +53,7 @@ if TYPE_CHECKING: # --------------------------------------------------------------------------- # Layout constants # --------------------------------------------------------------------------- -MAPPING_MAX = 2200 # longest side of mapping area (pixels) +MAPPING_MAX = 4000 # longest side of mapping area (pixels) LEGEND_HEIGHT = 480 # legend strip at the bottom PADDING_FRAC = 0.04 # % padding inside mapping area @@ -87,6 +87,7 @@ def _overlap_control_point( start: QPointF, end: QPointF, segments: list[tuple[QPointF, QPointF]], + base_scale: float = 1.0, ) -> QPointF | None: """Return a quadratic Bezier control point if *start-end* overlaps an existing segment, otherwise None (straight line).""" @@ -114,21 +115,21 @@ def _overlap_control_point( # Perpendicular distance d0 = abs((prev_s.x() - start.x()) * unit_ay - (prev_s.y() - start.y()) * unit_ax) d1 = abs((prev_e.x() - start.x()) * unit_ay - (prev_e.y() - start.y()) * unit_ax) - if max(d0, d1) > 5.0: + if max(d0, d1) > 5.0 * base_scale: continue # Overlap projection b0 = (prev_s.x() - start.x()) * unit_ax + (prev_s.y() - start.y()) * unit_ay b1 = (prev_e.x() - start.x()) * unit_ax + (prev_e.y() - start.y()) * unit_ay ov_s = max(0.0, min(b0, b1)) ov_e = min(length, max(b0, b1)) - if (ov_e - ov_s) >= min(10.0, min(length, plen) * 0.35): + if (ov_e - ov_s) >= min(10.0 * base_scale, min(length, plen) * 0.35): previous_count += 1 segments.append((QPointF(start), QPointF(end))) if previous_count == 0: return None - bend = min(16.0, max(7.0, length * 0.24)) + bend = min(16.0 * base_scale, max(7.0 * base_scale, length * 0.24)) side = 1.0 if previous_count % 2 == 1 else -1.0 layer = 1.0 + (previous_count - 1) // 2 offset = side * bend * layer @@ -144,9 +145,10 @@ def _append_connection( start: QPointF, end: QPointF, segments: list[tuple[QPointF, QPointF]], + base_scale: float = 1.0, ) -> QPointF | None: path.moveTo(start) - control = _overlap_control_point(start, end, segments) + control = _overlap_control_point(start, end, segments, base_scale) if control is None: path.lineTo(end) else: @@ -154,11 +156,17 @@ def _append_connection( return control -def _append_arrow(path: QPainterPath, start: QPointF, end: QPointF, control: QPointF | None) -> None: +def _append_arrow( + path: QPainterPath, + start: QPointF, + end: QPointF, + control: QPointF | None, + base_scale: float = 1.0, +) -> None: dx = end.x() - start.x() dy = end.y() - start.y() length = math.hypot(dx, dy) - if length < 8.0: + if length < 8.0 * base_scale: return t = 0.55 if control is None: @@ -177,7 +185,7 @@ def _append_arrow(path: QPainterPath, start: QPointF, end: QPointF, control: QPo return unit_x = tan_x / tl unit_y = tan_y / tl - alen = min(7.0, length * 0.28) + alen = min(7.0 * base_scale, length * 0.28) awid = alen * 0.55 base_x = mid_x - unit_x * alen base_y = mid_y - unit_y * alen @@ -205,6 +213,9 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s block_text_color = QColor(DARK_BLOCK_TEXT if is_dark else "#475569") unassigned_color = QColor(UNASSIGNED if is_dark else "#cbd5e1") + # Scale factor based on resolution + base_scale = MAPPING_MAX / 2200.0 + # ── 1. World bounds ───────────────────────────────────────────────────── xs = [p.x for p in doc.points] ys = [p.y for p in doc.points] @@ -235,7 +246,7 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s py = ((max_y + pad_y) - y) * scale # flip y return QPointF(px, py) - dot_r = max(5.0, min(14.0, scale * 1.6)) + dot_r = max(5.0 * base_scale, min(14.0 * base_scale, scale * 1.6)) # ── 2. Lookups ─────────────────────────────────────────────────────────── point_by_uid: dict[int, object] = {p.uid: p for p in doc.points} @@ -260,16 +271,22 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s cur += n # 3. Pre-compute legend height so image is created at the right size - _lf_pre = LEGEND_LINE_ROW - _bf_pre = LEGEND_BLOCK_ROW + legend_line_row = LEGEND_LINE_ROW * base_scale + legend_block_row = LEGEND_BLOCK_ROW * base_scale + legend_v_pad = LEGEND_V_PAD * base_scale + legend_h_pad = LEGEND_H_PAD * base_scale + legend_col_gap = LEGEND_COL_GAP * base_scale + legend_swatch_w = LEGEND_SWATCH_W * base_scale + legend_swatch_h = LEGEND_SWATCH_H * base_scale + max_col_h_pre = max( - (LEGEND_LINE_ROW + len(ln.blocks) * LEGEND_BLOCK_ROW for ln in doc.lines), + (legend_line_row + len(ln.blocks) * legend_block_row for ln in doc.lines), default=0, ) - legend_height_pre = LEGEND_V_PAD * 2 + max(max_col_h_pre, 60) + legend_height_pre = legend_v_pad * 2 + max(max_col_h_pre, 60.0 * base_scale) img_w = map_px_w - img_h = map_px_h + legend_height_pre + img_h = map_px_h + round(legend_height_pre) image = QImage(img_w, img_h, QImage.Format.Format_ARGB32) image.fill(bg_color) @@ -279,9 +296,9 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s # ── 4. Connections ─────────────────────────────────────────────────────── segments: list[tuple[QPointF, QPointF]] = [] - line_w_inner = max(1.2, dot_r * 0.30) - line_w_bridge = max(1.0, dot_r * 0.20) - line_w_arrow = max(1.0, dot_r * 0.22) + line_w_inner = max(1.2 * base_scale, dot_r * 0.30) + line_w_bridge = max(1.0 * base_scale, dot_r * 0.20) + line_w_arrow = max(1.0 * base_scale, dot_r * 0.22) for line in doc.lines: lc = QColor(line.color) @@ -295,8 +312,8 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s for a, b in zip(pts, pts[1:]): s = to_px(a.x, a.y) # type: ignore[union-attr] e = to_px(b.x, b.y) # type: ignore[union-attr] - ctrl = _append_connection(inner_path, s, e, segments) - _append_arrow(arrow_path, s, e, ctrl) + ctrl = _append_connection(inner_path, s, e, segments, base_scale) + _append_arrow(arrow_path, s, e, ctrl, base_scale) c_inner = QColor(lc) c_inner.setAlphaF(0.85) @@ -326,8 +343,8 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s if first_pt is not None and prev_end_pt is not None: s = to_px(prev_end_pt.x, prev_end_pt.y) # type: ignore[union-attr] e = to_px(first_pt.x, first_pt.y) # type: ignore[union-attr] - ctrl = _append_connection(bridge_path, s, e, segments) - _append_arrow(b_arrow_path, s, e, ctrl) + ctrl = _append_connection(bridge_path, s, e, segments, base_scale) + _append_arrow(b_arrow_path, s, e, ctrl, base_scale) if last_pt is not None: prev_end_pt = last_pt @@ -363,7 +380,6 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s fm = QFontMetricsF(badge_font) # First pass: compute proposed rect for each badge - # badge list: (uid, text, badge_type) sorted so start badges come first badge_rects: list[tuple[str, str, QRectF, QColor]] = [] # (text, badge_type, rect, bg) def proposed_rect(uid: int, text: str, btype: str) -> tuple[QRectF, QColor]: @@ -373,14 +389,14 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s px = to_px(p.x, p.y) # type: ignore[union-attr] tw = fm.horizontalAdvance(text) th = fm.height() - bx = px.x() - tw / 2 - 5 - bw = tw + 10 - bh = th + 4 + bx = px.x() - tw / 2 - 5 * base_scale + bw = tw + 10 * base_scale + bh = th + 4 * base_scale if btype in ("start", "single"): - by = px.y() - dot_r - bh - 4 + by = px.y() - dot_r - bh - 4 * base_scale bg = QColor(BADGE_START if btype == "start" else BADGE_SINGLE) else: - by = px.y() + dot_r + 4 + by = px.y() + dot_r + 4 * base_scale bg = QColor(BADGE_END) return QRectF(bx, by, bw, bh), bg @@ -391,9 +407,9 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s # Build placed rects with nudge to avoid overlap placed: list[QRectF] = [] - NUDGE_STEP = fm.height() + 6 + NUDGE_STEP = fm.height() + 6 * base_scale # Maximum y for badge bottom must stay within mapping area - badge_max_y = map_px_h - 4 + badge_max_y = map_px_h - 4 * base_scale for uid, text, btype in raw_badges: rect, bg = proposed_rect(uid, text, btype) @@ -418,49 +434,49 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s for text, btype, rect, bg in badge_rects: painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QBrush(bg)) - painter.drawRoundedRect(rect, 3, 3) + painter.drawRoundedRect(rect, 3 * base_scale, 3 * base_scale) painter.setPen(QColor(Qt.GlobalColor.white)) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, text) # ── 7. Legend strip ─────────────────────────────────────────────────────── - title_font = QFont("Segoe UI", 16) + title_font = QFont("Segoe UI", int(16 * base_scale)) title_font.setBold(True) - block_font = QFont("Segoe UI", 13) + block_font = QFont("Segoe UI", int(13 * base_scale)) legend_y0 = map_px_h - legend_h = legend_height_pre + legend_h = round(legend_height_pre) painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QBrush(legend_bg_color)) painter.drawRect(0, legend_y0, img_w, legend_h) # divider - painter.setPen(QPen(divider_color, 2)) + painter.setPen(QPen(divider_color, int(2 * base_scale))) painter.drawLine(0, legend_y0, img_w, legend_y0) if doc.lines: n_lines = len(doc.lines) - avail_w = img_w - 2 * LEGEND_H_PAD - col_w = (avail_w - LEGEND_COL_GAP * (n_lines - 1)) / n_lines + avail_w = img_w - 2 * legend_h_pad + col_w = (avail_w - legend_col_gap * (n_lines - 1)) / n_lines for i, line in enumerate(doc.lines): - cx = float(LEGEND_H_PAD) + i * (col_w + LEGEND_COL_GAP) - cy = float(legend_y0 + LEGEND_V_PAD) + cx = float(legend_h_pad) + i * (col_w + legend_col_gap) + cy = float(legend_y0 + legend_v_pad) # Colour swatch + line name painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QBrush(QColor(line.color))) painter.drawRoundedRect( - QRectF(cx, cy + 4, LEGEND_SWATCH_W, LEGEND_SWATCH_H), 3, 3 + QRectF(cx, cy + 4 * base_scale, legend_swatch_w, legend_swatch_h), 3 * base_scale, 3 * base_scale ) painter.setFont(title_font) painter.setPen(line_text_color) painter.drawText( - QRectF(cx + LEGEND_SWATCH_W + 10, cy, col_w - LEGEND_SWATCH_W - 10, LEGEND_LINE_ROW), + QRectF(cx + legend_swatch_w + 10 * base_scale, cy, col_w - legend_swatch_w - 10 * base_scale, legend_line_row), Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, line.name, ) - cy += LEGEND_LINE_ROW + cy += legend_line_row # Blocks (top-to-bottom within column) cur = 0 @@ -473,15 +489,15 @@ def render_mapping_image(doc: "MappingDocument", file_path: str | Path, theme: s # Thin colour bar on the left painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(QBrush(QColor(line.color))) - painter.drawRect(QRectF(cx + 4, cy + 4, 3, LEGEND_BLOCK_ROW - 8)) + painter.drawRect(QRectF(cx + 4 * base_scale, cy + 4 * base_scale, 3 * base_scale, legend_block_row - 8 * base_scale)) painter.setPen(block_text_color) painter.drawText( - QRectF(cx + 16, cy, col_w - 16, LEGEND_BLOCK_ROW), + QRectF(cx + 16 * base_scale, cy, col_w - 16 * base_scale, legend_block_row), Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, f"{block.name}: {rng}", ) - cy += LEGEND_BLOCK_ROW + cy += legend_block_row painter.end() return image.save(str(file_path))