From bd6fd225226733764aa8b52e8778c031c9be6b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Fri, 21 Nov 2025 20:28:02 +0100 Subject: [PATCH] feat(phase-13): implement selection refinement operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive selection refinement tools for precise selection editing. Features: - Expand selection by N pixels using dilation algorithm - Contract selection by N pixels using erosion algorithm - Feather selection with Gaussian blur - Invert selection (already existed) - All operations work on selection mask data - Morphological operations: * Expand: Dilate mask by checking max neighbor values * Contract: Erode mask by checking min neighbor values * Feather: Apply separable Gaussian blur (horizontal + vertical) Changes: - Updated store/selection-store.ts with three new functions: * expandSelection(pixels) - Dilate selection * contractSelection(pixels) - Erode selection * featherSelection(radius) - Gaussian blur - Implements proper image processing algorithms - Works on Uint8Array mask data - Updates feather property in selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- store/selection-store.ts | 165 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/store/selection-store.ts b/store/selection-store.ts index 8aaf8b6..4eef910 100644 --- a/store/selection-store.ts +++ b/store/selection-store.ts @@ -16,6 +16,9 @@ interface SelectionStore extends SelectionState { clearSelection: () => void; selectAll: () => void; invertSelection: () => void; + expandSelection: (pixels: number) => void; + contractSelection: (pixels: number) => void; + featherSelection: (radius: number) => void; } export const useSelectionStore = create((set) => ({ @@ -107,4 +110,166 @@ export const useSelectionStore = create((set) => ({ } return state; }), + + expandSelection: (pixels) => + set((state) => { + if (!state.activeSelection) return state; + + const { mask } = state.activeSelection; + const { width, height, data } = mask; + const newData = new Uint8Array(data.length); + + // Expand selection by dilating the mask + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + let maxValue = data[idx]; + + // Check neighbors within radius + for (let dy = -pixels; dy <= pixels; dy++) { + for (let dx = -pixels; dx <= pixels; dx++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + const nidx = ny * width + nx; + if (data[nidx] > maxValue) { + maxValue = data[nidx]; + } + } + } + } + + newData[idx] = maxValue; + } + } + + return { + activeSelection: { + ...state.activeSelection, + mask: { + ...mask, + data: newData, + }, + }, + }; + }), + + contractSelection: (pixels) => + set((state) => { + if (!state.activeSelection) return state; + + const { mask } = state.activeSelection; + const { width, height, data } = mask; + const newData = new Uint8Array(data.length); + + // Contract selection by eroding the mask + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + let minValue = data[idx]; + + // Check neighbors within radius + for (let dy = -pixels; dy <= pixels; dy++) { + for (let dx = -pixels; dx <= pixels; dx++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + const nidx = ny * width + nx; + if (data[nidx] < minValue) { + minValue = data[nidx]; + } + } + } + } + + newData[idx] = minValue; + } + } + + return { + activeSelection: { + ...state.activeSelection, + mask: { + ...mask, + data: newData, + }, + }, + }; + }), + + featherSelection: (radius) => + set((state) => { + if (!state.activeSelection) return state; + + const { mask } = state.activeSelection; + const { width, height, data } = mask; + const newData = new Uint8Array(data.length); + + // Apply Gaussian blur for feathering + const sigma = radius / 3; + const kernelSize = Math.ceil(radius * 2) + 1; + const kernel: number[] = []; + let kernelSum = 0; + + // Generate Gaussian kernel + for (let i = 0; i < kernelSize; i++) { + const x = i - Math.floor(kernelSize / 2); + const value = Math.exp(-(x * x) / (2 * sigma * sigma)); + kernel.push(value); + kernelSum += value; + } + + // Normalize kernel + for (let i = 0; i < kernel.length; i++) { + kernel[i] /= kernelSum; + } + + // Horizontal pass + const temp = new Uint8Array(data.length); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let sum = 0; + const halfKernel = Math.floor(kernelSize / 2); + + for (let k = 0; k < kernelSize; k++) { + const sx = x + k - halfKernel; + if (sx >= 0 && sx < width) { + sum += data[y * width + sx] * kernel[k]; + } + } + + temp[y * width + x] = Math.round(sum); + } + } + + // Vertical pass + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let sum = 0; + const halfKernel = Math.floor(kernelSize / 2); + + for (let k = 0; k < kernelSize; k++) { + const sy = y + k - halfKernel; + if (sy >= 0 && sy < height) { + sum += temp[sy * width + x] * kernel[k]; + } + } + + newData[y * width + x] = Math.round(sum); + } + } + + return { + activeSelection: { + ...state.activeSelection, + mask: { + ...mask, + data: newData, + }, + feather: radius, + }, + }; + }), }));