import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Canvas, useFrame, useThree } from '@react-three/fiber'; import { MapControls, OrthographicCamera, useGLTF } from '@react-three/drei'; import * as THREE from 'three'; // ---------------------------------------------------------------------------- // 1. Terrain Scene // ---------------------------------------------------------------------------- function TerrainScene() { const { scene } = useGLTF('/models/terrain/terrain.glb'); return ( ); } // ---------------------------------------------------------------------------- // 2. Waypoint Overlay (Debug visualization) // ---------------------------------------------------------------------------- function WaypointOverlay({ waypoints, visible }: { waypoints: any[], visible: boolean }) { if (!visible) return null; return ( {waypoints.map((w) => ( ))} ); } // ---------------------------------------------------------------------------- // 3. Camera Manager (Handles Orthographic Math & Downloads) // ---------------------------------------------------------------------------- function CameraManager({ autoBounds, boundsTextRef }: { autoBounds: any, boundsTextRef: React.RefObject }) { const { camera, gl, scene } = useThree(); const controlsRef = useRef(null); // Apply Auto-Bounds function useEffect(() => { const applyAutoBounds = () => { if (camera instanceof THREE.OrthographicCamera && autoBounds) { const width = autoBounds.maxX - autoBounds.minX; const height = autoBounds.maxZ - autoBounds.minZ; const centerX = (autoBounds.minX + autoBounds.maxX) / 2; const centerZ = (autoBounds.minZ + autoBounds.maxZ) / 2; camera.position.set(centerX, 200, centerZ); camera.left = -width / 2; camera.right = width / 2; camera.top = height / 2; camera.bottom = -height / 2; camera.zoom = 1; camera.updateProjectionMatrix(); if (controlsRef.current) { controlsRef.current.target.set(centerX, 0, centerZ); controlsRef.current.update(); } } }; (window as any).applyAutoBounds = applyAutoBounds; // Initial apply applyAutoBounds(); return () => { delete (window as any).applyAutoBounds; }; }, [camera, autoBounds]); // Track dynamic bounds without triggering React re-renders! useFrame(() => { if (camera instanceof THREE.OrthographicCamera && boundsTextRef.current) { const width = (camera.right - camera.left) / camera.zoom; const height = (camera.top - camera.bottom) / camera.zoom; const minX = Math.round(camera.position.x - width / 2); const maxX = Math.round(camera.position.x + width / 2); const minZ = Math.round(camera.position.z - height / 2); const maxZ = Math.round(camera.position.z + height / 2); // Direct DOM mutation for 60fps performance (prevents WebGL Context Lost!) boundsTextRef.current.innerText = JSON.stringify({ minX, maxX, minZ, maxZ }, null, 2); } }); // Attach screenshot capture logic useEffect(() => { (window as any).downloadMapScreenshot = () => { // Force an immediate render frame to ensure no UI overlays are missing gl.render(scene, camera); const dataUrl = gl.domElement.toDataURL("image/png"); const a = document.createElement("a"); a.href = dataUrl; a.download = "map_background.png"; a.click(); }; return () => { delete (window as any).downloadMapScreenshot; }; }, [gl, camera, scene]); return ; } // ---------------------------------------------------------------------------- // 4. Main Page Route Component // ---------------------------------------------------------------------------- export function BackgroundMapPage() { const [waypoints, setWaypoints] = useState([]); const [showWaypoints, setShowWaypoints] = useState(true); const boundsTextRef = useRef(null); // Load road network waypoints to compute perfect GPS bounds useEffect(() => { const saved = localStorage.getItem('la-fabrik-waypoints'); if (saved) { setWaypoints(JSON.parse(saved)); } else { fetch('/roadNetwork.json') .then(res => res.json()) .then(data => setWaypoints(data)) .catch(() => { }); } }, []); // Compute exact bounds that the EbikeGPSMap will use by default const autoBounds = useMemo(() => { if (waypoints.length === 0) return null; const xs = waypoints.map(w => w.x); const zs = waypoints.map(w => w.z); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minZ = Math.min(...zs); const maxZ = Math.max(...zs); // CRITICAL: We MUST force the camera bounds to be a PERFECT SQUARE. // If the camera is rectangular, the exported PNG will be distorted when drawn // on the EbikeGPSMap's 1024x1024 canvas! const width = maxX - minX; const height = maxZ - minZ; const maxDim = Math.max(width, height); const centerX = (minX + maxX) / 2; const centerZ = (minZ + maxZ) / 2; const paddedDim = maxDim * 1.15 || 100; return { minX: centerX - paddedDim / 2, maxX: centerX + paddedDim / 2, minZ: centerZ - paddedDim / 2, maxZ: centerZ + paddedDim / 2, }; }, [waypoints]); return (
{/* CRITICAL: The DOM element MUST be a perfect square so the resulting PNG is exactly 1:1, preventing stretching in the EbikeGPSMap canvas texture! */}
{/* Premium Glassmorphic UI Dashboard */}

GPS Map Generator

1. Cadrez votre carte (ou utilisez le Cadrage Automatique).
2. Masquez les waypoints (fond visuel seul).
3. Cliquez sur Capturer la carte.

Limites Actuelles (worldBounds):
            Calcul...
          
*Si vous décadrez à la souris, vous devrez copier ces valeurs exactes dans la prop worldBounds de votre composant EbikeGPSMap !

Astuce : Utilisez le Cadrage Automatique pour ne rien avoir à configurer.
); }