Add advanced editing features: layers, background removal, smart selection
Backend: - Add /tools router with background removal, smart select, color select - Add rembg dependency for AI background removal - Add layer management API (list, flatten) - Fix transparency preservation in blend_patch (veil collapse fix) - Preserve alpha channel when reverting/resetting images Frontend: - Add AdvancedTools panel with background removal, smart select, color select - Add Layers panel with drag-to-reorder, visibility toggle, flatten - Add toolsApi for new backend endpoints - Make right panel scrollable for additional controls This adds "Photoshop light" capabilities: - Remove background and create layer - Smart object selection (click to select) - Color selection with tolerance - Layer system with compositing
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { toolsApi } from '../utils/api';
|
||||
import './Layers.css';
|
||||
|
||||
const Layers = ({
|
||||
projectId,
|
||||
layers,
|
||||
setLayers,
|
||||
activeLayer,
|
||||
setActiveLayer,
|
||||
onLayerVisibilityChange,
|
||||
onFlatten,
|
||||
isProcessing,
|
||||
}) => {
|
||||
const [draggedLayer, setDraggedLayer] = useState(null);
|
||||
|
||||
// Load layers on mount and when projectId changes
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
loadLayers();
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadLayers = async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
const result = await toolsApi.listLayers(projectId);
|
||||
setLayers(result.layers || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load layers:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (e, index) => {
|
||||
setDraggedLayer(index);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
if (draggedLayer === null || draggedLayer === index) return;
|
||||
|
||||
// Reorder layers
|
||||
const newLayers = [...layers];
|
||||
const [removed] = newLayers.splice(draggedLayer, 1);
|
||||
newLayers.splice(index, 0, removed);
|
||||
setLayers(newLayers);
|
||||
setDraggedLayer(index);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedLayer(null);
|
||||
};
|
||||
|
||||
const toggleVisibility = (layerId) => {
|
||||
const layer = layers.find((l) => l.id === layerId);
|
||||
if (layer) {
|
||||
layer.visible = !layer.visible;
|
||||
setLayers([...layers]);
|
||||
onLayerVisibilityChange?.(layerId, layer.visible);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFlatten = async () => {
|
||||
if (!projectId || layers.length === 0) return;
|
||||
const visibleLayers = layers.filter((l) => l.visible !== false);
|
||||
const layerOrder = visibleLayers.map((l) => l.id);
|
||||
await onFlatten(layerOrder);
|
||||
loadLayers();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layers-panel">
|
||||
<div className="layers-header">
|
||||
<h3>Layers</h3>
|
||||
{layers.length > 0 && (
|
||||
<button
|
||||
className="flatten-btn"
|
||||
onClick={handleFlatten}
|
||||
disabled={isProcessing}
|
||||
title="Merge all visible layers"
|
||||
>
|
||||
Flatten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="layers-list">
|
||||
{/* Background layer (always present) */}
|
||||
<div
|
||||
className={`layer-item ${activeLayer === 'background' ? 'active' : ''}`}
|
||||
onClick={() => setActiveLayer('background')}
|
||||
>
|
||||
<span className="layer-visibility">
|
||||
<input type="checkbox" checked disabled />
|
||||
</span>
|
||||
<span className="layer-preview background-preview"></span>
|
||||
<span className="layer-name">Background</span>
|
||||
<span className="layer-lock">🔒</span>
|
||||
</div>
|
||||
|
||||
{/* Dynamic layers */}
|
||||
{layers.map((layer, index) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className={`layer-item ${activeLayer === layer.id ? 'active' : ''} ${
|
||||
draggedLayer === index ? 'dragging' : ''
|
||||
}`}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onClick={() => setActiveLayer(layer.id)}
|
||||
>
|
||||
<span className="layer-visibility">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={layer.visible !== false}
|
||||
onChange={() => toggleVisibility(layer.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</span>
|
||||
<span className="layer-preview">
|
||||
{layer.thumbnail && (
|
||||
<img src={layer.thumbnail} alt={layer.name} />
|
||||
)}
|
||||
</span>
|
||||
<span className="layer-name">{layer.name}</span>
|
||||
<span className="layer-drag-handle">⋮⋮</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{layers.length === 0 && (
|
||||
<div className="no-layers-hint">
|
||||
Use "Remove Background" to create layers
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="layers-actions">
|
||||
<button
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || !projectId}
|
||||
title="Add new empty layer"
|
||||
>
|
||||
+ New Layer
|
||||
</button>
|
||||
<button
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || activeLayer === 'background'}
|
||||
title="Delete selected layer"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || activeLayer === 'background'}
|
||||
title="Duplicate selected layer"
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layers;
|
||||
Reference in New Issue
Block a user