Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pick_escape.ts
|
||||
* 풀기 한 길 — Esc = 고름 한 칸 비움 (233-3 · 조사 AF (나)).
|
||||
*
|
||||
* 값을 비우면 구독한 화면이 모두 따라 풂 — 종단(측점선 · 알약) · 3D(띠 · 핀 · 부재) · 배수유역도 ·
|
||||
* 좌측 폼 · 횡단 카드 · 범위 테두리 · 포장층 · 형상. 입력 칸 · 창(모달) · 메뉴 · 종단 도구가 먼저
|
||||
* 받은 Esc(입력 취소 · 창 닫기 · 도구 끝)는 그쪽 몫이라 건드리지 않음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { NO_SELECTION, selectDesign } from "./design_selection";
|
||||
|
||||
/** Esc 로 푼 고름의 출처 이름 — 구독자가 폼까지 닫을지 가르는 데 씀. */
|
||||
export const ESCAPE_FROM = "esc";
|
||||
|
||||
const OWN_ESCAPE =
|
||||
"input, select, textarea, [contenteditable='true'], [role='dialog'], .ui-modal__backdrop";
|
||||
|
||||
/** 보기마다 건다 — 보이는 보기 하나만 받음 · 보기를 떠나면 뗌. */
|
||||
export function bindEscapeRelease(
|
||||
projectId: string | null,
|
||||
host: { visible: () => boolean; onDispose: (fn: () => void) => void },
|
||||
): void {
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
if (event.key !== "Escape" || event.defaultPrevented || !host.visible()) return;
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target?.closest?.(OWN_ESCAPE) || document.querySelector(".ui-modal__backdrop")) return;
|
||||
selectDesign(projectId, NO_SELECTION, { from: ESCAPE_FROM });
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
host.onDispose(() => window.removeEventListener("keydown", onKey));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pick_profile.ts
|
||||
* 종단도 고름 강조(측점 세로선 · 알약 · 구간 띠) = 고름 한 칸에서 파생 (233-1 · 조사 AF (나) ②).
|
||||
* 종단 보기 고름 강조 = 고름 한 칸에서 파생 — 종단도(측점 세로선 · 알약 · 구간 띠 · 233-1) ·
|
||||
* 3D(측점 띠 · 핀 · 부재 · 233-2) · 배수유역도(관 마커 · 유역 · 측점 마름모 · 233-3) (조사 AF (나) ② ③ ⑤).
|
||||
*
|
||||
* 고름 한 칸(`structure-pick`)이 유일 원천이다. 값이 바뀌면(구독) 그 값으로 종단도를 다시 세우고,
|
||||
* 관 · 구조물 목록이 바뀐 뒤에도 같은 값으로 다시 얹는다(`paint`) — 화면끼리 손으로 부르던 길이
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
subscribeSelection,
|
||||
type DesignSelection,
|
||||
} from "./design_selection";
|
||||
import { bindEscapeRelease, ESCAPE_FROM } from "./design_pick_escape";
|
||||
import { graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection";
|
||||
import { irregularStationId } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
|
||||
@@ -32,6 +34,11 @@ export interface ProfilePickSource {
|
||||
export interface ProfilePickTarget {
|
||||
setSelectedStation: (stationId: string | null) => void;
|
||||
setSelectedStructure: (structureId: string | null) => void;
|
||||
/** 배수유역도 — 관 마커 · 유역(알림 없이) · 측점 마름모. */
|
||||
drainage?: {
|
||||
selectPipeAtChainage: (chainageM: number | null, notify?: boolean) => void;
|
||||
markStation: (chainageM: number | null) => void;
|
||||
} | null;
|
||||
}
|
||||
|
||||
/** 같은 값으로 적힌 측점인가 — 고름 한 칸은 고른 측점의 누가거리를 그대로 적음(찾는 폭 아님). */
|
||||
@@ -70,19 +77,76 @@ export function paintProfilePick(target: ProfilePickTarget, pick: ProfilePick):
|
||||
else target.setSelectedStation(pick.stationId);
|
||||
}
|
||||
|
||||
/** 배수유역도 = 그 자리 관 마커 · 유역(없으면 풂) + 측점 마름모 — 재계산이 지운 뒤에도 다시 얹음(a3 · a6). */
|
||||
export function paintMapPick(
|
||||
map: NonNullable<ProfilePickTarget["drainage"]>,
|
||||
selection: DesignSelection | null,
|
||||
): void {
|
||||
const at = selection?.chainageM ?? null;
|
||||
map.selectPipeAtChainage(at, false);
|
||||
map.markStation(at);
|
||||
}
|
||||
|
||||
export interface ViewerPickTarget {
|
||||
markers: { selectStation: (stationId: string | null, notify?: boolean) => void };
|
||||
structurePick: {
|
||||
select: (pick: { chainageM: number; kind: string; key?: string } | null) => void;
|
||||
};
|
||||
}
|
||||
|
||||
/** 고름 값 → 3D 가 켤 것 — 띠(+핀) = 종단 측점선과 같은 측점(구조물 고름은 그 자리 구조물 측점만) ·
|
||||
* 부재 = 고른 측점의 부재 전부(부재키가 있으면 그 부재 하나). */
|
||||
export function viewerPickOf(
|
||||
selection: (DesignSelection & { partKey?: string }) | null,
|
||||
source: ProfilePickSource,
|
||||
): { stationId: string | null; member: { chainageM: number; kind: string; key?: string } | null } {
|
||||
if (!selection || selection.chainageM === null) return { stationId: null, member: null };
|
||||
const at = selection.chainageM;
|
||||
const pick = profilePickOf(selection, source);
|
||||
const own = selection.structureId
|
||||
? nearestWithin(source.irregular(), at, (entry) => entry.chainage_m)
|
||||
: undefined;
|
||||
const stationId = "stationId" in pick ? pick.stationId : own ? irregularStationId(own.id) : null;
|
||||
return { stationId, member: { chainageM: at, kind: "", key: selection.partKey } };
|
||||
}
|
||||
|
||||
export function paintViewerPick(
|
||||
target: ViewerPickTarget,
|
||||
pick: ReturnType<typeof viewerPickOf>,
|
||||
): void {
|
||||
target.markers.selectStation(pick.stationId, false); // 칠하기만 — 알림이 사슬을 다시 돌리지 않게
|
||||
target.structurePick.select(pick.member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 종단도를 고름 한 칸에 묶음 — 값이 바뀔 때마다 그림 · `paint()` = 목록이 바뀐 뒤 같은 값으로 다시 얹음.
|
||||
* 보기가 숨어 있으면(다른 보기가 종단도를 쥠) 안 그림 — 그 보기가 제 규칙으로 그림 · 보기를 떠나면 구독 끊음.
|
||||
* 종단 보기(종단도 · 3D · 배수유역도)를 고름 한 칸에 묶음 — 값이 바뀔 때마다 그림 · `paint()` = 목록이
|
||||
* 바뀐 뒤 · 재계산 뒤 · 폼이 닫힌 뒤 같은 값으로 다시 얹음. 보기가 숨어 있으면(횡단 보기가 종단도를 쥠)
|
||||
* 안 그림 — 그 보기가 제 규칙으로 그림 · 보기를 떠나면 구독 끊음. Esc(풀기 한 길)는 좌측 폼까지 닫음.
|
||||
*/
|
||||
export function bindProfilePick(
|
||||
projectId: string | null,
|
||||
target: ProfilePickTarget,
|
||||
target: {
|
||||
profile: ProfilePickTarget;
|
||||
viewer?: ViewerPickTarget;
|
||||
form?: { hasSelection: () => boolean; selectById: (id: null) => void };
|
||||
},
|
||||
host: { visible: () => boolean; onDispose: (fn: () => void) => void },
|
||||
source: ProfilePickSource,
|
||||
): { paint: () => void } {
|
||||
const paint = (): void => {
|
||||
if (host.visible()) paintProfilePick(target, profilePickOf(readSelection(projectId), source));
|
||||
if (!host.visible()) return;
|
||||
const selection = readSelection(projectId);
|
||||
paintProfilePick(target.profile, profilePickOf(selection, source));
|
||||
if (target.viewer) paintViewerPick(target.viewer, viewerPickOf(selection, source));
|
||||
if (target.profile.drainage) paintMapPick(target.profile.drainage, selection);
|
||||
};
|
||||
host.onDispose(subscribeSelection(projectId, paint));
|
||||
host.onDispose(
|
||||
subscribeSelection(projectId, (_selection, from) => {
|
||||
paint();
|
||||
if (from === ESCAPE_FROM && host.visible() && target.form?.hasSelection())
|
||||
target.form.selectById(null);
|
||||
}),
|
||||
);
|
||||
bindEscapeRelease(projectId, host);
|
||||
return { paint };
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { readStructurePick } from "../../B05_Profile/B05_Profile_UI_Structure_Pi
|
||||
import { NO_SELECTION, PIPE_MATCH_M, selectDesign } from "./design_selection";
|
||||
import { readPendingPipes } from "../../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import { applyStructurePick } from "../../B06_Section/B06_Section_UI_Page_Structure_Pick";
|
||||
import { bindCrossPick } from "../../B06_Section/B06_Section_UI_Page_Pick_Sync";
|
||||
import { createSectionView } from "../../B06_Section/B06_Section_UI_Section_View";
|
||||
import { revetFormOf, revetWallSpec } from "../../B06_Section/B06_Section_UI_Cross_Culvert_Const";
|
||||
import {
|
||||
@@ -387,7 +388,7 @@ export async function mountCrossView(
|
||||
: null;
|
||||
if (!target) {
|
||||
structuresPanel.showAtChainage(null);
|
||||
selectDesign(projectId, NO_SELECTION, { from: "b06" }); // 카드 해제 — 선택도 비운다.
|
||||
releaseCross(); // 카드 다시 누름 = 풀기 한 길(범위 테두리 · 형상까지 · 233-2)
|
||||
return;
|
||||
}
|
||||
const owner = isWallSelecting() ? stationControls.structureSpan.ownerOf(target) : null;
|
||||
@@ -494,23 +495,20 @@ export async function mountCrossView(
|
||||
if (applied) appliedPick = signature;
|
||||
}
|
||||
|
||||
/** 고름을 다 풂(54-38) — 카드 · 벽 · 세월교 · BOX 강조 · 넘김 되얹기 표시 · 선택 칸. */
|
||||
// 횡단 보기 강조 = 고름 한 칸 구독 · 다른 곳(Esc 등)이 바꾼 값으로 다시 세움(233-2)
|
||||
const crossPick = bindCrossPick(projectId, host, {
|
||||
sections: () => sectionDetail?.cross_sections ?? [],
|
||||
controls: stationControls,
|
||||
releaseForm: () => structuresPanel.showAtChainage(null),
|
||||
view: sectionView,
|
||||
reapply: () => {
|
||||
appliedPick = null;
|
||||
restoreStructurePick();
|
||||
},
|
||||
});
|
||||
/** 고름을 다 풂(54-38) — 강조 모두 끄고 넘김 되얹기 표시 · 선택 칸 비움(풀기 한 길 · 233-2). */
|
||||
function releaseCross(): void {
|
||||
const { revetOffset, ford, box } = stationControls;
|
||||
const lit = (sectionDetail?.cross_sections ?? []).filter(
|
||||
(s) =>
|
||||
revetOffset.highlightFor(s) ||
|
||||
ford.selectedFor(s.chainage_m) ||
|
||||
box.selectedFor(s.chainage_m),
|
||||
);
|
||||
for (const s of lit) {
|
||||
if (revetOffset.selectedFor(s)) revetOffset.select(s.chainage_m, null);
|
||||
if (ford.selectedFor(s.chainage_m)) ford.select(s.chainage_m, null);
|
||||
if (box.selectedFor(s.chainage_m)) box.select(s.chainage_m, null);
|
||||
}
|
||||
structuresPanel.releasePipe();
|
||||
sectionView.focusStation(null);
|
||||
sectionView.refreshCards(lit.map((s) => s.chainage_m));
|
||||
crossPick.release();
|
||||
appliedPick = null;
|
||||
selectDesign(projectId, NO_SELECTION, { from: "b06" });
|
||||
}
|
||||
|
||||
@@ -31,13 +31,7 @@ import {
|
||||
restoreStructurePick,
|
||||
wireStructurePick,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import {
|
||||
coversAtStation,
|
||||
NO_SELECTION,
|
||||
nearestWithin,
|
||||
readSelection,
|
||||
selectDesign,
|
||||
} from "./design_selection";
|
||||
import { coversAtStation, NO_SELECTION, readSelection, selectDesign } from "./design_selection";
|
||||
import type { DesignPageHost } from "./design_page";
|
||||
import { bindProfilePick } from "./design_pick_profile";
|
||||
import {
|
||||
@@ -174,8 +168,8 @@ export async function mountProfileView(
|
||||
panel.structures.addAt(chainage, LEGACY_TYPE_IDS[type] ?? "etc"),
|
||||
onIrregularSelect: (station) => syncIrregularSelection(irregularStationId(station.id)),
|
||||
// 배수유역도에서 유역·관 마커를 고르면 그 관의 측점을 그래프·3D·사이드 패널에서도 고른다.
|
||||
onBasinSelected: (chainageM) => selectStationOfPipe(chainageM),
|
||||
onPipeSelected: (chainageM) => selectStationOfPipe(chainageM),
|
||||
onBasinSelected: (chainageM) => pickFromMap(chainageM),
|
||||
onPipeSelected: (chainageM) => pickFromMap(chainageM),
|
||||
// 구조물 알약 — 그래프에서 고르면 사이드 폼도 같은 항목을 연다. 계곡 통과 시설
|
||||
// (가상 id `pipe-*`)은 관 정본 소관이라 누가거리로 되돌려 보낸다(2026-08-17).
|
||||
onStructureSelect: (structureId) => {
|
||||
@@ -221,7 +215,7 @@ export async function mountProfileView(
|
||||
* 저장 시 uphill_overrides로 백엔드/DB(종단 정본)에 병합한다(3D 구슬 입력은 176-3 에서 지움). */
|
||||
const uphillOverrides = loadUphillOverrides(activeProjectId);
|
||||
|
||||
const { syncIrregularSelection, syncBasinHighlight, selectStationOfPipe } = createSelectionSync({
|
||||
const { syncIrregularSelection, selectStationOfPipe } = createSelectionSync({
|
||||
stations: () => bridge.irregularStations(),
|
||||
isSyncing: () => selectionSyncing,
|
||||
setSyncing: (value) => {
|
||||
@@ -245,6 +239,12 @@ export async function mountProfileView(
|
||||
remember: (chainageM) => rememberPick(chainageM, null),
|
||||
});
|
||||
|
||||
/** 배수유역도 유역 · 관 고름도 고름 한 칸에(사슬 도중 되울림은 안 적음 · 233-3). */
|
||||
const pickFromMap = (chainageM: number | null): void => {
|
||||
if (!selectionSyncing) rememberPick(chainageM, null);
|
||||
selectStationOfPipe(chainageM);
|
||||
};
|
||||
|
||||
/** 세션에 남은 구조물 선택을 되살린다 — 횡단 보기에서 고른 것도 그대로 이어 받는다
|
||||
* (2026-09-04). 이미 무언가 골라져 있으면 건드리지 않는다. */
|
||||
function restorePick(): void {
|
||||
@@ -320,39 +320,18 @@ export async function mountProfileView(
|
||||
forwardPipeOptions(shared, from, attributes.options as Record<string, number | string>);
|
||||
},
|
||||
onPipeFacilityRemove: (chainage) => profilePanel.drainage.removePipe(chainage),
|
||||
// 목록 고름(null = 다시 누름 해제)도 고름 한 칸에 — 종단 · 3D · 배수유역도는 구독이 그 값으로 그림(233-3)
|
||||
onPipeFacilitySelect: (chainage) => {
|
||||
rememberPick(chainage, null); // 목록 선택도 횡단 보기로 이어진다.
|
||||
if (selectionSyncing) return;
|
||||
selectionSyncing = true;
|
||||
try {
|
||||
// null = 사이드 재클릭 해제 — 그래프·3D·배수유역도 선택도 함께 푼다(2026-08-18).
|
||||
const station =
|
||||
chainage === null
|
||||
? undefined
|
||||
: nearestWithin(bridge.irregularStations(), chainage, (entry) => entry.chainage_m);
|
||||
const id = station ? irregularStationId(station.id) : null;
|
||||
viewer.markers.selectStation(id);
|
||||
selectGraphAt(id, chainage);
|
||||
syncBasinHighlight(id);
|
||||
// 배수유역 마커 선택 + 부속 옵션 폼도 연다(2026-08-17 전역 선택 동기화).
|
||||
profilePanel.drainage.selectPipeAtChainage(chainage);
|
||||
viewer.structurePick.selectAtChainage(chainage); // 3D 도 따라옴(53-2)
|
||||
} finally {
|
||||
selectionSyncing = false;
|
||||
}
|
||||
rememberPick(chainage, null);
|
||||
if (!selectionSyncing) profilePick.paint();
|
||||
},
|
||||
onListRelease: () => rememberPick(null, null), // 목록 다시 누름 = 풀기 한 길(233-3)
|
||||
onStructureSelect: (structure) => {
|
||||
// 목록에서 고른 구조물도 횡단 보기로 이어진다 — 해제(null)는 여기서 적지 않는다(관 폼을
|
||||
// 여는 길이 구조물 폼을 먼저 비우며 이 콜백을 null 로 부르기 때문).
|
||||
if (structure) rememberPick(structureAnchorM(structure), structure.structure_id ?? null);
|
||||
if (selectionSyncing) return;
|
||||
selectionSyncing = true;
|
||||
try {
|
||||
profilePick.paint(); // 폼이 닫혀도(저장 뒤 · 관 폼으로) 종단은 고름 한 칸대로(233-1)
|
||||
viewer.structurePick.selectAtChainage(structure ? structureAnchorM(structure) : null);
|
||||
} finally {
|
||||
selectionSyncing = false;
|
||||
}
|
||||
// 폼이 닫혀도(저장 뒤 · 관 폼으로) 종단 · 3D 는 고름 한 칸대로(233-1 · 233-2)
|
||||
if (!selectionSyncing) profilePick.paint();
|
||||
},
|
||||
},
|
||||
shared,
|
||||
@@ -411,12 +390,17 @@ export async function mountProfileView(
|
||||
},
|
||||
isRestoring: () => restoring,
|
||||
});
|
||||
// 종단도 강조 = 고름 한 칸에서 파생 · 이 보기가 보일 때만(횡단 보기는 제 규칙 · 233-1)
|
||||
const profilePick = bindProfilePick(activeProjectId, profilePanel, host, {
|
||||
irregular: () => bridge.irregularStations(),
|
||||
pipes: () => lastPipes ?? [],
|
||||
sections: () => currentSectionDetail?.cross_sections ?? [],
|
||||
});
|
||||
// 종단도 · 3D 강조 = 고름 한 칸에서 파생 · 이 보기가 보일 때만(횡단 보기는 제 규칙 · 233-1 · 233-2)
|
||||
const profilePick = bindProfilePick(
|
||||
activeProjectId,
|
||||
{ profile: profilePanel, viewer, form: panel.structures },
|
||||
host,
|
||||
{
|
||||
irregular: () => bridge.irregularStations(),
|
||||
pipes: () => lastPipes ?? [],
|
||||
sections: () => currentSectionDetail?.cross_sections ?? [],
|
||||
},
|
||||
);
|
||||
// 3D 노선 우클릭 → 구조물 배치 메뉴(그래프·배수유역도와 동일, 2026-08-19).
|
||||
bridge.mountViewerMenu(viewer, () => latest?.route_points ?? []);
|
||||
|
||||
@@ -573,7 +557,6 @@ export async function mountProfileView(
|
||||
);
|
||||
return;
|
||||
}
|
||||
viewer.markers.selectStation(null);
|
||||
profilePick.paint();
|
||||
},
|
||||
// 보기를 떠나는 자리 = 코리도 영구저장 시점(2026-08-23 사용자 확정) — 이동은 막지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user