Merge remote-tracking branch 'origin/sub_laptop_4' into sub_laptop_7
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]],
|
||||
@@ -386,6 +424,8 @@ def _cross_line_entities(
|
||||
CROSS_MM = 1000.0 / DRAWING_SCALE_CROSS
|
||||
# 단면을 그리는 가로 범위: 설계선(절·성토)·구조물이 원지반과 갈라지는 구간 + 여유(m).
|
||||
CROSS_SECTION_MARGIN_M = 1.5
|
||||
# 단면 블록 테두리 ↔ 안쪽 그림 간격(mm) — 한 장에 최대한 많이 담는다.
|
||||
CROSS_BORDER_PAD_MM = 1.0
|
||||
# 원지반과 "만난다"고 볼 표고 차이(m) — 이보다 작으면 붙어 있는 것으로 본다.
|
||||
_TOUCH_TOLERANCE_M = 0.02
|
||||
|
||||
@@ -507,7 +547,7 @@ def build_cross_drawing(
|
||||
# 표는 단면 중심 아래에 붙인다 (좌표는 mm). 그리는 창이 좌우 비대칭이면
|
||||
# 단면 중심이 원점과 어긋나므로 표도 같은 중심을 쓴다.
|
||||
center_x = ox + (x0 + x1) / 2.0 * CROSS_MM
|
||||
half_height = own_half_height * CROSS_MM + 5.0
|
||||
half_height = own_half_height * CROSS_MM
|
||||
table_bottom = oy - half_height
|
||||
if quantity_table is not None:
|
||||
table_top = oy - half_height - 8.0
|
||||
@@ -520,10 +560,11 @@ def build_cross_drawing(
|
||||
if cell_frame is not None:
|
||||
frame_left, frame_bottom, frame_right, frame_top = cell_frame
|
||||
else:
|
||||
frame_x = max((x1 - x0) / 2.0 * CROSS_MM + 4.0, cross_table_width() / 2.0 + 4.0)
|
||||
pad = CROSS_BORDER_PAD_MM
|
||||
frame_x = max((x1 - x0) / 2.0 * CROSS_MM + pad, cross_table_width() / 2.0 + pad)
|
||||
frame_left, frame_right = center_x - frame_x, center_x + frame_x
|
||||
frame_top = oy + half_height + 4.0
|
||||
frame_bottom = table_bottom - 4.0
|
||||
frame_top = oy + half_height + pad
|
||||
frame_bottom = table_bottom - pad
|
||||
corners = [
|
||||
(frame_left, frame_bottom),
|
||||
(frame_right, frame_bottom),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections import cross_key
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
CROSS_BORDER_PAD_MM,
|
||||
CROSS_MM,
|
||||
CROSS_TABLE_LAYER_ID,
|
||||
DESIGN_LAYER_ID,
|
||||
@@ -44,7 +45,7 @@ CROSS_SHEET_ID = re.compile(r"cross_s(\d{2,}(?:\.\d{2})?)m?")
|
||||
# 블록 사이 여백(mm)과 테두리 여유 — build_cross_drawing의 테두리 값과 맞춘다.
|
||||
# 여백 0 = 이웃 칸의 테두리가 맞닿는다(2026-09-04 사용자 지시 — 박스 사이 간격 제거).
|
||||
_BLOCK_GAP_MM = 0.0
|
||||
_BORDER_PAD_MM = 4.0
|
||||
_BORDER_PAD_MM = CROSS_BORDER_PAD_MM
|
||||
|
||||
|
||||
def _design_points(design_line: list[Any] | None) -> list[tuple[float, float]]:
|
||||
@@ -72,7 +73,7 @@ def section_block_size(
|
||||
x0, x1 = _section_window(ground, design)
|
||||
ground_clipped = _clip_polyline(ground, x0, x1)
|
||||
ys = [y for _x, y in ground_clipped] + [y for x, y in design if x0 <= x <= x1]
|
||||
half_height = ((max(ys) - min(ys)) / 2.0 if ys else 5.0) * CROSS_MM + 5.0
|
||||
half_height = ((max(ys) - min(ys)) / 2.0 if ys else 5.0) * CROSS_MM
|
||||
|
||||
width = 2.0 * max(
|
||||
(x1 - x0) / 2.0 * CROSS_MM + _BORDER_PAD_MM,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,8 @@ import { notifyWindow } from '../state';
|
||||
export type InspectorTab = 'properties' | 'layers';
|
||||
|
||||
let inspectorTab: InspectorTab = 'layers';
|
||||
let inspectorCollapsed = false;
|
||||
/** 특성·도면층 패널은 닫힌 채 시작 — 펼치기 단추로 연다. */
|
||||
let inspectorCollapsed = true;
|
||||
let quickPropertiesVisible = false;
|
||||
let activeRibbonTab = 'home';
|
||||
let blockLibraryVisible = false;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""B07 횡단면도 — 단면 블록 테두리 ↔ 안쪽 그림 간격 1 mm (59-1)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
CROSS_BORDER_PAD_MM,
|
||||
FRAME_LAYER_ID,
|
||||
build_cross_drawing,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import section_block_size
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import entities_bbox
|
||||
|
||||
_SOURCE = {
|
||||
"chainage_m": 20.0,
|
||||
"samples": [
|
||||
{"valid": True, "offset_m": float(x), "elevation_m": 100.0 + 0.3 * x}
|
||||
for x in range(-10, 11)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_frame_hugs_section_by_one_mm() -> None:
|
||||
drawing = build_cross_drawing(_SOURCE, "cross_00020m", None, None, None, "")
|
||||
lines = [e for e in drawing["entities"] if e.get("layerId") != FRAME_LAYER_ID]
|
||||
frame = [e for e in drawing["entities"] if e.get("layerId") == FRAME_LAYER_ID]
|
||||
min_x, min_y, max_x, max_y = entities_bbox(lines)
|
||||
f_min_x, f_min_y, f_max_x, f_max_y = entities_bbox(frame)
|
||||
assert CROSS_BORDER_PAD_MM == 1.0
|
||||
assert f_max_y - max_y == pytest.approx(1.0)
|
||||
assert min_y - f_min_y == pytest.approx(1.0)
|
||||
# 표 없는 블록 크기 = 테두리 크기
|
||||
width, height = section_block_size(_SOURCE, None, with_table=False)
|
||||
assert height == pytest.approx(f_max_y - f_min_y)
|
||||
assert width == pytest.approx(f_max_x - f_min_x)
|
||||
@@ -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