import { SVGContainer, TLBounds, Utils } from '@tldraw/core'; import { intersectBoundsBounds, intersectBoundsPolyline, intersectLineSegmentBounds, intersectLineSegmentLineSegment, } from '@tldraw/intersect'; import { Vec } from '@tldraw/vec'; import { DashStyle, DrawShape, GHOSTED_OPACITY, TDMeta, TDShapeType, TransformInfo, } from '@toeverything/components/board-types'; import * as React from 'react'; import { defaultStyle, getShapeStyle } from '../shared/shape-styles'; import { TDShapeUtil } from '../TDShapeUtil'; import { getDrawStrokePathTDSnapshot, getFillPath, getSolidStrokePathTDSnapshot, } from './draw-helpers'; type T = DrawShape; type E = SVGSVGElement; export class DrawUtil extends TDShapeUtil { type = TDShapeType.Draw as const; pointsBoundsCache = new WeakMap([]); shapeBoundsCache = new Map(); rotatedCache = new WeakMap([]); pointCache: Record = {}; override canClone = true; getShape = (props: Partial): T => { return Utils.deepMerge( { id: 'id', type: TDShapeType.Draw, name: 'Draw', parentId: 'page', childIndex: 1, point: [0, 0], rotation: 0, style: defaultStyle, points: [], isComplete: false, workspace: props.workspace, }, props ); }; Component = TDShapeUtil.Component( ({ shape, meta, isSelected, isGhost, events }, ref) => { const { points, style, isComplete } = shape; const polygon_path_td_snapshot = React.useMemo(() => { return getFillPath(shape); }, [points, style.strokeWidth]); const path_td_snapshot = React.useMemo(() => { return style.dash === DashStyle.Draw ? getDrawStrokePathTDSnapshot(shape) : getSolidStrokePathTDSnapshot(shape); }, [points, style.strokeWidth, style.dash, isComplete]); const styles = getShapeStyle(style, meta.isDarkMode); const { stroke, fill, strokeWidth } = styles; // For very short lines, draw a point instead of a line const bounds = this.getBounds(shape); const verySmall = bounds.width <= strokeWidth / 2 && bounds.height <= strokeWidth / 2; if (verySmall) { const sw = 1 + strokeWidth; return ( ); } const shouldFill = style.isFilled && points.length > 3 && Vec.dist(points[0], points[points.length - 1]) < strokeWidth * 2; if (shape.style.dash === DashStyle.Draw) { return ( {shouldFill && ( )} ); } // For solid, dash and dotted lines, draw a regular stroke path const strokeDasharray = { [DashStyle.None]: 'none', [DashStyle.Draw]: 'none', [DashStyle.Solid]: `none`, [DashStyle.Dotted]: `0.1 ${strokeWidth * 4}`, [DashStyle.Dashed]: `${strokeWidth * 4} ${strokeWidth * 4}`, }[style.dash]; const strokeDashoffset = { [DashStyle.None]: 'none', [DashStyle.Draw]: 'none', [DashStyle.Solid]: `none`, [DashStyle.Dotted]: `0`, [DashStyle.Dashed]: `0`, }[style.dash]; const sw = 1 + strokeWidth * 1.5; return ( ); } ); Indicator = TDShapeUtil.Indicator(({ shape }) => { const { points } = shape; const path_td_snapshot = React.useMemo(() => { return getSolidStrokePathTDSnapshot(shape); }, [points]); const bounds = this.getBounds(shape); const verySmall = bounds.width < 4 && bounds.height < 4; if (verySmall) { return ; } return ; }); override transform = ( shape: T, bounds: TLBounds, { initialShape, scaleX, scaleY }: TransformInfo ): Partial => { const initialShapeBounds = Utils.getFromCache( this.boundsCache, initialShape, () => Utils.getBoundsFromPoints(initialShape.points) ); const points = initialShape.points.map(([x, y, r]) => { return [ bounds.width * (scaleX < 0 // * sin? ? 1 - x / initialShapeBounds.width : x / initialShapeBounds.width), bounds.height * (scaleY < 0 // * cos? ? 1 - y / initialShapeBounds.height : y / initialShapeBounds.height), r, ]; }); const newBounds = Utils.getBoundsFromPoints(shape.points); const point = Vec.sub( [bounds.minX, bounds.minY], [newBounds.minX, newBounds.minY] ); return { points, point, }; }; getBounds = (shape: T) => { // The goal here is to avoid recalculating the bounds from the // points array, which is expensive. However, we still need a // new bounds if the point has changed, but we will reuse the // previous bounds-from-points result if we can. const pointsHaveChanged = !this.pointsBoundsCache.has(shape.points); const pointHasChanged = !(this.pointCache[shape.id] === shape.point); if (pointsHaveChanged) { // If the points have changed, then bust the points cache const bounds = Utils.getBoundsFromPoints(shape.points); this.pointsBoundsCache.set(shape.points, bounds); this.shapeBoundsCache.set( shape.id, Utils.translateBounds(bounds, shape.point) ); this.pointCache[shape.id] = shape.point; } else if (pointHasChanged && !pointsHaveChanged) { // If the point have has changed, then bust the point cache this.pointCache[shape.id] = shape.point; this.shapeBoundsCache.set( shape.id, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Utils.translateBounds( this.pointsBoundsCache.get(shape.points)!, shape.point ) ); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.shapeBoundsCache.get(shape.id)!; }; override shouldRender = (prev: T, next: T) => { return ( next.points !== prev.points || next.style !== prev.style || next.isComplete !== prev.isComplete ); }; override hitTestPoint = (shape: T, point: number[]) => { const ptA = Vec.sub(point, shape.point); return Utils.pointInPolyline(ptA, shape.points); }; override hitTestLineSegment = ( shape: T, A: number[], B: number[] ): boolean => { const { points, point } = shape; const ptA = Vec.sub(A, point); const ptB = Vec.sub(B, point); const bounds = this.getBounds(shape); if (points.length <= 2) { return Vec.distanceToLineSegment(A, B, shape.point) < 4; } if (intersectLineSegmentBounds(ptA, ptB, bounds)) { for (let i = 1; i < points.length; i++) { if ( intersectLineSegmentLineSegment( points[i - 1], points[i], ptA, ptB ).didIntersect ) { return true; } } } return false; }; override hitTestBounds = (shape: T, bounds: TLBounds) => { // Test axis-aligned shape if (!shape.rotation) { const shapeBounds = this.getBounds(shape); return ( Utils.boundsContain(bounds, shapeBounds) || ((Utils.boundsContain(shapeBounds, bounds) || intersectBoundsBounds(shapeBounds, bounds).length > 0) && intersectBoundsPolyline( Utils.translateBounds(bounds, Vec.neg(shape.point)), shape.points ).length > 0) ); } // Test rotated shape const rBounds = this.getRotatedBounds(shape); const rotatedBounds = Utils.getFromCache( this.rotatedCache, shape, () => { const c = Utils.getBoundsCenter( Utils.getBoundsFromPoints(shape.points) ); return shape.points.map(pt => Vec.rotWith(pt, c, shape.rotation || 0) ); } ); return ( Utils.boundsContain(bounds, rBounds) || intersectBoundsPolyline( Utils.translateBounds(bounds, Vec.neg(shape.point)), rotatedBounds ).length > 0 ); }; }