From 75232b9f73465ece58fccd74de3f5d9a1ffe4f2a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 5 Aug 2026 19:21:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=203D=20=EC=84=A0=ED=83=9D=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=C2=B7=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=ED=8F=BC=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=C2=B7=ED=8C=A8=EB=84=90=20=EC=86=90=EC=9E=A1?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=95=EB=A0=AC=C2=B7=EC=A2=85=EB=8B=A8=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=EB=86=92=EC=9D=B4=20=ED=99=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3D 빈 공간 클릭(회전 드래그 포함)으로 측점 선택이 풀리지 않게 — 해제는 그래프·사이드 패널에서만 (3D 영역 한정, 2026-08-05 사용자 지시) - 구조물 폼: 편집 중 타입 변경 시 신규 추가로 전환(입력값 유지), 자동 배관(origin=pipe)은 타입 잠금(관종·직경만 변경 가능), 리셋 시 폼 초기화 - 접힌 서브패널 손잡이는 열린 패널의 위 경계 수평선에 일렬 배치 (테이블 접힘+유토 펼침 시 유토곡선 상단) - 종단 그래프 최소높이(100px) 부족 시 메인 하단 패널 높이를 자동 확장 (뷰포트 92% 상한) — 서브패널 펼침으로 종단이 뒤로 넘어가는 역전 방지 Co-Authored-By: Claude Fable 5 --- .../B05_wf2_Route_UI_IrregularStations.ts | 15 ++++++++++++++- B05_wf2_Route/B05_wf2_Route_UI_Markers.ts | 5 +++-- .../B05_wf2_Route_UI_Profile_MassHaul.ts | 15 ++++++++++++--- B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts | 13 +++++++++++++ .../B05_wf2_Route_UI_Profile_TableOverlay.ts | 5 +++-- 5 files changed, 45 insertions(+), 8 deletions(-) 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`; }