diff --git a/packages/editor/src/lib/primitives/geometry/CubicBezier2d.ts b/packages/editor/src/lib/primitives/geometry/CubicBezier2d.ts index cd659885e..7bced191e 100644 --- a/packages/editor/src/lib/primitives/geometry/CubicBezier2d.ts +++ b/packages/editor/src/lib/primitives/geometry/CubicBezier2d.ts @@ -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 + ) +} diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx index 1ed664de7..9408dd100 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx @@ -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 { const outline = spline.points const pathData = 'M' + outline[0] + 'L' + outline.slice(1) - const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge - return ( @@ -215,7 +213,7 @@ export class LineShapeUtil extends ShapeUtil { 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 { } if (dash === 'dashed' || dash === 'dotted') { - const fn = spline instanceof CubicSpline2d ? getSvgPathForBezierCurve : getSvgPathForEdge - return ( @@ -284,7 +280,7 @@ export class LineShapeUtil extends ShapeUtil { 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 { 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 + } + } } } diff --git a/packages/tldraw/src/lib/shapes/line/components/getLineSvg.ts b/packages/tldraw/src/lib/shapes/line/components/getLineSvg.ts deleted file mode 100644 index 670d40657..000000000 --- a/packages/tldraw/src/lib/shapes/line/components/getLineSvg.ts +++ /dev/null @@ -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, - }) - } -} diff --git a/packages/tldraw/src/lib/shapes/line/components/svg.ts b/packages/tldraw/src/lib/shapes/line/components/svg.ts index 23617daf2..09619ee7f 100644 --- a/packages/tldraw/src/lib/shapes/line/components/svg.ts +++ b/packages/tldraw/src/lib/shapes/line/components/svg.ts @@ -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) }, '')