319 lines
8.2 KiB
JavaScript
319 lines
8.2 KiB
JavaScript
/**
|
|
* Annotation Overlay Module
|
|
*
|
|
* Creates and manages the overlay canvas that sits above the LiteGraph node editor.
|
|
* Handles:
|
|
* - Overlay DOM element creation and positioning
|
|
* - High-DPI canvas scaling for crisp rendering
|
|
* - Resize observation and canvas dimension updates
|
|
* - Pointer events mode switching (draw/erase vs pass-through)
|
|
*/
|
|
|
|
const OVERLAY_ID = 'annotation-overlay';
|
|
const CANVAS_ID = 'annotation-canvas';
|
|
|
|
// State
|
|
let overlayElement = null;
|
|
let canvasElement = null;
|
|
let canvasContext = null;
|
|
let resizeObserver = null;
|
|
let litegraphCanvas = null;
|
|
|
|
// Callbacks
|
|
let onResizeCallback = null;
|
|
|
|
/**
|
|
* Creates the overlay container and drawing canvas.
|
|
* @param {HTMLElement} container - The parent element (canvas.parentElement)
|
|
* @returns {{ overlay: HTMLDivElement, canvas: HTMLCanvasElement }}
|
|
*/
|
|
export function createOverlay(container) {
|
|
if (overlayElement) {
|
|
console.warn('[Annotations] Overlay already exists');
|
|
return { overlay: overlayElement, canvas: canvasElement };
|
|
}
|
|
|
|
// Create overlay container with inline styles (ensures visibility)
|
|
// z-index 10 keeps overlay above LiteGraph canvas but below ComfyUI's main UI
|
|
// (sidebars, menus, dialogs typically have z-index 100+)
|
|
overlayElement = document.createElement('div');
|
|
overlayElement.id = OVERLAY_ID;
|
|
overlayElement.className = 'annotation-overlay';
|
|
overlayElement.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 10;
|
|
overflow: hidden;
|
|
pointer-events: none;
|
|
`;
|
|
|
|
// Create drawing canvas with inline styles
|
|
canvasElement = document.createElement('canvas');
|
|
canvasElement.id = CANVAS_ID;
|
|
canvasElement.className = 'annotation-canvas';
|
|
canvasElement.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: inherit;
|
|
touch-action: none;
|
|
`;
|
|
|
|
overlayElement.appendChild(canvasElement);
|
|
container.appendChild(overlayElement);
|
|
|
|
console.log('[Annotations] Overlay element created:', overlayElement);
|
|
console.log('[Annotations] Canvas element created:', canvasElement);
|
|
console.log('[Annotations] Container:', container);
|
|
|
|
// Get 2D context with optimizations
|
|
canvasContext = canvasElement.getContext('2d', {
|
|
alpha: true,
|
|
desynchronized: true, // Reduces latency on supported browsers
|
|
willReadFrequently: false
|
|
});
|
|
|
|
// Find and store reference to LiteGraph canvas
|
|
litegraphCanvas = container.querySelector('canvas:not(#' + CANVAS_ID + ')');
|
|
|
|
// Initial sizing
|
|
updateCanvasSize();
|
|
|
|
// Setup resize observer
|
|
setupResizeObserver(container);
|
|
|
|
// Default to pass-through mode (annotations OFF)
|
|
setPointerEventsMode('none');
|
|
|
|
console.log('[Annotations] Overlay created');
|
|
|
|
return { overlay: overlayElement, canvas: canvasElement };
|
|
}
|
|
|
|
/**
|
|
* Updates canvas dimensions to match container, accounting for devicePixelRatio.
|
|
* This ensures crisp rendering on high-DPI displays.
|
|
*/
|
|
export function updateCanvasSize() {
|
|
if (!canvasElement || !overlayElement) return;
|
|
|
|
const rect = overlayElement.getBoundingClientRect();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
|
|
// Set display size (CSS pixels)
|
|
const displayWidth = rect.width;
|
|
const displayHeight = rect.height;
|
|
|
|
// Set actual canvas buffer size (physical pixels)
|
|
const bufferWidth = Math.floor(displayWidth * dpr);
|
|
const bufferHeight = Math.floor(displayHeight * dpr);
|
|
|
|
// Only resize if dimensions actually changed
|
|
if (canvasElement.width !== bufferWidth || canvasElement.height !== bufferHeight) {
|
|
canvasElement.width = bufferWidth;
|
|
canvasElement.height = bufferHeight;
|
|
|
|
// Scale context to account for DPR
|
|
// This means we can draw in CSS pixel coordinates
|
|
canvasContext.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
console.log(`[Annotations] Canvas resized: ${displayWidth}x${displayHeight} (buffer: ${bufferWidth}x${bufferHeight}, DPR: ${dpr})`);
|
|
|
|
// Notify listeners
|
|
if (onResizeCallback) {
|
|
onResizeCallback(displayWidth, displayHeight, dpr);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up a ResizeObserver to track container size changes.
|
|
* @param {HTMLElement} container
|
|
*/
|
|
function setupResizeObserver(container) {
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect();
|
|
}
|
|
|
|
resizeObserver = new ResizeObserver((entries) => {
|
|
// Debounce rapid resize events
|
|
requestAnimationFrame(() => {
|
|
updateCanvasSize();
|
|
});
|
|
});
|
|
|
|
resizeObserver.observe(container);
|
|
|
|
// Also listen for window resize as fallback
|
|
window.addEventListener('resize', handleWindowResize);
|
|
}
|
|
|
|
/**
|
|
* Window resize handler (fallback for ResizeObserver)
|
|
*/
|
|
function handleWindowResize() {
|
|
requestAnimationFrame(() => {
|
|
updateCanvasSize();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets the pointer-events mode on the overlay.
|
|
* @param {'none' | 'auto'} mode - 'none' passes through, 'auto' captures events
|
|
*/
|
|
export function setPointerEventsMode(mode) {
|
|
if (!overlayElement) return;
|
|
|
|
overlayElement.style.pointerEvents = mode;
|
|
|
|
// Update cursor based on mode
|
|
if (mode === 'none') {
|
|
canvasElement.style.cursor = 'default';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the cursor style for the canvas.
|
|
* @param {string} cursor - CSS cursor value
|
|
*/
|
|
export function setCursor(cursor) {
|
|
if (canvasElement) {
|
|
canvasElement.style.cursor = cursor;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows the overlay (makes it visible).
|
|
*/
|
|
export function showOverlay() {
|
|
if (overlayElement) {
|
|
overlayElement.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hides the overlay (display: none).
|
|
*/
|
|
export function hideOverlay() {
|
|
if (overlayElement) {
|
|
overlayElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if overlay is currently visible.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isOverlayVisible() {
|
|
return overlayElement?.style.display !== 'none';
|
|
}
|
|
|
|
/**
|
|
* Gets the 2D rendering context.
|
|
* @returns {CanvasRenderingContext2D | null}
|
|
*/
|
|
export function getContext() {
|
|
return canvasContext;
|
|
}
|
|
|
|
/**
|
|
* Gets the canvas element.
|
|
* @returns {HTMLCanvasElement | null}
|
|
*/
|
|
export function getCanvas() {
|
|
return canvasElement;
|
|
}
|
|
|
|
/**
|
|
* Gets the overlay container element.
|
|
* @returns {HTMLDivElement | null}
|
|
*/
|
|
export function getOverlay() {
|
|
return overlayElement;
|
|
}
|
|
|
|
/**
|
|
* Gets the LiteGraph canvas element.
|
|
* @returns {HTMLCanvasElement | null}
|
|
*/
|
|
export function getLitegraphCanvas() {
|
|
return litegraphCanvas;
|
|
}
|
|
|
|
/**
|
|
* Gets canvas dimensions in CSS pixels.
|
|
* @returns {{ width: number, height: number, dpr: number }}
|
|
*/
|
|
export function getCanvasDimensions() {
|
|
if (!canvasElement) {
|
|
return { width: 0, height: 0, dpr: 1 };
|
|
}
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
|
return {
|
|
width: canvasElement.width / dpr,
|
|
height: canvasElement.height / dpr,
|
|
dpr
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Registers a callback for resize events.
|
|
* @param {(width: number, height: number, dpr: number) => void} callback
|
|
*/
|
|
export function onResize(callback) {
|
|
onResizeCallback = callback;
|
|
}
|
|
|
|
/**
|
|
* Clears the entire canvas.
|
|
*/
|
|
export function clearCanvas() {
|
|
if (!canvasContext || !canvasElement) return;
|
|
|
|
// Reset to identity transform and clear
|
|
canvasContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height);
|
|
}
|
|
|
|
/**
|
|
* Destroys the overlay and cleans up resources.
|
|
*/
|
|
export function destroyOverlay() {
|
|
// Remove resize observer
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect();
|
|
resizeObserver = null;
|
|
}
|
|
|
|
// Remove window listener
|
|
window.removeEventListener('resize', handleWindowResize);
|
|
|
|
// Remove DOM elements
|
|
if (overlayElement && overlayElement.parentElement) {
|
|
overlayElement.parentElement.removeChild(overlayElement);
|
|
}
|
|
|
|
// Clear references
|
|
overlayElement = null;
|
|
canvasElement = null;
|
|
canvasContext = null;
|
|
litegraphCanvas = null;
|
|
onResizeCallback = null;
|
|
|
|
console.log('[Annotations] Overlay destroyed');
|
|
}
|
|
|
|
/**
|
|
* Checks if overlay has been initialized.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isInitialized() {
|
|
return overlayElement !== null && canvasElement !== null;
|
|
}
|