From 7a4326e339ba51bca4f1bdeb94e3ff08b37a04f4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 6 Aug 2026 18:02:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=203D=20=EB=A7=88=EC=BB=A4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=E2=80=94=20=EC=B8=A1=EA=B5=AC=20=EB=9E=A8?= =?UTF-8?q?=ED=94=84=20=EC=B6=95=EC=86=8C=C2=B7=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EB=9D=A0=20=EA=B0=80=EC=8B=9C=EC=84=B1=C2=B7=ED=9A=8C=EC=A0=84?= =?UTF-8?q?=20=EC=A1=B0=EC=A4=80=EC=A0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 보고 3건(2026-08-06) 수정: 1. 측구 방향 램프 절반 축소(반지름 1.1→0.55, 띄움 0.6→0.3) — 측점 고를 때 가리는 문제. 클릭 판정은 투명 히트 구(1.1)로 기존 범위 유지 2. 측점선을 Line → 납작 띠 Mesh(BoxGeometry 길이×0.18×0.5, 띄움 0.45→0.8) — WebGL Line은 굵기 지정이 안 먹혀 1px라 지형에 묻혀 안 보이고 클릭도 어려웠음. 띠는 굵게 보이고 레이캐스트도 몸통 전체. selectStation 색 갱신을 Mesh 기준으로 전환 3. 회전 중심 불투명 구슬 → 링+십자 조준점 스프라이트(캔버스 텍스처, 항상 카메라 정면, 반투명). 비율 0.02 — 구슬(0.024)보다 지름 작고 속이 비어 시각적 무게 대폭 감소. 공용 유틸이라 B04 지형 뷰어들도 함께 적용 실측 확인: 띠 클릭 선택 정상(빨간 핀), 램프 축소 확인, 조준점 지형 위 또렷, 콘솔 에러 0. Co-Authored-By: Claude Fable 5 --- B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts | 65 ++++++++++++++++---- B05_wf2_Route/B05_wf2_Route_UI_Markers.ts | 42 +++++++++---- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts index 2b5f06b5..9c0bb402 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts @@ -60,9 +60,52 @@ const POLAR_EPSILON = 0.02; const POINT_PICK_RATIO = 0.01; /** 휠 한 칸당 배율. 휠을 위로 올리면 이 값의 역수만큼 멀어진다(2026-08-01 사용자 지시). */ const ZOOM_STEP = 0.9; -/** 회전 중심 구슬의 화면상 크기 비율(카메라 거리 대비). 멀어져도 같은 크기로 보인다. */ -const PIVOT_MARKER_RATIO = 0.012; -const PIVOT_MARKER_COLOR = 0xf59e0b; +/** 회전 중심 조준점의 화면상 크기 비율(카메라 거리 대비). 멀어져도 같은 크기로 보인다. + * 예전 불투명 구슬(지름 = 거리×0.024)이 크고 투박하다는 보고(2026-08-06)로 얇은 링 + * 조준점으로 교체. 0.012(구슬의 절반 폭)는 링이 뭉개져 안 보였다 — 0.02면 화면에서 + * 약 15px: 구슬보다 지름은 약간 작고, 속이 빈 링이라 시각적 무게는 훨씬 가볍다. */ +const PIVOT_MARKER_RATIO = 0.02; +const PIVOT_MARKER_COLOR = "rgba(245, 158, 11, 0.95)"; + +/** 링+십자+중심점 조준점 텍스처 — 구슬 대신 축 위치만 가볍게 표시한다(2026-08-06). */ +function pivotReticleTexture(): THREE.CanvasTexture { + const size = 96; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + const context = canvas.getContext("2d"); + if (context) { + const center = size / 2; + context.strokeStyle = PIVOT_MARKER_COLOR; + context.fillStyle = PIVOT_MARKER_COLOR; + context.lineWidth = 6; + context.beginPath(); + context.arc(center, center, size * 0.3, 0, Math.PI * 2); + context.stroke(); + // 링 안팎으로 걸치는 십자 4획 — 조준점임을 또렷하게. + const inner = size * 0.16; + const outer = size * 0.47; + ( + [ + [1, 0], + [-1, 0], + [0, 1], + [0, -1], + ] as const + ).forEach(([dx, dy]) => { + context.beginPath(); + context.moveTo(center + dx * inner, center + dy * inner); + context.lineTo(center + dx * outer, center + dy * outer); + context.stroke(); + }); + context.beginPath(); + context.arc(center, center, 5, 0, Math.PI * 2); + context.fill(); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} export interface CursorPivotOptions { camera: THREE.PerspectiveCamera; @@ -86,12 +129,12 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void // 가운데 버튼 드래그 = 화면 이동(전역 공통). 기본값(DOLLY)은 휠 줌과 겹쳐 쓸모가 없다. controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN; - // 회전 중심 구슬 — 돌리는 동안에만 보인다. 화면상 크기는 거리와 무관하게 일정하다. + // 회전 중심 조준점 — 돌리는 동안에만 보인다. 항상 카메라를 향하는 스프라이트라 + // 어느 각도에서도 또렷하고, 화면상 크기는 거리와 무관하게 일정하다. const pivotMarker = options.scene - ? new THREE.Mesh( - new THREE.SphereGeometry(1, 16, 12), - new THREE.MeshBasicMaterial({ - color: PIVOT_MARKER_COLOR, + ? new THREE.Sprite( + new THREE.SpriteMaterial({ + map: pivotReticleTexture(), // 지형에 묻혀 안 보이면 축을 확인할 수 없으므로 항상 위에 그린다. depthTest: false, transparent: true, @@ -105,7 +148,7 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void options.scene.add(pivotMarker); } - /** 구슬을 현재 축 위치·크기로 맞춘다. */ + /** 조준점을 현재 축 위치·크기로 맞춘다. 스프라이트 scale = 쿼드의 월드 폭. */ function syncPivotMarker(): void { if (!pivotMarker || !pivotMarker.visible) return; pivotMarker.position.copy(pivot); @@ -246,8 +289,8 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void element.removeEventListener("wheel", onWheel); if (pivotMarker) { pivotMarker.removeFromParent(); - pivotMarker.geometry.dispose(); - (pivotMarker.material as THREE.Material).dispose(); + pivotMarker.material.map?.dispose(); + pivotMarker.material.dispose(); } }; } diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts index 253472ca..6e464f3e 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Markers.ts @@ -335,7 +335,8 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou stations.forEach((station) => { if (station.center_z === null) return; const [leftX, leftY] = station.frame.left_xy; - const center = { x: station.center_x, y: station.center_y, z: station.center_z + 0.45 }; + // 띄움 0.8 — 예전 0.45는 굴곡진 지형에 절반쯤 묻혀 잘 안 보였다(2026-08-06 보고). + const center = { x: station.center_x, y: station.center_y, z: station.center_z + 0.8 }; stationCenters.set(station.station_id, modelToScene(center, bounds)); const points = [ modelToScene( @@ -348,13 +349,19 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou ), ]; const selected = station.station_id === selectedStationId; - const line = new THREE.Line( - new THREE.BufferGeometry().setFromPoints(points), - new THREE.LineBasicMaterial({ color: selected ? 0xef4444 : 0xfacc15 }), + // 측점선은 선(Line)이 아니라 **납작한 띠(Mesh)**로 그린다 — WebGL에서 Line은 + // 굵기 지정이 안 먹혀 항상 1px라 안 보이고, 레이캐스트도 얇아 클릭이 어려웠다 + // (2026-08-06 보고). 띠는 굵게 보이고 클릭 판정도 몸통 전체다. + const barLength = points[0].distanceTo(points[1]); + const bar = new THREE.Mesh( + new THREE.BoxGeometry(barLength, 0.18, 0.5), + new THREE.MeshBasicMaterial({ color: selected ? 0xef4444 : 0xfacc15 }), ); - line.userData.stationId = station.station_id; - if (selected) line.material.linewidth = 2; - stationGroup.add(line); + bar.position.lerpVectors(points[0], points[1], 0.5); + const direction = points[1].clone().sub(points[0]); + bar.rotation.y = Math.atan2(-direction.z, direction.x); + bar.userData.stationId = station.station_id; + stationGroup.add(bar); // 측점 바 양 끝 원형 램프: 상단(등고 높은 쪽) 예상측 컬러, 반대측 회색. // 클릭하면 그 측을 상단측(=측구 방향)으로 지정한다(onUphillPick). @@ -365,17 +372,27 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou (["left", "right"] as const).forEach((side, endIndex) => { const active = station.uphill_side === side; + // 램프 절반 축소(1.1→0.55) — 측점 고를 때 가리는 문제(2026-08-06 보고). const lamp = new THREE.Mesh( - new THREE.SphereGeometry(1.1, 14, 10), + new THREE.SphereGeometry(0.55, 14, 10), new THREE.MeshBasicMaterial({ color: active ? UPHILL_LAMP_COLOR : UPHILL_LAMP_INACTIVE_COLOR, }), ); lamp.position.copy(points[endIndex]); - lamp.position.y += 0.6; + lamp.position.y += 0.3; lamp.userData.uphillStationId = station.station_id; lamp.userData.uphillSide = side; stationGroup.add(lamp); + // 클릭 판정은 예전 크기 그대로 — 투명 히트 구를 겹쳐 "작게 보여도 잘 눌리게". + const lampHit = new THREE.Mesh( + new THREE.SphereGeometry(1.1, 8, 6), + new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }), + ); + lampHit.position.copy(lamp.position); + lampHit.userData.uphillStationId = station.station_id; + lampHit.userData.uphillSide = side; + stationGroup.add(lampHit); }); }); // 재렌더로 좌표가 갱신됐으니 선택 핀도 그 자리로 다시 놓는다. @@ -413,11 +430,10 @@ export function createRouteMarkers(scene: THREE.Scene, getBounds: () => ModelBou function selectStation(stationId: string | null): void { selectedStationId = stationId; stationGroup.children.forEach((object) => { - if (!(object instanceof THREE.Line)) return; + // 측점 띠(stationId 보유 Mesh)만 색을 바꾼다 — 램프(uphill*)는 제외. + if (!(object instanceof THREE.Mesh) || typeof object.userData.stationId !== "string") return; const selected = object.userData.stationId === selectedStationId; - const material = object.material as THREE.LineBasicMaterial; - material.color.set(selected ? 0xef4444 : 0xfacc15); - material.linewidth = selected ? 3 : 1; + (object.material as THREE.MeshBasicMaterial).color.set(selected ? 0xef4444 : 0xfacc15); }); syncSelectionPin(); stationSelectionListener?.(selectedStationId);