191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
"""B07 상세 설계 FastAPI 라우터 — WebCAD PoC 임시 API.
|
|
|
|
⚠️ PoC 전용: DB·워크플로우 연동 없음. 종단·횡단도 형태를 모사한 샘플 도면을
|
|
ezdxf(MIT)로 생성하여 (1) 기하 JSON, (2) DXF 파일로 제공한다.
|
|
브라우저는 DXF를 파싱하지 않고 JSON만 렌더링한다 (GPL 배제 아키텍처).
|
|
Phase 2 본 구현 시 실제 B06 산출 데이터 기반으로 대체된다.
|
|
"""
|
|
|
|
import io
|
|
import logging
|
|
from typing import Any
|
|
|
|
import ezdxf
|
|
from fastapi import APIRouter, Query
|
|
from fastapi.responses import Response
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/b07", tags=["B07 Design Detail"])
|
|
|
|
# PoC 샘플 도면 레이어 구성 (name, ACI 색번호)
|
|
_POC_LAYERS: list[tuple[str, int]] = [
|
|
("GRID", 8), # 격자 — 회색
|
|
("GROUND", 42), # 자연 지반선 — 갈색 계열
|
|
("DESIGN", 1), # 계획선 — 빨강
|
|
("CENTER", 4), # 중심선 — 하늘색
|
|
("LABEL", 7), # 문자 — 흰색/검정
|
|
]
|
|
|
|
_VERTICAL_EXAGGERATION = 2.0
|
|
_BASE_ELEVATION = 100.0
|
|
|
|
|
|
def _ground_elevation(station: float) -> float:
|
|
"""샘플 자연 지반고(측점 거리 기반 결정적 파형)."""
|
|
import math
|
|
|
|
return 102.0 + station * 0.05 + 1.8 * math.sin(station / 37.0) + 0.6 * math.sin(station / 11.0)
|
|
|
|
|
|
def _design_elevation(station: float) -> float:
|
|
"""샘플 계획고(직선 종단 기울기 4%)."""
|
|
return 102.5 + station * 0.04
|
|
|
|
|
|
def _profile_y(elevation: float) -> float:
|
|
"""표고 → 종단면도 Y 좌표(수직 과장 적용)."""
|
|
return (elevation - _BASE_ELEVATION) * _VERTICAL_EXAGGERATION
|
|
|
|
|
|
def _build_sample_doc(stations: int, interval: float) -> "ezdxf.document.Drawing":
|
|
"""종단면도 + 횡단면도(3개소)를 모사한 샘플 DXF 문서 생성."""
|
|
doc = ezdxf.new("R2010", setup=True)
|
|
for name, aci in _POC_LAYERS:
|
|
doc.layers.add(name, color=aci)
|
|
msp = doc.modelspace()
|
|
|
|
length = (stations - 1) * interval
|
|
ground = [(i * interval, _ground_elevation(i * interval)) for i in range(stations)]
|
|
design = [(i * interval, _design_elevation(i * interval)) for i in range(stations)]
|
|
top = max(_profile_y(e) for _, e in ground) + 8.0
|
|
|
|
# --- 종단면도: 격자(측점 세로선 + 기준 가로선) ---
|
|
msp.add_lwpolyline(
|
|
[(0, 0), (length, 0), (length, top), (0, top)],
|
|
close=True,
|
|
dxfattribs={"layer": "GRID"},
|
|
)
|
|
for x, _ in ground:
|
|
msp.add_line((x, 0), (x, top), dxfattribs={"layer": "GRID"})
|
|
|
|
# --- 지반선·계획선 ---
|
|
msp.add_lwpolyline([(x, _profile_y(e)) for x, e in ground], dxfattribs={"layer": "GROUND"})
|
|
msp.add_lwpolyline([(x, _profile_y(e)) for x, e in design], dxfattribs={"layer": "DESIGN"})
|
|
|
|
# --- 측점 라벨·표고 문자 ---
|
|
for index, (x, elevation) in enumerate(ground):
|
|
msp.add_text(
|
|
f"No.{index}",
|
|
height=1.6,
|
|
dxfattribs={"layer": "LABEL", "insert": (x - 2.0, -4.0)},
|
|
)
|
|
msp.add_text(
|
|
f"{elevation:.2f}",
|
|
height=1.2,
|
|
rotation=90,
|
|
dxfattribs={"layer": "LABEL", "insert": (x + 0.5, _profile_y(elevation) + 1.5)},
|
|
)
|
|
msp.add_text(
|
|
"종단면도 (PoC)",
|
|
height=3.0,
|
|
dxfattribs={"layer": "LABEL", "insert": (length / 2 - 12.0, top + 4.0)},
|
|
)
|
|
|
|
# --- 횡단면도 3개소 (종단면도 아래 배치) ---
|
|
section_base_y = -30.0
|
|
half_width = 10.0
|
|
road_half = 2.0
|
|
for section_no in range(3):
|
|
cx = section_no * 34.0 + 10.0
|
|
design_y = section_base_y + 4.0
|
|
points = []
|
|
for offset_step in range(-5, 6):
|
|
offset = offset_step * (half_width / 5.0)
|
|
rel = 0.12 * offset + 0.02 * offset * offset * (1 if section_no % 2 else -1)
|
|
points.append((cx + offset, design_y + rel + 1.2))
|
|
msp.add_lwpolyline(points, dxfattribs={"layer": "GROUND"})
|
|
msp.add_line(
|
|
(cx - road_half, design_y),
|
|
(cx + road_half, design_y),
|
|
dxfattribs={"layer": "DESIGN"},
|
|
)
|
|
msp.add_line(
|
|
(cx, design_y - 2.5),
|
|
(cx, design_y + 4.5),
|
|
dxfattribs={"layer": "CENTER"},
|
|
)
|
|
msp.add_text(
|
|
f"No.{section_no} 횡단",
|
|
height=1.4,
|
|
dxfattribs={"layer": "LABEL", "insert": (cx - 4.0, section_base_y - 3.0)},
|
|
)
|
|
return doc
|
|
|
|
|
|
def _doc_to_geometry_json(doc: "ezdxf.document.Drawing") -> dict[str, Any]:
|
|
"""DXF 문서 → 뷰어 렌더링용 기하 JSON (LWPOLYLINE/LINE/TEXT만 사용)."""
|
|
entities: list[dict[str, Any]] = []
|
|
for entity in doc.modelspace():
|
|
kind = entity.dxftype()
|
|
if kind == "LWPOLYLINE":
|
|
entities.append(
|
|
{
|
|
"type": "LWPOLYLINE",
|
|
"layer": entity.dxf.layer,
|
|
"points": [[round(x, 4), round(y, 4)] for x, y, *_ in entity.get_points()],
|
|
"closed": bool(entity.closed),
|
|
}
|
|
)
|
|
elif kind == "LINE":
|
|
entities.append(
|
|
{
|
|
"type": "LINE",
|
|
"layer": entity.dxf.layer,
|
|
"start": [round(entity.dxf.start.x, 4), round(entity.dxf.start.y, 4)],
|
|
"end": [round(entity.dxf.end.x, 4), round(entity.dxf.end.y, 4)],
|
|
}
|
|
)
|
|
elif kind == "TEXT":
|
|
entities.append(
|
|
{
|
|
"type": "TEXT",
|
|
"layer": entity.dxf.layer,
|
|
"text": entity.dxf.text,
|
|
"insert": [round(entity.dxf.insert.x, 4), round(entity.dxf.insert.y, 4)],
|
|
"height": entity.dxf.height,
|
|
"rotation": entity.dxf.rotation,
|
|
}
|
|
)
|
|
return {
|
|
"layers": [{"name": name, "color_aci": aci} for name, aci in _POC_LAYERS],
|
|
"entities": entities,
|
|
}
|
|
|
|
|
|
@router.get("/poc/sample-drawing")
|
|
async def get_poc_sample_drawing(
|
|
stations: int = Query(default=11, ge=2, le=2000),
|
|
interval: float = Query(default=20.0, gt=0, le=100.0),
|
|
) -> dict[str, Any]:
|
|
"""샘플 종단·횡단 도면의 기하 JSON을 반환한다 (stations 증가로 부하 테스트 가능)."""
|
|
doc = _build_sample_doc(stations, interval)
|
|
payload = _doc_to_geometry_json(doc)
|
|
logger.info("B07 PoC sample drawing generated: %d entities", len(payload["entities"]))
|
|
return payload
|
|
|
|
|
|
@router.get("/poc/sample-drawing/dxf")
|
|
async def download_poc_sample_drawing(
|
|
stations: int = Query(default=11, ge=2, le=2000),
|
|
interval: float = Query(default=20.0, gt=0, le=100.0),
|
|
) -> Response:
|
|
"""동일 샘플 도면을 DXF 파일로 반환한다 (ezdxf 쓰기 왕복 검증용)."""
|
|
doc = _build_sample_doc(stations, interval)
|
|
buffer = io.StringIO()
|
|
doc.write(buffer)
|
|
return Response(
|
|
content=buffer.getvalue().encode("utf-8"),
|
|
media_type="application/dxf",
|
|
headers={"Content-Disposition": 'attachment; filename="b07_poc_sample.dxf"'},
|
|
)
|