[tinyish] Simplify / skip some work in Shape (#3176)

This PR is a minor cleanup of the Shape component.

Here we:
- use some dumb memoized info to avoid unnecessary style changes
- move the dpr check up out of the shapes themselves, avoiding renders
on instance state changes

Culled shapes:
- move the props setting on the culled shape component to a layout
reactor
- no longer set the height / width on the culled shape component
- no longer update the culled shape component when the shape changes

Random:
- move the arrow shape defs to the arrow shape util (using that neat API
we didn't used to have)

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Test Plan

1. Use shapes
2. Use culled shapes

### Release Notes

- SDK: minor improvements to the Shape component
This commit is contained in:
Steve Ruiz 2024-03-17 21:37:37 +00:00 committed by GitHub
parent 4e0df0730d
commit 4801b35768
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 112 deletions

View file

@ -332,6 +332,8 @@ input,
.tl-shape__culled { .tl-shape__culled {
position: relative; position: relative;
background-color: var(--color-culled); background-color: var(--color-culled);
width: 100%;
height: 100%;
} }
/* ---------------- Shape Containers ---------------- */ /* ---------------- Shape Containers ---------------- */

View file

@ -1,26 +1,27 @@
import { track, useLayoutReaction, useStateTracking } from '@tldraw/state' import { useLayoutReaction, useStateTracking } from '@tldraw/state'
import { IdOf } from '@tldraw/store'
import { TLShape, TLShapeId } from '@tldraw/tlschema' import { TLShape, TLShapeId } from '@tldraw/tlschema'
import * as React from 'react' import { memo, useCallback, useLayoutEffect, useRef } from 'react'
import { ShapeUtil } from '../editor/shapes/ShapeUtil' import { ShapeUtil } from '../editor/shapes/ShapeUtil'
import { useEditor } from '../hooks/useEditor' import { useEditor } from '../hooks/useEditor'
import { useEditorComponents } from '../hooks/useEditorComponents' import { useEditorComponents } from '../hooks/useEditorComponents'
import { Mat } from '../primitives/Mat' import { Mat } from '../primitives/Mat'
import { toDomPrecision } from '../primitives/utils' import { toDomPrecision } from '../primitives/utils'
import { nearestMultiple } from '../utils/nearestMultiple' import { setStyleProperty } from '../utils/dom'
import { OptionalErrorBoundary } from './ErrorBoundary' import { OptionalErrorBoundary } from './ErrorBoundary'
/* /*
This component renders shapes on the canvas. There are two stages: positioning This component renders shapes on the canvas. There are two stages: positioning
and styling the shape's container using CSS, and then rendering the shape's and styling the shape's container using CSS, and then rendering the shape's
JSX using its shape util's render method. Rendering the "inside" of a shape is JSX using its shape util's render method. Rendering the "inside" of a shape is
more expensive than positioning it or changing its color, so we use React.memo more expensive than positioning it or changing its color, so we use memo
to wrap the inner shape and only re-render it when the shape's props change. to wrap the inner shape and only re-render it when the shape's props change.
The shape also receives props for its index and opacity. The index is used to The shape also receives props for its index and opacity. The index is used to
determine the z-index of the shape, and the opacity is used to set the shape's determine the z-index of the shape, and the opacity is used to set the shape's
opacity based on its own opacity and that of its parent's. opacity based on its own opacity and that of its parent's.
*/ */
export const Shape = track(function Shape({ export const Shape = memo(function Shape({
id, id,
shape, shape,
util, util,
@ -28,6 +29,7 @@ export const Shape = track(function Shape({
backgroundIndex, backgroundIndex,
opacity, opacity,
isCulled, isCulled,
dprMultiple,
}: { }: {
id: TLShapeId id: TLShapeId
shape: TLShape shape: TLShape
@ -36,56 +38,79 @@ export const Shape = track(function Shape({
backgroundIndex: number backgroundIndex: number
opacity: number opacity: number
isCulled: boolean isCulled: boolean
dprMultiple: number
}) { }) {
const editor = useEditor() const editor = useEditor()
const { ShapeErrorFallback } = useEditorComponents() const { ShapeErrorFallback } = useEditorComponents()
const containerRef = React.useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const backgroundContainerRef = React.useRef<HTMLDivElement>(null) const bgContainerRef = useRef<HTMLDivElement>(null)
const setProperty = React.useCallback((property: string, value: string) => { const memoizedStuffRef = useRef({
containerRef.current?.style.setProperty(property, value) transform: '',
backgroundContainerRef.current?.style.setProperty(property, value) clipPath: 'none',
}, []) width: 0,
height: 0,
})
useLayoutReaction('set shape stuff', () => { useLayoutReaction('set shape stuff', () => {
const shape = editor.getShape(id) const shape = editor.getShape(id)
if (!shape) return // probably the shape was just deleted if (!shape) return // probably the shape was just deleted
const pageTransform = editor.getShapePageTransform(id) const prev = memoizedStuffRef.current
const transform = Mat.toCssString(pageTransform)
setProperty('transform', transform)
const clipPath = editor.getShapeClipPath(id) // Clip path
setProperty('clip-path', clipPath ?? 'none') const clipPath = editor.getShapeClipPath(id) ?? 'none'
if (clipPath !== prev.clipPath) {
setStyleProperty(containerRef.current, 'clip-path', clipPath)
setStyleProperty(bgContainerRef.current, 'clip-path', clipPath)
prev.clipPath = clipPath
}
// Page transform
const transform = Mat.toCssString(editor.getShapePageTransform(id))
if (transform !== prev.transform) {
setStyleProperty(containerRef.current, 'transform', transform)
setStyleProperty(bgContainerRef.current, 'transform', transform)
prev.transform = transform
}
// Width / Height
// We round the shape width and height up to the nearest multiple of dprMultiple
// to avoid the browser making miscalculations when applying the transform.
const bounds = editor.getShapeGeometry(shape).bounds const bounds = editor.getShapeGeometry(shape).bounds
const dpr = Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100
// dprMultiple is the smallest number we can multiply dpr by to get an integer
// it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
const dprMultiple = nearestMultiple(dpr)
// We round the shape width and height up to the nearest multiple of dprMultiple to avoid the browser
// making miscalculations when applying the transform.
const widthRemainder = bounds.w % dprMultiple const widthRemainder = bounds.w % dprMultiple
const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
const heightRemainder = bounds.h % dprMultiple const heightRemainder = bounds.h % dprMultiple
const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
const height = heightRemainder === 0 ? bounds.h : bounds.h + (dprMultiple - heightRemainder) const height = heightRemainder === 0 ? bounds.h : bounds.h + (dprMultiple - heightRemainder)
setProperty('width', Math.max(width, dprMultiple) + 'px')
setProperty('height', Math.max(height, dprMultiple) + 'px') if (width !== prev.width || height !== prev.height) {
setStyleProperty(containerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
setStyleProperty(containerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
setStyleProperty(bgContainerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
setStyleProperty(bgContainerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
prev.width = width
prev.height = height
}
}) })
// Set the opacity of the container when the opacity changes // This stuff changes pretty infrequently, so we can change them together
React.useLayoutEffect(() => { useLayoutEffect(() => {
setProperty('opacity', opacity + '') const container = containerRef.current
containerRef.current?.style.setProperty('z-index', index + '') const bgContainer = bgContainerRef.current
backgroundContainerRef.current?.style.setProperty('z-index', backgroundIndex + '')
}, [opacity, index, backgroundIndex, setProperty])
const annotateError = React.useCallback( // Opacity
(error: any) => { setStyleProperty(container, 'opacity', opacity)
editor.annotateError(error, { origin: 'react.shape', willCrashApp: false }) setStyleProperty(bgContainer, 'opacity', opacity)
},
// Z-Index
setStyleProperty(container, 'z-index', index)
setStyleProperty(bgContainer, 'z-index', backgroundIndex)
}, [opacity, index, backgroundIndex])
const annotateError = useCallback(
(error: any) => editor.annotateError(error, { origin: 'shape', willCrashApp: false }),
[editor] [editor]
) )
@ -95,12 +120,12 @@ export const Shape = track(function Shape({
<> <>
{util.backgroundComponent && ( {util.backgroundComponent && (
<div <div
ref={backgroundContainerRef} ref={bgContainerRef}
className="tl-shape tl-shape-background" className="tl-shape tl-shape-background"
data-shape-type={shape.type} data-shape-type={shape.type}
draggable={false} draggable={false}
> >
{!isCulled && ( {isCulled ? null : (
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}> <OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
<InnerShapeBackground shape={shape} util={util} /> <InnerShapeBackground shape={shape} util={util} />
</OptionalErrorBoundary> </OptionalErrorBoundary>
@ -109,7 +134,7 @@ export const Shape = track(function Shape({
)} )}
<div ref={containerRef} className="tl-shape" data-shape-type={shape.type} draggable={false}> <div ref={containerRef} className="tl-shape" data-shape-type={shape.type} draggable={false}>
{isCulled ? ( {isCulled ? (
<CulledShape shape={shape} /> <CulledShape shapeId={shape.id} />
) : ( ) : (
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}> <OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
<InnerShape shape={shape} util={util} /> <InnerShape shape={shape} util={util} />
@ -120,17 +145,14 @@ export const Shape = track(function Shape({
) )
}) })
const InnerShape = React.memo( const InnerShape = memo(
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) { function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
return useStateTracking('InnerShape:' + shape.type, () => util.component(shape)) return useStateTracking('InnerShape:' + shape.type, () => util.component(shape))
}, },
(prev, next) => (prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
prev.shape.props === next.shape.props &&
prev.shape.meta === next.shape.meta &&
prev.util === next.util
) )
const InnerShapeBackground = React.memo( const InnerShapeBackground = memo(
function InnerShapeBackground<T extends TLShape>({ function InnerShapeBackground<T extends TLShape>({
shape, shape,
util, util,
@ -143,23 +165,18 @@ const InnerShapeBackground = React.memo(
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta (prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
) )
const CulledShape = React.memo( const CulledShape = function CulledShape<T extends TLShape>({ shapeId }: { shapeId: IdOf<T> }) {
function CulledShape<T extends TLShape>({ shape }: { shape: T }) { const editor = useEditor()
const editor = useEditor() const culledRef = useRef<HTMLDivElement>(null)
const bounds = editor.getShapeGeometry(shape).bounds
return ( useLayoutReaction('set shape stuff', () => {
<div const bounds = editor.getShapeGeometry(shapeId).bounds
className="tl-shape__culled" setStyleProperty(
style={{ culledRef.current,
transform: `translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision( 'transform',
bounds.minY `translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision(bounds.minY)}px)`
)}px)`,
width: Math.max(1, toDomPrecision(bounds.width)),
height: Math.max(1, toDomPrecision(bounds.height)),
}}
/>
) )
}, })
() => true
) return <div ref={culledRef} className="tl-shape__culled" />
}

View file

@ -1,8 +1,8 @@
import { react, track, useLayoutReaction, useValue } from '@tldraw/state' import { react, useLayoutReaction, useValue } from '@tldraw/state'
import { TLHandle, TLShapeId } from '@tldraw/tlschema' import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils' import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
import classNames from 'classnames' import classNames from 'classnames'
import React from 'react' import { Fragment, JSX, useEffect, useRef, useState } from 'react'
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants' import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants'
import { useCanvasEvents } from '../../hooks/useCanvasEvents' import { useCanvasEvents } from '../../hooks/useCanvasEvents'
import { useCoarsePointer } from '../../hooks/useCoarsePointer' import { useCoarsePointer } from '../../hooks/useCoarsePointer'
@ -17,6 +17,8 @@ import { Mat } from '../../primitives/Mat'
import { Vec } from '../../primitives/Vec' import { Vec } from '../../primitives/Vec'
import { toDomPrecision } from '../../primitives/utils' import { toDomPrecision } from '../../primitives/utils'
import { debugFlags } from '../../utils/debug-flags' import { debugFlags } from '../../utils/debug-flags'
import { setStyleProperty } from '../../utils/dom'
import { nearestMultiple } from '../../utils/nearestMultiple'
import { GeometryDebuggingView } from '../GeometryDebuggingView' import { GeometryDebuggingView } from '../GeometryDebuggingView'
import { LiveCollaborators } from '../LiveCollaborators' import { LiveCollaborators } from '../LiveCollaborators'
import { Shape } from '../Shape' import { Shape } from '../Shape'
@ -30,9 +32,9 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const { Background, SvgDefs } = useEditorComponents() const { Background, SvgDefs } = useEditorComponents()
const rCanvas = React.useRef<HTMLDivElement>(null) const rCanvas = useRef<HTMLDivElement>(null)
const rHtmlLayer = React.useRef<HTMLDivElement>(null) const rHtmlLayer = useRef<HTMLDivElement>(null)
const rHtmlLayer2 = React.useRef<HTMLDivElement>(null) const rHtmlLayer2 = useRef<HTMLDivElement>(null)
useScreenBounds(rCanvas) useScreenBounds(rCanvas)
useDocumentEvents() useDocumentEvents()
@ -42,11 +44,6 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
useFixSafariDoubleTapZoomPencilEvents(rCanvas) useFixSafariDoubleTapZoomPencilEvents(rCanvas)
useLayoutReaction('position layers', () => { useLayoutReaction('position layers', () => {
const htmlElm = rHtmlLayer.current
if (!htmlElm) return
const htmlElm2 = rHtmlLayer2.current
if (!htmlElm2) return
const { x, y, z } = editor.getCamera() const { x, y, z } = editor.getCamera()
// Because the html container has a width/height of 1px, we // Because the html container has a width/height of 1px, we
@ -58,8 +55,8 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision( const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision(
x + offset x + offset
)}px,${toDomPrecision(y + offset)}px)` )}px,${toDomPrecision(y + offset)}px)`
htmlElm.style.setProperty('transform', transform) setStyleProperty(rHtmlLayer.current, 'transform', transform)
htmlElm2.style.setProperty('transform', transform) setStyleProperty(rHtmlLayer2.current, 'transform', transform)
}) })
const events = useCanvasEvents() const events = useCanvasEvents()
@ -67,7 +64,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const shapeSvgDefs = useValue( const shapeSvgDefs = useValue(
'shapeSvgDefs', 'shapeSvgDefs',
() => { () => {
const shapeSvgDefsByKey = new Map<string, React.JSX.Element>() const shapeSvgDefsByKey = new Map<string, JSX.Element>()
for (const util of objectMapValues(editor.shapeUtils)) { for (const util of objectMapValues(editor.shapeUtils)) {
if (!util) return if (!util) return
const defs = util.getCanvasSvgDefs() const defs = util.getCanvasSvgDefs()
@ -98,10 +95,8 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
<svg className="tl-svg-context"> <svg className="tl-svg-context">
<defs> <defs>
{shapeSvgDefs} {shapeSvgDefs}
{Cursor && <Cursor />} <CursorDef />
<CollaboratorHint /> <CollaboratorHintDef />
<ArrowheadDot />
<ArrowheadCross />
{SvgDefs && <SvgDefs />} {SvgDefs && <SvgDefs />}
</defs> </defs>
</svg> </svg>
@ -330,13 +325,22 @@ function ShapesWithSVGs() {
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const dprMultiple = useValue(
'dpr multiple',
() =>
// dprMultiple is the smallest number we can multiply dpr by to get an integer
// it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
[editor]
)
return ( return (
<> <>
{renderingShapes.map((result) => ( {renderingShapes.map((result) => (
<React.Fragment key={result.id + '_fragment'}> <Fragment key={result.id + '_fragment'}>
<Shape {...result} /> <Shape {...result} dprMultiple={dprMultiple} />
<DebugSvgCopy id={result.id} /> <DebugSvgCopy id={result.id} />
</React.Fragment> </Fragment>
))} ))}
</> </>
) )
@ -347,10 +351,19 @@ function ShapesToDisplay() {
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const dprMultiple = useValue(
'dpr multiple',
() =>
// dprMultiple is the smallest number we can multiply dpr by to get an integer
// it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
[editor]
)
return ( return (
<> <>
{renderingShapes.map((result) => ( {renderingShapes.map((result) => (
<Shape key={result.id + '_shape'} {...result} /> <Shape key={result.id + '_shape'} {...result} dprMultiple={dprMultiple} />
))} ))}
</> </>
) )
@ -420,11 +433,11 @@ const HoveredShapeIndicator = function HoveredShapeIndicator() {
return <HoveredShapeIndicator shapeId={hoveredShapeId} /> return <HoveredShapeIndicator shapeId={hoveredShapeId} />
} }
const HintedShapeIndicator = track(function HintedShapeIndicator() { function HintedShapeIndicator() {
const editor = useEditor() const editor = useEditor()
const { ShapeIndicator } = useEditorComponents() const { ShapeIndicator } = useEditorComponents()
const ids = dedupe(editor.getHintingShapeIds()) const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
if (!ids.length) return null if (!ids.length) return null
if (!ShapeIndicator) return null if (!ShapeIndicator) return null
@ -436,9 +449,9 @@ const HintedShapeIndicator = track(function HintedShapeIndicator() {
))} ))}
</> </>
) )
}) }
function Cursor() { function CursorDef() {
return ( return (
<g id="cursor"> <g id="cursor">
<g fill="rgba(0,0,0,.2)" transform="translate(-11,-11)"> <g fill="rgba(0,0,0,.2)" transform="translate(-11,-11)">
@ -457,36 +470,25 @@ function Cursor() {
) )
} }
function CollaboratorHint() { function CollaboratorHintDef() {
return <path id="cursor_hint" fill="currentColor" d="M -2,-5 2,0 -2,5 Z" /> return <path id="cursor_hint" fill="currentColor" d="M -2,-5 2,0 -2,5 Z" />
} }
function ArrowheadDot() { function DebugSvgCopy({ id }: { id: TLShapeId }) {
return (
<marker id="arrowhead-dot" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="0">
<circle cx="3" cy="3" r="2" strokeDasharray="100%" />
</marker>
)
}
function ArrowheadCross() {
return (
<marker id="arrowhead-cross" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="auto">
<line x1="1.5" y1="1.5" x2="4.5" y2="4.5" strokeDasharray="100%" />
<line x1="1.5" y1="4.5" x2="4.5" y2="1.5" strokeDasharray="100%" />
</marker>
)
}
const DebugSvgCopy = track(function DupSvg({ id }: { id: TLShapeId }) {
const editor = useEditor() const editor = useEditor()
const shape = editor.getShape(id)
const [html, setHtml] = React.useState('') const [html, setHtml] = useState('')
const isInRoot = shape?.parentId === editor.getCurrentPageId() const isInRoot = useValue(
'is in root',
() => {
const shape = editor.getShape(id)
return shape?.parentId === editor.getCurrentPageId()
},
[editor, id]
)
React.useEffect(() => { useEffect(() => {
if (!isInRoot) return if (!isInRoot) return
let latest = null let latest = null
@ -520,7 +522,7 @@ const DebugSvgCopy = track(function DupSvg({ id }: { id: TLShapeId }) {
<div style={{ display: 'flex' }} dangerouslySetInnerHTML={{ __html: html }} /> <div style={{ display: 'flex' }} dangerouslySetInnerHTML={{ __html: html }} />
</div> </div>
) )
}) }
function SelectionForegroundWrapper() { function SelectionForegroundWrapper() {
const editor = useEditor() const editor = useEditor()

View file

@ -80,3 +80,13 @@ export function releasePointerCapture(
/** @public */ /** @public */
export const stopEventPropagation = (e: any) => e.stopPropagation() export const stopEventPropagation = (e: any) => e.stopPropagation()
/** @internal */
export const setStyleProperty = (
elm: HTMLElement | null,
property: string,
value: string | number
) => {
if (!elm) return
elm.style.setProperty(property, value as string)
}

View file

@ -1014,7 +1014,17 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] { override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
return [getFillDefForCanvas()] return [
getFillDefForCanvas(),
{
key: `arrow:dot`,
component: ArrowheadDotDef,
},
{
key: `arrow:cross`,
component: ArrowheadCrossDef,
},
]
} }
} }
@ -1082,3 +1092,20 @@ const shapeAtTranslationStart = new WeakMap<
> >
} }
>() >()
function ArrowheadDotDef() {
return (
<marker id="arrowhead-dot" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="0">
<circle cx="3" cy="3" r="2" strokeDasharray="100%" />
</marker>
)
}
function ArrowheadCrossDef() {
return (
<marker id="arrowhead-cross" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="auto">
<line x1="1.5" y1="1.5" x2="4.5" y2="4.5" strokeDasharray="100%" />
<line x1="1.5" y1="4.5" x2="4.5" y2="1.5" strokeDasharray="100%" />
</marker>
)
}