/** * Inline Detection View - Show all detected embryos on the main image */ import { appState, setCurrentEmbryoIndex, getEmbryoRemark } from '../state.js'; import { loadImage } from '../utils/imageUtils.js'; import { showToast } from './toast.js'; import { showConfirmModal } from './modal.js'; let canvas, ctx; let selectedEmbryoIndex = null; let isDragging = false; let dragType = null; // 'move', 'nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w' let dragStart = { x: 0, y: 0 }; let originalBox = null; let imageElement = null; let scaleFactor = 1; let offsetX = 0; let offsetY = 0; const HANDLE_SIZE = 10; const COLORS = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B739', '#52B788' ]; /** * Initialize the inline detection canvas */ export async function initializeInlineDetection(imageData, detections) { canvas = document.getElementById('detectionCanvas'); if (!canvas) { console.error('Detection canvas not found'); showToast('Failed to initialize detection view', 'error'); return; } ctx = canvas.getContext('2d'); if (!ctx) { console.error('Failed to get canvas context'); showToast('Canvas not supported', 'error'); return; } // Load the original image imageElement = await loadImage(imageData); // Set canvas size to match image canvas.width = imageElement.width; canvas.height = imageElement.height; // Calculate scale factor for display const container = canvas.parentElement; if (!container) { console.error('Canvas container not found'); return; } const maxWidth = container.clientWidth - 40; const maxHeight = 600; scaleFactor = Math.min( maxWidth / imageElement.width, maxHeight / imageElement.height, 1 ); canvas.style.width = `${imageElement.width * scaleFactor}px`; canvas.style.height = `${imageElement.height * scaleFactor}px`; // Draw initial view drawDetections(); // Add event listeners canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); canvas.addEventListener('mouseleave', handleMouseUp); // Touch events for mobile canvas.addEventListener('touchstart', handleTouchStart); canvas.addEventListener('touchmove', handleTouchMove); canvas.addEventListener('touchend', handleTouchEnd); // Update embryo list updateEmbryoList(detections); // Setup detection info with manual add button const info = document.getElementById('detectionInfo'); if (info) { info.innerHTML = `
Click on an embryo to adjust its crop area
`; } // Setup manual add button setupManualAddButton(); } /** * Draw all detections on the canvas */ function drawDetections() { if (!imageElement || !ctx) return; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the original image ctx.drawImage(imageElement, 0, 0, canvas.width, canvas.height); // Draw all bounding boxes appState.croppedEmbryos.forEach((detection, index) => { const isSelected = index === selectedEmbryoIndex; const color = COLORS[index % COLORS.length]; drawBoundingBox(detection.box, color, isSelected, index); }); } /** * Draw a single bounding box */ function drawBoundingBox(box, color, isSelected, index) { const lineWidth = isSelected ? 3 : 2; const alpha = isSelected ? 1 : 0.7; // Draw box ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.globalAlpha = alpha; ctx.strokeRect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1); // Draw label background const label = `Embryo ${index + 1}`; ctx.font = '14px Arial'; const textWidth = ctx.measureText(label).width; const labelHeight = 20; ctx.fillStyle = color; ctx.globalAlpha = 0.9; ctx.fillRect(box.x1, box.y1 - labelHeight, textWidth + 10, labelHeight); // Draw label text ctx.fillStyle = 'white'; ctx.globalAlpha = 1; ctx.fillText(label, box.x1 + 5, box.y1 - 5); // Draw resize handles if selected if (isSelected) { drawResizeHandles(box, color); } ctx.globalAlpha = 1; } /** * Draw resize handles on the selected box */ function drawResizeHandles(box, color) { const handles = [ { x: box.x1, y: box.y1, type: 'nw' }, { x: box.x2, y: box.y1, type: 'ne' }, { x: box.x1, y: box.y2, type: 'sw' }, { x: box.x2, y: box.y2, type: 'se' }, { x: (box.x1 + box.x2) / 2, y: box.y1, type: 'n' }, { x: (box.x1 + box.x2) / 2, y: box.y2, type: 's' }, { x: box.x1, y: (box.y1 + box.y2) / 2, type: 'w' }, { x: box.x2, y: (box.y1 + box.y2) / 2, type: 'e' } ]; ctx.fillStyle = color; handles.forEach(handle => { ctx.fillRect( handle.x - HANDLE_SIZE / 2, handle.y - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE ); }); } /** * Get mouse position relative to canvas */ function getMousePos(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; } /** * Get handle at position */ function getHandleAtPosition(box, x, y) { const handles = [ { x: box.x1, y: box.y1, type: 'nw' }, { x: box.x2, y: box.y1, type: 'ne' }, { x: box.x1, y: box.y2, type: 'sw' }, { x: box.x2, y: box.y2, type: 'se' }, { x: (box.x1 + box.x2) / 2, y: box.y1, type: 'n' }, { x: (box.x1 + box.x2) / 2, y: box.y2, type: 's' }, { x: box.x1, y: (box.y1 + box.y2) / 2, type: 'w' }, { x: box.x2, y: (box.y1 + box.y2) / 2, type: 'e' } ]; for (const handle of handles) { const dist = Math.sqrt( Math.pow(x - handle.x, 2) + Math.pow(y - handle.y, 2) ); if (dist < HANDLE_SIZE) { return handle.type; } } return null; } /** * Check if point is inside a box */ function isPointInBox(box, x, y) { return x >= box.x1 && x <= box.x2 && y >= box.y1 && y <= box.y2; } /** * Handle mouse down */ function handleMouseDown(e) { const pos = getMousePos(e); // If in manual draw mode, start drawing new box if (isDrawingNewBox) { newBoxStart = pos; return; } // Check if clicking on selected box handles first if (selectedEmbryoIndex !== null) { const box = appState.croppedEmbryos[selectedEmbryoIndex].box; const handle = getHandleAtPosition(box, pos.x, pos.y); if (handle) { isDragging = true; dragType = handle; dragStart = pos; originalBox = { ...box }; return; } // Check if clicking inside selected box to move it if (isPointInBox(box, pos.x, pos.y)) { isDragging = true; dragType = 'move'; dragStart = pos; originalBox = { ...box }; return; } } // Check if clicking on any box to select it for (let i = appState.croppedEmbryos.length - 1; i >= 0; i--) { const box = appState.croppedEmbryos[i].box; if (isPointInBox(box, pos.x, pos.y)) { selectEmbryo(i); return; } } // Clicked outside all boxes - deselect selectedEmbryoIndex = null; drawDetections(); updateEmbryoList(appState.croppedEmbryos); // Reset detection info with button const info = document.getElementById('detectionInfo'); if (info) { info.innerHTML = `Click on an embryo to adjust its crop area
`; setupManualAddButton(); } } /** * Handle mouse move */ function handleMouseMove(e) { const pos = getMousePos(e); // If drawing new box, show preview if (isDrawingNewBox && newBoxStart) { drawDetections(); // Draw preview box ctx.strokeStyle = '#00B8D4'; ctx.lineWidth = 3; ctx.setLineDash([5, 5]); ctx.strokeRect( newBoxStart.x, newBoxStart.y, pos.x - newBoxStart.x, pos.y - newBoxStart.y ); ctx.setLineDash([]); return; } if (!isDragging || selectedEmbryoIndex === null) { // Update cursor based on hover updateCursor(pos); return; } const dx = pos.x - dragStart.x; const dy = pos.y - dragStart.y; const box = appState.croppedEmbryos[selectedEmbryoIndex].box; if (dragType === 'move') { // Move the entire box box.x1 = Math.max(0, Math.min(originalBox.x1 + dx, canvas.width - (originalBox.x2 - originalBox.x1))); box.y1 = Math.max(0, Math.min(originalBox.y1 + dy, canvas.height - (originalBox.y2 - originalBox.y1))); box.x2 = box.x1 + (originalBox.x2 - originalBox.x1); box.y2 = box.y1 + (originalBox.y2 - originalBox.y1); } else { // Resize the box based on handle resizeBox(box, originalBox, dx, dy, dragType); } drawDetections(); updateEmbryoCrop(selectedEmbryoIndex); } /** * Resize box based on drag type */ function resizeBox(box, original, dx, dy, type) { const minSize = 30; switch (type) { case 'nw': box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize)); box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize)); break; case 'ne': box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize)); box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize)); break; case 'sw': box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize)); box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize)); break; case 'se': box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize)); box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize)); break; case 'n': box.y1 = Math.max(0, Math.min(original.y1 + dy, original.y2 - minSize)); break; case 's': box.y2 = Math.min(canvas.height, Math.max(original.y2 + dy, original.y1 + minSize)); break; case 'w': box.x1 = Math.max(0, Math.min(original.x1 + dx, original.x2 - minSize)); break; case 'e': box.x2 = Math.min(canvas.width, Math.max(original.x2 + dx, original.x1 + minSize)); break; } } /** * Update cursor based on position */ function updateCursor(pos) { // If in drawing mode, show crosshair cursor if (isDrawingNewBox) { canvas.style.cursor = 'crosshair'; return; } if (selectedEmbryoIndex !== null) { const box = appState.croppedEmbryos[selectedEmbryoIndex].box; const handle = getHandleAtPosition(box, pos.x, pos.y); if (handle) { const cursors = { 'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize', 'n': 'n-resize', 's': 's-resize', 'w': 'w-resize', 'e': 'e-resize' }; canvas.style.cursor = cursors[handle]; return; } if (isPointInBox(box, pos.x, pos.y)) { canvas.style.cursor = 'move'; return; } } // Check if over any box for (const detection of appState.croppedEmbryos) { if (isPointInBox(detection.box, pos.x, pos.y)) { canvas.style.cursor = 'pointer'; return; } } canvas.style.cursor = 'default'; } /** * Handle mouse up */ function handleMouseUp(e) { // If finishing drawing new box if (isDrawingNewBox && newBoxStart) { const pos = getMousePos(e); const newBox = { x1: newBoxStart.x, y1: newBoxStart.y, x2: pos.x, y2: pos.y }; completeManualCrop(newBox); return; } if (isDragging && selectedEmbryoIndex !== null) { // Finalize the crop update updateEmbryoCrop(selectedEmbryoIndex); showToast('Crop area updated', 'success'); } isDragging = false; dragType = null; } /** * Handle touch events */ function handleTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); } function handleTouchMove(e) { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); } function handleTouchEnd(e) { e.preventDefault(); // Get the last touch position or use changedTouches const touch = e.changedTouches[0]; const mouseEvent = new MouseEvent('mouseup', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); } /** * Select an embryo */ function selectEmbryo(index) { selectedEmbryoIndex = index; setCurrentEmbryoIndex(index); drawDetections(); updateEmbryoList(appState.croppedEmbryos); const info = document.getElementById('detectionInfo'); if (info) { info.innerHTML = `Embryo ${index + 1} selected - Drag to move, drag handles to resize
`; setupManualAddButton(); } } /** * Update embryo crop after modification */ async function updateEmbryoCrop(index) { const detection = appState.croppedEmbryos[index]; const box = detection.box; // Create a temporary canvas to crop the image const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); const width = box.x2 - box.x1; const height = box.y2 - box.y1; tempCanvas.width = width; tempCanvas.height = height; // Draw the cropped portion tempCtx.drawImage( imageElement, box.x1, box.y1, width, height, 0, 0, width, height ); // Update the detection's imageData detection.imageData = tempCanvas.toDataURL('image/png'); // Update the list updateEmbryoList(appState.croppedEmbryos); } /** * Update the embryo list in the sidebar */ function updateEmbryoList(detections) { const listContainer = document.getElementById('embryoList'); if (!listContainer) return; listContainer.innerHTML = ''; detections.forEach((detection, index) => { const card = document.createElement('div'); card.className = 'embryo-card'; if (index === selectedEmbryoIndex) { card.classList.add('selected'); } const hasRemark = getEmbryoRemark(index).trim().length > 0; card.innerHTML = `Draw a box around the embryo
Click and drag on the image to create a new bounding box
`; const cancelBtn = document.getElementById('cancelManualAdd'); if (cancelBtn) { cancelBtn.addEventListener('click', cancelManualCrop); } } drawDetections(); showToast('Draw a box around the embryo', 'info'); } /** * Cancel manual crop mode */ function cancelManualCrop() { isDrawingNewBox = false; newBoxStart = null; const detectionInfo = document.getElementById('detectionInfo'); if (detectionInfo) { detectionInfo.innerHTML = `Click on an embryo to adjust its crop area
`; setupManualAddButton(); } drawDetections(); } /** * Complete manual crop and add embryo */ async function completeManualCrop(box) { // Validate box size const minSize = 50; const boxWidth = Math.abs(box.x2 - box.x1); const boxHeight = Math.abs(box.y2 - box.y1); if (boxWidth < minSize || boxHeight < minSize) { showToast('Box too small. Please draw a larger area.', 'warning'); return; } // Normalize box coordinates const normalizedBox = { x1: Math.min(box.x1, box.x2), y1: Math.min(box.y1, box.y2), x2: Math.max(box.x1, box.x2), y2: Math.max(box.y1, box.y2) }; // Crop the embryo from the original image const croppedCanvas = document.createElement('canvas'); const croppedCtx = croppedCanvas.getContext('2d'); croppedCanvas.width = normalizedBox.x2 - normalizedBox.x1; croppedCanvas.height = normalizedBox.y2 - normalizedBox.y1; croppedCtx.drawImage( imageElement, normalizedBox.x1, normalizedBox.y1, croppedCanvas.width, croppedCanvas.height, 0, 0, croppedCanvas.width, croppedCanvas.height ); const croppedImageData = croppedCanvas.toDataURL(); // Add to embryos array const newEmbryo = { imageData: croppedImageData, box: normalizedBox, confidence: 1.0, // Manual additions have 100% confidence isManual: true }; appState.croppedEmbryos.push(newEmbryo); // Reset manual mode isDrawingNewBox = false; newBoxStart = null; // Update UI cancelManualCrop(); drawDetections(); updateEmbryoList(appState.croppedEmbryos); // Enable next button if this is the first embryo if (appState.croppedEmbryos.length > 0) { import('./stepper.js').then(module => { module.enableNextButton(2); }); } showToast('Embryo added successfully', 'success'); } /** * Export functions */ export function getSelectedEmbryoIndex() { return selectedEmbryoIndex; } export function redrawDetections() { drawDetections(); }