// EtapesScreen v3 — oval circuit layout const { useState, useEffect, useMemo, useRef } = React; // ── Panneau "Toutes les entreprises" (partagé avec StageDetail) ────────────── function AllCompaniesPanel({ entreprises, onClose, onPick }) { const [enter, setEnter] = useState(false); useEffect(() => { requestAnimationFrame(() => setEnter(true)); }, []); const unique = useMemo(() => { const seen = new Set(); return entreprises .filter(e => { if (seen.has(e.nom)) return false; seen.add(e.nom); return true; }) .sort((a, b) => a.nom.localeCompare(b.nom, 'fr')); }, [entreprises]); return ReactDOM.createPortal(
e.stopPropagation()} style={{ transform: enter ? 'translateX(0)' : 'translateX(60px)', opacity: enter ? 1 : 0, transition: 'transform 360ms cubic-bezier(0.22,1,0.36,1), opacity 360ms cubic-bezier(0.22,1,0.36,1)', }} >
Répertoire

Toutes les entreprises

{unique.length} entreprise{unique.length > 1 ? 's' : ''}
{unique.map((ent, i) => ( ))}
, document.body ); } window.AllCompaniesPanel = AllCompaniesPanel; const RIVER_ICON_BASE = 'assets/river_icons_couleurs/'; // Oval parameters (SVG viewBox 1000×600) // start=70 : les deux extrémités (i=0 et i=8) sont symétriques autour du sommet (α=90°), // ce qui centre le vide en haut de la page. // cy=280, ry=200 : conserve le bas à y=80% tout en gagnant de l'espace vers le haut. const OV = { cx: 500, cy: 280, rx: 370, ry: 200, n: 9, step: 40, start: 70 }; // Bezier approximation factor for a circular arc segment of `step` degrees const BKAPPA = (4 / 3) * Math.tan((OV.step / 2) * Math.PI / 180); // ≈ 0.4853 function ovalPoint(i) { const a = (OV.start - i * OV.step) * Math.PI / 180; // sens horaire depuis l'angle de départ return { x: OV.cx + OV.rx * Math.cos(a), y: OV.cy - OV.ry * Math.sin(a), a }; } // Arrondi à la dizaine inférieure : 169 → 160, 170 → 170 function floorTen(n) { return Math.floor(n / 10) * 10; } function CircuitLayout({ numbered, allEntreprises, totalEntreprises, onPickEtape, onPickTransversal, onPickEntreprise }) { const [revealedCount, setRevealedCount] = useState(0); const [showAllPanel, setShowAllPanel] = useState(false); // 9 nœuds sur un ovale ouvert (arc 320°), Bauxite centré en haut // Raffinage (i=1) et Recyclage (i=7) avancés de 8° vers le sommet le long de la courbe const angleAdjust = { 1: 8, 7: -8 }; const positions = useMemo(() => { const pts = []; for (let i = 0; i < OV.n; i++) { const a = (OV.start - i * OV.step + (angleAdjust[i] || 0)) * Math.PI / 180; const x = OV.cx + OV.rx * Math.cos(a); const y = OV.cy - OV.ry * Math.sin(a); pts[i] = { x: x / 1000, y: y / 600 }; } return pts; }, []); // Nœuds à afficher const displayNodes = useMemo(() => { function mergeCount(indices) { const ents = new Set(); indices.forEach(i => numbered[i].categories.forEach(c => c.services.forEach(s => s.entreprises.forEach(e => ents.add(e.nom))))); return ents.size; } const clean = nom => nom.replace(/^\d+[a-z]?\.\s*/i, ''); return [ { name: clean(numbered[0].nom), etapeIndex: 0, icon: 'icon_bauxite.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[0]).unique }, { name: clean(numbered[1].nom), etapeIndex: 1, icon: 'icon_raffinage.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[1]).unique }, { name: clean(numbered[2].nom), etapeIndex: 2, icon: 'icon_fusion.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[2]).unique }, { name: clean(numbered[3].nom), etapeIndex: 3, icon: 'icon_moulage_alliage.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[3]).unique }, { name: 'Transformation intermédiaire', etapeIndex: 5, icon: 'icon_transformation_int.svg', count: mergeCount([4, 5]) }, { name: 'Fabrication (pièces)', etapeIndex: 7, icon: 'icon_fabrication.svg', count: mergeCount([6, 7]) }, { name: clean(numbered[8].nom), etapeIndex: 8, icon: 'icon_distribution.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[8]).unique }, { name: clean(numbered[9].nom), etapeIndex: 9, icon: 'icon_recyclage.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[9]).unique }, { name: clean(numbered[10].nom), etapeIndex: 10, icon: 'icon_soutien_ecosystem.svg', count: window.SVAUtils.countEntreprisesInEtape(numbered[10]).unique }, ]; }, [numbered]); // Arc elliptique natif SVG — vraiment round, sans aucune jonction bezier const pathD = useMemo(() => { const p0 = ovalPoint(0); const p8 = ovalPoint(OV.n - 1); // A rx ry x-rotation large-arc-flag(1=arc>180°) sweep-flag(1=sens horaire SVG) x y return `M ${p0.x.toFixed(2)} ${p0.y.toFixed(2)} A ${OV.rx} ${OV.ry} 0 1 1 ${p8.x.toFixed(2)} ${p8.y.toFixed(2)}`; }, []); useEffect(() => { let cancelled = false; const ids = []; for (let i = 0; i <= displayNodes.length + 1; i++) { ids.push(setTimeout(() => { if (!cancelled) setRevealedCount(i); }, 200 + i * 110)); } return () => { cancelled = true; ids.forEach(clearTimeout); }; }, []); const pathRef = useRef(null); const [pathLen, setPathLen] = useState(0); useEffect(() => { if (pathRef.current) setPathLen(pathRef.current.getTotalLength()); }, [pathD]); const header = (
Vallée de l'aluminium

Le circuit de l'aluminium

{floorTen(totalEntreprises)}+
entreprises
référencées
); return (
{/* ── Desktop ── */} {header}
{displayNodes.map((node, i) => { const pos = positions[i]; if (!pos) return null; const visible = i < revealedCount; return ( ); })}
Région du Saguenay–Lac-St-Jean
{/* ── Mobile list ── */}
{displayNodes.map((node, i) => ( ))}
{showAllPanel && ( setShowAllPanel(false)} onPick={(ent) => { setShowAllPanel(false); onPickEntreprise(ent); }} /> )}
); } function CircuitScreen({ data, onPickEtape, onPickTransversal, onPickEntreprise }) { const numbered = data.etapes.slice(0, 11); const { allEntreprises, totalEntreprises } = React.useMemo(() => { const seen = new Set(); const all = []; data.etapes.forEach(e => e.categories.forEach(c => c.services.forEach(s => s.entreprises.forEach(ent => { if (!seen.has(ent.nom)) { seen.add(ent.nom); all.push(ent); } })))); return { allEntreprises: all, totalEntreprises: seen.size }; }, [data]); return ( ); } window.CircuitScreen = CircuitScreen;