[fix] Line shape rendering (#1825)

This PR fixes several bugs in the line shape, both rendering in the app
and in SVG exports.

<img width="634" alt="image"
src="https://github.com/tldraw/tldraw/assets/23072548/473db62f-2f18-40ef-992a-f5dac895d4ae">
<img width="525" alt="image"
src="https://github.com/tldraw/tldraw/assets/23072548/0673767c-b0e5-415c-962c-92bb1249261e">


### Change Type

- [x] `patch` — Bug fix

### Test Plan

1. Make line shapes.
2. Export them as SVGs.
This commit is contained in:
Steve Ruiz 2023-08-25 19:24:30 +02:00 committed by GitHub
parent b203967341
commit ba7a95d5f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 144 deletions

View file

@ -29,7 +29,7 @@ export class CubicBezier2d extends Polyline2d {
const vertices = [] as Vec2d[]
const { a, b, c, d } = this
// we'll always use ten vertices for each bezier curve
for (let i = 0, n = 10; i < n; i++) {
for (let i = 0, n = 10; i <= n; i++) {
const t = i / n
vertices.push(
new Vec2d(
@ -48,18 +48,7 @@ export class CubicBezier2d extends Polyline2d {
}
midPoint() {
const { a, b, c, d } = this
const t = 0.5
return new Vec2d(
(1 - t) * (1 - t) * (1 - t) * a.x +
3 * ((1 - t) * (1 - t)) * t * b.x +
3 * (1 - t) * (t * t) * c.x +
t * t * t * d.x,
(1 - t) * (1 - t) * (1 - t) * a.y +
3 * ((1 - t) * (1 - t)) * t * b.y +
3 * (1 - t) * (t * t) * c.y +
t * t * t * d.y
)
return getAtT(this, 0.5)
}
nearestPoint(A: Vec2d): Vec2d {
@ -78,3 +67,17 @@ export class CubicBezier2d extends Polyline2d {
return nearest
}
}
function getAtT(segment: CubicBezier2d, t: number) {
const { a, b, c, d } = segment
return new Vec2d(
(1 - t) * (1 - t) * (1 - t) * a.x +
3 * ((1 - t) * (1 - t)) * t * b.x +
3 * (1 - t) * (t * t) * c.x +
t * t * t * d.x,
(1 - t) * (1 - t) * (1 - t) * a.y +
3 * ((1 - t) * (1 - t)) * t * b.y +
3 * (1 - t) * (t * t) * c.y +
t * t * t * d.y
)
}

View file

@ -23,9 +23,9 @@ import { STROKE_SIZES } from '../shared/default-shape-constants'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { getDrawLinePathData } from '../shared/polygon-helpers'
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
import { getLineSvg } from './components/getLineSvg'
import {
getSvgPathForBezierCurve,
getSvgPathForCubicSpline,
getSvgPathForEdge,
getSvgPathForLineGeometry,
} from './components/svg'
@ -193,8 +193,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
const outline = spline.points
const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge
return (
<SVGContainer id={shape.id}>
<ShapeFill d={pathData} fill={'none'} color={color} theme={theme} />
@ -215,7 +213,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
key={i}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
d={fn(segment as any, i === 0)}
d={getSvgPathForEdge(segment as any, true)}
fill="none"
/>
)
@ -262,8 +260,6 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
}
if (dash === 'dashed' || dash === 'dotted') {
const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge
return (
<SVGContainer id={shape.id}>
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} />
@ -284,7 +280,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
key={i}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
d={fn(segment as any, i === 0)}
d={getSvgPathForBezierCurve(segment as any, true)}
fill="none"
/>
)
@ -336,7 +332,75 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const color = theme[shape.props.color].solid
const spline = getGeometryForLineShape(shape)
return getLineSvg(shape, spline, color, STROKE_SIZES[shape.props.size])
const strokeWidth = STROKE_SIZES[shape.props.size]
switch (shape.props.dash) {
case 'draw': {
let pathData: string
if (spline instanceof CubicSpline2d) {
pathData = getLineDrawPath(shape, spline, strokeWidth)
} else {
const [_, outerPathData] = getDrawLinePathData(shape.id, spline.points, strokeWidth)
pathData = outerPathData
}
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('stroke-width', strokeWidth + 'px')
p.setAttribute('stroke', color)
p.setAttribute('fill', 'none')
p.setAttribute('d', pathData)
return p
}
case 'solid': {
let pathData: string
if (spline instanceof CubicSpline2d) {
pathData = getSvgPathForCubicSpline(spline, false)
} else {
const outline = spline.points
pathData = 'M' + outline[0] + 'L' + outline.slice(1)
}
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('stroke-width', strokeWidth + 'px')
p.setAttribute('stroke', color)
p.setAttribute('fill', 'none')
p.setAttribute('d', pathData)
return p
}
default: {
const { segments } = spline
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
g.setAttribute('stroke', color)
g.setAttribute('stroke-width', strokeWidth.toString())
const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge
segments.forEach((segment, i) => {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
segment.length,
strokeWidth,
{
style: shape.props.dash,
start: i > 0 ? 'outset' : 'none',
end: i < segments.length - 1 ? 'outset' : 'none',
}
)
path.setAttribute('stroke-dasharray', strokeDasharray.toString())
path.setAttribute('stroke-dashoffset', strokeDashoffset.toString())
path.setAttribute('d', fn(segment as any, true))
path.setAttribute('fill', 'none')
g.appendChild(path)
})
return g
}
}
}
}

View file

@ -1,114 +0,0 @@
import { CubicSpline2d, Polyline2d, TLDefaultDashStyle, TLLineShape } from '@tldraw/editor'
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
import { getLineDrawPath } from './getLinePath'
import { getSvgPathForBezierCurve, getSvgPathForEdge, getSvgPathForLineGeometry } from './svg'
export function getDrawLineShapeSvg({
shape,
strokeWidth,
spline,
color,
}: {
shape: TLLineShape
strokeWidth: number
spline: CubicSpline2d | Polyline2d
color: string
}) {
const pfPath = getLineDrawPath(shape, spline, strokeWidth)
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('stroke-width', '0')
p.setAttribute('stroke', 'none')
p.setAttribute('fill', color)
p.setAttribute('d', pfPath)
return p
}
export function getDashedLineShapeSvg({
dash,
strokeWidth,
spline,
color,
}: {
dash: TLDefaultDashStyle
strokeWidth: number
spline: CubicSpline2d | Polyline2d
color: string
}) {
const { segments } = spline
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
g.setAttribute('stroke', color)
g.setAttribute('stroke-width', strokeWidth.toString())
const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge
segments.forEach((segment, i) => {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(segment.length, strokeWidth, {
style: dash,
start: i > 0 ? 'outset' : 'none',
end: i < segments.length - 1 ? 'outset' : 'none',
})
path.setAttribute('stroke-dasharray', strokeDasharray.toString())
path.setAttribute('stroke-dashoffset', strokeDashoffset.toString())
path.setAttribute('d', fn(segment as any, i === 0))
path.setAttribute('fill', 'none')
g.appendChild(path)
})
return g
}
export function getSolidLineShapeSvg({
strokeWidth,
spline,
color,
}: {
strokeWidth: number
spline: CubicSpline2d | Polyline2d
color: string
}) {
const path = getSvgPathForLineGeometry(spline)
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
p.setAttribute('stroke-width', strokeWidth.toString())
p.setAttribute('stroke', color)
p.setAttribute('fill', 'none')
p.setAttribute('d', path)
return p
}
export function getLineSvg(
shape: TLLineShape,
spline: CubicSpline2d | Polyline2d,
color: string,
strokeWidth: number
) {
switch (shape.props.dash) {
case 'draw':
return getDrawLineShapeSvg({
shape,
strokeWidth,
spline,
color,
})
case 'solid':
return getSolidLineShapeSvg({
strokeWidth,
spline,
color,
})
default:
return getDashedLineShapeSvg({
strokeWidth,
spline,
dash: shape.props.dash,
color,
})
}
}

View file

@ -1,11 +1,20 @@
import { CubicBezier2d, CubicSpline2d, Edge2d, Polyline2d, Vec2d } from '@tldraw/editor'
import {
CubicBezier2d,
CubicSpline2d,
Edge2d,
Polyline2d,
Vec2d,
toDomPrecision,
} from '@tldraw/editor'
export function getSvgPathForEdge(edge: Edge2d, first: boolean) {
const { start, end } = edge
if (first) {
return `M${start.x},${start.y} L${end.x},${end.y} `
return `M${toDomPrecision(start.x)},${toDomPrecision(start.y)} L${toDomPrecision(
end.x
)},${toDomPrecision(end.y)} `
}
return `${end.x},${end.y} `
return `${toDomPrecision(end.x)},${toDomPrecision(end.y)} `
}
export function getSvgPathForBezierCurve(curve: CubicBezier2d, first: boolean) {
@ -13,15 +22,15 @@ export function getSvgPathForBezierCurve(curve: CubicBezier2d, first: boolean) {
if (Vec2d.Equals(a, d)) return ''
return `${first ? `M${a.x.toFixed(2)},${a.y.toFixed(2)}C` : ``}${b.x.toFixed(2)},${b.y.toFixed(
2
)} ${c.x.toFixed(2)},${c.y.toFixed(2)} ${d.x.toFixed(2)},${d.y.toFixed(2)}`
return `${first ? `M${toDomPrecision(a.x)},${toDomPrecision(a.y)}` : ``}C${toDomPrecision(
b.x
)},${toDomPrecision(b.y)} ${toDomPrecision(c.x)},${toDomPrecision(c.y)} ${toDomPrecision(
d.x
)},${toDomPrecision(d.y)}`
}
export function getSvgPathForCubicSpline(spline: CubicSpline2d, isClosed: boolean) {
let d = ''
spline.segments.reduce((d, segment, i) => {
let d = spline.segments.reduce((d, segment, i) => {
return d + getSvgPathForBezierCurve(segment, i === 0)
}, '')