fix(467 덧 셋): 제자리로 온 관 = 저장 정본 표본 · 일반 측점 자리 관 지움 = 갈아끼움 · 시설 줄 종류 표 전 측점 안 칠함

- swapCrossSections 같은 자리 원천 끝에 저장 정본(Section_Canon CanonRow 에 자리 · 표본 · Section_Store 가 savedDesigns 넘김) — 옮겼다 제자리로 온 관 · 버린 노선 초안 뒤 측점 = 정본 표본 · 기하
- sameStationList — 시설 측점(irregular) 자리가 일반 측점으로 돌아가면 다른 목록(관 지운 카드가 남던 흠)
- design_facility_lane paint — 종류 표 전 빈 측점을 종단도에 안 씀
- 시험 test_272_swap_keeps_canon · test_272_berm_source 에 셋(옛 코드 실패 확인)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 07:54:29 +09:00
co-authored by Claude Opus 5.5
parent e0ab0dce0f
commit dd70ce9b90
9 changed files with 144 additions and 20 deletions
+4 -2
View File
@@ -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[]);
}
+30 -5
View File
@@ -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<string, CanonRow[]>();
/** 묶음이 먼저 푼 세션 상세 — 상세 가게가 처음 읽을 때 가져감(다시 안 풂). */
@@ -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,
})),
);
}
+1
View File
@@ -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)
+30 -9
View File
@@ -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<string>,
stations: ReadonlyArray<PipelineStation>,
): 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<string>,
intervalM: number | null,
routeDraft = false,
saved: ReadonlyArray<Partial<CanonPlace>> = [],
): StationSwap {
const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section]));
const used = new Set<CrossSection>();
@@ -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<CanonPlace> | 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));
@@ -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);
@@ -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);
@@ -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,
+5
View File
@@ -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 # 받은 뒤 한 번
+20 -3
View File
@@ -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"] # 그 자리 정본 표본 · 기하