117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
|
|
import { BaseTool } from './base-tool';
|
||
|
|
import type { PointerState, LassoPoint } from '@/types';
|
||
|
|
import { useSelectionStore } from '@/store/selection-store';
|
||
|
|
import { useLayerStore } from '@/store/layer-store';
|
||
|
|
import {
|
||
|
|
createLassoMask,
|
||
|
|
createSelection,
|
||
|
|
combineMasks,
|
||
|
|
} from '@/lib/selection-utils';
|
||
|
|
|
||
|
|
export class LassoSelectionTool extends BaseTool {
|
||
|
|
private points: LassoPoint[] = [];
|
||
|
|
private minDistance = 2; // Minimum distance between points
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super('Lasso Selection');
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerDown(pointer: PointerState): void {
|
||
|
|
this.isActive = true;
|
||
|
|
this.isDrawing = true;
|
||
|
|
this.points = [];
|
||
|
|
this.points.push({ x: pointer.x, y: pointer.y });
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerMove(pointer: PointerState, ctx: CanvasRenderingContext2D): void {
|
||
|
|
if (!this.isDrawing) return;
|
||
|
|
|
||
|
|
const lastPoint = this.points[this.points.length - 1];
|
||
|
|
const dx = pointer.x - lastPoint.x;
|
||
|
|
const dy = pointer.y - lastPoint.y;
|
||
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
|
||
|
|
// Only add point if far enough from last point
|
||
|
|
if (distance >= this.minDistance) {
|
||
|
|
this.points.push({ x: pointer.x, y: pointer.y });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Draw preview
|
||
|
|
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]);
|
||
|
|
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.moveTo(this.points[0].x, this.points[0].y);
|
||
|
|
for (let i = 1; i < this.points.length; i++) {
|
||
|
|
ctx.lineTo(this.points[i].x, this.points[i].y);
|
||
|
|
}
|
||
|
|
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 || this.points.length < 3) {
|
||
|
|
this.isDrawing = false;
|
||
|
|
this.isActive = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close the path if not already closed
|
||
|
|
const firstPoint = this.points[0];
|
||
|
|
const lastPoint = this.points[this.points.length - 1];
|
||
|
|
const dx = lastPoint.x - firstPoint.x;
|
||
|
|
const dy = lastPoint.y - firstPoint.y;
|
||
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
|
||
|
|
if (distance > this.minDistance) {
|
||
|
|
this.points.push(firstPoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
const { selectionMode, feather, activeSelection } =
|
||
|
|
useSelectionStore.getState();
|
||
|
|
|
||
|
|
const newMask = createLassoMask(
|
||
|
|
this.points,
|
||
|
|
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;
|
||
|
|
this.points = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
getCursor(): string {
|
||
|
|
return 'crosshair';
|
||
|
|
}
|
||
|
|
|
||
|
|
private getActiveLayer() {
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
return layers.find((l) => l.id === activeLayerId);
|
||
|
|
}
|
||
|
|
}
|