refactor(vwb): refonte complète capture écran — stable définitivement
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 13s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 15s
tests / Tests sécurité (critique) (push) Has been skipped
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 13s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 13s
tests / Tests unitaires (sans GPU) (push) Failing after 15s
tests / Tests sécurité (critique) (push) Has been skipped
FullscreenSelector réécrit : - Overlay unique positionné via getBoundingClientRect() - Recalcul auto au resize - Coordonnées souris relatives à l'image - Plus de décalage bboxes/sélection Capture backend : - mss.monitors[0] (écran composite) au lieu de pyautogui.screenshot() - Capture la VM en plein écran correctement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,10 +52,24 @@ def capture_screen():
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import pyautogui
|
# Utiliser mss pour capturer TOUS les moniteurs (ecran compose).
|
||||||
|
# pyautogui.screenshot() capture uniquement le premier moniteur,
|
||||||
|
# ce qui rate la VM en plein ecran sur un second ecran ou via QEMU/spice.
|
||||||
|
# mss.monitors[0] = ecran compose (tous les moniteurs), ce qui capture
|
||||||
|
# exactement ce que l'utilisateur voit quel que soit le setup.
|
||||||
|
try:
|
||||||
|
import mss
|
||||||
|
with mss.mss() as sct:
|
||||||
|
# monitors[0] = ecran virtuel englobant tous les moniteurs
|
||||||
|
monitor = sct.monitors[0]
|
||||||
|
sct_img = sct.grab(monitor)
|
||||||
|
# Convertir mss ScreenShot (BGRA) en PIL Image RGB
|
||||||
|
screenshot = Image.frombytes('RGB', sct_img.size, sct_img.rgb)
|
||||||
|
except ImportError:
|
||||||
|
# Fallback pyautogui si mss n'est pas installe
|
||||||
|
import pyautogui
|
||||||
|
screenshot = pyautogui.screenshot()
|
||||||
|
|
||||||
# Capture écran
|
|
||||||
screenshot = pyautogui.screenshot()
|
|
||||||
width, height = screenshot.size
|
width, height = screenshot.size
|
||||||
|
|
||||||
# Convertir en base64
|
# Convertir en base64
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import type { Capture, ExecutionMode } from '../types';
|
import type { Capture, ExecutionMode } from '../types';
|
||||||
import type { UIElement } from '../services/uiDetection';
|
import type { UIElement } from '../services/uiDetection';
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
compressThumbnail,
|
compressThumbnail,
|
||||||
} from '../services/captureLibraryStorage';
|
} from '../services/captureLibraryStorage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detecte PNG vs JPEG depuis le contenu base64 et retourne un data: URL.
|
||||||
|
* GARDER CETTE FONCTION : elle evite les bugs d'affichage PNG/JPEG.
|
||||||
|
*/
|
||||||
function b64ImgSrc(base64: string): string {
|
function b64ImgSrc(base64: string): string {
|
||||||
if (base64.startsWith('data:')) return base64;
|
if (base64.startsWith('data:')) return base64;
|
||||||
const mime = base64.startsWith('/9j/') ? 'image/jpeg' : 'image/png';
|
const mime = base64.startsWith('/9j/') ? 'image/jpeg' : 'image/png';
|
||||||
@@ -53,7 +57,7 @@ export default function CapturePanel({
|
|||||||
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
const [currentCapture, setCurrentCapture] = useState<Capture | null>(null);
|
||||||
const [timerSeconds, setTimerSeconds] = useState(0);
|
const [timerSeconds, setTimerSeconds] = useState(0);
|
||||||
const [countdown, setCountdown] = useState<number | null>(null);
|
const [countdown, setCountdown] = useState<number | null>(null);
|
||||||
// Éléments détectés sur l'aperçu miniature
|
// Elements detectes sur l'apercu miniature
|
||||||
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
|
const [previewElements, setPreviewElements] = useState<UIElement[]>([]);
|
||||||
const [isDetectingPreview, setIsDetectingPreview] = useState(false);
|
const [isDetectingPreview, setIsDetectingPreview] = useState(false);
|
||||||
const previewImgRef = useRef<HTMLImageElement>(null);
|
const previewImgRef = useRef<HTMLImageElement>(null);
|
||||||
@@ -61,8 +65,7 @@ export default function CapturePanel({
|
|||||||
|
|
||||||
const isDebugMode = executionMode === 'debug';
|
const isDebugMode = executionMode === 'debug';
|
||||||
|
|
||||||
// Charger la bibliothèque depuis le backend (prioritaire), fallback localStorage.
|
// Charger la bibliotheque depuis le backend (prioritaire), fallback localStorage.
|
||||||
// Le helper loadLibraryAsync() migre aussi les données de l'ancienne clé 'captureLibrary'.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLibraryAsync().then((loaded) => {
|
loadLibraryAsync().then((loaded) => {
|
||||||
setLibrary(
|
setLibrary(
|
||||||
@@ -77,12 +80,12 @@ export default function CapturePanel({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Sauvegarder la bibliothèque (localStorage + gestion de quota)
|
// Sauvegarder la bibliotheque (localStorage + backend)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveLibrary(library);
|
saveLibrary(library);
|
||||||
}, [library]);
|
}, [library]);
|
||||||
|
|
||||||
// Ajouter capture à la bibliothèque (thumbnail compressé JPEG 320x240)
|
// Ajouter capture a la bibliotheque (thumbnail compresse JPEG 320x240)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!capture) return;
|
if (!capture) return;
|
||||||
setCurrentCapture(capture);
|
setCurrentCapture(capture);
|
||||||
@@ -101,7 +104,7 @@ export default function CapturePanel({
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [capture]);
|
}, [capture]);
|
||||||
|
|
||||||
// Détecter les éléments UI quand une capture arrive
|
// Detecter les elements UI quand une capture arrive
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentCapture) {
|
if (!currentCapture) {
|
||||||
setPreviewElements([]);
|
setPreviewElements([]);
|
||||||
@@ -118,7 +121,7 @@ export default function CapturePanel({
|
|||||||
);
|
);
|
||||||
setPreviewElements(result.elements);
|
setPreviewElements(result.elements);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Détection aperçu échouée:', err);
|
console.error('Detection apercu echouee:', err);
|
||||||
setPreviewElements([]);
|
setPreviewElements([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDetectingPreview(false);
|
setIsDetectingPreview(false);
|
||||||
@@ -128,7 +131,7 @@ export default function CapturePanel({
|
|||||||
runDetection();
|
runDetection();
|
||||||
}, [currentCapture]);
|
}, [currentCapture]);
|
||||||
|
|
||||||
// Calculer le scale de l'aperçu quand l'image est chargée
|
// Calculer le scale de l'apercu quand l'image est chargee
|
||||||
const handlePreviewImageLoad = () => {
|
const handlePreviewImageLoad = () => {
|
||||||
if (previewImgRef.current) {
|
if (previewImgRef.current) {
|
||||||
setPreviewScale({
|
setPreviewScale({
|
||||||
@@ -153,7 +156,6 @@ export default function CapturePanel({
|
|||||||
} as any);
|
} as any);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Agent indisponible — fallback capture locale
|
|
||||||
console.warn('Agent Windows indisponible, fallback local:', data.error);
|
console.warn('Agent Windows indisponible, fallback local:', data.error);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Erreur capture Windows, fallback local:', err);
|
console.warn('Erreur capture Windows, fallback local:', err);
|
||||||
@@ -185,7 +187,7 @@ export default function CapturePanel({
|
|||||||
|
|
||||||
const handleLibrarySelect = (item: LibraryItem) => {
|
const handleLibrarySelect = (item: LibraryItem) => {
|
||||||
setCurrentCapture(item.capture);
|
setCurrentCapture(item.capture);
|
||||||
setIsFullscreen(true); // Ouvrir en plein écran pour sélectionner
|
setIsFullscreen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLibraryItem = (id: string) => {
|
const handleDeleteLibraryItem = (id: string) => {
|
||||||
@@ -196,13 +198,13 @@ export default function CapturePanel({
|
|||||||
<div className="capture-panel">
|
<div className="capture-panel">
|
||||||
<h3>Capture</h3>
|
<h3>Capture</h3>
|
||||||
|
|
||||||
{/* Capture — auto-détection OS navigateur */}
|
{/* Capture — auto-detection OS navigateur */}
|
||||||
<div className="capture-controls">
|
<div className="capture-controls">
|
||||||
<button disabled={countdown !== null} onClick={doSmartCapture}>
|
<button disabled={countdown !== null} onClick={doSmartCapture}>
|
||||||
📸 Capturer
|
Capturer
|
||||||
</button>
|
</button>
|
||||||
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
|
<select value={timerSeconds} onChange={(e) => setTimerSeconds(Number(e.target.value))}>
|
||||||
<option value="0">Immédiat</option>
|
<option value="0">Immediat</option>
|
||||||
<option value="3">3 sec</option>
|
<option value="3">3 sec</option>
|
||||||
<option value="5">5 sec</option>
|
<option value="5">5 sec</option>
|
||||||
<option value="10">10 sec</option>
|
<option value="10">10 sec</option>
|
||||||
@@ -212,7 +214,7 @@ export default function CapturePanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Aperçu de la capture avec détection */}
|
{/* Apercu de la capture avec detection */}
|
||||||
{currentCapture && (
|
{currentCapture && (
|
||||||
<div className="capture-preview">
|
<div className="capture-preview">
|
||||||
<div className="capture-preview-container">
|
<div className="capture-preview-container">
|
||||||
@@ -223,7 +225,7 @@ export default function CapturePanel({
|
|||||||
onClick={() => setIsFullscreen(true)}
|
onClick={() => setIsFullscreen(true)}
|
||||||
onLoad={handlePreviewImageLoad}
|
onLoad={handlePreviewImageLoad}
|
||||||
/>
|
/>
|
||||||
{/* Bounding boxes des éléments détectés sur l'aperçu miniature */}
|
{/* Bounding boxes des elements detectes sur l'apercu miniature */}
|
||||||
{previewElements.map((elem) => (
|
{previewElements.map((elem) => (
|
||||||
<div
|
<div
|
||||||
key={elem.id}
|
key={elem.id}
|
||||||
@@ -239,43 +241,43 @@ export default function CapturePanel({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isDetectingPreview && (
|
{isDetectingPreview && (
|
||||||
<div className="capture-preview-detecting">Détection...</div>
|
<div className="capture-preview-detecting">Detection...</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="capture-info">
|
<p className="capture-info">
|
||||||
{currentCapture.width}x{currentCapture.height}
|
{currentCapture.width}x{currentCapture.height}
|
||||||
{previewElements.length > 0 && (
|
{previewElements.length > 0 && (
|
||||||
<span className="capture-elements-count">
|
<span className="capture-elements-count">
|
||||||
{previewElements.length} éléments
|
{previewElements.length} elements
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => setIsFullscreen(true)}>Plein écran</button>
|
<button onClick={() => setIsFullscreen(true)}>Plein ecran</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasSelectedStep && currentCapture && (
|
{!hasSelectedStep && currentCapture && (
|
||||||
<p className="capture-hint">Sélectionnez une étape pour définir l'ancre</p>
|
<p className="capture-hint">Selectionnez une etape pour definir l'ancre</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Zone de détection */}
|
{/* Zone de detection */}
|
||||||
{detectionZone && (
|
{detectionZone && (
|
||||||
<div className="detection-zone-info">
|
<div className="detection-zone-info">
|
||||||
<span>Zone de détection: {detectionZone.width}x{detectionZone.height}</span>
|
<span>Zone de detection: {detectionZone.width}x{detectionZone.height}</span>
|
||||||
<button onClick={() => onSetDetectionZone?.(null)}>Effacer</button>
|
<button onClick={() => onSetDetectionZone?.(null)}>Effacer</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && (
|
{!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && (
|
||||||
<p className="capture-hint zone-hint">
|
<p className="capture-hint zone-hint">
|
||||||
Cliquez sur "Plein écran" puis "Zone de détection" pour cibler une zone
|
Cliquez sur "Plein ecran" puis "Zone de detection" pour cibler une zone
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bibliothèque */}
|
{/* Bibliotheque */}
|
||||||
<div className="capture-library">
|
<div className="capture-library">
|
||||||
<h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}>
|
<h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}>
|
||||||
Bibliothèque ({library.length})
|
Bibliotheque ({library.length})
|
||||||
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>▸ Ouvrir</span>}
|
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>Ouvrir</span>}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="library-grid">
|
<div className="library-grid">
|
||||||
{library.slice(0, 4).map(item => (
|
{library.slice(0, 4).map(item => (
|
||||||
@@ -289,7 +291,7 @@ export default function CapturePanel({
|
|||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteLibraryItem(item.id); }}
|
onClick={(e) => { e.stopPropagation(); handleDeleteLibraryItem(item.id); }}
|
||||||
>
|
>
|
||||||
×
|
x
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -304,12 +306,12 @@ export default function CapturePanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Galerie plein écran de la bibliothèque */}
|
{/* Galerie plein ecran de la bibliotheque */}
|
||||||
{showLibraryGallery && (
|
{showLibraryGallery && (
|
||||||
<div className="fullscreen-modal">
|
<div className="fullscreen-modal">
|
||||||
<div className="fullscreen-header">
|
<div className="fullscreen-header">
|
||||||
<span>Bibliothèque — {library.length} captures — Cliquez pour sélectionner</span>
|
<span>Bibliotheque -- {library.length} captures -- Cliquez pour selectionner</span>
|
||||||
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Échap)</button>
|
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Echap)</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="library-gallery-grid">
|
<div className="library-gallery-grid">
|
||||||
{library.map(item => (
|
{library.map(item => (
|
||||||
@@ -334,7 +336,7 @@ export default function CapturePanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal plein écran */}
|
{/* Modal plein ecran */}
|
||||||
{isFullscreen && currentCapture && (
|
{isFullscreen && currentCapture && (
|
||||||
<FullscreenSelector
|
<FullscreenSelector
|
||||||
capture={currentCapture}
|
capture={currentCapture}
|
||||||
@@ -353,7 +355,24 @@ export default function CapturePanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Composant sélecteur plein écran
|
// ============================================================================
|
||||||
|
// FullscreenSelector — composant refonde pour stabilite definitive
|
||||||
|
//
|
||||||
|
// Architecture :
|
||||||
|
// .fullscreen-modal (fixed, flex column)
|
||||||
|
// .fullscreen-header (bar d'outils)
|
||||||
|
// .fullscreen-content (flex: 1, flex center, overflow hidden, position relative)
|
||||||
|
// <img> (max-width/max-height 100%, object-fit contain)
|
||||||
|
// <div overlay> (position absolute, memes dimensions que l'image via imageRect)
|
||||||
|
// bboxes detection (position absolute, coords = naturel * scale)
|
||||||
|
// zone detection (position absolute, coords = naturel * scale)
|
||||||
|
// selection utilisateur (position absolute, coords relatives a l'image affichee)
|
||||||
|
//
|
||||||
|
// PRINCIPE CLE : l'overlay est positionne exactement sur l'image via imageRect.
|
||||||
|
// Tous les overlays sont enfants de cet overlay, positionnes relativement a lui.
|
||||||
|
// Pas de calcul d'offset disperse — tout passe par imageRect.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
function FullscreenSelector({
|
function FullscreenSelector({
|
||||||
capture,
|
capture,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -372,17 +391,24 @@ function FullscreenSelector({
|
|||||||
onSetDetectionZone?: (zone: DetectionZone | null) => void;
|
onSetDetectionZone?: (zone: DetectionZone | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [isSelecting, setIsSelecting] = useState(false);
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
|
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
|
||||||
const [selection, setSelection] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
const [selection, setSelection] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||||
const [detectedElements, setDetectedElements] = useState<UIElement[]>([]);
|
const [detectedElements, setDetectedElements] = useState<UIElement[]>([]);
|
||||||
const [isDetecting, setIsDetecting] = useState(false);
|
const [isDetecting, setIsDetecting] = useState(false);
|
||||||
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
|
||||||
const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor');
|
const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor');
|
||||||
const [showDetection, setShowDetection] = useState(true);
|
const [showDetection, setShowDetection] = useState(true);
|
||||||
|
|
||||||
// Lancer la détection automatiquement (tous modes)
|
// imageRect: position et dimensions de l'image AFFICHEE dans le conteneur
|
||||||
|
// Calcule par getBoundingClientRect() de l'image relativement au conteneur parent
|
||||||
|
const [imageRect, setImageRect] = useState({ left: 0, top: 0, width: 0, height: 0 });
|
||||||
|
|
||||||
|
// Scale de l'image affichee par rapport a l'image naturelle
|
||||||
|
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||||
|
|
||||||
|
// ── Detection automatique des elements UI ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const runDetection = async () => {
|
const runDetection = async () => {
|
||||||
setIsDetecting(true);
|
setIsDetecting(true);
|
||||||
@@ -394,7 +420,7 @@ function FullscreenSelector({
|
|||||||
);
|
);
|
||||||
setDetectedElements(result.elements);
|
setDetectedElements(result.elements);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur détection:', err);
|
console.error('Erreur detection:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDetecting(false);
|
setIsDetecting(false);
|
||||||
}
|
}
|
||||||
@@ -403,17 +429,38 @@ function FullscreenSelector({
|
|||||||
runDetection();
|
runDetection();
|
||||||
}, [capture.screenshot_base64]);
|
}, [capture.screenshot_base64]);
|
||||||
|
|
||||||
// Calculer le scale quand l'image est chargée
|
// ── Calcul de imageRect et imageScale ──
|
||||||
|
// Appele au chargement de l'image ET au redimensionnement de la fenetre.
|
||||||
|
const recalcImageRect = useCallback(() => {
|
||||||
|
if (!imgRef.current || !contentRef.current) return;
|
||||||
|
|
||||||
|
const containerRect = contentRef.current.getBoundingClientRect();
|
||||||
|
const imgBounds = imgRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
setImageRect({
|
||||||
|
left: imgBounds.left - containerRect.left,
|
||||||
|
top: imgBounds.top - containerRect.top,
|
||||||
|
width: imgBounds.width,
|
||||||
|
height: imgBounds.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
setImageScale({
|
||||||
|
x: imgBounds.width / imgRef.current.naturalWidth,
|
||||||
|
y: imgBounds.height / imgRef.current.naturalHeight,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
const handleImageLoad = () => {
|
||||||
if (imgRef.current) {
|
recalcImageRect();
|
||||||
setImageScale({
|
|
||||||
x: imgRef.current.width / imgRef.current.naturalWidth,
|
|
||||||
y: imgRef.current.height / imgRef.current.naturalHeight
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cliquer sur un élément détecté
|
// Recalculer sur resize
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', recalcImageRect);
|
||||||
|
return () => window.removeEventListener('resize', recalcImageRect);
|
||||||
|
}, [recalcImageRect]);
|
||||||
|
|
||||||
|
// ── Clic sur un element detecte ──
|
||||||
const handleElementClick = (elem: UIElement) => {
|
const handleElementClick = (elem: UIElement) => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
const bbox = {
|
const bbox = {
|
||||||
@@ -425,6 +472,7 @@ function FullscreenSelector({
|
|||||||
onSelect(bbox);
|
onSelect(bbox);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Fermeture par Echap ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') onClose();
|
if (e.key === 'Escape') onClose();
|
||||||
@@ -433,35 +481,48 @@ function FullscreenSelector({
|
|||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
// ── Gestion de la selection par dessin (souris) ──
|
||||||
// Permettre le dessin en mode zone même sans étape sélectionnée
|
// Toutes les coordonnees sont relatives a l'image AFFICHEE (pas au conteneur).
|
||||||
const canDraw = selectionMode === 'zone' || enabled;
|
// On les convertit en coordonnees image naturelle au mouseUp.
|
||||||
if (!canDraw || !imgRef.current) return;
|
|
||||||
|
|
||||||
const rect = imgRef.current.getBoundingClientRect();
|
const getImageRelativePos = (e: React.MouseEvent): { x: number; y: number } | null => {
|
||||||
const x = e.clientX - rect.left;
|
if (!imgRef.current) return null;
|
||||||
const y = e.clientY - rect.top;
|
const imgBounds = imgRef.current.getBoundingClientRect();
|
||||||
|
const x = e.clientX - imgBounds.left;
|
||||||
|
const y = e.clientY - imgBounds.top;
|
||||||
|
// Limiter aux bornes de l'image
|
||||||
|
return {
|
||||||
|
x: Math.max(0, Math.min(x, imgBounds.width)),
|
||||||
|
y: Math.max(0, Math.min(y, imgBounds.height)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
const canDraw = selectionMode === 'zone' || enabled;
|
||||||
|
if (!canDraw) return;
|
||||||
|
|
||||||
|
const pos = getImageRelativePos(e);
|
||||||
|
if (!pos) return;
|
||||||
|
|
||||||
setIsSelecting(true);
|
setIsSelecting(true);
|
||||||
setStartPos({ x, y });
|
setStartPos(pos);
|
||||||
setSelection({ x, y, width: 0, height: 0 });
|
setSelection({ x: pos.x, y: pos.y, width: 0, height: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e: React.MouseEvent) => {
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
if (!isSelecting || !imgRef.current) return;
|
if (!isSelecting) return;
|
||||||
|
|
||||||
const rect = imgRef.current.getBoundingClientRect();
|
const pos = getImageRelativePos(e);
|
||||||
const currentX = e.clientX - rect.left;
|
if (!pos) return;
|
||||||
const currentY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const width = currentX - startPos.x;
|
const width = pos.x - startPos.x;
|
||||||
const height = currentY - startPos.y;
|
const height = pos.y - startPos.y;
|
||||||
|
|
||||||
setSelection({
|
setSelection({
|
||||||
x: width < 0 ? currentX : startPos.x,
|
x: width < 0 ? pos.x : startPos.x,
|
||||||
y: height < 0 ? currentY : startPos.y,
|
y: height < 0 ? pos.y : startPos.y,
|
||||||
width: Math.abs(width),
|
width: Math.abs(width),
|
||||||
height: Math.abs(height)
|
height: Math.abs(height),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -474,24 +535,22 @@ function FullscreenSelector({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convertir en coordonnées réelles de l'image
|
// Convertir en coordonnees naturelles de l'image
|
||||||
const scaleX = imgRef.current.naturalWidth / imgRef.current.width;
|
const scaleX = imgRef.current.naturalWidth / imgRef.current.getBoundingClientRect().width;
|
||||||
const scaleY = imgRef.current.naturalHeight / imgRef.current.height;
|
const scaleY = imgRef.current.naturalHeight / imgRef.current.getBoundingClientRect().height;
|
||||||
|
|
||||||
const realBbox = {
|
const realBbox = {
|
||||||
x: Math.round(selection.x * scaleX),
|
x: Math.round(selection.x * scaleX),
|
||||||
y: Math.round(selection.y * scaleY),
|
y: Math.round(selection.y * scaleY),
|
||||||
width: Math.round(selection.width * scaleX),
|
width: Math.round(selection.width * scaleX),
|
||||||
height: Math.round(selection.height * scaleY)
|
height: Math.round(selection.height * scaleY),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectionMode === 'zone' && onSetDetectionZone) {
|
if (selectionMode === 'zone' && onSetDetectionZone) {
|
||||||
// Mode zone de détection - fonctionne même sans étape sélectionnée
|
|
||||||
onSetDetectionZone(realBbox);
|
onSetDetectionZone(realBbox);
|
||||||
setSelectionMode('anchor'); // Revenir au mode ancre
|
setSelectionMode('anchor');
|
||||||
setSelection({ x: 0, y: 0, width: 0, height: 0 });
|
setSelection({ x: 0, y: 0, width: 0, height: 0 });
|
||||||
} else if (enabled) {
|
} else if (enabled) {
|
||||||
// Mode ancre normal - nécessite une étape sélectionnée
|
|
||||||
onSelect(realBbox);
|
onSelect(realBbox);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -501,21 +560,20 @@ function FullscreenSelector({
|
|||||||
<div className="fullscreen-header">
|
<div className="fullscreen-header">
|
||||||
<div className="header-left">
|
<div className="header-left">
|
||||||
<span>
|
<span>
|
||||||
{isDetecting && 'Détection en cours... '}
|
{isDetecting && 'Detection en cours... '}
|
||||||
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} éléments - `}
|
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} elements - `}
|
||||||
{selectionMode === 'zone'
|
{selectionMode === 'zone'
|
||||||
? 'Dessinez la zone de détection'
|
? 'Dessinez la zone de detection'
|
||||||
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un élément détecté' : 'Sélectionnez d\'abord une étape')}
|
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un element detecte' : 'Selectionnez d\'abord une etape')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-center">
|
<div className="header-center">
|
||||||
{/* Bouton afficher/masquer la détection */}
|
|
||||||
<button
|
<button
|
||||||
className={`zone-select-btn ${showDetection ? 'active' : ''}`}
|
className={`zone-select-btn ${showDetection ? 'active' : ''}`}
|
||||||
onClick={() => setShowDetection(!showDetection)}
|
onClick={() => setShowDetection(!showDetection)}
|
||||||
style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}}
|
style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}}
|
||||||
>
|
>
|
||||||
{showDetection ? 'Masquer détection' : 'Afficher détection'}
|
{showDetection ? 'Masquer detection' : 'Afficher detection'}
|
||||||
</button>
|
</button>
|
||||||
{onSetDetectionZone && (
|
{onSetDetectionZone && (
|
||||||
<>
|
<>
|
||||||
@@ -523,7 +581,7 @@ function FullscreenSelector({
|
|||||||
className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`}
|
className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`}
|
||||||
onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')}
|
onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')}
|
||||||
>
|
>
|
||||||
{selectionMode === 'zone' ? 'Annuler' : 'Zone de détection'}
|
{selectionMode === 'zone' ? 'Annuler' : 'Zone de detection'}
|
||||||
</button>
|
</button>
|
||||||
{detectionZone && (
|
{detectionZone && (
|
||||||
<button
|
<button
|
||||||
@@ -536,26 +594,40 @@ function FullscreenSelector({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose}>Fermer (Échap)</button>
|
<button onClick={onClose}>Fermer (Echap)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Zone de contenu : flex center, l'image se dimensionne via max-width/max-height */}
|
||||||
<div
|
<div
|
||||||
className="fullscreen-content"
|
className="fullscreen-content"
|
||||||
|
ref={contentRef}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
>
|
>
|
||||||
{/* Conteneur relatif pour positionner les bboxes et la sélection par rapport à l'image */}
|
<img
|
||||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
ref={imgRef}
|
||||||
<img
|
src={b64ImgSrc(capture.screenshot_base64)}
|
||||||
ref={imgRef}
|
alt="Capture plein ecran"
|
||||||
src={b64ImgSrc(capture.screenshot_base64)}
|
draggable={false}
|
||||||
alt="Capture plein écran"
|
onLoad={handleImageLoad}
|
||||||
draggable={false}
|
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
||||||
onLoad={handleImageLoad}
|
/>
|
||||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Overlay des éléments détectés — visible dans tous les modes */}
|
{/* Overlay : positionne exactement sur l'image via imageRect.
|
||||||
|
Tous les enfants sont positionnes relativement a ce div,
|
||||||
|
donc les coordonnees = coords_naturelles * imageScale. */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: imageRect.left,
|
||||||
|
top: imageRect.top,
|
||||||
|
width: imageRect.width,
|
||||||
|
height: imageRect.height,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Bboxes des elements detectes */}
|
||||||
{showDetection && detectedElements.map((elem) => (
|
{showDetection && detectedElements.map((elem) => (
|
||||||
<div
|
<div
|
||||||
key={elem.id}
|
key={elem.id}
|
||||||
@@ -569,6 +641,7 @@ function FullscreenSelector({
|
|||||||
border: '2px solid #e94560',
|
border: '2px solid #e94560',
|
||||||
background: 'rgba(233, 69, 96, 0.15)',
|
background: 'rgba(233, 69, 96, 0.15)',
|
||||||
cursor: enabled ? 'pointer' : 'default',
|
cursor: enabled ? 'pointer' : 'default',
|
||||||
|
pointerEvents: 'auto',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -593,8 +666,8 @@ function FullscreenSelector({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Zone de détection existante */}
|
{/* Zone de detection existante */}
|
||||||
{detectionZone && imgRef.current && (
|
{detectionZone && (
|
||||||
<div
|
<div
|
||||||
className="detection-zone-overlay"
|
className="detection-zone-overlay"
|
||||||
style={{
|
style={{
|
||||||
@@ -606,7 +679,7 @@ function FullscreenSelector({
|
|||||||
border: '3px solid #4caf50',
|
border: '3px solid #4caf50',
|
||||||
background: 'rgba(76, 175, 80, 0.15)',
|
background: 'rgba(76, 175, 80, 0.15)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
zIndex: 5
|
zIndex: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{
|
<span style={{
|
||||||
@@ -618,24 +691,23 @@ function FullscreenSelector({
|
|||||||
padding: '2px 8px',
|
padding: '2px 8px',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold',
|
||||||
}}>
|
}}>
|
||||||
Zone de détection
|
Zone de detection
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Rectangle de sélection manuelle */}
|
{/* Rectangle de selection manuelle */}
|
||||||
{(isSelecting || selection.width > 0) && (
|
{(isSelecting || selection.width > 0) && (
|
||||||
<div
|
<div
|
||||||
ref={overlayRef}
|
|
||||||
className={`selection-overlay ${selectionMode === 'zone' ? 'zone-mode' : ''}`}
|
className={`selection-overlay ${selectionMode === 'zone' ? 'zone-mode' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: selection.x,
|
left: selection.x,
|
||||||
top: selection.y,
|
top: selection.y,
|
||||||
width: selection.width,
|
width: selection.width,
|
||||||
height: selection.height
|
height: selection.height,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user