fix(B07): 종단도 장마다 그림 좌하단을 작도영역 좌하단에 맞춤 · 넘치면 장 더 나눔
- frame_entities bottom_left 선택 — 종단도만 좌하단 맞춤(나머지 중심 맞춤 그대로) - longitudinal_chunks: 30측점 묶음이 A1 작도영역을 넘으면 측점을 줄여 장을 더 나눔 · 축척 H 1/1,000 · V 1/200 고정 - 원·호 엔티티 헬퍼를 Engine_Cad.py 로 옮김(Long.py 700줄 아래) - 검증_sub4 종단 3장 그림 좌하단(도각 기준) 180,132 · 180,193 · 363,220 → 모두 47.05,52.04 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -75,6 +75,44 @@ def _line_entity(
|
||||
}
|
||||
|
||||
|
||||
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 polyline_entity(
|
||||
drawing_id: str,
|
||||
points: list[tuple[float, float]],
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
|
||||
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,
|
||||
@@ -28,6 +26,8 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
TABLE_LABEL_COLOR,
|
||||
TABLE_LINE_COLOR,
|
||||
TABLE_VALUE_COLOR,
|
||||
_arc_entity,
|
||||
_circle_entity,
|
||||
_design_profile_points,
|
||||
_format,
|
||||
_interpolate,
|
||||
@@ -45,6 +45,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
entities_bbox,
|
||||
frame_entities,
|
||||
scale_fields,
|
||||
usable_area,
|
||||
)
|
||||
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
|
||||
@@ -76,79 +77,50 @@ _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]:
|
||||
def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, float]:
|
||||
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},
|
||||
"start_m": float(stations[index]["chainage_m"]),
|
||||
"end_m": float(stations[index + take - 1]["chainage_m"]),
|
||||
}
|
||||
|
||||
|
||||
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 _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool:
|
||||
"""이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 드는가."""
|
||||
bbox = entities_bbox(_long_content(longitudinal, "probe", span))
|
||||
if bbox is None:
|
||||
return True
|
||||
usable_w, usable_h = usable_area()
|
||||
return bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6
|
||||
|
||||
|
||||
def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""측점 30개 단위 분할 목록. 각 항목: {id, label, start_m, end_m}.
|
||||
|
||||
30개 이하면 단일 도면(id="longitudinal", 기존 manifest 호환). 초과 시
|
||||
한 장이면 단일 도면(id="longitudinal", 기존 manifest 호환). 여러 장이면
|
||||
경계 측점 1개를 중복시켜 도면 간 선이 이어지게 한다(납품 도면 관례).
|
||||
축척(H 1/1,000 · V 1/200)은 모든 장이 같다 — 작도 영역을 넘으면 측점을 줄여
|
||||
장을 더 나눌 뿐 줄이지 않는다.
|
||||
"""
|
||||
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측점 중복
|
||||
spans: list[dict[str, float]] = []
|
||||
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
|
||||
while True:
|
||||
take = min(LONG_SPLIT_STATION_COUNT, len(stations) - index)
|
||||
# ponytail: 한 측점씩 줄이며 다시 잰다 — 넘치는 장에서만 돎
|
||||
while take > 2 and not _fits_sheet(longitudinal, _span(stations, index, take)):
|
||||
take -= 1
|
||||
spans.append(_span(stations, index, take))
|
||||
if index + take >= len(stations):
|
||||
break
|
||||
index += take - 1 # 경계 1측점 중복
|
||||
if len(spans) == 1:
|
||||
return [{"id": "longitudinal", "label": "종단도", **spans[0]}]
|
||||
return [
|
||||
{"id": f"longitudinal_{number}", "label": f"종단도({number})", **span}
|
||||
for number, span in enumerate(spans, start=1)
|
||||
]
|
||||
|
||||
|
||||
def _long_table_values(
|
||||
@@ -600,10 +572,10 @@ def _long_table_entities(
|
||||
return entities
|
||||
|
||||
|
||||
def build_longitudinal_drawing(
|
||||
def _long_content(
|
||||
longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다."""
|
||||
) -> list[dict[str, Any]]:
|
||||
"""종단도 청크 하나의 지반선+계획선+그래프 축·격자+측점 테이블(도각 제외)."""
|
||||
start_m = chunk.get("start_m")
|
||||
end_m = chunk.get("end_m")
|
||||
|
||||
@@ -667,7 +639,16 @@ def build_longitudinal_drawing(
|
||||
)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def build_longitudinal_drawing(
|
||||
longitudinal: dict[str, Any], drawing_id: str, chunk: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""종단도 청크 하나를 지반선+계획선+그래프 축·격자+측점 테이블로 만든다."""
|
||||
entities = _long_content(longitudinal, drawing_id, chunk)
|
||||
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
||||
# 그림 좌하단 = 작도영역 좌하단 — 장마다 같은 자리에서 시작한다.
|
||||
bbox = entities_bbox(entities)
|
||||
if bbox:
|
||||
entities.extend(
|
||||
@@ -675,6 +656,7 @@ def build_longitudinal_drawing(
|
||||
drawing_id,
|
||||
bbox,
|
||||
fit=False,
|
||||
bottom_left=True,
|
||||
fields={
|
||||
"도면명": "종단면도",
|
||||
**scale_fields(("H", DRAWING_SCALE_LONG_H), ("V", DRAWING_SCALE_LONG_V)),
|
||||
|
||||
@@ -481,12 +481,14 @@ def frame_entities(
|
||||
template_name: str = A1_TEMPLATE,
|
||||
fit: bool = True,
|
||||
fields: dict[str, str] | None = None,
|
||||
bottom_left: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""콘텐츠 bbox를 감싸는 도각 프레임 엔티티 목록(잠금 레이어). 템플릿 없으면 빈 목록.
|
||||
|
||||
fit=False면 도각을 **실치수(1:1)** 로 두고 위치만 맞춘다 — 콘텐츠가 이미 종이
|
||||
밀리미터로 그려진 척도 고정 도면(종단·횡단)용. fit=True는 척도가 없는 도면을
|
||||
도각에 맞춰 늘리던 기존 동작이다.
|
||||
도각에 맞춰 늘리던 기존 동작이다. bottom_left=True면 콘텐츠 좌하단을 작도영역
|
||||
좌하단에 맞춘다(기본은 중심 맞춤).
|
||||
"""
|
||||
template = _load_template(template_name)
|
||||
if not template:
|
||||
@@ -513,6 +515,8 @@ def frame_entities(
|
||||
# 콘텐츠 중심 = 내부 작도 영역 중심이 되도록 이동량 산출.
|
||||
dx = (min_x + max_x) / 2.0 - (ix0 + ix1) / 2.0 * scale
|
||||
dy = (min_y + max_y) / 2.0 - (iy0 + iy1) / 2.0 * scale
|
||||
if bottom_left:
|
||||
dx, dy = min_x - ix0 * scale, min_y - iy0 * scale
|
||||
|
||||
placed = [
|
||||
_transform_entity(entity, f"{drawing_id}:frame:{index}", scale, dx, dy)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""B07 종단도 — 장마다 그림 좌하단 = 작도영역 좌하단 · 고정 축척 · 넘치면 장을 더 나눔 (59-3)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import FRAME_LAYER_ID
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
|
||||
build_longitudinal_drawing,
|
||||
longitudinal_chunks,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox, usable_area
|
||||
|
||||
|
||||
def _longitudinal(count: int, interval: float, slope: float) -> dict:
|
||||
xs = [index * interval for index in range(count)]
|
||||
return {
|
||||
"samples": [{"valid": True, "chainage_m": x, "elevation_m": 100.0 + slope * x} for x in xs],
|
||||
"stations": [{"chainage_m": x} for x in xs],
|
||||
"design_profiles": [
|
||||
{"samples": [{"chainage_m": x, "elevation_m": 100.5 + slope * x} for x in xs]}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _sheets(longitudinal: dict) -> list[tuple[tuple, tuple]]:
|
||||
result = []
|
||||
for chunk in longitudinal_chunks(longitudinal):
|
||||
entities = build_longitudinal_drawing(longitudinal, chunk["id"], chunk)["entities"]
|
||||
content = entities_bbox([e for e in entities if e.get("layerId") != FRAME_LAYER_ID])
|
||||
frame = entities_bbox([e for e in entities if e.get("layerId") == FRAME_LAYER_ID])
|
||||
result.append((content, frame))
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("count", "interval", "slope"),
|
||||
[(66, 20.0, 0.05), (40, 40.0, 0.0), (40, 20.0, 0.25)],
|
||||
)
|
||||
def test_every_sheet_fits_and_starts_at_same_corner(count, interval, slope) -> None:
|
||||
longitudinal = _longitudinal(count, interval, slope)
|
||||
sheets = _sheets(longitudinal)
|
||||
usable_w, usable_h = usable_area()
|
||||
corners = set()
|
||||
for content, frame in sheets:
|
||||
assert content[2] - content[0] <= usable_w + 1e-6
|
||||
assert content[3] - content[1] <= usable_h + 1e-6
|
||||
corners.add((round(content[0] - frame[0], 3), round(content[1] - frame[1], 3)))
|
||||
assert len(corners) == 1
|
||||
chunks = longitudinal_chunks(longitudinal)
|
||||
# 이어지는 장은 경계 측점 하나를 나눠 갖고 끝까지 덮는다.
|
||||
assert chunks[0]["start_m"] == 0.0
|
||||
assert chunks[-1]["end_m"] == (count - 1) * interval
|
||||
for before, after in zip(chunks, chunks[1:]):
|
||||
assert before["end_m"] == after["start_m"]
|
||||
|
||||
|
||||
def test_wide_interval_splits_more() -> None:
|
||||
# 40 m 간격 30측점 = 1,160 mm > 작도영역 폭 → 30개보다 적게 담는다
|
||||
chunks = longitudinal_chunks(_longitudinal(40, 40.0, 0.0))
|
||||
assert len(chunks) > 2
|
||||
Reference in New Issue
Block a user