feat(B07): 종단도 장마다 우측 상단 평면 위치도 · 종단 ⚙ 「평면도」 척도 칸 (521-2 · 요구 340-2)
- 새 Engine_Cad_LongPlan — 작도 영역 우상단 5 mm 안쪽 150×100 mm 칸 · 자동 = 전체 노선 맞춤(분모 500 올림) · 척도 정함 = 이 장 중심 확대 · 밖 노선 흐린 파선 · 칸 밖 잘림 · 이 장 굵게 · 시 · 종점 측점 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」 · 좌표 없으면 안 그림 - 장 나눔 — 그래프가 칸을 덮으면 측점 줄여 장 더 나눔 · 장 목록에 number · total - 축척 — longitudinal.plan 기본 자동 · 범위 1/500~1/50,000 · ⚙ 칸 = 서버 기본값 열쇠 그대로(셋째 칸 + 자동) - 칸 앞 확정 종단 저장본 — 목록 받을 때 종단만 한 번 모두 풂(DRAWING_FORMAT 그대로) - 시험 — 칸 자리 · 안 겹침 · 장마다 강조 · 척도 확대 · 좌표 없음 · 한 번 풂 · plan 범위 · ⚙ 평면 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -243,12 +243,15 @@ export function saveCrossLayout(projectId: string, layout: CrossLayout): Promise
|
||||
|
||||
/** 도면 축척 종류(485) — 표준 횡단 · 평면도 · 구조물은 뺌. */
|
||||
export type DrawingScaleKind = "longitudinal" | "cross" | "mass_haul" | "development";
|
||||
/** 축척 분모 — null = 자동(토적 가로 · 전개도) · 범위 = [최소, 최대]. */
|
||||
export type DrawingScaleAxes = { x: number | null; y: number | null };
|
||||
/** 축척 분모 — null = 자동(토적 가로 · 전개도 · 종단 평면 위치도) · 범위 = [최소, 최대]. */
|
||||
export type DrawingScaleAxes = { x: number | null; y: number | null; plan?: number | null };
|
||||
export interface DrawingScales {
|
||||
scales: Record<DrawingScaleKind, DrawingScaleAxes>;
|
||||
defaults: Record<DrawingScaleKind, DrawingScaleAxes>;
|
||||
ranges: Record<DrawingScaleKind, { x: [number, number]; y: [number, number] }>;
|
||||
ranges: Record<
|
||||
DrawingScaleKind,
|
||||
{ x: [number, number]; y: [number, number]; plan?: [number, number] }
|
||||
>;
|
||||
/** 종류마다 저장본(확정) 장 수 — 바꾸면 풀리므로 화면이 먼저 물음. */
|
||||
saved: Record<DrawingScaleKind, number>;
|
||||
/** PUT 응답만 — 이번에 풀린 저장본 장 수. */
|
||||
|
||||
@@ -42,6 +42,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
polyline_entity,
|
||||
station_no_label,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import (
|
||||
LONG_PLAN_LAYER_ID,
|
||||
long_plan_entities,
|
||||
plan_box_overlaps,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
drawing_scale,
|
||||
entities_bbox,
|
||||
@@ -93,12 +98,14 @@ def _span(stations: list[dict[str, Any]], index: int, take: int) -> dict[str, fl
|
||||
|
||||
|
||||
def _fits_sheet(longitudinal: dict[str, Any], span: dict[str, float]) -> bool:
|
||||
"""이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 드는가."""
|
||||
bbox = entities_bbox(_long_content(longitudinal, "probe", span))
|
||||
"""이 구간을 고정 축척으로 그렸을 때 A1 작도 영역에 들고 평면 위치도 칸을 안 덮는가."""
|
||||
content = _long_content(longitudinal, "probe", span)
|
||||
bbox = entities_bbox(content)
|
||||
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
|
||||
fits = bbox[2] - bbox[0] <= usable_w + 1e-6 and bbox[3] - bbox[1] <= usable_h + 1e-6
|
||||
return fits and not plan_box_overlaps(longitudinal, content, bbox)
|
||||
|
||||
|
||||
def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
@@ -124,9 +131,10 @@ def longitudinal_chunks(longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
break
|
||||
index += take - 1 # 경계 1측점 중복
|
||||
if len(spans) == 1:
|
||||
return [{"id": "longitudinal", "label": "종단도", **spans[0]}]
|
||||
return [{"id": "longitudinal", "label": "종단도", "number": 1, "total": 1, **spans[0]}]
|
||||
return [
|
||||
{"id": f"longitudinal_{number}", "label": f"종단도({number})", **span}
|
||||
{"id": f"longitudinal_{number}", "label": f"종단도({number})", "number": number, **span}
|
||||
| {"total": len(spans)}
|
||||
for number, span in enumerate(spans, start=1)
|
||||
]
|
||||
|
||||
@@ -660,6 +668,7 @@ def build_longitudinal_drawing(
|
||||
# A1 도각: 콘텐츠가 이미 종이 mm라 도각도 실치수(1:1)로 두고 위치만 맞춘다.
|
||||
# 그림 좌하단 = 작도영역 좌하단 — 장마다 같은 자리에서 시작한다.
|
||||
bbox = entities_bbox(entities)
|
||||
entities.extend(long_plan_entities(longitudinal, drawing_id, chunk, bbox)) # 521-2
|
||||
if bbox:
|
||||
entities.extend(
|
||||
frame_entities(
|
||||
@@ -682,6 +691,7 @@ def build_longitudinal_drawing(
|
||||
_layer(DESIGN_LAYER_ID, "계획선"),
|
||||
_layer(LONG_GRID_LAYER_ID, "그래프 격자", locked=True),
|
||||
_layer(LONG_TABLE_LAYER_ID, "측점표"),
|
||||
_layer(LONG_PLAN_LAYER_ID, "평면 위치도"),
|
||||
_layer(FRAME_LAYER_ID, "도각", locked=True),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""B07 종단도 우측 상단 평면 위치도(521-2 · 요구 340-2) — 장마다 작도 영역 우상단 칸 하나.
|
||||
|
||||
칸 = 작도 영역 우상단 5 mm 안쪽 150×100 mm(작도 영역 좌하단 = 그림 좌하단 · `frame_entities`
|
||||
bottom_left). 척도 = 종단 축척 `plan` 칸(None = 자동). 방위표만 도각 층.
|
||||
- 자동: 전체 노선을 칸에 맞춤(분모 500 단위 올림) · 전체 노선 가는 선 · 이 장 구간 굵게.
|
||||
- 척도 정함: 이 장 중심 확대 · 이 장 굵게 · 밖 노선 흐린 파선 · 칸 밖은 잘림.
|
||||
칸 내용 = 노선 · 이 장 시 · 종점 측점 글 · 방위표 · 「평면도 S=1/n」 · 「n/N 장」.
|
||||
좌표 = 종단 측점 center_x · center_y — 없으면 칸 안 그림. 서버 단독(CLAUDE.md 5장 ③).
|
||||
"""
|
||||
|
||||
import math
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from shapely.geometry import LineString, Point
|
||||
from shapely.geometry import box as shapely_box
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
DESIGN_COLOR,
|
||||
TABLE_LABEL_COLOR,
|
||||
TABLE_LINE_COLOR,
|
||||
_stations,
|
||||
_text_entity,
|
||||
infer_station_interval,
|
||||
polyline_entity,
|
||||
station_no_label,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanLabels import text_rect
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
compass_entities,
|
||||
drawing_scale,
|
||||
usable_area,
|
||||
)
|
||||
|
||||
LONG_PLAN_LAYER_ID = "b07-long-plan"
|
||||
BOX_W, BOX_H = 150.0, 100.0 # 칸 크기(종이 mm)
|
||||
_INSET = 5.0 # 작도 영역 테두리에서 안쪽
|
||||
_MAP_PAD = (4.0, 10.0, 4.0, 4.0) # 칸 안 노선 자리 여백(좌 · 하 · 우 · 상) — 아래는 글 줄
|
||||
_AUTO_STEP = 500 # 자동 분모 올림 단위
|
||||
_FONT = 2.5
|
||||
_COMPASS = 12.0
|
||||
_ROUTE_COLOR = "#7d8590" # 전체 · 밖 노선 — 흐리게
|
||||
|
||||
|
||||
def plan_box(content_bbox: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
|
||||
"""칸 (x0, y0, x1, y1) — 그림 좌표(작도 영역 좌하단 = 그림 좌하단)."""
|
||||
usable_w, usable_h = usable_area()
|
||||
x1 = content_bbox[0] + usable_w - _INSET
|
||||
y1 = content_bbox[1] + usable_h - _INSET
|
||||
return (x1 - BOX_W, y1 - BOX_H, x1, y1)
|
||||
|
||||
|
||||
def _plan_points(longitudinal: dict[str, Any]) -> list[tuple[float, float, float]]:
|
||||
"""종단 측점의 (누가거리 m, x, y) — `Router_Support_Basin.plan_stations` 와 같은 셈."""
|
||||
points: list[tuple[float, float, float]] = []
|
||||
for station in _stations(longitudinal):
|
||||
x, y = station.get("center_x"), station.get("center_y")
|
||||
if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in (x, y)):
|
||||
points.append((float(station["chainage_m"]), float(x), float(y)))
|
||||
return sorted(points)
|
||||
|
||||
|
||||
def _shapes(entities: list[dict[str, Any]]) -> Iterator[Any]:
|
||||
"""엔티티 → shapely 도형(선 · 글 사각 · 원) — 칸 겹침 판정용."""
|
||||
for entity in entities:
|
||||
yield from _shapes(entity.get("children") or [])
|
||||
shape = entity.get("shapeData") or {}
|
||||
if "startPoint" in shape:
|
||||
a, b = shape["startPoint"], shape["endPoint"]
|
||||
yield LineString([(a["x"], a["y"]), (b["x"], b["y"])])
|
||||
elif "basePoint" in shape:
|
||||
base, options = shape["basePoint"], shape.get("options") or {}
|
||||
direction = options.get("textDirection") or {}
|
||||
yield shapely_box(
|
||||
*text_rect(
|
||||
str(shape.get("label") or ""),
|
||||
base["x"],
|
||||
base["y"],
|
||||
float(options.get("fontSize", _FONT)),
|
||||
str(options.get("textAlign", "center")),
|
||||
(float(direction.get("x", 1.0)), float(direction.get("y", 0.0))),
|
||||
)
|
||||
)
|
||||
elif "center" in shape:
|
||||
center = shape["center"]
|
||||
yield Point(center["x"], center["y"]).buffer(float(shape.get("radius", 0.0)))
|
||||
|
||||
|
||||
def plan_box_overlaps(
|
||||
longitudinal: dict[str, Any],
|
||||
entities: list[dict[str, Any]],
|
||||
content_bbox: tuple[float, float, float, float] | None,
|
||||
) -> bool:
|
||||
"""그래프 · 측점표가 칸을 덮는가 — 칸을 안 그리는 종단(좌표 없음)은 늘 거짓."""
|
||||
if content_bbox is None or len(_plan_points(longitudinal)) < 2:
|
||||
return False
|
||||
x0, y0, x1, y1 = plan_box(content_bbox)
|
||||
if content_bbox[2] < x0 or content_bbox[3] < y0:
|
||||
return False
|
||||
region = shapely_box(x0, y0, x1, y1)
|
||||
return any(region.intersects(shape) for shape in _shapes(entities))
|
||||
|
||||
|
||||
def _auto_denominator(points: list[tuple[float, float, float]], map_w: float, map_h: float) -> int:
|
||||
width = max(p[1] for p in points) - min(p[1] for p in points)
|
||||
height = max(p[2] for p in points) - min(p[2] for p in points)
|
||||
need = max(width / map_w, height / map_h) * 1000.0
|
||||
return max(_AUTO_STEP, math.ceil(need / _AUTO_STEP) * _AUTO_STEP)
|
||||
|
||||
|
||||
def _clipped(
|
||||
drawing_id: str,
|
||||
points: list[tuple[float, float]],
|
||||
region: Any,
|
||||
suffix: str,
|
||||
color: str,
|
||||
dash: list[int] | None = None,
|
||||
width: int = 1,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""종이 점열을 칸 노선 자리로 잘라 PolyLine 들로."""
|
||||
if len(points) < 2:
|
||||
return []
|
||||
clipped = LineString(points).intersection(region)
|
||||
parts = getattr(clipped, "geoms", [clipped])
|
||||
out = []
|
||||
for index, part in enumerate(parts):
|
||||
if isinstance(part, LineString) and not part.is_empty:
|
||||
line = polyline_entity(
|
||||
drawing_id,
|
||||
list(part.coords),
|
||||
LONG_PLAN_LAYER_ID,
|
||||
color,
|
||||
suffix=f"{suffix}:{index}",
|
||||
dash=dash,
|
||||
width=width,
|
||||
)
|
||||
if line:
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def long_plan_entities(
|
||||
longitudinal: dict[str, Any],
|
||||
drawing_id: str,
|
||||
chunk: dict[str, Any],
|
||||
content_bbox: tuple[float, float, float, float] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""이 장의 평면 위치도 칸 — 좌표 없거나 그림이 비면 빈 목록."""
|
||||
points = _plan_points(longitudinal)
|
||||
if content_bbox is None or len(points) < 2:
|
||||
return []
|
||||
start_m, end_m = chunk.get("start_m"), chunk.get("end_m")
|
||||
if start_m is None or end_m is None:
|
||||
start_m, end_m = points[0][0], points[-1][0]
|
||||
inside = [p for p in points if start_m - 1e-6 <= p[0] <= end_m + 1e-6]
|
||||
x0, y0, x1, y1 = plan_box(content_bbox)
|
||||
left, bottom, right, top = _MAP_PAD
|
||||
map_box = (x0 + left, y0 + bottom, x1 - right, y1 - top)
|
||||
map_w, map_h = map_box[2] - map_box[0], map_box[3] - map_box[1]
|
||||
|
||||
fixed = drawing_scale("longitudinal").get("plan")
|
||||
denominator = fixed or _auto_denominator(points, map_w, map_h)
|
||||
focus = inside if fixed and inside else points
|
||||
center_x = (min(p[1] for p in focus) + max(p[1] for p in focus)) / 2.0
|
||||
center_y = (min(p[2] for p in focus) + max(p[2] for p in focus)) / 2.0
|
||||
mm = 1000.0 / denominator
|
||||
box_cx, box_cy = (map_box[0] + map_box[2]) / 2.0, (map_box[1] + map_box[3]) / 2.0
|
||||
|
||||
def paper(x: float, y: float) -> tuple[float, float]:
|
||||
return (box_cx + (x - center_x) * mm, box_cy + (y - center_y) * mm)
|
||||
|
||||
region = shapely_box(*map_box)
|
||||
border = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
|
||||
entities = [polyline_entity(drawing_id, border, LONG_PLAN_LAYER_ID, TABLE_LINE_COLOR, ":box")]
|
||||
entities += _clipped(
|
||||
drawing_id,
|
||||
[paper(x, y) for _c, x, y in points],
|
||||
region,
|
||||
":all",
|
||||
_ROUTE_COLOR,
|
||||
dash=[3, 3] if fixed else None,
|
||||
)
|
||||
entities += _clipped(
|
||||
drawing_id, [paper(x, y) for _c, x, y in inside], region, ":this", DESIGN_COLOR, width=3
|
||||
)
|
||||
interval_m = infer_station_interval(_stations(longitudinal))
|
||||
for name, (chainage, x, y) in (("start", inside[0]), ("end", inside[-1])) if inside else ():
|
||||
px, py = paper(x, y)
|
||||
if region.contains(Point(px, py)):
|
||||
entities.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:lplan:{name}",
|
||||
station_no_label(chainage, interval_m),
|
||||
px,
|
||||
py + _FONT,
|
||||
LONG_PLAN_LAYER_ID,
|
||||
_FONT,
|
||||
TABLE_LABEL_COLOR,
|
||||
)
|
||||
)
|
||||
number, total = int(chunk.get("number") or 1), int(chunk.get("total") or 1)
|
||||
for name, label, x, align in (
|
||||
("scale", f"평면도 S=1/{denominator:,}", x0 + left, "left"),
|
||||
("page", f"{number}/{total} 장", x1 - right, "right"),
|
||||
):
|
||||
entities.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:lplan:{name}",
|
||||
label,
|
||||
x,
|
||||
y0 + bottom / 2.0,
|
||||
LONG_PLAN_LAYER_ID,
|
||||
_FONT,
|
||||
TABLE_LABEL_COLOR,
|
||||
align=align,
|
||||
)
|
||||
)
|
||||
# 방위표 = 다른 평면 도면처럼 도각 층(잠금)
|
||||
compass = compass_entities(
|
||||
f"{drawing_id}:lplan",
|
||||
(x0 + left + _COMPASS / 2.0, y1 - top - _COMPASS / 2.0),
|
||||
_COMPASS,
|
||||
)
|
||||
return [entity for entity in entities if entity] + compass
|
||||
@@ -97,7 +97,8 @@ def add_title_fields(extra: dict[str, str]) -> None:
|
||||
# 읽는 프로젝트 = 위 문맥 변수(라우터가 세움) — 엔진 서명을 고치지 않는다.
|
||||
DRAWING_SCALES_FILE = "drawing_scales.json"
|
||||
DRAWING_SCALE_DEFAULTS: dict[str, dict[str, int | None]] = {
|
||||
"longitudinal": {"x": DRAWING_SCALE_LONG_H, "y": DRAWING_SCALE_LONG_V},
|
||||
# plan = 종단 장 우측 상단 평면 위치도(521-2) · None = 자동(노선을 칸에 맞춤)
|
||||
"longitudinal": {"x": DRAWING_SCALE_LONG_H, "y": DRAWING_SCALE_LONG_V, "plan": None},
|
||||
"cross": {"x": DRAWING_SCALE_CROSS, "y": DRAWING_SCALE_CROSS},
|
||||
# 토적 세로 = 1 mm 당 50 ㎥ → 표기 1:50,000 (분모 / 1000 = 종이 1 mm 의 ㎥)
|
||||
"mass_haul": {"x": None, "y": int(DRAWING_SCALE_MASSHAUL_V_M3_MM * 1000)},
|
||||
|
||||
@@ -90,6 +90,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
_read_manifest,
|
||||
_station_map,
|
||||
_write_manifest,
|
||||
release_old_long_sheets,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
PLAN_KIND_ID,
|
||||
@@ -196,6 +197,7 @@ def write_cross_layout(
|
||||
def _drawing_list(project_root: Path, longitudinal_path: Path) -> list[DesignDrawingItem]:
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
station_by_chainage = _station_map(longitudinal)
|
||||
release_old_long_sheets(project_root) # 521-2
|
||||
manifest_drawings = _read_manifest(project_root)["drawings"]
|
||||
# 종단도: 측점 30개 초과 시 30개 단위 분할 도면을 각각 목록에 노출한다(N-1-1).
|
||||
drawings = [
|
||||
|
||||
@@ -80,3 +80,31 @@ def _write_manifest(project_root: Path, manifest: dict[str, Any]) -> None:
|
||||
temporary = path.with_suffix(".tmp")
|
||||
temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def release_old_long_sheets(project_root: Path) -> None:
|
||||
"""평면 위치도(521-2) 앞 확정 종단 저장본 — 종단만 모두 한 번 풂(칸 · 장 나눔이 바뀜).
|
||||
|
||||
풀린 장의 저장본은 `_write_manifest` 가 걷으니 다음부터는 새 저장본(칸 층 있음)만 남음.
|
||||
"""
|
||||
# 엔진 → 양식 → 이 모듈 차례라 위에서 부르면 돌고 돎
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_LongPlan import LONG_PLAN_LAYER_ID
|
||||
|
||||
manifest = _read_manifest(project_root)
|
||||
confirmed = [
|
||||
(drawing_id, entry)
|
||||
for drawing_id, entry in manifest["drawings"].items()
|
||||
if _LONG_ID.fullmatch(drawing_id) and entry.get("confirmed")
|
||||
]
|
||||
if not confirmed:
|
||||
return
|
||||
# ponytail: 첫 장만 봄 — 옛 장은 한꺼번에 풀리므로 새 장과 섞이지 않음
|
||||
saved = _design_root(project_root) / "drawings" / f"{confirmed[0][0]}.json"
|
||||
try:
|
||||
if f'"{LONG_PLAN_LAYER_ID}"' in saved.read_text(encoding="utf-8"):
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
for _drawing_id, entry in confirmed:
|
||||
entry["confirmed"] = False
|
||||
_write_manifest(project_root, manifest)
|
||||
|
||||
@@ -36,7 +36,7 @@ router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
|
||||
# 분모 범위(정수) — 기본 · 자동 후보(토적 500~6,000 · 전개도 50~5,000)를 모두 담음.
|
||||
SCALE_RANGES: dict[str, dict[str, tuple[int, int]]] = {
|
||||
"longitudinal": {"x": (100, 10000), "y": (10, 2000)},
|
||||
"longitudinal": {"x": (100, 10000), "y": (10, 2000), "plan": (500, 50000)},
|
||||
"cross": {"x": (10, 1000), "y": (10, 1000)},
|
||||
"mass_haul": {"x": (100, 20000), "y": (1000, 1000000)},
|
||||
"development": {"x": (10, 10000), "y": (10, 10000)},
|
||||
@@ -65,7 +65,7 @@ def checked_scales(scales: Any) -> dict[str, dict[str, int | None]]:
|
||||
if value is not None and (
|
||||
isinstance(value, bool) or not isinstance(value, int) or not low <= value <= high
|
||||
):
|
||||
name = "X(가로)" if axis == "x" else "Y(세로)"
|
||||
name = {"x": "X(가로)", "y": "Y(세로)"}.get(axis, "평면도")
|
||||
raise ValueError(
|
||||
f"{_KIND_LABEL[kind]} {name} 축척은 1/{low:,}~1/{high:,} 사이 정수로 적으세요."
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* B07_DesignDetail_UI_Settings_Scale.ts
|
||||
* ⚙ 모달 X · Y 축척 칸(485-2 · 요구 280-5) — 종단 · 횡단 · 토적 · 전개도만.
|
||||
* 종단 = 셋째 칸 「평면도」(521-2 · 우측 상단 평면 위치도) — 칸은 서버 기본값 열쇠 그대로.
|
||||
*
|
||||
* 칸 = 분모 정수 · 범위 밖 = 칸 붉게 + [축척 저장] 꺼짐 · 기본이 자동인 칸(토적 X · 전개도) = 「자동」
|
||||
* [축척 저장] = 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT → 화면 다시 세움(도면 다시 그림)
|
||||
@@ -32,10 +33,8 @@ const LABEL: Record<DrawingScaleKind, string> = {
|
||||
mass_haul: "토적도",
|
||||
development: "전개도",
|
||||
};
|
||||
const AXES = [
|
||||
["x", "X(가로) 1/"],
|
||||
["y", "Y(세로) 1/"],
|
||||
] as const;
|
||||
type Axis = keyof DrawingScaleAxes;
|
||||
const AXIS_LABEL: Record<Axis, string> = { x: "X(가로) 1/", y: "Y(세로) 1/", plan: "평면도 1/" };
|
||||
|
||||
const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL;
|
||||
|
||||
@@ -86,9 +85,11 @@ function buildScaleFields(
|
||||
data: DrawingScales,
|
||||
save: (next: DrawingScaleAxes) => Promise<void>,
|
||||
): void {
|
||||
const fields = AXES.map(([axis, label]) => {
|
||||
const fallback = data.defaults[kind][axis];
|
||||
const range = data.ranges[kind][axis];
|
||||
const axes = Object.keys(data.defaults[kind]) as Axis[];
|
||||
const fields = axes.map((axis) => {
|
||||
const label = AXIS_LABEL[axis];
|
||||
const fallback = data.defaults[kind][axis] ?? null;
|
||||
const range = data.ranges[kind][axis] ?? [1, 1];
|
||||
const [min, max] = range;
|
||||
const field = createInputField({ label, type: "number", compact: true, min, max });
|
||||
field.root.classList.add("b07-scale__field");
|
||||
@@ -99,7 +100,7 @@ function buildScaleFields(
|
||||
if (auto) auto.input.checked = value === null;
|
||||
field.input.disabled = value === null && !!auto;
|
||||
};
|
||||
set(data.scales[kind][axis]);
|
||||
set(data.scales[kind][axis] ?? null);
|
||||
const read = () => readScaleAxis(field.input.value, !!auto?.input.checked, range);
|
||||
return { axis, field, auto, fallback, set, read };
|
||||
});
|
||||
@@ -107,13 +108,14 @@ function buildScaleFields(
|
||||
const saveButton = createButton({
|
||||
label: "축척 저장",
|
||||
onClick: () => {
|
||||
const [x, y] = fields.map((f) => f.read()) as [number | null, number | null];
|
||||
const read = fields.map((f) => [f, f.read() as number | null] as const);
|
||||
const now = data.scales[kind];
|
||||
if (x === now.x && y === now.y) {
|
||||
if (read.every(([f, value]) => value === (now[f.axis] ?? null))) {
|
||||
showToast("바뀐 축척이 없습니다.", "info");
|
||||
return;
|
||||
}
|
||||
void save({ x: scaleToSend(x, fields[0].fallback), y: scaleToSend(y, fields[1].fallback) });
|
||||
const next = Object.fromEntries(read.map(([f, v]) => [f.axis, scaleToSend(v, f.fallback)]));
|
||||
void save(next as DrawingScaleAxes);
|
||||
},
|
||||
});
|
||||
// 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림
|
||||
@@ -127,7 +129,8 @@ function buildScaleFields(
|
||||
const result = f.read();
|
||||
const message = typeof result === "string" ? result : undefined;
|
||||
f.field.setError(message);
|
||||
if (message) errors.set(message, [...(errors.get(message) ?? []), f.axis.toUpperCase()]);
|
||||
const name = f.axis === "plan" ? "평면도" : f.axis.toUpperCase();
|
||||
if (message) errors.set(message, [...(errors.get(message) ?? []), name]);
|
||||
}
|
||||
alertLine.textContent = [...errors].map(([m, axes]) => `${axes.join(" · ")}: ${m}`).join(" / ");
|
||||
alertLine.hidden = errors.size === 0;
|
||||
|
||||
Reference in New Issue
Block a user