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:
|
||||
# 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
|
||||
|
||||
# Capture écran
|
||||
screenshot = pyautogui.screenshot()
|
||||
|
||||
width, height = screenshot.size
|
||||
|
||||
# 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 { UIElement } from '../services/uiDetection';
|
||||
import {
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
compressThumbnail,
|
||||
} 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 {
|
||||
if (base64.startsWith('data:')) return base64;
|
||||
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 [timerSeconds, setTimerSeconds] = useState(0);
|
||||
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 [isDetectingPreview, setIsDetectingPreview] = useState(false);
|
||||
const previewImgRef = useRef<HTMLImageElement>(null);
|
||||
@@ -61,8 +65,7 @@ export default function CapturePanel({
|
||||
|
||||
const isDebugMode = executionMode === 'debug';
|
||||
|
||||
// Charger la bibliothèque depuis le backend (prioritaire), fallback localStorage.
|
||||
// Le helper loadLibraryAsync() migre aussi les données de l'ancienne clé 'captureLibrary'.
|
||||
// Charger la bibliotheque depuis le backend (prioritaire), fallback localStorage.
|
||||
useEffect(() => {
|
||||
loadLibraryAsync().then((loaded) => {
|
||||
setLibrary(
|
||||
@@ -77,12 +80,12 @@ export default function CapturePanel({
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Sauvegarder la bibliothèque (localStorage + gestion de quota)
|
||||
// Sauvegarder la bibliotheque (localStorage + backend)
|
||||
useEffect(() => {
|
||||
saveLibrary(library);
|
||||
}, [library]);
|
||||
|
||||
// Ajouter capture à la bibliothèque (thumbnail compressé JPEG 320x240)
|
||||
// Ajouter capture a la bibliotheque (thumbnail compresse JPEG 320x240)
|
||||
useEffect(() => {
|
||||
if (!capture) return;
|
||||
setCurrentCapture(capture);
|
||||
@@ -101,7 +104,7 @@ export default function CapturePanel({
|
||||
return () => { cancelled = true; };
|
||||
}, [capture]);
|
||||
|
||||
// Détecter les éléments UI quand une capture arrive
|
||||
// Detecter les elements UI quand une capture arrive
|
||||
useEffect(() => {
|
||||
if (!currentCapture) {
|
||||
setPreviewElements([]);
|
||||
@@ -118,7 +121,7 @@ export default function CapturePanel({
|
||||
);
|
||||
setPreviewElements(result.elements);
|
||||
} catch (err) {
|
||||
console.error('Détection aperçu échouée:', err);
|
||||
console.error('Detection apercu echouee:', err);
|
||||
setPreviewElements([]);
|
||||
} finally {
|
||||
setIsDetectingPreview(false);
|
||||
@@ -128,7 +131,7 @@ export default function CapturePanel({
|
||||
runDetection();
|
||||
}, [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 = () => {
|
||||
if (previewImgRef.current) {
|
||||
setPreviewScale({
|
||||
@@ -153,7 +156,6 @@ export default function CapturePanel({
|
||||
} as any);
|
||||
return;
|
||||
}
|
||||
// Agent indisponible — fallback capture locale
|
||||
console.warn('Agent Windows indisponible, fallback local:', data.error);
|
||||
} catch (err) {
|
||||
console.warn('Erreur capture Windows, fallback local:', err);
|
||||
@@ -185,7 +187,7 @@ export default function CapturePanel({
|
||||
|
||||
const handleLibrarySelect = (item: LibraryItem) => {
|
||||
setCurrentCapture(item.capture);
|
||||
setIsFullscreen(true); // Ouvrir en plein écran pour sélectionner
|
||||
setIsFullscreen(true);
|
||||
};
|
||||
|
||||
const handleDeleteLibraryItem = (id: string) => {
|
||||
@@ -196,13 +198,13 @@ export default function CapturePanel({
|
||||
<div className="capture-panel">
|
||||
<h3>Capture</h3>
|
||||
|
||||
{/* Capture — auto-détection OS navigateur */}
|
||||
{/* Capture — auto-detection OS navigateur */}
|
||||
<div className="capture-controls">
|
||||
<button disabled={countdown !== null} onClick={doSmartCapture}>
|
||||
📸 Capturer
|
||||
Capturer
|
||||
</button>
|
||||
<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="5">5 sec</option>
|
||||
<option value="10">10 sec</option>
|
||||
@@ -212,7 +214,7 @@ export default function CapturePanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Aperçu de la capture avec détection */}
|
||||
{/* Apercu de la capture avec detection */}
|
||||
{currentCapture && (
|
||||
<div className="capture-preview">
|
||||
<div className="capture-preview-container">
|
||||
@@ -223,7 +225,7 @@ export default function CapturePanel({
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
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) => (
|
||||
<div
|
||||
key={elem.id}
|
||||
@@ -239,43 +241,43 @@ export default function CapturePanel({
|
||||
/>
|
||||
))}
|
||||
{isDetectingPreview && (
|
||||
<div className="capture-preview-detecting">Détection...</div>
|
||||
<div className="capture-preview-detecting">Detection...</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="capture-info">
|
||||
{currentCapture.width}x{currentCapture.height}
|
||||
{previewElements.length > 0 && (
|
||||
<span className="capture-elements-count">
|
||||
{previewElements.length} éléments
|
||||
{previewElements.length} elements
|
||||
</span>
|
||||
)}
|
||||
<button onClick={() => setIsFullscreen(true)}>Plein écran</button>
|
||||
<button onClick={() => setIsFullscreen(true)}>Plein ecran</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!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 && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
{!detectionZone && currentCapture && (executionMode === 'intelligent' || executionMode === 'debug') && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* Bibliothèque */}
|
||||
{/* Bibliotheque */}
|
||||
<div className="capture-library">
|
||||
<h4 style={{ cursor: 'pointer' }} onClick={() => library.length > 0 && setShowLibraryGallery(true)}>
|
||||
Bibliothèque ({library.length})
|
||||
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>▸ Ouvrir</span>}
|
||||
Bibliotheque ({library.length})
|
||||
{library.length > 0 && <span style={{ fontSize: '11px', marginLeft: '8px', color: 'var(--primary)' }}>Ouvrir</span>}
|
||||
</h4>
|
||||
<div className="library-grid">
|
||||
{library.slice(0, 4).map(item => (
|
||||
@@ -289,7 +291,7 @@ export default function CapturePanel({
|
||||
className="delete-btn"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteLibraryItem(item.id); }}
|
||||
>
|
||||
×
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -304,12 +306,12 @@ export default function CapturePanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Galerie plein écran de la bibliothèque */}
|
||||
{/* Galerie plein ecran de la bibliotheque */}
|
||||
{showLibraryGallery && (
|
||||
<div className="fullscreen-modal">
|
||||
<div className="fullscreen-header">
|
||||
<span>Bibliothèque — {library.length} captures — Cliquez pour sélectionner</span>
|
||||
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Échap)</button>
|
||||
<span>Bibliotheque -- {library.length} captures -- Cliquez pour selectionner</span>
|
||||
<button onClick={() => setShowLibraryGallery(false)}>Fermer (Echap)</button>
|
||||
</div>
|
||||
<div className="library-gallery-grid">
|
||||
{library.map(item => (
|
||||
@@ -334,7 +336,7 @@ export default function CapturePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal plein écran */}
|
||||
{/* Modal plein ecran */}
|
||||
{isFullscreen && currentCapture && (
|
||||
<FullscreenSelector
|
||||
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({
|
||||
capture,
|
||||
onClose,
|
||||
@@ -372,17 +391,24 @@ function FullscreenSelector({
|
||||
onSetDetectionZone?: (zone: DetectionZone | null) => void;
|
||||
}) {
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
|
||||
const [selection, setSelection] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const [detectedElements, setDetectedElements] = useState<UIElement[]>([]);
|
||||
const [isDetecting, setIsDetecting] = useState(false);
|
||||
const [imageScale, setImageScale] = useState({ x: 1, y: 1 });
|
||||
const [selectionMode, setSelectionMode] = useState<'anchor' | 'zone'>('anchor');
|
||||
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(() => {
|
||||
const runDetection = async () => {
|
||||
setIsDetecting(true);
|
||||
@@ -394,7 +420,7 @@ function FullscreenSelector({
|
||||
);
|
||||
setDetectedElements(result.elements);
|
||||
} catch (err) {
|
||||
console.error('Erreur détection:', err);
|
||||
console.error('Erreur detection:', err);
|
||||
} finally {
|
||||
setIsDetecting(false);
|
||||
}
|
||||
@@ -403,17 +429,38 @@ function FullscreenSelector({
|
||||
runDetection();
|
||||
}, [capture.screenshot_base64]);
|
||||
|
||||
// Calculer le scale quand l'image est chargée
|
||||
const handleImageLoad = () => {
|
||||
if (imgRef.current) {
|
||||
setImageScale({
|
||||
x: imgRef.current.width / imgRef.current.naturalWidth,
|
||||
y: imgRef.current.height / imgRef.current.naturalHeight
|
||||
// ── 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 = () => {
|
||||
recalcImageRect();
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
if (!enabled) return;
|
||||
const bbox = {
|
||||
@@ -425,6 +472,7 @@ function FullscreenSelector({
|
||||
onSelect(bbox);
|
||||
};
|
||||
|
||||
// ── Fermeture par Echap ──
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
@@ -433,35 +481,48 @@ function FullscreenSelector({
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
// Permettre le dessin en mode zone même sans étape sélectionnée
|
||||
const canDraw = selectionMode === 'zone' || enabled;
|
||||
if (!canDraw || !imgRef.current) return;
|
||||
// ── Gestion de la selection par dessin (souris) ──
|
||||
// Toutes les coordonnees sont relatives a l'image AFFICHEE (pas au conteneur).
|
||||
// On les convertit en coordonnees image naturelle au mouseUp.
|
||||
|
||||
const rect = imgRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const getImageRelativePos = (e: React.MouseEvent): { x: number; y: number } | null => {
|
||||
if (!imgRef.current) return null;
|
||||
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);
|
||||
setStartPos({ x, y });
|
||||
setSelection({ x, y, width: 0, height: 0 });
|
||||
setStartPos(pos);
|
||||
setSelection({ x: pos.x, y: pos.y, width: 0, height: 0 });
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!isSelecting || !imgRef.current) return;
|
||||
if (!isSelecting) return;
|
||||
|
||||
const rect = imgRef.current.getBoundingClientRect();
|
||||
const currentX = e.clientX - rect.left;
|
||||
const currentY = e.clientY - rect.top;
|
||||
const pos = getImageRelativePos(e);
|
||||
if (!pos) return;
|
||||
|
||||
const width = currentX - startPos.x;
|
||||
const height = currentY - startPos.y;
|
||||
const width = pos.x - startPos.x;
|
||||
const height = pos.y - startPos.y;
|
||||
|
||||
setSelection({
|
||||
x: width < 0 ? currentX : startPos.x,
|
||||
y: height < 0 ? currentY : startPos.y,
|
||||
x: width < 0 ? pos.x : startPos.x,
|
||||
y: height < 0 ? pos.y : startPos.y,
|
||||
width: Math.abs(width),
|
||||
height: Math.abs(height)
|
||||
height: Math.abs(height),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -474,24 +535,22 @@ function FullscreenSelector({
|
||||
return;
|
||||
}
|
||||
|
||||
// Convertir en coordonnées réelles de l'image
|
||||
const scaleX = imgRef.current.naturalWidth / imgRef.current.width;
|
||||
const scaleY = imgRef.current.naturalHeight / imgRef.current.height;
|
||||
// Convertir en coordonnees naturelles de l'image
|
||||
const scaleX = imgRef.current.naturalWidth / imgRef.current.getBoundingClientRect().width;
|
||||
const scaleY = imgRef.current.naturalHeight / imgRef.current.getBoundingClientRect().height;
|
||||
|
||||
const realBbox = {
|
||||
x: Math.round(selection.x * scaleX),
|
||||
y: Math.round(selection.y * scaleY),
|
||||
width: Math.round(selection.width * scaleX),
|
||||
height: Math.round(selection.height * scaleY)
|
||||
height: Math.round(selection.height * scaleY),
|
||||
};
|
||||
|
||||
if (selectionMode === 'zone' && onSetDetectionZone) {
|
||||
// Mode zone de détection - fonctionne même sans étape sélectionnée
|
||||
onSetDetectionZone(realBbox);
|
||||
setSelectionMode('anchor'); // Revenir au mode ancre
|
||||
setSelectionMode('anchor');
|
||||
setSelection({ x: 0, y: 0, width: 0, height: 0 });
|
||||
} else if (enabled) {
|
||||
// Mode ancre normal - nécessite une étape sélectionnée
|
||||
onSelect(realBbox);
|
||||
}
|
||||
};
|
||||
@@ -501,21 +560,20 @@ function FullscreenSelector({
|
||||
<div className="fullscreen-header">
|
||||
<div className="header-left">
|
||||
<span>
|
||||
{isDetecting && 'Détection en cours... '}
|
||||
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} éléments - `}
|
||||
{isDetecting && 'Detection en cours... '}
|
||||
{!isDetecting && detectedElements.length > 0 && `${detectedElements.length} elements - `}
|
||||
{selectionMode === 'zone'
|
||||
? 'Dessinez la zone de détection'
|
||||
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un élément détecté' : 'Sélectionnez d\'abord une étape')}
|
||||
? 'Dessinez la zone de detection'
|
||||
: (enabled ? 'Dessinez un rectangle pour l\'ancre ou cliquez sur un element detecte' : 'Selectionnez d\'abord une etape')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="header-center">
|
||||
{/* Bouton afficher/masquer la détection */}
|
||||
<button
|
||||
className={`zone-select-btn ${showDetection ? 'active' : ''}`}
|
||||
onClick={() => setShowDetection(!showDetection)}
|
||||
style={showDetection ? { background: 'var(--secondary)', borderColor: 'var(--secondary)', color: 'white' } : {}}
|
||||
>
|
||||
{showDetection ? 'Masquer détection' : 'Afficher détection'}
|
||||
{showDetection ? 'Masquer detection' : 'Afficher detection'}
|
||||
</button>
|
||||
{onSetDetectionZone && (
|
||||
<>
|
||||
@@ -523,7 +581,7 @@ function FullscreenSelector({
|
||||
className={`zone-select-btn ${selectionMode === 'zone' ? 'active' : ''}`}
|
||||
onClick={() => setSelectionMode(selectionMode === 'zone' ? 'anchor' : 'zone')}
|
||||
>
|
||||
{selectionMode === 'zone' ? 'Annuler' : 'Zone de détection'}
|
||||
{selectionMode === 'zone' ? 'Annuler' : 'Zone de detection'}
|
||||
</button>
|
||||
{detectionZone && (
|
||||
<button
|
||||
@@ -536,26 +594,40 @@ function FullscreenSelector({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={onClose}>Fermer (Échap)</button>
|
||||
<button onClick={onClose}>Fermer (Echap)</button>
|
||||
</div>
|
||||
|
||||
{/* Zone de contenu : flex center, l'image se dimensionne via max-width/max-height */}
|
||||
<div
|
||||
className="fullscreen-content"
|
||||
ref={contentRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
{/* Conteneur relatif pour positionner les bboxes et la sélection par rapport à l'image */}
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={b64ImgSrc(capture.screenshot_base64)}
|
||||
alt="Capture plein écran"
|
||||
alt="Capture plein ecran"
|
||||
draggable={false}
|
||||
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) => (
|
||||
<div
|
||||
key={elem.id}
|
||||
@@ -569,6 +641,7 @@ function FullscreenSelector({
|
||||
border: '2px solid #e94560',
|
||||
background: 'rgba(233, 69, 96, 0.15)',
|
||||
cursor: enabled ? 'pointer' : 'default',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 10,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
@@ -593,8 +666,8 @@ function FullscreenSelector({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Zone de détection existante */}
|
||||
{detectionZone && imgRef.current && (
|
||||
{/* Zone de detection existante */}
|
||||
{detectionZone && (
|
||||
<div
|
||||
className="detection-zone-overlay"
|
||||
style={{
|
||||
@@ -606,7 +679,7 @@ function FullscreenSelector({
|
||||
border: '3px solid #4caf50',
|
||||
background: 'rgba(76, 175, 80, 0.15)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 5
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
@@ -618,24 +691,23 @@ function FullscreenSelector({
|
||||
padding: '2px 8px',
|
||||
borderRadius: '3px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
Zone de détection
|
||||
Zone de detection
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rectangle de sélection manuelle */}
|
||||
{/* Rectangle de selection manuelle */}
|
||||
{(isSelecting || selection.width > 0) && (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className={`selection-overlay ${selectionMode === 'zone' ? 'zone-mode' : ''}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: selection.x,
|
||||
top: selection.y,
|
||||
width: selection.width,
|
||||
height: selection.height
|
||||
height: selection.height,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user