From 08e493f13228c5cbf0eaecb9080cb65354f7132d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 29 Sep 2026 11:54:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(B05=C2=B7B06=C2=B7B07):=20=EC=86=8C?= =?UTF-8?q?=EC=88=98=20=EC=B8=A1=EC=A0=90=20=ED=9A=A1=EB=8B=A8=20=E2=80=94?= =?UTF-8?q?=20=EA=B4=80=20=C2=B7=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=EB=A7=88=EB=8B=A4=20=EB=94=B0=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=86=8C=EC=88=98=202=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=20=C2=B7=20=ED=99=94=EB=A9=B4=20=ED=91=9C=EA=B8=B0=EB=A7=8C=20?= =?UTF-8?q?=EC=A0=95=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 20장 소수 측점 횡단(sub6). 사용자 확정: 관 · 구조물이 놓인 소수 측점(예 119.8)에 그 자리 횡단을 따로 둠 · 백엔드 · 파일 · 저장값은 소수 그대로 · 화면 표기만 「측점 + 거리」의 거리를 정수로(5+20). - 이름 규칙 한 벌(B05_Profile_Engine_Sections): cross_key · cross_filename · parse_cross_key · CROSS_KEY_PATTERN — 정수 m 는 옛 이름 그대로(cross_00120m) · 소수 측점은 cross_00119.80m. find_cross_file = 읽기용(새 이름이 없으면 옛 정수 m 이름) · prune_stale_cross_files 도 이것으로 가려 옛 프로젝트의 정수 이름 파일을 지우지 않음 - generate_sections: 정수 m 격자 붙임을 풂 — 같은 이름이 되는 0.005m 안 겹침만 하나로 - B06: 찾기 · 패치 입력(Router_Design 둘) · 샘플 넓힘(SampleExtend)이 find_cross_file · DB 설계 찾기 · 병합(get_cross_section_design · merge_…_by_round)은 ROUND(chainage_m)=정수 → 0.005m 안 매칭 - B07: 도면 id · 장 id(cross_{key}m · cross_s{key}m) 정규식 · 측점 키(cross_station — 정수는 int 그대로라 옛 id · 수량표 키 안 바뀜) · 측점은 파일 속 값으로(옛 정수 이름 파일도) · 횡단 파일 찾기 - 화면 표기 한 벌 common_util/common_util_station.ts(stationLabel · stationPartsLabel · 순수 함수) — common_util_svg 가 다시 내보내고 B05 formatStation 을 대신함(B05 목록 · 표시 · 폼 범위 · 계획노선 편집 · 비정규 측점 라벨 · B06 카드 · 종단 · 유토) · 입력 칸 · 저장값은 소수 그대로 - B05_Profile_UI_Structures_Panel 700줄 → 690(순수 도우미 셋을 Panel_Types 로) - M02_Table_Fill 관연장 찾기는 이미 측점 값(정확 먼저 · 0.5m 안)이라 그대로 시험: test_b05_station_roundtrip(표기 5+20 · 옛 정수 이름 찾기 · 안 지움 · 119.8 과 120 따로) · test_b05_extra_stations_supplier(새 규칙) · test_b06_patch_station(119.8 한 건) · test_b05_b06_save_roundtrip(보낸 관 측점마다 소수 이름 횡단 파일 있음) 등 고친 대상 108 통과 · test_b05_station_merge 통과 · 타입검사 통과. ORCA(IP 5181 · 사본 프로젝트 sub6_검증 — 끝나고 DB 행 · 폴더 지움 · 원본 안 씀): 노출형 횡단수로를 5+19.8 에 놓고 저장 → structures.json 119.8 · 종단 측점 119.8(irregular) + 120 · 파일 cross_00119.80m.json 과 cross_00120m.json · B05 목록 「5+20」 · B06 카드 「5+20 · 119.8m」 열림. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg --- B05_Profile/B05_Profile_Engine_Sections.py | 35 ++++++++++++++++- .../B05_Profile_Engine_Sections_Core.py | 29 ++++---------- .../B05_Profile_UI_IrregularStations.ts | 5 ++- B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 2 +- .../B05_Profile_UI_RouteEdit_Measure.ts | 2 +- .../B05_Profile_UI_RouteEdit_Render.ts | 2 +- B05_Profile/B05_Profile_UI_Structures_List.ts | 2 +- .../B05_Profile_UI_Structures_Marks.ts | 2 +- .../B05_Profile_UI_Structures_Panel.ts | 37 +++++------------- .../B05_Profile_UI_Structures_Panel_Types.ts | 39 ++++++++++++++++++- B05_Profile/B05_Profile_Util_Station.ts | 9 +---- .../B06_Section_Engine_SampleExtend.py | 4 +- B06_Section/B06_Section_Repository.py | 20 +++++----- B06_Section/B06_Section_Router_Design.py | 6 +-- .../B07_DesignDetail_Engine_Cad_Sheet.py | 7 ++-- B07_DesignDetail/B07_DesignDetail_Router.py | 35 ++++++++++------- .../B07_DesignDetail_Router_Support.py | 26 +++++++++---- .../B07_DesignDetail_Router_Support_Io.py | 6 +-- common_util/common_util_station.ts | 25 ++++++++++++ common_util/common_util_svg.ts | 13 +------ .../tester/helper_b05_station_roundtrip.cjs | 12 ++++++ .../tester/test_b05_b06_save_roundtrip.py | 6 +++ .../test_b05_extra_stations_supplier.py | 31 ++++++++------- .../tester/test_b05_station_roundtrip.py | 33 ++++++++++++++-- resources/tester/test_b06_patch_station.py | 6 +++ 25 files changed, 254 insertions(+), 140 deletions(-) create mode 100644 common_util/common_util_station.ts diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index d1491a4b..f804f7e1 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -98,9 +98,39 @@ def _load_design_curves(project_root: Path) -> list[dict[str, Any]]: ) +# ── 횡단 이름 규칙(한 벌) — 파일 이름 · B07 도면 id · 찾기가 모두 이것을 씀 ──────────── +#: 이름 속 측점 글 — 정수 m `00120` · 소수 측점 `00119.80` +CROSS_KEY_PATTERN = r"(\d+(?:\.\d{2})?)" + + +def cross_station(chainage_m: float) -> int | float: + """측점 → 이름 열쇠 값 — 정수 m(0.005m 안)면 int(옛 이름 · 키 그대로) · 아니면 소수 2자리.""" + value = round(float(chainage_m), 2) + whole = round(value) + return int(whole) if abs(value - whole) < 0.005 else value + + +def cross_key(chainage_m: float) -> str: + """이름 속 측점 글 — `00120`(정수 · 옛 이름 그대로) · `00119.80`(소수 측점).""" + station = cross_station(chainage_m) + return f"{station:05d}" if isinstance(station, int) else f"{station:08.2f}" + + +def parse_cross_key(text: str) -> int | float: + """`cross_key` 되읽기.""" + return cross_station(float(text)) + + def cross_filename(chainage_m: float) -> str: """측점 chainage에 대응하는 횡단면 파일명(단일 규칙).""" - return f"cross_{int(round(float(chainage_m))):05d}m.json" + return f"cross_{cross_key(chainage_m)}m.json" + + +def find_cross_file(cross_dir: Path, chainage_m: float) -> Path: + """읽기용 — 지금 이름이 없으면 옛 정수 m 이름(소수 측점이 정수로 붙던 옛 프로젝트).""" + path = cross_dir / cross_filename(chainage_m) + legacy = cross_dir / f"cross_{int(round(float(chainage_m))):05d}m.json" + return path if path.exists() or not legacy.exists() else legacy def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]: @@ -109,8 +139,9 @@ def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]: 측점 정보가 비어 있으면 오삭제를 피하기 위해 아무것도 지우지 않고 빈 집합을 반환한다(호출부는 빈 집합이면 필터를 생략한다). """ + # 옛 프로젝트는 소수 측점 파일이 정수 m 이름이라 찾기 규칙(find_cross_file)으로 가려야 안 지워짐 valid = { - cross_filename(station["chainage_m"]) + find_cross_file(cross_dir, station["chainage_m"]).name for station in stations if isinstance(station, dict) and isinstance(station.get("chainage_m"), (int, float)) } diff --git a/B05_Profile/B05_Profile_Engine_Sections_Core.py b/B05_Profile/B05_Profile_Engine_Sections_Core.py index a76490ee..72279681 100644 --- a/B05_Profile/B05_Profile_Engine_Sections_Core.py +++ b/B05_Profile/B05_Profile_Engine_Sections_Core.py @@ -341,34 +341,19 @@ def generate_sections( if 0.0 <= float(chainage) <= total } if extras: - # 횡단 파일명이 정수 미터 반올림이라(cross_filename) 한 정수 미터에 측점은 하나만 둔다. - # 관 · 구조물 자체 측점은 소수 그대로 — 격자 측점과 같은 정수 미터면 격자 대신 그 측점을 - # 쓴다(시 · 종점 격자만 그대로 두고 라벨을 얹음). 비정규끼리 겹치면 앞선 것만 남긴다. - # ponytail: 파일명이 정수 미터인 동안의 가드. 소수 이름 규칙으로 바꾸면 지운다. + # 관 · 구조물 자리마다 그 소수 측점의 횡단을 따로 둔다(2026-09-29 사용자 확정) — 이름은 + # 소수 2자리(cross_00119.80m · cross_filename). 같은 이름이 되는 0.005m 안 겹침만 하나로: + # 격자와 겹치면 격자 측점에 라벨을 얹고, 비정규끼리 겹치면 앞선 것만 남긴다. grid = [round(float(value), 6) for value in station_chainage] - ends = {grid[0], grid[-1]} - buckets = {int(round(value)): value for value in grid} - taken: set[int] = set() - dropped: set[float] = set() snapped: dict[float, str] = {} for chainage in sorted(extras): - bucket = int(round(chainage)) - if bucket in taken: + near = next((value for value in grid if abs(value - chainage) < 0.005), chainage) + if any(abs(near - taken) < 0.005 for taken in snapped): continue - taken.add(bucket) - regular = buckets.get(bucket) - if regular in ends: - snapped[regular] = extras[chainage] - continue - if regular is not None: - dropped.add(regular) - snapped[chainage] = extras[chainage] + snapped[near] = extras[chainage] extras = snapped station_chainage = np.unique( - np.r_[ - np.array([value for value in grid if value not in dropped], dtype=np.float64), - np.array(sorted(extras), dtype=np.float64), - ] + np.r_[station_chainage, np.array(sorted(extras), dtype=np.float64)] ) station_xy = _interpolate_xy(points, route_chainage, station_chainage) tangents = np.vstack( diff --git a/B05_Profile/B05_Profile_UI_IrregularStations.ts b/B05_Profile/B05_Profile_UI_IrregularStations.ts index e3837b76..798caaf8 100644 --- a/B05_Profile/B05_Profile_UI_IrregularStations.ts +++ b/B05_Profile/B05_Profile_UI_IrregularStations.ts @@ -10,6 +10,7 @@ * chainage_m = 측점번호 × 측점간격(m) + 잔여거리(m). 측점간격은 `getInterval()`로 실시간 조회한다. * ========================================================================== */ +import { stationPartsLabel } from "../common_util/common_util_station"; import { ESCAPE_ROUTE_DEFAULT_WIDTH_M, ESCAPE_ROUTE_WIDTHS_M, @@ -130,9 +131,9 @@ export function irregularStationId(id: string): string { return `irregular:${id}`; } -/** 측점번호+잔여거리 표기 (예: 3+18.0). */ +/** 측점번호+잔여거리 표기 (예: 3+18 · 공용 표기 한 벌 — 거리는 정수로 보이기만). */ export function irregularLabel(station: IrregularStation): string { - return `${station.station}+${station.remainder.toFixed(1)}`; + return stationPartsLabel(station.station, station.remainder); } function field(labelText: string, input: HTMLElement): HTMLLabelElement { diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 2ed105aa..65065cb9 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -24,7 +24,7 @@ import { designHalfWidths, summarizeCross, } from "./B05_Profile_UI_RouteEdit_Cross_Draw"; -import { formatStation } from "./B05_Profile_Util_Station"; +import { stationLabel as formatStation } from "../common_util/common_util_station"; export interface CrossPreviewParams { projectId: string; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts index 0d05da59..b7cffb06 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Measure.ts @@ -13,7 +13,7 @@ import { fetchRouteElevations } from "./B05_Profile_Api_Replan"; import { routePointAtScreen, type RoutePointHit } from "./B05_Profile_UI_RouteEdit_Input"; -import { formatStation } from "./B05_Profile_Util_Station"; +import { stationLabel as formatStation } from "../common_util/common_util_station"; type Vertex = [number, number]; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts index c4ed86a3..7e603d50 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts @@ -26,7 +26,7 @@ import { contourBandRect } from "./B05_Profile_UI_RouteEdit_Input"; import type { CrossWatch } from "./B05_Profile_UI_RouteEdit_Cross"; import { stationFrameAt } from "./B05_Profile_UI_RouteEdit_Frame"; import type { MeasureMark } from "./B05_Profile_UI_RouteEdit_Measure"; -import { formatStation } from "./B05_Profile_Util_Station"; +import { stationLabel as formatStation } from "../common_util/common_util_station"; /** 노드 반지름(px). */ const NODE_R = 4; diff --git a/B05_Profile/B05_Profile_UI_Structures_List.ts b/B05_Profile/B05_Profile_UI_Structures_List.ts index ea48714c..a5d63704 100644 --- a/B05_Profile/B05_Profile_UI_Structures_List.ts +++ b/B05_Profile/B05_Profile_UI_Structures_List.ts @@ -14,7 +14,7 @@ import { type StructureType, } from "./B05_Profile_Api_Structures"; import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel"; -import { formatStation } from "./B05_Profile_Util_Station"; +import { stationLabel as formatStation } from "../common_util/common_util_station"; export interface StructureListParams { /** 목록을 담을