Merge branch 'main' into feature-27-round-arrow-angle

This commit is contained in:
Steve Ruiz 2021-07-08 13:36:31 +01:00
commit b9eed30518
11 changed files with 164 additions and 108 deletions

View file

@ -1,9 +1,14 @@
# tldraw
A tiny little drawing app by [steveruizok](https://twitter.com/steveruizok).
A tiny little drawing app.
Visit [tldraw.com](https://tldraw.com/).
## Author
- [steveruizok](https://twitter.com/steveruizok)
- ...and more!
## Support
To support this project (and gain access to the project while it is in development) you can [sponsor the author](https://github.com/sponsors/steveruizok) on GitHub. Thanks!

View file

@ -19,13 +19,13 @@
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "yarn lint && yarn type-check"
"pre-push": "yarn lint && yarn format && yarn type-check"
}
},
"lint-staged": {
"*.@(ts|tsx)": [
"yarn lint",
"yarn format"
"yarn format",
"yarn lint"
]
},
"dependencies": {
@ -95,4 +95,4 @@
"tabWidth": 2,
"useTabs": false
}
}
}

View file

@ -14,6 +14,7 @@ import {
clampToRotationToSegments,
lerpAngles,
clamp,
getFromCache,
} from 'utils'
import {
ArrowShape,
@ -31,6 +32,8 @@ import getStroke from 'perfect-freehand'
import React from 'react'
import { registerShapeUtils } from './register'
const pathCache = new WeakMap<ArrowShape['handles'], string>([])
// A cache for semi-expensive circles calculated from three points
function getCtp(shape: ArrowShape) {
const { start, end, bend } = shape.handles
@ -126,7 +129,9 @@ const arrow = registerShapeUtils<ArrowShape>({
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
const path = isDraw
? renderFreehandArrowShaft(shape)
? getFromCache(pathCache, shape.handles, (cache) =>
cache.set(shape.handles, renderFreehandArrowShaft(shape))
)
: 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
@ -168,7 +173,12 @@ const arrow = registerShapeUtils<ArrowShape>({
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
const path = isDraw
? renderCurvedFreehandArrowShaft(shape, circle)
? getFromCache(pathCache, shape.handles, (cache) =>
cache.set(
shape.handles,
renderCurvedFreehandArrowShaft(shape, circle)
)
)
: getArrowArcPath(start, end, circle, bend)
const arcLength = getArcLength(
@ -277,15 +287,15 @@ const arrow = registerShapeUtils<ArrowShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
const { start, bend, end } = shape.handles
this.boundsCache.set(
cache.set(
shape,
getBoundsFromPoints([start.point, bend.point, end.point])
)
}
})
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getRotatedBounds(shape) {
@ -597,8 +607,8 @@ function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
size: strokeWidth / 2,
thinning: 0.5 + getRandom() * 0.3,
easing: (t) => t * t,
end: { taper: 0 },
start: { taper: 1 + 32 * (st * st * st) },
end: { taper: shape.decorations.end ? 1 : 1 + 32 * (st * st * st) },
start: { taper: shape.decorations.start ? 1 : 1 + 32 * (st * st * st) },
simulatePressure: true,
streamline: 0.01,
last: true,

View file

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getFromCache, uniqueId } from 'utils/utils'
import { DotShape, ShapeType } from 'types'
import { intersectCircleBounds } from 'utils/intersections'
import { boundsContained, translateBounds } from 'utils'
@ -30,20 +30,18 @@ const dot = registerShapeUtils<DotShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = {
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
cache.set(shape, {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
})
})
this.boundsCache.set(shape, bounds)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getRotatedBounds(shape) {

View file

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getFromCache, uniqueId } from 'utils/utils'
import vec from 'utils/vec'
import { DashStyle, DrawShape, ShapeType } from 'types'
import { intersectPolylineBounds } from 'utils/intersections'
@ -69,26 +69,26 @@ const draw = registerShapeUtils<DrawShape>({
// For drawn lines, draw a line from the path cache
if (shape.style.dash === DashStyle.Draw) {
if (!drawPathCache.has(points)) {
drawPathCache.set(shape.points, getDrawStrokePath(shape))
}
const polygonPathData = getFromCache(polygonCache, points, (cache) => {
cache.set(shape.points, getFillPath(shape))
})
if (shouldFill && !polygonCache.has(points)) {
polygonCache.set(shape.points, getFillPath(shape))
}
const drawPathData = getFromCache(drawPathCache, points, (cache) => {
cache.set(shape.points, getDrawStrokePath(shape))
})
return (
<g id={id}>
{shouldFill && (
<path
d={polygonCache.get(points)}
d={polygonPathData}
strokeWidth="0"
stroke="none"
fill={styles.fill}
/>
)}
<path
d={drawPathCache.get(points)}
d={drawPathData}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={strokeWidth}
@ -100,15 +100,17 @@ const draw = registerShapeUtils<DrawShape>({
// 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}`,
[DashStyle.Solid]: `none`,
}[style.dash]
const strokeDashoffset = {
[DashStyle.Draw]: 'none',
[DashStyle.Solid]: `none`,
[DashStyle.Dotted]: `-${strokeWidth / 20}`,
[DashStyle.Dashed]: `-${strokeWidth}`,
[DashStyle.Solid]: `none`,
}[style.dash]
if (!simplePathCache.has(points)) {
@ -140,12 +142,11 @@ const draw = registerShapeUtils<DrawShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = getBoundsFromPoints(shape.points)
this.boundsCache.set(shape, bounds)
}
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
cache.set(shape, getBoundsFromPoints(shape.points))
})
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getRotatedBounds(shape) {
@ -183,25 +184,32 @@ const draw = registerShapeUtils<DrawShape>({
// Test rotated shape
const rBounds = this.getRotatedBounds(shape)
if (!rotatedCache.has(shape)) {
const rotatedBounds = getFromCache(rotatedCache, shape, (cache) => {
const c = getBoundsCenter(getBoundsFromPoints(shape.points))
rotatedCache.set(
cache.set(
shape,
shape.points.map((pt) => vec.rotWith(pt, c, shape.rotation))
)
}
})
return (
boundsContain(brushBounds, rBounds) ||
intersectPolylineBounds(
rotatedCache.get(shape),
rotatedBounds,
translateBounds(brushBounds, vec.neg(shape.point))
).length > 0
)
},
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
const initialShapeBounds = this.boundsCache.get(initialShape)
const initialShapeBounds = getFromCache(
this.boundsCache,
initialShape,
(cache) => {
cache.set(shape, getBoundsFromPoints(initialShape.points))
}
)
shape.points = initialShape.points.map(([x, y, r]) => {
return [
bounds.width *
@ -314,15 +322,6 @@ function getDrawStrokePath(shape: DrawShape) {
return getSvgPathFromStroke(stroke)
}
/**
* Get the path data for a solid draw stroke.
*
* ### Example
*
*```ts
* getSolidStrokePath(shape)
*```
*/
function getSolidStrokePath(shape: DrawShape) {
let { points } = shape
@ -331,13 +330,7 @@ function getSolidStrokePath(shape: DrawShape) {
if (len === 0) return 'M 0 0 L 0 0'
if (len < 3) return `M ${points[0][0]} ${points[0][1]}`
// Remove duplicates from points
points = points.reduce<number[][]>((acc, pt, i) => {
if (i === 0 || !vec.isEqual(pt, acc[i - 1])) {
acc.push(pt)
}
return acc
}, [])
points = getStrokePoints(points).map((pt) => pt.point)
len = points.length
@ -364,3 +357,33 @@ function getSolidStrokePath(shape: DrawShape) {
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)
// }

View file

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getFromCache, uniqueId } from 'utils/utils'
import vec from 'utils/vec'
import { LineShape, ShapeType } from 'types'
import { intersectCircleBounds } from 'utils/intersections'
@ -43,20 +43,18 @@ const line = registerShapeUtils<LineShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = {
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
cache.set(shape, {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
})
})
this.boundsCache.set(shape, bounds)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getRotatedBounds(shape) {

View file

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getFromCache, uniqueId } from 'utils/utils'
import vec from 'utils/vec'
import { PolylineShape, ShapeType } from 'types'
import { intersectPolylineBounds } from 'utils/intersections'
@ -45,11 +45,11 @@ const polyline = registerShapeUtils<PolylineShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
}
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
cache.set(shape, getBoundsFromPoints(shape.points))
})
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getRotatedBounds(shape) {

View file

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getFromCache, uniqueId } from 'utils/utils'
import vec from 'utils/vec'
import { RayShape, ShapeType } from 'types'
import { intersectCircleBounds } from 'utils/intersections'
@ -46,20 +46,18 @@ const ray = registerShapeUtils<RayShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = {
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
cache.set(shape, {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
})
})
this.boundsCache.set(shape, bounds)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
getCenter(shape) {

View file

@ -1,4 +1,4 @@
import { uniqueId, getPerfectDashProps } from 'utils/utils'
import { uniqueId, getPerfectDashProps, getFromCache } from 'utils/utils'
import vec from 'utils/vec'
import { DashStyle, RectangleShape, ShapeType } from 'types'
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
@ -34,11 +34,9 @@ const rectangle = registerShapeUtils<RectangleShape>({
const strokeWidth = +styles.strokeWidth
if (style.dash === DashStyle.Draw) {
if (!pathCache.has(shape.size)) {
renderPath(shape)
}
const path = pathCache.get(shape.size)
const pathData = getFromCache(pathCache, shape.size, (cache) => {
cache.set(shape.size, renderPath(shape))
})
return (
<g id={id}>
@ -54,7 +52,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
stroke={styles.stroke}
/>
<path
d={path}
d={pathData}
fill={styles.stroke}
stroke={styles.stroke}
strokeWidth={styles.strokeWidth}
@ -114,21 +112,19 @@ const rectangle = registerShapeUtils<RectangleShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
const [width, height] = shape.size
const bounds = {
cache.set(shape, {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
}
})
})
this.boundsCache.set(shape, bounds)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
return translateBounds(bounds, shape.point)
},
hitTest() {
@ -218,5 +214,5 @@ function renderPath(shape: RectangleShape) {
}
)
pathCache.set(shape.size, getSvgPathFromStroke(stroke))
return getSvgPathFromStroke(stroke)
}

View file

@ -1,4 +1,4 @@
import { uniqueId, isMobile } from 'utils/utils'
import { uniqueId, isMobile, getFromCache } from 'utils/utils'
import vec from 'utils/vec'
import TextAreaUtils from 'utils/text-area'
import { TextShape, ShapeType } from 'types'
@ -13,12 +13,8 @@ import state from 'state'
import { registerShapeUtils } from './register'
// A div used for measurement
document.getElementById('__textMeasure')?.remove()
if (document.getElementById('__textMeasure')) {
document.getElementById('__textMeasure').remove()
}
// A div used for measurement
const mdiv = document.createElement('pre')
mdiv.id = '__textMeasure'
@ -128,6 +124,10 @@ const text = registerShapeUtils<TextShape>({
const fontSize = getFontSize(shape.style.size) * shape.scale
const lineHeight = fontSize * 1.4
if (ref === undefined) {
throw Error('This component should receive a ref.')
}
if (!isEditing) {
return (
<g id={id} pointerEvents="none">
@ -155,7 +155,6 @@ const text = registerShapeUtils<TextShape>({
</g>
)
}
return (
<foreignObject
id={id}
@ -164,7 +163,7 @@ const text = registerShapeUtils<TextShape>({
pointerEvents="none"
>
<StyledTextArea
ref={ref}
ref={ref as React.RefObject<HTMLTextAreaElement>}
style={{
font,
color: styles.stroke,
@ -178,7 +177,7 @@ const text = registerShapeUtils<TextShape>({
autoSave="false"
placeholder=""
color={styles.stroke}
autoFocus={isMobile() ? true : false}
autoFocus={!!isMobile()}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
@ -190,14 +189,14 @@ const text = registerShapeUtils<TextShape>({
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
mdiv.innerHTML = shape.text + '&zwj;'
const bounds = getFromCache(this.boundsCache, shape, (cache) => {
mdiv.innerHTML = `${shape.text}&zwj;`
mdiv.style.font = getFontStyle(shape.scale, shape.style)
const [minX, minY] = shape.point
const [width, height] = [mdiv.offsetWidth, mdiv.offsetHeight]
this.boundsCache.set(shape, {
cache.set(shape, {
minX,
maxX: minX + width,
minY,
@ -205,9 +204,9 @@ const text = registerShapeUtils<TextShape>({
width,
height,
})
}
})
return this.boundsCache.get(shape)
return bounds
},
hitTest() {

View file

@ -1480,6 +1480,35 @@ export function getBoundsCenter(bounds: Bounds): number[] {
/* Lists and Collections */
/* -------------------------------------------------- */
/**
* Get a value from a cache (a WeakMap), filling the value if it is not present.
*
* ### Example
*
*```ts
* getFromCache(boundsCache, shape, (cache) => cache.set(shape, "value"))
*```
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function getFromCache<V, I extends object>(
cache: WeakMap<I, V>,
item: I,
replace: (cache: WeakMap<I, V>) => void
): V {
let value = cache.get(item)
if (value === undefined) {
replace(cache)
value = cache.get(item)
if (value === undefined) {
throw Error('Cache did not include item!')
}
}
return value
}
/**
* Get a unique string id.
*/