- 원인: 관 알림 전 「모름」을 빈 목록으로 심어 종단도 선이 저장 관 측점(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>
262 lines
11 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|