Files
Aislo/B06_Section/logic/B06_Section_Station_Swap.ts
T
eomsangdonandClaude Opus 5.5 281c71b55b refactor(logic): 348-1 순수 계산 TS 49 파일 <페이지>/logic/ 로 옮김 — 이름 앞머리 그대로 · 동작 고침 없음
- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로
- 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판)
- import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/
- 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob
- design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄)
- 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 13:44:50 +09:00

183 lines
7.6 KiB
TypeScript

/* =============================================================================
* B06_Section_Station_Swap.ts
* 측점 수가 바뀔 때 횡단 목록 갈아끼움(PLAN 346-2 4파) — 계산 묶음 측점(`PipelineStation[]`) → 새 상세 객체.
*
* · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 ·
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
*
* ⚠ 파이썬·TS 짝 — `B05_Profile_Engine_Sections_Core.py` `format_station` ↔ `formatStation`.
* 거울 시험 `resources/tester/test_346_2_station_swap.py`.
* 순수 계산(DOM · fetch · 세션 없음).
* ========================================================================== */
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
interface Sample {
offset_m: number;
elevation_m: number | null;
valid: boolean;
}
/** 지반 표본(중심 → 바깥 · 좌 +) → 엔진 표본(오프셋 오름차순 · 우 −). */
export function groundSamples(station: PipelineStation): Sample[] {
const ground = station.ground;
if (!ground) return [];
const point = (offset: number, z: number | null): Sample => ({
offset_m: offset,
elevation_m: z,
valid: z !== null && Number.isFinite(z),
});
const right = ground.right.map((s) => point(-s.offsetM, s.z));
const left = ground.left.map((s) => point(s.offsetM, s.z));
const out = [...right, ...left];
if (ground.centerZ !== null && !out.some((s) => Math.abs(s.offset_m) < 1e-9))
out.push(point(0, ground.centerZ));
return out.sort((a, b) => a.offset_m - b.offset_m);
}
/** 짝: `format_station` — `STA.k+mmm.mmm`. */
export function formatStation(chainageM: number): string {
const at = Math.max(chainageM, 0);
const km = Math.floor(at / 1000);
const rest = (at - km * 1000).toFixed(3).padStart(7, "0");
return `STA.${km}+${rest}`;
}
/** 서버 측점 id 꼴(`station_000000484210` = 누가거리 mm 12자리). */
export const stationIdOf = (chainageM: number): string =>
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움. */
const freshDesign = () =>
({
design_elevation_m: 0,
ground_type: "ripping_rock",
section_mode: "left_cut",
ditch_type: "standard",
}) as unknown as NonNullable<CrossSection["design"]>;
function kindOf(station: PipelineStation): SectionStation["kind"] {
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
return station.kind === "grid" ? "regular" : "irregular";
}
/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음). */
export function sameStationList(
sections: ReadonlyArray<{ chainage_m: number }>,
oldKeys: ReadonlyArray<string>,
stations: ReadonlyArray<PipelineStation>,
): boolean {
return (
sections.length === stations.length &&
stations.every(
(station, index) =>
Math.abs(station.chainageM - sections[index].chainage_m) < 1e-6 &&
(oldKeys[index] === station.key || kindOf(station) === "regular"),
)
);
}
/** 조작값 맵 열쇠(`85.05` · `85.05:outlet` · `b85.05`)의 누가거리를 새 자리로 — 안 옮기는 열쇠는 그대로. */
export function renameChainageKeys<V>(
values: ReadonlyMap<string, V>,
moves: ReadonlyMap<string, string>,
): Map<string, V> {
const out = new Map<string, V>();
for (const [key, value] of values) {
const match = /^(b?)(-?\d+\.\d{2})(:.*)?$/.exec(key);
const to = match && moves.get(match[2]);
out.set(to ? `${match[1]}${to}${match[3] ?? ""}` : key, value);
}
return out;
}
export interface StationSwap {
detail: SectionDetailResponse;
added: string[];
removed: string[];
/** 이어 받은 측점 중 누가거리가 바뀐 것 — 옛 0.01 m 글 → 새 글(조작값 맵 옮김). */
moves: Map<string, string>;
/** 이어 받은 측점 중 좌표가 0.01 m 넘게 움직인 것이 있나 — 노선이 바뀐 차례(「노선 밖 고침」 셈). */
moved: boolean;
}
/**
* 새 측점 목록으로 상세를 새로 짬. `oldKeys` = 지금 상세 측점마다 열쇠(같은 차례 · `stationKeysOf`) ·
* 열쇠가 안 맞으면 같은 자리(0.01 m) · 같은 종류의 앞 횡단을 씀.
*/
export function swapCrossSections(
detail: SectionDetailResponse,
stations: ReadonlyArray<PipelineStation>,
oldKeys: ReadonlyArray<string>,
intervalM: number | null,
): StationSwap {
const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section]));
const used = new Set<CrossSection>();
let moved = false;
const added: string[] = [];
const moves = new Map<string, string>();
const text = (chainageM: number) => chainageM.toFixed(2);
const priorOf = (station: PipelineStation): CrossSection | undefined => {
const hit = byKey.get(station.key);
if (hit && !used.has(hit)) return hit;
return detail.cross_sections.find(
(section) =>
!used.has(section) &&
section.kind === kindOf(station) &&
Math.abs(section.chainage_m - station.chainageM) < 0.01,
);
};
const longStations: SectionStation[] = [];
const cross_sections = stations.map((station): CrossSection => {
const prior = priorOf(station);
if (prior) used.add(prior);
else added.push(station.key);
if (prior && Math.hypot(prior.center_x - station.x, prior.center_y - station.y) > 0.01)
moved = true;
if (prior && text(prior.chainage_m) !== text(station.chainageM))
moves.set(text(prior.chainage_m), text(station.chainageM));
const kind = kindOf(station);
const onGrid =
kind === "irregular" &&
!!intervalM &&
Math.abs(station.chainageM / intervalM - Math.round(station.chainageM / intervalM)) < 1e-6;
const place = {
station_id: prior?.station_id ?? stationIdOf(station.chainageM),
chainage_m: station.chainageM,
label: formatStation(station.chainageM),
kind,
...(onGrid ? { grid: true } : {}),
center_z: station.ground?.centerZ ?? prior?.center_z ?? null,
azimuth_deg: station.azimuthDeg,
plan_radius_m: station.planRadiusM,
curve_outer_side: station.curveOuterSide,
curve_widening_m: station.curveWideningM,
center_x: station.x,
center_y: station.y,
frame: { left_xy: [station.left[0], station.left[1]] as [number, number] },
};
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []);
return prior ? { ...prior, ...place, samples } : { ...place, samples, design: freshDesign() };
});
const removed = detail.cross_sections
.map((section, i) => (used.has(section) ? null : oldKeys[i]))
.filter((key): key is string => key !== null);
const last = stations[stations.length - 1]?.chainageM ?? detail.longitudinal.length_m;
return {
detail: {
...detail,
longitudinal: { ...detail.longitudinal, stations: longStations, length_m: last },
cross_sections,
},
added,
removed,
moved,
moves,
};
}