Increase exported mapping image resolution to 4K and scale layout proportionally
- Значение MAPPING_MAX увеличено с 2200 до 4000 для повышения разрешения экспортируемого изображения до уровня 4K. - Добавлено пропорциональное масштабирование всех констант разметки, размеров шрифтов, толщины линий, отступов и скруглений на основе коэффициента base_scale. - Параметр масштабирования base_scale прокинут в функции отрисовки стрелок направления и компенсации пересечений.
This commit is contained in:
parent
5507d1c7c7
commit
3fe3afa3d7
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user