- 덧그림 범위를 서버가 화면과 같은 TS 로 셈 — Node 진입점 B07_DesignDetail_Cross_Extent_Node(최소 DOM 위에서 오프스크린 SVG 그리기 그대로) · 번들 build:b07-extent - 화면 그리기·수확 부분을 B07_DesignDetail_Cross_Overlay 로 떼어 화면과 Node 가 한 벌로 씀 - 장 배치·블록 테두리가 측점별 덧그림 범위를 덮음(목록·장 열기·확정·수정 네 길 모두 같은 계획) - 글자는 CAD 그리기대로 세로 가운데 · 폭은 글꼴 어림(한글 1.0 · 나머지 0.6 배) - 번들 낡음 판정에 B07 맨 위 TS 포함 - 검증_sub4: 66개 박스 ↔ 그림(덧그림·글자 실폭 포함) 모두 1.0 mm · 잘림 0 · 겹침 0 · 20장 → 21장 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
81 lines
3.2 KiB
Python
81 lines
3.2 KiB
Python
"""B07 횡단 장 배치용 **덧그림 범위** — 화면이 B06 산식으로 덧그리는 설계선·구조물이
|
|
측점마다 차지하는 범위(오프셋·표고 m)를 서버가 잰다(68-3).
|
|
|
|
계산은 새로 짜지 않는다 — 화면이 쓰는 TS(`B07_DesignDetail_Cross_Overlay`)를 Node
|
|
진입점(`B07_DesignDetail_Cross_Extent_Node.ts`)으로 그대로 돌린다(CLAUDE.md 5장 ②).
|
|
입력은 화면이 받는 것과 같은 종횡단 상세(`get_section_detail`)다.
|
|
|
|
실패는 비치명적이다 — 빈 범위를 내면 장 배치는 서버 그림만으로 선다(68-1 동작).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from B05_Profile.B05_Profile_Engine_Sections import cross_station
|
|
from common_util.common_util_node_bundle import bundle_stale, run_bundle_json
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BUNDLE = ROOT / "config" / "b07_extent_node" / "B07_DesignDetail_Cross_Extent_Node.js"
|
|
_NPM_SCRIPT = "build:b07-extent"
|
|
|
|
Extent = tuple[float, float, float, float]
|
|
|
|
# 같은 상세면 같은 범위 — 목록 · 장마다 다시 부르므로 상세 내용으로 기억한다.
|
|
# ponytail: 프로세스 메모리 · 최근 몇 벌만. 서버 여러 대면 파일 캐시로.
|
|
_CACHE: dict[str, dict[int | float, Extent]] = {}
|
|
_CACHE_LIMIT = 8
|
|
|
|
|
|
def _extents_from(output: Any) -> dict[int | float, Extent]:
|
|
extents: dict[int | float, Extent] = {}
|
|
rows = output.get("extents") if isinstance(output, dict) else None
|
|
for row in rows if isinstance(rows, list) else []:
|
|
try:
|
|
extents[cross_station(float(row["chainage_m"]))] = (
|
|
float(row["x_min"]),
|
|
float(row["x_max"]),
|
|
float(row["y_min"]),
|
|
float(row["y_max"]),
|
|
)
|
|
except (KeyError, TypeError, ValueError):
|
|
continue
|
|
return extents
|
|
|
|
|
|
async def cross_overlay_extents(project_id: UUID | str, route_id: int) -> dict[int | float, Extent]:
|
|
"""{측점: (오프셋 최소, 최대, 표고 최소, 최대)} — 덧그림이 없는 측점은 빠진다."""
|
|
from B06_Section.B06_Section_Router import get_section_detail
|
|
|
|
try:
|
|
response = await get_section_detail(UUID(str(project_id)), route_id)
|
|
dump = getattr(response, "model_dump", None)
|
|
if dump is None: # JSONResponse = 실패
|
|
return {}
|
|
detail = dump(mode="json")
|
|
key = hashlib.sha1(
|
|
json.dumps(detail, sort_keys=True, default=str).encode("utf-8")
|
|
).hexdigest()
|
|
# 번들이 낡았으면(화면 TS 가 바뀜) 기억을 버리고 다시 잰다 — 다시 빌드도 함께.
|
|
if key in _CACHE and not await asyncio.to_thread(bundle_stale, BUNDLE):
|
|
return _CACHE[key]
|
|
output = await asyncio.to_thread(run_bundle_json, BUNDLE, _NPM_SCRIPT, {"detail": detail})
|
|
except Exception:
|
|
logger.warning("B07 덧그림 범위 계산 실패 — 서버 그림만으로 배치", exc_info=True)
|
|
return {}
|
|
if output is None:
|
|
return {}
|
|
extents = _extents_from(output)
|
|
if len(_CACHE) >= _CACHE_LIMIT:
|
|
_CACHE.pop(next(iter(_CACHE)))
|
|
_CACHE[key] = extents
|
|
return extents
|