Files
Aislo/A00_Common/design/design_facility_lane.ts
T
eomsangdonandClaude Opus 5.5 e90b546ed9 fix(462 B05 B06 한 페이지): 시설 줄 페이지 한 벌 — 알약 · 비정규 측점 · 흙덮기 관 · 관 id 장부를 숨은 보기와 무관하게 · 관 옵션 넘김 세션 칸
- design_facility_lane 새 파일 — 페이지가 세움 · 관 알림 · B06 관 목록 · 구조물 · 종류 · 추가 측점 → 종단도 · 평면 마커 원천 한 곳
- B05 다리 = 목록 · 종류 · 저장만 · B06 종단도 = 읽기만 · 숨었다 보일 때 다시 셈 없음(편집 두 번 옮김 길 걷힘)
- 노선 따라감 = 시설 줄 follow(편집 안 옮김) · 사용자 고침만 계획고 편집 옮김
- pipeOptionQueue(메모리) → 세션 칸 pipe-option-queue
- 시험 test_271_facility_lane(R3 뒤집기 · B06 먼저 = B05 먼저 · 따라감) · 옮긴 자리 문자열 시험 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:05:23 +09:00

220 lines
9.0 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)
* ========================================================================== */
import { useDesignStore } from "./design_store";
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[];
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[] = [];
let structures: StructureInstance[] = [];
/** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */
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)];
function paint(force = false): void {
const to = profile();
if (!to) return;
to.setStructureTypes([...types]);
to.setStructures(marks());
const signature = JSON.stringify(stations);
// 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림
if (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 (types.length) {
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;
if (next.pipes) reported = [...next.pipes];
refresh(carryEdits);
},
follow() {
structures = store.get("structures") ?? stored;
refresh(false);
},
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,
intervalM: () => intervalM,
onChange(fn) {
listeners.add(fn);
return () => void listeners.delete(fn);
},
dispose() {
stopPlus();
listeners.clear();
},
};
}