UNPKG

@teachinglab/omd

Version:

omd

323 lines (274 loc) 9.52 kB
import { Tool } from './tool.js'; import { Stroke } from '../drawing/stroke.js'; /** * Eraser tool for removing strokes */ export class EraserTool extends Tool { constructor(canvas, options = {}) { super(canvas, { size: 12, hardness: 0.8, mode: 'radius', // 'stroke' or 'radius' ...options }); this.displayName = 'Eraser'; this.description = 'Erase strokes (M to toggle mode)'; this.icon = 'eraser'; this.shortcut = 'E'; this.category = 'editing'; // Eraser state this.isErasing = false; this.erasedPoints = new Set(); // Track erased points for radius mode } /** * Start erasing */ onPointerDown(event) { if (!this.canUse()) return; this.isErasing = true; this._eraseAtPoint(event.x, event.y); this.canvas.emit('eraseStarted', { tool: this.name, point: { x: event.x, y: event.y } }); } /** * Continue erasing */ onPointerMove(event) { if (!this.isErasing) return; this._eraseAtPoint(event.x, event.y); } /** * Stop erasing */ onPointerUp(event) { if (!this.isErasing) return; this.isErasing = false; this.canvas.emit('eraseCompleted', { tool: this.name }); } /** * Cancel erasing */ onCancel() { this.isErasing = false; super.onCancel(); } /** * Erase strokes at point * @private */ _eraseAtPoint(x, y) { if (this.config.mode === 'stroke') { this._eraseWholeStrokes(x, y); } else { this._eraseInRadius(x, y); } } /** * Erase whole strokes (original behavior) * @private */ _eraseWholeStrokes(x, y) { const tolerance = this.config.size || 20; const strokesToRemove = []; // Check each stroke to see if it's near the eraser point for (const [id, stroke] of this.canvas.strokes) { if (stroke.isNearPoint(x, y, tolerance)) { strokesToRemove.push(id); } } // Remove the strokes strokesToRemove.forEach(id => { this.canvas.removeStroke(id); }); } /** * Erase within radius (traditional eraser behavior) * @private */ _eraseInRadius(x, y) { const radius = this.config.size || 20; const radiusSquared = radius * radius; const strokesToModify = []; // Find strokes that intersect with the eraser circle for (const [id, stroke] of this.canvas.strokes) { const boundingBox = stroke.getBoundingBox(); // Quick bounding box check first if (this._circleIntersectsRect(x, y, radius, boundingBox)) { // Check individual points const pointsToRemove = []; for (let i = 0; i < stroke.points.length; i++) { const point = stroke.points[i]; const dx = point.x - x; const dy = point.y - y; const distanceSquared = dx * dx + dy * dy; if (distanceSquared <= radiusSquared) { pointsToRemove.push(i); } } if (pointsToRemove.length > 0) { strokesToModify.push({ id, stroke, pointsToRemove }); } } } // Modify or remove strokes strokesToModify.forEach(({ id, stroke, pointsToRemove }) => { if (pointsToRemove.length >= stroke.points.length * 0.8) { // If most points are erased, remove the whole stroke this.canvas.removeStroke(id); } else { // Remove points and split stroke if necessary this._splitStrokeAtErasedPoints(stroke, pointsToRemove); } }); } /** * Check if circle intersects with rectangle * @private */ _circleIntersectsRect(cx, cy, radius, rect) { const closestX = Math.max(rect.left, Math.min(cx, rect.right)); const closestY = Math.max(rect.top, Math.min(cy, rect.bottom)); const dx = cx - closestX; const dy = cy - closestY; return (dx * dx + dy * dy) <= (radius * radius); } /** * Split stroke at erased points or remove segments * @private */ _splitStrokeAtErasedPoints(stroke, pointsToRemove) { if (pointsToRemove.length === 0) return; // Sort indices in ascending order for processing pointsToRemove.sort((a, b) => a - b); // Find continuous segments to keep const segments = []; let startIndex = 0; for (let i = 0; i < pointsToRemove.length; i++) { const removeIndex = pointsToRemove[i]; // If there's a gap before this point, create a segment if (removeIndex > startIndex) { const segmentPoints = stroke.points.slice(startIndex, removeIndex); if (segmentPoints.length >= 2) { segments.push(segmentPoints); } } startIndex = removeIndex + 1; } // Add final segment if there are remaining points if (startIndex < stroke.points.length) { const finalSegment = stroke.points.slice(startIndex); if (finalSegment.length >= 2) { segments.push(finalSegment); } } // Remove original stroke this.canvas.removeStroke(stroke.id); // Create new strokes for each segment segments.forEach((segmentPoints, index) => { const newStroke = new Stroke({ strokeWidth: stroke.strokeWidth, strokeColor: stroke.strokeColor, strokeOpacity: stroke.strokeOpacity, tool: stroke.tool }); // Add all points to the new stroke segmentPoints.forEach(point => { newStroke.addPoint(point); }); newStroke.finish(); this.canvas.addStroke(newStroke); }); } /** * Get eraser cursor */ getCursor() { return 'eraser'; } /** * Handle keyboard shortcuts */ onKeyboardShortcut(key, event) { switch (key) { case '[': // Decrease eraser size this.updateConfig({ size: Math.max(5, this.config.size - 5) }); return true; case ']': // Increase eraser size this.updateConfig({ size: Math.min(100, this.config.size + 5) }); return true; case 'm': // Toggle eraser mode this.toggleMode(); return true; default: return super.onKeyboardShortcut(key, event); } } /** * Toggle between stroke and radius erasing modes */ toggleMode() { const newMode = this.config.mode === 'stroke' ? 'radius' : 'stroke'; this.updateConfig({ mode: newMode }); // Update cursor appearance if (this.canvas.cursor) { this.canvas.cursor.updateFromToolConfig(this.config); } // Emit mode change event this.canvas.emit('eraserModeChanged', { mode: newMode, description: this._getModeDescription(newMode) }); console.log(`Eraser mode changed to: ${this._getModeDescription(newMode)}`); } /** * Get mode description * @private */ _getModeDescription(mode) { return mode === 'stroke' ? 'Whole Stroke Erasing' : 'Radius Erasing'; } /** * Update configuration */ onConfigUpdate() { // Update cursor size if available if (this.canvas.cursor) { this.canvas.cursor.setSize(this.config.size); } } /** * Get help text */ getHelpText() { return `${super.getHelpText()}\nShortcuts: [ ] to adjust size, M to toggle mode\nCurrent mode: ${this._getModeDescription(this.config.mode)}`; } /** * Get current eraser mode */ getMode() { return this.config.mode; } /** * Set eraser mode * @param {string} mode - 'stroke' or 'radius' */ setMode(mode) { if (mode === 'stroke' || mode === 'radius') { this.updateConfig({ mode }); if (this.canvas.cursor) { this.canvas.cursor.updateFromToolConfig(this.config); } } } }