diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index b7d5234e5..8d6e8082e 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -74,7 +74,7 @@ export const STATE_REGISTRY = { "routeedit-locks": { bucket: "pref", scope: "project" }, /** B05 계획노선 편집 창 — 분석 지형(las · sheet). 확정 지표면은 안 바꿈 */ "routeedit-terrain": { bucket: "pref", scope: "project" }, - /** B05 계획노선 편집 창 — 측점 스냅 간격(0.1 · 1 · 10 · station) */ + /** B05 계획노선 편집 창 — 측점 스냅 간격(10 · station · 280-4) */ "routeedit-snap-step": { bucket: "pref", scope: "project" }, "masshaul-open": { bucket: "pref", diff --git a/A00_Common/design/design_dock.ts b/A00_Common/design/design_dock.ts index 8f29d14e0..9b2c1a00a 100644 --- a/A00_Common/design/design_dock.ts +++ b/A00_Common/design/design_dock.ts @@ -2,9 +2,8 @@ * design_dock.ts * B05 · B06 한 페이지 두 보기의 **좌측 패널 맨 아래 고정 영역**(계획서 24-1 · 24-2b). * - * 구성 = [위에 놓을 것(구조물 목록)][구분선][단추 행]. 단추 넷은 두 보기가 **같다**: - * [초기화] [저장] [확정] [보기 전환](종단 보기에서는 「횡단 보기」 · 횡단 보기에서는 - * 「종단 보기」). 누르면 무엇을 하는지는 페이지가 `DesignDockActions` 로 건넨다 — 이 파일은 + * 구성 = [위에 놓을 것(구조물 목록)][구분선][단추 행]. 단추 셋은 두 보기가 **같다**: + * [초기화] [저장] [확정](보기 오가기는 공용 [<] [>] · 요구 280-2). 누르면 무엇을 하는지는 페이지가 `DesignDockActions` 로 건넨다 — 이 파일은 * 단추를 만들고 켜고 끄기만 한다(저장 흐름은 `design_save.ts` 몫). * ========================================================================== */ @@ -26,15 +25,13 @@ export interface DesignDockActions { reset: DesignDockAction; save: DesignDockAction; confirm: DesignDockAction; - /** [보기 전환] — 지금은 주소 이동이다(2단계에서 같은 페이지 안 전환으로 바뀜). */ - switchView: DesignDockAction; } export interface DesignDock { root: HTMLElement; - /** 단추 넷의 켬·끔·설명을 다시 준다(자료가 늦게 오는 페이지가 부름). */ + /** 단추 셋의 켬·끔·설명을 다시 준다(자료가 늦게 오는 페이지가 부름). */ setActions: (actions: Partial) => void; - /** 보기가 바뀌면 [보기 전환] 글자가 따라 바뀐다. */ + /** 보기가 바뀌면 단추 줄 class 가 따라 바뀐다. */ setView: (view: DesignView) => void; /** 「계산 중」 점 — 묶음 차례가 떠 있는 동안 켬(386-2). */ setBusy: (busy: boolean) => void; @@ -44,11 +41,6 @@ function label(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } -/** 보기 전환 단추 글자 — 가려는 **쪽** 이름이다. */ -export function switchViewLabel(view: DesignView): string { - return label(view === "profile" ? "Design_Btn_ToCross" : "Design_Btn_ToProfile"); -} - /** * 아래 고정 영역을 만든다. `above` 는 단추 행 위에 놓을 것(구조물 목록) — 목록을 「구조물 배치」 * 본문에 두면 고른 구조물의 폼 길이에 밀려 화면 밖으로 나갔다(2026-08-18 사용자 지시). @@ -93,20 +85,13 @@ export function createDesignDock( variant: "filled", onClick: press("confirm"), }); - const switchButton = createButton({ - label: switchViewLabel(view), - variant: "ghost", - onClick: press("switchView"), - }); resetButton.dataset.dockAction = "reset"; saveButton.dataset.dockAction = "save"; confirmButton.dataset.dockAction = "confirm"; - switchButton.dataset.dockAction = "switchView"; const buttons = { reset: resetButton, save: saveButton, confirm: confirmButton, - switchView: switchButton, }; const sync = (): void => { @@ -116,12 +101,10 @@ export function createDesignDock( if (action.title) buttons[name].title = action.title; else buttons[name].removeAttribute("title"); }); - (switchButton.querySelector(".ui-btn__label") ?? switchButton).textContent = - switchViewLabel(current); row.className = current === "profile" ? "b05-route__actions" : "b06-profile__actions"; }; - row.append(resetButton, saveButton, confirmButton, switchButton); + row.append(resetButton, saveButton, confirmButton); root.append(...above, divider, row, busy); sync(); diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 4a7c4fe4d..cef8daf8e 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -11,13 +11,12 @@ * [확정]은 횡단 보기를 숨긴 채 세워 횡단 자료를 같은 페이지에서 얻는다(`ensureView`). * ========================================================================== */ -import { ROUTES } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; import { splitSidebarActions } from "@ui/ui_template_overlay"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav"; import { readState, writeState } from "../b_page_state"; -import { currentRoute, navigateTo, remountRoute } from "../router"; +import { currentRoute, remountRoute } from "../router"; import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch"; import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Context"; import { createStructuresSection, type StructuresSection } from "../b_structures_section"; @@ -330,7 +329,6 @@ function routeIdOf(shared: DesignPageShared): number | null { /** 좌측 패널 한 벌 — 부품은 두 보기가 `shared.parts` 에 넣고, 칸은 보기가 처음 보일 때 선다. */ export function sharedPanel(shared: DesignPageShared): DesignPanel { if (shared.panel) return shared.panel; - const other = (): DesignView => (shared.view() === "profile" ? "cross" : "profile"); const panel = createDesignPanel( { projectId: shared.projectId, @@ -345,10 +343,6 @@ export function sharedPanel(shared: DesignPageShared): DesignPanel { // 자료가 서기 전에는 잠근다 — 보기가 `designReady` 로 연다. save: { run: () => saveSharedDesign(shared, false), enabled: false }, confirm: { run: () => saveSharedDesign(shared, true), enabled: false }, - // [보기 전환] = 주소만 바꾼다 — 같은 페이지라 라우터가 보기만 바꾼다(다시 안 만듦). - switchView: { - run: () => navigateTo(other() === "profile" ? ROUTES.B05_PROFILE : ROUTES.B06_SECTION), - }, }, }, shared.view(), diff --git a/A00_Common/design/logic/design_pipeline_types.ts b/A00_Common/design/logic/design_pipeline_types.ts index f63d5fc4a..1c1e6cc14 100644 --- a/A00_Common/design/logic/design_pipeline_types.ts +++ b/A00_Common/design/logic/design_pipeline_types.ts @@ -39,7 +39,7 @@ * * 측점 열쇠(344-3 · `stationKey` · `B05_Profile/logic/B05_Profile_Sections_Stations.ts`): * | 측점 | 열쇠 | 뜻 | - * | 일반(격자) | grid:k | k = 원래 격자 번호(누가거리 ÷ 측점 간격 반올림) · 노선 모양이 바뀌어도 같은 번호 · 스냅으로 옮긴 측점은 원래 번호 | + * | 일반(격자) | grid:k | k = 원래 격자 번호(누가거리 ÷ 측점 간격 반올림) · 노선 모양이 바뀌어도 같은 번호 | * | 관 | pipe: | 관 세션 id(없으면 `pipe-{누가거리 2자리}`) | * | 구조물 | structure: | structure_id · 구간형 시작 · 끝 = `:start` · `:end` 덧붙임 | * | 시종점 | end:BP · end:EP | 그 자리 관 · 구조물이 있어도 시종점(서버 bp · ep 와 같음) | @@ -72,7 +72,7 @@ export interface RouteDraft { curveArc: Array; /** [측점 스냅](요구 224-2) — 켜면 호 안 측점 하나를 곡선 중점으로. */ stationSnap: boolean; - /** 스냅 간격(m · 0.1 · 1 · 10 · null·없음 = 측점 No.) — MC 측점 값 자릿수(390-5). */ + /** 스냅 간격(m · 10 · null·없음 = 측점 No. · 280-4) — MC 측점 값 자릿수(390-5). */ stationSnapStep?: number | null; /** 분석 지형(`las` · `sheet` · null = 확정 지표면) — ground 단계가 고를 격자. */ terrain: string | null; diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index 2b1d11a84..df97ed54a 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -117,7 +117,7 @@ export interface RoutePlanResponse { edited: boolean; /** [측점 스냅] 켬(요구 224-2) — [확인] 때 곡선표에 적은 값. 칸 없으면 끔. */ station_snap?: boolean; - /** 스냅 간격(m · 0.1 · 1 · 10) — 칸 없거나 null = 측점 No.(390-5 · MC 측점 값 자릿수). */ + /** 스냅 간격(m · 10) — 칸 없거나 null = 측점 No.(390-5 · MC 측점 값 자릿수). */ station_snap_step?: number | null; /** 현재 초기값 — 서버가 [확인] 때 프로젝트에 적어 둔 초기노선 갈래(계획서 20). */ current_initial?: { logic: string; weights: Record | null }; diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index 108484692..4960428ae 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -107,10 +107,10 @@ def load_station_snap(project_root: Path) -> bool: def load_station_snap_step(project_root: Path) -> float | None: """스냅 간격(m) — 곡선표 `"station_snap_step"`(390-5 · [확인] 때 적힘). - 칸 없으면 None(측점 No.). + 10 만 그 값 · 칸 없음 · 옛 0.1 · 1 m 등 그 밖 = None(측점 No. · 280-4). """ step = _read_design_curve_table(project_root).get("station_snap_step") - return float(step) if isinstance(step, int | float) and step > 0 else None + return 10.0 if isinstance(step, int | float) and step == 10 else None def _read_design_curve_table(project_root: Path) -> dict[str, Any]: diff --git a/B05_Profile/B05_Profile_Engine_Sections_Core.py b/B05_Profile/B05_Profile_Engine_Sections_Core.py index b1ef38200..8cc6afa33 100644 --- a/B05_Profile/B05_Profile_Engine_Sections_Core.py +++ b/B05_Profile/B05_Profile_Engine_Sections_Core.py @@ -6,10 +6,10 @@ ⚠ 파이썬·TS 짝 — `_interpolate_xy`·`_tangent_at` ↔ `B05_Profile_UI_RouteEdit_Frame.ts` (`stationFrameAt`). 거울 시험 `resources/tester/test_b05_routeedit_frame_mirror.py`. -⚠ 파이썬·TS 짝 — 측점 스냅 `_chainages` · `_design_curve_spans` · `snap_curve_stations` · +⚠ 파이썬·TS 짝 — 측점 스냅 `_chainages` · `_design_curve_spans` · `curve_stations` · `_mc_scale` ↔ `B05_Profile_UI_RouteEdit_Frame.ts` `stationsAlong` · `curveSpans` · -`snapCurveStations` · `mcScale`. 거울 시험 `resources/tester/test_224_2_station_snap.py` · -간격별 MC 값 `test_390_5_snap_step_ops.py`. +`curveStations` · `mcScale`. 거울 시험 `resources/tester/test_224_2_station_snap.py` · +`test_280_4_stations.py` · 간격별 MC 값 `test_390_5_snap_step_ops.py`. ⚠ 파이썬·TS 짝 — `generate_sections` 지반 부분 ↔ `B05_Profile/logic/B05_Profile_Sections_Ground.ts`. ⚠ 파이썬·TS 짝 — `generate_sections` 측점 부분(격자 · 스냅 · 구조물 · 곡선 측점 · 종류 · 방위각) · `_plan_radii` · `_curve_widenings` · `_design_curve_widenings` ↔ `B05_Profile/logic/B05_Profile_Sections_Stations.ts` @@ -295,35 +295,6 @@ def _mc_scale(step_m: float | None) -> int: return 10 if step_m is not None and step_m < 1 else 1 -def snap_curve_stations( - stations: np.ndarray | list[float], - spans: list[tuple[float, ...]], - step_m: float | None = None, -) -> np.ndarray: - """측점 스냅(요구 224-2) — 호(BC~EC) 안 측점 중 곡선 중점 MC 에 가장 가까운 하나를 MC - (floor(MC·s+0.5)/s · s = `_mc_scale(step_m)`)로 옮김. 측점 없는 호는 그대로 · 옮긴 자리가 다른 - 측점과 같은 값이면 하나로. - - `spans` = `_design_curve_spans` 꼴(앞 두 칸 = BC · EC 누가거리) · `step_m` = 스냅 간격 - (0.1 · 1 · 10 · None = 측점 No. · 390-5). - """ - scale = _mc_scale(step_m) - values = [float(value) for value in stations] - for span in spans: - start, end = float(span[0]), float(span[1]) - middle = (start + end) / 2.0 - inside = [ - index - for index, value in enumerate(values) - if start + CURVE_STATION_EDGE_M < value < end - CURVE_STATION_EDGE_M - ] - if not inside: - continue - nearest = min(inside, key=lambda index: abs(values[index] - middle)) - values[nearest] = math.floor(middle * scale + 0.5) / scale - return np.unique(np.asarray(values, dtype=np.float64)) - - def curve_stations( stations: np.ndarray | list[float], spans: list[tuple[float, ...]], @@ -331,7 +302,7 @@ def curve_stations( step_m: float | None = None, ) -> list[float]: """곡선 측점(요구 252-2) — MC(floor(MC·s+0.5)/s · s = `_mc_scale(step_m)`)에 측점이 - 없으면 그 값 · 0.005 m 안 측점(일반 · 스냅으로 옮긴 · 관 · 구조물)이 있거나 노선 밖이면 뺌 · + 없으면 그 값 · 0.005 m 안 측점(일반 · 관 · 구조물)이 있거나 노선 밖이면 뺌 · BC · EC 는 안 더함. `curveStations` 짝.""" scale = _mc_scale(step_m) taken = [float(value) for value in stations] @@ -393,9 +364,8 @@ def _station_layout( 곡선 측점). `generate_sections` · `planned_curve_stations`(B06 측점 맞춤 판정 · 407-3)가 같은 길.""" total = float(route_chainage[-1]) station_chainage = _chainages(total, options.station_interval_m, options.include_endpoint) + # 일반측점은 스냅이 옮기지 않음(요구 280-4) — MC 는 아래 곡선 측점으로 더함 spans = _design_curve_spans(points, route_chainage, design_curves or []) - if station_snap: - station_chainage = snap_curve_stations(station_chainage, spans, station_snap_step) # 규칙(격자) 측점 chainage 집합 — 비정규와 겹칠 때 규칙 측점을 우선 판정하는 데 쓴다. regular_rounded = {round(float(value), 6) for value in station_chainage} # 비정규 측점(구조물)을 격자 측점 배열에 병합한다. 정렬·중복 제거만 하면 이후 접선·횡단 @@ -459,9 +429,9 @@ def generate_sections( ) -> dict[str, Any]: """확정 경로로 CAD 인계 가능한 종단·횡단 원시 데이터를 생성한다. - `station_snap` — 호 안 측점 하나를 곡선 중점으로 옮김(`snap_curve_stations` · 요구 224-2) · - `station_snap_step` — 그 MC 측점 값 자릿수를 정하는 스냅 간격(390-5 · None = 측점 No.). - MC 에 측점이 없으면 곡선 측점(kind `curve` · `curve_stations` · 요구 252-2) 하나를 더함. + `station_snap` · `station_snap_step` — MC 측점 값 자릿수를 정하는 스냅 간격(390-5 · None = 측점 No.) · + 일반측점은 옮기지 않음(요구 280-4). MC 에 측점이 없으면 곡선 측점(kind `curve` · + `curve_stations` · 요구 252-2 · 「No.2+10 (MC)」) 하나를 더함. """ options = options or SectionGenerationOptions() options.validate() diff --git a/B05_Profile/B05_Profile_Schema.py b/B05_Profile/B05_Profile_Schema.py index 7ab3241d1..aaa7a3c75 100644 --- a/B05_Profile/B05_Profile_Schema.py +++ b/B05_Profile/B05_Profile_Schema.py @@ -433,4 +433,4 @@ class RouteReplanRequest(BaseModel): station_snap: bool = False """[측점 스냅] 켬(요구 224-2) — 곡선표 `"station_snap"` 으로 남아 측점 셈이 읽음.""" station_snap_step: float | None = None - """스냅 간격(m · 0.1 · 1 · 10 · None = 측점 No. · 390-5) — 곡선표 `"station_snap_step"`.""" + """스냅 간격(m · 10 · None = 측점 No. · 390-5 · 280-4) — 곡선표 `"station_snap_step"`.""" diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index ad721cc0f..a59ecb239 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -110,12 +110,10 @@ export function createRouteEditChrome( + title="켜면 고친 곡선의 가운데(MC)를 고른 간격 배수에 맞춥니다 — 일반측점(20 m)은 그대로 · MC 가 일반측점 위가 아니면 「No.N+10 (MC)」 특별측점을 더함 · 손대지 않은 곡선은 그 자리 유지 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장">측점 스냅