diff --git a/A00_Common/design/design_facility_lane.ts b/A00_Common/design/design_facility_lane.ts index 374ef631e..1db49cdf5 100644 --- a/A00_Common/design/design_facility_lane.ts +++ b/A00_Common/design/design_facility_lane.ts @@ -171,10 +171,12 @@ export function createFacilityLane( to.setStructureTypes([...types]); to.setStructures(marks()); const signature = JSON.stringify(stations); + // 종류 표를 받기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467) · // 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림 - if (force || painted?.to !== to || painted.stations !== signature) + if (types.length && (force || painted?.to !== to || painted.stations !== signature)) { to.setIrregularStations(stations); - painted = { to, stations: signature }; + painted = { to, stations: signature }; + } to.setMinCoverPipes(pipes as MinCoverPipe[]); } diff --git a/B06_Section/B06_Section_Section_Canon.ts b/B06_Section/B06_Section_Section_Canon.ts index 85f3a3014..a72253419 100644 --- a/B06_Section/B06_Section_Section_Canon.ts +++ b/B06_Section/B06_Section_Section_Canon.ts @@ -9,8 +9,23 @@ import { readState } from "../A00_Common/b_page_state"; import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch"; -/** 정본 측점 설계 — 받을 때 뜬 사본(다시 셈 · 초안 얹기가 고치는 캐시 객체와 따로). */ -export type CanonRow = { chainage_m: number; design: CrossSection["design"] }; +/** 정본 측점 설계 — 받을 때 뜬 사본(다시 셈 · 초안 얹기가 고치는 캐시 객체와 따로) · 자리 · 표본(제자리로 돌아온 + * 측점이 정본 표본을 다시 씀 · 467 · 배열은 받은 것 그대로 · 다시 셈은 새 배열로 갈아 끼움). */ +export type CanonRow = { chainage_m: number; design: CrossSection["design"] } & Partial< + Pick< + CrossSection, + | "center_x" + | "center_y" + | "center_z" + | "frame" + | "samples" + | "azimuth_deg" + | "plan_radius_m" + | "curve_outer_side" + | "curve_widening_m" + | "uphill_side" + > +>; const canon = new Map(); /** 묶음이 먼저 푼 세션 상세 — 상세 가게가 처음 읽을 때 가져감(다시 안 풂). */ @@ -23,9 +38,19 @@ export function keepCanon(projectId: string, routeId: number, detail: SectionDet parsed.delete(key); canon.set( key, - detail.cross_sections.map(({ chainage_m, design }) => ({ - chainage_m, - design: design ? { ...design } : design, + detail.cross_sections.map((section) => ({ + chainage_m: section.chainage_m, + design: section.design ? { ...section.design } : section.design, + center_x: section.center_x, + center_y: section.center_y, + center_z: section.center_z, + frame: section.frame, + samples: section.samples, + azimuth_deg: section.azimuth_deg, + plan_radius_m: section.plan_radius_m, + curve_outer_side: section.curve_outer_side, + curve_widening_m: section.curve_widening_m, + uphill_side: section.uphill_side, })), ); } diff --git a/B06_Section/B06_Section_Section_Store.ts b/B06_Section/B06_Section_Section_Store.ts index a212a4780..d263eefb2 100644 --- a/B06_Section/B06_Section_Section_Store.ts +++ b/B06_Section/B06_Section_Section_Store.ts @@ -244,6 +244,7 @@ export async function swapSectionStations( oldKeys, interval, useDesignStore(projectId, null).get("routeDraft") !== null, + savedDesigns(projectId, routeId) ?? [], ); // 초안 없으면 정본 표본(467) // 설계는 사본 — 면적 얹기 · 다시 셈이 앞 상세(초안 전 상세 등)의 설계를 제자리로 안 고침(349-0o) for (const section of swap.detail.cross_sections) diff --git a/B06_Section/logic/B06_Section_Station_Swap.ts b/B06_Section/logic/B06_Section_Station_Swap.ts index ac3583929..0c87930ba 100644 --- a/B06_Section/logic/B06_Section_Station_Swap.ts +++ b/B06_Section/logic/B06_Section_Station_Swap.ts @@ -7,7 +7,7 @@ * · 지반 표본 · 상단측 · 중심고 = 자리 · 축이 같은 앞 횡단이면 앞 값(서버 샘플링 정본 · 덧 측점만 넣어도 나머지 * 측점 면적이 브라우저 지형으로 안 바뀜 · 433-11) · 새 측점 · 옮긴 측점만 지반 표본(채워 왔으면). * · 노선 초안 없는 차례(관 · 구조물 · 추가 측점만) = 같은 자리(누가거리 · 좌표 0.01 m · 축 1e-3) 앞 횡단 — 없으면 - * 그 자리 옛 횡단(격자에 구조물을 놓고 뺌) — 의 기하 · 표본 그대로(브라우저 측점 틀 ↔ 서버 틀 mm 차가 정본 표본을 + * 그 자리 옛 횡단(격자에 구조물을 놓고 뺌) · 그다음 저장 정본(`saved` · 옮겼다 제자리로 온 관) — 의 기하 · 표본 그대로(브라우저 측점 틀 ↔ 서버 틀 mm 차가 정본 표본을 * 안 덮음 · 467 · 요구 272) · 설계는 지금 규칙. * · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움). * · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428). @@ -109,9 +109,10 @@ function kindOf(station: PipelineStation): SectionStation["kind"] { return station.kind === "grid" ? "regular" : "irregular"; } -/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음). */ +/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음) · 시설 측점 자리가 일반 + * 측점으로 돌아감(관 · 구조물 지움 · 467)은 다른 목록. */ export function sameStationList( - sections: ReadonlyArray<{ chainage_m: number }>, + sections: ReadonlyArray<{ chainage_m: number; kind?: SectionStation["kind"] }>, oldKeys: ReadonlyArray, stations: ReadonlyArray, ): boolean { @@ -120,7 +121,8 @@ export function sameStationList( stations.every( (station, index) => Math.abs(station.chainageM - sections[index].chainage_m) < 1e-6 && - (oldKeys[index] === station.key || kindOf(station) === "regular"), + (oldKeys[index] === station.key || + (kindOf(station) === "regular" && sections[index].kind !== "irregular")), ) ); } @@ -149,8 +151,24 @@ export interface StationSwap { moved: boolean; } +/** 정본 자리 한 벌(저장 상세 측점 · 467) — 제자리로 돌아온 측점이 정본 기하 · 표본을 다시 씀. */ +export type CanonPlace = Pick< + CrossSection, + | "chainage_m" + | "center_x" + | "center_y" + | "center_z" + | "frame" + | "samples" + | "azimuth_deg" + | "plan_radius_m" + | "curve_outer_side" + | "curve_widening_m" + | "uphill_side" +>; + /** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터 · 허용 `axisTol`)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */ -function samePlace(prior: CrossSection, station: PipelineStation, axisTol = 1e-4): boolean { +function samePlace(prior: CanonPlace, station: PipelineStation, axisTol = 1e-4): boolean { const [lx, ly] = prior.frame?.left_xy ?? [Number.NaN, Number.NaN]; return ( Math.abs(prior.chainage_m - station.chainageM) < 0.01 && @@ -174,6 +192,7 @@ export function swapCrossSections( oldKeys: ReadonlyArray, intervalM: number | null, routeDraft = false, + saved: ReadonlyArray> = [], ): StationSwap { const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section])); const used = new Set(); @@ -218,13 +237,15 @@ export function swapCrossSections( moves.set(text(prior.chainage_m), text(station.chainageM)); // 노선 초안 없는 차례 · 같은 자리 = 앞 횡단(없으면 그 자리 옛 횡단 · 격자에 구조물을 놓고 뺌) 기하 · 표본 그대로 // (467 · 되돌리면 정본과 같음) - const atPlace = (section: CrossSection | undefined): section is CrossSection => - !!section?.samples?.length && samePlace(section, station, CANON_AXIS_TOL); - const canon = routeDraft + const atPlace = (section: Partial | undefined): section is CanonPlace => + !!section?.samples?.length && + !!section.frame && + samePlace(section as CanonPlace, station, CANON_AXIS_TOL); + const canon: CanonPlace | undefined = routeDraft ? undefined : atPlace(prior) ? prior - : detail.cross_sections.find(atPlace); + : (detail.cross_sections.find(atPlace) ?? saved.find(atPlace)); // 자리 같은 앞 횡단 = 서버 표본 그대로(433-11) · 아니면 새 지반 표본(채워 왔으면) const fresh = !canon && !!station.ground && !(prior?.samples?.length && samePlace(prior, station)); diff --git a/resources/tester/helper_272_berm_source.mjs b/resources/tester/helper_272_berm_source.mjs index cb34698d0..1f22b7f66 100644 --- a/resources/tester/helper_272_berm_source.mjs +++ b/resources/tester/helper_272_berm_source.mjs @@ -74,6 +74,27 @@ out.stored = job.storedCases.map((spec) => m.storedBermSpec(spec)); out.lane = steps; } +/* 2-2. 시설 줄 칠함 — 종류 표 받기 전 측점(빈 목록)을 종단도에 안 씀 · 받은 뒤 한 번 */ +{ + const P2 = "p272paint"; + const wrote = []; + const fake = { + setStructureTypes: () => undefined, + setStructures: () => undefined, + setIrregularStations: (list) => wrote.push(list.length), + setMinCoverPipes: () => undefined, + resetStationEdit: () => undefined, + }; + const lane = m.createFacilityLane(P2, () => fake); + lane.set({ pipes: [] }); + lane.set({ structures: [] }, false); + lane.paint(); + const before = wrote.length; + lane.setTypes([{ type_id: "berm", name: "소단", placement: "interval" }]); + out.paint = { before, after: wrote.length }; + lane.dispose(); +} + /* 3. 다시 셈 소단 원천 — 사본 없음(저장분 design.berm) · 사본 = 같은 구간 · 사본 = 빈 목록 */ { m.writeState("std-cross", job.standard, P); diff --git a/resources/tester/helper_272_swap_canon.mjs b/resources/tester/helper_272_swap_canon.mjs index ca96b8122..f73030665 100644 --- a/resources/tester/helper_272_swap_canon.mjs +++ b/resources/tester/helper_272_swap_canon.mjs @@ -183,6 +183,35 @@ const out = {}; }; } +/* 1-3. 관을 옮겼다 제자리로 — 저장 정본(saved)이 있으면 관 측점도 정본 표본 · 기하 */ +{ + const moved = stationsAt(55); + const keep = m.swapCrossSections(canonDetail(), moved, KEYS, 20, false); + const saved = canonDetail().cross_sections; + const back = m.swapCrossSections(keep.detail, stationsAt(45), keysOf(moved), 20, false, saved); + out.pipeBack = { pipe: report(back.detail)["45.00"], bytes: bytesSame(back.detail, [45]) }; +} + +/* 1-4. 일반 측점 자리(40)에 선 관을 지움 — 자리 · 수 같아도 다른 목록(관 카드가 안 남음) */ +{ + const onGrid = stationsAt(45).map((s) => + s.key === "grid:2" ? { ...s, key: "pipe:p9", kind: "pipe", id: "p9" } : s, + ); + const placed = m.swapCrossSections(canonDetail(), onGrid, KEYS, 20, false).detail; + placed.cross_sections.find((s) => s.chainage_m === 40).culvert = { pipe: "p9" }; + const gone = stationsAt(45); + out.pipeGone = { + placedKind: placed.cross_sections.find((s) => s.chainage_m === 40).kind, + same: m.sameStationList(placed.cross_sections, keysOf(onGrid), gone), + sameUntouched: m.sameStationList(canonDetail().cross_sections, KEYS, gone), + }; + const after = m.swapCrossSections(placed, gone, keysOf(onGrid), 20, false).detail; + const at40 = after.cross_sections.find((s) => s.chainage_m === 40); + out.pipeGone.culvertLeft = "culvert" in at40; + out.pipeGone.kind = at40.kind; + out.pipeGone.canon = report(after)["40.00"].same; +} + /* 2. 가게 길(swapSectionStations) — 가게 routeDraft 로 가름: 없음 · 있음 · 초안 버린 직후 */ const seed = (P, R) => { m.writeState("section-detail", canonDetail(), P, R); diff --git a/resources/tester/helper_272_swap_canon_entry.ts b/resources/tester/helper_272_swap_canon_entry.ts index f5cc450e5..b9cd58c81 100644 --- a/resources/tester/helper_272_swap_canon_entry.ts +++ b/resources/tester/helper_272_swap_canon_entry.ts @@ -1,5 +1,8 @@ /* 467 시험 묶음 진입점 — 갈아끼움 정본 표본 유지(진짜 모듈 · vite ssr). */ -export { swapCrossSections } from "../../B06_Section/logic/B06_Section_Station_Swap"; +export { + sameStationList, + swapCrossSections, +} from "../../B06_Section/logic/B06_Section_Station_Swap"; export { loadSectionDetail, swapSectionStations, diff --git a/resources/tester/test_272_berm_source.py b/resources/tester/test_272_berm_source.py index aff987337..380e3368d 100644 --- a/resources/tester/test_272_berm_source.py +++ b/resources/tester/test_272_berm_source.py @@ -198,3 +198,8 @@ def test_쓰는_자리_하나(): assert "writeBermSpans" not in sync and "useStructureIds(projectId, structures)" in sync lane = (ROOT / "A00_Common" / "design" / "design_facility_lane.ts").read_text(encoding="utf-8") assert "writeBermSpans(projectId, routeId, next)" in lane + + +def test_시설_줄_종류_표_전_측점_안_칠함(out): + assert out["paint"]["before"] == 0 # 종류 표 전 = 빈 측점을 종단도에 안 씀 + assert out["paint"]["after"] == 1 # 받은 뒤 한 번 diff --git a/resources/tester/test_272_swap_keeps_canon.py b/resources/tester/test_272_swap_keeps_canon.py index 32e6204e4..e52eca43a 100644 --- a/resources/tester/test_272_swap_keeps_canon.py +++ b/resources/tester/test_272_swap_keeps_canon.py @@ -84,7 +84,9 @@ def test_되돌림_정본과_바이트_같음(out): def test_격자에_구조물_놓고_뺌_같은_자리_정본(out): g = out["gridStructure"] - assert g["placed"]["same"] and not g["placed"]["browser"] # 열쇠 바뀜(구조물) · 같은 자리 = 정본 + assert ( + g["placed"]["same"] and not g["placed"]["browser"] + ) # 열쇠 바뀜(구조물) · 같은 자리 = 정본 assert g["removed"]["same"] and not g["removed"]["browser"] # 다시 격자 = 정본 표본 · 기하 assert g["removedBytes"] @@ -112,7 +114,22 @@ def test_가게_길_초안_있음_버린_직후(out): for at in STRAIGHT: # 초안 차례 = 옛 규칙(기하 새 틀 · 표본 앞 값) assert not s["draft"][at]["same"] and not s["draft"][at]["browser"], at dropped = s["dropped"] - for at in CURVES: # 버린 뒤 = 저장 노선 자리 표본(초안 3 cm 자리 표본이 안 남음) - assert dropped[at]["browser"], at + for at in CURVES: # 버린 뒤 = 저장 정본 자리 · 표본(초안 3 cm 자리 표본이 안 남음 · 467 덧 정본 물러섬) + assert dropped[at]["same"] and not dropped[at]["browser"], at for at in STRAIGHT: # 초안이 안 옮긴 측점 = 앞 횡단 표본(서버) 그대로 assert not dropped[at]["browser"], at + + +def test_옮겼다_제자리_관_측점_정본(out): + p = out["pipeBack"] + assert p["pipe"]["same"] and not p["pipe"]["browser"] # 저장 정본 표본 · 기하 + assert p["bytes"] # 관 밖 횡단 = 정본 바이트(관 설계 = 옮김 규칙 · 다시 셈 입력은 정본 물러섬) + + +def test_일반_측점_자리_관_지움_다른_목록(out): + g = out["pipeGone"] + assert g["placedKind"] == "irregular" + assert g["same"] is False # 관 카드 → 일반 측점 = 갈아끼움 + assert g["sameUntouched"] is True # 일반 측점 그대로 = 같은 목록 + assert g["culvertLeft"] is False and g["kind"] == "regular" # 관 카드 안 남음 + assert g["canon"] # 그 자리 정본 표본 · 기하