"""B07 평면도 글 자리 잡기(141-1) — 글끼리 · 장애물(IP 표 · 필지 상자)과 안 겹치게. 조각(`_piece_entities`)은 글을 바로 그리지 않고 후보 자리째 모은다. 여기서 우선순위 차례로 후보 중 첫 빈 자리를 고르고(못 찾으면 첫 후보) 멀리 놓인 글엔 지시선을 단다. 우선순위 = 시종점 > 측점 NO > IP > R > 배수 > 대피소. 서버 단독 계산(CLAUDE.md 5장 ③). 좌표 = 종이 mm. 글 사각 = 글 크기 × (한글 1.0 · 그 밖 0.6) 폭 · 글 크기 높이(세로 가운데 기준 — CAD 가 textBaseline middle 로 그림) · 방향이 있으면 돌린 사각의 바깥 상자. """ import math from typing import Any from B07_DesignDetail.B07_DesignDetail_Engine_Cad import _text_entity, polyline_entity PRIORITY = ("endpoints", "station", "ip", "radius", "drainage", "refuge") _GAP_MM = 0.6 # 글 사이 틈(종이 mm) Rect = tuple[float, float, float, float] def text_width(label: str, size: float) -> float: return sum(1.0 if ord(char) > 0x2E7F else 0.6 for char in label) * size def text_rect( label: str, x: float, y: float, size: float, align: str = "center", direction: tuple[float, float] = (1.0, 0.0), ) -> Rect: width = text_width(label, size) left = {"left": 0.0, "right": -width}.get(align, -width / 2.0) dx, dy = direction length = math.hypot(dx, dy) or 1.0 dx, dy = dx / length, dy / length corners = [ (x + cx * dx - cy * dy, y + cx * dy + cy * dx) for cx in (left, left + width) for cy in (-size / 2.0, size / 2.0) ] xs, ys = [p[0] for p in corners], [p[1] for p in corners] return min(xs), min(ys), max(xs), max(ys) def overlaps(a: Rect, b: Rect, gap: float = 0.0) -> bool: return a[0] < b[2] + gap and b[0] < a[2] + gap and a[1] < b[3] + gap and b[1] < a[3] + gap def _rect(item: dict[str, Any], spot: dict[str, Any]) -> Rect: return text_rect( item["label"], spot["x"], spot["y"], item["size"], spot.get("align", "center"), spot.get("direction", (1.0, 0.0)), ) def place_labels(labels: list[dict[str, Any]], obstacles: list[Rect]) -> list[dict[str, Any]]: """우선순위 차례로 후보 중 첫 빈 자리 — 글마다 고른 자리(`spot`) · 사각(`rect`)을 붙여 냄. labels = [{piece, seed, label, size, layer, color, anchor?, candidates: [{x, y, align?, direction?, far?}]}] — 첫 후보 = 옛 자리. """ taken = list(obstacles) placed = [] rank = {piece: index for index, piece in enumerate(PRIORITY)} for item in sorted(labels, key=lambda label: rank.get(label["piece"], len(PRIORITY))): rects = [_rect(item, spot) for spot in item["candidates"]] # ponytail: 후보가 모두 막히면 첫 후보 그대로(겹침 남음) — 더 필요하면 후보 고리를 늘림 index = next( ( i for i, rect in enumerate(rects) if not any(overlaps(rect, t, _GAP_MM) for t in taken) ), 0, ) taken.append(rects[index]) placed.append({**item, "spot": item["candidates"][index], "rect": rects[index]}) return placed def _nearest_on_rect(point: tuple[float, float], rect: Rect) -> tuple[float, float]: return (min(max(point[0], rect[0]), rect[2]), min(max(point[1], rect[1]), rect[3])) def label_entities(drawing_id: str, placed: list[dict[str, Any]]) -> list[dict[str, Any]]: """자리 잡은 글 + 멀리 놓인 글(후보 far)의 지시선.""" out: list[dict[str, Any]] = [] for item in placed: spot = item["spot"] out.append( _text_entity( item["seed"], item["label"], spot["x"], spot["y"], item["layer"], item["size"], item["color"], align=spot.get("align", "center"), direction=spot.get("direction", (1.0, 0.0)), ) ) anchor = item.get("anchor") if spot.get("far") and anchor: leader = polyline_entity( drawing_id, [anchor, _nearest_on_rect(anchor, item["rect"])], item["layer"], item["color"], suffix=f":leader:{item['seed']}", ) if leader: out.append(leader) return out