// Select and Mask workspace.
//
// A full-area overlay that lets the user refine the current selection with
// the same feather/expand/smooth/border controls as Refine Selection, plus a
// quick brush to add to or subtract from the selection directly, and an
// output-to choice (selection, layer mask, new layer, new layer with mask).
//
// The module owns only the overlay + a working document-space mask canvas; it
// never mutates editor state itself. The caller supplies `refine` (the shared
// refineSelectionMask) and receives the finished mask through `onCommit`.
import { refineSelectionMask } from './selection-mask.js';
const REFINE_KEYS = [
{ key: 'feather', label: 'Feather', min: 0, max: 100, value: 0, suffix: 'px' },
{ key: 'expand', label: 'Expand / Contract', min: -100, max: 100, value: 0, suffix: 'px' },
{ key: 'smooth', label: 'Smooth', min: 0, max: 50, value: 0, suffix: 'px' },
{ key: 'border', label: 'Border', min: 0, max: 100, value: 0, suffix: 'px' },
];
export function openSelectAndMask({
mask = null,
width = 0,
height = 0,
refine = refineSelectionMask,
onCommit,
onCancel,
} = {}) {
const docW = width || mask?.width || 512;
const docH = height || mask?.height || 512;
const work = document.createElement('canvas');
work.width = docW;
work.height = docH;
const workCtx = work.getContext('2d');
if (mask) workCtx.drawImage(mask, 0, 0, docW, docH);
const params = { feather: 0, expand: 0, smooth: 0, border: 0 };
let brushSize = Math.max(8, Math.round(Math.min(docW, docH) / 8));
let brushMode = 'add';
let painting = false;
const overlay = document.createElement('div');
overlay.className = 'ge-sam-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'Select and Mask');
const refineRows = REFINE_KEYS.map((k) => `
`).join('');
const outputOptions = [
['selection', 'Selection'],
['mask', 'Layer Mask'],
['newLayer', 'New Layer'],
['newLayerWithMask', 'New Layer with Mask'],
].map(([v, l]) => ``).join('');
overlay.innerHTML = `
Select and Mask
${refineRows}
`;
document.body.appendChild(overlay);
const preview = overlay.querySelector('.ge-sam-preview');
const previewCtx = preview.getContext('2d');
const fit = Math.min(1, 320 / docW, 260 / docH);
preview.width = Math.max(1, Math.round(docW * fit));
preview.height = Math.max(1, Math.round(docH * fit));
const refined = () => (typeof refine === 'function' ? refine(work, { ...params }) : work);
const render = () => {
const out = refined();
previewCtx.clearRect(0, 0, preview.width, preview.height);
previewCtx.fillStyle = '#11151b';
previewCtx.fillRect(0, 0, preview.width, preview.height);
previewCtx.drawImage(out, 0, 0, preview.width, preview.height);
};
const setValueLabel = (key, value, suffix = '') => {
const el = overlay.querySelector(`[data-sam-value="${key}"]`);
if (el) el.textContent = `${value}${suffix}`;
};
for (const k of REFINE_KEYS) {
const input = overlay.querySelector(`[data-sam-range="${k.key}"]`);
input?.addEventListener('input', () => {
params[k.key] = parseInt(input.value, 10);
setValueLabel(k.key, params[k.key], k.suffix);
render();
});
}
const brushSizeInput = overlay.querySelector('[data-sam-brush-size]');
brushSizeInput?.addEventListener('input', () => {
brushSize = parseInt(brushSizeInput.value, 10);
setValueLabel('brushSize', brushSize, 'px');
});
overlay.querySelectorAll('[data-sam-brush]').forEach((btn) => {
btn.addEventListener('click', () => {
brushMode = btn.dataset.samBrush;
overlay.querySelectorAll('[data-sam-brush]').forEach((b) => b.classList.toggle('active', b === btn));
});
});
const paintAt = (event) => {
const rect = preview.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width * docW;
const y = (event.clientY - rect.top) / rect.height * docH;
const radius = brushSize / 2;
workCtx.save();
workCtx.globalCompositeOperation = brushMode === 'subtract' ? 'destination-out' : 'source-over';
workCtx.fillStyle = '#ffffff';
workCtx.beginPath();
workCtx.arc(x, y, radius, 0, Math.PI * 2);
workCtx.fill();
workCtx.restore();
render();
};
preview.addEventListener('pointerdown', (e) => {
painting = true;
preview.setPointerCapture?.(e.pointerId);
paintAt(e);
});
preview.addEventListener('pointermove', (e) => {
if (painting) paintAt(e);
});
const endPaint = () => { painting = false; };
preview.addEventListener('pointerup', endPaint);
preview.addEventListener('pointercancel', endPaint);
preview.addEventListener('pointerleave', endPaint);
const close = () => {
document.removeEventListener('keydown', onKeydown);
overlay.remove();
};
const outputSel = overlay.querySelector('.ge-sam-output');
overlay.querySelector('.ge-sam-cancel')?.addEventListener('click', () => {
onCancel?.();
close();
});
overlay.querySelector('.ge-sam-apply')?.addEventListener('click', () => {
const finalMask = refined();
onCommit?.({ mask: finalMask, output: outputSel?.value || 'selection' });
close();
});
function onKeydown(event) {
if (event.key === 'Escape') {
event.preventDefault();
onCancel?.();
close();
} else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
overlay.querySelector('.ge-sam-apply')?.click();
}
}
document.addEventListener('keydown', onKeydown);
render();
return { close, getMask: () => refined() };
}