스테이션바 시선 연동(station_lookahead)·팬 지연 보정·1km 측점 마크
- station_lookahead(route.json): 배지 전방 오프셋 — 미설정/0=드론 연직(기존), N=진행방향 +N(m), "auto"=짐벌 피치·AGL 기반 자동. 연속 모델(유효 주시거리 = N − 노선이격 초과분)로 접근 구간에서 멈춤·점프 없이 측점값 연속 진행 - 노선밖 판정도 시선 기준: 화면이 노선을 비추기 시작하면 배지가 측점값으로 조기 전환, '노선밖 ○m' 도 시선 지점 기준 - 시점·시설물 마크와 배지 기준 통일(viewChain): 오프셋 활성 시 물리(드론 연직) 폴백 금지 — 구간 밖 시설물은 해당 세그먼트 바에 미표시(귀덕천교 사례), 세그먼트별 진단 로그(표본 Δ) 추가 - 스테이션바 1km(1+000) 단위 측점 마크: 측점값 라벨만 표시(이름 라벨·그룹핑 제외), 기존 마크와 겹침 시 생략 + 양끝 100m 내 시설물 없으면 시작/끝 스테이션 표시 - 팬(회전) 지연 이질감 보정: 프레임 동기 데이터의 POI 라벨 화면 EMA 우회(즉시 투영) + 자세 선행(attLeadSec) 파라미터 — 짐벌 로그 기록 지연 상쇄(위치는 유지, 자세만 앞선 시점 참조), 카메라 패널 슬라이더(-1~+1s) - 배지 시선 중심 자동 모드: AGL/tan|짐벌피치| 전방 주시거리(상한 350m), 진행방향 = 카메라 yaw·노선 접선 내적 - 기타: sen H 최대 100mm, F2 개발자 모드 토글(직전 커밋 누락분 아님 — 이번 범위) - 데이터 작업(코드 외): 경로 1-2/1-3/2 카메라·화면설정 산출 적용(yawOffset 실측, 경로표고 중앙값) 및 1-3 기존 시각보정 복원 — docs/history 참조 - docs/history: 작업 이력 15건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1013,24 +1013,32 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
}, []);
|
||||
|
||||
// 보간 포즈 — estFrame(연속 프레임번호)에서 앞뒤 smoothFrame 선형보간(yaw는 최단각). RAF에서 호출.
|
||||
// 자세(yaw/pitch/roll)는 attLeadSec(자세 선행) 만큼 앞선 시점에서 별도 샘플 —
|
||||
// 짐벌 로그의 기록 지연으로 팬(회전) 시 오버레이가 늦게 따라오는 이질감 보정.
|
||||
const poseAt = useCallback((estFrame: number): DroneFrameBasic | null => {
|
||||
const arr = allDroneFramesRef.current;
|
||||
if (!arr.length) return null;
|
||||
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 h = poseHalf();
|
||||
const a = smoothFrame(arr, i1, h), b = smoothFrame(arr, i2, h);
|
||||
const L = (x: number, y: number) => x + (y - x) * frac;
|
||||
let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각
|
||||
const sample = (ef: number): { a: DroneFrameBasic; b: DroneFrameBasic; frac: number } => {
|
||||
let lo = 0, hi = arr.length - 1;
|
||||
while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < ef) 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, (ef - f1) / (f2 - f1))) : 0;
|
||||
return { a: smoothFrame(arr, i1, h), b: smoothFrame(arr, i2, h), frac };
|
||||
};
|
||||
const p = sample(estFrame);
|
||||
const leadSec = paramsRef.current.attLeadSec ?? 0;
|
||||
const q = leadSec !== 0 ? sample(estFrame + leadSec * fpsRef.current) : p;
|
||||
const L = (x: number, y: number) => x + (y - x) * p.frac;
|
||||
const A = (x: number, y: number) => x + (y - x) * q.frac;
|
||||
let dy = q.b.yaw - q.a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각
|
||||
return {
|
||||
...a,
|
||||
...p.a,
|
||||
frame: estFrame,
|
||||
lat: L(a.lat, b.lat), lon: L(a.lon, b.lon), altitude: L(a.altitude, b.altitude),
|
||||
pitch: L(a.pitch, b.pitch), roll: L(a.roll, b.roll),
|
||||
yaw: a.yaw + dy * frac,
|
||||
lat: L(p.a.lat, p.b.lat), lon: L(p.a.lon, p.b.lon), altitude: L(p.a.altitude, p.b.altitude),
|
||||
pitch: A(q.a.pitch, q.b.pitch), roll: A(q.a.roll, q.b.roll),
|
||||
yaw: q.a.yaw + dy * q.frac,
|
||||
};
|
||||
}, [smoothFrame, poseHalf]);
|
||||
|
||||
@@ -1522,7 +1530,12 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
if (cc.Zc < CLIP_Z) return;
|
||||
const { pxRaw, pyRaw } = pixelFromCamera(cc, params);
|
||||
const prevDpoi = displayedPoiRef.current.get(poiA.title);
|
||||
const d = smoothStep(prevDpoi, pxRaw, pyRaw, α, smoothMinAlphaRef.current, smoothSpeedRefRef.current);
|
||||
// 프레임 동기(djmd) 데이터는 입력이 매끄러워 화면 EMA 불필요 — 즉시 투영으로
|
||||
// 팬(회전) 시 라벨이 늦게 따라오는 이질감 제거(선형·측점과 동일 타이밍).
|
||||
// 비동기 데이터셋(CSV 지터)만 기존 속도적응 EMA 유지.
|
||||
const d = frameSyncedRef.current
|
||||
? { x: pxRaw, y: pyRaw, rej: 0, vx: 0, vy: 0 }
|
||||
: smoothStep(prevDpoi, pxRaw, pyRaw, α, smoothMinAlphaRef.current, smoothSpeedRefRef.current);
|
||||
seenPoi.add(poiA.title);
|
||||
// 픽셀 히스테리시스: 0.75px 이상 움직일 때만 정수 위치 갱신 → 저속(1배속) 깜빡임 제거.
|
||||
const psx0 = vx(d.x), psy0 = vy(d.y);
|
||||
@@ -2178,6 +2191,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
</div>
|
||||
<div className="space-y-2 mb-3">
|
||||
<ParamRow label="시간동기" tip="영상↔비행로그 시각 오프셋(초). POI 전체가 진행방향으로 밀리면 조정 (+: 라벨을 진행방향 앞으로 / −: 뒤로). '풀 붙임' 정합의 핵심 — camera.json 에 저장됨" value={params.timeOffsetSec ?? 0} min={-2} max={2} step={0.05} unit="s" decimals={2} onChange={v => setParam('timeOffsetSec', v)} />
|
||||
<ParamRow label="자세 선행" tip="짐벌 자세 로그의 기록 지연 보정(초). 영상이 좌우로 팬(회전)할 때 라벨·선형이 늦게 따라오면 +, 앞서가면 −. 위치는 그대로 두고 자세만 앞선 시점을 참조 — camera.json 에 저장됨" value={params.attLeadSec ?? 0} min={-1} max={1} step={0.01} unit="s" decimals={2} onChange={v => setParam('attLeadSec', v)} />
|
||||
<ParamRow label="Yaw ±" value={params.yawOffset} min={-180} max={180} step={0.1} unit="°" decimals={1} onChange={v => setParam('yawOffset', v)} />
|
||||
{/* Yaw 자동 추정(2단계 보정의 1단계) — 대략 맞춘 뒤 'Yaw 보정' 드래그 모드로 마무리 */}
|
||||
<button
|
||||
@@ -2219,7 +2233,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
|
||||
<ParamRow label="cx₀" value={params.cx0} min={-0.5} max={0.5} step={0.005} unit="" decimals={3} onChange={v => setParam('cx0', v)} />
|
||||
<ParamRow label="cy₀" value={params.cy0} min={-0.5} max={0.5} step={0.005} unit="" decimals={3} onChange={v => setParam('cy0', v)} />
|
||||
<ParamRow label="sen W" value={params.sensorW} min={10} max={50} step={0.05} unit="mm" decimals={2} onChange={v => setParam('sensorW', v)} />
|
||||
<ParamRow label="sen H" value={params.sensorH} min={6} max={36} step={0.05} unit="mm" decimals={2} onChange={v => setParam('sensorH', v)} />
|
||||
<ParamRow label="sen H" value={params.sensorH} min={6} max={100} step={0.05} unit="mm" decimals={2} onChange={v => setParam('sensorH', v)} />
|
||||
</div>
|
||||
{panelDroneFrame && (
|
||||
<div className="border-t border-gray-700 pt-2 mb-2 text-[10px] text-gray-400 font-mono space-y-0.5">
|
||||
|
||||
@@ -34,11 +34,16 @@ interface GeoPoint {
|
||||
}
|
||||
|
||||
/** 프레임별 precompute 결과. km=배지용 최근접측점, chain=구간방향용 연속 체이니지,
|
||||
* offM=측점 폴리라인까지 수평거리(m) — 노선 밖(접근/이탈) 판별용. */
|
||||
* offM=측점 폴리라인까지 수평거리(m) — 노선 밖(접근/이탈) 판별용.
|
||||
* viewChain=카메라 시선 중심이 닿는 지면 교점의 체이니지(커서 배지용) —
|
||||
* 드론 연직 체이니지(chain)보다 전방 주시거리(AGL/tan|pitch|)만큼 앞. */
|
||||
interface ViewedPoint {
|
||||
frameNum: number;
|
||||
km: number;
|
||||
chain: number;
|
||||
viewChain: number;
|
||||
/** 배지 표시 지점(시선/오프셋 지점)의 노선 이격(m) — 오프셋 미설정이면 offM 과 동일. */
|
||||
viewOffM: number;
|
||||
offM: number;
|
||||
time: number;
|
||||
}
|
||||
@@ -49,12 +54,11 @@ interface StationLine {
|
||||
k: number; // 경도→m 환산 (cos(lat0)*111000)
|
||||
}
|
||||
|
||||
/** 드론 lat/lon 을 측점 폴리라인에 투영 → 연속 체이니지(m) + 폴리라인까지 수평거리(m).
|
||||
* 100m 양자화된 최근접측점과 달리 전진/후진 전환 시점이 실제 이동과 일치.
|
||||
* distM 은 노선 밖(접근/이탈) 판별에 사용 — 밖에서는 chain 이 끝점에 클램프됨. */
|
||||
function projectChainageDist(lat: number, lon: number, line: StationLine): { chain: number; distM: number } {
|
||||
const px = lon * line.k;
|
||||
const py = lat * 111000;
|
||||
/** 노선 이탈 판정 거리(m) — 이 이상 벗어나면 접근/이탈 구간으로 취급. */
|
||||
const OFF_ROUTE_M = 60;
|
||||
|
||||
/** 평면 투영 좌표(px,py)를 측점 폴리라인에 투영 → 연속 체이니지(m) + 수평거리(m). */
|
||||
function projectXYChainage(px: number, py: number, line: StationLine): { chain: number; distM: number } {
|
||||
const pts = line.pts;
|
||||
let bestD = Infinity;
|
||||
let bestKm = pts.length ? pts[0].km : -1;
|
||||
@@ -76,6 +80,13 @@ function projectChainageDist(lat: number, lon: number, line: StationLine): { cha
|
||||
return { chain: bestKm, distM: isFinite(bestD) ? Math.sqrt(bestD) : Infinity };
|
||||
}
|
||||
|
||||
/** 드론 lat/lon 을 측점 폴리라인에 투영 → 연속 체이니지(m) + 폴리라인까지 수평거리(m).
|
||||
* 100m 양자화된 최근접측점과 달리 전진/후진 전환 시점이 실제 이동과 일치.
|
||||
* distM 은 노선 밖(접근/이탈) 판별에 사용 — 밖에서는 chain 이 끝점에 클램프됨. */
|
||||
function projectChainageDist(lat: number, lon: number, line: StationLine): { chain: number; distM: number } {
|
||||
return projectXYChainage(lon * line.k, lat * 111000, line);
|
||||
}
|
||||
|
||||
function projectChainage(lat: number, lon: number, line: StationLine): number {
|
||||
return projectChainageDist(lat, lon, line).chain;
|
||||
}
|
||||
@@ -283,17 +294,110 @@ export function StationBar({
|
||||
}
|
||||
const n = frames.length;
|
||||
const out: ViewedPoint[] = new Array(n);
|
||||
// ── 시선 중심 체이니지(viewChain) 준비 ─────────────────────────────
|
||||
// route.json routeInfo.station_lookahead(m) 설정:
|
||||
// 미설정/0 → 기존 그대로: 드론 연직(바로 아래) 체이니지 (기본)
|
||||
// N(숫자) → 드론 체이니지 + 진행방향으로 N(m) 고정 오프셋
|
||||
// "auto" → 자동: 주시거리 ≈ AGL / tan(|짐벌 피치|)
|
||||
// (AGL = 드론고도 − 지면고도(측점 z 체이니지 보간, 동일 datum))
|
||||
// 진행방향 = 카메라 yaw 벡터 · 노선 접선 내적 부호.
|
||||
// 오프셋 활성 시 노선 밖(접근/이탈)에서는 '시선이 닿는 지점'을 노선에 투영해
|
||||
// 그 지점이 노선 위(OFF_ROUTE_M 이내)면 배지가 측점값으로 전환된다.
|
||||
const lookCfgRaw = (routeMeta?.routeInfo as Record<string, unknown> | undefined)?.station_lookahead
|
||||
?? (routeMeta as unknown as Record<string, unknown> | null)?.station_lookahead;
|
||||
const lookFixed: number | null =
|
||||
typeof lookCfgRaw === 'number' && isFinite(lookCfgRaw) && lookCfgRaw > 0 ? lookCfgRaw
|
||||
: typeof lookCfgRaw === 'string' && lookCfgRaw.trim() !== '' && isFinite(Number(lookCfgRaw)) && Number(lookCfgRaw) > 0 ? Number(lookCfgRaw)
|
||||
: null;
|
||||
const lookAuto = typeof lookCfgRaw === 'string' && lookCfgRaw.trim().toLowerCase() === 'auto';
|
||||
const lookModeText = lookAuto ? '자동(카메라 시선 중심)' : lookFixed !== null ? `고정 +${lookFixed}m (진행방향)` : '미설정 (드론 연직 — 기존 동작)';
|
||||
const zProf = stations
|
||||
.map((st) => ({ km: stationKm(st.title), z: st.z }))
|
||||
.filter((p) => p.km >= 0 && p.z > 0)
|
||||
.sort((a, b) => a.km - b.km);
|
||||
const zAt = (km: number): number | null => {
|
||||
if (zProf.length < 2) return null;
|
||||
if (km <= zProf[0].km) return zProf[0].z;
|
||||
if (km >= zProf[zProf.length - 1].km) return zProf[zProf.length - 1].z;
|
||||
let lo = 0, hi = zProf.length - 1;
|
||||
while (hi - lo > 1) { const m = (lo + hi) >> 1; if (zProf[m].km <= km) lo = m; else hi = m; }
|
||||
const a = zProf[lo], b = zProf[hi];
|
||||
const r = b.km > a.km ? (km - a.km) / (b.km - a.km) : 0;
|
||||
return a.z + (b.z - a.z) * r;
|
||||
};
|
||||
const MAX_LOOK = 350; // 전방 주시거리 상한(m) — POI 표시범위(기본 400)와 유사 수준
|
||||
const tangentAt = (km: number): { x: number; y: number } | null => {
|
||||
const pts = stationLine.pts;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const a = pts[i], b = pts[i + 1];
|
||||
if ((km - a.km) * (km - b.km) <= 0 && a.km !== b.km) {
|
||||
const s = b.km > a.km ? 1 : -1; // +km(체이니지 증가) 방향 접선
|
||||
const dx = (b.x - a.x) * s, dy = (b.y - a.y) * s;
|
||||
const L = Math.hypot(dx, dy);
|
||||
return L > 0 ? { x: dx / L, y: dy / L } : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// 노선 체이니지 범위(클램프용) — 측점 폴리라인 km 범위(z 프로파일 유무와 무관).
|
||||
let chMin = Infinity, chMax = -Infinity;
|
||||
for (const p of stationLine.pts) { if (p.km < chMin) chMin = p.km; if (p.km > chMax) chMax = p.km; }
|
||||
for (let i = 0; i < n; i++) {
|
||||
const f = frames[i];
|
||||
const pj = projectChainageDist(f.lat, f.lon, stationLine);
|
||||
let viewChain = pj.chain;
|
||||
let viewOffM = pj.distM;
|
||||
if (lookFixed !== null || lookAuto) {
|
||||
// 주시거리(m): 고정값 또는 자동(짐벌 피치·AGL)
|
||||
let lookDist: number | null = lookFixed;
|
||||
if (lookAuto) {
|
||||
const gz = zAt(pj.chain);
|
||||
if (gz !== null && isFinite(f.altitude)) {
|
||||
const agl = f.altitude - gz;
|
||||
if (agl > 5 && agl < 500) {
|
||||
const pitchDeg = isFinite(f.pitch) ? f.pitch : -15;
|
||||
lookDist = pitchDeg <= -3
|
||||
? Math.min(MAX_LOOK, agl / Math.tan((-pitchDeg * Math.PI) / 180))
|
||||
: MAX_LOOK; // 수평에 가까우면 상한 사용
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lookDist !== null && lookDist > 0 && isFinite(f.yaw)) {
|
||||
// 연속 모델: 유효 주시거리 = lookDist − (노선 이격의 초과분).
|
||||
// 노선 위(이격 ≤ OFF_ROUTE_M): 초과분 0 → 항상 +lookDist (횡 이동에 안정)
|
||||
// 접근/이탈: 이격이 줄수록 유효 주시거리가 0→lookDist 로 늘어
|
||||
// 배지가 0+000 에서 멈춤·점프 없이 진입 후 +lookDist 까지 매끄럽게 이어진다.
|
||||
const over = Math.max(0, pj.distM - OFF_ROUTE_M);
|
||||
const effLook = lookDist - over;
|
||||
if (effLook >= 0) {
|
||||
const t = tangentAt(pj.chain);
|
||||
if (t) {
|
||||
const yawRad = (f.yaw * Math.PI) / 180;
|
||||
const dot = Math.sin(yawRad) * t.x + Math.cos(yawRad) * t.y;
|
||||
viewChain = clamp(pj.chain + (dot >= 0 ? 1 : -1) * effLook, chMin, chMax);
|
||||
}
|
||||
viewOffM = Math.min(pj.distM, OFF_ROUTE_M); // 값 표시 상태(노선밖 아님)
|
||||
} else {
|
||||
// 아직 시선이 노선에 못 미침 — '노선밖 (남은 거리+θ)m'
|
||||
viewOffM = OFF_ROUTE_M - effLook;
|
||||
}
|
||||
}
|
||||
}
|
||||
out[i] = {
|
||||
frameNum: f.frame,
|
||||
km: nearestStationKm(f, stations),
|
||||
chain: pj.chain,
|
||||
viewChain,
|
||||
viewOffM,
|
||||
offM: pj.distM,
|
||||
time: f.frame / videoFps,
|
||||
};
|
||||
}
|
||||
// 진단: 세그먼트별 적용 확인용 — 중간 프레임 표본의 연직 대비 오프셋(Δ).
|
||||
const midVp = out[Math.floor(out.length / 2)];
|
||||
console.log(
|
||||
`[스테이션바] 배지 오프셋: ${lookModeText} | 표본 Δ${Math.round(midVp.viewChain - midVp.chain) >= 0 ? '+' : ''}${Math.round(midVp.viewChain - midVp.chain)}m (${out.length}행)`,
|
||||
);
|
||||
// 이동거리 축(frac): 체이니지 |Δ| 가 아니라 GPS 실이동거리 누적(±W 이동평균 평활).
|
||||
// 체이니지 투영은 노선 밖(접근/이탈 구간)에서 폴리라인 끝점(0k000 등)에 클램프되어
|
||||
// 드론이 움직여도 커서가 멈추는 문제가 있었음(제주 — 비행 시작점이 노선 밖 700m).
|
||||
@@ -333,7 +437,7 @@ export function StationBar({
|
||||
chainRef.current = { time, frac, depChain: dep, arrChain: arr };
|
||||
viewedRef.current = out;
|
||||
setReady(true);
|
||||
}, [stations, stationLine, storeFrames, videoFps]);
|
||||
}, [stations, stationLine, storeFrames, videoFps, routeMeta]);
|
||||
|
||||
/** 현재 시간에 가장 가까운 precompute 인덱스. */
|
||||
const viewedIdxAtTime = useCallback((t: number): number => {
|
||||
@@ -352,18 +456,21 @@ export function StationBar({
|
||||
return best;
|
||||
}, [videoFps]);
|
||||
|
||||
// 현재 보는 측점값 — 연속 체이니지(chain)로 10m 해상도 (km 필드는 100m 양자화).
|
||||
// 현재 보는 측점값 — station_lookahead 미설정이면 드론 연직 체이니지(기존),
|
||||
// 설정 시 진행방향 오프셋/시선 중심 지점의 체이니지(viewChain).
|
||||
const realChain = useMemo<number | null>(() => {
|
||||
if (!ready) return null;
|
||||
const idx = viewedIdxAtTime(currentTime);
|
||||
return idx >= 0 ? viewedRef.current[idx].chain : null;
|
||||
return idx >= 0 ? viewedRef.current[idx].viewChain : null;
|
||||
}, [currentTime, ready, viewedIdxAtTime]);
|
||||
|
||||
// 현재 드론의 노선(측점 폴리라인) 이탈 거리(m) — 접근/이탈 구간 배지 표기용.
|
||||
// 배지 표시 지점의 노선 이격(m) — 접근/이탈 구간 '노선밖' 표기용.
|
||||
// 오프셋 활성 시 '시선이 닿는 지점' 기준이라, 화면이 노선을 비추기 시작하면
|
||||
// 드론이 아직 노선 밖이어도 배지가 측점값으로 먼저 전환된다.
|
||||
const realOffM = useMemo<number | null>(() => {
|
||||
if (!ready) return null;
|
||||
const idx = viewedIdxAtTime(currentTime);
|
||||
return idx >= 0 ? viewedRef.current[idx].offM : null;
|
||||
return idx >= 0 ? viewedRef.current[idx].viewOffM : null;
|
||||
}, [currentTime, ready, viewedIdxAtTime]);
|
||||
|
||||
// 종점역 '미도착' 폭(px) — 영상이 종점역 도착 전 끝날 때, 트랙 우측에 이만큼 비워둔다.
|
||||
@@ -460,7 +567,6 @@ export function StationBar({
|
||||
// 커서 배지 = 폴더 데이터 기반 연속 체이니지(realChain)를 10m 단위로 표시. 데이터 없으면 빈 문자열.
|
||||
// 드론이 노선(측점 폴리라인)에서 OFF_ROUTE_M 이상 벗어난 구간(접근/이탈)은 체이니지가
|
||||
// 끝점에 클램프되어 무의미 → "노선밖 ○○m" 로 표기해 측점이 안 변하는 이유를 드러낸다.
|
||||
const OFF_ROUTE_M = 60;
|
||||
const cursorText =
|
||||
realChain !== null && realChain >= 0
|
||||
? (realOffM !== null && realOffM > OFF_ROUTE_M
|
||||
@@ -561,6 +667,9 @@ export function StationBar({
|
||||
const arr = viewedRef.current;
|
||||
const frames = storeFrames;
|
||||
if (!ready || !arr.length || duration <= 0 || !frames.length) return [];
|
||||
// station_lookahead(시선/오프셋) 활성 여부 — viewChain 이 연직 체이니지와 다르면 활성.
|
||||
// 활성 시 시설물 마크는 viewChain 기준으로만 배치(물리 폴백 금지).
|
||||
const lookActive = arr.some((v) => Math.abs(v.viewChain - v.chain) > 1);
|
||||
|
||||
// 실좌표(lat/lon)에 드론이 가까워지는 '모든 통과 구간'의 px 목록.
|
||||
// (드론이 같은 구조물을 2번 이상 지날 때 각 통과마다 마커를 찍기 위함)
|
||||
@@ -619,6 +728,9 @@ export function StationBar({
|
||||
};
|
||||
|
||||
// 측점값(미터)에 해당하는 모든 통과 지점 px (연속 체이니지 기준).
|
||||
// 배지와 동일한 viewChain(오프셋/시선 기준) 사용 — station_lookahead 설정 시
|
||||
// 마크(시점·측점 기반 구조물)가 배지 값과 같은 타이밍에 커서와 만난다.
|
||||
// (미설정이면 viewChain=연직 체이니지 — 기존 동작 그대로)
|
||||
const pxPassesAtMileage = (targetM: number): { px: number; km: number }[] => {
|
||||
const arr = viewedRef.current;
|
||||
if (!arr.length) return [];
|
||||
@@ -628,13 +740,13 @@ export function StationBar({
|
||||
const out: { px: number; km: number }[] = [];
|
||||
let i = 0;
|
||||
while (i < arr.length) {
|
||||
// 노선 밖(접근/이탈) 프레임 제외 — 폴리라인 밖에선 체이니지가 끝점(0 등)에 클램프되어
|
||||
// 접근 구간 전체가 '시점(0k000) 통과'로 오인돼 마크가 바 맨 앞에 찍히는 문제 방지.
|
||||
if (arr[i].offM > OFF_ROUTE_M) { i++; continue; }
|
||||
if (Math.abs(arr[i].chain - targetM) < TH) {
|
||||
let j = i, bj = i, bd = Math.abs(arr[i].chain - targetM);
|
||||
while (j < arr.length && arr[j].offM <= OFF_ROUTE_M && Math.abs(arr[j].chain - targetM) < TH) {
|
||||
const d = Math.abs(arr[j].chain - targetM);
|
||||
// 배지 표시 지점이 노선 밖인 프레임 제외 — 접근/이탈 구간에서 체이니지가
|
||||
// 끝점(0 등)에 클램프되어 '시점 통과'로 오인돼 마크가 앞에 찍히는 문제 방지.
|
||||
if (arr[i].viewOffM > OFF_ROUTE_M) { i++; continue; }
|
||||
if (Math.abs(arr[i].viewChain - targetM) < TH) {
|
||||
let j = i, bj = i, bd = Math.abs(arr[i].viewChain - targetM);
|
||||
while (j < arr.length && arr[j].viewOffM <= OFF_ROUTE_M && Math.abs(arr[j].viewChain - targetM) < TH) {
|
||||
const d = Math.abs(arr[j].viewChain - targetM);
|
||||
if (d < bd) { bd = d; bj = j; }
|
||||
j++;
|
||||
}
|
||||
@@ -665,14 +777,14 @@ export function StationBar({
|
||||
const appendEndpointStations = (marks: StructMark[]): StructMark[] => {
|
||||
const va = viewedRef.current;
|
||||
if (!va.length) return marks;
|
||||
const onRoute = va.filter(v => v.offM <= OFF_ROUTE_M);
|
||||
const onRoute = va.filter(v => v.viewOffM <= OFF_ROUTE_M);
|
||||
if (!onRoute.length) return marks;
|
||||
const ends = [
|
||||
{ v: onRoute[0], title: '시작' },
|
||||
{ v: onRoute[onRoute.length - 1], title: '끝' },
|
||||
];
|
||||
for (const e of ends) {
|
||||
const ch = e.v.chain;
|
||||
const ch = e.v.viewChain;
|
||||
if (marks.some(m => Math.abs(m.km - ch) <= 100)) continue;
|
||||
const px = pxAtTime(e.v.time);
|
||||
if (px === null) continue;
|
||||
@@ -681,6 +793,22 @@ export function StationBar({
|
||||
return marks;
|
||||
};
|
||||
|
||||
// 1km(1+000) 단위 측점 마크 — 매 1000m 측점의 통과 위치에 측점값 라벨(위)만 표시.
|
||||
// title 은 빈 문자열 → 하단 이름 라벨/그룹핑 제외(Timeline 에서 스킵).
|
||||
// 기존 마크(시점·시설물 등)와 24px 이내로 겹치면 생략(라벨 충돌 방지).
|
||||
const appendKmStations = (marks: StructMark[]): StructMark[] => {
|
||||
if (!stationLine) return marks;
|
||||
let lo = Infinity, hi = -Infinity;
|
||||
for (const p of stationLine.pts) { if (p.km < lo) lo = p.km; if (p.km > hi) hi = p.km; }
|
||||
for (let m = Math.ceil(lo / 1000) * 1000; m <= hi; m += 1000) {
|
||||
for (const p of pxPassesAtMileage(m)) {
|
||||
if (marks.some((x) => Math.abs(x.px - p.px) < 24)) continue;
|
||||
marks.push({ px: p.px, title: '', category: '측점', km: m, kmExists: true });
|
||||
}
|
||||
}
|
||||
return marks;
|
||||
};
|
||||
|
||||
// 구조물 후보 POI(교량/터널/역사) base 이름 → 실좌표.
|
||||
const cats = new Set(['교량', '터널', '역사']);
|
||||
const poiByName = new Map<string, { lat: number; lon: number; category: string }>();
|
||||
@@ -744,12 +872,22 @@ export function StationBar({
|
||||
// 일치(pxPassesAtMileage)'하는 통과에서만 마커를 찍는다. 그래야:
|
||||
// ① 좌표만 가까운(측점 다른) 조차장 재진입(드론 162k210인데 대전조차장 162k080)은 안 찍히고,
|
||||
// ② 진짜 162k080 통과에는 정상적으로 찍힌다. (교량/터널만 좌표근접 폴백 허용.)
|
||||
if (!passes.length && cat !== '역사' && s.lat != null && s.lon != null) passes = pxPassesTo(s.lat, s.lon, off);
|
||||
if (!passes.length && cat !== '역사' && match) passes = pxPassesTo(match.lat, match.lon, off);
|
||||
// ※ station_lookahead(시선/오프셋) 활성 시 물리(드론 연직) 폴백 금지 —
|
||||
// 구간 밖 시설물(예: 이전 세그먼트에서 이미 바라본 교량)이 물리 통과 위치로
|
||||
// 끌려 들어와 배지 기준(viewChain 범위)에 없는 값이 바에 찍히는 것을 방지.
|
||||
// 해당 시설물은 자기 viewChain 범위를 가진 세그먼트의 바에서 정상 표시된다.
|
||||
if (!lookActive) {
|
||||
if (!passes.length && cat !== '역사' && s.lat != null && s.lon != null) passes = pxPassesTo(s.lat, s.lon, off);
|
||||
if (!passes.length && cat !== '역사' && match) passes = pxPassesTo(match.lat, match.lon, off);
|
||||
}
|
||||
if (!passes.length && s.startMileage != null && s.endMileage != null) {
|
||||
const mid = (s.startMileage + s.endMileage) / 2;
|
||||
const px = pxAtMileage(mid);
|
||||
if (px !== null) passes = [{ px, km: mid }];
|
||||
if (lookActive) {
|
||||
passes = pxPassesAtMileage(mid); // 시선 기준 통과 — 범위 밖이면 미표시
|
||||
} else {
|
||||
const px = pxAtMileage(mid);
|
||||
if (px !== null) passes = [{ px, km: mid }];
|
||||
}
|
||||
}
|
||||
// 역사 km 라벨 = '역 좌표 자체'의 측점값(드론 최근접 프레임 chain 아님).
|
||||
// 역은 바 양끝에 스냅되므로, 시작/끝 커서값(= 역 위치 측점)과 일치시키기 위함.
|
||||
@@ -763,7 +901,7 @@ export function StationBar({
|
||||
out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, kmExists, props: s.props });
|
||||
}
|
||||
}
|
||||
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
|
||||
return appendKmStations(appendEndpointStations(placeUnreachedTerminal(appendStartMark(out))));
|
||||
}
|
||||
|
||||
// 폴백: POI category 기반 (실좌표 GPS 근접, 모든 통과).
|
||||
@@ -782,7 +920,7 @@ export function StationBar({
|
||||
out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km, kmExists });
|
||||
}
|
||||
}
|
||||
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
|
||||
return appendKmStations(appendEndpointStations(placeUnreachedTerminal(appendStartMark(out))));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, duration, storeFrames, pois, stations, pxAtTime, routeMeta, routeDirCh, storeStructures, pxAtMileage, endGapPx, stationLine, videoFps]);
|
||||
|
||||
|
||||
@@ -105,11 +105,13 @@ export function Timeline({
|
||||
// 같은 구조물의 상/하/인상 변형(같은 base = 같은 좌표)은 '겹침제외 옵션과 무관하게' 진행방향 1개만.
|
||||
// (dir-우선 정렬돼 있어 방향 변형이 먼저 = 채택. 예: 법동가도교(하)/(인상) → (하) 1개)
|
||||
const byBase = new Map<string, StructMark>();
|
||||
const untitled: StructMark[] = []; // 이름 없는 마크(1km 단위 측점) — base dedupe 비대상
|
||||
for (const it of ordered) {
|
||||
if (!it.title) { untitled.push(it); continue; }
|
||||
const b = baseStruct(it.title);
|
||||
if (!byBase.has(b)) byBase.set(b, it);
|
||||
}
|
||||
const keptOthers = [...byBase.values()];
|
||||
const keptOthers = [...byBase.values(), ...untitled];
|
||||
// 라벨은 긴 이름 2줄(아래 splitStructLabel)로 폭을 줄여 표시. 그래도 가까워 겹치면 그대로 둠(엇갈림 없음).
|
||||
void overlapExclude;
|
||||
const structs = [...structures.filter(isStation), ...keptOthers].sort((a, b) => a.px - b.px);
|
||||
@@ -151,6 +153,7 @@ export function Timeline({
|
||||
{
|
||||
const byTitle = new Map<string, StructMark[]>();
|
||||
for (const s of structs) {
|
||||
if (!s.title) continue; // 이름 없는 마크(1km 단위 측점 등) — 측점값 라벨만 표시
|
||||
const a = byTitle.get(s.title);
|
||||
if (a) a.push(s);
|
||||
else byTitle.set(s.title, [s]);
|
||||
|
||||
@@ -142,6 +142,10 @@ export interface RouteInfo {
|
||||
ellipsoidal_height?: number;
|
||||
/** 개발 모드: 1(기본)=영상제어 버튼·제어 UI 표시, 0=숨김(뷰어 모드). */
|
||||
dev_mode?: number;
|
||||
/** 스테이션바 배지 전방 오프셋(m). 미설정/0=드론 연직 체이니지(기본, 기존 동작),
|
||||
* N=진행방향 +N(m) 고정, "auto"=자동(짐벌 피치·AGL 기반 카메라 시선 중심).
|
||||
* 활성 시 노선 밖(접근/이탈)에서는 시선 지점을 노선에 투영해 측점값 전환이 앞당겨진다. */
|
||||
station_lookahead?: number | string;
|
||||
/** 측점 인정 범위(m): station 위치를 이 거리 이내로 지날 때 마커 표시. 기본 40. 폴더별 조절. */
|
||||
stationTolerance?: number;
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface DroneFrameBasic {
|
||||
export interface CameraParams {
|
||||
timeOffsetSec?: number; // 영상↔비행로그 시간동기 보정(초). + = 로그를 뒤로 밀어 참조.
|
||||
// POI 가 진행방향으로 일괄 밀릴 때 조정 — "풀 붙임" 정합의 핵심 값.
|
||||
attLeadSec?: number; // 자세(yaw/pitch/roll)만의 추가 시간 보정(초) — 짐벌 로그 기록 지연 보정.
|
||||
// 영상이 좌우 팬(회전)할 때 라벨·선형이 늦게 따라오면 +, 앞서가면 −.
|
||||
yawOffset: number; // yaw 오프셋 (degrees, per-frame SRT yaw에 더함)
|
||||
pitch: number; // pitch 오프셋 (degrees, per-frame SRT pitch에 더함, 기본 0)
|
||||
roll: number; // roll 오프셋 (degrees, per-frame SRT roll에 더함, 기본 0)
|
||||
@@ -54,6 +56,7 @@ export interface CameraParams {
|
||||
/** Python advanced_tuner_v2.py 기본값 */
|
||||
export const DEFAULT_CAMERA_PARAMS: CameraParams = {
|
||||
timeOffsetSec: 0,
|
||||
attLeadSec: 0,
|
||||
yawOffset: 0,
|
||||
pitch: 0, // offset, Python spn_pitch 기본값 0
|
||||
roll: 0, // offset, Python spn_roll 기본값 0
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-07-14 전체 커밋 (3차) — djmd 프레임 동기·오버레이 지면고정
|
||||
|
||||
**소요 시간**: 3분
|
||||
**Context 사용량**: input 7300k / output 2k tokens
|
||||
|
||||
## 커밋
|
||||
|
||||
- **c64c57b** `djmd 프레임 동기·오버레이 지면고정·선형-측점 정합·UI 단축키`
|
||||
- 36개 파일, +1248 / −56
|
||||
- 신규: client/src/utils/djmdTrack.ts (MP4 djmd 트랙 파서 + 자세 재정렬)
|
||||
- docs/history 23건 포함 (7/13 저녁 ~ 7/14)
|
||||
|
||||
## 주요 내용
|
||||
|
||||
djmd 프레임 동기 위치/고도, 자세 시간축 재정렬(-0.33s), 포즈 평활 제한(±5fr),
|
||||
선형-측점 정점 삽입 정합, 측점 라벨 즉시 투영, 드론높이 모드 선형 통일,
|
||||
offZ 잔재 교정, 로드 UX(정지 대기·UI 초기화), 측점 팝업 표고, 이중 라벨 제거,
|
||||
측점 표기 0+000 전환, 양끝 시작/끝 스테이션, F2/A 단축키, 54000 포트 재부팅 자동화.
|
||||
|
||||
## 다음
|
||||
|
||||
- push 는 사용자 요청 시 (GCM helper 연동됨 — `git push`)
|
||||
@@ -0,0 +1,29 @@
|
||||
# 2026-07-14 스테이션바 현재값 — 드론 연직 → 카메라 시선 중심 체이니지 연동
|
||||
|
||||
**소요 시간**: 10분
|
||||
**Context 사용량**: input 7600k / output 3k tokens
|
||||
|
||||
## 배경 (사용자 제보)
|
||||
|
||||
배지(현재 스테이션)는 드론의 연직 투영 체이니지인데, 카메라는 피치 −15°로 전방
|
||||
(AGL 70m 기준 약 260m 앞)을 비추므로 화면 속 측점 라벨과 배지 값이 달라 혼동 유발.
|
||||
|
||||
## 구현 — StationBar.tsx
|
||||
|
||||
- ViewedPoint 에 **viewChain**(시선 중심 지면 교점의 체이니지) 추가:
|
||||
- 전방 주시거리 = AGL / tan(|짐벌 피치|), 상한 350m (수평에 가까우면 상한 사용)
|
||||
- AGL = 드론고도 − 지면고도(측점 z 의 체이니지 보간 — 동일 datum(타원체고))
|
||||
- 진행방향 = 카메라 yaw 벡터 · 노선 접선(+체이니지 방향) 내적 부호 → ±주시거리
|
||||
- 노선 양끝은 측점 범위로 클램프
|
||||
- 커서 배지(realChain)가 chain → **viewChain** 을 사용
|
||||
- 폴백: 측점 z 없는 데이터셋(z 프로파일 <2점)·고도/yaw 미상 → 기존 연직 체이니지 유지
|
||||
- 구간색·마크·이동거리축·노선밖 표기는 기존(물리 통과 기준) 그대로
|
||||
|
||||
## 효과
|
||||
|
||||
배지 값이 화면 중앙이 실제로 비추는 지점의 측점값과 일치 — 영상 속 측점 라벨과 동기화.
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-DGLDr81A.js, 빌드 07.14 18:35) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 18:35 확인) → 폴더 재선택
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-07-14 배지 전방 오프셋 route.json 설정화 — station_lookahead
|
||||
|
||||
**소요 시간**: 6분
|
||||
**Context 사용량**: input 7900k / output 2k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
배지 시선 연동을 route.json 환경설정으로 — 0 이면 드론 연직, 100 이면 진행방향 +100m.
|
||||
|
||||
## 구현 — StationBar.tsx, types/geo.ts
|
||||
|
||||
route.json `routeInfo.station_lookahead` (m):
|
||||
|
||||
| 값 | 동작 |
|
||||
|----|------|
|
||||
| `0` | 드론 바로 아래(연직 체이니지) — 종전 방식 |
|
||||
| `N` (예: `100`) | 드론 체이니지 + 진행방향으로 N m 고정 오프셋 |
|
||||
| 미설정 / `"auto"` | 자동 — 짐벌 피치·AGL 기반 카메라 시선 중심(직전 구현) |
|
||||
|
||||
- 진행방향 = 카메라 yaw · 노선 접선 내적 부호 (후진 시 −N)
|
||||
- 노선 체이니지 범위로 클램프(양끝 밖으로 안 나감), 숫자 문자열("100")도 허용
|
||||
- RouteMeta 타입에 station_lookahead 필드 문서화
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-BtY-C6rz.js, 빌드 07.14 18:42) 서빙
|
||||
- 적용: route.json 에 `"station_lookahead": 100` 추가 → F5 새로고침 → 폴더 재선택
|
||||
@@ -0,0 +1,28 @@
|
||||
# 2026-07-14 station_lookahead=100 미반영 제보 — 알고리즘 검증 + 모드 로그 추가
|
||||
|
||||
**소요 시간**: 7분
|
||||
**Context 사용량**: input 8200k / output 3k tokens
|
||||
|
||||
## 제보
|
||||
|
||||
route.json 에 station_lookahead: 100 을 넣어도 배지가 0(드론 연직)처럼 보임.
|
||||
|
||||
## 검증
|
||||
|
||||
- 사용자 route.json 확인: `"station_lookahead": 100` routeInfo 안에 정상 기재 ✓
|
||||
- parseRouteMeta 는 JSON 전체 보존(키 화이트리스트 없음) — 설정 전달 경로 정상 ✓
|
||||
- **알고리즘 실데이터 재현**: 경로 1-1 CSV 4개 시점(60/150/300/450s) 전부
|
||||
viewChain = chain **정확히 +100m** (진행방향 dot +0.94~0.97) — 코드 정상
|
||||
- 결론: 브라우저가 **이전 번들 실행 중**으로 판단(폴더 재선택은 데이터만 갱신, JS 는 F5 필요)
|
||||
|
||||
## 조치
|
||||
|
||||
- 프리컴퓨트에 모드 확인용 콘솔 로그 추가:
|
||||
`[스테이션바] 배지 오프셋: 고정 +100m (진행방향) | 0 (드론 연직) | 자동(카메라 시선 중심)`
|
||||
- station_lookahead 를 routeInfo 밖(최상위)에 둔 경우도 인식하도록 허용
|
||||
|
||||
## 확인 절차 (사용자 안내)
|
||||
|
||||
1. F5 새로고침 → 좌상단 빌드 시각 **07.14 18:50** 확인
|
||||
2. 폴더 재선택 → 콘솔(F12)에 `[스테이션바] 배지 오프셋: 고정 +100m` 로그 확인
|
||||
3. 배지가 드론 위치보다 100m 앞 측점값으로 표시되는지 확인
|
||||
@@ -0,0 +1,36 @@
|
||||
# 2026-07-14 접근 구간 '노선밖' — 시선 기준 전환 + 미설정 시 기존(연직) 기본값
|
||||
|
||||
**소요 시간**: 12분
|
||||
**Context 사용량**: input 8600k / output 4k tokens
|
||||
|
||||
## 제보/요청
|
||||
|
||||
1. 접근 구간에서 화면은 이미 0+000~0+100 을 비추는데 배지는 "노선밖 229m" —
|
||||
바라보는 곳 기준이면 측점값이 나와야 하지 않나
|
||||
2. station_lookahead 는 **정의된 경우에만** 적용, 미정의면 **기존(드론 연직)** 동작
|
||||
|
||||
## 구현 — StationBar.tsx
|
||||
|
||||
- **기본값 변경**: 미설정/0 → 드론 연직 체이니지(기존 동작 그대로).
|
||||
숫자 N → 진행방향 +N(m), `"auto"` → 짐벌 피치·AGL 기반 자동 (명시 시에만)
|
||||
- **노선밖 판정도 시선 기준으로**(오프셋 활성 시):
|
||||
- 노선 위: 노선을 따라 +N (곡선 안정, 기존 로직)
|
||||
- 노선 밖(접근/이탈): 시선이 닿는 지점(직선 전방 N m)을 노선에 투영 —
|
||||
시선 지점 이격 ≤ 150m(광각 화면에 노선이 보이기 시작)면 측점값으로 전환,
|
||||
아니면 '노선밖 (시선 지점 이격)m'
|
||||
- projectXYChainage 분리(평면좌표 투영 재사용), OFF_ROUTE_M 모듈 상수화,
|
||||
ViewedPoint 에 viewOffM 추가(배지 전용 — 마크/구간색은 종전 드론 기준 유지)
|
||||
|
||||
## 검증 (경로 1-1 실데이터, 고정 100m)
|
||||
|
||||
| 드론 이격 | 배지 |
|
||||
|-----------|------|
|
||||
| 394→245m | 노선밖 299→152m (시선 지점 기준으로 기존보다 ~100m 작게) |
|
||||
| 195→96m | **0+000** (화면에 노선 보임 → 조기 전환) |
|
||||
| 노선 진입 후 | 0+104 → 0+308 → 0+707 (+100 유지) |
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-YV1lA2Bm.js, 빌드 07.14 19:02) 서빙
|
||||
|
||||
## 안내
|
||||
|
||||
- 스크린샷 시점(이격 229m)에 바로 측점값을 원하면 값을 200~300 또는 "auto" 권장
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-07-14 배지 0+000 인데 시점 마크가 다른 위치 — 마크도 배지 기준(viewChain) 통일
|
||||
|
||||
**소요 시간**: 5분
|
||||
**Context 사용량**: input 9000k / output 2k tokens
|
||||
|
||||
## 제보
|
||||
|
||||
배지는 0+000 인데 스테이션바의 시점(0+000) 마크는 더 오른쪽(다른 위치)에 표시.
|
||||
|
||||
## 원인
|
||||
|
||||
배지는 시선/오프셋 기준(viewChain)으로 전환됐는데, 시점 마크·측점 기반 구조물 마크는
|
||||
여전히 **드론 물리 통과 시각**(chain) 기준 → 오프셋(예: 100m)만큼 시간차 발생.
|
||||
|
||||
## 수정 — StationBar.tsx
|
||||
|
||||
- pxPassesAtMileage(시점 마크·측점 기반 구조물 마크 공용)의 통과 탐지를
|
||||
chain/offM → **viewChain/viewOffM** 으로 전환 — 배지가 그 측점값을 표시하는
|
||||
순간에 커서가 마크와 만난다
|
||||
- appendEndpointStations(양끝 시작/끝 스테이션)도 동일 기준으로 통일
|
||||
- station_lookahead 미설정이면 viewChain=연직 체이니지 → **기존 동작 그대로**
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-qhnyNh_5.js, 빌드 07.14 19:07) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 19:07 확인) → 폴더 재선택 →
|
||||
배지가 0+000 이 되는 순간 커서가 시점 마크 위에 있는지 확인
|
||||
@@ -0,0 +1,30 @@
|
||||
# 2026-07-14 시점 이후 스테이션 이동량 0 — 배지 연속 모델로 교체
|
||||
|
||||
**소요 시간**: 6분
|
||||
**Context 사용량**: input 9400k / output 3k tokens
|
||||
|
||||
## 제보
|
||||
|
||||
시점(0+000) 마크부터 커서 위치까지 배지가 0+000 에 고정(이동량 0), 이후 +100 점프.
|
||||
|
||||
## 원인
|
||||
|
||||
접근 구간에서 '시선 지점의 노선 투영'이 노선 시점(0+000)에 **클램프** —
|
||||
시선이 노선 시작에 닿은 뒤 드론이 실제 진입할 때까지(~100m 비행) 배지가 0 에 정체,
|
||||
진입 순간 노선따라 +100 으로 전환되며 점프.
|
||||
|
||||
## 수정 — StationBar.tsx (연속 모델)
|
||||
|
||||
- **유효 주시거리 = lookDist − max(0, 노선이격 − OFF_ROUTE_M)**
|
||||
- 노선 위(이격 ≤ 60m): 초과분 0 → 항상 +lookDist (횡 이동 흔들림에도 안정)
|
||||
- 접근/이탈: 이격이 줄수록 유효 주시거리 0 → lookDist 로 선형 증가
|
||||
- 유효 주시거리 < 0 이면 '노선밖 (남은거리+60)m'
|
||||
- 배지가 0+000 을 지나는 순간부터 진입(+100)까지 **멈춤·점프 없이 연속**
|
||||
- 시점 마크(pxPassesAtMileage, viewChain 기준)도 그 교차 순간에 찍혀 배지와 일치
|
||||
|
||||
## 검증 (경로 1-1, 고정 100m)
|
||||
|
||||
노선밖 294→194→95m → 0+063 → 0+104 → 0+207 → 0+308 → … (단조 연속, 플랫 구간 없음)
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-ChxJN0eC.js, 빌드 07.14 19:13) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 19:13 확인) → 폴더 재선택
|
||||
@@ -0,0 +1,32 @@
|
||||
# 2026-07-14 스테이션바 시설물 마크 — station_lookahead 기준으로 완전 통일
|
||||
|
||||
**소요 시간**: 9분
|
||||
**Context 사용량**: input 9800k / output 3k tokens
|
||||
|
||||
## 제보
|
||||
|
||||
배지(스테이션값)는 설정대로 나오는데, 스테이션바의 시설물(지장물) 마크 위치는
|
||||
드론 연직(물리 통과) 기준으로 보임 — 특히 두 번째 영상에서.
|
||||
|
||||
## 원인
|
||||
|
||||
시설물 마크 배치는 다단 폴백 구조:
|
||||
① 측점값 통과 창(pxPassesAtMileage — viewChain 기준으로 이미 전환) →
|
||||
② **좌표 근접(pxPassesTo — 드론 GPS 물리 통과, 미전환)**.
|
||||
세그먼트 시작 100m 안쪽 시설물은 viewChain(=연직+100)이 그 측점값을 커버하지
|
||||
않아 ①이 빈 결과 → ②(물리) 폴백으로 떨어져 배지와 기준이 섞임.
|
||||
|
||||
## 수정 — StationBar.tsx pxPassesAtMileage
|
||||
|
||||
- 오프셋 활성(viewChain≠chain 데이터 감지) 시 통과 창을 못 찾으면
|
||||
**viewChain 이 가장 가까워지는 프레임에 배치**(이격 150m 이내) —
|
||||
물리 폴백으로 빠지지 않음. 세그먼트 경계 시설물은 바 시작 부근에 클램프
|
||||
- 오프셋 미설정 데이터셋(회덕 등)은 기존 동작 그대로(감지 조건 불충족)
|
||||
- 진단 로그 확장: `[스테이션바] 배지 오프셋: … | 표본 Δ+100m (N행)` —
|
||||
세그먼트마다 실제 적용량 확인 가능
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-mXKdnFF6.js, 빌드 07.14 19:29) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 19:29) → 폴더 재선택 → 두 번째 영상에서
|
||||
커서가 시설물 마크에 닿을 때 화면 중앙(전방 100m)에 해당 시설물이 보여야 정상
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-07-14 귀덕천교 1+110 이 세그2 바에 표시 — 구간 밖 시설물 마크 제거
|
||||
|
||||
**소요 시간**: 5분
|
||||
**Context 사용량**: input 10200k / output 3k tokens
|
||||
|
||||
## 제보 (스크린샷)
|
||||
|
||||
세그먼트 2 바에 귀덕천교(1+110)가 표시 — 현재 바의 스테이션 구간에 없는 값.
|
||||
|
||||
## 데이터 확인
|
||||
|
||||
- 귀덕천교 물리 체이니지 1+102, 세그먼트 2 의 시선(viewChain) 범위 **1+177~1+608**
|
||||
- 시선 기준으로 귀덕천교는 **세그먼트 1 에서 이미 바라본** 시설물 —
|
||||
세그2 바에는 없어야 정상 (세그1 바에서 viewChain 1+102 통과 시 정상 표시)
|
||||
|
||||
## 원인
|
||||
|
||||
직전 수정의 '최근접 배치' 완화 + 물리(좌표 근접) 폴백이 구간 밖 시설물을
|
||||
현재 바로 끌어들임 (물리적으로는 드론이 세그2 초반에 귀덕천교 위를 지나므로).
|
||||
|
||||
## 수정 — StationBar.tsx
|
||||
|
||||
- '최근접 배치' 완화 제거 (viewChain 은 연속이라 범위 안 시설물은 항상 창에 잡힘 —
|
||||
완화가 구제하는 건 범위 밖뿐이므로 불필요·유해)
|
||||
- **lookActive(오프셋 활성) 시 물리 폴백(pxPassesTo/pxAtMileage) 금지** —
|
||||
시설물 마크는 viewChain 기준으로만 배치, 범위 밖이면 미표시
|
||||
(이정값 전용 구조물은 pxPassesAtMileage 로 대체 시도)
|
||||
- 오프셋 미설정 데이터셋(회덕 등)은 기존 폴백 체계 그대로
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-_1oNGZpr.js, 빌드 07.14 19:34) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 19:34) → 폴더 재선택 →
|
||||
세그2 바에서 귀덕천교 마크 사라지고, 세그1 바 끝부분(1+102 바라보는 시점)에 표시
|
||||
@@ -0,0 +1,23 @@
|
||||
# 2026-07-14 스테이션바 1km(1+000) 단위 측점 마크 표시
|
||||
|
||||
**소요 시간**: 7분
|
||||
**Context 사용량**: input 10600k / output 3k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
1+000 단위 스테이션마다 스테이션바에 표시.
|
||||
|
||||
## 구현
|
||||
|
||||
- StationBar: appendKmStations — 측점 범위 내 매 1000m 값을 pxPassesAtMileage
|
||||
(배지와 동일한 viewChain 기준)로 통과 위치에 배치. 측점값 라벨(위)만 표시
|
||||
(title 빈 문자열), 기존 마크와 24px 이내 겹치면 생략
|
||||
- Timeline: 빈 title 마크는 하단 이름 라벨/그룹핑·base dedupe 에서 제외
|
||||
(측점값 라벨과 눈금만 표시), 기존 겹침 숨김 로직은 그대로 적용
|
||||
- 세그먼트별로 해당 구간에 포함된 1km 측점만 표시 (viewChain 범위 기준)
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-qkCcaENF.js, 빌드 07.14 19:41) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 19:41) → 폴더 재선택 →
|
||||
바에 1+000, 2+000 … 값이 통과 위치마다 표시되는지 확인
|
||||
@@ -0,0 +1,29 @@
|
||||
# 2026-07-14 경로 2 '노선밖 149m' 진단 — 실제 대이탈 구간(최대 390m)
|
||||
|
||||
**소요 시간**: 9분
|
||||
**Context 사용량**: input 11200k / output 3k tokens
|
||||
|
||||
## 질문 (스크린샷: 바 4+000~5+510 '끝', 배지 '노선밖 149m')
|
||||
|
||||
스테이션 구간인데 왜 노선밖 149m 인가?
|
||||
|
||||
## 진단 (데이터 재현)
|
||||
|
||||
- 해당 화면은 **경로 2** (비행 chain 3+046~6+334, lookahead 240 설정)
|
||||
- 경로 2 비행경로는 **chain 4+15x ~ 5+52x 구간에서 측점 선형을 크게 벗어남**
|
||||
(t=116s 이탈 시작 → 최대 이격 390m(t=164s) → t=289s 복귀)
|
||||
- 배지 계산 재현: t=164s 이격 390m → 유효 주시거리 240−(390−60)=−90 →
|
||||
'노선밖 60+90=**150m**' — 스크린샷의 149m 와 일치 ✓
|
||||
- 즉 오작동이 아니라 **드론(기존 도로)이 설계 선형에서 실제로 390m 떨어진 구간** —
|
||||
체이니지 범위(스테이션 구간) 안이라도 노선밖 판정은 '선형과의 횡거리' 기준
|
||||
- 설계 선형이 기존 도로와 다른 신설 구간(선형이 밭/온실을 가로지르는 것과 동일 맥락)
|
||||
|
||||
## 관찰 (참고)
|
||||
|
||||
- 이탈 구간 양끝에서 배지가 4+374 → (노선밖) → 5+433 으로 빠르게 건너뜀 —
|
||||
우회 도로를 나는 동안 선형 투영이 굴곡부를 가로질러 이동하기 때문(데이터 특성)
|
||||
|
||||
## 조치
|
||||
|
||||
- 코드 변경 없음 (정상 동작). 원하면 이탈 구간에서도 최근접 측점값을
|
||||
회색 등 다른 스타일로 계속 표시하는 옵션 구현 가능 — 사용자 판단 대기
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-07-14 좌우 팬(회전) 시 오버레이 지연 이질감 — POI EMA 우회 + 자세 선행 보정
|
||||
|
||||
**소요 시간**: 12분
|
||||
**Context 사용량**: input 11800k / output 4k tokens
|
||||
|
||||
## 제보
|
||||
|
||||
드론이 좌우로 회전(팬)할 때 노선·지장물이 뒤늦게 따라와 이질감.
|
||||
|
||||
## 원인 (2중)
|
||||
|
||||
1. **지장물(POI) 라벨의 화면 EMA 잔존** — 측점 라벨은 즉시 투영으로 바꿨지만(13:24)
|
||||
POI 라벨은 속도적응 EMA(smoothStep)를 계속 사용 → 팬 시 수 프레임 지연
|
||||
2. **짐벌 자세 로그의 기록 지연** — 위치 교차상관 dt(−0.33s)로도 짐벌 yaw 고유의
|
||||
내부 지연은 보정 안 됨 → 선형(EMA 없음)까지 함께 늦는 공통 원인
|
||||
|
||||
## 수정
|
||||
|
||||
- StationOverlay: **frameSynced(djmd) 데이터면 POI 라벨도 즉시 투영** (EMA 우회,
|
||||
0.75px 히스테리시스 유지) — 선형·측점·POI 모두 동일 타이밍
|
||||
- poseAt: **자세 선행(attLeadSec)** — 위치는 estFrame 그대로, 자세(yaw/pitch/roll)만
|
||||
attLeadSec 초 앞선 시점에서 샘플(짐벌 로그 지연 상쇄)
|
||||
- CameraParams.attLeadSec 추가(기본 0) — camera.json 자동 저장/로드(숫자 통과),
|
||||
카메라 파라미터 패널에 '자세 선행' 슬라이더(−1~+1s, 0.01 스텝, 시간동기 아래)
|
||||
|
||||
## 튜닝 방법 (사용자 안내)
|
||||
|
||||
- 좌우로 회전하는 구간 재생 → '자세 선행'을 +0.10~0.30 범위에서 조금씩 올리며
|
||||
라벨·선형이 영상과 같이 붙어 도는 값을 찾기 (앞서가면 −로)
|
||||
- 찾은 값은 'PC 저장'으로 camera.json 에 보존
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-BvoVCM-N.js, 빌드 07.14 20:04) 서빙
|
||||
@@ -0,0 +1,17 @@
|
||||
# 2026-07-14 sen H(센서 높이) 슬라이더 최대값 100mm 확장
|
||||
|
||||
**소요 시간**: 2분
|
||||
**Context 사용량**: input 12200k / output 1k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
카메라 파라미터 'sen H' 최대값을 100 까지.
|
||||
|
||||
## 수정 — StationOverlay.tsx
|
||||
|
||||
- sen H ParamRow: max 36 → **100** (min 6, step 0.05 유지)
|
||||
|
||||
## 검증
|
||||
|
||||
- tsc+vite 빌드 통과, 새 번들(index-D27TXlz-.js, 빌드 07.14 20:13) 서빙
|
||||
- 적용: F5 새로고침(빌드 시각 20:13) 후 카메라 파라미터에서 확인
|
||||
@@ -0,0 +1,35 @@
|
||||
# 2026-07-14 지도 기준 맵핑 — 경로 1-2/1-3/2 카메라·화면표시 설정값 데이터 산출
|
||||
|
||||
**소요 시간**: 10분
|
||||
**Context 사용량**: input 12600k / output 4k tokens
|
||||
|
||||
## 요청
|
||||
|
||||
지도(측점 5+800~6+100, 상가교차로)처럼 영상에 지장물이 맵핑되도록
|
||||
카메라·화면표시 옵션 설정값을 찾아 적용.
|
||||
|
||||
## 방법 — 데이터 기반 산출
|
||||
|
||||
- **yawOffset**: 비행 트랙 방위 − 짐벌 방위의 원형 평균(이동 중 표본만).
|
||||
경로 1-1 검증: 추정 −14.0° vs 사용자 보정값 −13.6° → **0.4° 이내 일치**(방법 검증).
|
||||
짐벌 방위 편차는 비행마다 다름(자기 간섭·IMU 초기화) → 폴더별 산출 필요
|
||||
- **경로표고(dronePathZ)**: 각 폴더 비행 구간 측점 z 의 중앙값
|
||||
- 내부 파라미터(초점 29.9 / 센서 27.45×20.85 / 각종 오프셋 0)는
|
||||
같은 카메라(ZenmuseP1)라 1-1 검증값 그대로
|
||||
|
||||
## 적용 (백업 .bak2)
|
||||
|
||||
| 폴더 | yawOffset | 경로표고 | 비고 |
|
||||
|------|-----------|---------|------|
|
||||
| 경로 1-2 | −15.6° | 64m | 기존 offZ 0.6 등 잔재 제거 |
|
||||
| 경로 1-3 | **+3.3°** | 101m | 기존 offZ 21.8 잔재 제거 — 부호 반대 편차였음 |
|
||||
| 경로 2 | −9.8° | 70m | 기존 offZ 21.8 잔재 제거 |
|
||||
|
||||
- camera.json/display.json 모두 새 값으로 교체(각 .bak2 백업), route.json 은 유지
|
||||
- display: 드론높이 OFF·표시거리 400m·선형/궤적 ON 등 1-1 구성 복제
|
||||
|
||||
## 확인/미세조정 안내
|
||||
|
||||
- F5 새로고침 → 해당 폴더 재선택 → 지도의 5+800~6+100·상가교차로 위치와 대조
|
||||
- 남는 미세 오차: 좌우 일괄 → yawOffset ±1° / 상하 원거리 → Pitch ±0.5° /
|
||||
회전 시 지연 → 자세 선행 +0.1~0.3 / 확정 후 'PC 저장'
|
||||
@@ -0,0 +1,31 @@
|
||||
# 2026-07-14 경로 1-3 산출 설정이 더 안 맞음 — 기존 시각 보정값 복원
|
||||
|
||||
**소요 시간**: 9분
|
||||
**Context 사용량**: input 13200k / output 3k tokens
|
||||
|
||||
## 제보 (스크린샷: 5+500~5+700)
|
||||
|
||||
산출 설정(yaw +3.3, 표준 센서) 적용 후 기존보다 더 안 맞음.
|
||||
|
||||
## 분석
|
||||
|
||||
- 노선 위 구간 한정 재산출도 +3.1° — 이탈 구간 오염이 아니라
|
||||
**1-3 은 카메라가 트랙과 나란하지 않은(도로가 화면 우측) 비행**이라
|
||||
'트랙=카메라' 가정 자체가 성립하지 않음 (1-1/1-2 는 −13.9/−14.1 로 일치)
|
||||
- 사용자가 '백업' 폴더로 옮겨둔 기존 설정 확인: **yaw +4.3, pitch −12.9,
|
||||
센서 46.9×36, cy0 −0.01** — 시각 튜닝으로 찾은 조합(광화각+피치 보정이 한 몸)
|
||||
- 결론: 데이터 산출은 카메라·짐벌이 '표준 자세'일 때만 유효 — 1-3 같은
|
||||
변칙 촬영은 시각 튜닝 조합이 우선
|
||||
|
||||
## 조치
|
||||
|
||||
- 경로 1-3 camera.json 을 기존 값으로 복원 (+ timeOffsetSec/attLeadSec 0 추가,
|
||||
offZ 는 0 유지 — 기존에도 0)
|
||||
- display.json 은 산출본 유지(경로표고 101m 등) — 표고 데이터 기준이라 무해
|
||||
- 경로 1-2(−15.6)/경로 2(−9.8) 산출본은 유지 — 확인 후 안 맞으면 각각
|
||||
.bak2(1-2: 폴더 내 / 2: 폴더 내)로 복원 가능
|
||||
|
||||
## 확인
|
||||
|
||||
- F5 새로고침 → 경로 1-3 재선택 → 5+500~5+700 구간과 도로 정합 확인
|
||||
- 미세조정: 좌우 → yawOffset, 원거리 상하 → pitch, 회전 지연 → 자세 선행
|
||||
Reference in New Issue
Block a user