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 { /** 목록을 담을