PointTrackApp / src /components /VideoSection.vue
2nzi's picture
first commit
b4f9490 verified
<template>
<div class="video-section">
<tool-bar
:current-tool="currentTool"
@tool-selected="selectTool"
/>
<div class="video-container" ref="container">
<div class="video-wrapper">
<video
ref="videoRef"
class="video-element"
crossorigin="anonymous"
muted
></video>
</div>
<div class="canvas-wrapper">
<v-stage
ref="stage"
:config="stageConfig"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
class="canvas-overlay"
>
<v-layer ref="layer">
<!-- Fond transparent explicite -->
<v-rect
:config="{
x: position.x,
y: position.y,
width: imageWidth,
height: imageHeight,
fill: 'transparent'
}"
/>
<!-- Lignes de guidage -->
<v-line
v-if="mousePosition.x !== null && isInsideImage(mousePosition)"
:config="{
points: [
position.x, mousePosition.y,
position.x + imageWidth, mousePosition.y
],
stroke: '#ffffff',
strokeWidth: 1,
dash: [5, 5],
opacity: 0.5
}"
/>
<v-line
v-if="mousePosition.y !== null && isInsideImage(mousePosition)"
:config="{
points: [
mousePosition.x, position.y,
mousePosition.x, position.y + imageHeight
],
stroke: '#ffffff',
strokeWidth: 1,
dash: [5, 5],
opacity: 0.5
}"
/>
<!-- Rectangle en cours de dessin -->
<v-rect
v-if="isDrawing && currentTool === 'rectangle'"
:config="{
x: rectangleStart.x,
y: rectangleStart.y,
width: rectangleSize.width,
height: rectangleSize.height,
stroke: getObjectColor(annotationStore.selectedObjectId),
strokeWidth: 1,
fill: null,
dash: []
}"
/>
<!-- Rectangles sauvegardés -->
<v-rect
v-for="rect in rectangles"
:key="rect.id"
:config="{
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
stroke: selectedId === rect.id ? '#FFD700' : rect.color,
strokeWidth: selectedId === rect.id ? 3 : (rect.objectId === annotationStore.selectedObjectId ? 2 : 1),
fill: null,
dash: rect.type === 'proxy' ? [5, 5] : [],
id: rect.id,
objectId: rect.objectId
}"
@mousedown="handleShapeMouseDown($event, rect.id)"
/>
<!-- Halo de sélection pour le rectangle sélectionné -->
<v-rect
v-if="selectedId && rectangles.find(r => r.id === selectedId)"
:key="`halo-${selectedId}`"
:config="{
x: rectangles.find(r => r.id === selectedId).x - 3,
y: rectangles.find(r => r.id === selectedId).y - 3,
width: rectangles.find(r => r.id === selectedId).width + 6,
height: rectangles.find(r => r.id === selectedId).height + 6,
stroke: '#FFD700',
strokeWidth: 1,
fill: null,
dash: [6, 6],
opacity: 0.6,
listening: false
}"
/>
<!-- Poignées de redimensionnement pour le rectangle sélectionné -->
<template v-if="selectedId && currentTool === 'arrow'">
<v-circle
v-for="handle in getResizeHandles()"
:key="handle.position"
:config="{
x: handle.x,
y: handle.y,
radius: 4,
fill: 'white',
stroke: '#4CAF50',
strokeWidth: 1,
draggable: true
}"
@dragmove="handleResize($event, handle.position)"
/>
</template>
<!-- Points existants -->
<v-group
v-for="point in points"
:key="point.id"
:config="{
x: point.x,
y: point.y,
objectId: point.objectId,
listening: true,
id: point.id
}"
@mousedown="handlePointClick(point.id, $event)"
>
<!-- Halo de sélection pour le point sélectionné -->
<v-circle
v-if="selectedId === point.id"
:config="{
radius: 12,
fill: 'transparent',
stroke: '#FFD700',
strokeWidth: 2,
dash: [4, 4],
opacity: 0.8
}"
/>
<v-circle
:config="{
radius: selectedId === point.id ? 7 : (point.objectId === annotationStore.selectedObjectId ? 6 : 5),
fill: point.color,
stroke: selectedId === point.id ? '#FFD700' : 'white',
strokeWidth: selectedId === point.id ? 3 : (point.objectId === annotationStore.selectedObjectId ? 2 : 1)
}"
/>
<v-line
:config="{
points: [-2, 0, 2, 0],
stroke: selectedId === point.id ? '#FFD700' : 'white',
strokeWidth: selectedId === point.id ? 2 : 1
}"
/>
<v-line
v-if="point.type === 'positive'"
:config="{
points: [0, -2, 0, 2],
stroke: selectedId === point.id ? '#FFD700' : 'white',
strokeWidth: selectedId === point.id ? 2 : 1
}"
/>
</v-group>
<!-- Masques de segmentation -->
<v-shape
v-for="annotation in maskedAnnotations"
:key="`mask-${annotation.id}`"
:config="{
sceneFunc: (context, shape) => drawMask(context, shape, annotation),
fill: annotation.objectId === annotationStore.selectedObjectId ?
`${getObjectColor(annotation.objectId)}88` :
`${getObjectColor(annotation.objectId)}44`,
stroke: getObjectColor(annotation.objectId),
strokeWidth: annotation.objectId === annotationStore.selectedObjectId ? 2 : 1,
opacity: 0.8,
listening: true,
id: annotation.id
}"
@mousedown="handleMaskClick(annotation.id)"
/>
<!-- Bounding boxes de tous les objets -->
<v-rect
v-for="bbox in allBoundingBoxes"
v-show="showAllBoundingBoxes && bbox"
:key="bbox.id"
:config="{
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
stroke: bbox.color,
strokeWidth: bbox.objectId === annotationStore.selectedObjectId ? 2 : 1,
fill: null,
dash: [4, 2], // Trait pointillé uniforme pour tous les rectangles
opacity: 0.8,
listening: false // Ne pas intercepter les clics sur les bbox d'affichage
}"
/>
</v-layer>
</v-stage>
</div>
</div>
</div>
</template>
<script>
import { useVideoStore } from '@/stores/videoStore'
import { useAnnotationStore } from '@/stores/annotationStore'
import ToolBar from './tools/ToolBar.vue'
/*
FONCTIONNALITÉS DES BOUNDING BOXES :
1. Affichage automatique des bounding boxes pour tous les objets sur chaque frame :
- Annotations en cours : rectangles et masques créés dans cette session
- Objet sélectionné : trait plus épais pour mise en évidence
2. Différenciation visuelle :
- Annotations rectangles : trait continu []
- Annotations masques : trait pointillé court [- - -]
- Couleurs : selon la couleur de l'objet définie
3. Contrôles clavier :
- Touche 'b' : Basculer l'affichage des bounding boxes (on/off)
- Touche 'd' : Afficher les informations de débogage dans la console
- Par défaut : affichage activé
4. Sources de données :
- Store d'annotations (this.annotationStore) : annotations créées en temps réel
5. Stockage des bounding boxes :
- Rectangles : coordonnées directes (x, y, width, height)
- Masques de segmentation : bbox calculée à partir des points ou du masque
6. Débogage :
- Appel automatique de debugBoundingBoxes() à chaque changement de frame
- Informations détaillées sur les annotations trouvées
*/
export default {
name: 'VideoSection',
components: {
ToolBar
},
props: {
selectedObjectId: {
type: String,
default: null
}
},
setup() {
const videoStore = useVideoStore()
const annotationStore = useAnnotationStore()
return { videoStore, annotationStore }
},
data() {
return {
videoElement: null,
imageWidth: 0,
imageHeight: 0,
position: { x: 0, y: 0 },
stageConfig: {
width: 0,
height: 0
},
currentTool: 'arrow',
isDrawing: false,
rectangleStart: { x: 0, y: 0 },
rectangleSize: { width: 0, height: 0 },
mousePosition: { x: null, y: null },
selectedId: null,
isDragging: false,
dragStartPos: { x: 0, y: 0 },
resizing: false,
resizeTimeout: null,
animationId: null,
currentFrameNumber: 0,
originalVideoPath: null,
proxyVideoPath: null,
isUsingProxy: true,
originalVideoDimensions: { width: 0, height: 0 },
maskCache: {},
showAllBoundingBoxes: true, // Nouvelle propriété pour contrôler l'affichage des bbox
}
},
mounted() {
// Réactiver l'élément vidéo
this.videoElement = this.$refs.videoRef
this.videoElement.muted = true
this.videoElement.addEventListener('loadedmetadata', this.handleVideoLoaded)
this.videoElement.addEventListener('timeupdate', this.updateCurrentFrame)
// S'abonner aux changements de vidéo dans le store
this.subscribeToVideoStore()
window.addEventListener('resize', this.handleWindowResize)
window.addEventListener('keydown', this.handleKeyDown)
},
beforeUnmount() {
window.removeEventListener('resize', this.handleWindowResize)
window.removeEventListener('keydown', this.handleKeyDown)
if (this.videoElement) {
this.videoElement.removeEventListener('loadedmetadata', this.handleVideoLoaded)
this.videoElement.pause()
}
this.stopAnimation()
// Supprimer l'écouteur d'événement
this.videoElement.removeEventListener('timeupdate', this.updateCurrentFrame)
},
computed: {
availableObjects() {
return Object.values(this.annotationStore.objects)
},
hasSelectedObject() {
return !!this.annotationStore.selectedObjectId
},
rectangles() {
const frameAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber) || []
// Convertir les annotations en rectangles pour l'affichage
return frameAnnotations
.filter(annotation => annotation && annotation.type === 'rectangle')
.map(annotation => {
const object = this.annotationStore.objects[annotation.objectId]
const color = object ? object.color : '#4CAF50'
// Convertir les coordonnées originales en coordonnées d'affichage
const displayX = this.position.x + (annotation.x / this.scaleX)
const displayY = this.position.y + (annotation.y / this.scaleY)
const displayWidth = annotation.width / this.scaleX
const displayHeight = annotation.height / this.scaleY
return {
id: annotation.id,
objectId: annotation.objectId,
x: displayX,
y: displayY,
width: displayWidth,
height: displayHeight,
color: color
}
})
},
points() {
const frameAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber) || []
// Points directs de type "point" (nouveau système)
const directPoints = frameAnnotations
.filter(annotation => annotation && annotation.type === 'point')
.map(annotation => ({
id: annotation.id,
objectId: annotation.objectId,
x: this.position.x + (annotation.x / this.scaleX),
y: this.position.y + (annotation.y / this.scaleY),
type: annotation.pointType,
color: this.getObjectColor(annotation.objectId),
isDirect: true
}))
// Points des annotations de type "mask" qui contiennent des points (ancien système)
const annotationPoints = frameAnnotations
.filter(annotation => annotation && annotation.type === 'mask' && annotation.points)
.flatMap(annotation => annotation.points.map(point => ({
id: `${annotation.id}-point-${point.x}-${point.y}`,
objectId: annotation.objectId,
x: this.position.x + (point.x / this.scaleX),
y: this.position.y + (point.y / this.scaleY),
type: point.type,
color: this.getObjectColor(annotation.objectId),
fromAnnotation: annotation.id
})))
// Points temporaires (ne devrait plus être utilisé mais gardé pour sécurité)
const tempPoints = (this.annotationStore.temporaryPoints || []).map(point => ({
id: `temp-point-${point.id}`,
objectId: point.objectId,
x: this.position.x + (point.x / this.scaleX),
y: this.position.y + (point.y / this.scaleY),
type: point.pointType,
color: this.getObjectColor(point.objectId),
isTemporary: true
}))
return [...directPoints, ...annotationPoints, ...tempPoints]
},
scaleX() {
if (this.originalVideoDimensions.width && this.imageWidth) {
return this.originalVideoDimensions.width / this.imageWidth
}
if (!this.videoElement || !this.imageWidth) return 1
return this.videoElement.videoWidth / this.imageWidth
},
scaleY() {
if (this.originalVideoDimensions.height && this.imageHeight) {
return this.originalVideoDimensions.height / this.imageHeight
}
if (!this.videoElement || !this.imageHeight) return 1
return this.videoElement.videoHeight / this.imageHeight
},
maskedAnnotations() {
const frameAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber) || [];
return frameAnnotations.filter(annotation => annotation && annotation.mask && annotation.maskImageSize);
},
// Nouvelle computed property pour toutes les bounding boxes
allBoundingBoxes() {
// Temporairement désactivé pour isoler l'erreur
return []
// Le code original sera restauré une fois l'erreur identifiée
/*
const frameAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber) || []
const boundingBoxes = []
// 1. BOUNDING BOXES DES ANNOTATIONS EN COURS (STORE D'ANNOTATIONS)
frameAnnotations.forEach(annotation => {
if (!annotation) return // Vérifier que l'annotation existe
const object = this.annotationStore.objects[annotation.objectId]
const color = object ? object.color : '#CCCCCC'
const objectName = object ? object.name : `Objet ${annotation.objectId}`
if (annotation.type === 'rectangle') {
// Pour les rectangles, utiliser directement leurs coordonnées
const displayX = this.position.x + (annotation.x / this.scaleX)
const displayY = this.position.y + (annotation.y / this.scaleY)
const displayWidth = annotation.width / this.scaleX
const displayHeight = annotation.height / this.scaleY
boundingBoxes.push({
id: `bbox-${annotation.id}`,
objectId: annotation.objectId,
x: displayX,
y: displayY,
width: displayWidth,
height: displayHeight,
color: color,
name: objectName,
type: 'rectangle',
annotationId: annotation.id,
source: 'current' // Annotations en cours
})
} else if (annotation.type === 'mask' && annotation.mask) {
// Pour les masques, calculer la bounding box ou utiliser celle stockée
let bbox = null
if (annotation.bbox && annotation.bbox.output) {
// Si la bbox est déjà stockée, l'utiliser
bbox = annotation.bbox.output
} else if (annotation.bbox) {
// Si c'est juste bbox sans output
bbox = annotation.bbox
} else {
// Sinon, essayer de calculer une bbox approximative à partir des dimensions du masque
// Cette méthode n'est pas parfaite mais donne une approximation
const maskSize = annotation.maskImageSize || { width: this.originalVideoDimensions.width, height: this.originalVideoDimensions.height }
// Pour une approximation, on peut supposer que le masque couvre une région significative
// En attendant une vraie bbox calculée côté serveur
bbox = {
x: Math.round(maskSize.width * 0.1), // 10% du bord gauche
y: Math.round(maskSize.height * 0.1), // 10% du bord haut
width: Math.round(maskSize.width * 0.8), // 80% de la largeur
height: Math.round(maskSize.height * 0.8) // 80% de la hauteur
}
}
if (bbox) {
// Convertir les coordonnées originales en coordonnées d'affichage
const displayX = this.position.x + (bbox.x / this.scaleX)
const displayY = this.position.y + (bbox.y / this.scaleY)
const displayWidth = bbox.width / this.scaleX
const displayHeight = bbox.height / this.scaleY
boundingBoxes.push({
id: `bbox-${annotation.id}`,
objectId: annotation.objectId,
x: displayX,
y: displayY,
width: displayWidth,
height: displayHeight,
color: color,
name: objectName,
type: 'mask',
annotationId: annotation.id,
source: 'current' // Annotations en cours
})
}
}
})
// Filtrer les bounding boxes invalides avant de retourner
return boundingBoxes.filter(bbox => bbox && bbox.type && bbox.id)
*/
},
},
methods: {
subscribeToVideoStore() {
const videoStore = useVideoStore()
// Observer les changements dans le store
this.$watch(
() => videoStore.selectedVideo,
(newVideo) => {
if (newVideo) {
console.log('Nouvelle vidéo sélectionnée dans VideoSection:', newVideo)
this.loadVideo(newVideo.path)
}
},
{ immediate: true }
)
// Observer les changements de temps dans la timeline
this.$watch(
() => videoStore.currentTime,
(newTime) => {
if (this.videoElement && newTime !== undefined) {
// Seulement mettre à jour si la différence est significative
if (Math.abs(this.videoElement.currentTime - newTime) > 0.05) {
this.videoElement.currentTime = newTime
}
}
}
)
// Observer l'état de lecture (play/pause)
this.$watch(
() => videoStore.isPlaying,
(isPlaying) => {
// console.log('État de lecture changé dans VideoSection:', isPlaying)
if (isPlaying) {
this.playVideo()
} else {
this.pauseVideo()
}
}
)
},
loadVideo(videoPath) {
if (!videoPath) return
// Arrêter toute animation en cours
this.stopAnimation()
this.originalVideoPath = videoPath
// Si l'élément vidéo est commenté, ne pas essayer de charger la vidéo
if (!this.videoElement) {
console.log("Mode test: élément vidéo non disponible, simulation uniquement")
// Simuler des dimensions pour le test
this.imageWidth = 640
this.imageHeight = 360
this.updateDimensions()
return
}
// Si c'est une URL blob (vidéo uploadée), charger directement sans proxy
if (videoPath.startsWith('blob:')) {
console.log("Chargement direct d'une vidéo uploadée (blob URL)")
this.videoElement.src = videoPath
this.videoElement.load()
return
}
// Si c'est un fichier média par défaut depuis les assets, le charger directement
if (videoPath.includes('/assets/') || videoPath.includes('assets/') ||
videoPath.includes('.jpg') || videoPath.includes('.png') ||
videoPath.includes('.jpeg') || videoPath.includes('.mp4') ||
videoPath.includes('.webm') || videoPath.includes('.mov')) {
console.log("Chargement direct d'un fichier média par défaut")
this.videoElement.src = videoPath
this.videoElement.load()
return
}
// Pour les fichiers locaux, utiliser le système de proxy existant
this.getOriginalVideoDimensions(videoPath)
.then(dimensions => {
console.log("Dimensions de la vidéo originale:", dimensions.width, "x", dimensions.height)
// Stocker les dimensions originales pour les utiliser dans les calculs de coordonnées
this.originalVideoDimensions = dimensions
// Vérifier si un proxy existe déjà ou en créer un
this.createOrLoadProxy(videoPath)
.then(proxyPath => {
this.proxyVideoPath = proxyPath
// Charger le proxy si l'option est activée, sinon charger l'original
const sourceToLoad = this.isUsingProxy ? proxyPath : videoPath
this.videoElement.src = sourceToLoad
this.videoElement.load()
})
.catch(err => {
console.error("Erreur lors de la création du proxy:", err)
// En cas d'erreur, charger la vidéo originale
this.videoElement.src = videoPath
this.videoElement.load()
})
})
.catch(err => {
console.error("Erreur lors de l'obtention des dimensions de la vidéo originale:", err)
// Continuer avec le chargement normal
this.videoElement.src = videoPath
this.videoElement.load()
})
},
async getOriginalVideoDimensions(videoPath) {
return new Promise((resolve, reject) => {
// Créer un élément vidéo temporaire pour obtenir les dimensions
const tempVideo = document.createElement('video')
tempVideo.style.display = 'none'
// Configurer les gestionnaires d'événements
tempVideo.onloadedmetadata = () => {
const dimensions = {
width: tempVideo.videoWidth,
height: tempVideo.videoHeight
}
// Nettoyer
document.body.removeChild(tempVideo)
resolve(dimensions)
}
tempVideo.onerror = (error) => {
// Nettoyer
if (document.body.contains(tempVideo)) {
document.body.removeChild(tempVideo)
}
reject(error)
}
// Ajouter l'élément au DOM et charger la vidéo
document.body.appendChild(tempVideo)
tempVideo.src = videoPath
})
},
async createOrLoadProxy(originalPath) {
// Générer un nom de fichier pour le proxy
const proxyPath = this.generateProxyPath(originalPath)
// Vérifier si le proxy existe déjà
const proxyExists = await this.checkIfFileExists(proxyPath)
if (proxyExists) {
console.log("Proxy vidéo existant trouvé:", proxyPath)
return proxyPath
}
// Créer un nouveau proxy
console.log("Création d'un nouveau proxy vidéo...")
return this.createVideoProxy(originalPath, proxyPath)
},
generateProxyPath(originalPath) {
// Exemple: transformer "/videos/original.mp4" en "/videos/original_proxy.mp4"
const pathParts = originalPath.split('.')
const extension = pathParts.pop()
return `${pathParts.join('.')}_proxy.${extension}`
},
async checkIfFileExists() {
// DÉSACTIVÉ - utilisation de vidéo statique dans assets
console.log("Vérification de fichier désactivée - utilisation de vidéo statique");
return true; // Toujours vrai pour la vidéo statique
},
async createVideoProxy(originalPath) {
// DÉSACTIVÉ - utilisation de vidéo statique dans assets
console.log("Création de proxy désactivée - utilisation de vidéo statique");
return originalPath; // Retourner le chemin original
},
toggleProxyMode() {
this.isUsingProxy = !this.isUsingProxy
// Sauvegarder la position actuelle
const currentTime = this.videoElement.currentTime
// Charger la vidéo appropriée
this.videoElement.src = this.isUsingProxy ? this.proxyVideoPath : this.originalVideoPath
this.videoElement.load()
// Restaurer la position après le chargement
this.videoElement.addEventListener('loadedmetadata', () => {
this.videoElement.currentTime = currentTime
}, { once: true })
},
handleVideoLoaded() {
console.log('Vidéo chargée, dimensions:', this.videoElement.videoWidth, 'x', this.videoElement.videoHeight)
// Vérifier que les dimensions sont valides
if (!this.videoElement.videoWidth || !this.videoElement.videoHeight) {
console.error('Dimensions de vidéo invalides après chargement')
return
}
// Stocker les dimensions si elles ne sont pas déjà définies
if (!this.originalVideoDimensions.width || !this.originalVideoDimensions.height) {
this.originalVideoDimensions = {
width: this.videoElement.videoWidth,
height: this.videoElement.videoHeight
}
}
// Ajouter un log pour indiquer si c'est le proxy ou l'original
const sourceType = this.isUsingProxy ? "proxy" : "originale"
console.log(`Vidéo ${sourceType} chargée. Dimensions d'affichage:`,
this.videoElement.videoWidth, 'x', this.videoElement.videoHeight)
this.initializeView()
// Démarrer l'animation pour le rendu fluide
this.startAnimation()
},
playVideo() {
if (!this.videoElement) {
console.log("Mode test: élément vidéo non disponible")
return
}
this.videoElement.play()
this.startAnimation()
},
pauseVideo() {
if (!this.videoElement) {
console.log("Mode test: élément vidéo non disponible")
return
}
this.videoElement.pause()
},
startAnimation() {
// Arrêter l'animation existante si elle existe
this.stopAnimation()
// Démarrer l'animation
this.animationId = requestAnimationFrame(this.animate)
},
stopAnimation() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
},
initializeView() {
this.$nextTick(() => {
this.updateDimensions()
})
},
handleWindowResize() {
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout)
}
this.resizeTimeout = setTimeout(() => {
this.updateDimensions()
}, 100)
},
updateDimensions() {
const container = this.$refs.container
if (!container) return
const containerWidth = container.clientWidth
const containerHeight = container.clientHeight
// Obtenir les dimensions de la vidéo avec des valeurs par défaut
let videoWidth = 0
let videoHeight = 0
if (this.videoElement && this.videoElement.videoWidth && this.videoElement.videoHeight) {
videoWidth = this.videoElement.videoWidth
videoHeight = this.videoElement.videoHeight
} else if (this.originalVideoDimensions.width && this.originalVideoDimensions.height) {
videoWidth = this.originalVideoDimensions.width
videoHeight = this.originalVideoDimensions.height
} else if (this.imageWidth && this.imageHeight) {
videoWidth = this.imageWidth
videoHeight = this.imageHeight
} else {
// Valeurs par défaut si aucune dimension n'est disponible
videoWidth = 640
videoHeight = 480
}
// Vérifier que les dimensions sont valides
if (!videoWidth || !videoHeight || videoWidth <= 0 || videoHeight <= 0) {
console.warn('Dimensions vidéo invalides, utilisation de valeurs par défaut')
videoWidth = 640
videoHeight = 480
}
const videoRatio = videoWidth / videoHeight
let width = containerWidth
let height = width / videoRatio
if (height > containerHeight) {
height = containerHeight
width = height * videoRatio
}
// S'assurer que les dimensions finales sont valides
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
console.error('Dimensions calculées invalides, utilisation des dimensions du conteneur')
width = containerWidth || 640
height = containerHeight || 480
}
this.stageConfig.width = containerWidth
this.stageConfig.height = containerHeight
this.imageWidth = width
this.imageHeight = height
this.position = {
x: Math.floor((containerWidth - width) / 2),
y: Math.floor((containerHeight - height) / 2)
}
console.log('Dimensions mises à jour:', { videoWidth, videoHeight, displayWidth: width, displayHeight: height })
// Forcer une mise à jour du canvas
if (this.$refs.layer) {
const layer = this.$refs.layer.getNode();
layer.batchDraw();
}
},
selectTool(tool) {
this.currentTool = tool
},
handleMouseDown(e) {
if (e.evt.button !== 0) return
const stage = this.$refs.stage.getStage()
const pointerPos = stage.getPointerPosition()
if (!this.isInsideImage(pointerPos)) return
if (this.currentTool === 'arrow' && this.selectedId) {
const handles = this.getResizeHandles()
const clickedHandle = handles.find(handle => {
const dx = handle.x - pointerPos.x
const dy = handle.y - pointerPos.y
return Math.sqrt(dx * dx + dy * dy) <= 5
})
if (clickedHandle) {
this.resizing = true
return
}
}
// Sélection des rectangles (fonctionne avec tous les outils)
const clickedRect = this.rectangles.find(rect =>
pointerPos.x >= rect.x &&
pointerPos.x <= rect.x + rect.width &&
pointerPos.y >= rect.y &&
pointerPos.y <= rect.y + rect.height
)
if (clickedRect) {
this.selectedId = clickedRect.id
if (this.currentTool === 'arrow') {
this.isDragging = true
this.dragStartPos = pointerPos
}
console.log('Selected rectangle:', this.selectedId)
return
}
// Sélection des points (fonctionne avec tous les outils)
const clickedPoint = this.points.find(point => {
const dx = point.x - pointerPos.x
const dy = point.y - pointerPos.y
return Math.sqrt(dx * dx + dy * dy) <= 8 // Augmenter la zone de détection
})
if (clickedPoint) {
this.selectedId = clickedPoint.id
if (this.currentTool === 'arrow') {
this.isDragging = true
this.dragStartPos = pointerPos
}
console.log('Selected point:', this.selectedId)
return
}
// Déselectionner si aucun élément n'est cliqué
this.selectedId = null
switch(this.currentTool) {
case 'rectangle':
if (!this.annotationStore.selectedObjectId) {
this.annotationStore.addObject()
}
this.isDrawing = true
this.rectangleStart = {
x: pointerPos.x,
y: pointerPos.y
}
this.rectangleSize = { width: 0, height: 0 }
break
case 'positive':
this.addPoint(pointerPos, 'positive')
break
case 'negative':
this.addPoint(pointerPos, 'negative')
break
}
},
handleMouseMove() {
const stage = this.$refs.stage.getStage();
const pointerPos = stage.getPointerPosition();
this.mousePosition = pointerPos;
if (this.isDragging && this.selectedId && this.currentTool === 'arrow') {
const dx = pointerPos.x - this.dragStartPos.x
const dy = pointerPos.y - this.dragStartPos.y
const selectedRect = this.rectangles.find(r => r.id === this.selectedId)
if (selectedRect) {
selectedRect.x += dx
selectedRect.y += dy
}
const selectedPoint = this.points.find(p => p.id === this.selectedId)
if (selectedPoint) {
selectedPoint.x += dx
selectedPoint.y += dy
}
this.dragStartPos = pointerPos
return
}
if (!this.isDrawing || this.currentTool !== 'rectangle') return;
this.rectangleSize = {
width: pointerPos.x - this.rectangleStart.x,
height: pointerPos.y - this.rectangleStart.y
};
},
async handleMouseUp() {
if (this.resizing) {
this.resizing = false;
return;
}
if (this.isDragging) {
this.isDragging = false;
// Mettre à jour la position dans le store après le drag
if (this.selectedId) {
const selectedRect = this.rectangles.find(r => r.id === this.selectedId)
if (selectedRect) {
// Convertir les coordonnées d'affichage en coordonnées réelles
const realX = Math.round((selectedRect.x - this.position.x) * this.scaleX)
const realY = Math.round((selectedRect.y - this.position.y) * this.scaleY)
const realWidth = Math.round(selectedRect.width * this.scaleX)
const realHeight = Math.round(selectedRect.height * this.scaleY)
// Mettre à jour l'annotation dans le store
this.annotationStore.updateAnnotation(this.currentFrameNumber, this.selectedId, {
x: realX,
y: realY,
width: realWidth,
height: realHeight
})
}
const selectedPoint = this.points.find(p => p.id === this.selectedId)
if (selectedPoint) {
// Convertir les coordonnées d'affichage en coordonnées réelles
const realX = Math.round((selectedPoint.x - this.position.x) * this.scaleX)
const realY = Math.round((selectedPoint.y - this.position.y) * this.scaleY)
// Mettre à jour l'annotation dans le store
this.annotationStore.updateAnnotation(this.currentFrameNumber, this.selectedId, {
x: realX,
y: realY
})
}
}
return;
}
if (!this.isDrawing || this.currentTool !== 'rectangle') return;
// Normaliser les coordonnées du rectangle pour s'assurer que x, y est le coin supérieur gauche
// et que width, height sont positifs
let normalizedRect = this.normalizeRectangle(
this.rectangleStart.x,
this.rectangleStart.y,
this.rectangleSize.width,
this.rectangleSize.height
);
const relativeStart = {
x: normalizedRect.x - this.position.x,
y: normalizedRect.y - this.position.y
};
// Utiliser les dimensions réelles de la vidéo originale, pas du proxy
const originalRect = {
x: Math.round(relativeStart.x * this.scaleX),
y: Math.round(relativeStart.y * this.scaleY),
width: Math.round(normalizedRect.width * this.scaleX),
height: Math.round(normalizedRect.height * this.scaleY)
};
// Vérifier s'il existe déjà des annotations pour cet objet sur cette frame
const existingAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber)
.filter(annotation => annotation.objectId === this.annotationStore.selectedObjectId);
// Si des annotations existent déjà, les supprimer avant d'ajouter la nouvelle
if (existingAnnotations.length > 0) {
console.log(`Suppression de ${existingAnnotations.length} annotations existantes pour l'objet ${this.annotationStore.selectedObjectId}`);
existingAnnotations.forEach(annotation => {
this.annotationStore.removeAnnotation(this.currentFrameNumber, annotation.id);
});
}
// Créer l'annotation avec les coordonnées réelles
const annotation = {
objectId: this.annotationStore.selectedObjectId,
type: 'rectangle',
x: originalRect.x,
y: originalRect.y,
width: originalRect.width,
height: originalRect.height
};
// Ajouter l'annotation au store
const annotationId = this.annotationStore.addAnnotation(this.currentFrameNumber, annotation);
// Log détaillé pour le mode annotation uniquement
console.log('Rectangle ajouté à la frame', this.currentFrameNumber, 'avec ID:', annotationId, ':', annotation);
console.log('État actuel des annotations:', JSON.parse(JSON.stringify(this.annotationStore.frameAnnotations)));
this.isDrawing = false;
this.rectangleSize = { width: 0, height: 0 };
},
// Ajouter cette nouvelle méthode pour normaliser les coordonnées du rectangle
normalizeRectangle(x, y, width, height) {
// Si la largeur est négative, ajuster x et width
let newX = x;
let newWidth = width;
if (width < 0) {
newX = x + width;
newWidth = Math.abs(width);
}
// Si la hauteur est négative, ajuster y et height
let newY = y;
let newHeight = height;
if (height < 0) {
newY = y + height;
newHeight = Math.abs(height);
}
return {
x: newX,
y: newY,
width: newWidth,
height: newHeight
};
},
isInsideImage(point) {
return point.x >= this.position.x &&
point.x <= this.position.x + this.imageWidth &&
point.y >= this.position.y &&
point.y <= this.position.y + this.imageHeight
},
addPoint(pos, type) {
if (!this.annotationStore.selectedObjectId) {
this.annotationStore.addObject()
}
const relativeX = pos.x - this.position.x
const relativeY = pos.y - this.position.y
// Utiliser les dimensions réelles de la vidéo originale, pas du proxy
const imageX = Math.round(relativeX * this.scaleX)
const imageY = Math.round(relativeY * this.scaleY)
// Vérifier s'il existe des rectangles pour cet objet sur cette frame
const existingRectangles = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber)
.filter(annotation =>
annotation.objectId === this.annotationStore.selectedObjectId &&
annotation.type === 'rectangle'
);
// Si des rectangles existent, les supprimer avant d'ajouter le point
if (existingRectangles.length > 0) {
console.log(`Suppression de ${existingRectangles.length} rectangles existants pour l'objet ${this.annotationStore.selectedObjectId}`);
existingRectangles.forEach(rectangle => {
this.annotationStore.removeAnnotation(this.currentFrameNumber, rectangle.id);
});
// Supprimer également les masques associés à ces rectangles
const associatedMasks = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber)
.filter(annotation =>
annotation.objectId === this.annotationStore.selectedObjectId &&
annotation.type === 'mask' &&
(!annotation.points || annotation.points.length === 0)
);
associatedMasks.forEach(mask => {
this.annotationStore.removeAnnotation(this.currentFrameNumber, mask.id);
});
}
// Créer directement une annotation pour le point (comme pour les rectangles)
const annotation = {
objectId: this.annotationStore.selectedObjectId,
type: 'point',
x: imageX,
y: imageY,
pointType: type
};
// Ajouter l'annotation au store
const annotationId = this.annotationStore.addAnnotation(this.currentFrameNumber, annotation);
// Log détaillé
console.log('Point ajouté à la frame', this.currentFrameNumber, 'avec ID:', annotationId, ':', annotation);
console.log('État des frameAnnotations après ajout:', JSON.parse(JSON.stringify(this.annotationStore.frameAnnotations)));
},
handleKeyDown(e) {
// Raccourci pour supprimer un élément sélectionné (annotations uniquement)
// Note: Pour supprimer un objet, utiliser Ctrl+Suppr dans la liste des objets
if (e.key === 'Delete' && this.selectedId) {
console.log('Tentative de suppression de l\'élément:', this.selectedId)
// Déterminer le type d'élément sélectionné
const selectedRect = this.rectangles.find(r => r.id === this.selectedId);
const selectedPoint = this.points.find(p => p.id === this.selectedId);
if (selectedRect) {
console.log('Suppression du rectangle:', this.selectedId)
// Supprimer le rectangle
this.annotationStore.removeAnnotation(this.currentFrameNumber, this.selectedId);
// Vérifier s'il reste des annotations pour cet objet sur cette frame
this.checkAndCleanupMasks(selectedRect.objectId);
}
else if (selectedPoint) {
console.log('Suppression du point:', this.selectedId, 'Type:', selectedPoint.isDirect ? 'direct' : 'depuis annotation')
if (selectedPoint.isTemporary) {
// Supprimer un point temporaire
this.annotationStore.removeTemporaryPoint(selectedPoint.id.replace('temp-point-', ''));
} else if (selectedPoint.isDirect) {
// Nouveau système : point direct - supprimer directement l'annotation
this.annotationStore.removeAnnotation(this.currentFrameNumber, this.selectedId);
console.log('Point direct supprimé')
} else {
// Ancien système : point d'une annotation existante
const annotationId = selectedPoint.fromAnnotation;
const annotation = this.annotationStore.getAnnotation(this.currentFrameNumber, annotationId);
if (annotation && annotation.points) {
// Filtrer les points pour retirer celui qui est sélectionné
const pointKey = selectedPoint.id.split('-point-')[1]; // Récupérer les coordonnées du point
const [pointX, pointY] = pointKey.split('-').map(Number);
const updatedPoints = annotation.points.filter(p =>
!(p.x === pointX && p.y === pointY)
);
if (updatedPoints.length > 0) {
// Mettre à jour l'annotation avec les points restants
this.annotationStore.updateAnnotation(this.currentFrameNumber, annotationId, {
points: updatedPoints
});
console.log('Point retiré de l\'annotation, points restants:', updatedPoints.length)
} else {
// Si plus aucun point, supprimer l'annotation complètement
this.annotationStore.removeAnnotation(this.currentFrameNumber, annotationId);
console.log('Annotation complètement supprimée (plus de points)')
}
// Vérifier s'il reste des annotations pour cet objet sur cette frame
this.checkAndCleanupMasks(selectedPoint.objectId);
}
}
} else {
console.log('Aucun élément trouvé avec l\'ID:', this.selectedId)
}
this.selectedId = null;
console.log('Element deleted');
}
// Raccourci "v" supprimé - Les points sont maintenant sauvegardés directement
// Raccourci Escape supprimé - Plus de points temporaires à annuler
// Raccourci pour basculer l'affichage des bounding boxes (touche 'b')
if (e.key === 'b' || e.key === 'B') {
e.preventDefault();
this.toggleBoundingBoxes();
}
// Raccourci pour déboguer les bounding boxes (touche 'd')
if (e.key === 'd' || e.key === 'D') {
e.preventDefault();
this.debugBoundingBoxes();
}
// Navigation frame par frame avec les flèches
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault() // Empêcher le défilement de la page
// Calculer la nouvelle frame
const frameRate = this.annotationStore.currentSession.frameRate || 30
const currentFrame = this.currentFrameNumber
const newFrame = e.key === 'ArrowLeft' ? Math.max(0, currentFrame - 1) : currentFrame + 1
// Calculer le nouveau temps basé sur la frame
const newTime = newFrame / frameRate
// Mettre à jour le temps dans le store et la vidéo
this.videoStore.setCurrentTime(newTime)
if (this.videoElement) {
this.videoElement.currentTime = newTime
}
// console.log(`Navigation: Frame ${currentFrame} -> ${newFrame}, Temps: ${newTime.toFixed(3)}s`)
}
},
// Nouvelle méthode pour vérifier et nettoyer les masques orphelins
checkAndCleanupMasks(objectId) {
const frameAnnotations = this.annotationStore.getAnnotationsForFrame(this.currentFrameNumber) || [];
// Vérifier s'il reste des annotations (rectangles ou points) pour cet objet sur cette frame
const hasRemainingElements = frameAnnotations.some(annotation =>
annotation.objectId === objectId &&
(annotation.type === 'rectangle' ||
(annotation.type === 'mask' && annotation.points && annotation.points.length > 0))
);
if (!hasRemainingElements) {
// Si aucun élément ne reste, supprimer tous les masques associés à cet objet sur cette frame
const masksToRemove = frameAnnotations
.filter(annotation =>
annotation.objectId === objectId &&
annotation.type === 'mask' &&
(!annotation.points || annotation.points.length === 0)
)
.map(annotation => annotation.id);
masksToRemove.forEach(maskId => {
this.annotationStore.removeAnnotation(this.currentFrameNumber, maskId);
});
if (masksToRemove.length > 0) {
console.log(`Suppression de ${masksToRemove.length} masques orphelins pour l'objet ${objectId}`);
}
}
},
getResizeHandles() {
const rect = this.rectangles.find(r => r.id === this.selectedId)
if (!rect) return []
return [
{ position: 'nw', x: rect.x, y: rect.y },
{ position: 'ne', x: rect.x + rect.width, y: rect.y },
{ position: 'se', x: rect.x + rect.width, y: rect.y + rect.height },
{ position: 'sw', x: rect.x, y: rect.y + rect.height },
{ position: 'n', x: rect.x + rect.width/2, y: rect.y },
{ position: 'e', x: rect.x + rect.width, y: rect.y + rect.height/2 },
{ position: 's', x: rect.x + rect.width/2, y: rect.y + rect.height },
{ position: 'w', x: rect.x, y: rect.y + rect.height/2 }
]
},
handleResize(e, position) {
const rect = this.rectangles.find(r => r.id === this.selectedId)
if (!rect) return
const stage = this.$refs.stage.getStage()
const pos = stage.getPointerPosition()
const originalX = rect.x
const originalY = rect.y
const originalWidth = rect.width
const originalHeight = rect.height
let newWidth, newHeight
// Appliquer le redimensionnement selon la poignée utilisée
switch (position) {
case 'e':
rect.width = Math.max(10, pos.x - rect.x)
break
case 'w':
newWidth = originalWidth + (originalX - pos.x)
if (newWidth >= 10) {
rect.x = pos.x
rect.width = newWidth
}
break
case 'n':
newHeight = originalHeight + (originalY - pos.y)
if (newHeight >= 10) {
rect.y = pos.y
rect.height = newHeight
}
break
case 's':
rect.height = Math.max(10, pos.y - rect.y)
break
case 'nw':
if (originalWidth + (originalX - pos.x) >= 10) {
rect.x = pos.x
rect.width = originalWidth + (originalX - pos.x)
}
if (originalHeight + (originalY - pos.y) >= 10) {
rect.y = pos.y
rect.height = originalHeight + (originalY - pos.y)
}
break
case 'ne':
rect.width = Math.max(10, pos.x - rect.x)
if (originalHeight + (originalY - pos.y) >= 10) {
rect.y = pos.y
rect.height = originalHeight + (originalY - pos.y)
}
break
case 'se':
rect.width = Math.max(10, pos.x - rect.x)
rect.height = Math.max(10, pos.y - rect.y)
break
case 'sw':
if (originalWidth + (originalX - pos.x) >= 10) {
rect.x = pos.x
rect.width = originalWidth + (originalX - pos.x)
}
rect.height = Math.max(10, pos.y - rect.y)
break
}
// Convertir les coordonnées d'affichage en coordonnées réelles
const realX = Math.round((rect.x - this.position.x) * this.scaleX)
const realY = Math.round((rect.y - this.position.y) * this.scaleY)
const realWidth = Math.round(rect.width * this.scaleX)
const realHeight = Math.round(rect.height * this.scaleY)
// Mettre à jour l'annotation dans le store avec les coordonnées réelles
this.annotationStore.updateAnnotation(this.currentFrameNumber, this.selectedId, {
x: realX,
y: realY,
width: realWidth,
height: realHeight
})
},
updateCurrentFrame() {
if (!this.videoElement) return
const frameRate = this.annotationStore.currentSession.frameRate || 30
// Utiliser Math.round au lieu de Math.floor pour une meilleure précision
const newFrameNumber = Math.round(this.videoElement.currentTime * frameRate)
// Ne mettre à jour que si la frame a changé
if (newFrameNumber !== this.currentFrameNumber) {
this.currentFrameNumber = newFrameNumber
// Forcer le rafraîchissement du canvas pour s'assurer que seules les annotations
// de la frame actuelle sont affichées
if (this.$refs.layer) {
const layer = this.$refs.layer.getNode()
layer.batchDraw()
}
// Debug des bounding boxes pour la nouvelle frame
this.$nextTick(() => {
this.debugBoundingBoxes()
})
// Log pour débogage
// console.log(`Temps: ${this.videoElement.currentTime.toFixed(3)}s, Frame: ${this.currentFrameNumber}`)
}
},
selectObject(objectId) {
this.annotationStore.selectObject(objectId)
this.$emit('object-selected', objectId)
},
createNewObject() {
this.annotationStore.addObject()
},
animate() {
// Récupérer les éléments sélectionnés
if (this.$refs.layer && this.annotationStore.selectedObjectId) {
const layer = this.$refs.layer.getNode();
// Trouver tous les éléments de l'objet sélectionné
const selectedRects = layer.find('Rect').filter(rect => {
return rect.attrs.objectId === this.annotationStore.selectedObjectId;
});
const selectedPoints = layer.find('Group').filter(group => {
return group.attrs.objectId === this.annotationStore.selectedObjectId;
});
// Appliquer l'animation
[...selectedRects, ...selectedPoints].forEach(shape => {
// Animation de pulsation
const scale = 1 + Math.sin(Date.now() / 300) * 0.05; // Pulsation subtile
shape.scale({ x: scale, y: scale });
});
layer.batchDraw();
}
// Continuer l'animation
this.animationId = requestAnimationFrame(this.animate);
},
getObjectColor(objectId) {
const object = this.annotationStore.objects[objectId];
return object ? object.color : '#CCCCCC';
},
handleMaskClick(maskId, e) {
// Empêcher la propagation pour éviter que handleMouseDown ne soit aussi appelé
if (e && e.evt) {
e.evt.stopPropagation(); // Remplacer cancelBubble par stopPropagation
}
// Sélectionner le masque
this.selectedId = maskId;
console.log('Selected mask:', maskId);
},
drawMask(context, shape, annotation) {
if (!annotation.mask || !annotation.maskImageSize) {
console.warn('Annotation sans masque ou dimensions:', annotation);
return;
}
// Vérifier si le masque est déjà dans le cache
const cacheKey = `${annotation.id}-${annotation.mask.substring(0, 20)}`;
let maskImage = this.maskCache[cacheKey];
if (!maskImage) {
try {
console.log(`Décodage du masque pour l'annotation ${annotation.id}`);
console.log('Début du masque:', annotation.mask.substring(0, 50) + '...');
// Créer une nouvelle image pour charger le masque base64
maskImage = new Image();
// Attendre que l'image soit chargée avant de continuer
const loadPromise = new Promise((resolve, reject) => {
maskImage.onload = () => resolve();
maskImage.onerror = (e) => reject(new Error(`Erreur de chargement de l'image: ${e}`));
});
// Définir la source de l'image (base64)
if (annotation.mask.startsWith('data:')) {
// Si c'est déjà un data URL
maskImage.src = annotation.mask;
} else {
// Sinon, supposer que c'est un base64 brut et créer un data URL
maskImage.src = `data:image/png;base64,${annotation.mask}`;
}
// Attendre que l'image soit chargée
loadPromise.then(() => {
console.log(`Image du masque chargée: ${maskImage.width}x${maskImage.height}`);
// Créer un canvas temporaire pour traiter l'image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = maskImage.width;
tempCanvas.height = maskImage.height;
const tempCtx = tempCanvas.getContext('2d');
// Dessiner l'image sur le canvas temporaire
tempCtx.drawImage(maskImage, 0, 0);
// Obtenir les données de l'image
const imageData = tempCtx.getImageData(0, 0, maskImage.width, maskImage.height);
const data = imageData.data;
// Obtenir la couleur de l'objet
const objectColor = this.getObjectColor(annotation.objectId);
const r = parseInt(objectColor.slice(1, 3), 16);
const g = parseInt(objectColor.slice(3, 5), 16);
const b = parseInt(objectColor.slice(5, 7), 16);
// Parcourir tous les pixels
for (let i = 0; i < data.length; i += 4) {
// Si le pixel est blanc (ou presque blanc)
if (data[i] > 200 && data[i+1] > 200 && data[i+2] > 200) {
// Remplacer par la couleur de l'objet avec une transparence
data[i] = r;
data[i+1] = g;
data[i+2] = b;
data[i+3] = 180; // Semi-transparent
} else {
// Rendre le pixel complètement transparent
data[i+3] = 0;
}
}
// Remettre les données modifiées dans le canvas
tempCtx.putImageData(imageData, 0, 0);
// Créer une nouvelle image à partir du canvas modifié
const coloredMaskImage = new Image();
coloredMaskImage.src = tempCanvas.toDataURL();
// Mettre en cache l'image colorée
this.maskCache[cacheKey] = coloredMaskImage;
// Forcer un nouveau rendu
this.$nextTick(() => {
if (this.$refs.layer) {
this.$refs.layer.getNode().batchDraw();
}
});
}).catch(error => {
console.error('Erreur lors du traitement de l\'image du masque:', error);
});
// Retourner tôt car l'image n'est pas encore chargée
return;
} catch (error) {
console.error('Erreur lors de la création de l\'image du masque:', error);
return;
}
}
// Si l'image n'est pas encore complètement chargée, retourner
if (!maskImage.complete) {
return;
}
// Calculer l'échelle pour adapter le masque à la taille d'affichage
const scaleX = this.imageWidth / annotation.maskImageSize.width;
const scaleY = this.imageHeight / annotation.maskImageSize.height;
// Dessiner le masque sur le canvas principal
const ctx = context._context;
ctx.save();
// Appliquer la transformation pour positionner correctement le masque
ctx.translate(this.position.x, this.position.y);
ctx.scale(scaleX, scaleY);
// Dessiner l'image du masque coloré
ctx.drawImage(maskImage, 0, 0);
// Restaurer le contexte
ctx.restore();
// Indiquer à Konva que le dessin est terminé
shape.strokeEnabled(false); // Désactiver le contour automatique
},
handleShapeMouseDown(e, shapeId) {
// Empêcher la propagation pour éviter que handleMouseDown ne soit aussi appelé
e.evt.stopPropagation(); // Remplacer cancelBubble par stopPropagation
// Sélectionner la forme
this.selectedId = shapeId;
// Si l'outil actuel est la flèche, activer le mode de déplacement
if (this.currentTool === 'arrow') {
this.isDragging = true;
const stage = this.$refs.stage.getStage();
this.dragStartPos = stage.getPointerPosition();
console.log('Selected shape:', shapeId);
}
},
// Ajouter cette méthode pour gérer l'ajout de points à un masque existant
addPointToExistingMask(pos, type) {
if (!this.annotationStore.selectedObjectId) {
this.annotationStore.addObject();
}
const relativeX = pos.x - this.position.x;
const relativeY = pos.y - this.position.y;
// Utiliser les dimensions réelles de la vidéo originale
const imageX = Math.round(relativeX * this.scaleX);
const imageY = Math.round(relativeY * this.scaleY);
// Ajouter le point à la collection temporaire
this.annotationStore.addTemporaryPoint({
objectId: this.annotationStore.selectedObjectId,
x: imageX,
y: imageY,
pointType: type
});
console.log('Point temporaire ajouté:', { x: imageX, y: imageY, type });
},
// Nouvelle méthode pour basculer l'affichage des bounding boxes
toggleBoundingBoxes() {
this.showAllBoundingBoxes = !this.showAllBoundingBoxes;
console.log('Affichage des bounding boxes:', this.showAllBoundingBoxes ? 'activé' : 'désactivé');
},
// Méthode de débogage pour afficher les informations des bounding boxes
debugBoundingBoxes() {
const boxes = this.allBoundingBoxes;
if (boxes.length > 0) {
console.log(`🔍 Frame ${this.currentFrameNumber}: ${boxes.length} bounding box(es) trouvée(s)`);
const currentBoxes = boxes.filter(b => b.source === 'current');
if (currentBoxes.length > 0) {
console.log(` ✏️ Annotations en cours: ${currentBoxes.length}`);
currentBoxes.forEach(box => {
console.log(` - ${box.name} (${box.type}): x=${Math.round(box.x)}, y=${Math.round(box.y)}, w=${Math.round(box.width)}, h=${Math.round(box.height)}`);
});
}
}
},
// Nouvelle méthode pour gérer les clics sur les points
handlePointClick(pointId, e) {
// Empêcher la propagation pour éviter que handleMouseDown ne soit aussi appelé
e.evt.stopPropagation()
// Sélectionner le point
this.selectedId = pointId
// Si l'outil actuel est la flèche, activer le mode de déplacement
if (this.currentTool === 'arrow') {
this.isDragging = true
const stage = this.$refs.stage.getStage()
this.dragStartPos = stage.getPointerPosition()
}
console.log('Selected point via direct click:', pointId)
},
},
watch: {
'stageConfig.width'() {
this.$nextTick(() => {
this.updateDimensions()
})
},
'stageConfig.height'() {
this.$nextTick(() => {
this.updateDimensions()
})
},
'videoStore.currentTime'() {
this.updateCurrentFrame()
},
'annotationStore.selectedObjectId'(newId) {
console.log('Objet sélectionné changé:', newId)
// Redémarrer l'animation quand l'objet sélectionné change
this.startAnimation()
},
currentFrameNumber() {
// Forcer le rafraîchissement du canvas quand la frame change
this.$nextTick(() => {
if (this.$refs.layer) {
const layer = this.$refs.layer.getNode()
layer.batchDraw()
}
})
}
},
}
</script>
<style scoped>
.video-section {
width: 100%;
height: 100%;
display: flex;
gap: 8px;
}
.video-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-wrapper, .canvas-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.video-wrapper {
z-index: 1;
}
.canvas-wrapper {
z-index: 999;
}
.video-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
z-index: 1;
}
.canvas-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background: transparent;
pointer-events: auto;
}
.tool-btn {
width: 34px;
height: 34px;
border: none;
border-radius: 4px;
background: transparent;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.tool-btn:hover {
background: #4a4a4a;
}
.tool-btn.active {
background: #3a3a3a;
color: white;
}
.tool-btn svg {
width: 20px;
height: 20px;
stroke-width: 2;
}
.tool-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: transparent;
}
.tool-btn:not(:disabled):hover {
background: #4a4a4a;
}
.pulse-animation {
animation: pulse 1.5s infinite ease-in-out;
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>