diff --git a/A00_Common/design/design_pipe_ids.ts b/A00_Common/design/design_pipe_ids.ts index c24a2d3d1..d5c9ef567 100644 --- a/A00_Common/design/design_pipe_ids.ts +++ b/A00_Common/design/design_pipe_ids.ts @@ -58,26 +58,36 @@ export function pipeChainageOf(id: string, projectId: string | null = active): n /** 관을 옮김 — 옛 자리 id 가 새 자리로(새 자리에 남은 옛 기록은 뺌) · 옮긴 관의 id. */ export function movePipeId(fromM: number, toM: number, projectId: string | null = active): string { if (!projectId) return `pipe-${toM.toFixed(2)}`; - return movePipeIds([[fromM, toM]], projectId)[0] ?? pipeIdAt(toM, projectId); + const book = bookOf(projectId); + const moving = nearestWithin(book.ids, fromM, (entry) => entry.at); + if (!moving) return pipeIdAt(toM, projectId); + const stale = nearestWithin( + book.ids.filter((entry) => entry !== moving), + toM, + (entry) => entry.at, + ); + const ids = book.ids + .filter((entry) => entry !== stale) + .map((entry) => (entry === moving ? { ...entry, at: toM } : entry)); + writeState("pipe-ids", { next: book.next, ids }, projectId); + return moving.id; } -/** 관 여럿을 한 번에 옮김(노선 따라감 · 411-B) — 옮길 기록을 먼저 다 찾고 옮김(앞 관이 뒤 관 옛 자리로 가도 - * 뒤 관 기록을 안 지움) · 새 자리에 남은 옮기지 않는 옛 기록은 뺌 · 옮긴 관마다 id(못 찾으면 null). */ -export function movePipeIds( - moves: ReadonlyArray, +/** 관 세션 id 를 그 자리로(화면 목록 맞춤 · 411-B) — 짝 = 관 고정 id 가 쥔 세션 id(노선 따라감 · 끌기 · 되돌리기 + * 어느 길로 옮겨도 그 id) · 그 자리 근처의 다른 기록(옛 측점 열쇠가 지은 헛 id)은 뺌 · 장부에 없는 id 는 적음. */ +export function placePipeIds( + places: ReadonlyArray, projectId: string | null = active, -): Array { - if (!projectId) return moves.map(() => null); +): void { + if (!projectId || !places.length) return; const book = bookOf(projectId); - const moving = moves.map(([fromM]) => nearestWithin(book.ids, fromM, (entry) => entry.at)); - const to = new Map(moving.flatMap((entry, i) => (entry ? [[entry, moves[i][1]]] : []))); - const still = book.ids.filter((entry) => !to.has(entry)); - const stale = new Set([...to.values()].map((at) => nearestWithin(still, at, (e) => e.at))); + const to = new Map(places); const ids = book.ids - .filter((entry) => !stale.has(entry)) - .map((entry) => (to.has(entry) ? { ...entry, at: to.get(entry)! } : entry)); - if (to.size) writeState("pipe-ids", { next: book.next, ids }, projectId); - return moving.map((entry) => entry?.id ?? null); + .filter((entry) => to.has(entry.id) || !places.some(([, at]) => isNear(entry.at, at))) + .map((entry) => (to.has(entry.id) ? { ...entry, at: to.get(entry.id)! } : entry)); + for (const [id, at] of places) if (!ids.some((entry) => entry.id === id)) ids.push({ id, at }); + if (JSON.stringify(ids) !== JSON.stringify(book.ids)) + writeState("pipe-ids", { next: book.next, ids }, projectId); } /** 관 목록에 세션 id 를 얹은 사본 — 측점 계획의 덮는 구조물 id(종단 측점 id · 알약 짝)가 세션 id. */ diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index a95ba7a9e..a93da27c5 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -317,13 +317,26 @@ function createPagePipeline( replay = false; if (kept) return { alignment: kept }; // 관 · 구조물도 세션 그대로(가게 · 쓰기 그대로) const line = ctx.before.route?.line ?? savedLine() ?? []; + // 자리를 읽을 선 — 초안 둔 채 열기(앞 노선 없음)의 가게 값 = 이미 초안 노선 자리(앞 따라감이 씀 · 410) + const frame = (list: unknown, saved: unknown) => + !ctx.before.route && ctx.inputs.route && list !== saved + ? (ctx.out.route?.line ?? line) + : line; const { pipes, structures } = ctx.inputs; // 좌표 없는 관 = 앞 따라감(같은 id · 같은 자리) → 정본 좌표 · 옮긴 것 = 그 자리의 앞 노선 위(410) const followed = (ctx.before.follow?.pipes ?? []) as FollowingStructure[]; const inputs = { ...ctx.inputs, - pipes: placed(pipes, line, [...(savedPipes as FollowingStructure[]), ...followed]), - structures: placed(structures, line, [], SECTION_STATION_INTERVAL_M), + pipes: placed(pipes, frame(pipes, savedPipes), [ + ...(savedPipes as FollowingStructure[]), + ...followed, + ]), + structures: placed( + structures, + frame(structures, savedStructures), + [], + SECTION_STATION_INTERVAL_M, + ), }; return followStage({ ...ctx, inputs }); }, diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index e7adc9de9..2ee2a5f5f 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -66,7 +66,7 @@ import "../../B05_Profile/B05_Profile_UI_Style.css"; import "../../B05_Profile/B05_Profile_UI_Style_Table.css"; import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css"; import "../../B05_Profile/B05_Profile_UI_Style_Structures.css"; -import { movePipeIds, pipeIdAt } from "./design_pipe_ids"; +import { pipeIdAt, placePipeIds } from "./design_pipe_ids"; import { useDesignStore } from "./design_store"; import { setPlusStations } from "../../common_util/common_util_structure_station"; import { @@ -114,21 +114,24 @@ export async function mountProfileView( let lastPipes: PipeReport | null = shared.lastPipes ?? null; const designStore = useDesignStore(activeProjectId, null); /** 화면 관 목록 = 배수유역도 알림을 가게 관 초안 측점으로(노선 따라 다시 매김 · 349-0d) · 관 세션 id 도 - * 옮김 — 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 그 관) · id 없는 관은 안 옮김(막 놓은 관). */ - let lastShown: PipeReport = []; + * 그 자리로 — 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 · 되돌려도 그 관) · id 없는 관(막 놓은 관)은 자리로. */ + let sessionIds = new Map(); // 관 고정 id → 세션 id(알약 id) const shownPipes = (pipes: PipeReport): PipeReport => { const shown = followPipeReports(pipes, designStore.get("pipes")).sort( (x, y) => x.chainage_m - y.chainage_m, ); - const was = new Map(lastShown.flatMap((pipe) => (pipe.id ? [[pipe.id, pipe.chainage_m]] : []))); - const moves = shown.flatMap((pipe) => { - const from = pipe.id ? was.get(pipe.id) : undefined; - return from === undefined || Math.abs(pipe.chainage_m - from) <= 0.005 - ? [] - : [[from, pipe.chainage_m] as const]; - }); - if (moves.length) movePipeIds(moves, activeProjectId); - lastShown = shown; + placePipeIds( + shown.flatMap((pipe) => { + const sid = pipe.id && sessionIds.get(pipe.id); + return sid ? [[sid, pipe.chainage_m] as const] : []; + }), + activeProjectId, + ); + sessionIds = new Map( + shown.flatMap((pipe) => + pipe.id ? [[pipe.id, pipeIdAt(pipe.chainage_m, activeProjectId)] as const] : [], + ), + ); return shown; }; const applyPipes = (reported: PipeReport, carryEdits = true): void => { diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index c58a3704f..e39ee5ee1 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -342,17 +342,26 @@ export function mountDrainageToggles( ); } -/** 노선 초안 follow 가 다시 매긴 관 자리(가게 관 초안)를 편집기에 얹음(410) — 짝 = 관 id · 시설 속성 · - * 고름도 따라감 · 재계산 · 초안 쓰기 없음(가게가 이미 그 값). 편집기가 옛 노선 누가거리로 초안을 덮지 않게. */ +/** 노선 초안 follow 가 다시 매긴 관 자리(가게 관 초안)를 편집기에 얹음(410) — 짝 = 관 id(막 놓아 초안에 id + * 없는 관끼리는 누가거리 차례 · 수가 같을 때만) · 시설 속성 · 고름도 따라감 · 재계산 · 초안 쓰기 없음(가게가 + * 이미 그 값). 편집기가 옛 노선 누가거리로 초안을 덮지 않게. */ export function followEditorPipes( editor: PipeEditor, facilities: FacilityStore, followed: ReadonlyArray<{ id?: string | null; chainage_m: number }>, ): void { - const to = new Map(followed.flatMap((pipe) => (pipe.id ? [[pipe.id, pipe.chainage_m]] : []))); + const to = new Map( + followed.flatMap((pipe) => (pipe.id ? [[pipe.id, pipe.chainage_m]] : [])), + ); const before = editor.pipes(); + const byOrder = (a: { chainage_m: number }, b: { chainage_m: number }) => + a.chainage_m - b.chainage_m; + const looseEditor = before.filter((pipe) => !(pipe.id && to.has(pipe.id))).sort(byOrder); + const looseFollowed = followed.filter((pipe) => !pipe.id).sort(byOrder); + if (looseEditor.length === looseFollowed.length) + looseEditor.forEach((pipe, i) => to.set(pipe, looseFollowed[i].chainage_m)); const moves = before.flatMap((pipe) => { - const at = pipe.id ? to.get(pipe.id) : undefined; + const at = (pipe.id ? to.get(pipe.id) : undefined) ?? to.get(pipe); return at === undefined || Math.abs(at - pipe.chainage_m) < 0.005 ? [] : [{ pipe, at, attributes: facilities.get(pipe.chainage_m) }]; diff --git a/resources/tester/helper_411_b_entry.ts b/resources/tester/helper_411_b_entry.ts index eec78f5c4..4023287b6 100644 --- a/resources/tester/helper_411_b_entry.ts +++ b/resources/tester/helper_411_b_entry.ts @@ -5,7 +5,7 @@ export { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_ export { followedMarks } from "../../B05_Profile/logic/B05_Profile_Structure_Follow"; export { movePipeId, - movePipeIds, + placePipeIds, pipeChainageOf, pipeIdAt, usePipeIds, diff --git a/resources/tester/helper_411_b_pipe_id_pairing.mjs b/resources/tester/helper_411_b_pipe_id_pairing.mjs index 7bc24f426..0af5598f8 100644 --- a/resources/tester/helper_411_b_pipe_id_pairing.mjs +++ b/resources/tester/helper_411_b_pipe_id_pairing.mjs @@ -46,28 +46,42 @@ out.reports = m .followPipeReports(reports, draft) .map((r) => [r.id, r.chainage_m, r.start_m ?? null]); -/* 2. 세션 id 장부 — 끌어 옮김(followMovedPipe 길) 뒤 화면 목록 짝(shownPipes 규칙 = id) */ +/* 2. 세션 id 장부 — 끌어 옮김(followMovedPipe 길) · 그 사이 옛 측점 열쇠가 옛 자리에 헛 id 를 지음 → + * 화면 목록 맞춤(shownPipes = 관 고정 id 가 쥔 세션 id 를 새 자리로 · placePipeIds) */ const idA = m.pipeIdAt(100, P); const idB = m.pipeIdAt(150, P); +const sessionIds = new Map([ + ["p10000", idA], + ["p15000", idB], +]); m.movePipeId(150, 80, P); // 끌기 = 옮긴 관 id 그대로 -const was = new Map(reports.map((r) => [r.id, r.chainage_m])); -const shown = m.followPipeReports(reports, draft); -const moves = shown.flatMap((r) => { - const from = was.get(r.id); - return from === undefined || Math.abs(from - r.chainage_m) <= 0.005 ? [] : [[from, r.chainage_m]]; -}); -m.movePipeIds(moves, P); -out.ids = { idA, idB, at80: m.pipeIdAt(80, P), at100: m.pipeIdAt(100, P) }; -out.bookNext = m.pipeIdAt(999, P); // 새 관 = 다음 차례(앞에서 헛 id 가 안 생겼으면 s3) +const stray = m.pipeIdAt(150, P); // 옛 측점 열쇠가 옛 자리를 물음 +const place = (shown, project) => + m.placePipeIds( + shown.map((r) => [sessionIds.get(r.id), r.chainage_m]), + project, + ); +place(m.followPipeReports(reports, draft), P); +out.ids = { idA, idB, stray, at80: m.pipeIdAt(80, P), at100: m.pipeIdAt(100, P) }; -/* 3. 노선 따라감이 두 관을 함께 밂(앞 관이 뒤 관 옛 자리로) — 한 번에 옮김 · 기록 안 잃음 */ +/* 2-1. 되돌림 — 관이 옛 자리로 돌아감 · 그 자리에 헛 id 가 먼저 있어도 그 관 id(헛 기록은 뺌) */ +place( + [ + { id: "p15000", chainage_m: 150 }, + { id: "p10000", chainage_m: 100 }, + ], + P, +); +out.back = { at150: m.pipeIdAt(150, P), strayAt: m.pipeChainageOf(stray, P) }; + +/* 3. 노선 따라감이 두 관을 함께 밂(앞 관이 뒤 관 옛 자리로) — 기록 안 잃음 · 헛 id 안 생김 */ const Q = "q411"; const q1 = m.pipeIdAt(100, Q); const q2 = m.pipeIdAt(103, Q); -m.movePipeIds( +m.placePipeIds( [ - [100, 103], - [103, 106], + [q1, 103], + [q2, 106], ], Q, ); @@ -111,6 +125,27 @@ out.editor = { m.followEditorPipes(editor, facilities, [{ id: "p10000", chainage_m: 103 }]); // 같은 값 = 그대로 out.editorSame = editor.pipes().map((p) => p.chainage_m); +/* 5-1. 막 놓은 관 — 초안엔 id 없음 · 편집기는 서버 id(재계산 응답) → id 없는 것끼리 차례 짝 */ +const fresh = m.createPipeEditor(() => {}); +fresh.setPipes([ + { chainage_m: 120, reason: "confirmed", id: "p12000" }, + { chainage_m: 351, reason: "stream", id: "p35100" }, +]); +m.followEditorPipes(fresh, m.createFacilityStore(), [ + { chainage_m: 120 }, + { id: "p35100", chainage_m: 353 }, +]); +const fresh2 = m.createPipeEditor(() => {}); +fresh2.setPipes([ + { chainage_m: 200, reason: "confirmed", id: "p20000" }, + { chainage_m: 351, reason: "stream", id: "p35100" }, +]); +m.followEditorPipes(fresh2, m.createFacilityStore(), [ + { chainage_m: 204 }, + { id: "p35100", chainage_m: 353 }, +]); +out.fresh = [fresh.pipes(), fresh2.pipes()].map((list) => list.map((p) => [p.id, p.chainage_m])); + /* 6. B06 측점 열쇠 — 서버 관 id 도 관(계산 묶음 `pipe:` 와 같음) */ const planned = [ { chainage_m: 80.17, owners: ["p8017"], role: "anchor" }, diff --git a/resources/tester/test_411_b_pipe_id_pairing.py b/resources/tester/test_411_b_pipe_id_pairing.py index 368b400b1..2e0a47b5e 100644 --- a/resources/tester/test_411_b_pipe_id_pairing.py +++ b/resources/tester/test_411_b_pipe_id_pairing.py @@ -6,7 +6,7 @@ B06 열쇠 = 계산 묶음과 같음. 새 `pipe-sN` 을 지어 새 측점 · 계획고 0 이 생기던 흠. 진짜 TS 를 vite ssr 로 묶어(`helper_411_b_entry.ts`) Node 로 잰다: ① `followPipeReports` 짝 = 관 고정 id - ② 세션 id 장부(`movePipeId` · `movePipeIds`) + ② 세션 id 장부(`movePipeId` 끌기 · `placePipeIds` 화면 맞춤) ③ `followedMarks` ④ 배수 편집기 `followEditorPipes`(410 · 따라간 자리 받음) ⑤ B06 `useStationOwners` · `stationKeysOf`. @@ -75,15 +75,19 @@ def test_알림_목록_짝은_id(out): def test_앞_관_지나_옮겨도_세션_id_그대로(out): ids = out["ids"] - assert ids["at80"] == ids["idB"] # 옮긴 관 = 제 id + assert ids["at80"] == ids["idB"] # 옮긴 관 = 제 id(옛 자리 헛 id 를 안 받음) assert ids["at100"] == ids["idA"] # 이웃 관 = 제 id(새 pipe-sN 아님) - assert out["bookNext"] == "pipe-s3" # 헛 id 없음 = 알약 · 측점 수 그대로 + + +def test_되돌려_옛_자리로_가도_그_관_id(out): + assert out["back"]["at150"] == out["ids"]["idB"] + assert out["back"]["strayAt"] is None # 그 자리 헛 기록은 장부에서 빠짐 def test_따라감이_여럿을_함께_밀어도_기록_안_잃음(out): batch = out["batch"] assert batch["at103"] == batch["q1"] and batch["at106"] == batch["q2"] - assert batch["next"] == "pipe-s3" + assert batch["next"] == "pipe-s3" # 헛 id 없음 = 알약 · 측점 수 그대로 def test_알약은_그_관_자리(out): @@ -98,6 +102,13 @@ def test_배수_편집기는_따라간_자리를_받음(out): assert out["editorSame"] == [98, 103] +def test_막_놓은_관은_id_없는_것끼리_차례로_따라감(out): + assert out["fresh"] == [ + [["p12000", 120], ["p35100", 353]], + [["p20000", 204], ["p35100", 353]], + ] + + def test_B06_열쇠는_서버_관_id_도_pipe(out): assert out["keys"] == ["structure:st-1", "pipe:p8017"] assert out["keys"][1] == out["pipelineKey"] # 계산 묶음 측점 열쇠와 같음