110 lines
2.8 KiB
TypeScript
110 lines
2.8 KiB
TypeScript
|
|
import { BaseTool } from './base-tool';
|
||
|
|
import type { PointerState } from '@/types';
|
||
|
|
import { useSelectionStore } from '@/store/selection-store';
|
||
|
|
import { useLayerStore } from '@/store/layer-store';
|
||
|
|
import {
|
||
|
|
createEllipticalMask,
|
||
|
|
createSelection,
|
||
|
|
combineMasks,
|
||
|
|
} from '@/lib/selection-utils';
|
||
|
|
|
||
|
|
export class EllipticalSelectionTool extends BaseTool {
|
||
|
|
private startX = 0;
|
||
|
|
private startY = 0;
|
||
|
|
private currentX = 0;
|
||
|
|
private currentY = 0;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super('Elliptical Selection');
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerDown(pointer: PointerState): void {
|
||
|
|
this.isActive = true;
|
||
|
|
this.isDrawing = true;
|
||
|
|
this.startX = pointer.x;
|
||
|
|
this.startY = pointer.y;
|
||
|
|
this.currentX = pointer.x;
|
||
|
|
this.currentY = pointer.y;
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerMove(pointer: PointerState, ctx: CanvasRenderingContext2D): void {
|
||
|
|
if (!this.isDrawing) return;
|
||
|
|
|
||
|
|
this.currentX = pointer.x;
|
||
|
|
this.currentY = pointer.y;
|
||
|
|
|
||
|
|
// Draw preview ellipse
|
||
|
|
const layer = this.getActiveLayer();
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
|
||
|
|
ctx.save();
|
||
|
|
ctx.strokeStyle = '#000';
|
||
|
|
ctx.lineWidth = 1;
|
||
|
|
ctx.setLineDash([4, 4]);
|
||
|
|
|
||
|
|
const cx = (this.startX + this.currentX) / 2;
|
||
|
|
const cy = (this.startY + this.currentY) / 2;
|
||
|
|
const rx = Math.abs(this.currentX - this.startX) / 2;
|
||
|
|
const ry = Math.abs(this.currentY - this.startY) / 2;
|
||
|
|
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
||
|
|
ctx.stroke();
|
||
|
|
|
||
|
|
ctx.strokeStyle = '#fff';
|
||
|
|
ctx.lineDashOffset = 4;
|
||
|
|
ctx.stroke();
|
||
|
|
|
||
|
|
ctx.restore();
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerUp(): void {
|
||
|
|
if (!this.isDrawing) return;
|
||
|
|
|
||
|
|
const layer = this.getActiveLayer();
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
const cx = (this.startX + this.currentX) / 2;
|
||
|
|
const cy = (this.startY + this.currentY) / 2;
|
||
|
|
const rx = Math.abs(this.currentX - this.startX) / 2;
|
||
|
|
const ry = Math.abs(this.currentY - this.startY) / 2;
|
||
|
|
|
||
|
|
if (rx > 0 && ry > 0) {
|
||
|
|
const { selectionMode, feather, activeSelection } =
|
||
|
|
useSelectionStore.getState();
|
||
|
|
|
||
|
|
const newMask = createEllipticalMask(
|
||
|
|
cx,
|
||
|
|
cy,
|
||
|
|
rx,
|
||
|
|
ry,
|
||
|
|
layer.canvas.width,
|
||
|
|
layer.canvas.height
|
||
|
|
);
|
||
|
|
|
||
|
|
let finalMask = newMask;
|
||
|
|
|
||
|
|
// Combine with existing selection if needed
|
||
|
|
if (activeSelection && selectionMode !== 'new') {
|
||
|
|
finalMask = combineMasks(activeSelection.mask, newMask, selectionMode);
|
||
|
|
}
|
||
|
|
|
||
|
|
const selection = createSelection(layer.id, finalMask, feather);
|
||
|
|
useSelectionStore.getState().setActiveSelection(selection);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.isDrawing = false;
|
||
|
|
this.isActive = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
getCursor(): string {
|
||
|
|
return 'crosshair';
|
||
|
|
}
|
||
|
|
|
||
|
|
private getActiveLayer() {
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
return layers.find((l) => l.id === activeLayerId);
|
||
|
|
}
|
||
|
|
}
|