Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -259,7 +259,9 @@ export async function mountProfileView(
|
||||
panel: () => panel,
|
||||
profilePanel: () => profilePanel,
|
||||
bridge: () => bridge,
|
||||
visible: () => host.visible(), // 숨은 동안 갈아끼운 상세(346-2)는 다시 보일 때 그림
|
||||
});
|
||||
host.onDispose(sections.stopSwap);
|
||||
const { restorePanel, renderLatest, restoreSections } = sections;
|
||||
const view3d = createProfile3d({
|
||||
projectId: activeProjectId,
|
||||
@@ -390,6 +392,7 @@ export async function mountProfileView(
|
||||
ready: () => !state.restoring,
|
||||
// 다시 보이면 횡단 보기가 한 벌(폼 · 종단도)에 실어 둔 값을 종단 보기 값으로 되돌린다.
|
||||
reload: async () => {
|
||||
sections.flushSwap(); // 숨은 동안 측점 목록이 바뀌었으면 종단도 · 3D 측점선 다시(346-2)
|
||||
await bridge.load();
|
||||
if (lastPipes) applyPipes(lastPipes);
|
||||
},
|
||||
|
||||
@@ -13,7 +13,12 @@ import type { createRouteProfilePanel } from "../../B05_Profile/B05_Profile_UI_P
|
||||
import type { createStructuresBridge } from "../../B05_Profile/B05_Profile_UI_Page_Structures";
|
||||
import type { RouteViewer } from "../../B05_Profile/B05_Profile_UI_Viewer";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import { loadSectionDetail } from "../../B06_Section/B06_Section_Section_Store";
|
||||
import {
|
||||
loadSectionDetail,
|
||||
subscribeSectionDetail,
|
||||
swapSectionStations,
|
||||
} from "../../B06_Section/B06_Section_Section_Store";
|
||||
import { useDesignStore } from "./design_store";
|
||||
import { migrateLegacyStations } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { loadCorridorIfFresh } from "../../B05_Profile/B05_Profile_UI_Corridor";
|
||||
import { stationLinesOf } from "../../B05_Profile/B05_Profile_UI_Page_Stations";
|
||||
@@ -42,6 +47,8 @@ export interface ProfileSectionsDeps {
|
||||
panel: () => RoutePanel;
|
||||
profilePanel: () => ReturnType<typeof createRouteProfilePanel>;
|
||||
bridge: () => ReturnType<typeof createStructuresBridge>;
|
||||
/** 종단 보기가 보이나 — 숨은 동안 갈아끼운 상세는 다시 보일 때 그림(346-2 · 없으면 늘 보임). */
|
||||
visible?: () => boolean;
|
||||
}
|
||||
|
||||
/** 구 구조물 측점 이관을 이미 시도한 노선 — 탭 수명 동안 유지한다. */
|
||||
@@ -114,6 +121,7 @@ export function createProfileSections(deps: ProfileSectionsDeps) {
|
||||
return;
|
||||
}
|
||||
profilePanel.setLoading(null);
|
||||
followSwap(routeId);
|
||||
try {
|
||||
renderSections(detail, routeId);
|
||||
// 구 확정분에 남은 비정규 측점(자유 텍스트)을 구조물 정본으로 1회 이관한다 — 멱등이지만
|
||||
@@ -154,6 +162,38 @@ export function createProfileSections(deps: ProfileSectionsDeps) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 측점 수가 바뀜(346-2 · 묶음 측점 → 횡단 쪽이 상세를 갈아끼움) — 종단도 · 표 · 3D 측점선을 새 목록으로
|
||||
* 다시(보일 때만 · 숨었으면 다시 보일 때 `flushSwap`). 노선마다 한 번 듣고, 노선이 바뀌면 옛 것을 끊음. */
|
||||
let swap: { routeId: number; stop: () => void } | null = null;
|
||||
let swapPending = false;
|
||||
function followSwap(routeId: number): void {
|
||||
if (swap?.routeId === routeId) return;
|
||||
swap?.stop();
|
||||
const stopListen = subscribeSectionDetail(projectId, routeId, (next) => {
|
||||
state.detail = next;
|
||||
swapPending = !(deps.visible?.() ?? true);
|
||||
if (!swapPending) renderSections(next, routeId);
|
||||
});
|
||||
// 묶음 측점(가게)이 바뀌면 갈아끼움 — 횡단 보기를 안 열었어도(그쪽도 부르면 둘째는 「같은 목록」)
|
||||
const stopStore = useDesignStore(projectId, null).subscribe("stations", (stations) => {
|
||||
if (stations?.length) void swapSectionStations(projectId, routeId, stations);
|
||||
});
|
||||
swap = {
|
||||
routeId,
|
||||
stop: () => {
|
||||
stopListen();
|
||||
stopStore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 숨은 동안 받은 새 상세를 그림 — 종단 보기가 다시 보일 때. */
|
||||
function flushSwap(): void {
|
||||
if (!swapPending || !state.detail || !swap) return;
|
||||
swapPending = false;
|
||||
renderSections(state.detail, swap.routeId);
|
||||
}
|
||||
|
||||
/** 「3D 늦음」 — [3D 업데이트] 표시(setCorridorPending 그대로)와 상태 한 칸을 같이 맞춘다. */
|
||||
function setCorridorPending(pending: boolean): void {
|
||||
state.corridorPending = pending;
|
||||
@@ -184,5 +224,7 @@ export function createProfileSections(deps: ProfileSectionsDeps) {
|
||||
restoreSections,
|
||||
renderLatest,
|
||||
setCorridorPending,
|
||||
flushSwap,
|
||||
stopSwap: () => swap?.stop(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ import {
|
||||
useStationKeyList,
|
||||
} from "./B06_Section_Station_Keys";
|
||||
import { sameStationList, swapCrossSections } from "./logic/B06_Section_Station_Swap";
|
||||
import { emitSwap, type DetailListener } from "./B06_Section_Swap_Bus";
|
||||
|
||||
export { onAnySwap } from "./B06_Section_Swap_Bus";
|
||||
|
||||
const cache = new Map<string, SectionDetailResponse>();
|
||||
const pending = new Map<string, Promise<SectionDetailResponse>>();
|
||||
@@ -111,8 +114,6 @@ export async function loadSectionDetail(
|
||||
return request;
|
||||
}
|
||||
|
||||
/** 새 상세 · 이어 받은 측점의 누가거리 옮김(옛 0.01 m 글 → 새 글 · 조작값 맵을 옮길 때). */
|
||||
type DetailListener = (detail: SectionDetailResponse, moves: ReadonlyMap<string, string>) => void;
|
||||
const listeners = new Map<string, Set<DetailListener>>();
|
||||
|
||||
/** 상세 객체가 갈아끼워짐(측점 수가 바뀜 · 346-2 4파) — 쥔 옛 객체를 버리고 새것으로 다시 그릴 것. */
|
||||
@@ -154,6 +155,7 @@ export async function swapSectionStations(
|
||||
await withDraftShapes(swap.detail, projectId); // 새 측점에도 구조물 그림 자료(323-4)
|
||||
withStructureAreas(swap.detail);
|
||||
if (cache.get(key) !== swap.detail) return null; // 그 사이 다른 갈아끼움 · 새로 받음
|
||||
emitSwap(swap.detail, swap.moves); // 세션 값 옮김 · 다시 그림(어느 보기든)
|
||||
listeners.get(key)?.forEach((fn) => fn(swap.detail, swap.moves));
|
||||
return { detail: swap.detail, moves: swap.moves };
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { chainageKey, looseChainage, stationKey } from "@util/common_util_station_key";
|
||||
import {
|
||||
chainageKey,
|
||||
dedupeStationKeys,
|
||||
looseChainage,
|
||||
stationKey,
|
||||
type StationKeyRole,
|
||||
} from "@util/common_util_station_key";
|
||||
import { inferStationInterval } from "@util/common_util_svg";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
@@ -46,7 +52,8 @@ type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
||||
|
||||
const lists = new Map<string, Keyed[]>();
|
||||
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
|
||||
const owners = new Map<string, Array<{ id: string; kind: "pipe" | "structure"; at: number }>>();
|
||||
type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
||||
const owners = new Map<string, Owner[]>();
|
||||
let current: { projectId: string; routeId: number } | null = null;
|
||||
|
||||
const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${routeId}`;
|
||||
@@ -54,7 +61,7 @@ const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${
|
||||
/** 측점 계획(초안 · `planStations`)이 낸 덮는 구조물 id — 관 세션 id 는 `pipe-` 머리. */
|
||||
export function useStationOwners(
|
||||
projectId: string,
|
||||
planned: ReadonlyArray<{ chainage_m: number; owners: string[] }>,
|
||||
planned: ReadonlyArray<{ chainage_m: number; owners: string[]; role?: StationKeyRole }>,
|
||||
): void {
|
||||
owners.set(
|
||||
projectId,
|
||||
@@ -63,6 +70,7 @@ export function useStationOwners(
|
||||
id,
|
||||
kind: id.startsWith("pipe-") ? ("pipe" as const) : ("structure" as const),
|
||||
at: station.chainage_m,
|
||||
role: station.role ?? "anchor",
|
||||
})),
|
||||
),
|
||||
);
|
||||
@@ -71,12 +79,25 @@ export function useStationOwners(
|
||||
/** 구조물 목록(횡단 보기) — 한 자리 구조물의 id. 관 자리는 관 장부가 정함. */
|
||||
export function useStructureIds(
|
||||
projectId: string,
|
||||
structures: ReadonlyArray<{ structure_id?: string | null; chainage_m?: number | null }>,
|
||||
structures: ReadonlyArray<{
|
||||
structure_id?: string | null;
|
||||
chainage_m?: number | null;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
}>,
|
||||
): void {
|
||||
const known = (owners.get(projectId) ?? []).filter((owner) => owner.kind === "pipe");
|
||||
for (const item of structures) {
|
||||
if (item.structure_id && typeof item.chainage_m === "number")
|
||||
known.push({ id: item.structure_id, kind: "structure", at: item.chainage_m });
|
||||
const id = item.structure_id;
|
||||
if (!id) continue;
|
||||
// 기준 측점 = structure:<id> · 구간형 시작 · 끝 = :start · :end(열쇠 한 벌 · common_util_station_key)
|
||||
const marks: Array<[StationKeyRole, number | null | undefined]> = [
|
||||
["anchor", item.chainage_m],
|
||||
["start", item.start_m],
|
||||
["end", item.end_m],
|
||||
];
|
||||
for (const [role, at] of marks)
|
||||
if (typeof at === "number") known.push({ id, kind: "structure", at, role });
|
||||
}
|
||||
owners.set(projectId, known);
|
||||
}
|
||||
@@ -84,20 +105,19 @@ export function useStructureIds(
|
||||
const looksLikePipe = (section: CrossSection): boolean =>
|
||||
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
||||
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:`). */
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). */
|
||||
export function stationKeysOf(
|
||||
projectId: string | null,
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
intervalM: number | null,
|
||||
): Keyed[] {
|
||||
const known = projectId ? (owners.get(projectId) ?? []) : [];
|
||||
const seen = new Set<string>();
|
||||
return sections.map((section) => {
|
||||
const keys = sections.map((section) => {
|
||||
const at = section.chainage_m;
|
||||
let key: string;
|
||||
if (section.kind === "irregular") {
|
||||
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
||||
else if (looksLikePipe(section))
|
||||
key = stationKey("pipe", at, intervalM, pipeIdAt(at, projectId));
|
||||
else
|
||||
@@ -105,10 +125,10 @@ export function stationKeysOf(
|
||||
? stationKey("regular", at, intervalM)
|
||||
: stationKey("irregular", at, intervalM);
|
||||
} else key = stationKey(section.kind, at, intervalM);
|
||||
if (seen.has(key)) key = `at:${chainageKey(at)}`;
|
||||
seen.add(key);
|
||||
return { key, chainageM: at };
|
||||
return key;
|
||||
});
|
||||
const chainages = sections.map((section) => section.chainage_m);
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
||||
}
|
||||
|
||||
type KeyedDetail = Pick<SectionDetailResponse, "cross_sections" | "longitudinal">;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Swap_Bus.ts
|
||||
* 상세 갈아끼움 알림판(PLAN 346-2) — 측점 수가 바뀌어 `Section_Store` 가 상세 객체를 갈아끼우면 여기로 알림.
|
||||
* 가져오는 것 없음 — 세션 조작값 맵(`_UI_Page_Link_Session`) · 암 경계 저장소(`_UI_Page_Persist`)가 모듈을 읽을 때
|
||||
* 붙어도 `Section_Store` 와 돌고 도는 가져오기가 안 생김.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Types";
|
||||
|
||||
/** 새 상세 · 이어 받은 측점의 누가거리 옮김(옛 0.01 m 글 → 새 글 · 조작값 맵을 옮길 때). */
|
||||
export type DetailListener = (
|
||||
detail: SectionDetailResponse,
|
||||
moves: ReadonlyMap<string, string>,
|
||||
) => void;
|
||||
|
||||
const anyListeners = new Set<DetailListener>();
|
||||
|
||||
/** 어느 노선이든 상세가 갈아끼워지면 부름(조정창 값 · 암 경계 옮김 · 다시 그림) — 돌려준 함수로 끊음. */
|
||||
export function onAnySwap(fn: DetailListener): () => void {
|
||||
anyListeners.add(fn);
|
||||
return () => anyListeners.delete(fn);
|
||||
}
|
||||
|
||||
/** 갈아끼운 쪽(`Section_Store`)이 부름. */
|
||||
export function emitSwap(detail: SectionDetailResponse, moves: ReadonlyMap<string, string>): void {
|
||||
anyListeners.forEach((fn) => fn(detail, moves));
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { culvertCardState } from "./B06_Section_UI_Cross_View_Structure";
|
||||
import type { PipeRevetChoice } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
import { renameChainageKeys } from "./logic/B06_Section_Station_Swap";
|
||||
import { onAnySwap } from "./B06_Section_Swap_Bus";
|
||||
|
||||
/** 세션에 담기는 측점별 조작값 한 겹 맵. */
|
||||
export interface SessionMap<V> {
|
||||
@@ -65,6 +66,9 @@ export function moveSessionMaps(moves: ReadonlyMap<string, string>): void {
|
||||
}
|
||||
}
|
||||
|
||||
// 상세 갈아끼움(어느 보기가 했든)마다 한 번 — 조정창 값을 옮긴 측점 자리로
|
||||
onAnySwap((_detail, moves) => moveSessionMaps(moves));
|
||||
|
||||
export function createSessionMap<V>(
|
||||
sessionKey: () => string | null,
|
||||
accept: (value: unknown) => V | undefined,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { invalidateSectionDetail } from "./B06_Section_Section_Store";
|
||||
import { onAnySwap } from "./B06_Section_Swap_Bus";
|
||||
import type { DesignViewSave } from "../A00_Common/design/design_shared";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./B06_Section_UI_Page_Patches";
|
||||
import {
|
||||
@@ -87,6 +88,9 @@ export function reloadKeyedStores(): void {
|
||||
keyedStores.forEach((store) => store.load());
|
||||
}
|
||||
|
||||
// 상세 갈아끼움(어느 보기가 했든)마다 한 번 — 암 경계 · 암 경사를 새 목록 누가거리로 다시 읽음
|
||||
onAnySwap(() => reloadKeyedStores());
|
||||
|
||||
/** 세션 열쇠 글(`aislo:draft:rockb:{p}:{r}`)의 프로젝트 · 노선 — 저장소가 측점 열쇠 창구로 읽고 씀. */
|
||||
function keyedScope(storageKey: string): { projectId: string; routeId: number } | null {
|
||||
const [projectId, route] = storageKey.split(":").slice(-2);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_UI_Page_Station_Swap.ts
|
||||
* 횡단 보기가 계산 묶음 측점을 따라감(PLAN 346-2 4파) — 가게 `stations` 가 측점 목록을 바꾸면(늘고 줄면)
|
||||
* 상세 객체를 갈아끼우고(`swapSectionStations`) 조정창 값 · 암 경계 · 암 경사를 새 자리로 옮긴 뒤 보일 때 카드 ·
|
||||
* 종단도를 다시(카드는 보이는 것 먼저 · 다시 셈은 그 길이 맡음 · 안 보이면 다시 보일 때 그림).
|
||||
* 상세 객체를 갈아끼우고(`swapSectionStations` · 조정창 값 · 암 경계 · 암 경사 옮김은 `onAnySwap` 이 맡음) 보일 때
|
||||
* 카드 · 종단도를 다시(카드는 보이는 것 먼저 · 다시 셈은 그 길이 맡음 · 안 보이면 다시 보일 때 그림).
|
||||
* 종단 보기도 같은 가게를 듣고 같은 함수를 부름 — 먼저 부른 쪽이 갈아끼우고 다음 쪽은 「같은 목록」.
|
||||
* ========================================================================== */
|
||||
|
||||
import { useDesignStore } from "../A00_Common/design/design_store";
|
||||
import { swapSectionStations } from "./B06_Section_Section_Store";
|
||||
import { moveSessionMaps } from "./B06_Section_UI_Page_Link_Session";
|
||||
import { reloadKeyedStores } from "./B06_Section_UI_Page_Persist";
|
||||
import { onAnySwap, swapSectionStations } from "./B06_Section_Section_Store";
|
||||
|
||||
/** 묶음 측점을 따라감 — 돌려준 함수로 끊음. `redraw` = 캐시에서 상세를 다시 읽어 카드 · 그림 다시. */
|
||||
export function followSwaps(
|
||||
@@ -18,14 +17,14 @@ export function followSwaps(
|
||||
redraw: () => Promise<void>,
|
||||
): () => void {
|
||||
if (!projectId) return () => undefined;
|
||||
return useDesignStore(projectId, null).subscribe("stations", (stations) => {
|
||||
// 누가 갈아끼웠든(종단 보기가 먼저 불렀어도) 보이면 다시 그림 — 한 페이지 한 노선
|
||||
const stopRedraw = onAnySwap(() => void (host.visible() && redraw()));
|
||||
const stopStore = useDesignStore(projectId, null).subscribe("stations", (stations) => {
|
||||
const route = routeId();
|
||||
if (route === null || !stations?.length) return;
|
||||
void swapSectionStations(projectId, route, stations).then((swap) => {
|
||||
if (!swap) return; // 같은 목록 — 제자리 갱신 길이 맡음
|
||||
moveSessionMaps(swap.moves); // 조정창 값(반폭 · 기슭막이 · 유입 · 집수정 · 추가 벽)
|
||||
reloadKeyedStores(); // 암 경계 · 암 경사 = 측점 열쇠 세션 → 새 목록 누가거리로
|
||||
if (host.visible()) void redraw();
|
||||
});
|
||||
if (route !== null && stations?.length) void swapSectionStations(projectId, route, stations);
|
||||
});
|
||||
return () => {
|
||||
stopRedraw();
|
||||
stopStore();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
일반 = ``grid:k``(누가거리 ÷ 측점 간격 반올림) · 관 = ``pipe:<id>`` · 구조물 = ``structure:<id>`` ·
|
||||
시종점 = ``end:BP`` · ``end:EP`` · id 를 모르는 구조물 측점 = ``at:<누가거리 0.01 m>``.
|
||||
구간형 구조물 시작 · 끝 측점 = ``structure:<id>:start`` · ``:end``(기준 측점은 ``structure:<id>``).
|
||||
한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>``(``dedupe_station_keys``).
|
||||
|
||||
⚠ 파이썬·TS 짝 — ``common_util_station_key.ts`` ``stationKey`` ↔ ``station_key``.
|
||||
한쪽을 고치면 다른 쪽도 고치고 거울 시험 ``resources/tester/test_344_3_station_key_mirror.py``.
|
||||
@@ -31,19 +33,43 @@ def station_key(
|
||||
interval_m: float | None,
|
||||
station_id: str | None = None,
|
||||
id_kind: str = "structure",
|
||||
role: str = "anchor",
|
||||
) -> str:
|
||||
"""측점 열쇠 — irregular 는 id 가 있으면 pipe: · structure:(``id_kind``) · 없으면 at:."""
|
||||
"""측점 열쇠 — irregular 는 id 가 있으면 pipe: · structure:(``id_kind``) · 없으면 at:.
|
||||
|
||||
구간형 시작 · 끝은 ``role`` 로 ``:start`` · ``:end``. ``grid`` · ``end`` 는 ``station_id`` 에
|
||||
원래 격자 번호 · BP/EP(없으면 누가거리 ÷ 간격 반올림 · 0 이면 BP).
|
||||
"""
|
||||
loose = f"at:{chainage_key(chainage_m)}"
|
||||
suffix = "" if role == "anchor" else f":{role}"
|
||||
if kind == "bp":
|
||||
return "end:BP"
|
||||
if kind == "ep":
|
||||
return "end:EP"
|
||||
if kind == "end":
|
||||
return (
|
||||
f"end:{station_id if station_id is not None else ('BP' if chainage_m <= 0 else 'EP')}"
|
||||
)
|
||||
if kind in ("pipe", "structure"):
|
||||
return f"{kind}:{station_id}" if station_id else f"at:{chainage_key(chainage_m)}"
|
||||
return f"{kind}:{station_id}{suffix}" if station_id else loose
|
||||
if kind == "irregular":
|
||||
return f"{id_kind}:{station_id}" if station_id else f"at:{chainage_key(chainage_m)}"
|
||||
return f"{id_kind}:{station_id}{suffix}" if station_id else loose
|
||||
if kind == "grid" and station_id is not None:
|
||||
return f"grid:{station_id}"
|
||||
if interval_m and interval_m > 0:
|
||||
return f"grid:{_half_up(chainage_m / interval_m)}"
|
||||
return f"at:{chainage_key(chainage_m)}"
|
||||
return loose
|
||||
|
||||
|
||||
def dedupe_station_keys(keys: list[str], chainages: list[float]) -> list[str]:
|
||||
"""한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>`` — 열쇠마다 측점 하나."""
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for key, chainage in zip(keys, chainages, strict=True):
|
||||
unique = f"at:{chainage_key(chainage)}" if key in seen else key
|
||||
seen.add(unique)
|
||||
out.append(unique)
|
||||
return out
|
||||
|
||||
|
||||
def loose_chainage(key: str) -> float | None:
|
||||
|
||||
@@ -3,21 +3,29 @@
|
||||
* 측점 열쇠 한 규칙(PLAN 344-3) — 노선이 바뀌어도 같은 측점을 같은 이름으로 부름.
|
||||
* 일반 = `grid:k`(누가거리 ÷ 측점 간격 반올림) · 관 = `pipe:<id>` · 구조물 = `structure:<id>` ·
|
||||
* 시종점 = `end:BP` · `end:EP` · id 를 모르는 구조물 측점 = `at:<누가거리 0.01 m>`(옛 초안 열쇠와 같은 몫).
|
||||
* 구간형 구조물의 시작 · 끝 측점 = `structure:<id>:start` · `:end`(기준 측점은 `structure:<id>` 그대로).
|
||||
* 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>`(`dedupeStationKeys`).
|
||||
* 쓰는 곳 = 서버 `B05_Profile_Route_Save_Parts` · 묶음 stations 단계 · B06 세션 횡단 초안(`stationKeysOf`).
|
||||
*
|
||||
* ⚠ 파이썬·TS 짝 — `common_util_station_key.py` `station_key` ↔ `stationKey`. 한쪽을 고치면 다른 쪽도
|
||||
* 고치고 거울 시험 `resources/tester/test_344_3_station_key_mirror.py`.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 측점 종류 — 서버 측점 `kind`(bp · ep · regular · irregular) 그대로 받음. */
|
||||
export type StationKeyKind = "bp" | "ep" | "regular" | "irregular" | "pipe" | "structure";
|
||||
/** 측점 종류 — 서버 측점 `kind`(bp · ep · regular · irregular) · 묶음 측점 `kind`(grid · end · pipe · structure). */
|
||||
export type StationKeyKind =
|
||||
"bp" | "ep" | "regular" | "irregular" | "grid" | "end" | "pipe" | "structure";
|
||||
|
||||
/** 구간형 구조물 측점 자리 — 기준(anchor) · 시작 · 끝. */
|
||||
export type StationKeyRole = "anchor" | "start" | "end";
|
||||
|
||||
/** 누가거리 0.01 m — 옛 초안 열쇠(`toFixed(2)`)와 같은 자릿수. */
|
||||
export const chainageKey = (chainageM: number): string =>
|
||||
(Math.round(chainageM * 100) / 100).toFixed(2);
|
||||
|
||||
/**
|
||||
* 측점 열쇠. irregular 는 id 가 있으면 `pipe:` · `structure:`(종류는 `idKind`) · 없으면 `at:`.
|
||||
* 간격이 없거나 0 이하면 일반 측점도 `at:`.
|
||||
* 측점 열쇠. irregular 는 id 가 있으면 `pipe:` · `structure:`(종류는 `idKind`) · 없으면 `at:` · 구간형 시작 · 끝은
|
||||
* `role` 로 `:start` · `:end` 덧붙임. `grid` · `end` 는 `id` 에 원래 격자 번호 · `BP`/`EP` 를 줄 수 있음(없으면
|
||||
* 누가거리 ÷ 간격 반올림 · 0 이면 BP). 간격이 없거나 0 이하면 일반 측점도 `at:`.
|
||||
*/
|
||||
export function stationKey(
|
||||
kind: StationKeyKind,
|
||||
@@ -25,14 +33,31 @@ export function stationKey(
|
||||
intervalM: number | null,
|
||||
id: string | null = null,
|
||||
idKind: "pipe" | "structure" = "structure",
|
||||
role: StationKeyRole = "anchor",
|
||||
): string {
|
||||
const loose = `at:${chainageKey(chainageM)}`;
|
||||
const suffix = role === "anchor" ? "" : `:${role}`;
|
||||
if (kind === "bp") return "end:BP";
|
||||
if (kind === "ep") return "end:EP";
|
||||
if (kind === "pipe" || kind === "structure")
|
||||
return id ? `${kind}:${id}` : `at:${chainageKey(chainageM)}`;
|
||||
if (kind === "irregular") return id ? `${idKind}:${id}` : `at:${chainageKey(chainageM)}`;
|
||||
if (kind === "end") return `end:${id ?? (chainageM <= 0 ? "BP" : "EP")}`;
|
||||
if (kind === "pipe" || kind === "structure") return id ? `${kind}:${id}${suffix}` : loose;
|
||||
if (kind === "irregular") return id ? `${idKind}:${id}${suffix}` : loose;
|
||||
if (kind === "grid" && id !== null) return `grid:${id}`;
|
||||
if (intervalM && intervalM > 0) return `grid:${Math.round(chainageM / intervalM)}`;
|
||||
return `at:${chainageKey(chainageM)}`;
|
||||
return loose;
|
||||
}
|
||||
|
||||
/** 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>` — 열쇠마다 측점 하나. */
|
||||
export function dedupeStationKeys(
|
||||
keys: ReadonlyArray<string>,
|
||||
chainages: ReadonlyArray<number>,
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
return keys.map((key, index) => {
|
||||
const unique = seen.has(key) ? `at:${chainageKey(chainages[index])}` : key;
|
||||
seen.add(unique);
|
||||
return unique;
|
||||
});
|
||||
}
|
||||
|
||||
/** 옛 초안 열쇠(누가거리 글 `"85.05"`) · `at:` 열쇠의 누가거리 — 측점 열쇠면 null. */
|
||||
|
||||
@@ -68,6 +68,10 @@ function loadTs(file) {
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
// 측점 열쇠 한 벌(344-3 · 346-2)은 진짜로 — 빈 대역은 열쇠 목록을 못 냄
|
||||
STUBS["@util/common_util_station_key"] = loadTs(
|
||||
path.join(ROOT, "common_util", "common_util_station_key.ts"),
|
||||
);
|
||||
const { saveDesign, savingDesign } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "design_save.ts"),
|
||||
);
|
||||
|
||||
@@ -26,3 +26,4 @@ export { createCrossPreview } from "../../B05_Profile/B05_Profile_UI_Profile_Pre
|
||||
export { createHaulPlanPrefetch } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
export { createDesignSync } from "../../B06_Section/B06_Section_UI_Page_Design_Sync";
|
||||
export { writeState } from "../../A00_Common/b_page_state";
|
||||
export { listedBase } from "../../A00_Common/design/design_profile_base";
|
||||
|
||||
@@ -114,29 +114,8 @@ const irregularStations = [
|
||||
{ id: "pipe-1", station: 4, remainder: 5.05, chainage_m: PIPE, structure: "파형강관 D800" },
|
||||
];
|
||||
|
||||
/* 화면 선 = design_profile.ts 의 진짜 listed() */
|
||||
const dpSrc = readFileSync(join(ROOT, "A00_Common", "design", "design_profile.ts"), "utf8");
|
||||
const listedSrc = dpSrc.match(/\r?\n function listed\(\)[\s\S]*?\r?\n \}\r?\n/)[0];
|
||||
const listedOf = new Function(
|
||||
"storedBase",
|
||||
"detail",
|
||||
"irregularStations",
|
||||
"oneStationList",
|
||||
"irregularGraphStations",
|
||||
"maxChainageOf",
|
||||
"followMovedNodes",
|
||||
`${transpile(listedSrc)}\nreturn listed();`,
|
||||
);
|
||||
const screenBase = (detail) =>
|
||||
listedOf(
|
||||
m.toAlignmentBase(stored),
|
||||
detail,
|
||||
irregularStations,
|
||||
m.oneStationList,
|
||||
m.irregularGraphStations,
|
||||
m.maxChainageOf,
|
||||
m.followMovedNodes,
|
||||
);
|
||||
/* 화면 선 = 종단도가 쓰는 진짜 `listedBase`(design_profile_base · 옛 design_profile 안 listed()) */
|
||||
const screenBase = (detail) => m.listedBase(m.toAlignmentBase(stored), detail, irregularStations);
|
||||
const staleOf = (detail, line) =>
|
||||
m.staleKey({
|
||||
longitudinal: { design_profiles: [m.toDesignProfile(line, undefined)] },
|
||||
|
||||
@@ -190,6 +190,60 @@ const NEW = (dy) => [
|
||||
stopListen();
|
||||
}
|
||||
|
||||
/* 2-2. 종단 보기(B05) · 3D 측점선 — 횡단 보기 없이도 가게 측점으로 갈아끼우고 보일 때만 다시 그림 */
|
||||
{
|
||||
const R2 = 11;
|
||||
m.writeState("section-detail", oldDetail(), P, R2);
|
||||
const renders = [];
|
||||
const lines = [];
|
||||
let shown = true;
|
||||
const sections = m.createProfileSections({
|
||||
projectId: P,
|
||||
state: {
|
||||
confirmedSurface: null,
|
||||
latest: { route_points: [] },
|
||||
roadWidths: { g: 5 },
|
||||
detail: null,
|
||||
corridorVisible: false,
|
||||
corridorPending: false,
|
||||
routeReady: true,
|
||||
restoring: false,
|
||||
},
|
||||
viewer: { renderStationLines: (list) => lines.push(list.length), markers: {} },
|
||||
panel: () => ({
|
||||
values: () => ({ gradeClass: "g", stationInterval: 20, enforcePipeClearance: false }),
|
||||
gradeLimitPct: () => 12,
|
||||
stationDisplayOffset: () => ({ station: 0, cumulative: 0 }),
|
||||
setCorridorPending: () => undefined,
|
||||
}),
|
||||
profilePanel: () => ({
|
||||
render: (detail) => renders.push(detail.cross_sections.length),
|
||||
setGradeLimit: () => undefined,
|
||||
setEnforceMinCover: () => undefined,
|
||||
setStationDisplay: () => undefined,
|
||||
setIrregularStations: () => undefined,
|
||||
alignmentSamples: () => null,
|
||||
setLoading: () => undefined,
|
||||
clear: () => undefined,
|
||||
}),
|
||||
bridge: () => ({ irregularStations: () => [] }),
|
||||
visible: () => shown,
|
||||
});
|
||||
await sections.restoreSections(R2);
|
||||
const shop = m.useDesignStore(P, null);
|
||||
shop.set("stations", NEW(0)); // 보임 — 바로 다시
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
const afterShown = [...renders];
|
||||
shown = false;
|
||||
shop.set("stations", NEW(0).slice(0, 6)); // 숨음 — 안 그림
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
const whileHidden = [...renders];
|
||||
shown = true;
|
||||
sections.flushSwap(); // 다시 보임
|
||||
out.profile = { afterShown, whileHidden, afterShow: [...renders], lines: lines.length };
|
||||
sections.stopSwap();
|
||||
}
|
||||
|
||||
/* 3. 보이는 카드 먼저 — 바로 그릴 수 */
|
||||
out.eager = [
|
||||
m.eagerCardCount({ clientHeight: 600, scrollTop: 0 }, 3, 250, 12),
|
||||
|
||||
@@ -23,3 +23,4 @@ export {
|
||||
} from "../../B06_Section/B06_Section_UI_Page_Link_Session";
|
||||
export { useDesignStore } from "../../A00_Common/design/design_store";
|
||||
export { readState, writeState } from "../../A00_Common/b_page_state";
|
||||
export { createProfileSections } from "../../A00_Common/design/design_view_profile_sections";
|
||||
|
||||
@@ -16,6 +16,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from common_util.common_util_station_key import ( # noqa: E402
|
||||
chainage_key,
|
||||
dedupe_station_keys,
|
||||
loose_chainage,
|
||||
station_key,
|
||||
)
|
||||
@@ -24,7 +25,17 @@ TS = PROJECT_ROOT / "common_util" / "common_util_station_key.ts"
|
||||
TYPESCRIPT = PROJECT_ROOT / "config" / "node_modules" / "typescript"
|
||||
|
||||
CASES = [
|
||||
("bp", 0.0, 20.0, None, "structure"),
|
||||
("bp", 0.0, 20.0, None, "structure", "anchor"),
|
||||
# 5파 — 묶음 측점 종류(grid · end) · 구간형 시작 · 끝(:start · :end)
|
||||
("grid", 41.3, 20.0, "2", "structure", "anchor"),
|
||||
("grid", 41.3, 20.0, None, "structure", "anchor"),
|
||||
("end", 0.0, 20.0, None, "structure", "anchor"),
|
||||
("end", 99.0, 20.0, None, "structure", "anchor"),
|
||||
("end", 99.0, 20.0, "EP", "structure", "anchor"),
|
||||
("structure", 30.0, 20.0, "st-9", "structure", "start"),
|
||||
("structure", 36.0, 20.0, "st-9", "structure", "end"),
|
||||
("irregular", 30.0, 20.0, "st-9", "structure", "start"),
|
||||
("pipe", 30.0, 20.0, None, "structure", "end"),
|
||||
("ep", 812.345, 20.0, None, "structure"),
|
||||
("regular", 40.0, 20.0, None, "structure"),
|
||||
("regular", 50.0, 20.0, None, "structure"), # 2.5 → 3(위로)
|
||||
@@ -37,6 +48,7 @@ CASES = [
|
||||
("pipe", 12.344, 20.0, "pipe-s1", "structure"),
|
||||
("structure", 12.345, 20.0, None, "structure"),
|
||||
]
|
||||
DEDUPE = (["grid:1", "structure:st-1", "grid:1", "structure:st-1"], [20.0, 25.0, 20.004, 25.006])
|
||||
LOOSE = ["85.05", "at:85.05", "grid:4", "pipe:pipe-s3", "-1.5", "end:BP"]
|
||||
|
||||
SCRIPT = r"""
|
||||
@@ -50,7 +62,8 @@ new Function("exports", "module", js)(box.exports, box);
|
||||
const m = box.exports;
|
||||
const input = JSON.parse(process.argv[3]);
|
||||
process.stdout.write(JSON.stringify({
|
||||
keys: input.cases.map((c) => m.stationKey(c[0], c[1], c[2], c[3], c[4])),
|
||||
keys: input.cases.map((c) => m.stationKey(c[0], c[1], c[2], c[3], c[4], c[5] ?? "anchor")),
|
||||
dedupe: m.dedupeStationKeys(input.dedupe[0], input.dedupe[1]),
|
||||
chainage: input.cases.map((c) => m.chainageKey(c[1])),
|
||||
loose: input.loose.map((k) => m.looseChainage(k)),
|
||||
}));
|
||||
@@ -65,7 +78,7 @@ def test_파이썬과_TS_열쇠_같음():
|
||||
SCRIPT,
|
||||
str(TYPESCRIPT),
|
||||
str(TS),
|
||||
json.dumps({"cases": CASES, "loose": LOOSE}),
|
||||
json.dumps({"cases": CASES, "loose": LOOSE, "dedupe": DEDUPE}),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -77,6 +90,7 @@ def test_파이썬과_TS_열쇠_같음():
|
||||
assert out["keys"] == [station_key(*case) for case in CASES]
|
||||
assert out["chainage"] == [chainage_key(case[1]) for case in CASES]
|
||||
assert out["loose"] == [loose_chainage(key) for key in LOOSE]
|
||||
assert out["dedupe"] == dedupe_station_keys(*DEDUPE)
|
||||
|
||||
|
||||
def test_열쇠_규칙():
|
||||
@@ -86,3 +100,9 @@ def test_열쇠_규칙():
|
||||
assert station_key("irregular", 85.054, 20.0, "pipe-s3", "pipe") == "pipe:pipe-s3"
|
||||
assert station_key("irregular", 85.054, 20.0) == "at:85.05"
|
||||
assert loose_chainage("85.05") == 85.05 and loose_chainage("grid:4") is None
|
||||
# 5파 — 구간형 시작 · 끝 · 기준은 그대로 · 원래 격자 번호 · 같은 열쇠 둘째는 at:
|
||||
assert station_key("structure", 30.0, 20.0, "st-9", role="start") == "structure:st-9:start"
|
||||
assert station_key("structure", 33.0, 20.0, "st-9") == "structure:st-9"
|
||||
assert station_key("grid", 41.3, 20.0, "2") == "grid:2"
|
||||
assert station_key("end", 99.0, 20.0) == "end:EP"
|
||||
assert dedupe_station_keys(*DEDUPE) == ["grid:1", "structure:st-1", "at:20.00", "at:25.01"]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- 가게 `stations` → 캐시 상세 갈아끼움 · 듣는 쪽에 새 상세 + 누가거리 옮김 ·
|
||||
카드 선택은 측점 열쇠로 새 자리(관 45.054 → 47 m) · 노선 모양이 바뀌면 넘김 규칙
|
||||
(단면 유형 버림) · 사라진 측점 고침 버림 · 조정창 값도 옮긴 자리로.
|
||||
- 종단 보기(B05) · 3D 측점선 — 횡단 보기를 안 열어도 가게 측점으로 갈아끼움 · 보일 때만 다시 그림.
|
||||
- 보이는 카드 먼저 — 보이는 줄 + 한 줄만 바로.
|
||||
- `formatStation` ↔ 파이썬 `format_station` 같은 글.
|
||||
|
||||
@@ -116,6 +117,14 @@ def test_가게_측점으로_상세_갈아끼움_열쇠로_이어_받기(out):
|
||||
assert store["again"] is True # 같은 목록 = 안 갈아끼움
|
||||
|
||||
|
||||
def test_종단_보기_3D_측점선도_새_목록_보일_때만(out):
|
||||
profile = out["profile"]
|
||||
assert profile["afterShown"] == [7, 8] # 처음 7 → 가게 측점으로 갈아끼움 8(횡단 보기 없이)
|
||||
assert profile["whileHidden"] == [7, 8] # 숨은 동안은 안 그림
|
||||
assert profile["afterShow"] == [7, 8, 6] # 다시 보일 때 새 목록(6)
|
||||
assert profile["lines"] == 3 # 3D 측점선도 같은 차례로 다시
|
||||
|
||||
|
||||
def test_보이는_카드_먼저(out):
|
||||
assert out["eager"] == [12, 27, 4] # 3열 · 600 px · (250+12) px 줄 = 3 줄 + 1
|
||||
assert out["rename"] == {"47.00": 1, "b47.00": 2, "47.00:outlet": 3, "60.00": 4}
|
||||
|
||||
Reference in New Issue
Block a user