- 고리 끊음: 묶음 측점 → 상세 갈아끼움 → 종단도 render · 비정규 측점 · 유토곡선 설정이 묶음을 처음부터 다시 부르던 것 — 다시 시작은 저장 정본(새로 받은 상세 · 새 노선)이 바뀔 때만 · 횡단 입력도 정본 상세에서만 모음 - 열 때(start 차례)는 측점 · 종단을 가게에 안 냄 — 상세 갈아끼움 없음 · 유토곡선 = 저장값 - 저장 노선(열 때 · 초안 버림)은 관 · 구조물 누가거리 · 계획고를 저장값 그대로(다시 투영 안 함 · 되돌리기 뒤 계획고 한 번 더 셈 없음) - 노선 초안 동안 종단도 render · 비정규 측점이 초안 그림(기준 · 미리보기 가게)을 덮지 않음 - 시험 test_349_pipeline_once 4건(검증_sub7 실제 노선 · 옛 코드에서 4건 실패 재현) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_Pipeline_Grade.ts
|
|
* 계산 묶음(PLAN 346-1) **follow · planline · profile 단계** + 바뀐 범위 — 계약 =
|
|
* `A00_Common/design/logic/design_pipeline_types.ts`. 셈은 이미 있는 짝을 부르기만(어댑터):
|
|
*
|
|
* follow — 관 · 구조물 좌표 따라감(`common_util_route_follow`) · 계획고 편집 옮김(`Grade_Carry`
|
|
* `carryProfileEdits` 의 edits 몫 · 옛 노선 `before.route` 가 있을 때만).
|
|
* planline — 노선 초안이 없으면 저장 종단 기준 그대로 · 있으면 새 지반 첫 계획선(`Grade_Ground`)으로
|
|
* 바뀐 구간만 다시 맞추고 밖은 저장 꼭짓점을 밂(`carryProfileEdits` 의 basePvi 몫).
|
|
* profile — `buildAlignment`(첫 계획선 + 계획고 편집) — 종단 보기가 쓰는 같은 함수.
|
|
*
|
|
* 화면 · fetch · 세션 없음(순수) — 저장 기준 · 저장 노선은 부르는 쪽이 넣음.
|
|
* ========================================================================== */
|
|
|
|
import type {
|
|
FollowPatch,
|
|
RangePart,
|
|
StageFn,
|
|
Vertex,
|
|
} from "../../A00_Common/design/logic/design_pipeline_types";
|
|
import {
|
|
followStructures,
|
|
projectPipePoints,
|
|
type FollowingPoint,
|
|
type FollowingStructure,
|
|
} from "../../common_util/common_util_route_follow";
|
|
import {
|
|
buildAlignment,
|
|
emptyEdits,
|
|
type AlignmentBase,
|
|
type AlignmentEdits,
|
|
} from "./B05_Profile_UI_Profile_Alignment";
|
|
import { carryProfileEdits, changedSpan } from "./B05_Profile_Grade_Carry";
|
|
import { groundFollowingBasePvi, groundProfile } from "./B05_Profile_Grade_Ground";
|
|
|
|
const xy = (line: ReadonlyArray<Vertex>): Array<{ x: number; y: number }> =>
|
|
line.map(([x, y]) => ({ x, y }));
|
|
|
|
/** 서버 측점 id 꼴(`generate_sections`) — 종단 표 · 고르기가 이 id 로 측점을 짚음. */
|
|
export const stationIdOf = (chainageM: number): string =>
|
|
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
|
|
|
|
/** 노선 바뀐 범위 — 처음 바뀐 접선점부터 끝(같으면 전 구간). */
|
|
export function routeRange(
|
|
before: ReadonlyArray<Vertex> | null,
|
|
after: ReadonlyArray<Vertex> | null,
|
|
): RangePart {
|
|
const span = before && after ? changedSpan(xy(before), xy(after)) : null;
|
|
return { fromM: span ? span.start_m : 0, toM: Infinity };
|
|
}
|
|
|
|
/** 계획고 바뀐 범위 — 바뀐 편집 키 앞뒤 이웃 꼭짓점 사이(`nodes` = 계획선 꼭짓점 누가거리). 같으면 null. */
|
|
export function gradeRange(
|
|
before: AlignmentEdits | null,
|
|
after: AlignmentEdits | null,
|
|
nodes: ReadonlyArray<number>,
|
|
): RangePart | null {
|
|
const changed: number[] = [];
|
|
for (const field of ["station_offsets", "curve_radii"] as const) {
|
|
const a = before?.[field] ?? {};
|
|
const b = after?.[field] ?? {};
|
|
for (const key of new Set([...Object.keys(a), ...Object.keys(b)]))
|
|
if (a[key] !== b[key]) changed.push(Number(key));
|
|
}
|
|
if (!changed.length) return null;
|
|
const low = Math.min(...changed);
|
|
const high = Math.max(...changed);
|
|
const ahead = nodes.filter((node) => node > high + 1e-6);
|
|
return {
|
|
fromM: Math.max(0, ...nodes.filter((node) => node < low - 1e-6)),
|
|
toM: ahead.length ? Math.min(...ahead) : Infinity,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* follow — 노선이 바뀐 차례에 한 번 · 관 · 구조물 좌표 따라감 · 계획고 편집 옮김. 첫 계산(옛 노선 없음)은
|
|
* 저장 노선(`savedLine`)을 옛 노선으로 보고 좌표만 따라감(투영은 몇 번 해도 같음 · 계획고 옮김은 안 함 —
|
|
* 열 때의 계획고 초안은 이미 그 노선 기준). 노선 초안이 없으면(열 때 · 초안 버림) `saved` 를 그대로 —
|
|
* 저장 노선의 관 · 구조물 누가거리 · 계획고는 정본이라 다시 투영하지 않음(투영 오차 = 측점 목록 어긋남).
|
|
*/
|
|
export function createFollowStage(deps: {
|
|
savedLine(): Vertex[] | null;
|
|
saved?(): FollowPatch;
|
|
}): StageFn<"follow"> {
|
|
return (ctx) => {
|
|
if (!ctx.inputs.route && deps.saved) return deps.saved();
|
|
const before = ctx.before.route?.line ?? deps.savedLine();
|
|
const now = ctx.out.route;
|
|
if (!before || !now) return {};
|
|
const line = xy(now.line);
|
|
const patch: FollowPatch = {};
|
|
const { pipes, structures, alignment } = ctx.inputs;
|
|
if (Array.isArray(pipes) && pipes.length)
|
|
patch.pipes = projectPipePoints(pipes as FollowingPoint[], line);
|
|
if (Array.isArray(structures) && structures.length)
|
|
patch.structures = followStructures(structures as FollowingStructure[], line).map(
|
|
(moved) => moved.item,
|
|
);
|
|
const span = ctx.before.route ? changedSpan(xy(before), line) : null;
|
|
if (span && alignment) patch.alignment = carryProfileEdits([], alignment, span, []).edits;
|
|
return patch;
|
|
};
|
|
}
|
|
|
|
export const followStage = createFollowStage({ savedLine: () => null });
|
|
|
|
export interface PlanlineDeps {
|
|
/** 저장 노선의 종단 기준(측점 한 벌 · 종단 보기 `listed`) — 없으면 아직 못 셈. */
|
|
savedBase(): AlignmentBase | null;
|
|
/** 저장 노선(그린 선) — 바뀐 구간을 가름. */
|
|
savedLine(): Vertex[] | null;
|
|
}
|
|
|
|
export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
|
return (ctx) => {
|
|
const saved = deps.savedBase();
|
|
if (!saved) throw new Error("저장 종단 기준이 아직 없습니다.");
|
|
const ground = ctx.out.ground;
|
|
const route = ctx.out.route;
|
|
if (!ctx.inputs.route || !ground || !route) return saved;
|
|
const { chainageM, z } = ground.longitudinal;
|
|
const samples = chainageM.map((chainage, index) => ({
|
|
chainage_m: chainage,
|
|
elevation_m: z[index],
|
|
}));
|
|
const profile = groundProfile(samples);
|
|
if (!profile) return saved; // 못 서는 땅 — 저장 기준 그대로(서버가 [저장] 때 다시 셈)
|
|
const groundPvi =
|
|
groundFollowingBasePvi({
|
|
samples,
|
|
stations: ground.stations.map((station) => station.chainageM),
|
|
policy: saved.policy,
|
|
}) ?? [];
|
|
const savedLine = deps.savedLine();
|
|
const span = savedLine ? changedSpan(xy(savedLine), xy(route.line)) : null;
|
|
return {
|
|
policy: saved.policy,
|
|
onlyExplicitCurves: saved.onlyExplicitCurves,
|
|
basePvi: span
|
|
? carryProfileEdits(saved.basePvi, null, span, groundPvi).basePvi
|
|
: saved.basePvi,
|
|
chainage: profile.chainage,
|
|
ground: profile.ground,
|
|
stations: ground.stations.map((station) => ({
|
|
station_id: stationIdOf(station.chainageM),
|
|
chainage_m: station.chainageM,
|
|
})),
|
|
};
|
|
};
|
|
}
|
|
|
|
/** profile — 첫 계획선 + 계획고 편집 → 종단(종단 보기와 같은 `buildAlignment`). */
|
|
export const profileStage: StageFn<"profile"> = (ctx) => {
|
|
const base = ctx.out.planline;
|
|
if (!base) throw new Error("첫 계획선이 없습니다.");
|
|
return buildAlignment(base, ctx.inputs.alignment ?? emptyEdits());
|
|
};
|