- C군 기본값 확정(2026-08-19 사용자): 높이 2.5·길이 10·전길이 5 — 기본값이 생겨 required 해제. 기준 측점만 넣으면 범위가 바로 계산된다. - 드래그 민감도 완화: 종단 알약(누적→변위 14px, 임계 전 시각 이동 금지)·배수유역도 관 마커(이동 시작 8px 임계) — 고르려다 옮겨지는 문제. - 3D 노선 우클릭 → 구조물 배치 메뉴(신규 B05_Profile_UI_Viewer_Menu) — 지형 픽을 노선 폴리라인에 투영(15m 이내), 그래프·배수유역도와 같은 2단 메뉴·addAt 경로. - 우클릭 메뉴 전역 단일화(ui_template_context_menu): 새 메뉴가 열리면 다른 영역 메뉴는 닫히고, 좌클릭·중간버튼은 어디서든 닫는다(캡처 단계). - 3D 비정규 측점(배관 등) 좌표를 노선 폴리라인 chainage 투영으로 정확화 — 규칙 측점 직선보간의 곡선 모서리 잘림(노선 이탈) 해소. - 3D 측점선(노란 띠) 도로 반폭 1.5배 연장 — 측구 방향 램프는 끝단에 따라붙음. - 검증: pytest 112 통과(정책 테스트 기본값 반영), tsc, 헤드 브라우저 저장 왕복 (기준 3+0 → 55~65·anchor 60·before 5, 목록 '배수관 1/2' 번호 표기). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
4.0 KiB
TypeScript
90 lines
4.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Viewer_Menu.ts
|
|
* 3D 뷰포트 노선 위 우클릭 → 구조물 배치 메뉴 (2026-08-19 사용자 지시).
|
|
*
|
|
* 종단그래프·배수유역도와 **같은 메뉴**(구조물군 → 종류 2단, 선택 = 사이드 폼
|
|
* addAt 경로)를 3D에도 단다. 지형 우클릭 지점을 노선 폴리라인에 투영해 누가거리를
|
|
* 얻고, 노선에서 먼 자리는 메뉴를 띄우지 않는다(회전 등 기본 조작 방해 금지).
|
|
* 전역 단일 메뉴 규칙(다른 영역에 열리면 이쪽은 닫힘, 좌·중클릭 닫힘)은 공용
|
|
* createMapContextMenu가 처리한다.
|
|
* ========================================================================== */
|
|
|
|
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
|
import {
|
|
structureGroupMenuItems,
|
|
type StructureMenuType,
|
|
} from "./B05_Profile_UI_Profile_Structures";
|
|
|
|
/** 노선 위로 본다 판정 거리(m) — 이보다 멀면 노선 우클릭이 아니다. */
|
|
const ROUTE_PICK_DISTANCE_M = 15;
|
|
|
|
export interface ViewerMenuDeps {
|
|
/** 3D 뷰포트 루트(position: relative) — 메뉴를 이 안에 절대 위치로 띄운다. */
|
|
root: HTMLElement;
|
|
/** 화면 좌표 아래 지형의 모델 좌표(없으면 null). */
|
|
modelPointAt: (clientX: number, clientY: number) => { x: number; y: number } | null;
|
|
/** 노선 폴리라인(모델 좌표, 시점→종점 순). 비어 있으면 메뉴를 띄우지 않는다. */
|
|
routePoints: () => ReadonlyArray<{ x: number; y: number }>;
|
|
/** 레지스트리 타입 목록 — 사이드 「구조물 배치」 종류와 같은 원천. */
|
|
structureTypes: () => ReadonlyArray<StructureMenuType>;
|
|
/** 종류 선택 — 사이드 폼 자동 지정(addAt 경로, A군은 임시 배치까지). */
|
|
onPick: (chainageM: number, typeId: string) => void;
|
|
}
|
|
|
|
/** 모델 좌표를 노선 폴리라인에 투영 — 최근접 지점의 누가거리와 거리(m). */
|
|
function projectToRoute(
|
|
polyline: ReadonlyArray<{ x: number; y: number }>,
|
|
x: number,
|
|
y: number,
|
|
): { chainage: number; distance: number } | null {
|
|
if (polyline.length < 2) return null;
|
|
let best: { chainage: number; distance: number } | null = null;
|
|
let travelled = 0;
|
|
for (let index = 1; index < polyline.length; index += 1) {
|
|
const from = polyline[index - 1];
|
|
const to = polyline[index];
|
|
const dx = to.x - from.x;
|
|
const dy = to.y - from.y;
|
|
const span2 = dx * dx + dy * dy;
|
|
if (span2 < 1e-12) continue;
|
|
const span = Math.sqrt(span2);
|
|
const t = Math.min(Math.max(((x - from.x) * dx + (y - from.y) * dy) / span2, 0), 1);
|
|
const px = from.x + dx * t;
|
|
const py = from.y + dy * t;
|
|
const distance = Math.hypot(x - px, y - py);
|
|
if (!best || distance < best.distance) {
|
|
best = { chainage: travelled + span * t, distance };
|
|
}
|
|
travelled += span;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/** 3D 뷰포트에 노선 우클릭 구조물 배치 메뉴를 붙인다(뷰어 수명과 같다). */
|
|
export function mountViewerStructureMenu(deps: ViewerMenuDeps): void {
|
|
const menu = createMapContextMenu("b05-profile-chart");
|
|
deps.root.append(menu.element);
|
|
|
|
deps.root.addEventListener("contextmenu", (event) => {
|
|
if (menu.contains(event.target)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
const types = deps.structureTypes();
|
|
if (!types.length) return;
|
|
const model = deps.modelPointAt(event.clientX, event.clientY);
|
|
if (!model) return;
|
|
const hit = projectToRoute(deps.routePoints(), model.x, model.y);
|
|
// 노선에서 먼 자리는 브라우저 기본 메뉴를 그대로 둔다 — 배치 대상이 아니다.
|
|
if (!hit || hit.distance > ROUTE_PICK_DISTANCE_M) return;
|
|
event.preventDefault();
|
|
const chainage = Number(hit.chainage.toFixed(2));
|
|
const rect = deps.root.getBoundingClientRect();
|
|
menu.open(
|
|
event.clientX - rect.left,
|
|
event.clientY - rect.top,
|
|
structureGroupMenuItems(types, (typeId) => deps.onPick(chainage, typeId)),
|
|
);
|
|
});
|
|
}
|