fix(B05): 3D 선택 유지·구조물 폼 규칙·패널 손잡이 정렬·종단 최소높이 확보
- 3D 빈 공간 클릭(회전 드래그 포함)으로 측점 선택이 풀리지 않게 — 해제는 그래프·사이드 패널에서만 (3D 영역 한정, 2026-08-05 사용자 지시) - 구조물 폼: 편집 중 타입 변경 시 신규 추가로 전환(입력값 유지), 자동 배관(origin=pipe)은 타입 잠금(관종·직경만 변경 가능), 리셋 시 폼 초기화 - 접힌 서브패널 손잡이는 열린 패널의 위 경계 수평선에 일렬 배치 (테이블 접힘+유토 펼침 시 유토곡선 상단) - 종단 그래프 최소높이(100px) 부족 시 메인 하단 패널 높이를 자동 확장 (뷰포트 92% 상한) — 서브패널 펼침으로 종단이 뒤로 넘어가는 역전 방지 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,17 @@ export function createIrregularStationsSection(
|
||||
nameFieldWrap.hidden = type !== "기타";
|
||||
subRow.hidden = type === "기성막이"; // 기성막이 하위 옵션 미정(추가반영 예정)
|
||||
}
|
||||
typeSelect.addEventListener("change", syncSubOptions);
|
||||
typeSelect.addEventListener("change", () => {
|
||||
syncSubOptions();
|
||||
// 편집 중 구조물 타입을 바꾸면 기존 항목 수정이 아니라 **신규 추가**로 전환한다
|
||||
// (2026-08-05 사용자 지시). 입력값은 그대로 두고 편집 상태만 푼다.
|
||||
if (editingId) {
|
||||
editingId = null;
|
||||
syncButtons();
|
||||
renderList();
|
||||
callbacks.onSelect(null);
|
||||
}
|
||||
});
|
||||
pipeTypeSelect.addEventListener("change", () => syncDiameterOptions());
|
||||
typeSelect.value = STRUCTURE_DEFAULT_TYPE;
|
||||
pipeTypeSelect.value = PIPE_DEFAULT_TYPE;
|
||||
@@ -280,6 +290,9 @@ export function createIrregularStationsSection(
|
||||
nameField.value = target
|
||||
? (target.customName ?? (target.structureType ? "" : target.structure))
|
||||
: "";
|
||||
// 배수유역 자동 배관(정본 투영)은 관종·직경만 바꿀 수 있다 — 타입 자체는 잠근다
|
||||
// (2026-08-05 사용자 지시. 배관을 다른 구조물로 바꾸면 관 지점 정본과 어긋난다).
|
||||
typeSelect.disabled = !!target && isPipeStation(target);
|
||||
syncSubOptions();
|
||||
syncButtons();
|
||||
// 선택 강조(is-selected)가 클릭 즉시 따라오도록 목록을 다시 그린다(토글 해제 포함).
|
||||
|
||||
@@ -453,8 +453,9 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou
|
||||
selectionListener?.(null);
|
||||
return;
|
||||
}
|
||||
// 빈 공간·경로점 클릭: 측점 선택을 해제해 하이라이트·하단 값 열 오버레이를 초기화한다(D-4).
|
||||
if (selectedStationId !== null) selectStation(null);
|
||||
// 3D 빈 공간 클릭으로는 측점 선택을 **해제하지 않는다**(2026-08-05 사용자 지시) —
|
||||
// 회전용 좌클릭 드래그가 클릭으로 인식되어 선택 핀이 풀리는 문제. 해제는 그래프·
|
||||
// 사이드 패널에서만 한다.
|
||||
selectedId =
|
||||
typeof object?.userData.routePointId === "string" ? object.userData.routePointId : null;
|
||||
renderMarkers();
|
||||
|
||||
@@ -128,6 +128,8 @@ export interface RouteMassHaulPanel {
|
||||
/** 오버레이 서브패널 — 호출한 쪽이 `position: relative`인 패널 본문 래퍼에 붙인다. */
|
||||
overlay: HTMLElement;
|
||||
isOpen(): boolean;
|
||||
/** 접힌 손잡이가 앉을 바닥 높이(px) — 열린 테이블 오버레이 위 경계에 일렬로 놓기 위함. */
|
||||
setHandleBase(px: number): void;
|
||||
setContext(next: RouteMassHaulContext | null): void;
|
||||
/** 종단 가로 스크롤러와 scrollLeft를 양방향 동기화한다(설치 1회). */
|
||||
attachScrollSync(main: HTMLElement): void;
|
||||
@@ -226,11 +228,13 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
||||
|
||||
/**
|
||||
* 손잡이를 오버레이 **위 경계**에 태운다(2026-08-04 사용자 지시 — 펼치면 같이 올라와
|
||||
* 테이블 영역을 침범해도 된다). 접혀 있으면 패널 바닥으로 돌아온다. 높이 조절 중에도
|
||||
* 리사이저 onResize → redraw 경로에서 다시 불려 항상 경계를 따라다닌다.
|
||||
* 테이블 영역을 침범해도 된다). 접혀 있으면 열린 서브패널 더미(테이블 오버레이)의
|
||||
* 위 경계(handleBase)로 올라간다 — 접힌 버튼들이 열린 패널 수평선에 일렬로 놓이도록
|
||||
* (2026-08-05 사용자 지시). 아무것도 안 열려 있으면 패널 바닥(0)이다.
|
||||
*/
|
||||
let handleBase = 0;
|
||||
function syncHandlePosition(): void {
|
||||
handle.style.bottom = open ? `${overlay.offsetHeight}px` : "0px";
|
||||
handle.style.bottom = open ? `${overlay.offsetHeight}px` : `${handleBase}px`;
|
||||
}
|
||||
|
||||
function applyOpen(next: boolean): void {
|
||||
@@ -367,6 +371,11 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
||||
handle,
|
||||
overlay,
|
||||
isOpen: () => open,
|
||||
setHandleBase(px) {
|
||||
if (handleBase === px) return;
|
||||
handleBase = px;
|
||||
syncHandlePosition();
|
||||
},
|
||||
setContext(next) {
|
||||
context = next;
|
||||
},
|
||||
|
||||
@@ -492,6 +492,19 @@ export function createRouteProfilePanel(
|
||||
const massHeight = massHaul.overlay.hidden ? 0 : massHaul.overlay.offsetHeight;
|
||||
const tableOverlayHeight = tableOverlay.isOpen() ? tableOverlay.overlay.offsetHeight : 0;
|
||||
tableOverlay.setBottomOffset(massHeight);
|
||||
// 접힌 유토곡선 손잡이는 열린 테이블의 위 경계에 일렬로 앉힌다(2026-08-05 사용자 지시).
|
||||
massHaul.setHandleBase(tableOverlayHeight);
|
||||
// 종단 그래프 최소 높이는 반드시 확보한다 — 서브패널이 갑자기 펼쳐져 자리가 모자라면
|
||||
// 종단이 뒤로 넘어가는 대신 **메인 하단 패널 높이를 키운다**(2026-08-05 사용자 지시).
|
||||
const deficit =
|
||||
MIN_CHART_HEIGHT + massHeight + tableOverlayHeight + MASSHAUL_HANDLE_GUTTER_PX - available;
|
||||
if (deficit > 1) {
|
||||
const grown = Math.min(root.offsetHeight + deficit, Math.round(window.innerHeight * 0.92));
|
||||
if (grown > root.offsetHeight + 1) {
|
||||
root.style.setProperty("--b05-profile-height", `${grown}px`);
|
||||
// ResizeObserver가 새 높이로 draw를 다시 부른다 — 이번 프레임은 그대로 마저 그린다.
|
||||
}
|
||||
}
|
||||
const chartHeight = Math.max(
|
||||
MIN_CHART_HEIGHT,
|
||||
available - massHeight - tableOverlayHeight - MASSHAUL_HANDLE_GUTTER_PX,
|
||||
|
||||
@@ -90,9 +90,10 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
/** 손잡이는 오버레이 위 경계를, 접히면 바닥(+유토곡선 간격)을 따라간다. */
|
||||
/** 손잡이는 오버레이 위 경계를, 접히면 열린 유토곡선의 위 경계(bottomOffset)를 따라간다
|
||||
* — 접힌 버튼이 펼쳐진 패널 수평선에 놓인다(2026-08-05 사용자 지시). */
|
||||
function syncHandlePosition(): void {
|
||||
handle.style.bottom = open ? `${bottomOffset + overlay.offsetHeight}px` : "0px";
|
||||
handle.style.bottom = open ? `${bottomOffset + overlay.offsetHeight}px` : `${bottomOffset}px`;
|
||||
overlay.style.bottom = `${bottomOffset}px`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user