Skip to Content
ExamplesWhiteboard

Eraser

This example shows how to create an eraser tool that allows you to delete nodes and edges by wiping them out. It’s made up of two parts:

  1. The Eraser component that handles the erasing logic and rendering of the eraser trail.
  2. The custom ErasableNode and ErasableEdge that reacts to the toBeDeleted flag.

Determining if the trail intersects with a node is fairly straight forward - however detecting intersections between the trail and an edge is a bit more complex: We sample points along the edge through the getPointAtLength method of the SVG path element, construct a polyline that we can then use to detect intersections with the eraser trail. This is a trade-off between performance and accuracy - you can play around with the sampleDistance variable to see the effect it has on the eraser trail.

// Utility functions for geometric intersection detection // Type definitions for better type safety type Point = [number, number]; type Rectangle = { x: number; y: number; width: number; height: number }; // Check if two line segments intersect function lineSegmentsIntersect(p1: Point, p2: Point, p3: Point, p4: Point): boolean { const [x1, y1] = p1; const [x2, y2] = p2; const [x3, y3] = p3; const [x4, y4] = p4; const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (Math.abs(denom) < 1e-10) return false; // Lines are parallel const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom; const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom; return t >= 0 && t <= 1 && u >= 0 && u <= 1; } // Check if a point is inside a rectangle function pointInRectangle(point: Point, rect: Rectangle): boolean { const [x, y] = point; return ( x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height ); } // Get the four edges of a rectangle as line segments function getRectangleEdges(rect: Rectangle): [Point, Point][] { const { x, y, width, height } = rect; return [ [ [x, y], [x + width, y], ], // top edge [ [x + width, y], [x + width, y + height], ], // right edge [ [x + width, y + height], [x, y + height], ], // bottom edge [ [x, y + height], [x, y], ], // left edge ]; } // Check if a polyline (series of connected line segments) intersects with a rectangle export function polylineIntersectsRectangle(points: Point[], rect: Rectangle): boolean { if (points.length < 2) return false; // Early return if any point is inside the rectangle for (const point of points) { if (pointInRectangle(point, rect)) { return true; } } // Check if any line segment intersects with rectangle edges const rectEdges = getRectangleEdges(rect); for (let i = 0; i < points.length - 1; i++) { const lineStart = points[i]; const lineEnd = points[i + 1]; for (const [edgeStart, edgeEnd] of rectEdges) { if (lineSegmentsIntersect(lineStart, lineEnd, edgeStart, edgeEnd)) { return true; } } } return false; } // Calculate distance between two points function distanceBetweenPoints(p1: Point, p2: Point): number { const [x1, y1] = p1; const [x2, y2] = p2; return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); } // Calculate the closest point on a line segment to a given point function closestPointOnSegment( point: Point, segmentStart: Point, segmentEnd: Point, ): Point { const [px, py] = point; const [x1, y1] = segmentStart; const [x2, y2] = segmentEnd; const dx = x2 - x1; const dy = y2 - y1; const lengthSquared = dx * dx + dy * dy; if (lengthSquared === 0) return segmentStart; // Segment is a point const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lengthSquared)); return [x1 + t * dx, y1 + t * dy]; } // Check if two paths intersect using a more efficient approach export function pathsIntersect( path1: Point[], path2: Point[], threshold: number = 1, ): boolean { if (path1.length < 2 || path2.length < 2) return false; // First, do the more precise line segment intersection check for (let i = 0; i < path1.length - 1; i++) { for (let j = 0; j < path2.length - 1; j++) { if (lineSegmentsIntersect(path1[i], path1[i + 1], path2[j], path2[j + 1])) { return true; } } } // If no exact intersection, check for proximity based on threshold if (threshold > 0) { for (let i = 0; i < path1.length - 1; i++) { const segment1Start = path1[i]; const segment1End = path1[i + 1]; for (let j = 0; j < path2.length - 1; j++) { const segment2Start = path2[j]; const segment2End = path2[j + 1]; // Check distance between segment endpoints and closest points const distances = [ distanceBetweenPoints( segment1Start, closestPointOnSegment(segment1Start, segment2Start, segment2End), ), distanceBetweenPoints( segment1End, closestPointOnSegment(segment1End, segment2Start, segment2End), ), distanceBetweenPoints( segment2Start, closestPointOnSegment(segment2Start, segment1Start, segment1End), ), distanceBetweenPoints( segment2End, closestPointOnSegment(segment2End, segment1Start, segment1End), ), ]; if (Math.min(...distances) <= threshold) { return true; } } } } return false; } // Simplified path sampling for cases where you need discrete points export function samplePathPoints(points: Point[], maxDistance: number = 5): Point[] { if (points.length < 2) return [...points]; const result: Point[] = [points[0]]; for (let i = 1; i < points.length; i++) { const prev = result[result.length - 1]; const current = points[i]; const distance = distanceBetweenPoints(prev, current); if (distance > maxDistance) { // Add intermediate points const numSegments = Math.ceil(distance / maxDistance); for (let j = 1; j < numSegments; j++) { const t = j / numSegments; const interpolated: Point = [ prev[0] + (current[0] - prev[0]) * t, prev[1] + (current[1] - prev[1]) * t, ]; result.push(interpolated); } } result.push(current); } return result; }
Last updated on