Files
AFFiNE-Mirror/libs/components/board-shapes/src/draw-util/DrawUtil.tsx
T
2022-08-13 03:16:26 +08:00

377 lines
13 KiB
TypeScript

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<T, E> {
type = TDShapeType.Draw as const;
pointsBoundsCache = new WeakMap<T['points'], TLBounds>([]);
shapeBoundsCache = new Map<string, TLBounds>();
rotatedCache = new WeakMap<T, number[][]>([]);
pointCache: Record<string, number[]> = {};
override canClone = true;
getShape = (props: Partial<T>): T => {
return Utils.deepMerge<T>(
{
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<T, E, TDMeta>(
({ 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 (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
<circle
r={sw}
fill={stroke}
stroke={stroke}
pointerEvents="all"
opacity={isGhost ? GHOSTED_OPACITY : 1}
/>
</SVGContainer>
);
}
const shouldFill =
style.isFilled &&
points.length > 3 &&
Vec.dist(points[0], points[points.length - 1]) <
strokeWidth * 2;
if (shape.style.dash === DashStyle.Draw) {
return (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
<g opacity={isGhost ? GHOSTED_OPACITY : 1}>
<path
className={
shouldFill || isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
d={path_td_snapshot}
/>
{shouldFill && (
<path
d={polygon_path_td_snapshot}
stroke="none"
fill={fill}
strokeLinejoin="round"
strokeLinecap="round"
pointerEvents="none"
/>
)}
<path
d={path_td_snapshot}
fill={stroke}
stroke={stroke}
strokeWidth={strokeWidth / 2}
strokeLinejoin="round"
strokeLinecap="round"
pointerEvents="none"
/>
</g>
</SVGContainer>
);
}
// 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 (
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
<g opacity={isGhost ? GHOSTED_OPACITY : 1}>
<path
className={
shouldFill && isSelected
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
d={path_td_snapshot}
/>
<path
d={path_td_snapshot}
fill={shouldFill ? fill : 'none'}
stroke="none"
strokeWidth={Math.min(4, strokeWidth * 2)}
strokeLinejoin="round"
strokeLinecap="round"
pointerEvents="none"
/>
<path
d={path_td_snapshot}
fill="none"
stroke={stroke}
strokeWidth={sw}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
strokeLinejoin="round"
strokeLinecap="round"
pointerEvents="none"
/>
</g>
</SVGContainer>
);
}
);
Indicator = TDShapeUtil.Indicator<T>(({ 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 <circle x={bounds.width / 2} y={bounds.height / 2} r={1} />;
}
return <path d={path_td_snapshot} />;
});
override transform = (
shape: T,
bounds: TLBounds,
{ initialShape, scaleX, scaleY }: TransformInfo<T>
): Partial<T> => {
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
);
};
}