[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:
parent
b203967341
commit
ba7a95d5f0
4 changed files with 106 additions and 144 deletions
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}, '')
|
||||
|
||||
|
|
Loading…
Reference in a new issue