Merge branch 'main' into claude/add-eye-detection-feature-69XOl
This commit is contained in:
@@ -11,8 +11,9 @@ const AdvancedTools = ({
|
||||
isProcessing,
|
||||
setIsProcessing,
|
||||
setError,
|
||||
activeToolMode,
|
||||
setActiveToolMode,
|
||||
}) => {
|
||||
const [activeToolMode, setActiveToolMode] = useState(null);
|
||||
const [colorTolerance, setColorTolerance] = useState(30);
|
||||
|
||||
const handleRemoveBackground = async () => {
|
||||
|
||||
@@ -46,3 +46,66 @@
|
||||
.clear-selection-btn:hover {
|
||||
background-color: #dd4444;
|
||||
}
|
||||
|
||||
/* Zoom controls */
|
||||
.zoom-controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.zoom-controls button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background-color: #444;
|
||||
color: white;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zoom-controls button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.zoom-level {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Tool mode indicator */
|
||||
.tool-mode-indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0, 120, 255, 0.9);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@@ -61,11 +61,36 @@ const ImageCanvas = forwardRef(({
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Mouse wheel zoom
|
||||
const handleWheel = (opt) => {
|
||||
const e = opt.e;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const delta = e.deltaY;
|
||||
let newZoom = canvas.getZoom();
|
||||
newZoom *= 0.999 ** delta;
|
||||
|
||||
// Clamp zoom between 0.1x and 10x
|
||||
if (newZoom > 10) newZoom = 10;
|
||||
if (newZoom < 0.1) newZoom = 0.1;
|
||||
|
||||
// Zoom to point under cursor
|
||||
const pointer = canvas.getPointer(e, true);
|
||||
canvas.zoomToPoint({ x: pointer.x, y: pointer.y }, newZoom);
|
||||
|
||||
setCurrentZoom(newZoom);
|
||||
onZoomChange?.(newZoom);
|
||||
};
|
||||
|
||||
canvas.on('mouse:wheel', handleWheel);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
canvas.off('mouse:wheel', handleWheel);
|
||||
canvas.dispose();
|
||||
};
|
||||
}, []);
|
||||
}, [onZoomChange]);
|
||||
|
||||
// Center and scale image
|
||||
const centerImage = (canvas, img, zoomFactor) => {
|
||||
@@ -601,6 +626,35 @@ const ImageCanvas = forwardRef(({
|
||||
}
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
if (!fabricCanvasRef.current) return;
|
||||
const canvas = fabricCanvasRef.current;
|
||||
let newZoom = canvas.getZoom() * 1.2;
|
||||
if (newZoom > 10) newZoom = 10;
|
||||
canvas.setZoom(newZoom);
|
||||
setCurrentZoom(newZoom);
|
||||
onZoomChange?.(newZoom);
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (!fabricCanvasRef.current) return;
|
||||
const canvas = fabricCanvasRef.current;
|
||||
let newZoom = canvas.getZoom() / 1.2;
|
||||
if (newZoom < 0.1) newZoom = 0.1;
|
||||
canvas.setZoom(newZoom);
|
||||
setCurrentZoom(newZoom);
|
||||
onZoomChange?.(newZoom);
|
||||
};
|
||||
|
||||
const handleZoomReset = () => {
|
||||
if (!fabricCanvasRef.current) return;
|
||||
const canvas = fabricCanvasRef.current;
|
||||
canvas.setZoom(1);
|
||||
canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
|
||||
setCurrentZoom(1);
|
||||
onZoomChange?.(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="canvas-container">
|
||||
<canvas ref={canvasRef} />
|
||||
|
||||
@@ -11,6 +11,7 @@ const Layers = ({
|
||||
onLayerVisibilityChange,
|
||||
onFlatten,
|
||||
isProcessing,
|
||||
onError,
|
||||
}) => {
|
||||
const [draggedLayer, setDraggedLayer] = useState(null);
|
||||
|
||||
@@ -69,6 +70,56 @@ const Layers = ({
|
||||
loadLayers();
|
||||
};
|
||||
|
||||
const handleNewLayer = async () => {
|
||||
if (!projectId) return;
|
||||
try {
|
||||
// Create a new empty transparent layer
|
||||
const newLayer = {
|
||||
id: `layer-${Date.now()}`,
|
||||
name: `Layer ${layers.length + 1}`,
|
||||
visible: true,
|
||||
thumbnail: null,
|
||||
};
|
||||
setLayers([...layers, newLayer]);
|
||||
setActiveLayer(newLayer.id);
|
||||
} catch (err) {
|
||||
onError?.(`Failed to create layer: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLayer = async () => {
|
||||
if (!projectId || activeLayer === 'background') return;
|
||||
try {
|
||||
const updatedLayers = layers.filter((l) => l.id !== activeLayer);
|
||||
setLayers(updatedLayers);
|
||||
setActiveLayer(updatedLayers.length > 0 ? updatedLayers[updatedLayers.length - 1].id : 'background');
|
||||
} catch (err) {
|
||||
onError?.(`Failed to delete layer: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateLayer = async () => {
|
||||
if (!projectId || activeLayer === 'background') return;
|
||||
try {
|
||||
const layerToDuplicate = layers.find((l) => l.id === activeLayer);
|
||||
if (!layerToDuplicate) return;
|
||||
|
||||
const newLayer = {
|
||||
...layerToDuplicate,
|
||||
id: `layer-${Date.now()}`,
|
||||
name: `${layerToDuplicate.name} copy`,
|
||||
};
|
||||
|
||||
const activeIndex = layers.findIndex((l) => l.id === activeLayer);
|
||||
const updatedLayers = [...layers];
|
||||
updatedLayers.splice(activeIndex + 1, 0, newLayer);
|
||||
setLayers(updatedLayers);
|
||||
setActiveLayer(newLayer.id);
|
||||
} catch (err) {
|
||||
onError?.(`Failed to duplicate layer: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layers-panel">
|
||||
<div className="layers-header">
|
||||
@@ -142,6 +193,7 @@ const Layers = ({
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || !projectId}
|
||||
title="Add new empty layer"
|
||||
onClick={handleNewLayer}
|
||||
>
|
||||
+ New Layer
|
||||
</button>
|
||||
@@ -149,6 +201,7 @@ const Layers = ({
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || activeLayer === 'background'}
|
||||
title="Delete selected layer"
|
||||
onClick={handleDeleteLayer}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -156,6 +209,7 @@ const Layers = ({
|
||||
className="layer-action-btn"
|
||||
disabled={isProcessing || activeLayer === 'background'}
|
||||
title="Duplicate selected layer"
|
||||
onClick={handleDuplicateLayer}
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user