feat(phase-13): implement selection refinement operations
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<SelectionStore>((set) => ({
|
||||
@@ -107,4 +110,166 @@ export const useSelectionStore = create<SelectionStore>((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,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user