(
point: { x: number; y: number },
robotPose: { x: number; y: number; theta: number },
canvasWidth: number,
canvasHeight: number,
config: CameraConfig
)
| 190 | * the ground/road perspective - closer points at bottom, farther at middle. |
| 191 | */ |
| 192 | export function projectPathPoint( |
| 193 | point: { x: number; y: number }, |
| 194 | robotPose: { x: number; y: number; theta: number }, |
| 195 | canvasWidth: number, |
| 196 | canvasHeight: number, |
| 197 | config: CameraConfig |
| 198 | ): ProjectedPathPoint { |
| 199 | // Calculate vector from robot to point |
| 200 | const dx = point.x - robotPose.x; |
| 201 | const dy = point.y - robotPose.y; |
| 202 | const distance = Math.sqrt(dx * dx + dy * dy); |
| 203 | |
| 204 | // Get bearing angle from robot to point in map frame |
| 205 | const pointBearing = Math.atan2(dy, dx); |
| 206 | |
| 207 | // Calculate relative bearing based on camera facing direction |
| 208 | let robotHeading = robotPose.theta; |
| 209 | if (!config.isFrontFacing) { |
| 210 | // For back camera, flip the robot heading by 180 degrees |
| 211 | robotHeading = normalizeAngle(robotHeading + Math.PI); |
| 212 | } |
| 213 | |
| 214 | const relativeBearing = normalizeAngle(pointBearing - robotHeading); |
| 215 | |
| 216 | // Determine if point is in front (within +/- 90 degrees of camera direction) |
| 217 | const isInFront = Math.abs(relativeBearing) < Math.PI / 2; |
| 218 | |
| 219 | // Determine if point is within camera FOV |
| 220 | const halfFov = config.horizontalFov / 2; |
| 221 | const isInView = Math.abs(relativeBearing) <= halfFov; |
| 222 | |
| 223 | // Calculate screen X position |
| 224 | const centerX = canvasWidth / 2; |
| 225 | let screenX: number; |
| 226 | |
| 227 | if (isInView) { |
| 228 | // Linear mapping within FOV (negate bearing for correct screen mapping) |
| 229 | screenX = centerX - (relativeBearing / halfFov) * (canvasWidth / 2); |
| 230 | } else { |
| 231 | // Clamp to edge for off-screen points |
| 232 | screenX = relativeBearing > 0 ? 0 : canvasWidth; |
| 233 | } |
| 234 | |
| 235 | // Calculate screen Y position based on distance |
| 236 | // Path should appear on the ground level - closer points near bottom, farther toward horizon |
| 237 | // Using a more limited range focused on the lower-middle portion of the screen |
| 238 | const minDistance = 0.5; |
| 239 | const maxDistance = 8; // Reduced from 20m - only show nearby path |
| 240 | const clampedDistance = Math.max(minDistance, Math.min(maxDistance, distance)); |
| 241 | |
| 242 | // Use exponential mapping for more natural perspective (closer points spread out more) |
| 243 | const normalizedDistance = (clampedDistance - minDistance) / (maxDistance - minDistance); |
| 244 | const perspectiveDistance = Math.pow(normalizedDistance, 0.7); // Exponential for better depth |
| 245 | |
| 246 | // Position path in lower-center area: close = 90% height, far = 55% height (horizon line) |
| 247 | const minY = canvasHeight * 0.55; // Horizon line (farther points) |
| 248 | const maxY = canvasHeight * 0.92; // Near bottom (closer points) |
| 249 | const screenY = maxY - perspectiveDistance * (maxY - minY); |
nothing calls this directly
no test coverage detected