kopia lustrzana https://github.com/Tldraw/Tldraw
242 wiersze
6.3 KiB
TypeScript
242 wiersze
6.3 KiB
TypeScript
/* eslint-disable react-hooks/rules-of-hooks */
|
|
import {
|
|
Circle2d,
|
|
Polygon2d,
|
|
SVGContainer,
|
|
ShapeUtil,
|
|
TLDrawShapeSegment,
|
|
TLHighlightShape,
|
|
TLOnResizeHandler,
|
|
VecLike,
|
|
highlightShapeMigrations,
|
|
highlightShapeProps,
|
|
last,
|
|
rng,
|
|
} from '@tldraw/editor'
|
|
import { tldrawConstants } from '../../tldraw-constants'
|
|
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
|
|
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
import { getStrokeOutlinePoints } from '../shared/freehand/getStrokeOutlinePoints'
|
|
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
|
import { setStrokePointRadii } from '../shared/freehand/setStrokePointRadii'
|
|
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
|
import { useColorSpace } from '../shared/useColorSpace'
|
|
import { useForceSolid } from '../shared/useForceSolid'
|
|
|
|
const { FONT_SIZES } = tldrawConstants
|
|
const OVERLAY_OPACITY = 0.35
|
|
const UNDERLAY_OPACITY = 0.82
|
|
|
|
/** @public */
|
|
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
|
static override type = 'highlight' as const
|
|
static override props = highlightShapeProps
|
|
static override migrations = highlightShapeMigrations
|
|
|
|
override hideResizeHandles = (shape: TLHighlightShape) => getIsDot(shape)
|
|
override hideRotateHandle = (shape: TLHighlightShape) => getIsDot(shape)
|
|
override hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
|
|
|
|
override getDefaultProps(): TLHighlightShape['props'] {
|
|
return {
|
|
segments: [],
|
|
color: 'black',
|
|
size: 'm',
|
|
isComplete: false,
|
|
isPen: false,
|
|
}
|
|
}
|
|
|
|
getGeometry(shape: TLHighlightShape) {
|
|
const strokeWidth = getStrokeWidth(shape)
|
|
if (getIsDot(shape)) {
|
|
return new Circle2d({
|
|
x: -strokeWidth / 2,
|
|
y: -strokeWidth / 2,
|
|
radius: strokeWidth / 2,
|
|
isFilled: true,
|
|
})
|
|
}
|
|
|
|
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, true)
|
|
const opts = getHighlightFreehandSettings({ strokeWidth: sw, showAsComplete: true })
|
|
setStrokePointRadii(strokePoints, opts)
|
|
|
|
return new Polygon2d({
|
|
points: getStrokeOutlinePoints(strokePoints, opts),
|
|
isFilled: true,
|
|
})
|
|
}
|
|
|
|
component(shape: TLHighlightShape) {
|
|
return (
|
|
<SVGContainer id={shape.id} style={{ opacity: OVERLAY_OPACITY }}>
|
|
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} />
|
|
</SVGContainer>
|
|
)
|
|
}
|
|
|
|
override backgroundComponent(shape: TLHighlightShape) {
|
|
return (
|
|
<SVGContainer id={shape.id} style={{ opacity: UNDERLAY_OPACITY }}>
|
|
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} />
|
|
</SVGContainer>
|
|
)
|
|
}
|
|
|
|
indicator(shape: TLHighlightShape) {
|
|
const forceSolid = useForceSolid()
|
|
const strokeWidth = getStrokeWidth(shape)
|
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
|
|
|
let sw = strokeWidth
|
|
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
|
}
|
|
|
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
|
const options = getHighlightFreehandSettings({
|
|
strokeWidth,
|
|
showAsComplete,
|
|
})
|
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
|
|
|
let strokePath
|
|
if (strokePoints.length < 2) {
|
|
strokePath = getIndicatorDot(allPointsFromSegments[0], sw)
|
|
} else {
|
|
strokePath = getSvgPathFromStrokePoints(strokePoints, false)
|
|
}
|
|
|
|
return <path d={strokePath} />
|
|
}
|
|
|
|
override toSvg(shape: TLHighlightShape) {
|
|
return (
|
|
<HighlightRenderer
|
|
strokeWidth={getStrokeWidth(shape)}
|
|
shape={shape}
|
|
opacity={OVERLAY_OPACITY}
|
|
/>
|
|
)
|
|
}
|
|
|
|
override toBackgroundSvg(shape: TLHighlightShape) {
|
|
return (
|
|
<HighlightRenderer
|
|
strokeWidth={getStrokeWidth(shape)}
|
|
shape={shape}
|
|
opacity={UNDERLAY_OPACITY}
|
|
/>
|
|
)
|
|
}
|
|
|
|
override onResize: TLOnResizeHandler<TLHighlightShape> = (shape, info) => {
|
|
const { scaleX, scaleY } = info
|
|
|
|
const newSegments: TLDrawShapeSegment[] = []
|
|
|
|
for (const segment of shape.props.segments) {
|
|
newSegments.push({
|
|
...segment,
|
|
points: segment.points.map(({ x, y, z }) => {
|
|
return {
|
|
x: scaleX * x,
|
|
y: scaleY * y,
|
|
z,
|
|
}
|
|
}),
|
|
})
|
|
}
|
|
|
|
return {
|
|
props: {
|
|
segments: newSegments,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
function getShapeDot(point: VecLike) {
|
|
const r = 0.1
|
|
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${
|
|
r * 2
|
|
},0`
|
|
}
|
|
|
|
function getIndicatorDot(point: VecLike, sw: number) {
|
|
const r = sw / 2
|
|
return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${
|
|
r * 2
|
|
},0`
|
|
}
|
|
|
|
function getHighlightStrokePoints(
|
|
shape: TLHighlightShape,
|
|
strokeWidth: number,
|
|
forceSolid: boolean
|
|
) {
|
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
|
|
|
let sw = strokeWidth
|
|
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
|
sw += rng(shape.id)() * (strokeWidth / 6)
|
|
}
|
|
|
|
const options = getHighlightFreehandSettings({
|
|
strokeWidth: sw,
|
|
showAsComplete,
|
|
})
|
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
|
|
|
return { strokePoints, sw }
|
|
}
|
|
|
|
function getHighlightSvgPath(shape: TLHighlightShape, strokeWidth: number, forceSolid: boolean) {
|
|
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid)
|
|
|
|
const solidStrokePath =
|
|
strokePoints.length > 1
|
|
? getSvgPathFromStrokePoints(strokePoints, false)
|
|
: getShapeDot(shape.props.segments[0].points[0])
|
|
|
|
return { solidStrokePath, sw }
|
|
}
|
|
|
|
function HighlightRenderer({
|
|
strokeWidth,
|
|
shape,
|
|
opacity,
|
|
}: {
|
|
strokeWidth: number
|
|
shape: TLHighlightShape
|
|
opacity?: number
|
|
}) {
|
|
const theme = useDefaultColorTheme()
|
|
const forceSolid = useForceSolid()
|
|
const { solidStrokePath, sw } = getHighlightSvgPath(shape, strokeWidth, forceSolid)
|
|
const colorSpace = useColorSpace()
|
|
const color = theme[shape.props.color].highlight[colorSpace]
|
|
|
|
return (
|
|
<path
|
|
d={solidStrokePath}
|
|
strokeLinecap="round"
|
|
fill="none"
|
|
pointerEvents="all"
|
|
stroke={color}
|
|
strokeWidth={sw}
|
|
opacity={opacity}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function getStrokeWidth(shape: TLHighlightShape) {
|
|
return FONT_SIZES[shape.props.size] * 1.12
|
|
}
|
|
|
|
function getIsDot(shape: TLHighlightShape) {
|
|
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
|
}
|