feat: 스테이션바 측점 정확도 개선 + 영상 컨트롤 UI 통합 + 종합 기술문서
스테이션바/측점(체이니지) - 역 재진입 마커 누락 수정: 측점기준 탐지에 좌표근접(pxPassesTo) 합집합 보강(중복 24px 제거) - kmExists 판정: 좌표 반경만으로 잡힌 통과는 측점값 라벨/검색에서 제외(마커 동그라미는 유지) - 측점값 라벨 겹침 숨김(좌측 우선), 동일항목 브래킷 클러스터 - 측점 검색 순환(통과방향 다음→끝에서 처음), 마커 기준 후보 산출, 입력값 유지/blur 삭제 - 연속 체이니지 수직투영 유틸 분리(client/src/utils/chainage.ts), Minimap 컴포넌트 추가 영상 컨트롤 UI - 선형/드론궤적/좌측패널 토글을 재생바로 통합(settingsStore 공유·persist) - 드론 GPS·고도 HUD 상시 표시, 경로표고·투명도 항상 노출, 겹침제외 버튼 숨김 - 재생바 요소 재배치(좌측패널→선형→드론궤적→드론위치정보→시설등급), 활성색 amber 통일 문서/도구 - 종합 기술명세서(인계+특허 기초) md/html/pdf 3종, UI개선 보고서 3종 - md→html→pdf 변환 스크립트(scripts/md2docs.sh) + 스타일(docs/assets/report.css) - 작업 히스토리 다수 추가(docs/history/) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../utils/geoProjection';
|
||||
import { useGeoStore } from '../../store/geoStore';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { Minimap } from './Minimap';
|
||||
import type { GeoPoint, CenterlinePoint, PoiOverrideMap, RouteStructure } from '../../types/geo';
|
||||
|
||||
const VIDEO_FPS = 30000 / 1001;
|
||||
@@ -75,6 +76,8 @@ interface Props {
|
||||
showPanel?: boolean;
|
||||
/** 카메라 파라미터 패널 top(px) — 노선 배너 아래로 배치. 기본 72. */
|
||||
topPx?: number;
|
||||
/** 하단 스테이션바 높이(px) — 우측 버튼을 좌측 패널과 같은 높이(bottom)에 맞추기 위함. */
|
||||
barHeight?: number;
|
||||
/** 부드러운 단조보간 재생시간(초) ref. VideoPlayer smoothTimeRef 전달 시
|
||||
* 라벨이 60fps로 매끄럽게 이동(일시정지·시크·배속 보정 포함). 없으면 prop 기반 추정. */
|
||||
timeRef?: React.MutableRefObject<number>;
|
||||
@@ -107,10 +110,10 @@ const DISPLAY_DEFAULTS = {
|
||||
emaAlpha: 1.0,
|
||||
smoothMinAlpha: 0.12, // 정지/떨림 시 최소 추종(작을수록 강한 평활)
|
||||
smoothSpeedRef: 0.010, // 즉시추종 기준 속도(클수록 더 강하게 평활)
|
||||
maxPoiRange: 300,
|
||||
maxPoiRange: 1000,
|
||||
// false = 지면고도 기준(POI를 지면고도 z에 고정 → gap=드론고도−지면고도 자동변동, 드론 오르내려도 지물에 붙음).
|
||||
// true = 드론−이격거리 고정(gap 상수). 드론 고도 변동 시 부정확 → 기본 off.
|
||||
poiDroneHeight: false,
|
||||
// true = 드론−이격거리 고정(gap 상수). 드론 고도 변동 시 부정확.
|
||||
poiDroneHeight: true,
|
||||
droneHeightDrop: 24,
|
||||
};
|
||||
|
||||
@@ -229,7 +232,7 @@ function ParamRow({ label, value, min, max, step, unit, decimals = 1, onChange,
|
||||
|
||||
// ── 메인 컴포넌트 ─────────────────────────────────────────────────────────────
|
||||
|
||||
export default function StationOverlay({ currentFrame, currentTime, fps, visible, videoReady = true, videoWidth = 0, videoHeight = 0, onTogglePlay, showPanel = true, topPx = 72, timeRef }: Props) {
|
||||
export default function StationOverlay({ currentFrame, currentTime, fps, visible, videoReady = true, videoWidth = 0, videoHeight = 0, onTogglePlay, showPanel = true, topPx = 72, barHeight = 0, timeRef }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvasSizeRef = useRef({ w: 0, h: 0 });
|
||||
|
||||
@@ -262,6 +265,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
const renderCacheRef = useRef<RenderCache | null>(null);
|
||||
// 나침반 전용(측점선 visible 무관 항상 갱신·표시)
|
||||
const compassRef = useRef<{ effectiveYaw: number; hFovRad: number } | null>(null);
|
||||
// 나침반 미니맵(DOM) root — RAF 에서 --rot(=-yaw) 갱신. minimapRotRef=누적 회전(360° 언랩).
|
||||
const minimapRef = useRef<HTMLDivElement>(null);
|
||||
const minimapRotRef = useRef(0);
|
||||
|
||||
// UI state
|
||||
const [params, setParams] = useState<CameraParams>(DEFAULT_CAMERA_PARAMS);
|
||||
@@ -279,24 +285,28 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
// POI 표시 범위(m) — 드론 위치와의 수평 직선거리(distH, 고도차 무시). 이 범위 안이면 표시,
|
||||
// 밖이면 제외. (이전: 진행방향 앞/옆 비등방 → 단일 반경으로 통합.) 패널에서 실시간 조절.
|
||||
const [maxPoiRange, setMaxPoiRange] = useState(DISPLAY_DEFAULTS.maxPoiRange);
|
||||
const maxPoiRangeRef = useRef(300);
|
||||
const maxPoiRangeRef = useRef(DISPLAY_DEFAULTS.maxPoiRange);
|
||||
// 비교 테스트: POI 높이를 선로표고(false) 대신 '드론 고도 − N미터'(true)로. N=droneHeightDrop.
|
||||
const [poiDroneHeight, setPoiDroneHeight] = useState(DISPLAY_DEFAULTS.poiDroneHeight);
|
||||
const [droneHeightDrop, setDroneHeightDrop] = useState(DISPLAY_DEFAULTS.droneHeightDrop);
|
||||
// 드론 경로 가시화 — 드론의 GPS 경로(lat/lon)를 지정 표고 z 에 투영해 선으로 표시.
|
||||
const [showDronePath, setShowDronePath] = useState(DISPLAY_DEFAULTS.showDronePath);
|
||||
// 선형/드론궤적 토글은 설정 스토어로 공유 → VideoPlayer 하단 바 버튼에서도 제어.
|
||||
const showDronePath = useSettingsStore(s => s.showDronePath);
|
||||
const setShowDronePath = useSettingsStore(s => s.setShowDronePath);
|
||||
const [dronePathZ, setDronePathZ] = useState(DISPLAY_DEFAULTS.dronePathZ); // 기본 경로 표고
|
||||
const showDronePathRef = useRef(true);
|
||||
const dronePathZRef = useRef(78);
|
||||
useEffect(() => { showDronePathRef.current = showDronePath; }, [showDronePath]);
|
||||
useEffect(() => { dronePathZRef.current = dronePathZ; }, [dronePathZ]);
|
||||
// 선형(중심선) 독립 토글 + 드론경로 투명도(0~1)
|
||||
const [showCenterline, setShowCenterline] = useState(DISPLAY_DEFAULTS.showCenterline);
|
||||
// 선형(중심선) 독립 토글 + 드론경로 투명도(0~1) — 토글은 설정 스토어 공유(바 버튼 제어).
|
||||
const showCenterline = useSettingsStore(s => s.showCenterline);
|
||||
const setShowCenterline = useSettingsStore(s => s.setShowCenterline);
|
||||
const showCenterlineRef = useRef(true);
|
||||
useEffect(() => { showCenterlineRef.current = showCenterline; }, [showCenterline]);
|
||||
const [dronePathAlpha, setDronePathAlpha] = useState(DISPLAY_DEFAULTS.dronePathAlpha);
|
||||
const dronePathAlphaRef = useRef(0.75);
|
||||
useEffect(() => { dronePathAlphaRef.current = dronePathAlpha; }, [dronePathAlpha]);
|
||||
// 측점 진단 토글 버튼은 삭제됨. (HUD 표시는 VideoPlayer 에서 showStationDiag 구독)
|
||||
// POI 위치 편집(드래그) 모드
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const editModeRef = useRef(false);
|
||||
@@ -442,6 +452,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
const poiOverlapExclude = useSettingsStore(s => s.poiOverlapExclude);
|
||||
const overlapExcludeRef = useRef(true);
|
||||
useEffect(() => { overlapExcludeRef.current = poiOverlapExclude; }, [poiOverlapExclude]);
|
||||
// 좌측 노선 패널(RoutePanel) 표시 토글은 하단 재생바(VideoPlayer)로 이동.
|
||||
const storeBaseName = useGeoStore(s => s.baseName);
|
||||
const storePoiOverrides = useGeoStore(s => s.poiOverrides);
|
||||
const setPoiOverride = useGeoStore(s => s.setPoiOverride);
|
||||
@@ -798,7 +809,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
}
|
||||
};
|
||||
|
||||
requestIdleCallback(step, { timeout: 200 });
|
||||
// 첫 청크(현재 프레임 포함)는 동기 실행 → 토글/재계산이 '정지 중에도' 즉시 반영.
|
||||
// (idle 대기 시 정지 상태에서 갱신이 지연/누락되던 문제 해결.) 나머지는 step 내부에서 idle 로 이어감.
|
||||
step();
|
||||
}, [nearestCL]);
|
||||
|
||||
// 모든 데이터 로드 완료 시 사전 계산 시작
|
||||
@@ -936,6 +949,27 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
: currentDroneFrameRef.current!);
|
||||
const lines = buildLines(dronePose);
|
||||
|
||||
// 나침반 미니맵(heading-up) 회전 — 영상과 즉시 동기. dronePose.yaw 는 ±smoothHalf(기본 60fr ≈2s)
|
||||
// 평활이라 회전 시 지연 → 가벼운 평활(±3fr)의 yaw 를 따로 보간해 사용. 360° 누적 언랩(점프 방지).
|
||||
if (minimapRef.current) {
|
||||
const arr = allDroneFramesRef.current;
|
||||
let rawYaw = dronePose.yaw;
|
||||
if (arr.length) {
|
||||
let lo = 0, hi = arr.length - 1;
|
||||
while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < estFrame) lo = m + 1; else hi = m; }
|
||||
const i2 = lo, i1 = Math.max(0, lo - 1);
|
||||
const f1 = arr[i1].frame, f2 = arr[i2].frame;
|
||||
const frac = f2 > f1 ? Math.min(1, Math.max(0, (estFrame - f1) / (f2 - f1))) : 0;
|
||||
const a = smoothFrame(arr, i1, 3), b = smoothFrame(arr, i2, 3); // ±3fr 가벼운 평활
|
||||
let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180;
|
||||
rawYaw = a.yaw + dy * frac;
|
||||
}
|
||||
const target = -(rawYaw + paramsRef.current.yawOffset);
|
||||
const delta = (((target - minimapRotRef.current) % 360) + 540) % 360 - 180;
|
||||
minimapRotRef.current += delta;
|
||||
minimapRef.current.style.setProperty('--rot', `${minimapRotRef.current}deg`);
|
||||
}
|
||||
|
||||
// 선로 중심선 (선형) — 독립 토글
|
||||
if (showCenterlineRef.current && lines.centerlineSegs.length > 0) {
|
||||
ctx.strokeStyle = 'rgba(255,50,50,0.85)';
|
||||
@@ -996,7 +1030,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
const seenSt = new Set<string>();
|
||||
const seenPoi = new Set<string>();
|
||||
|
||||
// 측점 라벨 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA
|
||||
// 측점 라벨 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA.
|
||||
// 선형(중심선) 토글과 함께 ON/OFF (빨간 스테이션 라벨 = 선형의 일부).
|
||||
if (showCenterlineRef.current) {
|
||||
ctx.font = 'bold 18px monospace';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
@@ -1028,6 +1064,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
const tw = ctx.measureText(cleanTitle(stA.title)).width;
|
||||
hitBoxes.push({ kind: 'station', title: stA.title, x0: x - 8, y0: y - 12, x1: lx + tw + 2, y1: y + 12 });
|
||||
});
|
||||
}
|
||||
|
||||
// POI 마커 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA
|
||||
ctx.font = 'bold 20px sans-serif';
|
||||
@@ -1102,37 +1139,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
|
||||
}
|
||||
|
||||
// 나침반 HUD — 우측 상단, 측점선 토글과 무관하게 항상 표시
|
||||
const compass = compassRef.current;
|
||||
if (compass) {
|
||||
const r = 52;
|
||||
const cx = W - (r + 14), cy = 16 + r;
|
||||
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
|
||||
ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
for (const [label, deg] of [['N',0],['E',90],['S',180],['W',270]] as const) {
|
||||
const rad = (deg-90)*Math.PI/180;
|
||||
ctx.fillStyle = label==='N' ? '#ff6060' : 'rgba(255,255,255,0.5)';
|
||||
ctx.fillText(label, cx+Math.cos(rad)*(r-9), cy+Math.sin(rad)*(r-9));
|
||||
}
|
||||
const yawRad = (compass.effectiveYaw-90)*Math.PI/180;
|
||||
const tx = cx+Math.cos(yawRad)*(r-14), ty = cy+Math.sin(yawRad)*(r-14);
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(tx, ty);
|
||||
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2.5; ctx.stroke();
|
||||
const ha = 0.42, hl = 9;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad-ha), ty-hl*Math.sin(yawRad-ha));
|
||||
ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad+ha), ty-hl*Math.sin(yawRad+ha));
|
||||
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r-2, yawRad-compass.hFovRad/2, yawRad+compass.hFovRad/2); ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(255,215,0,0.12)'; ctx.fill();
|
||||
ctx.strokeStyle = 'rgba(255,215,0,0.35)'; ctx.lineWidth = 1; ctx.stroke();
|
||||
ctx.font = '9px monospace'; ctx.textBaseline = 'top'; ctx.fillStyle = '#ffd700';
|
||||
ctx.fillText(`${((compass.effectiveYaw+360)%360).toFixed(1)}°`, cx, cy+r+2);
|
||||
ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
|
||||
}
|
||||
|
||||
|
||||
// 라벨 속성 팝업 위치 — 해당 라벨의 현재 표시좌표를 따라 매 프레임 갱신(재생 중 이동).
|
||||
// 라벨이 화면에서 사라지면(필터/화면밖) 팝업 숨김, 다시 보이면 표시.
|
||||
@@ -1155,15 +1162,14 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
popupMissRef.current.set(id, 0);
|
||||
const ph = el.offsetHeight || 120;
|
||||
const pw = el.offsetWidth || 240;
|
||||
// 팝업은 항상 라벨 '아래'. 라벨 위로는 띄우지 않는다.
|
||||
// 화면 하단을 넘칠 만큼 라벨이 내려가면(곧 사라지는 중) → 위로 올리지 않고 팝업을 숨김.
|
||||
const GAP = 26;
|
||||
// 기본은 라벨 '아래'. 아래 공간이 부족하면(화면 하단 근접) 라벨 '위'로 플립 →
|
||||
// 라벨과 겹치거나 화면 밖으로 잘리는 것을 방지. 위로도 부족하면 화면 안으로 클램프.
|
||||
const GAP = 16;
|
||||
const tx = Math.min(Math.max(8, sx - 10), Math.max(8, W - pw - 4));
|
||||
const ty = sy + GAP;
|
||||
let ty = sy + GAP; // 아래
|
||||
if (ty + ph > H - 2) {
|
||||
// 라벨이 화면 하단에 근접 → 아래로 못 펼침. 라벨 위로 올리지 않고 숨김(라벨과 함께 사라짐).
|
||||
el.style.visibility = 'hidden';
|
||||
return;
|
||||
const aboveTy = sy - GAP - ph; // 위로 플립 (팝업 하단이 라벨 위 ~GAP 지점)
|
||||
ty = aboveTy >= 2 ? aboveTy : Math.max(2, H - 2 - ph);
|
||||
}
|
||||
el.style.visibility = 'visible';
|
||||
const pp = popupPosRef.current.get(id);
|
||||
@@ -1435,6 +1441,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
onPointerCancel={onPoiPointerUp}
|
||||
/>
|
||||
|
||||
{/* 나침반 미니맵 — 우측 상단(기존 캔버스 나침반 대체). 핀은 RAF 가 드론 방위로 회전. */}
|
||||
{geoDataLoaded && <Minimap ref={minimapRef} />}
|
||||
|
||||
{/* 라벨 속성 팝업(다중) — 위치는 RAF 가 라벨을 따라 갱신. DOM 이라 텍스트 선명.
|
||||
컴팩트(기본 5필드) ↔ 전체 토글은 팝업 클릭. 자동(auto) 팝업은 ✕ 없음(가시 구조물 따라 표시). */}
|
||||
{infoPopups.map(pp => {
|
||||
@@ -1475,37 +1484,29 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
})}
|
||||
|
||||
{showPanel && (
|
||||
<div className="absolute right-2 z-30 flex flex-col items-end gap-1" style={{ top: Math.max(topPx, 150) }}>
|
||||
<div className="flex gap-1">
|
||||
<div className="absolute right-2 z-30 flex flex-col-reverse items-end gap-2" style={{ bottom: (barHeight || 130) + 8 }}>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setShowDisplay(v => !v)}
|
||||
className={`text-[10px] whitespace-nowrap px-2 py-1 rounded border shadow ${showDisplay ? 'bg-sky-700/90 border-sky-400 text-white' : 'bg-black/70 hover:bg-black/90 text-gray-200 border-gray-500'}`}>
|
||||
className={`text-xs whitespace-nowrap px-2 py-1 rounded border ${showDisplay ? 'bg-amber-400 border-amber-300 text-black' : 'bg-black/70 hover:bg-black/90 text-gray-300 border-gray-600'}`}>
|
||||
{showDisplay ? '▲ 화면표시 옵션' : '▼ 화면표시 옵션'}
|
||||
</button>
|
||||
<button onClick={() => setShowControls(v => !v)}
|
||||
className={`text-[10px] whitespace-nowrap px-2 py-1 rounded border shadow ${showControls ? 'bg-gray-700/90 border-gray-400 text-white' : 'bg-black/70 hover:bg-black/90 text-gray-200 border-gray-500'}`}>
|
||||
className={`text-xs whitespace-nowrap px-2 py-1 rounded border ${showControls ? 'bg-amber-400 border-amber-300 text-black' : 'bg-black/70 hover:bg-black/90 text-gray-300 border-gray-600'}`}>
|
||||
{showControls ? '▲ 카메라 파라미터' : '▼ 카메라 파라미터'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 두 패널을 가로로 나란히(우→좌) 펼침 — 둘 다 열어도 세로로 안 쌓여 화면 위로 안 넘침. */}
|
||||
<div className="flex flex-row-reverse items-end gap-2">
|
||||
{/* ── 화면표시 옵션 (카메라 외 모든 표시 설정) ── */}
|
||||
{showDisplay && (
|
||||
<div className="bg-black/90 border border-sky-700 rounded p-3 text-white w-72 select-none max-h-[80vh] overflow-y-auto shadow-xl">
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5 border-b border-gray-700 pb-2 mb-2">표시 토글</div>
|
||||
<div className="space-y-2 mb-3">
|
||||
<button onClick={() => setShowCenterline(v => !v)}
|
||||
className={`w-full text-[11px] px-2 py-1 rounded border transition-colors ${showCenterline ? 'bg-red-500/80 border-red-300 text-black font-bold' : 'bg-black/60 border-gray-600 text-gray-200 hover:border-gray-400'}`}>
|
||||
{showCenterline ? '■ 선형(중심선) 표시중' : '▶ 선형(중심선) 표시'}
|
||||
</button>
|
||||
<button onClick={() => setShowDronePath(v => !v)}
|
||||
className={`w-full text-[11px] px-2 py-1 rounded border transition-colors ${showDronePath ? 'bg-sky-500/80 border-sky-300 text-black font-bold' : 'bg-black/60 border-gray-600 text-gray-200 hover:border-gray-400'}`}>
|
||||
{showDronePath ? '■ 드론 궤적 표시중' : '▶ 드론 궤적 표시'}
|
||||
</button>
|
||||
{showDronePath && (
|
||||
<>
|
||||
<ParamRow label="경로 표고" tip={`GPS 측정점(노란 점)을 표고 ${dronePathZ}m 에 투영·연결`} value={dronePathZ} min={-10} max={pathZMax} step={1} unit="m" decimals={0} onChange={v => setDronePathZ(Math.round(v))} />
|
||||
<ParamRow label="투명도" tip={`드론 궤적 선/점 투명도 α=${dronePathAlpha.toFixed(2)}`} value={dronePathAlpha} min={0.1} max={1.0} step={0.05} unit="" decimals={2} onChange={v => setDronePathAlpha(v)} />
|
||||
</>
|
||||
)}
|
||||
{/* 선형/드론궤적/좌측패널 토글은 하단 재생바(VideoPlayer)로 이동. 측점 진단 버튼은 삭제. */}
|
||||
{/* 경로표고·투명도는 드론궤적 ON/OFF 무관하게 항상 표시. */}
|
||||
<ParamRow label="경로 표고" tip={`GPS 측정점(노란 점)을 표고 ${dronePathZ}m 에 투영·연결`} value={dronePathZ} min={-10} max={pathZMax} step={1} unit="m" decimals={0} onChange={v => setDronePathZ(Math.round(v))} />
|
||||
<ParamRow label="투명도" tip={`드론 궤적 선/점 투명도 α=${dronePathAlpha.toFixed(2)}`} value={dronePathAlpha} min={0.1} max={1.0} step={0.05} unit="" decimals={2} onChange={v => setDronePathAlpha(v)} />
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5 border-t border-gray-700 pt-2">스무딩 <span className="text-gray-600">(재계산 500ms 후)</span></div>
|
||||
<div className="mb-3 space-y-2">
|
||||
@@ -1630,6 +1631,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user