diff --git a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts index 76fb15cf..51434aca 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts @@ -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)가 클릭 즉시 따라오도록 목록을 다시 그린다(토글 해제 포함). diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts index ce48042c..253472ca 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts @@ -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(); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts index b39e65df..c1f95047 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts @@ -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; }, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index 30ef0f5a..c0977526 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -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, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_TableOverlay.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_TableOverlay.ts index c3a1510b..8dd8c06f 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_TableOverlay.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_TableOverlay.ts @@ -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`; }