fix(B07): 유역도에서 노선을 살리고 방위표·정보표 배치와 절취면을 정리한다

노선이 유역 해칭 아래에 깔려 보이지 않았다. polyline_entity에 width를 더해
노선만 굵게(3) 그리고, 그리기 순서를 해칭·정보표 다음으로 옮긴다. bbox
계산에는 그대로 포함시켜 오른쪽 칸 위치는 흔들리지 않는다.

오른쪽 칸을 방위표(맨 위) → 유역 정보표(아래로 쌓기) 순으로 바꾸고 방위표를
14mm에서 26mm로 키운다. 도면 오른쪽 위 구석에 따로 놓던 옛 배치는 걷어낸다.

등고선·세류선 절취가 경계 바깥 점을 하나씩 물고 나와 외곽이 들쭉날쭉했다.
Liang-Barsky 선분 절취로 바꿔 끝점이 경계 위에 정확히 놓이게 하고, 그만큼
두었던 물림 여유(_CLIP_SLACK 14mm)를 없앤다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 15:21:38 +09:00
co-authored by Claude Opus 5
parent 6a2c5d9f7b
commit 008b0ede14
3 changed files with 81 additions and 25 deletions
@@ -60,12 +60,13 @@ def _line_entity(
end: tuple[float, float],
layer_id: str,
color: str,
width: int = 1,
) -> dict[str, Any]:
return {
"id": str(uuid5(_ENTITY_NS, seed)),
"type": "Line",
"lineColor": color,
"lineWidth": 1,
"lineWidth": width,
"layerId": layer_id,
"shapeData": {
"startPoint": {"x": start[0], "y": start[1]},
@@ -81,6 +82,7 @@ def polyline_entity(
color: str,
suffix: str = "",
dash: list[int] | None = None,
width: int = 1,
) -> dict[str, Any] | None:
"""점열을 openwebcad PolyLine(자식 Line 묶음)으로 직렬화한다 (점 2개 미만이면 None)."""
if len(points) < 2:
@@ -89,7 +91,7 @@ def polyline_entity(
children = []
for index in range(len(points) - 1):
child = _line_entity(
f"{seed_base}:{index}", points[index], points[index + 1], layer_id, color
f"{seed_base}:{index}", points[index], points[index + 1], layer_id, color, width
)
if dash:
child["lineDash"] = dash
@@ -86,7 +86,13 @@ _BOX_GAP = 3.0
_BOX_MARGIN = 12.0 # 지형 콘텐츠 오른쪽 여백
_BOX_ROWS = (("유역면적", "area"), ("유역표고", "relief"), ("유하거리", "flow"))
_TITLE_BAND = 22.0 # 제목·척도가 차지하는 위쪽 띠(mm)
_CLIP_SLACK = 14.0 # 절취 경계 물림 여유(mm)
_CLIP_SLACK = 0.0 # 절취 경계에서 정확히 끊기므로 여유가 필요 없다(2026-08-31)
# 오른쪽 칸: 방위표를 맨 위에 놓고 그 아래로 유역 정보표를 쌓는다(2026-08-31 사용자 지시).
_COMPASS_SIZE = 26.0 # 방위표 긴 변(mm)
_COMPASS_GAP = 6.0 # 방위표와 첫 정보표 사이(mm)
# 노선은 지형·유역 위에 얹혀야 보인다 — 굵기도 배경선보다 굵게 한다.
_ROUTE_WIDTH = 3
def map_area_mm() -> tuple[float, float]:
@@ -307,13 +313,15 @@ def build_watershed_drawing(
)
if stream:
entities.append(stream)
# 노선은 지형·유역 위에 그려야 보인다 — 여기서 만들어 두고 맨 마지막에 얹는다.
route = polyline_entity(
drawing_id, [paper(point) for point in route_xy], ROUTE_LAYER_ID, ROUTE_COLOR
drawing_id,
[paper(point) for point in route_xy],
ROUTE_LAYER_ID,
ROUTE_COLOR,
width=_ROUTE_WIDTH,
)
if route:
entities.append(route)
map_bbox = entities_bbox(entities)
map_bbox = entities_bbox([*entities, route] if route else entities)
badges: list[dict[str, Any]] = []
for order, basin in enumerate(basins):
ring = [paper(point) for point in basin.get("ring") or []]
@@ -331,12 +339,20 @@ def build_watershed_drawing(
# 번호 배지는 맨 마지막에 몰아 그린다 — 뒤에 오는 유역 해칭·정보표에 가리면 안 된다.
badges.extend(_number_badge_entities(drawing_id, number, _centroid(ring), color))
# 정보표는 지형 콘텐츠 오른쪽에 위에서부터 쌓는다.
# 오른쪽 칸은 방위표가 맨 위, 그 아래로 유역 정보표를 쌓는다.
if map_bbox:
box_x = map_bbox[2] + _BOX_MARGIN
box_y = map_bbox[3]
column_top = map_bbox[3]
else:
box_x, box_y = 0.0, 0.0
box_x, column_top = 0.0, 0.0
entities.extend(
compass_entities(
drawing_id,
(box_x + _BOX_WIDTH / 2.0, column_top - _COMPASS_SIZE / 2.0),
_COMPASS_SIZE,
)
)
box_y = column_top - _COMPASS_SIZE - _COMPASS_GAP
for order, basin in enumerate(basins):
props = basin.get("props") or {}
number = int(props.get("index") or order + 1)
@@ -352,6 +368,8 @@ def build_watershed_drawing(
)
)
if route:
entities.append(route) # 지형·유역 위
entities.extend(badges) # 그리기 순서상 가장 위
bbox = entities_bbox(entities)
@@ -380,7 +398,6 @@ def build_watershed_drawing(
align="right",
)
)
entities.extend(compass_entities(drawing_id, (max_bx - 8.0, max_by - 8.0), 14.0))
entities.extend(
frame_entities(
drawing_id, entities_bbox(entities) or bbox, fit=False, fields={"도면명": "유역도"}
@@ -137,28 +137,65 @@ def _geometry_lines(geometry: Any) -> list[list[tuple[float, float]]]:
return []
def _clip_segment(
start: tuple[float, float],
end: tuple[float, float],
box: tuple[float, float, float, float],
) -> tuple[tuple[float, float], tuple[float, float]] | None:
"""선분에서 상자 안에 드는 부분만 돌려준다(Liang-Barsky). 겹치지 않으면 None."""
min_x, min_y, max_x, max_y = box
dx = end[0] - start[0]
dy = end[1] - start[1]
t0, t1 = 0.0, 1.0
for numerator, denominator in (
(start[0] - min_x, -dx),
(max_x - start[0], dx),
(start[1] - min_y, -dy),
(max_y - start[1], dy),
):
if denominator == 0.0:
if numerator < 0.0:
return None # 경계와 나란한데 바깥이다
continue
t = numerator / denominator
if denominator < 0.0:
if t > t1:
return None
t0 = max(t0, t)
else:
if t < t0:
return None
t1 = min(t1, t)
return (
(start[0] + t0 * dx, start[1] + t0 * dy),
(start[0] + t1 * dx, start[1] + t1 * dy),
)
def clip_line_to_box(
line: list[tuple[float, float]], box: tuple[float, float, float, float]
) -> list[list[tuple[float, float]]]:
"""범위 안에 든 연속 구간만 조각으로 잘라 낸다(경계 한 점씩 물려 끊긴 티를 줄인다).
"""범위 안에 든 부분만 조각으로 잘라 낸다 — 끝점은 경계 위에 정확히 놓인다.
도엽 등고선 한 줄은 도엽 끝까지 이어지므로, 걸치면 통째로 남기면 도면이 A1을 넘는다.
예전 구현은 경계 바깥 점을 하나씩 물고 나와 외곽선이 들쭉날쭉했다(2026-08-31 사용자
지적). 이제 교차점을 계산해 자르므로 절취면이 곧게 떨어진다.
"""
min_x, min_y, max_x, max_y = box
runs: list[list[tuple[float, float]]] = []
current: list[tuple[float, float]] = []
for index, point in enumerate(line):
x, y = point
if min_x <= x <= max_x and min_y <= y <= max_y:
if not current and index > 0:
current.append(line[index - 1]) # 바깥쪽 직전 점까지 물린다
current.append(point)
elif current:
current.append(point)
runs.append(current)
for start, end in zip(line, line[1:]):
piece = _clip_segment(start, end, box)
if piece is None:
current = []
if current:
runs.append(current)
continue
head, tail = piece
if head == tail:
continue # 경계에 점으로만 닿았다
if current and current[-1] == head:
current.append(tail)
else:
current = [head, tail]
runs.append(current)
return [run for run in runs if len(run) >= 2]