"""P2-B: SVG 시각화 좌표 계산 모듈. 벤 다이어그램, 원형 배치 등 시각화 블록의 좌표를 수학적으로 계산한다. 하드코딩 금지 — 모든 좌표는 N개 원소에 범용으로 계산. Phase 1: 3개 고정 SVG (검증 완료) Phase 2: N개 자동 배치 (이 모듈) """ from __future__ import annotations import math from typing import Any def calc_circle_positions( n: int, center_x: float = 300.0, center_y: float = 300.0, radius: float = 120.0, ) -> list[dict[str, float]]: """N개 원소를 원형으로 배치한다. 12시 방향부터 시계방향. Args: n: 원소 개수 (2~7) center_x: 큰 원 중심 X center_y: 큰 원 중심 Y radius: 큰 원 중심에서 작은 원 중심까지의 거리 Returns: [{"cx": float, "cy": float}, ...] 각 원소의 중심 좌표 """ if n <= 0: return [] positions = [] for i in range(n): # 12시(상단)부터 시계방향: -π/2에서 시작 angle = (2 * math.pi * i / n) - math.pi / 2 positions.append({ "cx": round(center_x + radius * math.cos(angle), 1), "cy": round(center_y + radius * math.sin(angle), 1), }) return positions def calc_item_radius(n: int, base_radius: float = 75.0) -> float: """N에 따라 작은 원 반지름을 자동 계산. 원소가 많아지면 작은 원이 작아져야 겹치지 않는다. 공식: base / (1 + max(0, n-3) * 0.2) """ if n <= 3: return base_radius shrink_factor = 1 + (n - 3) * 0.2 return round(base_radius / shrink_factor, 1) def calc_orbit_radius(n: int, base_orbit: float = 120.0) -> float: """N에 따라 궤도 반지름(큰 원 중심~작은 원 중심)을 자동 계산. 원소가 많아지면 궤도를 넓혀서 겹침 방지. """ if n <= 3: return base_orbit expand_factor = 1 + (n - 3) * 0.08 return round(base_orbit * expand_factor, 1) def calc_outer_radius(n: int, orbit_radius: float, item_radius: float) -> float: """큰 원 반지름을 계산. 모든 작은 원이 안에 들어가도록.""" # 궤도 + 작은 원 반지름 + 여백 margin = 40.0 return round(orbit_radius + item_radius + margin, 1) def prepare_venn_data( items: list[dict[str, Any]], center_label: str = "", center_sub: str = "", description: str = "", viewbox_width: float = 600.0, viewbox_height: float = 550.0, ) -> dict[str, Any]: """벤 다이어그램 렌더링에 필요한 전체 데이터를 준비한다. items에 cx, cy, r 좌표를 추가하고, 큰 원/SVG viewBox 크기를 계산. renderer.py에서 이 함수를 호출하여 Jinja2에 전달. Args: items: [{"label": "GIS", "color": "#059669", ...}, ...] center_label: 큰 원 중앙 텍스트 center_sub: 서브 텍스트 description: 하단 설명 viewbox_width: SVG viewBox 너비 viewbox_height: SVG viewBox 높이 Returns: Jinja2 템플릿에 전달할 dict """ n = len(items) if n == 0: return { "items": [], "center_label": center_label, "center_sub": center_sub, "description": description, } # 중심 좌표 (viewBox 기준) cx = viewbox_width / 2 cy = viewbox_height / 2 + 20 # 약간 아래로 (상단에 타이틀 공간) # N에 따른 자동 계산 orbit_r = calc_orbit_radius(n) item_r = calc_item_radius(n) outer_r = calc_outer_radius(n, orbit_r, item_r) # 각 원소 좌표 계산 positions = calc_circle_positions(n, center_x=cx, center_y=cy, radius=orbit_r) # items에 좌표 + 반지름 추가 for i, item in enumerate(items): item["cx"] = positions[i]["cx"] item["cy"] = positions[i]["cy"] item["r"] = item_r # 기본 색상 팔레트 (color가 없는 경우) default_colors = [ {"color": "#10b981", "color_light": "#6ee7b7"}, # 초록 {"color": "#3b82f6", "color_light": "#93c5fd"}, # 파랑 {"color": "#8b5cf6", "color_light": "#c4b5fd"}, # 보라 {"color": "#f59e0b", "color_light": "#fcd34d"}, # 노랑 {"color": "#ef4444", "color_light": "#fca5a5"}, # 빨강 {"color": "#06b6d4", "color_light": "#67e8f9"}, # 시안 {"color": "#ec4899", "color_light": "#f9a8d4"}, # 핑크 ] for i, item in enumerate(items): if "color" not in item: palette = default_colors[i % len(default_colors)] item["color"] = palette["color"] if "color_light" not in item: # color에서 밝은 버전 자동 생성 (간단: opacity 낮은 버전) item["color_light"] = item.get("color_light", item["color"] + "80") return { "items": items, "center_label": center_label, "center_sub": center_sub, "description": description, "outer_r": outer_r, "center_x": cx, "center_y": cy, "viewbox_width": viewbox_width, "viewbox_height": viewbox_height, }