diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 419c455fc..30eed5fd1 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -17,12 +17,7 @@ import { leaveForDashboard } from "../b_missing_data_guard"; import { readByKey, stateKey, writeByKey } from "../b_page_state"; import { showToast } from "@ui/ui_template_elements"; import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations"; -import type { PlanPipe } from "../../common_util/common_util_structure_station"; -import { - onStaleStructureTypes, - type StructureInstance, - type StructureType, -} from "../../B05_Profile/B05_Profile_Api_Structures"; +import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures"; import { fetchSectionContext, getSections, @@ -136,13 +131,7 @@ export async function mountCrossView( ); // 「구조물 배치」 — 종단 보기와 같은 폼 한 벌에 횡단 보기 동작을 건다(계획서 24-3b). - let structureMarksSink: - | (( - structures: StructureInstance[], - types: StructureType[], - pipes: ReadonlyArray, - ) => void) - | null = null; + let structureMarksSink: Parameters[0]["onMarks"] | null = null; /** 관 목록 초안을 고치면 한 벌 배수유역도가 그 초안으로 다시 선다(잇단 조작은 한 번). */ let pipeReloadTimer = 0; let wallAdjustTimer = 0; @@ -344,8 +333,7 @@ export async function mountCrossView( shared, ); // 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일). - structureMarksSink = (structures, types, pipes) => - sectionView.setStructureMarks(structures, types, pipes); + structureMarksSink = sectionView.setStructureMarks; // 계획선 편집(▲/▼)은 종단도 한 벌이 맡는다(계획서 24-2c) — 같은 초안 가게 · 같은 막기 규칙. // 종단 그래프 우클릭 — 종단 보기와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자). 어느 길로 diff --git a/B05_Profile/B05_Profile_UI_Structures_Snap.ts b/B05_Profile/B05_Profile_UI_Structures_Snap.ts index 982a3e666..d42ace5a7 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Snap.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Snap.ts @@ -125,11 +125,16 @@ export function snapOldStations(ctx: SnapContext): number { } plan.pipes.forEach(({ from, to, attributes }) => ctx.movePipe(from, to, attributes)); } - if (count || plan.skipped) - showToast( - (count ? `옛 소수 측점 ${count}건을 정수 m 로 옮겼습니다 — [저장]하면 남습니다.` : "") + - (plan.skipped ? ` 맞출 자리에 다른 관이 있는 관 ${plan.skipped}건은 그대로 둡니다.` : ""), - plan.skipped ? "info" : "success", - ); + snapNotice(count, plan.skipped); return count; } + +/** 옛 소수 맞춤 알림 한 줄 — B05 종단 보기 · B06 구조물 목록(관)이 같은 글. */ +export function snapNotice(count: number, skipped: number): void { + if (count || skipped) + showToast( + (count ? `옛 소수 측점 ${count}건을 정수 m 로 옮겼습니다 — [저장]하면 남습니다.` : "") + + (skipped ? ` 맞출 자리에 다른 관이 있는 관 ${skipped}건은 그대로 둡니다.` : ""), + skipped ? "info" : "success", + ); +} diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index d013f7967..ccb74a095 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -54,6 +54,7 @@ import { import { showToast } from "@ui/ui_template_elements"; import { snapStructureStation } from "../common_util/common_util_structure_station"; import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; +import { snapNotice, snapPlan } from "../B05_Profile/B05_Profile_UI_Structures_Snap"; import { adjustDockRoot, refreshAdjustSlots, @@ -427,6 +428,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc (basin) => Math.abs(basin.chainage_m - pipe.chainage_m) < PIPE_MATCH_M, )?.design_flow_m3s ?? null, })); + // 옛 소수 관 자리 = 정수 m 초안(요구 167 · B05 `snapOldStations` 와 같은 셈) — B06 을 바로 열어도 + // 카드 · 측점이 B05 를 거친 것과 같음 · 정본은 [저장]에서 + if (!pendingPipes) snapOldPipes(projectId); section.setPipeFacilities(pipeFacilities); // 종단 알약 레인 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합쳐 B05 와 같은 표기로. pushMarks(); @@ -500,6 +504,24 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc deps.onPipesWritten?.(); } + /** 옛 소수 관 자리 → 정수 m(확인 창 없이) — 계획고 편집 · 고름도 새 자리로(`movePipeTo` 와 같은 따라가기). */ + function snapOldPipes(projectId: string): void { + const plan = snapPlan([], pipeFacilities); + plan.pipes.forEach(({ from, to, attributes }) => { + const hit = pipeFacilities.find((pipe) => pipe.chainage_m === from); + if (!hit) return; + deps.shared.profile?.resetStationEdit(from, to); + followMovedPipe(projectId, from, to); + Object.assign(hit, { chainage_m: to, start_m: attributes.start_m, end_m: attributes.end_m }); + }); + if (plan.pipes.length) { + pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); + persistPipes(); + deps.onStructuresChanged?.(); // 새 자리 횡단을 미리 떠 카드로 + } + snapNotice(plan.pipes.length, plan.skipped); + } + /** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */ function movePipeTo(fromChainageM: number, toChainageM: number): void { if (!deps.projectId) { diff --git a/common_util/common_util_drainage_pipes.py b/common_util/common_util_drainage_pipes.py index 1b8454b1c..14c900656 100644 --- a/common_util/common_util_drainage_pipes.py +++ b/common_util/common_util_drainage_pipes.py @@ -221,7 +221,8 @@ def project_pipe_points(points: list[PipePoint], vertices: list[RouteVertex]) -> if line.length <= 0: return points # 새 노선 위 자리도 정수 m · 일반 측점 1 m 안이면 그 측점(234-1) — 시설 속성은 관과 함께 옮겨 감 - stations = route_general_stations(vertices[-1].chainage_m) + # 끝 = 선 길이(B05 계획선 꼭짓점은 누가거리 0 으로 와서 끝이 0 → 관이 전부 0 m 로 가던 흠) + stations = route_general_stations(line.length) for point in points: if point.x is None or point.y is None: continue diff --git a/resources/tester/helper_322_3_flow.cjs b/resources/tester/helper_322_3_flow.cjs index c4b15cf4e..ca2a6039d 100644 --- a/resources/tester/helper_322_3_flow.cjs +++ b/resources/tester/helper_322_3_flow.cjs @@ -68,6 +68,7 @@ function load(target) { const box = { exports: {} }; cache.set(full, box.exports); const need = (spec) => { + if (spec.startsWith("@ui/")) return { showToast: () => {} }; const alias = spec.startsWith("@util/") ? path.join(ROOT, "common_util", spec.slice(6)) : null; const base = alias ?? path.join(path.dirname(full), spec); const hit = [`${base}.ts`, base.replace(/\.js$/, ".ts"), base].find( @@ -189,6 +190,48 @@ async function run(setup, detail) { sel.selectDesign("P", { chainageM: 101, structureId: "w1" }); sel.followMovedPipe("P", 101, 120); out.follow = [moved, sel.readSelection("P")]; + // 옛 소수 관(검증_sub1 견본) — B06 을 바로 열어도 B05 와 같은 정수 m 초안(`snapPlan` · 요구 167) + const snap = load(file("B05_Profile", "B05_Profile_UI_Structures_Snap.ts")); + const DEC = [ + 85.05, 173.09, 258.12, 352.14, 440.24, 533.98, 619.71, 720.44, 804.18, 899.91, 982.64, + ]; + const ford = new Set([173.09, 352.14]); + const decPipes = DEC.map((at) => ({ + chainage_m: at, + facility: ford.has(at) ? "ford_bridge" : "pipe", + options: { pipe_diameter_mm: "800" }, + })); + const plan = snap.snapPlan([], decPipes); + out.snapTo = [plan.pipes.map((move) => move.to), plan.skipped]; + const snapped = decPipes.map((pipe) => ({ + ...pipe, + chainage_m: plan.pipes.find((move) => move.from === pipe.chainage_m)?.to ?? pipe.chainage_m, + })); + const gridTo = (end) => + Array.from({ length: end / 20 + 1 }, (_v, i) => ({ + chainage_m: i * 20, + kind: "regular", + station_id: `g${i * 20}`, + })); + const titleAt = (at) => (ford.has(at) ? "세월교" : "파형강관 D800"); + const own = [85, 173, 258, 352, 534, 804, 983]; + const onGrid = [440, 620, 720, 900]; + // 서버가 정수 자리로 심은 저장분(격자 자리는 구조물 측점 하나 · grid) — 새 미리 뜬 카드 0 + const savedInt = [ + ...gridTo(1000).filter((s) => !onGrid.includes(s.chainage_m)), + ...onGrid.map((at) => irregular(at, "파형강관 D800", { grid: true })), + ...own.map((at) => irregular(at, at === 173 || at === 352 ? "세월교" : "파형강관 D800")), + ]; + out.snapSavedInt = await run( + { savedPipes: decPipes, draftPipes: snapped, draftStructures: [] }, + { cross_sections: savedInt }, + ); + // 소수 자리로 심긴 저장분(검증_sub1 지금) = B05 를 먼저 연 것과 같음 — 소수 카드는 빼 두고 정수 자리 미리 뜸 + const savedDec = [...gridTo(1000), ...DEC.map((at) => irregular(at, titleAt(at)))]; + out.snapSavedDec = await run( + { savedPipes: decPipes, draftPipes: snapped }, + { cross_sections: savedDec }, + ); process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error?.stack ?? error)); diff --git a/resources/tester/test_322_3_flow.py b/resources/tester/test_322_3_flow.py index c16d9d59e..b902004e0 100644 --- a/resources/tester/test_322_3_flow.py +++ b/resources/tester/test_322_3_flow.py @@ -86,3 +86,28 @@ def test_remove_and_reopen(flows: dict) -> None: [101, "옹벽", False, []], [113, "옹벽", False, []], ] + + +# ── 옛 소수 관(검증_sub1 견본) — B06 을 바로 열어도 B05 와 같은 정수 m 초안(요구 167 · `snapPlan`) ── +SNAPPED = [85, 173, 258, 352, 440, 534, 620, 720, 804, 900, 983] + + +def test_old_decimal_pipes_snap_like_b05(flows: dict) -> None: + assert flows["snapTo"] == [SNAPPED, 0] + + +def test_saved_integer_stations_keep_cards(flows: dict) -> None: + # 서버가 정수 자리로 심은 저장분 = 계획과 같음 — 저장 카드 그대로 · 새 미리 뜬 카드 0 + rows = flows["snapSavedInt"] + assert [row[0] for row in _marked(rows)] == SNAPPED + assert not any(row[2] for row in rows) + assert len(rows) == 58 + + +def test_saved_decimal_stations_follow_plan(flows: dict) -> None: + # 소수 자리 저장분 = B05 를 먼저 연 것과 같음 — 소수 카드는 빼 두고 정수 자리(격자 자리는 사본) + rows = flows["snapSavedDec"] + assert [row[0] for row in _marked(rows)] == SNAPPED + assert all(row[2] for row in _marked(rows)) + assert all(float(row[0]).is_integer() for row in rows) + assert len(rows) == 58 diff --git a/resources/tester/test_322_3_saved_snap.py b/resources/tester/test_322_3_saved_snap.py new file mode 100644 index 000000000..3ada93205 --- /dev/null +++ b/resources/tester/test_322_3_saved_snap.py @@ -0,0 +1,25 @@ +"""PLAN 322-3 고침 — 노선이 바뀌어 관을 좌표로 옮길 때(투영 이월) 끝 = 선 길이. + +B05 계획선 꼭짓점은 누가거리 0 으로 와서 끝이 0 으로 잡혀 관이 전부 0 m 로 가던 흠. +""" + +from __future__ import annotations + +import json + +from B05_Profile.B05_Profile_Engine_Sections import _load_pipe_points + +LINE = [[0.0, 0.0, 100.0], [300.0, 0.0, 100.0]] + + +def test_projected_pipes_snap_to_whole_metres_not_zero(tmp_path) -> None: + folder = tmp_path / "B04_PreProcess" / "drainage" / "edits" + folder.mkdir(parents=True) + points = [ + {"chainage_m": 85.05, "source": "user", "x": 90.05, "y": 0.3}, + {"chainage_m": 173.09, "source": "user", "x": 178.4, "y": 0.2}, + ] + (folder / "pipe_points.json").write_text( + json.dumps({"route_signature": "옛노선", "points": points}), encoding="utf-8" + ) + assert [pipe.chainage_m for pipe in _load_pipe_points(tmp_path, LINE)] == [90.0, 178.0]