축척은 지어내는 값이 아니라 이미 정해져 있던 것 — `config_system` 의 도면별 축척 상수를 그대로 표제란에 싣는다. 도각이 `A1 = 1 :` 를 이미 찍으므로 **분모만** 낸다. - `scale_fields()` 신설 — A3 는 A1 도면을 절반으로 뽑는 종이라 분모가 2배. 가로·세로가 다른 도면(종단·토적)은 이름표를 붙여 함께 적는다. - 종단면도 `1000(H) · 200(V)` / 횡단면도 `100` / 유역도 `6000` / 토적도 `2000(H)`. A3 는 각각 2배. 담을 내용이 없는 빈 도면·표지는 축척이 없어 빈칸 그대로. 검증: `pytest tmp/tests/ -q` 135 passed / 0 failed(축척 2건 신규 — 상수 유래·A3 2배, 호출부가 넘긴 값이 도각에 실림). 백엔드 재시작 뒤 화면 실측(5174, wdw): 종단 `1000(H) · 200(V)`/`2000(H) · 400(V)`, 횡단 `100`/`200`, 유역도 `6000`/`12000`, 토적도 `2000(H)`/`4000(H)`, 표준도 빈칸. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
696 lines
24 KiB
Python
696 lines
24 KiB
Python
"""B07 종단도 CAD 조립 — 30측점 N분할 + 그래프 축·격자 + 하단 측점 테이블.
|
|
|
|
납품 양식:
|
|
- 그래프 영역: 좌측 Y축(표고 눈금·라벨), 기준선(X축, 최저 표고에서 5m 이상
|
|
여유), 측점별 회색 세로선.
|
|
- 테이블 영역: 측점 세로선 없이 가로 구분선 위 눈금(틱)으로 측점 표현,
|
|
값 텍스트는 세로쓰기. 행: 곡선/측점/거리/추가거리/지반고/계획고/절토고/
|
|
성토고/구배.
|
|
- 곡선행: 종단곡선 BVC·EVC 세로틱 + 수평선 + R/L 표기(브래킷형).
|
|
- 구배행: 구간 사선(상향/하향) + 가로 "구배% L=길이" + 구배 변화점에
|
|
원(내부 세로쓰기 계획고), 노선 시·종점은 반원.
|
|
"""
|
|
|
|
import math
|
|
from typing import Any
|
|
from uuid import uuid5
|
|
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
|
_ENTITY_NS,
|
|
DESIGN_COLOR,
|
|
DESIGN_LAYER_ID,
|
|
DRAWING_FORMAT,
|
|
FRAME_LAYER_ID,
|
|
GROUND_COLOR,
|
|
GROUND_LAYER_ID,
|
|
LONG_SPLIT_STATION_COUNT,
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LABEL_COLOR,
|
|
TABLE_LINE_COLOR,
|
|
TABLE_VALUE_COLOR,
|
|
_design_profile_points,
|
|
_format,
|
|
_interpolate,
|
|
_layer,
|
|
_line_entity,
|
|
_profile_alignment,
|
|
_stations,
|
|
_text_entity,
|
|
infer_station_interval,
|
|
points_from_samples,
|
|
polyline_entity,
|
|
station_no_label,
|
|
)
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
|
entities_bbox,
|
|
frame_entities,
|
|
scale_fields,
|
|
)
|
|
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
|
from config.config_system import DRAWING_SCALE_LONG_H, DRAWING_SCALE_LONG_V
|
|
|
|
# 도면 좌표 = 종이 mm. 실거리 1 m가 종이에서 차지하는 mm (1/1,000 -> 1.0, 1/200 -> 5.0).
|
|
MM_H = 1000.0 / DRAWING_SCALE_LONG_H
|
|
MM_V = 1000.0 / DRAWING_SCALE_LONG_V
|
|
|
|
# 종단 전용 레이어: 그래프 축·격자(잠금 — 참조용, 편집 제외).
|
|
LONG_GRID_LAYER_ID = "b08-long-grid"
|
|
GRID_COLOR = "#5b6572"
|
|
|
|
_VERTICAL = (0.0, 1.0) # 세로쓰기(아래→위)
|
|
|
|
# 테이블 행: (키, 헤더 라벨, 행 높이 m). 값 세로쓰기 행은 높게 잡는다.
|
|
_ROW_TALL = 9.0
|
|
_LONG_TABLE_ROWS: tuple[tuple[str, str, float], ...] = (
|
|
("curve", "곡선", 5.0),
|
|
("station", "측점", 7.0),
|
|
("distance", "거리", 4.5),
|
|
("cum_distance", "추가거리", _ROW_TALL),
|
|
("ground", "지반고", _ROW_TALL),
|
|
("design", "계획고", _ROW_TALL),
|
|
("cut", "절토고", _ROW_TALL),
|
|
("fill", "성토고", _ROW_TALL),
|
|
("grade", "구배", 10.0),
|
|
)
|
|
_FONT_SIZE = 2.2
|
|
_TICK_LEN = 0.8 # 측점 눈금 길이
|
|
|
|
|
|
def _circle_entity(
|
|
seed: str, center: tuple[float, float], radius: float, layer_id: str, color: str
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": str(uuid5(_ENTITY_NS, seed)),
|
|
"type": "Circle",
|
|
"lineColor": color,
|
|
"lineWidth": 1,
|
|
"layerId": layer_id,
|
|
"shapeData": {"center": {"x": center[0], "y": center[1]}, "radius": radius},
|
|
}
|
|
|
|
|
|
def _arc_entity(
|
|
seed: str,
|
|
center: tuple[float, float],
|
|
radius: float,
|
|
start_angle: float,
|
|
end_angle: float,
|
|
layer_id: str,
|
|
color: str,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"id": str(uuid5(_ENTITY_NS, seed)),
|
|
"type": "Arc",
|
|
"lineColor": color,
|
|
"lineWidth": 1,
|
|
"layerId": layer_id,
|
|
"shapeData": {
|
|
"center": {"x": center[0], "y": center[1]},
|
|
"radius": radius,
|
|
"startAngle": start_angle,
|
|
"endAngle": end_angle,
|
|
"counterClockwise": True,
|
|
},
|
|
}
|
|
|
|
|
|
def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""측점 30개 단위 분할 목록. 각 항목: {id, label, start_m, end_m}.
|
|
|
|
30개 이하면 단일 도면(id="longitudinal", 기존 manifest 호환). 초과 시
|
|
경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례).
|
|
"""
|
|
stations = sorted(_stations(longitudinal), key=lambda s: float(s["chainage_m"]))
|
|
if not stations:
|
|
return [{"id": "longitudinal", "label": "종단도", "start_m": None, "end_m": None}]
|
|
if len(stations) <= LONG_SPLIT_STATION_COUNT:
|
|
return [
|
|
{
|
|
"id": "longitudinal",
|
|
"label": "종단도",
|
|
"start_m": float(stations[0]["chainage_m"]),
|
|
"end_m": float(stations[-1]["chainage_m"]),
|
|
}
|
|
]
|
|
chunks: list[dict[str, Any]] = []
|
|
stride = LONG_SPLIT_STATION_COUNT - 1 # 경계 1측점 중복
|
|
index = 0
|
|
number = 1
|
|
while index < len(stations) - 1:
|
|
chunk_stations = stations[index : index + LONG_SPLIT_STATION_COUNT]
|
|
chunks.append(
|
|
{
|
|
"id": f"longitudinal_{number}",
|
|
"label": f"종단도({number})",
|
|
"start_m": float(chunk_stations[0]["chainage_m"]),
|
|
"end_m": float(chunk_stations[-1]["chainage_m"]),
|
|
}
|
|
)
|
|
index += stride
|
|
number += 1
|
|
return chunks
|
|
|
|
|
|
def _long_table_values(
|
|
stations: list[dict[str, Any]],
|
|
all_stations: list[dict[str, Any]],
|
|
ground_points: list[tuple[float, float]],
|
|
longitudinal: dict[str, Any],
|
|
interval_m: float,
|
|
) -> list[dict[str, str]]:
|
|
"""청크 측점별 테이블 셀 문자열. 거리는 노선 전체 기준 직전 측점과의 차."""
|
|
chainage_all = sorted(float(s["chainage_m"]) for s in all_stations)
|
|
rows: list[dict[str, str]] = []
|
|
for station in stations:
|
|
chainage = float(station["chainage_m"])
|
|
ground = station.get("center_z")
|
|
ground = (
|
|
float(ground)
|
|
if isinstance(ground, (int, float))
|
|
else _interpolate(ground_points, chainage)
|
|
)
|
|
design = design_elevation_from_longitudinal(longitudinal, chainage)
|
|
cut = max(ground - design, 0.0) if ground is not None and design is not None else None
|
|
fill = max(design - ground, 0.0) if ground is not None and design is not None else None
|
|
position = chainage_all.index(chainage) if chainage in chainage_all else -1
|
|
distance = chainage - chainage_all[position - 1] if position > 0 else None
|
|
rows.append(
|
|
{
|
|
"station": station_no_label(chainage, interval_m),
|
|
"distance": _format(distance, 1),
|
|
"cum_distance": _format(chainage, 1),
|
|
"ground": _format(ground),
|
|
"design": _format(design),
|
|
"cut": _format(cut) if cut else "",
|
|
"fill": _format(fill) if fill else "",
|
|
}
|
|
)
|
|
return rows
|
|
|
|
|
|
def _graph_grid_entities(
|
|
drawing_id: str,
|
|
chainages: list[float],
|
|
x0: float,
|
|
x1: float,
|
|
datum_m: float,
|
|
top_m: float,
|
|
) -> list[dict[str, Any]]:
|
|
"""그래프 영역: 기준선(X축)·Y축(표고 눈금/라벨)·측점 회색 세로선.
|
|
|
|
x는 종이 mm, datum_m·top_m은 실제 표고(m)로 받는다 — 눈금 라벨이 표고라서
|
|
m으로 돌고 좌표만 MM_V를 곱한다.
|
|
"""
|
|
# 기준선을 종이 y=0으로 두고 표고 차이만 MM_V로 올린다.
|
|
datum_y = 0.0
|
|
top_y = (top_m - datum_m) * MM_V
|
|
entities: list[dict[str, Any]] = [
|
|
# 기준선(X축)
|
|
_line_entity(
|
|
f"{drawing_id}:lgx", (x0, datum_y), (x1, datum_y), LONG_GRID_LAYER_ID, GRID_COLOR
|
|
),
|
|
# Y축
|
|
_line_entity(
|
|
f"{drawing_id}:lgy", (x0, datum_y), (x0, top_y), LONG_GRID_LAYER_ID, GRID_COLOR
|
|
),
|
|
]
|
|
# Y축 표고 눈금·라벨 (5m 간격, 가로쓰기)
|
|
level_m = datum_m
|
|
tick_index = 0
|
|
while level_m <= top_m + 1e-6:
|
|
level = (level_m - datum_m) * MM_V
|
|
entities.append(
|
|
_line_entity(
|
|
f"{drawing_id}:lgyt{tick_index}",
|
|
(x0 - 1.0, level),
|
|
(x0, level),
|
|
LONG_GRID_LAYER_ID,
|
|
GRID_COLOR,
|
|
)
|
|
)
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:lgyl{tick_index}",
|
|
_format(level_m),
|
|
x0 - 1.6,
|
|
level,
|
|
LONG_GRID_LAYER_ID,
|
|
_FONT_SIZE,
|
|
TABLE_LABEL_COLOR,
|
|
align="right",
|
|
)
|
|
)
|
|
level_m += 5.0
|
|
tick_index += 1
|
|
# 측점별 회색 세로선 (기준선 → 그래프 상단)
|
|
for index, x in enumerate(chainages):
|
|
entities.append(
|
|
_line_entity(
|
|
f"{drawing_id}:lgv{index}",
|
|
(x, datum_y),
|
|
(x, top_y),
|
|
LONG_GRID_LAYER_ID,
|
|
GRID_COLOR,
|
|
)
|
|
)
|
|
return entities
|
|
|
|
|
|
def _curve_row_entities(
|
|
drawing_id: str,
|
|
longitudinal: dict[str, Any],
|
|
x0: float,
|
|
x1: float,
|
|
y_top: float,
|
|
row_height: float,
|
|
) -> list[dict[str, Any]]:
|
|
"""곡선행: 종단곡선(BVC~EVC) 세로틱+수평선+R/L 표기 (브래킷형)."""
|
|
entities: list[dict[str, Any]] = []
|
|
curves = _profile_alignment(longitudinal).get("curves")
|
|
if not isinstance(curves, list):
|
|
return entities
|
|
y_line = y_top - row_height * 0.62
|
|
y_text = y_top - row_height * 0.30
|
|
tick = row_height * 0.24
|
|
for index, curve in enumerate(curves):
|
|
if not isinstance(curve, dict) or curve.get("omitted"):
|
|
continue
|
|
bvc = curve.get("bvc_m")
|
|
evc = curve.get("evc_m")
|
|
if not isinstance(bvc, (int, float)) or not isinstance(evc, (int, float)):
|
|
continue
|
|
start = max(float(bvc) * MM_H, x0)
|
|
end = min(float(evc) * MM_H, x1)
|
|
if end <= start:
|
|
continue
|
|
seed = f"{drawing_id}:lcurve:{index}"
|
|
entities.append(
|
|
_line_entity(
|
|
f"{seed}:l", (start, y_line), (end, y_line), LONG_TABLE_LAYER_ID, TABLE_LINE_COLOR
|
|
)
|
|
)
|
|
entities.append(
|
|
_line_entity(
|
|
f"{seed}:t0",
|
|
(start, y_line - tick),
|
|
(start, y_line + tick),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
entities.append(
|
|
_line_entity(
|
|
f"{seed}:t1",
|
|
(end, y_line - tick),
|
|
(end, y_line + tick),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
r_m = curve.get("r_m")
|
|
l_m = curve.get("l_m")
|
|
parts = []
|
|
if isinstance(r_m, (int, float)):
|
|
parts.append(f"R={r_m:g}")
|
|
if isinstance(l_m, (int, float)):
|
|
parts.append(f"L={l_m:g}")
|
|
if parts:
|
|
entities.append(
|
|
_text_entity(
|
|
f"{seed}:txt",
|
|
" ".join(parts),
|
|
(start + end) / 2.0,
|
|
y_text,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE * 0.9,
|
|
TABLE_VALUE_COLOR,
|
|
)
|
|
)
|
|
return entities
|
|
|
|
|
|
def _grade_break_points(
|
|
segments: list[Any], x0: float, x1: float, route_start: float, route_end: float
|
|
) -> list[tuple[float, bool]]:
|
|
"""구배 변화점 목록 [(chainage, 노선 시·종점 여부)] — 청크 범위 내만."""
|
|
boundaries: set[float] = set()
|
|
for segment in segments:
|
|
if not isinstance(segment, dict):
|
|
continue
|
|
for key in ("from_m", "to_m"):
|
|
value = segment.get(key)
|
|
if isinstance(value, (int, float)) and x0 - 1e-6 <= float(value) <= x1 + 1e-6:
|
|
boundaries.add(round(float(value), 4))
|
|
return [
|
|
(chainage, abs(chainage - route_start) < 1e-3 or abs(chainage - route_end) < 1e-3)
|
|
for chainage in sorted(boundaries)
|
|
]
|
|
|
|
|
|
def _grade_row_entities(
|
|
drawing_id: str,
|
|
longitudinal: dict[str, Any],
|
|
stations: list[dict[str, Any]],
|
|
x0: float,
|
|
x1: float,
|
|
y_top: float,
|
|
row_height: float,
|
|
route_start: float,
|
|
route_end: float,
|
|
) -> list[dict[str, Any]]:
|
|
"""구배행: 구간 사선 + 가로 구배 표기 + 변화점 원(내부 세로쓰기 계획고).
|
|
|
|
노선 시·종점은 반원(상반원)으로 표기한다. profile_alignment.segments가
|
|
없으면(구 폴백 계획선) 측점 간 계획고 차이 텍스트만 표기한다.
|
|
"""
|
|
entities: list[dict[str, Any]] = []
|
|
y_mid = y_top - row_height * 0.5
|
|
y_high = y_top - row_height * 0.18
|
|
y_low = y_top - row_height * 0.82
|
|
radius = row_height * 0.40
|
|
segments = _profile_alignment(longitudinal).get("segments")
|
|
if not (isinstance(segments, list) and segments):
|
|
# 폴백: 측점 간 계획고 차이 기반 구배 텍스트
|
|
for index in range(len(stations) - 1):
|
|
c0 = float(stations[index]["chainage_m"])
|
|
c1 = float(stations[index + 1]["chainage_m"])
|
|
d0 = design_elevation_from_longitudinal(longitudinal, c0)
|
|
d1 = design_elevation_from_longitudinal(longitudinal, c1)
|
|
span = c1 - c0
|
|
if d0 is None or d1 is None or span <= 0:
|
|
continue
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:lgrade:fb{index}",
|
|
f"{(d1 - d0) / span * 100:.2f}%",
|
|
(c0 + c1) / 2.0,
|
|
y_mid,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE * 0.9,
|
|
TABLE_VALUE_COLOR,
|
|
)
|
|
)
|
|
return entities
|
|
|
|
# 구간 사선 + 가로 구배/거리 표기 (원 반경만큼 안쪽으로 클리핑)
|
|
for index, segment in enumerate(segments):
|
|
if not isinstance(segment, dict):
|
|
continue
|
|
from_m = segment.get("from_m")
|
|
to_m = segment.get("to_m")
|
|
grade = segment.get("grade_percent")
|
|
if not isinstance(from_m, (int, float)) or not isinstance(to_m, (int, float)):
|
|
continue
|
|
start = max(float(from_m) * MM_H, x0) + radius
|
|
end = min(float(to_m) * MM_H, x1) - radius
|
|
if end <= start:
|
|
continue
|
|
seed = f"{drawing_id}:lgrade:{index}"
|
|
rising = isinstance(grade, (int, float)) and grade >= 0
|
|
entities.append(
|
|
_line_entity(
|
|
f"{seed}:d",
|
|
(start, y_low if rising else y_high),
|
|
(end, y_high if rising else y_low),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
if isinstance(grade, (int, float)):
|
|
length = segment.get("length_m")
|
|
length_text = f" L={length:g}" if isinstance(length, (int, float)) else ""
|
|
entities.append(
|
|
_text_entity(
|
|
f"{seed}:txt",
|
|
f"{grade:.2f}%{length_text}",
|
|
(start + end) / 2.0,
|
|
y_high + 0.2 if rising else y_low - 0.2,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE * 0.85,
|
|
TABLE_VALUE_COLOR,
|
|
)
|
|
)
|
|
|
|
# 구배 변화점: 원(정원) 또는 노선 시·종점 반원 + 내부 세로쓰기 계획고
|
|
for index, (chainage, is_route_end) in enumerate(
|
|
_grade_break_points(segments, x0 / MM_H, x1 / MM_H, route_start, route_end)
|
|
):
|
|
# chainage는 월드 거리 — 계획고 조회에 그대로 쓰고, 좌표에는 MM_H를 곱한다.
|
|
x_paper = chainage * MM_H
|
|
seed = f"{drawing_id}:lgradebp:{index}"
|
|
if is_route_end:
|
|
entities.append(
|
|
_arc_entity(
|
|
seed,
|
|
(x_paper, y_mid),
|
|
radius,
|
|
0.0,
|
|
math.pi,
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
else:
|
|
entities.append(
|
|
_circle_entity(
|
|
seed, (x_paper, y_mid), radius, LONG_TABLE_LAYER_ID, TABLE_LINE_COLOR
|
|
)
|
|
)
|
|
elevation = design_elevation_from_longitudinal(longitudinal, chainage)
|
|
if elevation is not None:
|
|
entities.append(
|
|
_text_entity(
|
|
f"{seed}:txt",
|
|
_format(elevation),
|
|
x_paper,
|
|
y_mid,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE * 0.8,
|
|
TABLE_VALUE_COLOR,
|
|
direction=_VERTICAL,
|
|
)
|
|
)
|
|
return entities
|
|
|
|
|
|
def _long_table_entities(
|
|
drawing_id: str,
|
|
stations: list[dict[str, Any]],
|
|
values: list[dict[str, str]],
|
|
longitudinal: dict[str, Any],
|
|
table_top: float,
|
|
interval_m: float,
|
|
route_start: float,
|
|
route_end: float,
|
|
) -> list[dict[str, Any]]:
|
|
"""종단 테이블: 가로 구분선 + 측점 눈금(세로선 없음) + 세로쓰기 값."""
|
|
chainages = [float(s["chainage_m"]) * MM_H for s in stations]
|
|
header_width = max(interval_m * MM_H, 12.0)
|
|
left = chainages[0] - header_width
|
|
right = chainages[-1]
|
|
entities: list[dict[str, Any]] = []
|
|
|
|
# 행 y 경계 (가변 행 높이)
|
|
boundaries = [table_top]
|
|
for _key, _header, height in _LONG_TABLE_ROWS:
|
|
boundaries.append(boundaries[-1] - height)
|
|
bottom = boundaries[-1]
|
|
|
|
# 가로 구분선 + 측점 눈금(구분선 아래 짧은 틱)
|
|
for row_index, y in enumerate(boundaries):
|
|
entities.append(
|
|
_line_entity(
|
|
f"{drawing_id}:ltgrid:h{row_index}",
|
|
(left, y),
|
|
(right, y),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
if row_index < len(boundaries) - 1:
|
|
for column_index, x in enumerate(chainages):
|
|
entities.append(
|
|
_line_entity(
|
|
f"{drawing_id}:lttick:{row_index}:{column_index}",
|
|
(x, y),
|
|
(x, y - _TICK_LEN),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
# 외곽·헤더 구분 세로선 (측점 세로선은 없음)
|
|
for seed, x in (("v-left", left), ("v-header", chainages[0]), ("v-right", right)):
|
|
entities.append(
|
|
_line_entity(
|
|
f"{drawing_id}:ltgrid:{seed}",
|
|
(x, table_top),
|
|
(x, bottom),
|
|
LONG_TABLE_LAYER_ID,
|
|
TABLE_LINE_COLOR,
|
|
)
|
|
)
|
|
|
|
for row_index, (key, header, height) in enumerate(_LONG_TABLE_ROWS):
|
|
y_top = boundaries[row_index]
|
|
y_mid = y_top - height * 0.5
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:ltable:header:{key}",
|
|
header,
|
|
left + header_width / 2.0,
|
|
y_mid,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE,
|
|
TABLE_LABEL_COLOR,
|
|
)
|
|
)
|
|
if key == "curve":
|
|
entities.extend(
|
|
_curve_row_entities(drawing_id, longitudinal, chainages[0], right, y_top, height)
|
|
)
|
|
continue
|
|
if key == "grade":
|
|
entities.extend(
|
|
_grade_row_entities(
|
|
drawing_id,
|
|
longitudinal,
|
|
stations,
|
|
chainages[0],
|
|
right,
|
|
y_top,
|
|
height,
|
|
route_start,
|
|
route_end,
|
|
)
|
|
)
|
|
continue
|
|
for value_index, row_values in enumerate(values):
|
|
label = row_values.get(key, "")
|
|
if not label:
|
|
continue
|
|
if key == "distance":
|
|
# 거리는 직전 측점과의 구간 중앙, 가로쓰기 (청크 첫 측점 제외)
|
|
if value_index == 0:
|
|
continue
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:ltable:{key}:{value_index}",
|
|
label,
|
|
(chainages[value_index - 1] + chainages[value_index]) / 2.0,
|
|
y_mid,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE * 0.9,
|
|
TABLE_VALUE_COLOR,
|
|
)
|
|
)
|
|
continue
|
|
# 측점·추가거리·지반고·계획고·절토고·성토고: 측점 위치 세로쓰기
|
|
entities.append(
|
|
_text_entity(
|
|
f"{drawing_id}:ltable:{key}:{value_index}",
|
|
label,
|
|
chainages[value_index],
|
|
y_mid,
|
|
LONG_TABLE_LAYER_ID,
|
|
_FONT_SIZE,
|
|
TABLE_VALUE_COLOR,
|
|
direction=_VERTICAL,
|
|
)
|
|
)
|
|
return entities
|
|
|
|
|
|
def build_longitudinal_drawing(
|
|
longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
"""종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다."""
|
|
start_m = chunk.get("start_m")
|
|
end_m = chunk.get("end_m")
|
|
|
|
def in_range(x: float) -> bool:
|
|
if start_m is None or end_m is None:
|
|
return True
|
|
return start_m - 1e-6 <= x <= end_m + 1e-6
|
|
|
|
ground_all = points_from_samples(longitudinal.get("samples", []), "chainage_m")
|
|
ground_points = [point for point in ground_all if in_range(point[0])]
|
|
design_points = [point for point in _design_profile_points(longitudinal) if in_range(point[0])]
|
|
all_stations = _stations(longitudinal)
|
|
stations = sorted(
|
|
(s for s in all_stations if in_range(float(s["chainage_m"]))),
|
|
key=lambda s: float(s["chainage_m"]),
|
|
)
|
|
interval_m = infer_station_interval(all_stations)
|
|
|
|
# 좌표는 종이 mm — 거리에 MM_H, 표고는 기준선(datum) 기준으로 MM_V를 곱한다.
|
|
elevations = [y for _x, y in [*ground_points, *design_points]]
|
|
min_e = min(elevations) if elevations else 0.0
|
|
max_e = max(elevations) if elevations else 10.0
|
|
# 기준선(datum): 최저 표고에서 5m 이상 여유를 두고 5m 단위로 내림 → 종이 y=0.
|
|
datum_m = math.floor((min_e - 5.0) / 5.0) * 5.0
|
|
top_m = max_e + 3.0
|
|
|
|
def paper(point: tuple[float, float]) -> tuple[float, float]:
|
|
return (point[0] * MM_H, (point[1] - datum_m) * MM_V)
|
|
|
|
entities: list[dict[str, Any]] = []
|
|
ground = polyline_entity(
|
|
drawing_id, [paper(p) for p in ground_points], GROUND_LAYER_ID, GROUND_COLOR
|
|
)
|
|
if ground:
|
|
entities.append(ground)
|
|
design = polyline_entity(
|
|
drawing_id, [paper(p) for p in design_points], DESIGN_LAYER_ID, DESIGN_COLOR
|
|
)
|
|
if design:
|
|
entities.append(design)
|
|
|
|
if stations:
|
|
chainages = [float(s["chainage_m"]) * MM_H for s in stations]
|
|
entities.extend(
|
|
_graph_grid_entities(drawing_id, chainages, chainages[0], chainages[-1], datum_m, top_m)
|
|
)
|
|
|
|
values = _long_table_values(stations, all_stations, ground_all, longitudinal, interval_m)
|
|
all_chainages = sorted(float(s["chainage_m"]) for s in all_stations)
|
|
table_top = -3.0 # 그래프-테이블 영역 분리 간격(mm, 기준선 y=0 아래)
|
|
entities.extend(
|
|
_long_table_entities(
|
|
drawing_id,
|
|
stations,
|
|
values,
|
|
longitudinal,
|
|
table_top,
|
|
interval_m,
|
|
all_chainages[0],
|
|
all_chainages[-1],
|
|
)
|
|
)
|
|
|
|
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
|
bbox = entities_bbox(entities)
|
|
if bbox:
|
|
entities.extend(
|
|
frame_entities(
|
|
drawing_id,
|
|
bbox,
|
|
fit=False,
|
|
fields={
|
|
"도면명": "종단면도",
|
|
**scale_fields(("H", DRAWING_SCALE_LONG_H), ("V", DRAWING_SCALE_LONG_V)),
|
|
},
|
|
)
|
|
)
|
|
|
|
return {
|
|
"format": DRAWING_FORMAT,
|
|
"entities": entities,
|
|
"layers": [
|
|
_layer(GROUND_LAYER_ID, "원지반", locked=True),
|
|
_layer(DESIGN_LAYER_ID, "계획선"),
|
|
_layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True),
|
|
_layer(LONG_TABLE_LAYER_ID, "측점표"),
|
|
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
|
],
|
|
}
|