Files
Aislo/A00_Common/design/design_facility_lane.ts
T
eomsangdonandClaude Opus 5.5 df8d33f349 fix(473 새로고침 누가토량 흔들림): 시설 줄 입력(종류 표 · 구조물 · 관 알림)이 다 온 뒤만 측점 심음 — 그 전 종단도 = 저장 기준 · 서버 측점 그대로
- 원인: 관 알림 전 「모름」을 빈 목록으로 심어 종단도 선이 저장 관 측점(389) 꼭짓점을 뺌 → 첫 cross 가 380 · 389 · 400 을 그 선으로 셈 → 배지 −211.6 ㎥ 1 초 남짓 뒤 −204.2(새로고침 20 번 중 1 ~ 2)
- design_facility_lane known() · ready · design_profile planted · listedBase null = 저장 기준 · Render deriveProfile 서버 측점 그대로 · design_view_profile_sections stationsReady
- 시험 test_473_lane_ready(옛 코드 빨강) · test_346_2 가짜 다리에 stationsReady · 위키 rules 한 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:42:27 +09:00

262 lines
11 KiB
TypeScript

/* =============================================================================
* A00_Common/design/design_facility_lane.ts
* B05 · B06 한 페이지 **시설 줄 한 벌**(요구 271-5 · 462) — 구조물 + 관 → 알약 · 비정규 측점 · 흙덮기 관 ·
* 관 id 장부 맞춤 · 평면 마커 원천.
*
* 페이지가 세움 — 보기가 숨어도 · 아직 안 섰어도 늘 셈(순수 계산 · 서버 GET 없음). 종단도에 알약 · 측점 ·
* 흙덮기 관을 쓰는 자리는 여기 하나 · 보기는 읽기만(`marks` · `stations` · `pipes`) · 바뀜은 `onChange`.
* 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게)
* 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도
* 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d)
* 소단 사본(세션 `berm`) = 구조물 소단 구간 — 쓰는 자리 여기 하나(B05 만 열어도 · 같으면 안 씀 · 쓰면 cross 한 번 · 467)
* ========================================================================== */
import { useDesignStore } from "./design_store";
import { pageRouteId } from "./design_page_route";
import { readState } from "../b_page_state";
import {
bermSpansFromStructures,
writeBermSpans,
type BermSpan,
} from "../../B06_Section/B06_Section_UI_Page_Persist";
import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids";
import { plannedIrregular } from "./design_structure_status";
import { planStations } from "../../common_util/common_util_structure_station";
import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow";
import { followedStation } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
import { followSnapped } from "../../B05_Profile/B05_Profile_UI_Structures_Snap";
import {
pipesToStructureMarks,
type StructureInstance,
type StructureType,
} from "../../B05_Profile/B05_Profile_Api_Structures";
import type { IrregularStation } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
import type { MinCoverPipe } from "../../B05_Profile/B05_Profile_UI_Profile_MinCover";
/** 관 한 건 — 배수유역도 알림 · B06 관 목록 어느 쪽이든(`id` = 관 고정 id · 새 관은 없음). */
export interface LanePipe {
chainage_m: number;
id?: string | null;
facility: string;
options?: Record<string, string | number> | null;
start_m?: number | null;
end_m?: number | null;
}
/** 시설 줄이 쓰는 종단도 몫. */
export interface LaneProfile {
setStructureTypes: (types: StructureType[]) => void;
setStructures: (next: StructureInstance[]) => void;
setIrregularStations: (stations: IrregularStation[]) => void;
setMinCoverPipes: (pipes: ReadonlyArray<MinCoverPipe>) => void;
resetStationEdit: (chainageM: number, moveTo?: number) => void;
}
export interface FacilityLane {
/** 종류 표 · 저장분 구조물(보기가 받은 것 — 같은 페이지 자료 줄). */
setTypes: (types: StructureType[]) => void;
setStored: (structures: StructureInstance[]) => void;
/** 구조물 · 관 목록이 바뀜 — `carryEdits` = 사용자 고침(계획고 편집 옮김) · 불러옴은 false. */
set: (
next: { structures?: StructureInstance[]; pipes?: ReadonlyArray<LanePipe> },
carryEdits?: boolean,
) => void;
/** 노선을 놓아 계산 묶음이 가게 관 · 구조물을 다시 매김(되돌림 포함) — 편집 안 옮김. */
follow: () => void;
setIntervalM: (intervalM: number) => void;
/** [초기선 복원] — 관 투영 · 측점만 비움(관 정본은 배수유역 몫). */
clearProjected: () => void;
/** 다시 셈(편집 안 옮김) · 종단도에 다시 씀 — 종단도가 막 섰을 때 · 다시 그렸을 때(추가 측점이 늦게 옴). */
paint: () => void;
pipes: () => LanePipe[];
structures: () => StructureInstance[];
types: () => StructureType[];
/** 알약 = 구조물 + 관(가상 구조물 · 관 세션 id) — 평면 마커도 이것. */
marks: () => StructureInstance[];
stations: () => IrregularStation[];
/** 종류 표 · 구조물 · 관 알림을 다 받음 = 측점 계획이 섬(그 전 측점은 「모름」 · 473). */
ready: () => boolean;
intervalM: () => number;
onChange: (fn: () => void) => () => void;
dispose: () => void;
}
const run = (fn: () => void): void => {
try {
fn();
} catch (error) {
console.error("[facility_lane]", error);
}
};
export function createFacilityLane(
projectId: string,
profile: () => LaneProfile | null,
): FacilityLane {
const store = useDesignStore(projectId, null);
let types: StructureType[] = [];
let stored: StructureInstance[] | null = null;
let structures: StructureInstance[] = [];
let structuresKnown = false; // 구조물을 받기 전 = 소단 사본 안 씀(빈 목록으로 저장분 소단을 지우지 않게)
/** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */
let reported: LanePipe[] | null = null;
let pipes: LanePipe[] = [];
let sessionIds = new Map<string, string>(); // 관 고정 id → 세션 id(알약 id)
let stations: IrregularStation[] = [];
let intervalM = 20;
let painted: { to: LaneProfile; stations: string } | null = null;
const listeners = new Set<() => void>();
/** 화면 관 목록 = 알림을 가게 관 초안 측점으로(노선 따라 다시 매김 · 349-0d) · 관 세션 id 도 그 자리로 —
* 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 · 되돌려도 그 관) · id 없는 관(막 놓은 관)은 자리로. */
const shownPipes = (list: ReadonlyArray<LanePipe>): LanePipe[] => {
const shown = followPipeReports(
list.map((pipe) => ({ ...pipe, id: pipe.id ?? undefined })),
store.get("pipes"),
).sort((x, y) => x.chainage_m - y.chainage_m);
placePipeIds(
shown.flatMap((pipe) => {
const sid = pipe.id && sessionIds.get(pipe.id);
return sid ? [[sid, pipe.chainage_m] as const] : [];
}),
projectId,
);
sessionIds = new Map(
shown.flatMap((pipe) =>
pipe.id ? [[pipe.id, pipeIdAt(pipe.chainage_m, projectId)] as const] : [],
),
);
prunePipeIds(
shown.map((pipe) => pipe.chainage_m),
projectId,
); // 헛 id 정리(411-E)
return shown;
};
/** 옛 측점에 남은 계획고 편집(유령 변화점)을 옮기거나 지움 — 옮김 = 0.006m 안 새 측점 · 같은 id ·
* 사라진 것 하나와 새것 하나(옮긴 관 · 213-2) · 없으면 맞춘 자리의 새 측점(234-1 · 272) · 나머지는 지움. */
const carry = (next: IrregularStation[]): void => {
const to = profile();
if (!to) return;
const nextKeys = new Set(next.map((station) => station.chainage_m.toFixed(3)));
const oldKeys = new Set(stations.map((station) => station.chainage_m.toFixed(3)));
const gone = stations.filter((station) => !nextKeys.has(station.chainage_m.toFixed(3)));
const born = next.filter((station) => !oldKeys.has(station.chainage_m.toFixed(3)));
gone.forEach((station) => {
const same = followedStation(station, gone, born) ?? followSnapped(station, born);
to.resetStationEdit(station.chainage_m, same?.chainage_m);
});
};
const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)];
/** 소단 사본 = 지금 구조물의 소단 구간(467 · 사본 없는 동안 다시 셈 = 저장분 `design.berm`) · 같으면 안 씀 ·
* 쓰면 저장 노선 cross 한 번 더(같은 값 측점은 다시 셈 기억이 건너뜀 · 묶음은 늦게 불러옴 · 순환 없음). */
function syncBerm(): void {
const routeId = pageRouteId(projectId);
if (!structuresKnown || routeId === null) return;
const copy = readState<BermSpan[]>("berm", projectId, routeId);
const next = bermSpansFromStructures(structures);
if (Array.isArray(copy) && JSON.stringify(copy) === JSON.stringify(next)) return;
writeBermSpans(projectId, routeId, next);
void Promise.all([import("./design_pipeline_cross"), import("./design_pipeline_page")])
.then(([cross, page]) => {
cross.forceCross();
page.pipelineCross(projectId);
})
.catch(() => undefined);
}
/** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점
* (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */
const known = (): boolean => types.length > 0 && reported !== null && structuresKnown;
function paint(force = false): void {
const to = profile();
if (!to) return;
to.setStructureTypes([...types]);
to.setStructures(marks());
const signature = JSON.stringify(stations);
// 입력이 다 오기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467 · 473) ·
// 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림
if (known() && (force || painted?.to !== to || painted.stations !== signature)) {
to.setIrregularStations(stations);
painted = { to, stations: signature };
}
to.setMinCoverPipes(pipes as MinCoverPipe[]);
}
/** 다시 셈 — 종류 표를 받기 전이면 측점은 미룸(받으면 다시 맞춤). */
function refresh(carryEdits: boolean, force = false): void {
pipes = reported ? shownPipes(reported) : [];
if (known()) {
const next = plannedIrregular(
planStations(withPipeIds(pipes, projectId), structures, types),
intervalM,
);
if (carryEdits) carry(next);
stations = next;
}
paint(force);
listeners.forEach(run);
}
// 추가 측점(368-2) — 초안 · 저장분이 바뀌면 측점 계획을 다시 맞춤
const stopPlus = store.subscribe("plusStations", () => refresh(true));
return {
setTypes(next) {
types = next;
refresh(false);
},
setStored(next) {
stored = next;
},
set(next, carryEdits = true) {
if (next.structures) {
structures = next.structures;
structuresKnown = true;
}
if (next.pipes) reported = [...next.pipes];
refresh(carryEdits);
if (next.structures) run(syncBerm);
},
follow() {
const drafted = store.get("structures");
structures = drafted ?? stored ?? [];
structuresKnown ||= drafted !== null || stored !== null;
refresh(false);
run(syncBerm);
},
setIntervalM(next) {
if (next > 0 && next !== intervalM) {
intervalM = next;
refresh(false);
}
},
clearProjected() {
reported = [];
pipes = [];
stations = [];
paint(true);
listeners.forEach(run);
},
paint: () => refresh(false, true),
pipes: () => pipes,
structures: () => structures,
types: () => types,
marks,
stations: () => stations,
ready: known,
intervalM: () => intervalM,
onChange(fn) {
listeners.add(fn);
return () => void listeners.delete(fn);
},
dispose() {
stopPlus();
listeners.clear();
},
};
}