123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- import { getFromCache, uniqueId } from 'utils/utils'
- import vec from 'utils/vec'
- import { DashStyle, DrawShape, ShapeType } from 'types'
- import { intersectPolylineBounds } from 'utils/intersections'
- import getStroke, { getStrokePoints } from 'perfect-freehand'
- import {
- getBoundsCenter,
- getBoundsFromPoints,
- getSvgPathFromStroke,
- translateBounds,
- boundsContain,
- } from 'utils'
- import { defaultStyle, getShapeStyle } from 'state/shape-styles'
- import { registerShapeUtils } from './register'
-
- const rotatedCache = new WeakMap<DrawShape, number[][]>([])
- const drawPathCache = new WeakMap<DrawShape['points'], string>([])
- const simplePathCache = new WeakMap<DrawShape['points'], string>([])
- const polygonCache = new WeakMap<DrawShape['points'], string>([])
-
- const draw = registerShapeUtils<DrawShape>({
- boundsCache: new WeakMap([]),
-
- canStyleFill: true,
-
- defaultProps: {
- id: uniqueId(),
- type: ShapeType.Draw,
- name: 'Draw',
- parentId: 'page1',
- childIndex: 0,
- point: [0, 0],
- points: [],
- rotation: 0,
-
- style: defaultStyle,
- },
-
- shouldRender(shape, prev) {
- return shape.points !== prev.points || shape.style !== prev.style
- },
-
- render(shape, { isHovered, isDarkMode }) {
- const { points, style } = shape
-
- const styles = getShapeStyle(style, isDarkMode)
-
- const strokeWidth = +styles.strokeWidth
-
- const shouldFill =
- style.isFilled &&
- points.length > 3 &&
- vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2
-
- // For very short lines, draw a point instead of a line
-
- if (points.length > 0 && points.length < 3) {
- const sw = strokeWidth * 0.618
-
- return (
- <circle
- r={strokeWidth * 0.618}
- fill={styles.stroke}
- stroke={styles.stroke}
- strokeWidth={sw}
- pointerEvents="all"
- filter={isHovered ? 'url(#expand)' : 'none'}
- />
- )
- }
-
- // For drawn lines, draw a line from the path cache
-
- if (shape.style.dash === DashStyle.Draw) {
- const polygonPathData = getFromCache(polygonCache, points, (cache) => {
- cache.set(shape.points, getFillPath(shape))
- })
-
- const drawPathData = getFromCache(drawPathCache, points, (cache) => {
- cache.set(shape.points, getDrawStrokePath(shape))
- })
-
- return (
- <>
- {shouldFill && (
- <path
- d={polygonPathData}
- stroke="none"
- fill={styles.fill}
- strokeLinejoin="round"
- strokeLinecap="round"
- pointerEvents="fill"
- />
- )}
- <path
- d={drawPathData}
- fill={styles.stroke}
- stroke={styles.stroke}
- strokeWidth={strokeWidth}
- strokeLinejoin="round"
- strokeLinecap="round"
- pointerEvents="all"
- filter={isHovered ? 'url(#expand)' : 'none'}
- />
- </>
- )
- }
-
- // For solid, dash and dotted lines, draw a regular stroke path
-
- const strokeDasharray = {
- [DashStyle.Draw]: 'none',
- [DashStyle.Solid]: `none`,
- [DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`,
- [DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`,
- }[style.dash]
-
- const strokeDashoffset = {
- [DashStyle.Draw]: 'none',
- [DashStyle.Solid]: `none`,
- [DashStyle.Dotted]: `-${strokeWidth / 20}`,
- [DashStyle.Dashed]: `-${strokeWidth}`,
- }[style.dash]
-
- if (!simplePathCache.has(points)) {
- simplePathCache.set(points, getSolidStrokePath(shape))
- }
-
- const path = simplePathCache.get(points)
-
- const sw = strokeWidth * 1.618
-
- return (
- <>
- <path
- d={path}
- fill={shouldFill ? styles.fill : 'none'}
- stroke="transparent"
- strokeWidth={Math.min(4, strokeWidth * 2)}
- strokeLinejoin="round"
- strokeLinecap="round"
- pointerEvents={shouldFill ? 'all' : 'stroke'}
- />
- <path
- d={path}
- fill="transparent"
- stroke={styles.stroke}
- strokeWidth={sw}
- strokeDasharray={strokeDasharray}
- strokeDashoffset={strokeDashoffset}
- strokeLinejoin="round"
- strokeLinecap="round"
- pointerEvents="stroke"
- filter={isHovered ? 'url(#expand)' : 'none'}
- />
- </>
- )
- },
-
- getBounds(shape) {
- const bounds = getFromCache(this.boundsCache, shape, (cache) => {
- cache.set(shape, getBoundsFromPoints(shape.points))
- })
-
- return translateBounds(bounds, shape.point)
- },
-
- getRotatedBounds(shape) {
- return translateBounds(
- getBoundsFromPoints(shape.points, shape.rotation),
- shape.point
- )
- },
-
- getCenter(shape) {
- return getBoundsCenter(this.getBounds(shape))
- },
-
- hitTest() {
- return true
- },
-
- hitTestBounds(this, shape, brushBounds) {
- // Test axis-aligned shape
- if (shape.rotation === 0) {
- return (
- boundsContain(brushBounds, this.getBounds(shape)) ||
- intersectPolylineBounds(
- shape.points,
- translateBounds(brushBounds, vec.neg(shape.point))
- ).length > 0
- )
- }
-
- // Test rotated shape
- const rBounds = this.getRotatedBounds(shape)
-
- const rotatedBounds = getFromCache(rotatedCache, shape, (cache) => {
- const c = getBoundsCenter(getBoundsFromPoints(shape.points))
- cache.set(
- shape,
- shape.points.map((pt) => vec.rotWith(pt, c, shape.rotation))
- )
- })
-
- return (
- boundsContain(brushBounds, rBounds) ||
- intersectPolylineBounds(
- rotatedBounds,
- translateBounds(brushBounds, vec.neg(shape.point))
- ).length > 0
- )
- },
-
- transform(shape, bounds, { initialShape, scaleX, scaleY }) {
- const initialShapeBounds = getFromCache(
- this.boundsCache,
- initialShape,
- (cache) => {
- cache.set(shape, getBoundsFromPoints(initialShape.points))
- }
- )
-
- shape.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 = getBoundsFromPoints(shape.points)
-
- shape.point = vec.sub(
- [bounds.minX, bounds.minY],
- [newBounds.minX, newBounds.minY]
- )
- return this
- },
-
- // applyStyles(shape, style) {
- // const styles = { ...shape.style, ...style }
- // styles.dash = DashStyle.Solid
- // shape.style = styles
- // return this
- // },
-
- onSessionComplete(shape) {
- const bounds = this.getBounds(shape)
-
- const [x1, y1] = vec.sub([bounds.minX, bounds.minY], shape.point)
-
- shape.points = shape.points.map(([x0, y0, p]) => [x0 - x1, y0 - y1, p])
-
- this.translateTo(shape, vec.add(shape.point, [x1, y1]))
-
- return this
- },
- })
-
- export default draw
-
- const simulatePressureSettings = {
- simulatePressure: true,
- }
-
- const realPressureSettings = {
- easing: (t: number) => t * t,
- simulatePressure: false,
- start: { taper: 1 },
- end: { taper: 1 },
- }
-
- /**
- * Get the fill path for a closed draw shape.
- *
- * ### Example
- *
- *```ts
- * someCache.set(getFillPath(shape))
- *```
- */
- function getFillPath(shape: DrawShape) {
- const styles = getShapeStyle(shape.style)
-
- if (shape.points.length < 2) {
- return ''
- }
-
- return getSvgPathFromStroke(
- getStrokePoints(shape.points, {
- size: 1 + +styles.strokeWidth * 2,
- thinning: 0.85,
- end: { taper: +styles.strokeWidth * 20 },
- start: { taper: +styles.strokeWidth * 20 },
- }).map((pt) => pt.point)
- )
- }
-
- /**
- * Get the path data for a draw stroke.
- *
- * ### Example
- *
- *```ts
- * someCache.set(getDrawStrokePath(shape))
- *```
- */
- function getDrawStrokePath(shape: DrawShape) {
- const styles = getShapeStyle(shape.style)
-
- if (shape.points.length < 2) {
- return ''
- }
-
- const options =
- shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
-
- const stroke = getStroke(shape.points, {
- size: 1 + +styles.strokeWidth * 2,
- thinning: 0.85,
- end: { taper: +styles.strokeWidth * 10 },
- start: { taper: +styles.strokeWidth * 10 },
- ...options,
- })
-
- return getSvgPathFromStroke(stroke)
- }
-
- function getSolidStrokePath(shape: DrawShape) {
- let { points } = shape
-
- let len = points.length
-
- if (len === 0) return 'M 0 0 L 0 0'
- if (len < 3) return `M ${points[0][0]} ${points[0][1]}`
-
- points = getStrokePoints(points).map((pt) => pt.point)
-
- len = points.length
-
- const d = points.reduce(
- (acc, [x0, y0], i, arr) => {
- if (i === len - 1) {
- acc.push('L', x0, y0)
- return acc
- }
-
- const [x1, y1] = arr[i + 1]
- acc.push(
- x0.toFixed(2),
- y0.toFixed(2),
- ((x0 + x1) / 2).toFixed(2),
- ((y0 + y1) / 2).toFixed(2)
- )
- return acc
- },
- ['M', points[0][0], points[0][1], 'Q']
- )
-
- const path = d.join(' ').replaceAll(/(\s[0-9]*\.[0-9]{2})([0-9]*)\b/g, '$1')
-
- return path
- }
-
- // /**
- // * Get the path data for a solid draw stroke.
- // *
- // * ### Example
- // *
- // *```ts
- // * getSolidStrokePath(shape)
- // *```
- // */
- // function getSolidDrawStrokePath(shape: DrawShape) {
- // const styles = getShapeStyle(shape.style)
-
- // if (shape.points.length < 2) {
- // return ''
- // }
-
- // const options =
- // shape.points[1][2] === 0.5 ? simulatePressureSettings : realPressureSettings
-
- // const stroke = getStroke(shape.points, {
- // size: 1 + +styles.strokeWidth * 2,
- // thinning: 0,
- // end: { taper: +styles.strokeWidth * 10 },
- // start: { taper: +styles.strokeWidth * 10 },
- // ...options,
- // })
-
- // return getSvgPathFromStroke(stroke)
- // }
|