Files
carrotpilot/selfdrive/carrot/web/js/realtime/home_drive.js
2026-06-04 20:44:17 +09:00

4257 lines
165 KiB
JavaScript

"use strict";
window.HomeDrive = (() => {
const stageEl = document.getElementById("carrotStage");
const videoEl = document.getElementById("carrotRoadVideo");
const videoHoldEl = document.getElementById("carrotLastFrameCanvas");
const canvasEl = document.getElementById("carrotOverlayCanvas");
const hudCanvasEl = document.getElementById("carrotHudCanvas");
const onroadAlertEl = document.getElementById("carrotOnroadAlert");
const onroadAlertBoxEl = document.getElementById("carrotOnroadAlertBox");
const onroadAlertText1El = document.getElementById("carrotOnroadAlertText1");
const onroadAlertText2El = document.getElementById("carrotOnroadAlertText2");
const stageLoadingEl = document.getElementById("carrotStageLoading");
const stageLoadingTextEl = document.getElementById("carrotStageLoadingText");
const stageLoadingDetailEl = document.getElementById("carrotStageLoadingDetail");
const statusEl = document.getElementById("carrotStageStatus");
const metaEl = document.getElementById("carrotStageMeta");
const debugEl = document.getElementById("carrotStageDebug");
const driveHudCardEl = document.getElementById("driveHudCard");
const rtcPerfHudEl = document.getElementById("carrotRtcPerfHud");
const rtcPerfGlanceEl = document.getElementById("carrotRtcPerfGlance");
const rtcPerfGlanceTextEl = document.getElementById("carrotRtcPerfGlanceText");
const rtcPerfSummaryEl = document.getElementById("carrotRtcPerfSummary");
const rtcPerfTitleEl = document.getElementById("carrotRtcPerfTitle");
const rtcPerfVideoEl = document.getElementById("carrotRtcPerfVideo");
const rtcPerfCodecEl = document.getElementById("carrotRtcPerfCodec");
const rtcPerfBitrateEl = document.getElementById("carrotRtcPerfBitrate");
const rtcPerfRttEl = document.getElementById("carrotRtcPerfRtt");
const rtcPerfJitterEl = document.getElementById("carrotRtcPerfJitter");
const rtcPerfLossEl = document.getElementById("carrotRtcPerfLoss");
const rtcPerfFreezeEl = document.getElementById("carrotRtcPerfFreeze");
const rtcPerfPathEl = document.getElementById("carrotRtcPerfPath");
const rtcPerfHoldTargetEl = document.getElementById("carrotRtcPerfHoldTarget");
const rtcPerfCloseBtnEl = document.getElementById("carrotRtcPerfCloseBtn");
const rtcPerfLogBtnEl = document.getElementById("carrotRtcPerfLogBtn");
const sourceVideoEl = videoEl;
const displayModeButton = document.getElementById("btnDisplayModeCycle");
if (!stageEl || !videoEl || !canvasEl || !hudCanvasEl || !statusEl || !metaEl || !debugEl) {
return {};
}
const ctx = canvasEl.getContext("2d");
const hudCtx = hudCanvasEl.getContext("2d");
if (!ctx || !hudCtx) {
return {};
}
// Rendering policy:
// - Keep geometry/projection in the web renderer.
// - Mirror native openpilot/Carrot style and animation rules instead of inventing web-only variants.
// - When UI params exist on-device, treat them as the source of truth so web follows native behavior automatically.
// Future visual changes should extend the native rule port first, not add disconnected styling here.
const VIEW_FROM_DEVICE = [
[0, 1, 0],
[0, 0, 1],
[1, 0, 0],
];
const BASE_CAMERA = {
width: 1928,
height: 1208,
focalX: 2648,
focalY: 2648,
};
const DISPLAY_MODES = [
{ key: "fit", labelKey: "display_fit", fallbackLabel: "Fit" },
{ key: "normal", labelKey: "display_normal", fallbackLabel: "Normal" },
{ key: "crop", labelKey: "display_crop", fallbackLabel: "Crop" },
];
const HUD_TEXT_FONT = "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
const DISPLAY_MODE_STORAGE_KEY = "home_drive_display_mode_index";
const PHONE_PORTRAIT_DPR_CAP = 1.0;
const MOBILE_DPR_CAP = 1.25;
const DESKTOP_DPR_CAP = 1.5;
const RENDER_INTERVAL_MS = 33; // ~30fps for denser plot data (C3: 20Hz/50ms)
const CAMERA_FRAME_RECHECK_MS = 250;
const RTC_PERF_HOLD_MS = 600;
const RTC_PERF_HOLD_MOVE_PX = 12;
const RTC_PERF_SUMMARY_AUTO_CLOSE_MS = 8000;
const MIN_ROAD_VIDEO_WIDTH = 320;
const MIN_ROAD_VIDEO_HEIGHT = 180;
const PATH_PALETTE = [
{ r: 255, g: 82, b: 82 },
{ r: 255, g: 153, b: 0 },
{ r: 255, g: 214, b: 74 },
{ r: 83, g: 220, b: 118 },
{ r: 78, g: 144, b: 255 },
{ r: 0, g: 0, b: 128 },
{ r: 139, g: 0, b: 255 },
{ r: 191, g: 150, b: 87 },
{ r: 255, g: 255, b: 255 },
{ r: 28, g: 28, b: 28 },
];
const PLOT_SERIES = [
{ color: "#f6d94f", label: "Y" },
{ color: "#58d97d", label: "G" },
{ color: "#ff9f43", label: "O" },
];
const PLOT_MAX_POINTS = 600; // ~20s at 30fps (C3: 400 at 20fps = 20s)
const PATH_HALF_WIDTH = 0.9;
const PATH_Z_OFFSET = 1.22;
const LONG_PLAN_SOURCE_LEAD1 = 2;
const MIN_DRAW_DISTANCE = 10;
const MAX_DRAW_DISTANCE = 100;
const RADAR_INTERPOLATION_MIN_MS = 16;
const RADAR_INTERPOLATION_DEFAULT_MS = 50;
const RADAR_INTERPOLATION_MAX_MS = 120;
const RADAR_INTERPOLATION_LEAD_MS = 12;
const TEST_PATH_VISIBILITY_SOLID_ALPHA = 0.50;
const TEST_PATH_VISIBILITY_MID_ALPHA = 0.24;
const TEST_LANE_PROB_MIN = 0.003;
const TEST_LANE_PROB_BOOST = 6;
const ALERT_STATUS_NORMAL = 0;
const ALERT_STATUS_USER_PROMPT = 1;
const ALERT_STATUS_CRITICAL = 2;
const ALERT_SIZE_NONE = 0;
const ALERT_SIZE_SMALL = 1;
const ALERT_SIZE_MID = 2;
const ALERT_SIZE_FULL = 3;
const ONROAD_ALERT_SCALE = 1.5;
const POLYLINE_SMOOTH_NEAR_DISTANCE = 16;
const POLYLINE_SMOOTH_FAR_DISTANCE = 52;
const POLYLINE_SMOOTH_MAX_STRENGTH = 0.34;
const POLYLINE_CENTER_SMOOTH_MAX_STRENGTH = 0.24;
const PATH_TEMPORAL_SMOOTH_ALPHA = 0.20;
const LANE_TEMPORAL_SMOOTH_ALPHA = 0.16;
const LANE_VISUAL_WIDTH_GAIN = 1.4;
const GEOMETRY_QUALITY_DEFAULT = "default";
const GEOMETRY_QUALITY_LANE = "lane";
const GEOMETRY_QUALITY_ROAD_EDGE = "road-edge";
const defaultParams = {
IsMetric: 1,
ShowPathEnd: 0,
ShowLaneInfo: 2,
ShowPathMode: 0,
ShowPathColor: 13,
ShowPathModeLane: 0,
ShowPathColorLane: 13,
ShowPathColorCruiseOff: 19,
ShowPathWidth: 100,
ShowPlotMode: 0,
ShowRadarInfo: 0,
RadarLatFactor: 0,
CustomSR: 0,
};
const overlayInfoState = {
carLabel: "",
branchLabel: "",
lastSignature: "",
loading: false,
nextRetryAt: 0,
};
function isMetricDisplay() {
return finiteNumber(paramsState.IsMetric, defaultParams.IsMetric) !== 0;
}
function displayDistanceMeters(distanceMeters) {
const distance = Number(distanceMeters);
if (!Number.isFinite(distance)) return NaN;
return isMetricDisplay() ? distance : distance * 3.28084;
}
let paramsState = { ...defaultParams };
let displayModeIndex = 1;
let overlaySizeSignature = "";
let hudSizeSignature = "";
let transformSignature = "";
let lastStatus = "";
let lastMeta = "";
let lastDebug = "";
let lastPlotMode = -1;
let radarInterpolationState = {
signature: "",
previous: null,
current: null,
previousAtMs: 0,
currentAtMs: 0,
};
/* ── EMA state for lead box smoothing ──
* C3 uses alpha=0.85 at a stable 20Hz UI loop. The web's actual frame
* rate varies, so we use time-based EMA: alpha_adj = 0.85^(dt/50ms).
* This guarantees the same wall-clock convergence speed as C3. */
const LEAD_EMA_ALPHA = 0.85;
const C3_FRAME_MS = 50; // C3 UI loop interval (~20Hz)
let leadEmaState = [
{ fx: NaN, fy: NaN, fw: NaN, trackId: -1, lastMs: 0 }, // slot 0: leadOne
{ fx: NaN, fy: NaN, fw: NaN, trackId: -1, lastMs: 0 }, // slot 1: leadTwo
];
let leadTwoEmaState = { xl: NaN, xr: NaN, y: NaN, lastMs: 0 };
/* ── Lead hold state: keeps box visible briefly when status flickers false ──
* C3 reads SubMaster at stable 20Hz; web's merged WebSocket state can
* briefly lose status between messages. Hold last valid box for up to
* LEAD_HOLD_MS so the visual experience matches C3's stability. */
const LEAD_HOLD_MS = 1500;
let leadHoldState = {
lastValidMs: 0,
box: null,
strokeColor: null,
isLeadScc: false,
radarDist: 0,
visionDist: 0,
badgeTextColor: "#ffffff",
};
let leadRenderState = {
lastCameraFrameId: NaN,
lastModelFrameId: NaN,
lastSourceWidth: NaN,
lastSourceHeight: NaN,
};
/* ── Phase 1-2: dirty check ── */
let _lastOverlaySig = "";
let _lastHudSig = "";
let _lastPlotInputSig = "";
let _lastAlertSig = "";
let _forceNextRender = true;
let _lastRenderTime = 0;
let _renderRafId = null;
let _renderTimerId = null;
let _renderVideoFrameId = null;
let _cameraFrameRecheckId = null;
let _roadCameraStreamState = {
stream: null,
decodedFramesAtBind: null,
currentTimeAtBind: 0,
firstRenderableSeen: false,
};
let _pendingRenderState = {
force: true,
overlayDirty: true,
hudDirty: true,
};
const _mergeRuntimeCache = {
refs: null,
result: null,
};
let _frameProjectionCache = {
pathLengthIdx: new WeakMap(),
ribbon: new WeakMap(),
verticalRibbon: new WeakMap(),
pathZ: new WeakMap(),
pathY: new WeakMap(),
};
function pathDataSignature(pathData) {
const x = Array.isArray(pathData?.x) ? pathData.x : [];
const y = Array.isArray(pathData?.y) ? pathData.y : [];
if (!x.length || !y.length) return "none";
const lastIndex = Math.min(x.length, y.length) - 1;
const midIndex = Math.min(lastIndex, Math.max(0, Math.floor(lastIndex / 2)));
const farIndex = Math.min(lastIndex, 16);
return [
x.length,
finiteNumber(x[0], 0).toFixed(2),
finiteNumber(y[0], 0).toFixed(2),
finiteNumber(x[midIndex], 0).toFixed(2),
finiteNumber(y[midIndex], 0).toFixed(2),
finiteNumber(x[farIndex], 0).toFixed(2),
finiteNumber(y[farIndex], 0).toFixed(2),
finiteNumber(x[lastIndex], 0).toFixed(2),
finiteNumber(y[lastIndex], 0).toFixed(2),
].join("|");
}
function plotInputSignature(plotData) {
if (!plotData) return "off";
return [
plotData.mode,
plotData.title,
finiteNumber(plotData.values?.[0], 0).toFixed(3),
finiteNumber(plotData.values?.[1], 0).toFixed(3),
finiteNumber(plotData.values?.[2], 0).toFixed(3),
].join("|");
}
function overlayDataSignature(hudState, overlayState, selectedPath, pathStyle, showLaneInfo) {
const model = overlayState?.modelV2;
const radar = overlayState?.radarState;
const liveCalibration = overlayState?.liveCalibration;
const carState = hudState?.carState;
const controlsState = hudState?.controlsState;
const longPlan = hudState?.longitudinalPlan;
return [
model?.frameId ?? "-",
selectedPath?.pathSource || "none",
pathDataSignature(selectedPath?.pathData),
radarStateSignature(radar),
finiteNumber(liveCalibration?.rpyCalib?.[0], 0).toFixed(3),
finiteNumber(liveCalibration?.rpyCalib?.[1], 0).toFixed(3),
finiteNumber(liveCalibration?.rpyCalib?.[2], 0).toFixed(3),
finiteNumber(liveCalibration?.height?.[0], 0).toFixed(2),
Boolean(controlsState?.activeLaneLine) ? 1 : 0,
Boolean(controlsState?.enabled) ? 1 : 0,
finiteNumber(carState?.useLaneLineSpeed, 0).toFixed(2),
Boolean(carState?.brakeLights) ? 1 : 0,
Boolean(overlayState?.carControl?.longActive) ? 1 : 0,
longPlan?.xState ?? "-",
longPlan?.trafficState ?? "-",
longPlan?.longitudinalPlanSource ?? "-",
showLaneInfo,
pathStyle?.mode ?? 0,
pathStyle?.colorIndex ?? 0,
paramsState.ShowPathMode,
paramsState.ShowPathColor,
paramsState.ShowPathModeLane,
paramsState.ShowPathColorLane,
paramsState.ShowPathColorCruiseOff,
paramsState.ShowPathWidth,
paramsState.ShowPathEnd,
paramsState.ShowLaneInfo,
paramsState.ShowRadarInfo,
paramsState.IsMetric,
].join("|");
}
function hudDataSignature(hudState, overlayState, plotData, selectedPath, debugText) {
const carState = hudState?.carState;
const carrotMan = hudState?.carrotMan;
const longPlan = hudState?.longitudinalPlan;
const selfdriveState = hudState?.selfdriveState;
const rtcPerfText = formatRtcPerfLabel();
return [
finiteNumber(carState?.vEgo, 0).toFixed(3),
finiteNumber(carState?.vEgoCluster, 0).toFixed(3),
finiteNumber(carState?.vCruiseCluster, 0).toFixed(2),
carrotMan?.xSpdLimit ?? "-",
carrotMan?.nRoadLimitSpeed ?? "-",
carrotMan?.desiredSpeed ?? "-",
carrotMan?.activeCarrot ?? "-",
selfdriveState?.personality ?? "-",
selfdriveState?.alertStatus ?? "-",
selfdriveState?.alertSize ?? "-",
selfdriveState?.alertType || "",
selfdriveState?.alertText1 || "",
selfdriveState?.alertText2 || "",
longPlan?.myDrivingMode ?? "-",
longPlan?.tFollow ?? "-",
longPlan?.desiredDistance ?? "-",
overlayState?.roadCameraState?.frameId ?? "-",
selectedPath?.latDebugText || "",
debugText || "",
rtcPerfText,
plotInputSignature(plotData),
paramsState.ShowPlotMode,
paramsState.CustomSR,
paramsState.IsMetric,
].join("|");
}
/* ── Phase 1-3: gradient cache ── */
const _gradientCache = new Map();
const GRADIENT_CACHE_MAX = 16;
const _hudGradientCache = new Map();
const HUD_GRADIENT_CACHE_MAX = 8;
const _roundedRectPathCache = new Map();
const ROUNDED_RECT_CACHE_MAX = 24;
const _textWidthCache = new Map();
const TEXT_WIDTH_CACHE_MAX = 256;
function getCachedGradient(key, factory) {
const cached = _gradientCache.get(key);
if (cached) return cached;
const g = factory();
if (_gradientCache.size >= GRADIENT_CACHE_MAX) {
const firstKey = _gradientCache.keys().next().value;
_gradientCache.delete(firstKey);
}
_gradientCache.set(key, g);
return g;
}
function getCachedHudGradient(key, factory) {
const cached = _hudGradientCache.get(key);
if (cached) return cached;
const gradient = factory();
if (_hudGradientCache.size >= HUD_GRADIENT_CACHE_MAX) {
const firstKey = _hudGradientCache.keys().next().value;
_hudGradientCache.delete(firstKey);
}
_hudGradientCache.set(key, gradient);
return gradient;
}
function getCachedTextWidth(canvasCtx, font, text) {
const label = String(text || "");
const key = `${font}|${label}`;
const cached = _textWidthCache.get(key);
if (cached != null) return cached;
canvasCtx.save();
canvasCtx.font = font;
const width = canvasCtx.measureText(label).width;
canvasCtx.restore();
if (_textWidthCache.size >= TEXT_WIDTH_CACHE_MAX) {
const firstKey = _textWidthCache.keys().next().value;
_textWidthCache.delete(firstKey);
}
_textWidthCache.set(key, width);
return width;
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function finiteNumber(value, fallback = 0) {
const num = Number(value);
return Number.isFinite(num) ? num : fallback;
}
function finiteParamNumber(value, fallback = 0) {
if (value == null) return fallback;
if (typeof value === "string" && !value.trim()) return fallback;
const num = Number(value);
return Number.isFinite(num) ? num : fallback;
}
function hasEnumerableKeys(value) {
if (!value || typeof value !== "object") return false;
for (const _key in value) {
return true;
}
return false;
}
function readRpyTriplet(liveCalibration) {
const source = Array.isArray(liveCalibration?.rpyCalib) ? liveCalibration.rpyCalib : null;
if (!source) return null;
const roll = Number(source[0]);
const pitch = Number(source[1]);
const yaw = Number(source[2]);
if (!Number.isFinite(roll) || !Number.isFinite(pitch) || !Number.isFinite(yaw)) return null;
return [roll, pitch, yaw];
}
function formatRpyTriplet(liveCalibration) {
const rpy = readRpyTriplet(liveCalibration);
if (!rpy) return "-";
return `${rpy[0].toFixed(3)},${rpy[1].toFixed(3)},${rpy[2].toFixed(3)}`;
}
function firstFinite(values, fallback = 0) {
if (!Array.isArray(values)) return fallback;
for (const value of values) {
const num = Number(value);
if (Number.isFinite(num)) return num;
}
return fallback;
}
function shortText(value, maxLength = 88) {
const text = String(value || "").trim();
if (!text) return "";
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
}
/* Phase 3: rgba string cache */
const _rgbaCache = new Map();
const RGBA_CACHE_MAX = 64;
const _emptyDash = [];
let _temporalRibbonState = new Map();
function rgba(rgb, alpha) {
const a = clamp(alpha, 0, 1);
const key = (rgb.r << 20) | (rgb.g << 10) | rgb.b | ((a * 1000 | 0) << 24);
let s = _rgbaCache.get(key);
if (s) return s;
s = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a.toFixed(3)})`;
if (_rgbaCache.size >= RGBA_CACHE_MAX) {
_rgbaCache.delete(_rgbaCache.keys().next().value);
}
_rgbaCache.set(key, s);
return s;
}
function paletteColor(index) {
const normalized = ((Number(index) % PATH_PALETTE.length) + PATH_PALETTE.length) % PATH_PALETTE.length;
return PATH_PALETTE[normalized] || PATH_PALETTE[3];
}
function resetFrameProjectionCache() {
_frameProjectionCache = {
pathLengthIdx: new WeakMap(),
ribbon: new WeakMap(),
verticalRibbon: new WeakMap(),
pathZ: new WeakMap(),
pathY: new WeakMap(),
};
}
function resetTemporalRibbonState() {
_temporalRibbonState = new Map();
}
function getWeakCacheBucket(weakMap, target) {
if (!target || typeof target !== "object") return null;
let bucket = weakMap.get(target);
if (!bucket) {
bucket = new Map();
weakMap.set(target, bucket);
}
return bucket;
}
function getProjectionSampleStride(distance, maxDistance) {
const dist = finiteNumber(distance, 0);
const maxDist = finiteNumber(maxDistance, MAX_DRAW_DISTANCE);
if (maxDist <= 36) return 1;
if (dist >= Math.min(maxDist * 0.72, 62)) return 3;
if (dist >= Math.min(maxDist * 0.44, 34)) return 2;
return 1;
}
function getProjectionSampleStrideForQuality(distance, maxDistance, quality = GEOMETRY_QUALITY_DEFAULT) {
const baseStride = getProjectionSampleStride(distance, maxDistance);
const dist = finiteNumber(distance, 0);
if (quality === GEOMETRY_QUALITY_ROAD_EDGE) {
if (dist >= 64) return Math.min(baseStride + 2, 5);
if (dist >= 40) return Math.min(baseStride + 1, 4);
return baseStride;
}
if (quality === GEOMETRY_QUALITY_LANE) {
if (dist >= 58) return Math.min(baseStride + 1, 4);
return baseStride;
}
return baseStride;
}
function forEachProjectedSampleIndex(xs, maxIdx, maxDistance, visitor, quality = GEOMETRY_QUALITY_DEFAULT) {
if (maxIdx < 0) return;
let i = 0;
let lastVisited = -1;
while (i <= maxIdx) {
visitor(i);
lastVisited = i;
i += getProjectionSampleStrideForQuality(xs[i], maxDistance, quality);
}
if (lastVisited !== maxIdx) visitor(maxIdx);
}
function getGeometrySmoothingGain(quality, axis = "side") {
if (quality === GEOMETRY_QUALITY_ROAD_EDGE) {
return axis === "center" ? 1.12 : 1.22;
}
if (quality === GEOMETRY_QUALITY_LANE) {
return axis === "center" ? 1.06 : 1.12;
}
return 1.0;
}
function getCachedRoundedRectPath(width, height, radius) {
if (typeof Path2D !== "function") return null;
const w = Math.max(0, finiteNumber(width, 0));
const h = Math.max(0, finiteNumber(height, 0));
const r = Math.min(finiteNumber(radius, 0), w / 2, h / 2);
const key = `${Math.round(w * 2)}|${Math.round(h * 2)}|${Math.round(r * 2)}`;
const cached = _roundedRectPathCache.get(key);
if (cached) return cached;
const path = new Path2D();
path.moveTo(r, 0);
path.lineTo(w - r, 0);
path.quadraticCurveTo(w, 0, w, r);
path.lineTo(w, h - r);
path.quadraticCurveTo(w, h, w - r, h);
path.lineTo(r, h);
path.quadraticCurveTo(0, h, 0, h - r);
path.lineTo(0, r);
path.quadraticCurveTo(0, 0, r, 0);
path.closePath();
if (_roundedRectPathCache.size >= ROUNDED_RECT_CACHE_MAX) {
_roundedRectPathCache.delete(_roundedRectPathCache.keys().next().value);
}
_roundedRectPathCache.set(key, path);
return path;
}
function mixPoint(a, b, ratio) {
return {
x: a.x + (b.x - a.x) * ratio,
y: a.y + (b.y - a.y) * ratio,
};
}
function getPolylineSmoothingStrength(distance, maxDistance, maxStrength) {
const dist = finiteNumber(distance, 0);
const smoothFarDistance = Math.max(
POLYLINE_SMOOTH_FAR_DISTANCE,
Math.min(finiteNumber(maxDistance, MAX_DRAW_DISTANCE), MAX_DRAW_DISTANCE),
);
if (dist <= POLYLINE_SMOOTH_NEAR_DISTANCE) return 0;
const ratio = clamp(
(dist - POLYLINE_SMOOTH_NEAR_DISTANCE) /
Math.max(1, smoothFarDistance - POLYLINE_SMOOTH_NEAR_DISTANCE),
0,
1,
);
return maxStrength * ratio;
}
function smoothProjectedPolyline(points, distances, maxDistance, maxStrength = POLYLINE_SMOOTH_MAX_STRENGTH) {
if (!Array.isArray(points) || points.length < 3) return points;
let smoothed = null;
for (let i = 1; i < points.length - 1; i += 1) {
const strength = getPolylineSmoothingStrength(distances?.[i], maxDistance, maxStrength);
if (strength <= 0.001) continue;
const prev = points[i - 1];
const current = points[i];
const next = points[i + 1];
const targetX = prev.x * 0.25 + current.x * 0.5 + next.x * 0.25;
const targetY = prev.y * 0.25 + current.y * 0.5 + next.y * 0.25;
if (!smoothed) {
smoothed = points.map((point) => ({ x: point.x, y: point.y }));
}
smoothed[i].x = current.x + (targetX - current.x) * strength;
smoothed[i].y = current.y + (targetY - current.y) * strength;
}
return smoothed || points;
}
function smoothPointListTemporal(previous, next, alpha) {
if (!Array.isArray(previous) || !Array.isArray(next) || previous.length !== next.length) {
return next.map((point) => ({ x: point.x, y: point.y }));
}
const a = clamp(alpha, 0, 1);
return next.map((point, index) => ({
x: previous[index].x + (point.x - previous[index].x) * a,
y: previous[index].y + (point.y - previous[index].y) * a,
}));
}
function smoothRibbonTemporal(key, ribbon, alpha) {
if (!key || !ribbon?.polygon?.length) return ribbon;
const previous = _temporalRibbonState.get(key);
const left = smoothPointListTemporal(previous?.left, ribbon.left, alpha);
const right = smoothPointListTemporal(previous?.right, ribbon.right, alpha);
const center = smoothPointListTemporal(previous?.center, ribbon.center, alpha);
const next = {
left,
right,
center,
polygon: left.length >= 2 && right.length >= 2 ? left.concat([...right].reverse()) : [],
};
_temporalRibbonState.set(key, next);
return next;
}
function mat3Multiply(a, b) {
return [
[
a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0],
a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1],
a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2],
],
[
a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0],
a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1],
a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2],
],
[
a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0],
a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1],
a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2],
],
];
}
/* Phase 3: reuse array to avoid per-call allocation */
const _mv3Out = [0, 0, 0];
function mat3Vector(a, v) {
_mv3Out[0] = a[0][0] * v[0] + a[0][1] * v[1] + a[0][2] * v[2];
_mv3Out[1] = a[1][0] * v[0] + a[1][1] * v[1] + a[1][2] * v[2];
_mv3Out[2] = a[2][0] * v[0] + a[2][1] * v[1] + a[2][2] * v[2];
return _mv3Out;
}
function rotFromEuler(roll, pitch, yaw) {
const sr = Math.sin(roll);
const cr = Math.cos(roll);
const sp = Math.sin(pitch);
const cp = Math.cos(pitch);
const sy = Math.sin(yaw);
const cy = Math.cos(yaw);
return [
[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
[sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
[-sp, cp * sr, cp * cr],
];
}
function getCalibrationMatrix(liveCalibration) {
const rpy = readRpyTriplet(liveCalibration);
if (!rpy) return VIEW_FROM_DEVICE;
return mat3Multiply(VIEW_FROM_DEVICE, rotFromEuler(rpy[0], rpy[1], rpy[2]));
}
function getIntrinsics(videoWidth, videoHeight) {
const scaleX = videoWidth / BASE_CAMERA.width;
const scaleY = videoHeight / BASE_CAMERA.height;
return [
[BASE_CAMERA.focalX * scaleX, 0, videoWidth / 2],
[0, BASE_CAMERA.focalY * scaleY, videoHeight / 2],
[0, 0, 1],
];
}
function getDisplayScale(videoWidth, videoHeight, stageWidth, stageHeight) {
const containScale = Math.min(stageWidth / videoWidth, stageHeight / videoHeight);
const coverScale = Math.max(stageWidth / videoWidth, stageHeight / videoHeight);
const mode = DISPLAY_MODES[displayModeIndex] || DISPLAY_MODES[1];
const isPortrait = stageHeight > stageWidth;
let scale = containScale;
if (mode.key === "fit") {
scale = containScale * 0.94;
} else if (mode.key === "crop") {
scale = coverScale;
} else if (mode.key === "normal" && isPortrait) {
scale = containScale * 0.985;
}
return {
mode,
containScale,
coverScale,
scale: Math.max(scale, 0.01),
};
}
function getStageTransform(videoWidth, videoHeight, stageWidth, stageHeight, calibration) {
const intrinsics = getIntrinsics(videoWidth, videoHeight);
const calibTransform = mat3Multiply(intrinsics, calibration);
const display = getDisplayScale(videoWidth, videoHeight, stageWidth, stageHeight);
const scale = display.scale;
const centerX = intrinsics[0][2];
const centerY = intrinsics[1][2];
const infinity = mat3Vector(calibTransform, [1000, 0, 0]);
const projectedX = infinity[2] > 1e-3 ? infinity[0] / infinity[2] : centerX;
const projectedY = infinity[2] > 1e-3 ? infinity[1] / infinity[2] : centerY;
const maxXOffset = Math.max(0, centerX * scale - stageWidth / 2 - 5);
const maxYOffset = Math.max(0, centerY * scale - stageHeight / 2 - 5);
const xOffset = clamp((projectedX - centerX) * scale, -maxXOffset, maxXOffset);
const yOffset = clamp((projectedY - centerY) * scale, -maxYOffset, maxYOffset);
return {
calibTransform,
displayMode: display.mode,
scale,
containScale: display.containScale,
coverScale: display.coverScale,
tx: (stageWidth / 2 - xOffset) - (centerX * scale),
ty: (stageHeight / 2 - yOffset) - (centerY * scale),
};
}
function getHudViewportRect(videoWidth, videoHeight, stageWidth, stageHeight, transform) {
const rawLeft = finiteNumber(transform?.tx, 0);
const rawTop = finiteNumber(transform?.ty, 0);
const rawRight = rawLeft + videoWidth * Math.max(finiteNumber(transform?.scale, 1), 0.01);
const rawBottom = rawTop + videoHeight * Math.max(finiteNumber(transform?.scale, 1), 0.01);
const left = clamp(Math.min(rawLeft, rawRight), 0, stageWidth);
const right = clamp(Math.max(rawLeft, rawRight), 0, stageWidth);
const top = clamp(Math.min(rawTop, rawBottom), 0, stageHeight);
const bottom = clamp(Math.max(rawTop, rawBottom), 0, stageHeight);
if (right - left < 2 || bottom - top < 2) {
return {
left: 0,
top: 0,
right: stageWidth,
bottom: stageHeight,
width: stageWidth,
height: stageHeight,
centerX: stageWidth / 2,
centerY: stageHeight / 2,
};
}
return {
left,
top,
right,
bottom,
width: right - left,
height: bottom - top,
centerX: (left + right) / 2,
centerY: (top + bottom) / 2,
};
}
function getVisibleSourceRect(videoWidth, videoHeight, stageWidth = videoWidth, stageHeight = videoHeight, transform = null) {
const scale = Math.max(finiteNumber(transform?.scale, 1), 0.01);
const tx = finiteNumber(transform?.tx, 0);
const ty = finiteNumber(transform?.ty, 0);
const rawLeft = (0 - tx) / scale;
const rawTop = (0 - ty) / scale;
const rawRight = (stageWidth - tx) / scale;
const rawBottom = (stageHeight - ty) / scale;
const left = clamp(Math.min(rawLeft, rawRight), 0, videoWidth);
const right = clamp(Math.max(rawLeft, rawRight), 0, videoWidth);
const top = clamp(Math.min(rawTop, rawBottom), 0, videoHeight);
const bottom = clamp(Math.max(rawTop, rawBottom), 0, videoHeight);
if (right - left < 2 || bottom - top < 2) {
return {
left: 0,
top: 0,
right: videoWidth,
bottom: videoHeight,
width: videoWidth,
height: videoHeight,
};
}
return {
left,
top,
right,
bottom,
width: right - left,
height: bottom - top,
};
}
function projectPoint(calibTransform, x, y, z) {
mat3Vector(calibTransform, [x, y, z]);
if (!Number.isFinite(_mv3Out[2]) || _mv3Out[2] <= 1e-3) return null;
return {
x: (_mv3Out[0] / _mv3Out[2]) | 0,
y: (_mv3Out[1] / _mv3Out[2]) | 0,
};
}
function projectPointPrecise(calibTransform, x, y, z) {
mat3Vector(calibTransform, [x, y, z]);
if (!Number.isFinite(_mv3Out[2]) || _mv3Out[2] <= 1e-3) return null;
return {
x: _mv3Out[0] / _mv3Out[2],
y: _mv3Out[1] / _mv3Out[2],
};
}
function getPathLengthIdx(line, maxDistance) {
const cacheBucket = getWeakCacheBucket(_frameProjectionCache.pathLengthIdx, line);
const cacheKey = Number.isFinite(Number(maxDistance))
? Math.round(Number(maxDistance) * 100)
: "default";
if (cacheBucket?.has(cacheKey)) return cacheBucket.get(cacheKey);
const xs = Array.isArray(line?.x) ? line.x : [];
let maxIdx = 0;
for (let i = 1; i < xs.length; i += 1) {
if (Number(xs[i]) > maxDistance) break;
maxIdx = i;
}
cacheBucket?.set(cacheKey, maxIdx);
return maxIdx;
}
function getHeldLeadBox(nowMs = performance.now()) {
if (!leadHoldState.box) return null;
if ((nowMs - finiteNumber(leadHoldState.lastValidMs, 0)) >= LEAD_HOLD_MS) return null;
return leadHoldState.box;
}
function getPrimaryLeadDistance(overlayState = null, nowMs = performance.now()) {
const leadOne = overlayState?.radarState?.leadOne;
const liveDistance = finiteNumber(leadOne?.dRel, NaN);
if (Boolean(leadOne?.status) && Number.isFinite(liveDistance) && liveDistance > 0) {
return liveDistance;
}
const heldDistance = finiteNumber(getHeldLeadBox(nowMs)?.dRel, NaN);
if (Number.isFinite(heldDistance) && heldDistance > 0) {
return heldDistance;
}
return NaN;
}
function getSceneMaxDistance(model, overlayState = null) {
const positionXs = Array.isArray(model?.position?.x) ? model.position.x : [];
const tailX = finiteNumber(positionXs[positionXs.length - 1], MIN_DRAW_DISTANCE);
let maxDistance = clamp(tailX, MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
const leadDistance = getPrimaryLeadDistance(overlayState);
if (Number.isFinite(leadDistance) && leadDistance > 0) {
maxDistance = Math.min(maxDistance, leadDistance);
}
return maxDistance;
}
function getPathMaxDistance(sceneMaxDistance) {
return Math.max(0, finiteNumber(sceneMaxDistance, 0) - 2.0);
}
function buildRibbon(calibTransform, line, halfWidth, zOffset, maxDistance, allowInvert = false, centerShift = 0, quality = GEOMETRY_QUALITY_DEFAULT) {
const cacheBucket = getWeakCacheBucket(_frameProjectionCache.ribbon, line);
const cacheKey = [
Math.round(finiteNumber(halfWidth, 0) * 1000),
Math.round(finiteNumber(zOffset, 0) * 1000),
Math.round(finiteNumber(maxDistance, MAX_DRAW_DISTANCE) * 100),
allowInvert ? 1 : 0,
Math.round(finiteNumber(centerShift, 0) * 1000),
quality,
].join("|");
if (cacheBucket?.has(cacheKey)) return cacheBucket.get(cacheKey);
const xs = Array.isArray(line?.x) ? line.x : [];
const ys = Array.isArray(line?.y) ? line.y : [];
const zs = Array.isArray(line?.z) ? line.z : [];
const left = [];
const right = [];
const center = [];
const distances = [];
const maxIdx = getPathLengthIdx(line, maxDistance);
forEachProjectedSampleIndex(xs, maxIdx, maxDistance, (i) => {
const x = finiteNumber(xs[i], NaN);
if (!Number.isFinite(x) || x < 0) return;
const y = finiteNumber(ys[i], 0) + centerShift;
const z = finiteNumber(zs[i], 0) + zOffset;
const leftPt = projectPointPrecise(calibTransform, x, y - halfWidth, z);
const rightPt = projectPointPrecise(calibTransform, x, y + halfWidth, z);
const centerPt = projectPointPrecise(calibTransform, x, y, z);
if (!leftPt || !rightPt || !centerPt) return;
if (!allowInvert && center.length && centerPt.y > center[center.length - 1].y) return;
left.push(leftPt);
right.push(rightPt);
center.push(centerPt);
distances.push(x);
}, quality);
const smoothMaxDistance = finiteNumber(maxDistance, MAX_DRAW_DISTANCE);
const sideSmoothGain = getGeometrySmoothingGain(quality, "side");
const centerSmoothGain = getGeometrySmoothingGain(quality, "center");
const smoothedLeft = smoothProjectedPolyline(left, distances, smoothMaxDistance, POLYLINE_SMOOTH_MAX_STRENGTH * sideSmoothGain);
const smoothedRight = smoothProjectedPolyline(right, distances, smoothMaxDistance, POLYLINE_SMOOTH_MAX_STRENGTH * sideSmoothGain);
const smoothedCenter = smoothProjectedPolyline(center, distances, smoothMaxDistance, POLYLINE_CENTER_SMOOTH_MAX_STRENGTH * centerSmoothGain);
const result = {
left: smoothedLeft,
right: smoothedRight,
center: smoothedCenter,
polygon: smoothedLeft.length >= 2 && smoothedRight.length >= 2 ? smoothedLeft.concat([...smoothedRight].reverse()) : [],
};
cacheBucket?.set(cacheKey, result);
return result;
}
function drawPolygon(points, fillStyle, strokeStyle = "", lineWidth = 1) {
if (!Array.isArray(points) || points.length < 3) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i += 1) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
if (strokeStyle) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeStyle;
ctx.stroke();
}
}
function drawPolyline(points, strokeStyle, lineWidth, dashPattern = [], dashOffset = 0) {
if (!Array.isArray(points) || points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i += 1) {
ctx.lineTo(points[i].x, points[i].y);
}
if (dashPattern.length) {
ctx.setLineDash(dashPattern);
ctx.lineDashOffset = dashOffset;
} else {
ctx.setLineDash(_emptyDash);
}
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeStyle;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.stroke();
}
function buildBandPolygon(left, right, startRatio, endRatio) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length < 2 || right.length < 2 || left.length !== right.length) {
return [];
}
const first = [];
const second = [];
for (let i = 0; i < left.length; i += 1) {
first.push(mixPoint(left[i], right[i], startRatio));
second.push(mixPoint(left[i], right[i], endRatio));
}
return first.concat(second.reverse());
}
function interp1D(x, xs, ys) {
if (!Array.isArray(xs) || !Array.isArray(ys) || xs.length < 2 || ys.length < 2) return NaN;
const target = finiteNumber(x, NaN);
if (!Number.isFinite(target)) return NaN;
const lastIdx = Math.min(xs.length, ys.length) - 1;
if (target <= finiteNumber(xs[0], 0)) return finiteNumber(ys[0], NaN);
for (let i = 1; i <= lastIdx; i += 1) {
const x0 = finiteNumber(xs[i - 1], NaN);
const x1 = finiteNumber(xs[i], NaN);
if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
if (target > x1 && i < lastIdx) continue;
const y0 = finiteNumber(ys[i - 1], NaN);
const y1 = finiteNumber(ys[i], NaN);
if (!Number.isFinite(y0) || !Number.isFinite(y1)) return NaN;
if (Math.abs(x1 - x0) < 1e-5) return y1;
const ratio = clamp((target - x0) / (x1 - x0), 0, 1);
return y0 + (y1 - y0) * ratio;
}
return finiteNumber(ys[lastIdx], NaN);
}
function normalizeVisualParams(values, fallback = defaultParams) {
const source = values && typeof values === "object" ? values : {};
return {
IsMetric: finiteParamNumber(source.IsMetric, fallback.IsMetric),
ShowPathEnd: finiteParamNumber(source.ShowPathEnd, fallback.ShowPathEnd),
ShowLaneInfo: finiteParamNumber(source.ShowLaneInfo, fallback.ShowLaneInfo),
ShowPathMode: finiteParamNumber(source.ShowPathMode, fallback.ShowPathMode),
ShowPathColor: finiteParamNumber(source.ShowPathColor, fallback.ShowPathColor),
ShowPathModeLane: finiteParamNumber(source.ShowPathModeLane, fallback.ShowPathModeLane),
ShowPathColorLane: finiteParamNumber(source.ShowPathColorLane, fallback.ShowPathColorLane),
ShowPathColorCruiseOff: finiteParamNumber(source.ShowPathColorCruiseOff, fallback.ShowPathColorCruiseOff),
ShowPathWidth: finiteParamNumber(source.ShowPathWidth, fallback.ShowPathWidth),
ShowPlotMode: finiteParamNumber(source.ShowPlotMode, fallback.ShowPlotMode),
ShowRadarInfo: finiteParamNumber(source.ShowRadarInfo, fallback.ShowRadarInfo),
RadarLatFactor: finiteParamNumber(source.RadarLatFactor, fallback.RadarLatFactor),
CustomSR: finiteParamNumber(source.CustomSR, fallback.CustomSR),
};
}
function readLiveRuntimeParams() {
const runtimeParams = window.CarrotLiveRuntimeState?.runtime?.params;
if (!runtimeParams || typeof runtimeParams !== "object") return null;
const normalized = normalizeVisualParams(runtimeParams, paramsState);
const hasPathKeys = (
runtimeParams.IsMetric != null ||
runtimeParams.ShowPathEnd != null ||
runtimeParams.ShowPathMode != null ||
runtimeParams.ShowPathColor != null ||
runtimeParams.ShowPathModeLane != null ||
runtimeParams.ShowPathColorLane != null ||
runtimeParams.ShowLaneInfo != null ||
runtimeParams.ShowRadarInfo != null ||
runtimeParams.RadarLatFactor != null ||
runtimeParams.ShowPlotMode != null
);
if (!hasPathKeys) return null;
return normalized;
}
function readLiveRuntimeServices() {
const services = window.CarrotLiveRuntimeState?.services;
return services && typeof services === "object" ? services : {};
}
function mergeServiceState(rawState, liveState) {
const raw = rawState && typeof rawState === "object" ? rawState : {};
const live = liveState && typeof liveState === "object" ? liveState : {};
if (raw === live) return raw;
if (!hasEnumerableKeys(live)) return raw;
if (!hasEnumerableKeys(raw)) return live;
return { ...raw, ...live };
}
function mergeDefinedState(baseState, preferredState) {
const base = baseState && typeof baseState === "object" ? baseState : {};
if (!preferredState || typeof preferredState !== "object") return base;
let merged = null;
for (const [key, value] of Object.entries(preferredState)) {
if (value === undefined || value === null) continue;
if (!merged) merged = { ...base };
merged[key] = value;
}
return merged || base;
}
function mergeRadarLead(rawLead, liveLead) {
return mergeDefinedState(liveLead, rawLead);
}
function mergeRadarState(rawState, liveState) {
const raw = rawState && typeof rawState === "object" ? rawState : {};
const live = liveState && typeof liveState === "object" ? liveState : {};
if (raw === live) return raw;
if (!hasEnumerableKeys(live)) return raw;
if (!hasEnumerableKeys(raw)) return live;
return {
...live,
...raw,
leadOne: mergeRadarLead(raw.leadOne, live.leadOne),
leadTwo: mergeRadarLead(raw.leadTwo, live.leadTwo),
leadLeft: mergeRadarLead(raw.leadLeft, live.leadLeft),
leadRight: mergeRadarLead(raw.leadRight, live.leadRight),
leadsLeft: Array.isArray(raw.leadsLeft) && raw.leadsLeft.length ? raw.leadsLeft : live.leadsLeft,
leadsCenter: Array.isArray(raw.leadsCenter) && raw.leadsCenter.length ? raw.leadsCenter : live.leadsCenter,
leadsRight: Array.isArray(raw.leadsRight) && raw.leadsRight.length ? raw.leadsRight : live.leadsRight,
};
}
function cloneRadarLead(lead) {
if (!lead || typeof lead !== "object") return null;
return {
dRel: finiteNumber(lead.dRel, 0),
yRel: finiteNumber(lead.yRel, 0),
vRel: finiteNumber(lead.vRel, 0),
aRel: finiteNumber(lead.aRel, 0),
vLead: finiteNumber(lead.vLead, 0),
dPath: finiteNumber(lead.dPath, 0),
vLat: finiteNumber(lead.vLat, 0),
vLeadK: finiteNumber(lead.vLeadK, 0),
aLead: finiteNumber(lead.aLead, 0),
aLeadK: finiteNumber(lead.aLeadK, 0),
aLeadTau: finiteNumber(lead.aLeadTau, 0),
modelProb: finiteNumber(lead.modelProb, 0),
score: finiteNumber(lead.score, 0),
jLead: finiteNumber(lead.jLead, 0),
fcw: Boolean(lead.fcw),
status: Boolean(lead.status),
radar: Boolean(lead.radar),
radarTrackId: finiteNumber(lead.radarTrackId, -1),
};
}
function cloneRadarState(radarState) {
const source = radarState && typeof radarState === "object" ? radarState : {};
return {
...source,
leadOne: cloneRadarLead(source.leadOne),
leadTwo: cloneRadarLead(source.leadTwo),
leadLeft: cloneRadarLead(source.leadLeft),
leadRight: cloneRadarLead(source.leadRight),
leadsLeft: Array.isArray(source.leadsLeft) ? source.leadsLeft.slice() : source.leadsLeft,
leadsCenter: Array.isArray(source.leadsCenter) ? source.leadsCenter.slice() : source.leadsCenter,
leadsRight: Array.isArray(source.leadsRight) ? source.leadsRight.slice() : source.leadsRight,
};
}
function radarLeadSignature(lead) {
if (!lead || typeof lead !== "object") return "null";
return [
Boolean(lead.status) ? 1 : 0,
Boolean(lead.radar) ? 1 : 0,
finiteNumber(lead.radarTrackId, -1),
finiteNumber(lead.dRel, 0).toFixed(3),
finiteNumber(lead.yRel, 0).toFixed(3),
finiteNumber(lead.vRel, 0).toFixed(3),
finiteNumber(lead.modelProb, 0).toFixed(3),
finiteNumber(lead.score, 0).toFixed(3),
].join("|");
}
function radarStateSignature(radarState) {
const source = radarState && typeof radarState === "object" ? radarState : {};
return [
radarLeadSignature(source.leadOne),
radarLeadSignature(source.leadTwo),
radarLeadSignature(source.leadLeft),
radarLeadSignature(source.leadRight),
].join("||");
}
function lerpNumber(a, b, t) {
return a + (b - a) * t;
}
function canInterpolateRadarLead(previousLead, currentLead) {
if (!previousLead || !currentLead) return false;
if (!previousLead.status || !currentLead.status) return false;
const previousTrackId = finiteNumber(previousLead.radarTrackId, -1);
const currentTrackId = finiteNumber(currentLead.radarTrackId, -1);
if (previousTrackId >= 0 && currentTrackId >= 0) {
return previousTrackId === currentTrackId;
}
const distanceDelta = Math.abs(finiteNumber(previousLead.dRel, 0) - finiteNumber(currentLead.dRel, 0));
const lateralDelta = Math.abs(finiteNumber(previousLead.yRel, 0) - finiteNumber(currentLead.yRel, 0));
return distanceDelta < 12 && lateralDelta < 2.5;
}
function lerpRadarLead(previousLead, currentLead, t) {
if (!previousLead) return cloneRadarLead(currentLead);
if (!currentLead) return cloneRadarLead(previousLead);
if (!canInterpolateRadarLead(previousLead, currentLead)) {
return cloneRadarLead(currentLead);
}
return {
dRel: lerpNumber(previousLead.dRel, currentLead.dRel, t),
yRel: lerpNumber(previousLead.yRel, currentLead.yRel, t),
vRel: lerpNumber(previousLead.vRel, currentLead.vRel, t),
aRel: lerpNumber(previousLead.aRel, currentLead.aRel, t),
vLead: lerpNumber(previousLead.vLead, currentLead.vLead, t),
dPath: lerpNumber(previousLead.dPath, currentLead.dPath, t),
vLat: lerpNumber(previousLead.vLat, currentLead.vLat, t),
vLeadK: lerpNumber(previousLead.vLeadK, currentLead.vLeadK, t),
aLead: lerpNumber(previousLead.aLead, currentLead.aLead, t),
aLeadK: lerpNumber(previousLead.aLeadK, currentLead.aLeadK, t),
aLeadTau: lerpNumber(previousLead.aLeadTau, currentLead.aLeadTau, t),
modelProb: lerpNumber(previousLead.modelProb, currentLead.modelProb, t),
score: lerpNumber(previousLead.score, currentLead.score, t),
jLead: lerpNumber(previousLead.jLead, currentLead.jLead, t),
fcw: currentLead.fcw,
status: currentLead.status,
radar: currentLead.radar,
radarTrackId: currentLead.radarTrackId,
};
}
function getInterpolatedRadarState(radarState, nowMs) {
const signature = radarStateSignature(radarState);
if (!radarInterpolationState.current) {
const initial = cloneRadarState(radarState);
radarInterpolationState = {
signature,
previous: initial,
current: initial,
previousAtMs: nowMs,
currentAtMs: nowMs,
};
return initial;
}
if (signature !== radarInterpolationState.signature) {
radarInterpolationState = {
signature,
previous: radarInterpolationState.current,
current: cloneRadarState(radarState),
previousAtMs: radarInterpolationState.currentAtMs || nowMs,
currentAtMs: nowMs,
};
}
const previous = radarInterpolationState.previous || radarInterpolationState.current;
const current = radarInterpolationState.current || cloneRadarState(radarState);
if (!previous || !current) return radarState;
if (previous === current) return current;
const intervalMs = clamp(
radarInterpolationState.currentAtMs - radarInterpolationState.previousAtMs || RADAR_INTERPOLATION_DEFAULT_MS,
RADAR_INTERPOLATION_MIN_MS,
RADAR_INTERPOLATION_MAX_MS,
);
const t = clamp((nowMs - radarInterpolationState.currentAtMs + RADAR_INTERPOLATION_LEAD_MS) / intervalMs, 0, 1);
return {
...current,
leadOne: lerpRadarLead(previous.leadOne, current.leadOne, t),
leadTwo: lerpRadarLead(previous.leadTwo, current.leadTwo, t),
leadLeft: lerpRadarLead(previous.leadLeft, current.leadLeft, t),
leadRight: lerpRadarLead(previous.leadRight, current.leadRight, t),
};
}
function mergeRuntimeState(rawHudState, rawOverlayState) {
const liveServices = readLiveRuntimeServices();
const cachedRefs = _mergeRuntimeCache.refs;
if (
cachedRefs &&
cachedRefs.rawHudState === rawHudState &&
cachedRefs.rawOverlayState === rawOverlayState &&
cachedRefs.liveServices === liveServices &&
cachedRefs.rawCarState === rawHudState?.carState &&
cachedRefs.rawControlsState === rawHudState?.controlsState &&
cachedRefs.rawDeviceState === rawHudState?.deviceState &&
cachedRefs.rawPeripheralState === rawHudState?.peripheralState &&
cachedRefs.rawSelfdriveState === rawHudState?.selfdriveState &&
cachedRefs.rawGpsLocationExternal === rawHudState?.gpsLocationExternal &&
cachedRefs.rawLongitudinalPlan === rawHudState?.longitudinalPlan &&
cachedRefs.rawCarrotMan === rawHudState?.carrotMan &&
cachedRefs.rawModelV2 === rawOverlayState?.modelV2 &&
cachedRefs.rawLiveCalibration === rawOverlayState?.liveCalibration &&
cachedRefs.rawRoadCameraState === rawOverlayState?.roadCameraState &&
cachedRefs.rawRadarState === rawOverlayState?.radarState &&
cachedRefs.rawLateralPlan === rawOverlayState?.lateralPlan &&
cachedRefs.rawCarControl === rawOverlayState?.carControl &&
cachedRefs.rawLiveDelay === rawOverlayState?.liveDelay &&
cachedRefs.rawLiveTorqueParameters === rawOverlayState?.liveTorqueParameters &&
cachedRefs.rawLiveParameters === rawOverlayState?.liveParameters &&
cachedRefs.liveCarState === liveServices?.carState &&
cachedRefs.liveControlsState === liveServices?.controlsState &&
cachedRefs.liveSelfdriveState === liveServices?.selfdriveState &&
cachedRefs.liveLongitudinalPlan === liveServices?.longitudinalPlan &&
cachedRefs.liveCarrotMan === liveServices?.carrotMan &&
cachedRefs.liveLateralPlan === liveServices?.lateralPlan &&
cachedRefs.liveRadarState === liveServices?.radarState
) {
return _mergeRuntimeCache.result;
}
const radarState = mergeRadarState(rawOverlayState?.radarState, liveServices.radarState);
const mergedHudState = {
...rawHudState,
carState: mergeServiceState(rawHudState?.carState, liveServices.carState),
controlsState: mergeServiceState(rawHudState?.controlsState, liveServices.controlsState),
selfdriveState: mergeServiceState(rawHudState?.selfdriveState, liveServices.selfdriveState),
longitudinalPlan: mergeServiceState(rawHudState?.longitudinalPlan, liveServices.longitudinalPlan),
carrotMan: mergeServiceState(rawHudState?.carrotMan, liveServices.carrotMan),
lateralPlan: mergeServiceState(rawOverlayState?.lateralPlan, liveServices.lateralPlan),
radarState,
};
const mergedOverlayState = {
...rawOverlayState,
radarState: mergedHudState.radarState,
lateralPlan: mergedHudState.lateralPlan,
carrotMan: mergedHudState.carrotMan,
};
const result = {
brokerServices: liveServices,
hudState: mergedHudState,
overlayState: mergedOverlayState,
};
_mergeRuntimeCache.refs = {
rawHudState,
rawOverlayState,
liveServices,
rawCarState: rawHudState?.carState,
rawControlsState: rawHudState?.controlsState,
rawDeviceState: rawHudState?.deviceState,
rawPeripheralState: rawHudState?.peripheralState,
rawSelfdriveState: rawHudState?.selfdriveState,
rawGpsLocationExternal: rawHudState?.gpsLocationExternal,
rawLongitudinalPlan: rawHudState?.longitudinalPlan,
rawCarrotMan: rawHudState?.carrotMan,
rawModelV2: rawOverlayState?.modelV2,
rawLiveCalibration: rawOverlayState?.liveCalibration,
rawRoadCameraState: rawOverlayState?.roadCameraState,
rawRadarState: rawOverlayState?.radarState,
rawLateralPlan: rawOverlayState?.lateralPlan,
rawCarControl: rawOverlayState?.carControl,
rawLiveDelay: rawOverlayState?.liveDelay,
rawLiveTorqueParameters: rawOverlayState?.liveTorqueParameters,
rawLiveParameters: rawOverlayState?.liveParameters,
liveCarState: liveServices?.carState,
liveControlsState: liveServices?.controlsState,
liveSelfdriveState: liveServices?.selfdriveState,
liveLongitudinalPlan: liveServices?.longitudinalPlan,
liveCarrotMan: liveServices?.carrotMan,
liveLateralPlan: liveServices?.lateralPlan,
liveRadarState: liveServices?.radarState,
};
_mergeRuntimeCache.result = result;
return result;
}
function firstNonEmptyText(...values) {
for (const value of values) {
const text = String(value || "").trim();
if (text) return text;
}
return "";
}
function buildOverlayCarLabel(values = {}) {
let label = firstNonEmptyText(values.CarName, values.CarSelected3);
if (!label) return "";
if (finiteParamNumber(values.HyundaiCameraSCC, 0) > 0) {
label += "(CAMERA SCC)";
}
if (firstNonEmptyText(values.NNFFModelName)) {
label += ",NNFF";
}
return label;
}
function buildOverlayBranchLabel(values = {}) {
const branch = firstNonEmptyText(values.GitBranch);
const commit = firstNonEmptyText(values.GitCommit);
if (!branch && !commit) return "";
if (!branch) return shortText(commit, 12);
return commit ? `${branch} (${commit.slice(0, 7)})` : branch;
}
async function refreshOverlayInfo(force = false) {
if (!force && overlayInfoState.loading) return;
if (!force && overlayInfoState.nextRetryAt > Date.now()) return;
if (typeof bulkGet !== "function") return;
overlayInfoState.loading = true;
try {
const values = await bulkGet([
"CarName",
"CarSelected3",
"HyundaiCameraSCC",
"NNFFModelName",
"GitBranch",
"GitCommit",
]);
const carLabel = buildOverlayCarLabel(values);
const branchLabel = buildOverlayBranchLabel(values);
const signature = `${carLabel}|${branchLabel}`;
overlayInfoState.carLabel = carLabel;
overlayInfoState.branchLabel = branchLabel;
overlayInfoState.nextRetryAt = signature ? 0 : (Date.now() + 5000);
if (signature !== overlayInfoState.lastSignature) {
overlayInfoState.lastSignature = signature;
requestRender({ force: true, hudDirty: true });
}
} catch {
overlayInfoState.nextRetryAt = Date.now() + 5000;
}
overlayInfoState.loading = false;
}
function setStatus(text) {
if (lastStatus === text) return;
lastStatus = text;
statusEl.textContent = text;
}
function getCarrotVisionState() {
return window.CarrotVisionState || {};
}
function isCarrotVisionActive() {
const state = getCarrotVisionState();
return Boolean(state.active ?? window.CARROT_VISION_ACTIVE);
}
function isCarrotVisionAvailable() {
const state = getCarrotVisionState();
return Boolean(state.available ?? window.CARROT_VISION_AVAILABLE);
}
function getCarrotVisionStatusText(fallback = "") {
const state = getCarrotVisionState();
return String(state.statusText || fallback || "");
}
function getCarrotVisionDetailText() {
const state = getCarrotVisionState();
return String(state.detailText || "");
}
function getCarrotVisionDisabledMessage() {
const state = getCarrotVisionState();
return String(state.disabledMessage || window.CARROT_VISION_DISABLED_MESSAGE || getUIText("disable_dm_inactive", "DisableDM is inactive."));
}
function setCarrotVisionRenderPhase(phase, detail = {}) {
// Single phase owner: home_drive is the authority on whether a real camera
// frame is on screen, but it no longer writes the phase directly. It
// reports renderability to vision_rtc, which owns the live/first-frame
// transitions (removes the old home_drive/vision_rtc phase race).
const rtc = window.CarrotVisionRtc;
if (rtc && typeof rtc.reportCameraRenderable === "function") {
if (phase === "ready") { rtc.reportCameraRenderable(true); return; }
if (phase === "first-frame-waiting") { rtc.reportCameraRenderable(false); return; }
}
// Fallback (other phases, or vision_rtc not loaded yet): set directly.
if (typeof window.CarrotVisionSetPhase !== "function") return;
window.CarrotVisionSetPhase(phase, {
source: "home_drive",
updateRtcStatus: false,
render: false,
...detail,
});
}
function setMeta(text) {
if (lastMeta === text) return;
lastMeta = text;
metaEl.textContent = text;
}
function setDebug(text) {
if (lastDebug === text) return;
lastDebug = text;
debugEl.textContent = text;
}
function hideOnroadAlert() {
if (!onroadAlertEl || !onroadAlertBoxEl || !onroadAlertText1El || !onroadAlertText2El) return;
if (_lastAlertSig === "hidden") return;
_lastAlertSig = "hidden";
onroadAlertEl.hidden = true;
onroadAlertEl.className = "carrot-stage__alert";
onroadAlertText1El.textContent = "";
onroadAlertText2El.textContent = "";
onroadAlertText2El.hidden = true;
}
function getAlertStatusClass(status) {
switch (finiteNumber(status, ALERT_STATUS_NORMAL)) {
case ALERT_STATUS_USER_PROMPT:
return "alert-status--user-prompt";
case ALERT_STATUS_CRITICAL:
return "alert-status--critical";
default:
return "alert-status--normal";
}
}
function getAlertSizeClass(size) {
switch (finiteNumber(size, ALERT_SIZE_NONE)) {
case ALERT_SIZE_SMALL:
return "alert-size--small";
case ALERT_SIZE_MID:
return "alert-size--mid";
case ALERT_SIZE_FULL:
return "alert-size--full";
default:
return "alert-size--none";
}
}
function renderOnroadAlert(stageWidth, stageHeight, selfdriveState) {
if (!onroadAlertEl || !onroadAlertBoxEl || !onroadAlertText1El || !onroadAlertText2El) return;
const text1 = String(selfdriveState?.alertText1 || "").trim();
const text2 = String(selfdriveState?.alertText2 || "").trim();
const alertType = String(selfdriveState?.alertType || "").trim();
const alertSize = finiteNumber(selfdriveState?.alertSize, ALERT_SIZE_NONE);
const alertStatus = finiteNumber(selfdriveState?.alertStatus, ALERT_STATUS_NORMAL);
if (alertSize === ALERT_SIZE_NONE || (!text1 && !text2 && !alertType)) {
hideOnroadAlert();
return;
}
const isPortrait = stageHeight > stageWidth;
const longPrimaryText = text1.length > 15;
const stageScale = clamp(Math.min(stageWidth / BASE_CAMERA.width, stageHeight / BASE_CAMERA.height), 0.52, 0.90);
const displayModeScale = displayModeIndex === 0 ? 0.88 : displayModeIndex === 2 ? 0.96 : 0.92;
const orientationScale = isPortrait ? 0.80 : 0.90;
const widthScale = isPortrait ? clamp(stageWidth / 420, 0.72, 0.94) : 1.0;
const resolutionScale = isPortrait
? clamp(stageHeight / BASE_CAMERA.height, 0.98, 1.06)
: clamp(stageWidth / BASE_CAMERA.width, 1.00, 1.08);
const textScale = stageScale * displayModeScale * orientationScale * widthScale * resolutionScale * 1.24;
const primaryBase = alertSize === ALERT_SIZE_SMALL
? 32
: alertSize === ALERT_SIZE_MID
? 40
: (longPrimaryText ? 44 : 48);
const secondaryBase = alertSize === ALERT_SIZE_SMALL ? 0 : (alertSize === ALERT_SIZE_MID ? 20 : 24);
const fontSize1 = clamp(
Math.round(primaryBase * textScale * ONROAD_ALERT_SCALE),
Math.round((alertSize === ALERT_SIZE_SMALL ? 16 : alertSize === ALERT_SIZE_MID ? 20 : 22) * ONROAD_ALERT_SCALE),
Math.round((alertSize === ALERT_SIZE_SMALL ? 32 : alertSize === ALERT_SIZE_MID ? 40 : 44) * ONROAD_ALERT_SCALE),
);
const fontSize2 = alertSize === ALERT_SIZE_SMALL
? 0
: clamp(
Math.round(secondaryBase * textScale * ONROAD_ALERT_SCALE),
Math.round(14 * ONROAD_ALERT_SCALE),
Math.round(29 * ONROAD_ALERT_SCALE),
);
const offsetRatio = isPortrait ? 0.06 : 0.11;
const displayModeYOffset = displayModeIndex === 0 ? -6 : displayModeIndex === 2 ? 6 : 0;
const offsetY = Math.round(stageHeight * offsetRatio) + displayModeYOffset;
const gap = text2 && alertSize !== ALERT_SIZE_SMALL
? clamp(Math.round(fontSize1 * 0.16), Math.round(6 * ONROAD_ALERT_SCALE), Math.round(14 * ONROAD_ALERT_SCALE))
: 0;
const maxWidthRatio = alertSize === ALERT_SIZE_FULL
? (isPortrait ? 0.94 : 0.84)
: (isPortrait ? 0.90 : 0.74);
const maxWidth = Math.round(stageWidth * maxWidthRatio);
const primaryColor = alertStatus === ALERT_STATUS_CRITICAL ? "#ff5a63" : "#ffb12a";
const secondaryColor = alertStatus === ALERT_STATUS_CRITICAL ? "#ffe3e5" : "#ffffff";
const alertScale = ONROAD_ALERT_SCALE;
const alertPx = (value) => `${value * alertScale}px`;
const signature = [
Math.round(stageWidth),
Math.round(stageHeight),
displayModeIndex,
alertSize,
alertStatus,
text1,
text2,
alertType,
fontSize1,
fontSize2,
offsetY,
gap,
maxWidth,
alertScale,
primaryColor,
secondaryColor,
].join("|");
if (_lastAlertSig === signature) return;
_lastAlertSig = signature;
onroadAlertEl.hidden = false;
onroadAlertEl.className = `carrot-stage__alert is-visible ${getAlertSizeClass(alertSize)} ${getAlertStatusClass(alertStatus)}`;
onroadAlertEl.classList.toggle("has-text2", Boolean(text2));
onroadAlertEl.classList.toggle("is-long-text1", longPrimaryText);
onroadAlertEl.style.setProperty("--carrot-alert-offset-y", `${offsetY}px`);
onroadAlertEl.style.setProperty("--carrot-alert-gap", `${gap}px`);
onroadAlertEl.style.setProperty("--carrot-alert-max-width", `${maxWidth}px`);
onroadAlertEl.style.setProperty("--carrot-alert-font1", `${fontSize1}px`);
onroadAlertEl.style.setProperty("--carrot-alert-font2", `${Math.max(fontSize2, 0)}px`);
onroadAlertEl.style.setProperty("--carrot-alert-pad-min", alertPx(4));
onroadAlertEl.style.setProperty("--carrot-alert-pad-fluid", `${1.4 * alertScale}vw`);
onroadAlertEl.style.setProperty("--carrot-alert-pad-max", alertPx(12));
onroadAlertEl.style.setProperty("--carrot-alert-shadow-y-lg", alertPx(8));
onroadAlertEl.style.setProperty("--carrot-alert-shadow-blur-lg", alertPx(24));
onroadAlertEl.style.setProperty("--carrot-alert-shadow-y-sm", alertPx(3));
onroadAlertEl.style.setProperty("--carrot-alert-shadow-blur-sm", alertPx(10));
onroadAlertEl.style.setProperty("--carrot-alert-outline-strong-pos", alertPx(2.25));
onroadAlertEl.style.setProperty("--carrot-alert-outline-strong-neg", alertPx(-2.25));
onroadAlertEl.style.setProperty("--carrot-alert-outline-soft-pos", alertPx(1.8));
onroadAlertEl.style.setProperty("--carrot-alert-outline-soft-neg", alertPx(-1.8));
onroadAlertEl.style.setProperty("--carrot-alert-stroke-primary", alertPx(0.55));
onroadAlertEl.style.setProperty("--carrot-alert-stroke-secondary", alertPx(0.48));
onroadAlertEl.style.setProperty("--carrot-alert-primary-color", primaryColor);
onroadAlertEl.style.setProperty("--carrot-alert-secondary-color", secondaryColor);
onroadAlertText1El.textContent = text1;
onroadAlertText2El.textContent = text2;
onroadAlertText2El.hidden = !text2 || alertSize === ALERT_SIZE_SMALL;
}
function syncDisplayModeButtons() {
if (!displayModeButton) return;
const mode = DISPLAY_MODES[displayModeIndex] || DISPLAY_MODES[1];
const label = getDisplayModeLabel(mode);
displayModeButton.textContent = label || "Normal";
displayModeButton.setAttribute("aria-label", `${getUIText("display_mode", "Display mode")}: ${label}`);
displayModeButton.title = label;
}
function setDisplayModeIndex(nextIndex) {
displayModeIndex = clamp(nextIndex, 0, DISPLAY_MODES.length - 1);
try {
localStorage.setItem(DISPLAY_MODE_STORAGE_KEY, String(displayModeIndex));
} catch {}
transformSignature = "";
syncDisplayModeButtons();
}
function getDisplayModeLabel(mode) {
if (!mode) return getUIText("display_normal", "Normal");
return getUIText(mode.labelKey, mode.fallbackLabel || mode.key || "Normal");
}
function syncSourceStream() {
const stream = sourceVideoEl.srcObject || null;
if (sourceVideoEl !== videoEl && videoEl.srcObject !== stream) {
videoEl.srcObject = stream;
}
const activeStream = (sourceVideoEl === videoEl ? sourceVideoEl : videoEl).srcObject || stream;
if (_roadCameraStreamState.stream !== activeStream) {
_roadCameraStreamState = {
stream: activeStream,
decodedFramesAtBind: activeStream ? getDecodedVideoFrameCount(videoEl) : null,
currentTimeAtBind: activeStream ? Number(videoEl.currentTime || 0) : 0,
firstRenderableSeen: false,
};
cancelCameraFrameRecheck();
}
const hasStream = Boolean(activeStream);
if (hasStream && videoEl.paused) {
videoEl.play().catch(() => {});
}
return hasStream;
}
function hasLiveVideoTrack(video) {
const stream = video?.srcObject;
if (!stream || stream.active === false) return false;
if (typeof stream.getVideoTracks !== "function") return true;
const tracks = stream.getVideoTracks();
if (!tracks.length) return false;
return tracks.some((track) => track && track.readyState !== "ended" && track.muted !== true);
}
function getDecodedVideoFrameCount(video) {
try {
if (typeof video?.getVideoPlaybackQuality === "function") {
const quality = video.getVideoPlaybackQuality();
const total = Number(quality?.totalVideoFrames);
if (Number.isFinite(total)) return total;
}
} catch {}
const webkitCount = Number(video?.webkitDecodedFrameCount);
return Number.isFinite(webkitCount) ? webkitCount : null;
}
function isRoadCameraFrameRenderable(video) {
if (!video || !video.srcObject || !hasLiveVideoTrack(video)) return false;
const videoWidth = Number(video.videoWidth || 0);
const videoHeight = Number(video.videoHeight || 0);
if (videoWidth < MIN_ROAD_VIDEO_WIDTH || videoHeight < MIN_ROAD_VIDEO_HEIGHT) return false;
if (Number(video.readyState || 0) < 2) return false;
if (_roadCameraStreamState.firstRenderableSeen) return true;
const decodedFrames = getDecodedVideoFrameCount(video);
const baselineFrames = _roadCameraStreamState.decodedFramesAtBind;
if (decodedFrames != null && (baselineFrames == null ? decodedFrames > 0 : decodedFrames > baselineFrames)) {
_roadCameraStreamState.firstRenderableSeen = true;
return true;
}
const currentTime = Number(video.currentTime || 0);
const baselineTime = Number(_roadCameraStreamState.currentTimeAtBind || 0);
if (Number.isFinite(currentTime) && currentTime > baselineTime + 0.05 && video.paused !== true) {
_roadCameraStreamState.firstRenderableSeen = true;
return true;
}
if (decodedFrames == null && Number(video.readyState || 0) >= 3 && video.paused !== true) {
_roadCameraStreamState.firstRenderableSeen = true;
return true;
}
return false;
}
function scheduleCameraFrameRecheck() {
if (_cameraFrameRecheckId != null || !isStageVisible() || !isCarrotVisionActive()) return;
_cameraFrameRecheckId = window.setTimeout(() => {
_cameraFrameRecheckId = null;
requestRender({ force: true, overlayDirty: true, hudDirty: true });
}, CAMERA_FRAME_RECHECK_MS);
}
function cancelCameraFrameRecheck() {
if (_cameraFrameRecheckId == null) return;
window.clearTimeout(_cameraFrameRecheckId);
_cameraFrameRecheckId = null;
}
let _lastStageReady = null;
function setStageReady(ready) {
const r = Boolean(ready);
if (_lastStageReady === r) return;
_lastStageReady = r;
stageEl.classList.toggle("is-stream-ready", r);
}
let _lastStageLoading = null;
let _lastStageLoadingText = "";
let _lastStageLoadingDetail = "";
function setStageLoading(loading, text = getUIText("connecting", "Connecting..."), detail = "") {
const l = Boolean(loading);
if (_lastStageLoading !== l) {
_lastStageLoading = l;
stageEl.classList.toggle("is-loading", l);
if (driveHudCardEl) driveHudCardEl.classList.toggle("driveHudCard--loading", l);
if (stageLoadingEl) stageLoadingEl.hidden = !l;
}
if (!l) return;
if (stageLoadingTextEl && _lastStageLoadingText !== text) {
_lastStageLoadingText = text;
stageLoadingTextEl.textContent = text;
}
const detailText = l ? String(detail || "") : "";
if (stageLoadingDetailEl && _lastStageLoadingDetail !== detailText) {
_lastStageLoadingDetail = detailText;
stageLoadingDetailEl.textContent = detailText;
stageLoadingDetailEl.hidden = !detailText;
}
}
let _lastHudLeft = "";
let _lastHudBottom = "";
let _lastViewportSig = "";
function resetCarrotHudLayout() {
if (driveHudCardEl) {
driveHudCardEl.style.removeProperty("--carrot-hud-left");
driveHudCardEl.style.removeProperty("--carrot-hud-bottom");
}
if (stageEl) {
stageEl.style.removeProperty("--carrot-viewport-left");
stageEl.style.removeProperty("--carrot-viewport-top");
stageEl.style.removeProperty("--carrot-viewport-width");
stageEl.style.removeProperty("--carrot-viewport-height");
}
_lastViewportSig = "";
}
function applyCarrotHudLayout(viewportRect) {
if (window.matchMedia("(orientation: portrait)").matches) {
if (_lastHudLeft !== "" || _lastHudBottom !== "") {
if (driveHudCardEl) {
driveHudCardEl.style.removeProperty("--carrot-hud-left");
driveHudCardEl.style.removeProperty("--carrot-hud-bottom");
}
_lastHudLeft = "";
_lastHudBottom = "";
}
return;
}
const stageWidth = stageEl?.clientWidth || viewportRect?.width || 0;
const stageHeight = stageEl?.clientHeight || viewportRect?.height || 0;
if (!stageWidth || !stageHeight) return;
const viewport = {
left: Math.round(finiteNumber(viewportRect?.left, 0)),
top: Math.round(finiteNumber(viewportRect?.top, 0)),
width: Math.round(finiteNumber(viewportRect?.width, stageWidth)),
height: Math.round(finiteNumber(viewportRect?.height, stageHeight)),
stageWidth: Math.round(stageWidth),
stageHeight: Math.round(stageHeight),
};
const viewportSig = `${viewport.left},${viewport.top},${viewport.width},${viewport.height},${viewport.stageWidth},${viewport.stageHeight}`;
if (stageEl && _lastViewportSig !== viewportSig) {
_lastViewportSig = viewportSig;
stageEl.style.setProperty("--carrot-viewport-left", `${viewport.left}px`);
stageEl.style.setProperty("--carrot-viewport-top", `${viewport.top}px`);
stageEl.style.setProperty("--carrot-viewport-width", `${viewport.width}px`);
stageEl.style.setProperty("--carrot-viewport-height", `${viewport.height}px`);
window.dispatchEvent(new CustomEvent("carrot:viewportlayout", { detail: viewport }));
}
if (!driveHudCardEl) return;
const overlayInsetX = clamp(stageWidth * 0.028, 16, 28);
const overlayInsetY = clamp(stageHeight * 0.038, 20, 30);
const leftVal = `${Math.round(overlayInsetX)}px`;
const bottomVal = `${Math.round(overlayInsetY)}px`;
if (_lastHudLeft !== leftVal) {
_lastHudLeft = leftVal;
driveHudCardEl.style.setProperty("--carrot-hud-left", leftVal);
}
if (_lastHudBottom !== bottomVal) {
_lastHudBottom = bottomVal;
driveHudCardEl.style.setProperty("--carrot-hud-bottom", bottomVal);
}
}
function getCanvasDpr() {
const rawDpr = window.devicePixelRatio || 1;
const portrait = window.matchMedia("(orientation: portrait)").matches;
const shortSide = Math.min(window.innerWidth || 0, window.innerHeight || 0);
if (portrait && shortSide > 0 && shortSide <= 540) {
return Math.min(rawDpr, PHONE_PORTRAIT_DPR_CAP);
}
const cap = shortSide >= 960 ? DESKTOP_DPR_CAP : MOBILE_DPR_CAP;
return Math.min(rawDpr, cap);
}
function syncCanvasSize(videoWidth, videoHeight, stageWidth, stageHeight) {
const dpr = getCanvasDpr();
const nextOverlaySignature = `${videoWidth}x${videoHeight}@${dpr.toFixed(2)}`;
if (overlaySizeSignature !== nextOverlaySignature) {
overlaySizeSignature = nextOverlaySignature;
resetTemporalRibbonState();
videoEl.style.width = `${videoWidth}px`;
videoEl.style.height = `${videoHeight}px`;
if (videoHoldEl) {
videoHoldEl.style.width = `${videoWidth}px`;
videoHoldEl.style.height = `${videoHeight}px`;
videoHoldEl.width = Math.max(1, Math.round(videoWidth * dpr));
videoHoldEl.height = Math.max(1, Math.round(videoHeight * dpr));
}
canvasEl.style.width = `${videoWidth}px`;
canvasEl.style.height = `${videoHeight}px`;
canvasEl.width = Math.max(1, Math.round(videoWidth * dpr));
canvasEl.height = Math.max(1, Math.round(videoHeight * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const nextHudSignature = `${stageWidth}x${stageHeight}@${dpr.toFixed(2)}`;
if (hudSizeSignature !== nextHudSignature) {
hudSizeSignature = nextHudSignature;
hudCanvasEl.style.width = `${stageWidth}px`;
hudCanvasEl.style.height = `${stageHeight}px`;
hudCanvasEl.width = Math.max(1, Math.round(stageWidth * dpr));
hudCanvasEl.height = Math.max(1, Math.round(stageHeight * dpr));
hudCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
}
function applyStageTransform(transform) {
const nextSignature = `${transform.scale.toFixed(6)}|${transform.tx.toFixed(2)}|${transform.ty.toFixed(2)}`;
if (transformSignature === nextSignature) return;
transformSignature = nextSignature;
resetTemporalRibbonState();
const cssMatrix = `matrix(${transform.scale}, 0, 0, ${transform.scale}, ${transform.tx}, ${transform.ty})`;
videoEl.style.transform = cssMatrix;
if (videoHoldEl) videoHoldEl.style.transform = cssMatrix;
canvasEl.style.transform = cssMatrix;
}
function clearOverlay(videoWidth, videoHeight) {
ctx.clearRect(0, 0, videoWidth, videoHeight);
}
function clearHud(stageWidth, stageHeight) {
hudCtx.clearRect(0, 0, stageWidth, stageHeight);
}
function fitSingleLineHudFontSize(text, preferredSize, maxWidth, minSize = 4.5, fontWeight = 900) {
const label = String(text || "").trim();
if (!label) return preferredSize;
let fontSize = preferredSize;
const font = `${fontWeight} ${fontSize}px ${HUD_TEXT_FONT}`;
const measured = getCachedTextWidth(hudCtx, font, label);
if (measured > maxWidth && measured > 1.0) {
fontSize = clamp(fontSize * ((maxWidth / measured) * 0.985), minSize, fontSize);
}
return fontSize;
}
function drawOutlinedHudText({
text,
x,
y,
color = "rgba(244, 244, 244, 0.94)",
strokeColor = "rgba(0, 0, 0, 0.94)",
strokeWidth = 3,
fontSize = 24,
fontWeight = 900,
alignX = "left",
alignY = "top",
maxWidth,
}) {
const label = String(text || "").trim();
if (!label) return;
hudCtx.save();
hudCtx.font = `${fontWeight} ${fontSize}px ${HUD_TEXT_FONT}`;
hudCtx.fillStyle = color;
hudCtx.strokeStyle = strokeColor;
hudCtx.lineWidth = strokeWidth;
hudCtx.lineJoin = "round";
hudCtx.miterLimit = 2;
hudCtx.textAlign = alignX === "center" ? "center" : alignX === "right" ? "right" : "left";
hudCtx.textBaseline = alignY === "middle" ? "middle" : alignY === "bottom" ? "bottom" : alignY === "baselineBottom" ? "alphabetic" : "top";
if (strokeWidth > 0) {
hudCtx.strokeText(label, x, y, maxWidth);
}
hudCtx.fillText(label, x, y, maxWidth);
hudCtx.restore();
}
function drawSegmentBands(left, right, style, step) {
if (!Array.isArray(left) || left.length < 3 || left.length !== right.length) return;
const baseColor = paletteColor(style.paletteIndex);
const stroke = style.emphasisStroke ? style.strokeColor : "";
for (let i = 0; i < left.length - 2; i += step) {
const next = Math.min(i + 2, left.length - 1);
const segment = [left[i], left[next], right[next], right[i]];
drawPolygon(segment, rgba(baseColor, 0.28), stroke, 1.1);
}
}
function createPathGradient(baseColor, canvasHeight, style) {
const mode = finiteNumber(style?.mode, 0);
let solidAlpha = mode === 0 ? 0.40 : 0.24;
let midAlpha = mode === 0 ? 0.20 : 0.12;
if (mode === 0 || style?.isCruiseOff) {
solidAlpha = Math.max(solidAlpha, TEST_PATH_VISIBILITY_SOLID_ALPHA);
midAlpha = Math.max(midAlpha, TEST_PATH_VISIBILITY_MID_ALPHA);
}
const cacheKey = `g:${baseColor.r},${baseColor.g},${baseColor.b}|${Math.round(canvasHeight)}|${solidAlpha}|${midAlpha}`;
return getCachedGradient(cacheKey, () => {
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, canvasHeight * 0.32);
gradient.addColorStop(0, rgba(baseColor, solidAlpha));
gradient.addColorStop(0.55, rgba(baseColor, midAlpha));
gradient.addColorStop(1, rgba(baseColor, 0.0));
return gradient;
});
}
function drawPathRibbon(ribbon, style, canvasHeight) {
if (!ribbon.polygon.length) return;
const baseColor = paletteColor(style.paletteIndex);
if (style.mode === 0) {
drawPolygon(
ribbon.polygon,
rgba(baseColor, 0.42),
style.emphasisStroke ? style.strokeColor : "",
style.emphasisStroke ? 2.0 : 0,
);
return;
}
const fill = createPathGradient(baseColor, canvasHeight, style);
drawPolygon(ribbon.polygon, fill, style.emphasisStroke ? style.strokeColor : "", style.emphasisStroke ? 1.7 : 0);
}
function drawAnimatedPath(ribbon, style) {
if (!ribbon.center.length) return;
const dashPresets = {
1: [14, 11],
2: [10, 8],
3: [26, 12],
4: [18, 10, 5, 10],
5: [30, 12],
6: [12, 7],
7: [20, 8, 4, 8],
8: [8, 6],
};
const baseColor = paletteColor(style.paletteIndex);
const dash = dashPresets[style.mode] || dashPresets[1];
const dashLength = dash.reduce((sum, value) => sum + value, 0);
const dashOffset = -((performance.now() / 120) % dashLength);
drawPolyline(ribbon.center, rgba(baseColor, 0.78), 5.5, dash, dashOffset);
if (style.emphasisStroke) {
drawPolyline(ribbon.center, style.strokeColor, 1.6);
}
}
function drawComplexPath(ribbon, style) {
const step = style.mode === 9 ? 3 : style.mode === 10 ? 4 : style.mode === 11 ? 5 : 6;
drawSegmentBands(ribbon.left, ribbon.right, style, step);
}
function drawSpecialPath(ribbon, style) {
const baseColor = paletteColor(style.paletteIndex);
const bands = [];
if (style.mode === 13 || style.mode === 14) {
bands.push(buildBandPolygon(ribbon.left, ribbon.right, 0.0, 0.18));
bands.push(buildBandPolygon(ribbon.left, ribbon.right, 0.82, 1.0));
}
if (style.mode === 13 || style.mode === 15) {
bands.push(buildBandPolygon(ribbon.left, ribbon.right, 0.38, 0.62));
}
for (const band of bands) {
drawPolygon(band, rgba(baseColor, 0.34), style.emphasisStroke ? style.strokeColor : "", style.emphasisStroke ? 1.4 : 0);
}
}
function getPathHalfWidth() {
const widthRatio = clamp(finiteNumber(paramsState.ShowPathWidth, 100) / 100, 0.1, 2.0);
return PATH_HALF_WIDTH * widthRatio;
}
function drawPath(pathData, model, overlayState, calibTransform, canvasHeight, style) {
if (!pathData || !Array.isArray(pathData.x) || !pathData.x.length) return;
const sceneMaxDistance = getSceneMaxDistance(model, overlayState);
const rawRibbon = buildRibbon(calibTransform, pathData, getPathHalfWidth(), PATH_Z_OFFSET, getPathMaxDistance(sceneMaxDistance), false);
const ribbon = smoothRibbonTemporal(`path:${style?.laneMode ? "lane" : "model"}:${style?.mode ?? 0}`, rawRibbon, PATH_TEMPORAL_SMOOTH_ALPHA);
if (ribbon.polygon.length < 3) return;
drawPathRibbon(ribbon, style, canvasHeight);
if (style.mode === 0) return;
if (style.mode >= 13 && style.mode <= 15) {
drawSpecialPath(ribbon, style);
return;
}
if (style.mode >= 9) {
drawComplexPath(ribbon, style);
return;
}
drawAnimatedPath(ribbon, style);
}
function drawLaneLines(model, overlayState, hudState, calibTransform) {
const laneLines = Array.isArray(model?.laneLines) ? model.laneLines : [];
const laneLineProbs = Array.isArray(model?.laneLineProbs) ? model.laneLineProbs : [];
if (!laneLines.length) return;
const leftLaneLine = finiteNumber(hudState?.carState?.leftLaneLine, 0);
const rightLaneLine = finiteNumber(hudState?.carState?.rightLaneLine, 0);
const sceneMaxDistance = getSceneMaxDistance(model, overlayState);
const maxIdx = getPathLengthIdx(laneLines[0], sceneMaxDistance);
for (let i = 0; i < laneLines.length; i += 1) {
const prob = clamp(finiteNumber(laneLineProbs[i], 0), 0, 0.9);
const renderProb = prob >= 0.02 ? prob : (prob >= TEST_LANE_PROB_MIN ? clamp(prob * TEST_LANE_PROB_BOOST, 0.02, 0.12) : 0);
if (renderProb <= 0) continue;
// Keep native probability-driven lanes, but allow a small test-page floor so
// very low-confidence indoor/stationary lanes are still inspectable.
const highlightedLeft = i === 1 && leftLaneLine >= 20;
const highlightedRight = i === 2 && rightLaneLine >= 20;
const laneColor = highlightedLeft || highlightedRight ? { r: 255, g: 217, b: 94 } : { r: 255, g: 255, b: 255 };
const baseHalfWidth = Math.max(highlightedLeft || highlightedRight ? 0.025 : 0.010, 0.025 * renderProb);
const halfWidth = baseHalfWidth * LANE_VISUAL_WIDTH_GAIN;
const fillAlpha = prob >= 0.02 ? clamp(renderProb, 0.12, 0.7) : clamp(renderProb * 3.0, 0.16, 0.26);
const strokeAlpha = prob >= 0.02 ? 0.20 : 0.24;
const rawRibbon = buildRibbon(
calibTransform,
laneLines[i],
halfWidth,
0,
finiteNumber(laneLines[i]?.x?.[maxIdx], MAX_DRAW_DISTANCE),
false,
0,
GEOMETRY_QUALITY_LANE,
);
const ribbon = smoothRibbonTemporal(`lane:${i}`, rawRibbon, LANE_TEMPORAL_SMOOTH_ALPHA);
drawPolygon(
ribbon.polygon,
`rgba(${laneColor.r},${laneColor.g},${laneColor.b},${fillAlpha.toFixed(3)})`,
`rgba(${laneColor.r},${laneColor.g},${laneColor.b},${strokeAlpha.toFixed(3)})`,
1,
);
if ((i === 1 && (leftLaneLine % 10) === 4) || (i === 2 && (rightLaneLine % 10) === 4)) {
const shift = i === 1 ? -0.3 : 0.3;
const rawDoubleRibbon = buildRibbon(
calibTransform,
laneLines[i],
halfWidth,
0,
finiteNumber(laneLines[i]?.x?.[maxIdx], MAX_DRAW_DISTANCE),
false,
shift,
GEOMETRY_QUALITY_LANE,
);
const doubleRibbon = smoothRibbonTemporal(`lane:${i}:double:${shift}`, rawDoubleRibbon, LANE_TEMPORAL_SMOOTH_ALPHA);
drawPolygon(
doubleRibbon.polygon,
`rgba(${laneColor.r},${laneColor.g},${laneColor.b},${fillAlpha.toFixed(3)})`,
`rgba(${laneColor.r},${laneColor.g},${laneColor.b},${strokeAlpha.toFixed(3)})`,
1,
);
}
}
}
function drawRoadEdges(model, overlayState, calibTransform) {
const roadEdges = Array.isArray(model?.roadEdges) ? model.roadEdges : [];
const roadEdgeStds = Array.isArray(model?.roadEdgeStds) ? model.roadEdgeStds : [];
if (!roadEdges.length) return;
const sceneMaxDistance = getSceneMaxDistance(model, overlayState);
const maxIdx = getPathLengthIdx(roadEdges[0], sceneMaxDistance);
for (let i = 0; i < roadEdges.length; i += 1) {
const edgeStd = clamp(finiteNumber(roadEdgeStds[i], 0.4), 0, 1);
const alpha = clamp(1 - edgeStd, 0.12, 0.66);
const ribbon = buildRibbon(
calibTransform,
roadEdges[i],
0.025,
0,
finiteNumber(roadEdges[i]?.x?.[maxIdx], MAX_DRAW_DISTANCE),
false,
0,
GEOMETRY_QUALITY_ROAD_EDGE,
);
drawPolygon(
ribbon.polygon,
`rgba(255,78,59,${alpha.toFixed(3)})`,
"rgba(255,124,104,0.28)",
1,
);
}
}
function samplePathZ(position, distance) {
const cacheBucket = getWeakCacheBucket(_frameProjectionCache.pathZ, position);
const cacheKey = Math.round(finiteNumber(distance, 0) * 100);
if (cacheBucket?.has(cacheKey)) return cacheBucket.get(cacheKey);
const zs = Array.isArray(position?.z) ? position.z : [];
const idx = getPathLengthIdx(position, distance);
const value = finiteNumber(zs[idx], 0);
cacheBucket?.set(cacheKey, value);
return value;
}
function samplePathY(position, distance) {
const cacheBucket = getWeakCacheBucket(_frameProjectionCache.pathY, position);
const cacheKey = Math.round(finiteNumber(distance, 0) * 100);
if (cacheBucket?.has(cacheKey)) return cacheBucket.get(cacheKey);
const value = interp1D(
distance,
Array.isArray(position?.x) ? position.x : [],
Array.isArray(position?.y) ? position.y : [],
);
cacheBucket?.set(cacheKey, value);
return value;
}
function circlePolygon(cx, cy, radius, points = 12) {
const polygon = [];
for (let i = 0; i < points; i += 1) {
const theta = (Math.PI * 2 * i) / points;
polygon.push({
x: cx + Math.cos(theta) * radius,
y: cy + Math.sin(theta) * radius,
});
}
return polygon;
}
function buildVerticalRibbon(calibTransform, line, centerShift, topZOffset, bottomZOffset, maxDistance) {
const cacheBucket = getWeakCacheBucket(_frameProjectionCache.verticalRibbon, line);
const cacheKey = [
Math.round(finiteNumber(centerShift, 0) * 1000),
Math.round(finiteNumber(topZOffset, 0) * 1000),
Math.round(finiteNumber(bottomZOffset, 0) * 1000),
Math.round(finiteNumber(maxDistance, MAX_DRAW_DISTANCE) * 100),
].join("|");
if (cacheBucket?.has(cacheKey)) return cacheBucket.get(cacheKey);
const xs = Array.isArray(line?.x) ? line.x : [];
const ys = Array.isArray(line?.y) ? line.y : [];
const zs = Array.isArray(line?.z) ? line.z : [];
const top = [];
const bottom = [];
const distances = [];
const maxIdx = getPathLengthIdx(line, maxDistance);
forEachProjectedSampleIndex(xs, maxIdx, maxDistance, (i) => {
const x = finiteNumber(xs[i], NaN);
if (!Number.isFinite(x) || x < 0) return;
const y = finiteNumber(ys[i], 0) + centerShift;
const z = finiteNumber(zs[i], 0);
const topPoint = projectPoint(calibTransform, x, y, z + topZOffset);
const bottomPoint = projectPoint(calibTransform, x, y, z + bottomZOffset);
if (!topPoint || !bottomPoint) return;
if (top.length && topPoint.y > top[top.length - 1].y) return;
top.push(topPoint);
bottom.push(bottomPoint);
distances.push(x);
});
const smoothMaxDistance = finiteNumber(maxDistance, MAX_DRAW_DISTANCE);
const smoothedTop = smoothProjectedPolyline(top, distances, smoothMaxDistance, POLYLINE_SMOOTH_MAX_STRENGTH);
const smoothedBottom = smoothProjectedPolyline(bottom, distances, smoothMaxDistance, POLYLINE_SMOOTH_MAX_STRENGTH);
const result = smoothedTop.length >= 2 && smoothedBottom.length >= 2 ? smoothedTop.concat([...smoothedBottom].reverse()) : [];
cacheBucket?.set(cacheKey, result);
return result;
}
function resetLeadEmaSlot(slot) {
if (slot !== 0 && slot !== 1) return;
leadEmaState[slot] = { fx: NaN, fy: NaN, fw: NaN, trackId: -1, lastMs: 0 };
}
function resetLeadTwoEma() {
leadTwoEmaState = { xl: NaN, xr: NaN, y: NaN, lastMs: 0 };
}
function syncLeadRenderState(videoWidth, videoHeight, modelFrameId, cameraFrameId) {
const nextWidth = finiteNumber(videoWidth, NaN);
const nextHeight = finiteNumber(videoHeight, NaN);
const nextModelFrameId = finiteNumber(modelFrameId, NaN);
const nextCameraFrameId = finiteNumber(cameraFrameId, NaN);
const prev = leadRenderState;
const sourceChanged =
Number.isFinite(prev.lastSourceWidth) &&
Number.isFinite(prev.lastSourceHeight) &&
(Math.abs(prev.lastSourceWidth - nextWidth) > 0.5 || Math.abs(prev.lastSourceHeight - nextHeight) > 0.5);
const cameraFrameRewind =
Number.isFinite(prev.lastCameraFrameId) &&
Number.isFinite(nextCameraFrameId) &&
nextCameraFrameId < prev.lastCameraFrameId;
const modelFrameRewind =
Number.isFinite(prev.lastModelFrameId) &&
Number.isFinite(nextModelFrameId) &&
nextModelFrameId < prev.lastModelFrameId;
if (sourceChanged || cameraFrameRewind || modelFrameRewind) {
resetLeadEmaSlot(0);
resetLeadEmaSlot(1);
resetLeadTwoEma();
leadHoldState.box = null;
}
leadRenderState = {
lastCameraFrameId: nextCameraFrameId,
lastModelFrameId: nextModelFrameId,
lastSourceWidth: nextWidth,
lastSourceHeight: nextHeight,
};
}
function getLeadBadgeOffsets(videoWidth, videoHeight) {
const uiScale = getLeadUiScale(videoWidth, videoHeight);
return {
dx: 80 * uiScale,
rectTopOffset: 25 * uiScale,
textBaselineOffset: 60 * uiScale,
badgeHeight: Math.max(42 * uiScale, 20),
fontSize: Math.max(40 * uiScale, 18),
radius: Math.max(12 * uiScale, 7),
strokeWidth: Math.max(4 * uiScale, 2.2),
};
}
function getLeadUiScale(videoWidth, videoHeight) {
const scaleX = videoWidth / BASE_CAMERA.width;
const scaleY = videoHeight / BASE_CAMERA.height;
return clamp(Math.min(scaleX, scaleY), 0.45, 1.0);
}
function hasNearbyAssistLead(lead, speedMps) {
const speed = finiteNumber(speedMps, 0);
if (speed <= 0) return false;
const threshold = speed * 3.0;
return Boolean(lead?.status) && finiteNumber(lead?.dRel, Infinity) > 0 && finiteNumber(lead?.dRel, Infinity) < threshold;
}
function drawBlindspotBarriers(modelPath, overlayState, hudState, calibTransform) {
if (!modelPath || !Array.isArray(modelPath.x) || modelPath.x.length < 2) return;
const radarState = overlayState?.radarState || {};
const carState = hudState?.carState || {};
const lateralPlan = overlayState?.lateralPlan || {};
const speedMps = finiteNumber(carState?.vEgo, finiteNumber(carState?.vEgoCluster, 0));
const laneChangeState = finiteNumber(lateralPlan?.laneChangeState, 0);
const laneChangeDirection = finiteNumber(lateralPlan?.laneChangeDirection, 0);
const leftBlindspot = Boolean(carState?.leftBlindspot);
const rightBlindspot = Boolean(carState?.rightBlindspot);
const leftAssistWarn = !leftBlindspot && laneChangeState === 1 && laneChangeDirection === 1 && hasNearbyAssistLead(radarState?.leadLeft, speedMps);
const rightAssistWarn = !rightBlindspot && laneChangeState === 1 && laneChangeDirection === 2 && hasNearbyAssistLead(radarState?.leadRight, speedMps);
if (!leftBlindspot && !rightBlindspot && !leftAssistWarn && !rightAssistWarn) return;
const goldFill = "rgba(255, 215, 0, 0.48)";
const goldStroke = "rgba(255, 215, 0, 0.84)";
const greenFill = "rgba(0, 204, 0, 0.44)";
const greenStroke = "rgba(0, 204, 0, 0.80)";
const drawRibbon = (shift, fill, stroke) => {
const ribbon = buildVerticalRibbon(calibTransform, modelPath, shift, 1.15, 0.60, 40);
if (ribbon.length < 8) return;
drawPolygon(ribbon, fill, stroke, 1.2);
};
if (leftBlindspot) drawRibbon(-1.7, goldFill, goldStroke);
else if (leftAssistWarn) drawRibbon(-1.7, greenFill, greenStroke);
if (rightBlindspot) drawRibbon(1.7, goldFill, goldStroke);
else if (rightAssistWarn) drawRibbon(1.7, greenFill, greenStroke);
}
function getLeadBoxClampMargins(videoWidth, videoHeight, stageWidth = videoWidth, stageHeight = videoHeight, transform = null, options = {}) {
const visibleRect = getVisibleSourceRect(videoWidth, videoHeight, stageWidth, stageHeight, transform);
const uiScale = getLeadUiScale(videoWidth, videoHeight);
// C3 fixed margins scaled to the encoded source resolution. Without this,
// 964x604 streams keep 1928x1208 margins and force lead UI into the center.
const topReserve = Math.max(200.0 * uiScale, 96.0);
const bottomReserve = Math.max(80.0 * uiScale, 42.0);
const sideReserve = Math.max(350.0 * uiScale, 120.0);
const topMargin = Math.max(topReserve, visibleRect.top + 6 * uiScale);
// C3 base: maxCenterY = fb_h - 80
let maxCenterY = videoHeight - bottomReserve;
// In crop/fit modes, also keep badges inside visible area
const offsets = getLeadBadgeOffsets(videoWidth, videoHeight);
let badgeReserve = 0;
if (options.includeDistanceBadge !== false) {
badgeReserve = Math.max(badgeReserve, offsets.rectTopOffset + offsets.badgeHeight + 8 * uiScale);
}
if (options.includeStateText) {
badgeReserve = Math.max(badgeReserve, offsets.textBaselineOffset + Math.max(offsets.fontSize * 0.28, 8 * uiScale));
}
maxCenterY = Math.min(maxCenterY, visibleRect.bottom - Math.max(badgeReserve, bottomReserve));
maxCenterY = Math.max(topMargin, maxCenterY);
return {
marginX: sideReserve,
topMargin,
bottomMargin: Math.max(bottomReserve, videoHeight - maxCenterY),
maxCenterY,
visibleRect,
bottomReserve: badgeReserve,
};
}
function projectLeadBox(lead, modelPath, calibTransform, videoWidth, videoHeight, slot = 0, stageWidth = videoWidth, stageHeight = videoHeight, transform = null, options = {}) {
if (!lead?.status) return null;
const distance = finiteNumber(lead.dRel, NaN);
if (!Number.isFinite(distance) || distance <= 0) return null;
const yCenter = -finiteNumber(lead.yRel, 0);
const z = samplePathZ(modelPath, distance) + PATH_Z_OFFSET;
const left = projectPointPrecise(calibTransform, distance, yCenter - 1.2, z);
const right = projectPointPrecise(calibTransform, distance, yCenter + 1.2, z);
if (!left || !right) return null;
const rawWidth = Math.abs(right.x - left.x);
if (!Number.isFinite(rawWidth) || rawWidth <= 1) return null;
const rawCenterX = (left.x + right.x) * 0.5;
const rawCenterY = (left.y + right.y) * 0.5;
const { marginX, topMargin, maxCenterY } = getLeadBoxClampMargins(
videoWidth,
videoHeight,
stageWidth,
stageHeight,
transform,
options,
);
// Match CarrotLink's adaptive bottom margin while keeping carrot.cc clamp policy.
const _path_x = clamp(rawCenterX, marginX, Math.max(marginX, videoWidth - marginX));
const _path_y = clamp(rawCenterY, topMargin, maxCenterY);
const uiScale = getLeadUiScale(videoWidth, videoHeight);
const _path_width = clamp(rawWidth, 120 * uiScale, 800 * uiScale);
// ── Step 2: Time-based EMA on clamped values ──
// C3 uses alpha=0.85 at stable 20Hz. Web frame rate varies, so
// alpha_adj = 0.85^(dt/50ms) gives identical wall-clock convergence.
const ema = leadEmaState[slot] || { fx: NaN, fy: NaN, fw: NaN, trackId: -1, lastMs: 0 };
const currentTrackId = (lead.radarTrackId != null) ? finiteNumber(lead.radarTrackId, -1) : -1;
const nowMs = performance.now();
const dt = ema.lastMs > 0 ? clamp(nowMs - ema.lastMs, 1, 500) : C3_FRAME_MS;
const alpha = Math.pow(LEAD_EMA_ALPHA, dt / C3_FRAME_MS);
const hasPrev = Number.isFinite(ema.fx) && Number.isFinite(ema.fy) && Number.isFinite(ema.fw);
const path_fx = hasPrev ? (ema.fx * alpha + _path_x * (1 - alpha)) : _path_x;
const path_fy = hasPrev ? (ema.fy * alpha + _path_y * (1 - alpha)) : _path_y;
const path_fw = hasPrev ? (ema.fw * alpha + _path_width * (1 - alpha)) : _path_width;
leadEmaState[slot] = { fx: path_fx, fy: path_fy, fw: path_fw, trackId: currentTrackId, lastMs: nowMs };
// ── Step 3: Build box from smoothed values ──
const path_x = Math.trunc(path_fx);
const path_y = Math.trunc(path_fy);
const width = Math.max(Math.trunc(path_fw), 1);
const sidePad = Math.max(10 * uiScale, 5);
const height = Math.max(Math.trunc(width * 0.8), Math.round(12 * uiScale));
// capnp default is -1; Number(null)=0 would falsely trigger radar-detected, so guard null
const radarTrackId = (lead.radarTrackId != null) ? finiteNumber(lead.radarTrackId, -1) : -1;
const radarDetected = radarTrackId >= 0;
return {
rect: {
x: path_x - width * 0.5 - sidePad,
y: path_y - height,
width: width + sidePad * 2,
height,
},
centerX: path_x,
centerY: path_y,
radar: Boolean(lead.radar),
radarDetected,
radarTrackId,
dRel: distance,
modelProb: finiteNumber(lead.modelProb, 0),
width,
videoWidth,
videoHeight,
};
}
function projectLeadTwoBox(lead, modelPath, calibTransform, videoWidth, videoHeight, stageWidth = videoWidth, stageHeight = videoHeight, transform = null) {
if (!lead?.status || !lead?.radar) return null;
const distance = finiteNumber(lead.dRel, NaN);
if (!Number.isFinite(distance) || distance <= 0) return null;
const yCenter = -finiteNumber(lead.yRel, 0);
const z = samplePathZ(modelPath, distance) + PATH_Z_OFFSET;
const left = projectPointPrecise(calibTransform, distance, yCenter - 1.2, z);
const right = projectPointPrecise(calibTransform, distance, yCenter + 1.2, z);
if (!left || !right) return null;
const prev = leadTwoEmaState;
const hasPrev = Number.isFinite(prev.xl) && Number.isFinite(prev.xr) && Number.isFinite(prev.y);
// C3 lead_two uses alpha=0.8 at 20Hz; apply same time-compensation
const nowMs2 = performance.now();
const dt2 = prev.lastMs > 0 ? clamp(nowMs2 - prev.lastMs, 1, 500) : C3_FRAME_MS;
const a2 = Math.pow(0.8, dt2 / C3_FRAME_MS);
const xl = hasPrev ? (prev.xl * a2 + left.x * (1 - a2)) : left.x;
const xr = hasPrev ? (prev.xr * a2 + right.x * (1 - a2)) : right.x;
const y = hasPrev ? (prev.y * a2 + left.y * (1 - a2)) : left.y;
leadTwoEmaState = { xl, xr, y, lastMs: nowMs2 };
const { marginX, topMargin, maxCenterY } = getLeadBoxClampMargins(
videoWidth,
videoHeight,
stageWidth,
stageHeight,
transform,
{ includeDistanceBadge: false, includeStateText: false },
);
const clampedCenterX = clamp((xl + xr) * 0.5, marginX, Math.max(marginX, videoWidth - marginX));
const rawWidth = Math.max(xr - xl, 1);
const uiScale = getLeadUiScale(videoWidth, videoHeight);
const sidePad = Math.max(10 * uiScale, 5);
const width = Math.max(Math.trunc(clamp(rawWidth, 120 * uiScale, 800 * uiScale)), 1);
const yInt = Math.trunc(clamp(y, topMargin, maxCenterY));
const xlInt = Math.trunc(clampedCenterX - width * 0.5);
const height = Math.max(Math.trunc(width * 0.8), Math.round(12 * uiScale));
return {
rect: {
x: xlInt - sidePad,
y: yInt - height,
width: width + sidePad * 2,
height,
},
dRel: distance,
videoWidth,
videoHeight,
};
}
function getPrimaryVisionDistance(model) {
const lead = Array.isArray(model?.leadsV3) ? model.leadsV3[0] : null;
const prob = finiteNumber(lead?.prob, 0);
const distance = finiteNumber(lead?.x?.[0], 0);
return prob > 0.5 && distance > 0 ? Math.max(0, distance - 1.52) : 0;
}
function drawLeadBoxCard(box, strokeColor, fillColor, primary = true) {
if (!box?.rect) return;
const { x, y, width, height } = box.rect;
const uiScale = getLeadUiScale(box.videoWidth || BASE_CAMERA.width, box.videoHeight || BASE_CAMERA.height);
const r = Math.max((primary ? 15 : 12) * uiScale, primary ? 7 : 6);
const sw = Math.max((primary ? 3.0 : 2.2) * uiScale, primary ? 1.7 : 1.3);
// C3 style: fill + stroke (carrot.cc ui_fill_rect with stroke color)
fillRoundedRect(ctx, x, y, width, height, r, fillColor);
strokeRoundedRect(ctx, x, y, width, height, r, strokeColor, sw);
}
function eraseLeadBoxOcclusion(box, primary = true) {
if (!box?.rect) return;
const { x, y, width, height } = box.rect;
const uiScale = getLeadUiScale(box.videoWidth || BASE_CAMERA.width, box.videoHeight || BASE_CAMERA.height);
ctx.save();
ctx.globalCompositeOperation = "destination-out";
fillRoundedRect(ctx, x, y, width, height, Math.max((primary ? 15 : 12) * uiScale, primary ? 7 : 6), "rgba(0,0,0,1)");
ctx.restore();
}
// C3-style dual distance badges: radar left (red/orange) + vision right (blue)
function drawLeadDistanceBadgesC3(box, radarDist, visionDist, isLeadScc, textColor = "#ffffff", videoWidth = BASE_CAMERA.width, videoHeight = BASE_CAMERA.height, stageWidth = videoWidth, stageHeight = videoHeight, transform = null) {
if (!box?.rect) return;
const cx = box.centerX;
const offsets = getLeadBadgeOffsets(videoWidth, videoHeight);
const badgeH = offsets.badgeHeight;
const fontSize = offsets.fontSize;
const visibleRect = getVisibleSourceRect(videoWidth, videoHeight, stageWidth, stageHeight, transform);
const baseY = Math.min(box.centerY + offsets.rectTopOffset, Math.max(visibleRect.top + 4, visibleRect.bottom - badgeH - 4));
const drawBadge = (text, centerX, bgColor) => {
const charW = fontSize * 0.62;
const w = Math.max(52 * getLeadUiScale(videoWidth, videoHeight), 16 * getLeadUiScale(videoWidth, videoHeight) + text.length * charW);
const bx = centerX - w * 0.5;
fillRoundedRect(ctx, bx, baseY, w, badgeH, offsets.radius, bgColor);
ctx.save();
ctx.font = `800 ${fontSize}px ${HUD_TEXT_FONT}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineJoin = "round";
ctx.strokeStyle = "rgba(0,0,0,0.82)";
ctx.fillStyle = textColor;
ctx.lineWidth = offsets.strokeWidth;
ctx.strokeText(text, centerX, baseY + badgeH * 0.54);
ctx.fillText(text, centerX, baseY + badgeH * 0.54);
ctx.restore();
};
const hasRadar = radarDist > 0;
const hasVision = visionDist > 0;
// C3 exact colors: COLOR_RED(255,0,0), COLOR_ORANGE(255,175,3), COLOR_BLUE(0,0,255)
const radarColor = isLeadScc ? "rgba(255,0,0,0.92)" : "rgba(255,175,3,0.92)";
const visionColor = "rgba(0,0,255,0.92)";
const radarCenterX = cx - offsets.dx;
const visionCenterX = cx + offsets.dx;
if (hasRadar && hasVision) {
const rText = radarDist < 10 ? radarDist.toFixed(1) : radarDist.toFixed(0);
const vText = visionDist < 10 ? visionDist.toFixed(1) : visionDist.toFixed(0);
drawBadge(rText, radarCenterX, radarColor);
drawBadge(vText, visionCenterX, visionColor);
} else if (hasRadar) {
const rText = radarDist < 10 ? radarDist.toFixed(1) : radarDist.toFixed(0);
drawBadge(rText, radarCenterX, radarColor);
} else if (hasVision) {
const vText = visionDist < 10 ? visionDist.toFixed(1) : visionDist.toFixed(0);
drawBadge(vText, visionCenterX, visionColor);
}
}
function leadStateAccentColor(xState) {
switch (xState) {
case 3:
case 5:
return "rgba(255, 167, 38, 0.82)";
case 4:
return "rgba(35, 213, 93, 0.82)";
case 1:
return "rgba(145, 164, 191, 0.82)";
default:
return "rgba(255, 255, 255, 0.82)";
}
}
function getLeadStateText(overlayState, hudState) {
const longPlan = hudState?.longitudinalPlan || {};
const carrotMan = overlayState?.carrotMan || hudState?.carrotMan || {};
const carState = hudState?.carState || {};
const xState = finiteNumber(longPlan?.xState, finiteNumber(carrotMan?.xState, -1));
const trafficState = finiteNumber(longPlan?.trafficState, finiteNumber(carrotMan?.trafficState, -1));
const longActive = Boolean(hudState?.controlsState?.enabled);
const vEgoMps = finiteNumber(carState?.vEgo, finiteNumber(carState?.vEgoCluster, 0));
const brakeHoldActive = Boolean(carState?.brakeHoldActive);
const softHoldActive = finiteNumber(carState?.softHoldActive, 0) > 0;
const carrotCruise = finiteNumber(carState?.carrotCruise, 0) > 0;
if (brakeHoldActive || softHoldActive || carrotCruise) {
return {
text: brakeHoldActive ? "AUTOHOLD" : (softHoldActive ? "SOFTHOLD" : "CARROT"),
xState,
showDistanceBadge: false,
};
}
if (!longActive) {
return {
text: "",
xState,
showDistanceBadge: true,
};
}
if (xState === 3 || xState === 5) {
return {
text: vEgoMps < 1.0 ? (trafficState >= 1000 ? "Signal Error" : "Signal Ready") : "Signal slowing",
xState,
showDistanceBadge: false,
};
}
if (xState === 4) {
return {
text: getUIText("e2e_driving", "E2E driving"),
xState,
showDistanceBadge: false,
};
}
if (xState === 0 || xState === 1 || xState === 2) {
return {
text: "",
xState,
showDistanceBadge: true,
};
}
return {
text: "",
xState,
showDistanceBadge: false,
};
}
function drawLeadStateBadge(box, text, _xState, videoWidth = BASE_CAMERA.width, videoHeight = BASE_CAMERA.height, stageWidth = videoWidth, stageHeight = videoHeight, transform = null) {
if (!box?.rect || !text) return;
const uiScale = getLeadUiScale(videoWidth, videoHeight);
const offsets = getLeadBadgeOffsets(videoWidth, videoHeight);
const visibleRect = getVisibleSourceRect(videoWidth, videoHeight, stageWidth, stageHeight, transform);
const textY = Math.min(box.centerY + offsets.textBaselineOffset, Math.max(visibleRect.top + 6 * uiScale, visibleRect.bottom - 6 * uiScale));
drawCanvasOutlinedText(text, box.centerX, textY, {
fontSize: Math.max(50 * uiScale, 22),
fontWeight: 900,
fillStyle: "#ffffff",
strokeStyle: "rgba(0,0,0,0.88)",
strokeWidth: Math.max(4.0 * uiScale, 2.2),
align: "center",
baseline: "bottom",
});
}
function drawCanvasOutlinedText(text, x, y, {
fontSize = 18,
fontWeight = 800,
fillStyle = "#ffffff",
strokeStyle = "rgba(0,0,0,0.86)",
strokeWidth = 3.4,
align = "center",
baseline = "middle",
canvasCtx = ctx,
} = {}) {
if (!text) return;
canvasCtx.save();
canvasCtx.font = `${fontWeight} ${fontSize}px ${HUD_TEXT_FONT}`;
canvasCtx.textAlign = align;
canvasCtx.textBaseline = baseline;
canvasCtx.lineJoin = "round";
canvasCtx.strokeStyle = strokeStyle;
canvasCtx.fillStyle = fillStyle;
canvasCtx.lineWidth = strokeWidth;
canvasCtx.strokeText(text, x, y);
canvasCtx.fillText(text, x, y);
canvasCtx.restore();
}
function clampTextAnchor(point, text, fontSize, videoWidth, videoHeight) {
const anchor = { x: finiteNumber(point?.x, 0), y: finiteNumber(point?.y, 0) };
const font = `800 ${fontSize}px ${HUD_TEXT_FONT}`;
const textWidth = Math.max(getCachedTextWidth(ctx, font, String(text || "")), 1);
const padding = 8;
anchor.x = clamp(anchor.x, padding, Math.max(padding, videoWidth - textWidth - padding));
anchor.y = clamp(anchor.y, fontSize + padding, Math.max(fontSize + padding, videoHeight - padding));
return anchor;
}
function drawRadarSpeedBadge(center, text, accentColor, videoWidth = BASE_CAMERA.width, videoHeight = BASE_CAMERA.height) {
if (!center || !text) return;
const uiScale = getLeadUiScale(videoWidth, videoHeight);
const badgeWidth = Math.max(40 * uiScale, 35 * uiScale * String(text).length);
const badgeHeight = Math.max(42 * uiScale, 20);
const badgeX = center.x - badgeWidth * 0.5;
const badgeY = center.y - 35 * uiScale;
fillRoundedRect(ctx, badgeX, badgeY, badgeWidth, badgeHeight, Math.max(15 * uiScale, 7), accentColor);
drawCanvasOutlinedText(String(text), center.x, center.y, {
fontSize: Math.max(40 * uiScale, 18),
fontWeight: 900,
strokeWidth: Math.max(4.2 * uiScale, 2.2),
});
}
function drawProjectedTfMarker(modelPath, longitudinalPlan, calibTransform, videoWidth, videoHeight) {
if (finiteNumber(paramsState.ShowPathEnd, 0) <= 0) return;
const tfDistance = finiteNumber(longitudinalPlan?.desiredDistance, 0);
if (!Number.isFinite(tfDistance) || tfDistance <= 0) return;
const xs = Array.isArray(modelPath?.x) ? modelPath.x : [];
if (xs.length < 2) return;
const lastX = finiteNumber(xs[xs.length - 1], 0);
if (!lastX || tfDistance > lastX) return;
const lineY = samplePathY(modelPath, tfDistance);
const lineZ = interp1D(tfDistance, xs, Array.isArray(modelPath?.z) ? modelPath.z : []);
if (!Number.isFinite(lineY) || !Number.isFinite(lineZ)) return;
const left = projectPoint(calibTransform, tfDistance, lineY - 1.0, lineZ + PATH_Z_OFFSET);
const right = projectPoint(calibTransform, tfDistance, lineY + 1.0, lineZ + PATH_Z_OFFSET);
if (!left || !right) return;
const uiScale = getLeadUiScale(videoWidth, videoHeight);
drawPolyline([left, right], "rgba(255,255,255,0.92)", Math.max(3.0 * uiScale, 1.6));
const labelText = `${displayDistanceMeters(tfDistance).toFixed(1)}(${finiteNumber(longitudinalPlan?.tFollow, 0).toFixed(2)})`;
const labelFontSize = Math.max(20 * uiScale, 12);
const labelAnchor = clampTextAnchor(
{ x: right.x + 10, y: right.y - 4 },
labelText,
labelFontSize,
videoWidth,
videoHeight,
);
drawCanvasOutlinedText(labelText, labelAnchor.x, labelAnchor.y, {
fontSize: labelFontSize,
fontWeight: 800,
align: "left",
strokeWidth: Math.max(3.4 * uiScale, 1.8),
});
}
function getPathStatusText(hudState) {
const carState = hudState?.carState || {};
if (Boolean(carState?.brakeHoldActive)) return "AUTOHOLD";
if (finiteNumber(carState?.softHoldActive, 0) > 0) return "SOFTHOLD";
if (finiteNumber(carState?.carrotCruise, 0) > 0) return "CARROT";
return "";
}
function projectPathEndAnchorBox(modelPath, calibTransform, videoWidth, videoHeight) {
const xs = Array.isArray(modelPath?.x) ? modelPath.x : [];
const ys = Array.isArray(modelPath?.y) ? modelPath.y : [];
const zs = Array.isArray(modelPath?.z) ? modelPath.z : [];
if (!xs.length || !ys.length || !zs.length) return null;
const tailDistance = clamp(finiteNumber(xs[xs.length - 1], 0), MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE);
const idx = getPathLengthIdx(modelPath, tailDistance);
const distance = finiteNumber(xs[idx], NaN);
const centerLineY = finiteNumber(ys[idx], NaN);
const centerLineZ = finiteNumber(zs[idx], NaN);
if (!Number.isFinite(distance) || !Number.isFinite(centerLineY) || !Number.isFinite(centerLineZ)) return null;
const left = projectPointPrecise(calibTransform, distance, centerLineY - 1.2, centerLineZ + PATH_Z_OFFSET);
const right = projectPointPrecise(calibTransform, distance, centerLineY + 1.2, centerLineZ + PATH_Z_OFFSET);
if (!left || !right) return null;
const rawWidth = Math.abs(right.x - left.x);
if (!Number.isFinite(rawWidth) || rawWidth <= 1) return null;
const rawCenterX = (left.x + right.x) * 0.5;
const rawCenterY = (left.y + right.y) * 0.5;
const { marginX, topMargin, bottomMargin } = getLeadBoxClampMargins(videoWidth, videoHeight);
const uiScale = getLeadUiScale(videoWidth, videoHeight);
const width = clamp(rawWidth, 120 * uiScale, 800 * uiScale);
const centerX = clamp(rawCenterX, marginX, Math.max(marginX, videoWidth - marginX));
const centerY = clamp(rawCenterY, topMargin, Math.max(topMargin, videoHeight - bottomMargin));
return {
centerX: Math.trunc(centerX),
centerY: Math.trunc(centerY),
width: Math.trunc(width),
};
}
function drawPathStatusText(modelPath, hudState, calibTransform, videoWidth, videoHeight, anchorBox = null) {
const text = getPathStatusText(hudState);
if (!text) return;
const anchor = anchorBox || projectPathEndAnchorBox(modelPath, calibTransform, videoWidth, videoHeight);
if (!anchor) return;
const scale = getLeadUiScale(videoWidth, videoHeight);
const fontSize = Math.max(50 * scale, 22);
const baselineY = clamp(anchor.centerY + 60 * scale, fontSize + 8 * scale, videoHeight - 10 * scale);
drawCanvasOutlinedText(text, anchor.centerX, baselineY, {
fontSize,
fontWeight: 900,
fillStyle: "#ffffff",
strokeStyle: "rgba(0,0,0,0.88)",
strokeWidth: Math.max(4.0 * scale, 2.2),
align: "center",
baseline: "bottom",
});
}
function getRadarTracks(radarState) {
const source = radarState && typeof radarState === "object" ? radarState : {};
const tracks = [
...(Array.isArray(source.leadsLeft) ? source.leadsLeft : []),
...(Array.isArray(source.leadsRight) ? source.leadsRight : []),
...(Array.isArray(source.leadsCenter) ? source.leadsCenter : []),
];
if (tracks.length) return tracks;
const fallback = [];
if (source.leadOne?.status) fallback.push(source.leadOne);
if (source.leadTwo?.status) fallback.push(source.leadTwo);
return fallback;
}
function getRadarProjectionLine(model) {
const laneLines = Array.isArray(model?.laneLines) ? model.laneLines : [];
const centerLane = laneLines[2];
if (Array.isArray(centerLane?.x) && Array.isArray(centerLane?.z) && centerLane.x.length && centerLane.z.length) {
return centerLane;
}
return model?.position || null;
}
function drawRadarTargets(radarState, model, calibTransform, videoWidth = BASE_CAMERA.width, videoHeight = BASE_CAMERA.height) {
const showRadarInfo = finiteNumber(paramsState.ShowRadarInfo, 0);
if (showRadarInfo <= 0) return;
const projectionLine = getRadarProjectionLine(model);
if (!projectionLine) return;
const radarLatFactor = finiteNumber(paramsState.RadarLatFactor, 0) / 100.0;
const isMetric = isMetricDisplay();
for (const radar of getRadarTracks(radarState)) {
const dRel = finiteNumber(radar?.dRel, 0);
if (!Number.isFinite(dRel) || dRel <= 2.5) continue;
const yRel = finiteNumber(radar?.yRel, 0);
const z = samplePathZ(projectionLine, dRel) - 0.61;
const center = projectPointPrecise(calibTransform, dRel, -yRel, z);
if (!center) continue;
const vLead = finiteNumber(radar?.vLeadK, finiteNumber(radar?.vRel, 0));
const vLat = finiteNumber(radar?.vLat, 0);
const vAbs = Math.sqrt((vLead * vLead) + (vLat * vLat));
const vSigned = vLead >= 0 ? vAbs : -vAbs;
const radarDetected = Boolean(radar?.radar);
const modelProb = finiteNumber(radar?.modelProb, 0);
if (vAbs > 3.0) {
const futureDRel = Math.max(2.0, dRel + vLead * radarLatFactor);
const futureYRel = yRel + vLat * radarLatFactor;
const future = Math.abs(vLead) > 3.0
? projectPointPrecise(calibTransform, futureDRel, -futureYRel, z)
: null;
if (future) {
const vectorColor = vSigned >= 0 ? "rgba(35,213,93,0.94)" : "rgba(255,59,48,0.94)";
const uiScale = getLeadUiScale(videoWidth, videoHeight);
drawPolyline([center, future], vectorColor, Math.max(3.0 * uiScale, 1.6));
drawPolygon(circlePolygon(future.x, future.y, Math.max(10 * uiScale, 5), 12), vectorColor);
}
let badgeColor = "rgba(255,59,48,0.96)";
if (!radarDetected) badgeColor = "rgba(61,123,255,0.96)";
else if (Math.abs(modelProb - 0.01) < 1e-3) badgeColor = "rgba(35,213,93,0.96)";
else if (vSigned > 0) badgeColor = "rgba(255,167,38,0.96)";
const speedValue = vSigned * (isMetric ? 3.6 : 2.2369363);
drawRadarSpeedBadge({ x: center.x, y: center.y }, speedValue.toFixed(0), badgeColor, videoWidth, videoHeight);
if (showRadarInfo >= 2) {
const uiScale = getLeadUiScale(videoWidth, videoHeight);
drawCanvasOutlinedText(displayDistanceMeters(finiteNumber(radar?.yRel, 0)).toFixed(1), center.x, center.y - 40 * uiScale, {
fontSize: Math.max(30 * uiScale, 15),
fontWeight: 900,
strokeWidth: Math.max(3.8 * uiScale, 2.0),
});
const distanceValue = displayDistanceMeters(dRel);
drawCanvasOutlinedText(distanceValue.toFixed(1), center.x, center.y + 30 * uiScale, {
fontSize: Math.max(30 * uiScale, 15),
fontWeight: 900,
strokeWidth: Math.max(3.8 * uiScale, 2.0),
});
}
} else if (showRadarInfo >= 3) {
const uiScale = getLeadUiScale(videoWidth, videoHeight);
drawCanvasOutlinedText("*", center.x, center.y, {
fontSize: Math.max(40 * uiScale, 18),
fontWeight: 900,
strokeWidth: Math.max(4.2 * uiScale, 2.2),
});
}
}
}
function drawRadarLeadBoxes(model, overlayState, hudState, calibTransform, videoWidth, videoHeight, stageWidth, stageHeight, transform) {
const radarState = overlayState?.radarState || {};
const modelPath = model?.position || null;
const leadState = getLeadStateText(overlayState, hudState);
const roadCameraState = overlayState?.roadCameraState || null;
const longPlan = hudState?.longitudinalPlan || {};
const leadTwoStatus = finiteNumber(longPlan?.longitudinalPlanSource, 0) === LONG_PLAN_SOURCE_LEAD1 ? 2 : 1;
syncLeadRenderState(videoWidth, videoHeight, model?.frameId, roadCameraState?.frameId);
const leadOneBox = projectLeadBox(
radarState?.leadOne,
modelPath,
calibTransform,
videoWidth,
videoHeight,
0,
stageWidth,
stageHeight,
transform,
{
includeDistanceBadge: leadState?.showDistanceBadge !== false,
includeStateText: Boolean(leadState?.text),
},
);
const nowMs = performance.now();
let primaryStatusAnchorBox = null;
if (leadOneBox) {
const isLeadScc = leadOneBox.radarTrackId < 1;
// C3 exact colors: COLOR_RED(255,0,0), COLOR_ORANGE(255,175,3), COLOR_BLUE(0,0,255)
const strokeColor = !leadOneBox.radarDetected
? "rgba(0,0,255,0.96)"
: (isLeadScc ? "rgba(255,0,0,0.96)" : "rgba(255,175,3,0.96)");
eraseLeadBoxOcclusion(leadOneBox, true);
drawLeadBoxCard(leadOneBox, strokeColor, "rgba(0,0,0,0.20)", true);
// Update hold state for persistence across brief status flickers
leadHoldState.lastValidMs = nowMs;
leadHoldState.box = leadOneBox;
leadHoldState.strokeColor = strokeColor;
leadHoldState.isLeadScc = isLeadScc;
// C3-style distance badges: radar (red/orange bg) + vision (blue bg) side by side below box
if (leadState?.showDistanceBadge !== false) {
const radarDist = Boolean(radarState?.leadOne?.radar) ? Math.max(0, finiteNumber(radarState?.leadOne?.dRel, 0)) : 0;
const visionDist = getPrimaryVisionDistance(model);
leadHoldState.radarDist = radarDist;
leadHoldState.visionDist = visionDist;
if (radarDist > 0 || visionDist > 0) {
const badgeTextColor = leadState?.xState === 0 ? "#ffffff" : (leadState?.xState === 1 ? "#b0b0b0" : "#23d55d");
leadHoldState.badgeTextColor = badgeTextColor;
drawLeadDistanceBadgesC3(leadOneBox, radarDist, visionDist, isLeadScc, badgeTextColor, videoWidth, videoHeight, stageWidth, stageHeight, transform);
}
}
if (leadState?.text) {
drawLeadStateBadge(leadOneBox, leadState.text, leadState.xState, videoWidth, videoHeight, stageWidth, stageHeight, transform);
}
primaryStatusAnchorBox = leadOneBox;
} else if (leadHoldState.box && (nowMs - leadHoldState.lastValidMs) < LEAD_HOLD_MS) {
// Status flickered false briefly — hold last valid box (C3's SubMaster doesn't flicker)
const held = leadHoldState;
eraseLeadBoxOcclusion(held.box, true);
drawLeadBoxCard(held.box, held.strokeColor, "rgba(0,0,0,0.20)", true);
if (leadState?.showDistanceBadge !== false && (held.radarDist > 0 || held.visionDist > 0)) {
drawLeadDistanceBadgesC3(held.box, held.radarDist, held.visionDist, held.isLeadScc, held.badgeTextColor, videoWidth, videoHeight, stageWidth, stageHeight, transform);
}
if (leadState?.text) {
drawLeadStateBadge(held.box, leadState.text, leadState.xState, videoWidth, videoHeight, stageWidth, stageHeight, transform);
}
primaryStatusAnchorBox = held.box;
} else {
leadHoldState.box = null;
}
// LeadTwo: same logic as C3 (radar && dRel > leadOne.dRel + 3)
const leadTwo = radarState?.leadTwo;
const validLeadTwo = Boolean(leadTwo?.status) &&
Boolean(leadTwo?.radar) &&
finiteNumber(leadTwo?.dRel, 0) > (finiteNumber(radarState?.leadOne?.dRel, 0) + 3);
if (validLeadTwo) {
const leadTwoBox = projectLeadTwoBox(leadTwo, modelPath, calibTransform, videoWidth, videoHeight, stageWidth, stageHeight, transform);
if (leadTwoBox) {
eraseLeadBoxOcclusion(leadTwoBox, false);
drawLeadBoxCard(
leadTwoBox,
"rgba(218,111,37,0.96)", // C3 COLOR_OCHRE(218,111,37)
leadTwoStatus === 2 ? "rgba(255,0,0,0.20)" : "rgba(0,0,0,0.20)", // C3 COLOR_RED_ALPHA(50)
false,
);
}
} else {
resetLeadEmaSlot(1);
resetLeadTwoEma();
}
drawPathStatusText(modelPath, hudState, calibTransform, videoWidth, videoHeight, primaryStatusAnchorBox);
drawRadarTargets(radarState, model, calibTransform, videoWidth, videoHeight);
}
function roundedRectPath(context, x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
context.beginPath();
context.moveTo(x + r, y);
context.lineTo(x + width - r, y);
context.quadraticCurveTo(x + width, y, x + width, y + r);
context.lineTo(x + width, y + height - r);
context.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
context.lineTo(x + r, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - r);
context.lineTo(x, y + r);
context.quadraticCurveTo(x, y, x + r, y);
context.closePath();
}
function fillRoundedRect(context, x, y, width, height, radius, fillStyle) {
context.fillStyle = fillStyle;
const cachedPath = getCachedRoundedRectPath(width, height, radius);
if (cachedPath) {
context.save();
context.translate(x, y);
context.fill(cachedPath);
context.restore();
return;
}
roundedRectPath(context, x, y, width, height, radius);
context.fill();
}
function strokeRoundedRect(context, x, y, width, height, radius, strokeStyle, lineWidth = 1) {
context.strokeStyle = strokeStyle;
context.lineWidth = lineWidth;
const cachedPath = getCachedRoundedRectPath(width, height, radius);
if (cachedPath) {
context.save();
context.translate(x, y);
context.stroke(cachedPath);
context.restore();
return;
}
roundedRectPath(context, x, y, width, height, radius);
context.stroke();
}
function getHudLabelAlpha(pathMode, phaseShift = 0) {
if (pathMode < 1 || pathMode > 8) return 0.94;
const wave = (Math.sin((performance.now() / 1000) * 0.78 + phaseShift) + 1.0) * 0.5;
const eased = Math.pow(wave, 1.7);
return clamp(0.14 + eased * 0.86, 0.14, 1.0);
}
function formatBottomCenterText(text, hudState) {
const raw = String(text || "").trim();
const modeText = laneModeLabel(hudState);
if (!raw) return modeText;
const normalized = raw.toLowerCase();
if (normalized.startsWith("lanemode") || normalized.startsWith("laneless")) {
return raw;
}
return `${modeText} | ${raw}`;
}
function drawHudTopRightText(stageWidth, stageHeight, viewportRect, text, pathMode) {
const label = shortText(text, 160);
if (!label) return;
const exactC3Mode = stageWidth >= 1280 && stageHeight >= 720;
const baseScale = Math.min(stageWidth / 1920, stageHeight / 1080);
const scale = clamp(baseScale, 0.48, 1.0);
const edgeInsetX = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const edgeInsetTop = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const maxWidth = Math.max(120.0, viewportRect.width - edgeInsetX * 2.0);
const fontSize = fitSingleLineHudFontSize(
label,
exactC3Mode ? 24.0 : clamp(24.0 * scale, 7.0, 24.0),
maxWidth,
4.5,
900,
);
const alpha = getHudLabelAlpha(pathMode, 0.0);
drawOutlinedHudText({
text: label,
x: viewportRect.right - edgeInsetX,
y: viewportRect.top + edgeInsetTop,
color: `rgba(244, 244, 244, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.2 * scale, 2.8, 5.4),
fontSize,
fontWeight: 900,
alignX: "right",
alignY: "top",
maxWidth,
});
}
function optionalNumber(value) {
if (value == null || value === "") return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function formatRtcNumber(value, suffix = "", digits = 0) {
return value == null ? `-${suffix}` : `${value.toFixed(digits)}${suffix}`;
}
function getRtcCodecLabel(codec, codecParams) {
const rawCodec = String(codec || "").split("/").pop().trim().toUpperCase();
if (!rawCodec) return "-";
if (rawCodec !== "H264") return rawCodec;
const match = /profile-level-id=([0-9a-fA-F]{6})/.exec(String(codecParams || ""));
const profilePrefix = match?.[1]?.slice(0, 2)?.toLowerCase() || "";
const profile = profilePrefix === "42"
? "Baseline"
: profilePrefix === "4d"
? "Main"
: profilePrefix === "64"
? "High"
: "";
return profile ? `${rawCodec} ${profile}` : rawCodec;
}
function buildRtcPerfHudModel() {
const perf = window.CarrotRtcPerf || null;
if (!perf?.active) {
return { visible: false, tone: "offline", title: "VISION OFFLINE", glance: "" };
}
const network = perf.network || {};
const inbound = perf.inbound || {};
const video = perf.video || {};
const resolution = String(network.resolutionLabel || "").trim() || "-";
const bitrateMbps = optionalNumber(network.bitrateMbps);
const fps = optionalNumber(inbound.framesPerSecond);
const rttMs = optionalNumber(network.rttMs);
const lossPct = optionalNumber(network.lossPct);
const jitterMs = optionalNumber(network.jitterMs);
const freezeCount = optionalNumber(inbound.freezeCount);
const framesDecoded = optionalNumber(inbound.framesDecoded);
const framesReceived = optionalNumber(inbound.framesReceived);
const readyState = optionalNumber(video.readyState);
const reconnecting = (
perf.connectionState === "connecting" ||
perf.iceConnectionState === "checking" ||
perf.iceConnectionState === "disconnected"
);
const stalled = (
(framesReceived != null && framesReceived > 0 && framesDecoded != null && framesDecoded <= 0) ||
(freezeCount != null && freezeCount > 0 && readyState != null && readyState < 3)
);
const degraded = (
(lossPct != null && lossPct >= 0.5) ||
(jitterMs != null && jitterMs >= 30) ||
(rttMs != null && rttMs >= 150)
);
const bitrateLabel = formatRtcNumber(bitrateMbps, "Mbps", bitrateMbps != null && bitrateMbps < 10 ? 2 : 1);
const fpsLabel = fps == null ? "-fps" : `${Math.round(fps)}fps`;
const rttLabel = rttMs == null ? "-ms" : `${Math.round(rttMs)}ms`;
const jitterLabel = jitterMs == null ? "-ms" : `${Math.round(jitterMs)}ms`;
const lossLabel = lossPct == null ? "-%" : `${lossPct.toFixed(lossPct >= 10 ? 0 : 1)}%`;
const codecLabel = getRtcCodecLabel(perf.codec, perf.codecParams);
const protocol = String(network.protocol || "-").toUpperCase();
const localType = String(network.localCandidateType || "-");
const remoteType = String(network.remoteCandidateType || "-");
let tone = "normal";
let title = "VISION OK";
let glance = `${resolution} | ${fpsLabel} | ${bitrateLabel} | ${rttLabel}`;
if (perf.error) {
tone = "offline";
title = "VISION OFFLINE";
glance = "OFFLINE | stats unavailable";
} else if (reconnecting) {
tone = "reconnecting";
title = "VISION RECONNECTING";
glance = "RECONNECTING | waiting stream";
} else if (stalled) {
tone = "stall";
title = "VISION STALL";
glance = `STALL | decode ${framesDecoded ?? "-"} | recv ${framesReceived ?? "-"}`;
} else if (degraded) {
tone = "degraded";
title = "VISION DEGRADED";
const warnings = [];
if (lossPct != null && lossPct >= 0.5) warnings.push(`loss ${lossLabel}`);
if (rttMs != null && rttMs >= 150) warnings.push(`RTT ${rttLabel}`);
if (jitterMs != null && jitterMs >= 30) warnings.push(`jitter ${jitterLabel}`);
glance = `DEGRADED | ${warnings.slice(0, 2).join(" | ")}`;
}
return {
visible: true,
tone,
title,
glance,
video: `${resolution} | ${fpsLabel}`,
codec: codecLabel,
bitrate: bitrateLabel,
rtt: rttLabel,
jitter: jitterLabel,
loss: lossLabel,
freeze: freezeCount == null ? "-" : String(Math.round(freezeCount)),
path: `${protocol} ${localType} -> ${remoteType}`,
};
}
function formatRtcPerfLabel() {
return buildRtcPerfHudModel().glance || "";
}
let _rtcPerfSummaryOpen = false;
let _rtcPerfSummaryCloseTimer = null;
function isRtcPerfSummaryAvailable() {
return window.matchMedia("(orientation: landscape)").matches;
}
function clearRtcPerfSummaryCloseTimer() {
if (_rtcPerfSummaryCloseTimer == null) return;
window.clearTimeout(_rtcPerfSummaryCloseTimer);
_rtcPerfSummaryCloseTimer = null;
}
function syncRtcPerfSummaryAutoClose(tone) {
clearRtcPerfSummaryCloseTimer();
if (!_rtcPerfSummaryOpen || tone !== "normal") return;
_rtcPerfSummaryCloseTimer = window.setTimeout(() => {
setRtcPerfSummaryOpen(false);
}, RTC_PERF_SUMMARY_AUTO_CLOSE_MS);
}
function setRtcPerfSummaryOpen(open) {
const model = buildRtcPerfHudModel();
const nextOpen = Boolean(open && model.visible && isRtcPerfSummaryAvailable());
_rtcPerfSummaryOpen = nextOpen;
if (rtcPerfSummaryEl) rtcPerfSummaryEl.hidden = !nextOpen;
if (!nextOpen) {
clearRtcPerfSummaryCloseTimer();
return;
}
syncRtcPerfSummaryAutoClose(model.tone);
}
function syncRtcPerfHud() {
if (!rtcPerfHudEl || !rtcPerfGlanceEl || !rtcPerfGlanceTextEl) return;
const model = buildRtcPerfHudModel();
rtcPerfHudEl.hidden = !model.visible;
rtcPerfGlanceEl.dataset.tone = model.tone;
rtcPerfGlanceTextEl.textContent = model.glance || model.title;
if (!model.visible || !isRtcPerfSummaryAvailable()) {
setRtcPerfSummaryOpen(false);
return;
}
if (rtcPerfSummaryEl) rtcPerfSummaryEl.dataset.tone = model.tone;
if (rtcPerfTitleEl) rtcPerfTitleEl.textContent = model.title;
if (rtcPerfVideoEl) rtcPerfVideoEl.textContent = model.video;
if (rtcPerfCodecEl) rtcPerfCodecEl.textContent = model.codec;
if (rtcPerfBitrateEl) rtcPerfBitrateEl.textContent = model.bitrate;
if (rtcPerfRttEl) rtcPerfRttEl.textContent = model.rtt;
if (rtcPerfJitterEl) rtcPerfJitterEl.textContent = model.jitter;
if (rtcPerfLossEl) rtcPerfLossEl.textContent = model.loss;
if (rtcPerfFreezeEl) rtcPerfFreezeEl.textContent = model.freeze;
if (rtcPerfPathEl) rtcPerfPathEl.textContent = model.path;
if (_rtcPerfSummaryOpen && _rtcPerfSummaryCloseTimer == null) {
syncRtcPerfSummaryAutoClose(model.tone);
} else if (_rtcPerfSummaryOpen && model.tone !== "normal") {
clearRtcPerfSummaryCloseTimer();
}
}
function drawStageEdgeFades(stageWidth, stageHeight) {
const topHeight = clamp(stageHeight * 0.16, 72, 148);
const bottomHeight = clamp(stageHeight * 0.24, 104, 212);
hudCtx.save();
hudCtx.globalCompositeOperation = "source-over";
const topGradientKey = `hud-top:${Math.round(stageWidth)}x${Math.round(stageHeight)}:${Math.round(topHeight)}`;
const topGradient = getCachedHudGradient(topGradientKey, () => {
const gradient = hudCtx.createLinearGradient(0, 0, 0, topHeight);
gradient.addColorStop(0.0, "rgba(0, 0, 0, 0.64)");
gradient.addColorStop(0.42, "rgba(0, 0, 0, 0.30)");
gradient.addColorStop(1.0, "rgba(0, 0, 0, 0.00)");
return gradient;
});
hudCtx.fillStyle = topGradient;
hudCtx.fillRect(0, 0, stageWidth, topHeight);
const bottomStart = Math.max(0, stageHeight - bottomHeight);
const bottomGradientKey = `hud-bottom:${Math.round(stageWidth)}x${Math.round(stageHeight)}:${Math.round(bottomStart)}:${Math.round(bottomHeight)}`;
const bottomGradient = getCachedHudGradient(bottomGradientKey, () => {
const gradient = hudCtx.createLinearGradient(0, bottomStart, 0, stageHeight);
gradient.addColorStop(0.0, "rgba(0, 0, 0, 0.00)");
gradient.addColorStop(0.42, "rgba(0, 0, 0, 0.28)");
gradient.addColorStop(1.0, "rgba(0, 0, 0, 0.74)");
return gradient;
});
hudCtx.fillStyle = bottomGradient;
hudCtx.fillRect(0, bottomStart, stageWidth, bottomHeight);
hudCtx.restore();
}
function drawHudTopLeftText(stageWidth, stageHeight, viewportRect, text, pathMode) {
if (window.matchMedia("(orientation: portrait)").matches) return;
const label = shortText(text, 160);
if (!label) return;
const exactC3Mode = stageWidth >= 1280 && stageHeight >= 720;
const baseScale = Math.min(stageWidth / 1920, stageHeight / 1080);
const scale = clamp(baseScale, 0.48, 1.0);
const edgeInsetX = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const edgeInsetTop = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const maxWidth = Math.max(120.0, viewportRect.width - edgeInsetX * 2.0);
const fontSize = fitSingleLineHudFontSize(
label,
exactC3Mode ? 24.0 : clamp(24.0 * scale, 7.0, 24.0),
maxWidth,
4.5,
900,
);
const alpha = getHudLabelAlpha(pathMode, 0.0);
drawOutlinedHudText({
text: label,
x: viewportRect.left + edgeInsetX,
y: viewportRect.top + edgeInsetTop,
color: `rgba(244, 244, 244, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.2 * scale, 2.8, 5.4),
fontSize,
fontWeight: 900,
alignX: "left",
alignY: "top",
maxWidth,
});
}
function drawHudLeftCenterLogs(stageWidth, stageHeight, viewportRect, statusText, metaText, pathMode) {
const statusLabel = shortText(statusText, 96);
const metaLabel = shortText(metaText, 160);
if (!statusLabel && !metaLabel) return;
const exactC3Mode = stageWidth >= 1280 && stageHeight >= 720;
const baseScale = Math.min(stageWidth / 1920, stageHeight / 1080);
const scale = clamp(baseScale, 0.48, 1.0);
const edgeInsetX = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const maxWidth = Math.max(180.0, viewportRect.width * 0.52);
const alpha = getHudLabelAlpha(pathMode, Math.PI * 0.5);
const centerY = viewportRect.centerY;
const statusFontSize = fitSingleLineHudFontSize(
statusLabel,
exactC3Mode ? 24.0 : clamp(24.0 * scale, 9.0, 24.0),
maxWidth,
6.0,
900,
);
const metaFontSize = fitSingleLineHudFontSize(
metaLabel,
exactC3Mode ? 20.0 : clamp(20.0 * scale, 8.0, 20.0),
maxWidth,
6.0,
800,
);
const statusY = centerY - (exactC3Mode ? 16.0 : clamp(18.0 * scale, 10.0, 18.0));
const metaY = centerY + (exactC3Mode ? 14.0 : clamp(16.0 * scale, 9.0, 16.0));
drawOutlinedHudText({
text: statusLabel,
x: viewportRect.left + edgeInsetX,
y: statusY,
color: `rgba(244, 244, 244, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.2 * scale, 2.8, 5.4),
fontSize: statusFontSize,
fontWeight: 900,
alignX: "left",
alignY: "bottom",
maxWidth,
});
drawOutlinedHudText({
text: metaLabel,
x: viewportRect.left + edgeInsetX,
y: metaY,
color: `rgba(236, 236, 236, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.0 * scale, 2.8, 5.2),
fontSize: metaFontSize,
fontWeight: 800,
alignX: "left",
alignY: "top",
maxWidth,
});
}
function drawHudBottomText(stageWidth, stageHeight, viewportRect, text, hudState, pathMode) {
const label = formatBottomCenterText(text, hudState);
if (!label) return;
const exactC3Mode = stageWidth >= 1280 && stageHeight >= 720;
const baseScale = Math.min(stageWidth / 1920, stageHeight / 1080);
const scale = clamp(baseScale, 0.48, 1.0);
const maxWidth = Math.max(120.0, viewportRect.width - 4.0);
const fontSize = fitSingleLineHudFontSize(
label,
exactC3Mode ? 24.0 : clamp(24.0 * scale, 7.0, 24.0),
maxWidth,
4.5,
900,
);
const bottomInset = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const alpha = getHudLabelAlpha(pathMode, Math.PI);
drawOutlinedHudText({
text: label,
x: viewportRect.centerX,
y: viewportRect.bottom - bottomInset,
color: `rgba(236, 236, 236, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.0 * scale, 2.8, 5.2),
fontSize,
fontWeight: 900,
alignX: "center",
alignY: "baselineBottom",
maxWidth,
});
}
function drawHudBottomLeftText(stageWidth, stageHeight, viewportRect, text, pathMode) {
const label = shortText(text, 160);
if (!label) return;
const exactC3Mode = stageWidth >= 1280 && stageHeight >= 720;
const baseScale = Math.min(stageWidth / 1920, stageHeight / 1080);
const scale = clamp(baseScale, 0.48, 1.0);
const edgeInsetX = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const bottomInset = exactC3Mode ? 1.5 : clamp(2.0 * scale, 1.0, 2.5);
const maxWidth = Math.max(120.0, viewportRect.width * 0.42);
const fontSize = fitSingleLineHudFontSize(
label,
exactC3Mode ? 24.0 : clamp(24.0 * scale, 7.0, 24.0),
maxWidth,
4.5,
900,
);
const alpha = getHudLabelAlpha(pathMode, Math.PI);
drawOutlinedHudText({
text: label,
x: viewportRect.left + edgeInsetX,
y: viewportRect.bottom - bottomInset,
color: `rgba(236, 236, 236, ${alpha.toFixed(3)})`,
strokeColor: `rgba(0, 0, 0, ${clamp(alpha + 0.08, 0.0, 1.0).toFixed(3)})`,
strokeWidth: clamp(4.0 * scale, 2.8, 5.2),
fontSize,
fontWeight: 900,
alignX: "left",
alignY: "baselineBottom",
maxWidth,
});
}
function formatDebugText(overlayState) {
const liveDelay = overlayState.liveDelay || {};
const liveTorqueParameters = overlayState.liveTorqueParameters || {};
const liveParameters = overlayState.liveParameters || {};
const customSr = finiteNumber(paramsState.CustomSR, 0) / 10.0;
return `LD[${finiteNumber(liveDelay.calPerc, 0).toFixed(0)}%,${finiteNumber(liveDelay.lateralDelay, 0).toFixed(2)}] ` +
`LT[${finiteNumber(liveTorqueParameters.calPerc, 0).toFixed(0)}%,${liveTorqueParameters.liveValid ? "ON" : "OFF"}]` +
`(${finiteNumber(liveTorqueParameters.latAccelFactorFiltered, 0).toFixed(2)}/${finiteNumber(liveTorqueParameters.frictionCoefficientFiltered, 0).toFixed(2)}) ` +
`SR(${finiteNumber(liveParameters.steerRatio, 0).toFixed(1)},${customSr.toFixed(1)})`;
}
function isLongActive(overlayState) {
return Boolean(overlayState?.carControl?.longActive);
}
function isLaneMode(hudState) {
return Boolean(hudState?.controlsState?.activeLaneLine) || finiteNumber(hudState?.carState?.useLaneLineSpeed, 0) > 0;
}
function getPathStyle(overlayState, hudState) {
// Mirror carrot.cc path mode/color selection so Carrot params drive web visuals too.
const laneMode = isLaneMode(hudState);
let mode = finiteNumber(laneMode ? paramsState.ShowPathModeLane : paramsState.ShowPathMode, 0);
let colorIndex = finiteNumber(laneMode ? paramsState.ShowPathColorLane : paramsState.ShowPathColor, 13);
const isCruiseOff = !isLongActive(overlayState);
if (isCruiseOff) {
colorIndex = finiteNumber(paramsState.ShowPathColorCruiseOff, 19);
} else if (colorIndex >= 20) {
const leadOne = overlayState?.radarState?.leadOne || {};
const accel = firstFinite(hudState?.longitudinalPlan?.accels, 0);
colorIndex = 13;
if (leadOne.status) {
if (Math.abs(accel) < 0.5) colorIndex = 12;
else if (accel >= 0.5) colorIndex = 11;
else colorIndex = 10;
}
}
return {
mode,
colorIndex,
paletteIndex: colorIndex % 10,
emphasisStroke: colorIndex >= 10 || Boolean(hudState?.carState?.brakeLights),
strokeColor: hudState?.carState?.brakeLights ? "rgba(255, 76, 76, 0.96)" : "rgba(255, 255, 255, 0.92)",
isCruiseOff,
laneMode,
};
}
function getSelectedPath(overlayState, hudState) {
const model = overlayState?.modelV2 || null;
const lateralPlan = overlayState?.lateralPlan || null;
const laneMode = isLaneMode(hudState);
const lanePath = lateralPlan?.position;
const hasLanePath = Array.isArray(lanePath?.x) && lanePath.x.length > 2;
if (laneMode && hasLanePath) {
return {
model,
pathData: lanePath,
pathSource: "lateralPlan",
latDebugText: lateralPlan?.latDebugText || "",
laneMode,
};
}
return {
model,
pathData: model?.position || null,
pathSource: "modelV2",
latDebugText: lateralPlan?.latDebugText || "",
laneMode,
};
}
function buildPlotData(overlayState, hudState) {
const mode = finiteNumber(paramsState.ShowPlotMode, 0);
if (!mode) return null;
const carState = hudState?.carState || {};
const controlsState = hudState?.controlsState || {};
const longPlan = hudState?.longitudinalPlan || {};
const carControl = overlayState?.carControl || {};
const actuators = carControl?.actuators || {};
const model = overlayState?.modelV2 || {};
const radarLead = overlayState?.radarState?.leadOne || {};
const liveParameters = overlayState?.liveParameters || {};
const aEgo = finiteNumber(carState?.aEgo, 0);
const vEgo = finiteNumber(carState?.vEgo, finiteNumber(carState?.vEgoCluster, 0));
const accelTarget = firstFinite(longPlan?.accels, 0);
const speedTarget = firstFinite(longPlan?.speeds, 0);
const modelPos32 = finiteNumber(model?.position?.x?.[32], firstFinite(model?.position?.x, 0));
const modelVel32 = finiteNumber(model?.velocity?.x?.[32], firstFinite(model?.velocity?.x, 0));
const modelVel0 = finiteNumber(model?.velocity?.x?.[0], 0);
switch (mode) {
case 1:
return {
mode,
title: "1.Accel (Y:a_ego, G:a_target, O:a_out)",
values: [aEgo, accelTarget, finiteNumber(actuators?.accel, 0)],
};
case 2:
return {
mode,
title: "2.Speed/Accel(Y:speed_0, G:v_ego, O:a_ego)",
values: [speedTarget, vEgo, aEgo],
};
case 3:
return {
mode,
title: "3.Model(Y:pos_32, G:vel_32, O:vel_0)",
values: [modelPos32, modelVel32, modelVel0],
};
case 4:
return {
mode,
title: "4.Lead(Y:accel, G:a_lead, O:v_rel)",
values: [accelTarget, finiteNumber(radarLead?.aLeadK, 0), finiteNumber(radarLead?.vRel, 0)],
};
case 5:
return {
mode,
title: "5.Lead(Y:a_ego, G:a_lead, O:j_lead)",
values: [aEgo, finiteNumber(radarLead?.aLead, 0), finiteNumber(radarLead?.jLead, 0)],
};
case 6:
return {
mode,
title: "6.Steer(Y:actual, G:desire, O:output)",
values: [
finiteNumber(controlsState?.actualLateralAccel, 0) * 10.0,
finiteNumber(controlsState?.desiredLateralAccel, 0) * 10.0,
finiteNumber(controlsState?.lateralOutput, 0) * 10.0,
],
};
case 7:
return {
mode,
title: "7.SteerA(Y:Actual, G:Target, O:Offset*10)",
values: [
finiteNumber(carState?.steeringAngleDeg, 0),
finiteNumber(actuators?.steeringAngleDeg, 0),
finiteNumber(liveParameters?.angleOffsetDeg, 0) * 10.0,
],
};
case 8:
return {
mode,
title: "8.SteerA(Y:Actual, G:Target, O:Offset*10)",
values: [
finiteNumber(actuators?.curvature, 0) * 10000,
finiteNumber(actuators?.curvature, 0) * 10000,
finiteNumber(actuators?.curvature, 0) * 10000,
],
};
default:
return {
mode,
title: "no data",
values: [0, 0, 0],
};
}
}
/* ── Phase 1-4: ring buffer for plot history ── */
const _plotRing = [
new Float64Array(PLOT_MAX_POINTS),
new Float64Array(PLOT_MAX_POINTS),
new Float64Array(PLOT_MAX_POINTS),
];
let _plotRingHead = 0;
let _plotRingSize = 0;
function _plotRingPush(values) {
const writeIdx = (_plotRingHead + _plotRingSize) % PLOT_MAX_POINTS;
for (let i = 0; i < 3; i += 1) {
_plotRing[i][writeIdx] = finiteNumber(values[i], 0);
}
if (_plotRingSize < PLOT_MAX_POINTS) _plotRingSize++;
else _plotRingHead = (_plotRingHead + 1) % PLOT_MAX_POINTS;
}
function _plotRingGet(series, idx) {
return _plotRing[series][(_plotRingHead + idx) % PLOT_MAX_POINTS];
}
function _plotRingReset() {
_plotRingHead = 0;
_plotRingSize = 0;
}
function updatePlotHistory(plotData) {
if (!plotData) {
lastPlotMode = -1;
_plotRingReset();
return;
}
if (plotData.mode !== lastPlotMode) {
lastPlotMode = plotData.mode;
_plotRingReset();
}
_plotRingPush(plotData.values);
}
function getPlotBounds() {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (let seriesIndex = 0; seriesIndex < 3; seriesIndex += 1) {
for (let i = 0; i < _plotRingSize; i += 1) {
const value = _plotRingGet(seriesIndex, i);
min = Math.min(min, value);
max = Math.max(max, value);
}
}
if (!Number.isFinite(min) || !Number.isFinite(max)) {
min = -2;
max = 2;
}
if (min > -2) min = -2;
if (max < 2) max = 2;
if (Math.abs(max - min) < 1e-3) {
max += 1;
min -= 1;
}
return { min, max };
}
function drawPlot(stageWidth, stageHeight, viewportRect, plotData) {
if (!plotData) return;
if (stageHeight > stageWidth) return;
const viewportWidth = finiteNumber(viewportRect?.width, stageWidth);
const viewportHeight = finiteNumber(viewportRect?.height, stageHeight);
const plotScale = Math.min(
viewportWidth / BASE_CAMERA.width,
viewportHeight / BASE_CAMERA.height,
);
const plotX = finiteNumber(viewportRect?.left, 0) + 22.0 * plotScale;
const plotY = finiteNumber(viewportRect?.top, 0) + 40.0 * plotScale;
const plotWidth = 1000.0 * plotScale;
const plotHeight = 300.0 * plotScale;
const plotDx = 2.0 * plotScale; // scale with plot area to match C3 density
const size = Math.min(_plotRingSize, PLOT_MAX_POINTS);
if (size < 2) return;
const bounds = getPlotBounds();
const range = Math.max(bounds.max - bounds.min, 1);
const visibleSize = Math.min(size, Math.max(2, Math.floor(plotWidth / Math.max(plotDx, 0.001))));
const latestPlotX = plotX + plotWidth;
const latestLabelX = latestPlotX + 50.0 * plotScale;
const titleX = plotX + 8.0 * plotScale;
const titleY = plotY + plotHeight + 18.0 * plotScale;
hudCtx.save();
for (let seriesIndex = 0; seriesIndex < 3; seriesIndex += 1) {
hudCtx.beginPath();
let latestPoint = null;
for (let i = 0; i < visibleSize; i += 1) {
const currentValue = _plotRingGet(seriesIndex, size - 1 - i);
const x = latestPlotX - i * plotDx;
const y = plotY + plotHeight - ((currentValue - bounds.min) / range) * plotHeight;
if (i === 0) {
hudCtx.moveTo(x, y);
latestPoint = { x, y, value: currentValue };
} else {
hudCtx.lineTo(x, y);
}
}
hudCtx.lineWidth = Math.max(1.6, 3.0 * plotScale);
hudCtx.strokeStyle = PLOT_SERIES[seriesIndex].color;
hudCtx.stroke();
if (latestPoint) {
const labelY = latestPoint.y + (seriesIndex > 0 ? 40.0 * plotScale : 0);
drawCanvasOutlinedText(latestPoint.value.toFixed(2), latestLabelX, labelY, {
fontSize: clamp(Math.round(34.0 * plotScale), 15, 34),
fontWeight: 900,
strokeWidth: Math.max(1.3, 3.2 * plotScale),
fillStyle: PLOT_SERIES[seriesIndex].color,
align: "left",
canvasCtx: hudCtx,
});
}
}
drawCanvasOutlinedText(plotData.title, titleX, titleY, {
fontSize: clamp(Math.round(19.0 * plotScale), 12, 19),
fontWeight: 800,
strokeWidth: Math.max(1.4, 3.6 * plotScale),
align: "left",
canvasCtx: hudCtx,
});
hudCtx.restore();
}
function laneModeLabel(hudState) {
return isLaneMode(hudState) ? "LaneMode" : "Laneless";
}
function refreshParams(force = false) {
const runtimeParams = readLiveRuntimeParams();
if (runtimeParams) {
paramsState = runtimeParams;
return;
}
if (force) {
paramsState = normalizeVisualParams({}, paramsState);
}
}
function isActive() {
return document.body?.dataset?.page === "carrot";
}
function renderActiveFrame(options = {}) {
refreshParams();
const stageWidth = Math.max(1, stageEl.clientWidth);
const stageHeight = Math.max(1, stageEl.clientHeight);
const forceAll = Boolean(options.force || _forceNextRender);
const rawOverlayState = window.CarrotOverlayState || {};
const rawHudState = window.CarrotHudState || {};
const runtimeState = mergeRuntimeState(rawHudState, rawOverlayState);
let overlayState = runtimeState.overlayState;
const hudState = runtimeState.hudState;
const brokerServices = runtimeState.brokerServices;
if (!isCarrotVisionActive()) {
if (forceAll || _lastOverlaySig !== "vision-disabled" || _lastHudSig !== "vision-disabled") {
_lastOverlaySig = "vision-disabled";
_lastHudSig = "vision-disabled";
_lastPlotInputSig = "off";
cancelCameraFrameRecheck();
resetTemporalRibbonState();
hideOnroadAlert();
setStageLoading(false);
setStageReady(false);
clearOverlay(canvasEl.width || 1, canvasEl.height || 1);
clearHud(hudCanvasEl.width || 1, hudCanvasEl.height || 1);
setStatus(!isCarrotVisionAvailable()
? getCarrotVisionDisabledMessage()
: getUIText("start_vision_hint", "Tap the start button to enable drive vision."));
setMeta("");
setDebug("");
syncRtcPerfHud();
}
return;
}
const hasStream = syncSourceStream();
if (!hasStream || !isRoadCameraFrameRenderable(videoEl)) {
if (hasStream) {
setCarrotVisionRenderPhase("first-frame-waiting", { reason: "camera stream waiting first frame" });
}
_lastOverlaySig = "";
_lastHudSig = "";
resetTemporalRibbonState();
hideOnroadAlert();
setStageLoading(true, getCarrotVisionStatusText(getUIText("connecting", "Connecting...")), getCarrotVisionDetailText());
setStageReady(false);
clearOverlay(canvasEl.width || 1, canvasEl.height || 1);
clearHud(hudCanvasEl.width || 1, hudCanvasEl.height || 1);
setStatus(getCarrotVisionStatusText(getUIText("waiting_road_stream", "Waiting road camera stream...")));
setMeta("road:- model:- path:-");
setDebug("LD:- LT:- SR:-");
applyCarrotHudLayout({
left: 0,
top: 0,
right: stageWidth,
bottom: stageHeight,
width: stageWidth,
height: stageHeight,
});
syncRtcPerfHud();
scheduleCameraFrameRecheck();
return;
}
cancelCameraFrameRecheck();
const videoWidth = videoEl.videoWidth;
const videoHeight = videoEl.videoHeight;
syncCanvasSize(videoWidth, videoHeight, stageWidth, stageHeight);
renderOnroadAlert(stageWidth, stageHeight, hudState?.selfdriveState);
// Use raw radar state directly — no interpolation (matches C3/CarrotLink).
// C3 reads SubMaster every frame; CarrotLink reads snapshot directly.
// Position smoothing is handled in projectLeadBox() via EMA.
const model = overlayState.modelV2 || null;
const liveCalibration = overlayState.liveCalibration || null;
const roadCameraState = overlayState.roadCameraState || null;
const selectedPath = getSelectedPath(overlayState, hudState);
const pathStyle = getPathStyle(overlayState, hudState);
const plotData = buildPlotData(overlayState, hudState);
const showLaneInfo = finiteNumber(paramsState.ShowLaneInfo, defaultParams.ShowLaneInfo);
const debugText = firstNonEmptyText(
brokerServices?.carrotMan?.stockDebugTopRightText,
overlayState?.carrotMan?.stockDebugTopRightText,
formatDebugText(overlayState),
);
const overlaySig = overlayDataSignature(hudState, overlayState, selectedPath, pathStyle, showLaneInfo);
// C3 pushes plot data EVERY frame unconditionally (drawPlot.draw→updatePlotQueue).
// Web must do the same for identical density — no signature gating.
updatePlotHistory(plotData);
const nextPlotSig = plotInputSignature(plotData);
const plotChanged = forceAll || Boolean(options.hudDirty) || nextPlotSig !== _lastPlotInputSig;
if (plotChanged) {
_lastPlotInputSig = nextPlotSig;
}
const hudSig = hudDataSignature(hudState, overlayState, plotData, selectedPath, debugText);
if ((!overlayInfoState.lastSignature || !overlayInfoState.carLabel || !overlayInfoState.branchLabel) && !overlayInfoState.loading) {
refreshOverlayInfo().catch(() => {});
}
const overlayDirty = forceAll || Boolean(options.overlayDirty) || overlaySig !== _lastOverlaySig;
const hudDirty = forceAll || overlayDirty || Boolean(options.hudDirty) || plotChanged || hudSig !== _lastHudSig;
if (!overlayDirty && !hudDirty) return;
_lastOverlaySig = overlaySig;
_lastHudSig = hudSig;
_forceNextRender = false;
const calibration = getCalibrationMatrix(liveCalibration);
const transform = getStageTransform(videoWidth, videoHeight, stageWidth, stageHeight, calibration);
const viewportRect = getHudViewportRect(videoWidth, videoHeight, stageWidth, stageHeight, transform);
const laneCount = Array.isArray(model?.laneLines) ? model.laneLines.length : 0;
const edgeCount = Array.isArray(model?.roadEdges) ? model.roadEdges.length : 0;
const leadCount = Array.isArray(model?.leadsV3) ? model.leadsV3.length : 0;
const rpyText = formatRpyTriplet(liveCalibration);
const modeLabel = getDisplayModeLabel(transform.displayMode || DISPLAY_MODES[1]);
const laneLabel = laneModeLabel(hudState);
applyStageTransform(transform);
setStageReady(true);
setCarrotVisionRenderPhase("ready", { reason: "camera frame renderable" });
applyCarrotHudLayout(viewportRect);
// The <video> may hold a renderable-but-stale last frame during a reconnect.
// vision_rtc only promotes to controlState "live" when the connection is
// genuinely up, so key the loading panel on that: live -> hide it; otherwise
// keep a "reconnecting/connecting" message over the frozen frame instead of
// a silent blank.
const carrotLive = (getCarrotVisionState().controlState || "") === "live";
setStageLoading(!carrotLive, getCarrotVisionStatusText(getUIText("connecting", "Connecting...")), getCarrotVisionDetailText());
if (overlayDirty) {
resetFrameProjectionCache();
clearOverlay(videoWidth, videoHeight);
if (model) {
if (showLaneInfo >= 1) drawLaneLines(model, overlayState, hudState, transform.calibTransform);
if (showLaneInfo > 1) drawRoadEdges(model, overlayState, transform.calibTransform);
if (showLaneInfo >= 0) drawPath(selectedPath.pathData, model, overlayState, transform.calibTransform, videoHeight, pathStyle);
drawBlindspotBarriers(model?.position, overlayState, hudState, transform.calibTransform);
drawRadarLeadBoxes(model, overlayState, hudState, transform.calibTransform, videoWidth, videoHeight, stageWidth, stageHeight, transform);
drawProjectedTfMarker(model?.position, hudState?.longitudinalPlan, transform.calibTransform, videoWidth, videoHeight);
}
}
if (hudDirty) {
clearHud(stageWidth, stageHeight);
drawStageEdgeFades(stageWidth, stageHeight);
drawPlot(stageWidth, stageHeight, viewportRect, plotData);
setDebug(debugText);
drawHudTopLeftText(stageWidth, stageHeight, viewportRect, overlayInfoState.carLabel, pathStyle.mode);
drawHudTopRightText(stageWidth, stageHeight, viewportRect, lastDebug, pathStyle.mode);
syncRtcPerfHud();
drawHudBottomLeftText(stageWidth, stageHeight, viewportRect, overlayInfoState.branchLabel, pathStyle.mode);
drawHudBottomText(stageWidth, stageHeight, viewportRect, selectedPath.latDebugText, hudState, pathStyle.mode);
}
if (!model) {
setStatus(`road ${videoWidth}x${videoHeight} · ${getUIText("waiting_model", "waiting modelV2...")} · ${laneLabel}`);
} else {
setStatus(`road ${videoWidth}x${videoHeight} · model ${model.frameId ?? "-"} · ${laneLabel} · ${modeLabel}`);
}
setMeta(
`road:${roadCameraState?.frameId ?? "-"} model:${model?.frameId ?? "-"} path:${selectedPath.pathSource}/${pathStyle.mode}:${pathStyle.colorIndex} width:${finiteNumber(paramsState.ShowPathWidth, 100)} laneInfo:${showLaneInfo} lane:${laneCount} edge:${edgeCount} lead:${leadCount} plot:${finiteNumber(paramsState.ShowPlotMode, 0)} rpy:${rpyText} h:${finiteNumber(liveCalibration?.height?.[0], 0).toFixed(2)}`,
);
if (!hudDirty) setDebug(debugText);
}
function cancelScheduledRender() {
cancelCameraFrameRecheck();
if (_renderRafId != null) {
window.cancelAnimationFrame(_renderRafId);
_renderRafId = null;
}
if (_renderTimerId != null) {
window.clearTimeout(_renderTimerId);
_renderTimerId = null;
}
if (_renderVideoFrameId != null) {
try {
if (typeof videoEl.cancelVideoFrameCallback === "function") {
videoEl.cancelVideoFrameCallback(_renderVideoFrameId);
}
} catch {}
_renderVideoFrameId = null;
}
}
function mergePendingRenderState(options = {}) {
const force = Boolean(options.force);
const overlayDirty = force || options.overlayDirty === true;
const hudDirty = force || options.hudDirty === true;
_pendingRenderState.force = _pendingRenderState.force || force;
_pendingRenderState.overlayDirty = _pendingRenderState.overlayDirty || overlayDirty;
_pendingRenderState.hudDirty = _pendingRenderState.hudDirty || hudDirty;
}
function clearPendingRenderState() {
_pendingRenderState.force = false;
_pendingRenderState.overlayDirty = false;
_pendingRenderState.hudDirty = false;
}
function isStageVisible() {
return isActive() && !document.hidden;
}
function flushScheduledRender() {
_renderRafId = null;
_renderTimerId = null;
_renderVideoFrameId = null;
if (!isStageVisible()) {
if (!isActive()) resetCarrotHudLayout();
return;
}
const pending = { ..._pendingRenderState };
clearPendingRenderState();
_lastRenderTime = performance.now();
renderActiveFrame(pending);
if (_pendingRenderState.force || _pendingRenderState.overlayDirty || _pendingRenderState.hudDirty) {
scheduleRender();
}
}
function canUseVideoFrameScheduling() {
return (
_pendingRenderState.overlayDirty &&
!_pendingRenderState.force &&
typeof videoEl.requestVideoFrameCallback === "function" &&
isCarrotVisionActive() &&
!videoEl.paused &&
videoEl.readyState >= 2
);
}
function scheduleRender(options = {}) {
mergePendingRenderState(options);
if (!isStageVisible()) {
cancelScheduledRender();
if (!isActive()) resetCarrotHudLayout();
return;
}
if (_renderRafId != null || _renderTimerId != null || _renderVideoFrameId != null) return;
const elapsed = performance.now() - _lastRenderTime;
const waitMs = Math.max(0, RENDER_INTERVAL_MS - elapsed);
if (waitMs > 0) {
_renderTimerId = window.setTimeout(() => {
_renderTimerId = null;
scheduleRender();
}, waitMs);
return;
}
if (canUseVideoFrameScheduling()) {
_renderVideoFrameId = videoEl.requestVideoFrameCallback(() => {
_renderVideoFrameId = null;
flushScheduledRender();
});
return;
}
_renderRafId = window.requestAnimationFrame(flushScheduledRender);
}
function requestRender(options = {}) {
const hasOverlayDirty = Object.prototype.hasOwnProperty.call(options, "overlayDirty");
const hasHudDirty = Object.prototype.hasOwnProperty.call(options, "hudDirty");
const normalized = {
force: Boolean(options.force),
overlayDirty: Boolean(options.force || (hasOverlayDirty ? options.overlayDirty : true)),
hudDirty: Boolean(options.force || (hasHudDirty ? options.hudDirty : true)),
};
scheduleRender(normalized);
}
function refresh() {
transformSignature = "";
overlaySizeSignature = "";
hudSizeSignature = "";
_lastOverlaySig = "";
_lastHudSig = "";
_lastPlotInputSig = "";
_forceNextRender = true;
_gradientCache.clear();
_hudGradientCache.clear();
_rgbaCache.clear();
resetTemporalRibbonState();
_mergeRuntimeCache.refs = null;
_mergeRuntimeCache.result = null;
}
if (displayModeButton) {
displayModeButton.addEventListener("click", () => {
const nextIndex = (displayModeIndex + 1) % DISPLAY_MODES.length;
setDisplayModeIndex(nextIndex);
requestRender({ force: true, overlayDirty: true, hudDirty: true });
});
}
let _rtcPerfHoldTimer = null;
let _rtcPerfHoldPointerId = null;
let _rtcPerfHoldStartX = 0;
let _rtcPerfHoldStartY = 0;
function clearRtcPerfHold() {
if (_rtcPerfHoldTimer != null) {
window.clearTimeout(_rtcPerfHoldTimer);
_rtcPerfHoldTimer = null;
}
_rtcPerfHoldPointerId = null;
if (rtcPerfHoldTargetEl) rtcPerfHoldTargetEl.classList.remove("is-holding");
}
function bindRtcPerfHudInteractions() {
if (!rtcPerfHoldTargetEl || !rtcPerfSummaryEl) return;
rtcPerfHoldTargetEl.addEventListener("pointerdown", (event) => {
event.stopPropagation();
clearRtcPerfHold();
if (!isActive() || !isRtcPerfSummaryAvailable()) return;
_rtcPerfHoldPointerId = event.pointerId;
_rtcPerfHoldStartX = event.clientX;
_rtcPerfHoldStartY = event.clientY;
rtcPerfHoldTargetEl.classList.add("is-holding");
rtcPerfHoldTargetEl.setPointerCapture?.(event.pointerId);
_rtcPerfHoldTimer = window.setTimeout(() => {
_rtcPerfHoldTimer = null;
rtcPerfHoldTargetEl.classList.remove("is-holding");
setRtcPerfSummaryOpen(!_rtcPerfSummaryOpen);
}, RTC_PERF_HOLD_MS);
});
rtcPerfHoldTargetEl.addEventListener("pointermove", (event) => {
if (_rtcPerfHoldPointerId !== event.pointerId) return;
if (Math.hypot(event.clientX - _rtcPerfHoldStartX, event.clientY - _rtcPerfHoldStartY) > RTC_PERF_HOLD_MOVE_PX) {
clearRtcPerfHold();
}
});
["pointerup", "pointercancel", "lostpointercapture"].forEach((eventName) => {
rtcPerfHoldTargetEl.addEventListener(eventName, clearRtcPerfHold);
});
rtcPerfHoldTargetEl.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
});
rtcPerfCloseBtnEl?.addEventListener("click", (event) => {
event.stopPropagation();
setRtcPerfSummaryOpen(false);
});
rtcPerfLogBtnEl?.addEventListener("click", (event) => {
event.stopPropagation();
const previousText = rtcPerfLogBtnEl.textContent;
rtcPerfLogBtnEl.disabled = true;
rtcPerfLogBtnEl.textContent = "SEND";
(async () => {
try {
if (!window.CarrotVisionDiag?.uploadDiscord) throw new Error("diagnostic upload unavailable");
await window.CarrotVisionDiag.uploadDiscord();
rtcPerfLogBtnEl.textContent = "SENT";
if (typeof showAppToast === "function") showAppToast("Carrot Vision log sent to Discord", { duration: 2600 });
} catch (error) {
rtcPerfLogBtnEl.textContent = "FAIL";
if (typeof showAppToast === "function") showAppToast(`Discord upload failed: ${error?.message || error}`, { tone: "error", duration: 4200 });
} finally {
window.setTimeout(() => {
rtcPerfLogBtnEl.disabled = false;
rtcPerfLogBtnEl.textContent = previousText;
}, 1400);
}
})();
});
document.addEventListener("pointerdown", (event) => {
if (!_rtcPerfSummaryOpen) return;
if (rtcPerfSummaryEl.contains(event.target) || rtcPerfHoldTargetEl.contains(event.target)) return;
setRtcPerfSummaryOpen(false);
}, true);
}
function shouldIgnoreStageFullscreenToggle(target) {
if (!(target instanceof Element)) return false;
if (target.closest("button, a, input, textarea, select, label")) return true;
if (target.closest(".carrot-stage__controls")) return true;
if (target.closest(".vision-start-overlay")) return true;
return false;
}
async function handleStageFullscreenToggle(event) {
if (!isCarrotVisionActive()) return;
if (shouldIgnoreStageFullscreenToggle(event?.target)) return;
if (typeof window.ToggleCarrotFullscreen !== "function") return;
await window.ToggleCarrotFullscreen({ quiet: false }).catch(() => {});
}
function requestFullRender() {
if (!isRtcPerfSummaryAvailable()) setRtcPerfSummaryOpen(false);
refresh();
requestRender({ force: true, overlayDirty: true, hudDirty: true });
}
function handleLifecycleChange() {
if (!isActive()) setRtcPerfSummaryOpen(false);
if (isStageVisible()) {
if (isCarrotVisionActive()) {
const live = (getCarrotVisionState().controlState || "") === "live";
setStageLoading(!live, getCarrotVisionStatusText(getUIText("connecting", "Connecting...")), getCarrotVisionDetailText());
}
refreshOverlayInfo().catch(() => {});
requestFullRender();
return;
}
cancelScheduledRender();
setStageLoading(false);
if (!isActive()) resetCarrotHudLayout();
}
stageEl.addEventListener("click", handleStageFullscreenToggle);
window.addEventListener("resize", requestFullRender);
window.addEventListener("orientationchange", requestFullRender);
document.addEventListener("fullscreenchange", requestFullRender);
document.addEventListener("webkitfullscreenchange", requestFullRender);
window.addEventListener("carrot:render-request", (event) => requestRender(event.detail || {}));
window.addEventListener("carrot:pagechange", handleLifecycleChange);
window.addEventListener("carrot:visionchange", handleLifecycleChange);
window.addEventListener("carrot:visionstatechange", handleLifecycleChange);
document.addEventListener("visibilitychange", handleLifecycleChange);
if (typeof ResizeObserver === "function") {
const stageResizeObserver = new ResizeObserver(requestFullRender);
stageResizeObserver.observe(stageEl);
}
if (window.visualViewport) {
window.visualViewport.addEventListener("resize", requestFullRender, { passive: true });
window.visualViewport.addEventListener("scroll", requestFullRender, { passive: true });
}
const renderVideoTargets = sourceVideoEl === videoEl ? [videoEl] : [sourceVideoEl, videoEl];
["loadedmetadata", "loadeddata", "playing", "resize", "emptied"].forEach((eventName) => {
renderVideoTargets.forEach((target) => target.addEventListener(eventName, requestFullRender));
});
try {
const stored = Number(localStorage.getItem(DISPLAY_MODE_STORAGE_KEY));
if (Number.isFinite(stored)) {
displayModeIndex = clamp(stored, 0, DISPLAY_MODES.length - 1);
}
} catch {}
syncDisplayModeButtons();
bindRtcPerfHudInteractions();
syncRtcPerfHud();
refreshParams(true);
refreshOverlayInfo(true).catch(() => {});
requestRender({ force: true, overlayDirty: true, hudDirty: true });
return {
refresh,
requestRender,
renderText: syncDisplayModeButtons,
setDisplayModeIndex,
};
})();