[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:
parent
4e0df0730d
commit
4801b35768
5 changed files with 170 additions and 112 deletions
|
@ -332,6 +332,8 @@ input,
|
|||
.tl-shape__culled {
|
||||
position: relative;
|
||||
background-color: var(--color-culled);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ---------------- Shape Containers ---------------- */
|
||||
|
|
|
@ -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 * as React from 'react'
|
||||
import { memo, useCallback, useLayoutEffect, useRef } from 'react'
|
||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
import { Mat } from '../primitives/Mat'
|
||||
import { toDomPrecision } from '../primitives/utils'
|
||||
import { nearestMultiple } from '../utils/nearestMultiple'
|
||||
import { setStyleProperty } from '../utils/dom'
|
||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||
|
||||
/*
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
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,
|
||||
shape,
|
||||
util,
|
||||
|
@ -28,6 +29,7 @@ export const Shape = track(function Shape({
|
|||
backgroundIndex,
|
||||
opacity,
|
||||
isCulled,
|
||||
dprMultiple,
|
||||
}: {
|
||||
id: TLShapeId
|
||||
shape: TLShape
|
||||
|
@ -36,56 +38,79 @@ export const Shape = track(function Shape({
|
|||
backgroundIndex: number
|
||||
opacity: number
|
||||
isCulled: boolean
|
||||
dprMultiple: number
|
||||
}) {
|
||||
const editor = useEditor()
|
||||
|
||||
const { ShapeErrorFallback } = useEditorComponents()
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const backgroundContainerRef = React.useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const bgContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const setProperty = React.useCallback((property: string, value: string) => {
|
||||
containerRef.current?.style.setProperty(property, value)
|
||||
backgroundContainerRef.current?.style.setProperty(property, value)
|
||||
}, [])
|
||||
const memoizedStuffRef = useRef({
|
||||
transform: '',
|
||||
clipPath: 'none',
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
useLayoutReaction('set shape stuff', () => {
|
||||
const shape = editor.getShape(id)
|
||||
if (!shape) return // probably the shape was just deleted
|
||||
|
||||
const pageTransform = editor.getShapePageTransform(id)
|
||||
const transform = Mat.toCssString(pageTransform)
|
||||
setProperty('transform', transform)
|
||||
const prev = memoizedStuffRef.current
|
||||
|
||||
const clipPath = editor.getShapeClipPath(id)
|
||||
setProperty('clip-path', clipPath ?? 'none')
|
||||
// Clip path
|
||||
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 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 width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
|
||||
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)
|
||||
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
|
||||
React.useLayoutEffect(() => {
|
||||
setProperty('opacity', opacity + '')
|
||||
containerRef.current?.style.setProperty('z-index', index + '')
|
||||
backgroundContainerRef.current?.style.setProperty('z-index', backgroundIndex + '')
|
||||
}, [opacity, index, backgroundIndex, setProperty])
|
||||
// This stuff changes pretty infrequently, so we can change them together
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current
|
||||
const bgContainer = bgContainerRef.current
|
||||
|
||||
const annotateError = React.useCallback(
|
||||
(error: any) => {
|
||||
editor.annotateError(error, { origin: 'react.shape', willCrashApp: false })
|
||||
},
|
||||
// Opacity
|
||||
setStyleProperty(container, 'opacity', opacity)
|
||||
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]
|
||||
)
|
||||
|
||||
|
@ -95,12 +120,12 @@ export const Shape = track(function Shape({
|
|||
<>
|
||||
{util.backgroundComponent && (
|
||||
<div
|
||||
ref={backgroundContainerRef}
|
||||
ref={bgContainerRef}
|
||||
className="tl-shape tl-shape-background"
|
||||
data-shape-type={shape.type}
|
||||
draggable={false}
|
||||
>
|
||||
{!isCulled && (
|
||||
{isCulled ? null : (
|
||||
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
|
||||
<InnerShapeBackground shape={shape} util={util} />
|
||||
</OptionalErrorBoundary>
|
||||
|
@ -109,7 +134,7 @@ export const Shape = track(function Shape({
|
|||
)}
|
||||
<div ref={containerRef} className="tl-shape" data-shape-type={shape.type} draggable={false}>
|
||||
{isCulled ? (
|
||||
<CulledShape shape={shape} />
|
||||
<CulledShape shapeId={shape.id} />
|
||||
) : (
|
||||
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
|
||||
<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> }) {
|
||||
return useStateTracking('InnerShape:' + shape.type, () => util.component(shape))
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.shape.props === next.shape.props &&
|
||||
prev.shape.meta === next.shape.meta &&
|
||||
prev.util === next.util
|
||||
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
||||
)
|
||||
|
||||
const InnerShapeBackground = React.memo(
|
||||
const InnerShapeBackground = memo(
|
||||
function InnerShapeBackground<T extends TLShape>({
|
||||
shape,
|
||||
util,
|
||||
|
@ -143,23 +165,18 @@ const InnerShapeBackground = React.memo(
|
|||
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
||||
)
|
||||
|
||||
const CulledShape = React.memo(
|
||||
function CulledShape<T extends TLShape>({ shape }: { shape: T }) {
|
||||
const CulledShape = function CulledShape<T extends TLShape>({ shapeId }: { shapeId: IdOf<T> }) {
|
||||
const editor = useEditor()
|
||||
const bounds = editor.getShapeGeometry(shape).bounds
|
||||
const culledRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tl-shape__culled"
|
||||
style={{
|
||||
transform: `translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision(
|
||||
bounds.minY
|
||||
)}px)`,
|
||||
width: Math.max(1, toDomPrecision(bounds.width)),
|
||||
height: Math.max(1, toDomPrecision(bounds.height)),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
useLayoutReaction('set shape stuff', () => {
|
||||
const bounds = editor.getShapeGeometry(shapeId).bounds
|
||||
setStyleProperty(
|
||||
culledRef.current,
|
||||
'transform',
|
||||
`translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision(bounds.minY)}px)`
|
||||
)
|
||||
})
|
||||
|
||||
return <div ref={culledRef} className="tl-shape__culled" />
|
||||
}
|
||||
|
|
|
@ -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 { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||
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 { useCanvasEvents } from '../../hooks/useCanvasEvents'
|
||||
import { useCoarsePointer } from '../../hooks/useCoarsePointer'
|
||||
|
@ -17,6 +17,8 @@ import { Mat } from '../../primitives/Mat'
|
|||
import { Vec } from '../../primitives/Vec'
|
||||
import { toDomPrecision } from '../../primitives/utils'
|
||||
import { debugFlags } from '../../utils/debug-flags'
|
||||
import { setStyleProperty } from '../../utils/dom'
|
||||
import { nearestMultiple } from '../../utils/nearestMultiple'
|
||||
import { GeometryDebuggingView } from '../GeometryDebuggingView'
|
||||
import { LiveCollaborators } from '../LiveCollaborators'
|
||||
import { Shape } from '../Shape'
|
||||
|
@ -30,9 +32,9 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
|
||||
const { Background, SvgDefs } = useEditorComponents()
|
||||
|
||||
const rCanvas = React.useRef<HTMLDivElement>(null)
|
||||
const rHtmlLayer = React.useRef<HTMLDivElement>(null)
|
||||
const rHtmlLayer2 = React.useRef<HTMLDivElement>(null)
|
||||
const rCanvas = useRef<HTMLDivElement>(null)
|
||||
const rHtmlLayer = useRef<HTMLDivElement>(null)
|
||||
const rHtmlLayer2 = useRef<HTMLDivElement>(null)
|
||||
|
||||
useScreenBounds(rCanvas)
|
||||
useDocumentEvents()
|
||||
|
@ -42,11 +44,6 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
useFixSafariDoubleTapZoomPencilEvents(rCanvas)
|
||||
|
||||
useLayoutReaction('position layers', () => {
|
||||
const htmlElm = rHtmlLayer.current
|
||||
if (!htmlElm) return
|
||||
const htmlElm2 = rHtmlLayer2.current
|
||||
if (!htmlElm2) return
|
||||
|
||||
const { x, y, z } = editor.getCamera()
|
||||
|
||||
// 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(
|
||||
x + offset
|
||||
)}px,${toDomPrecision(y + offset)}px)`
|
||||
htmlElm.style.setProperty('transform', transform)
|
||||
htmlElm2.style.setProperty('transform', transform)
|
||||
setStyleProperty(rHtmlLayer.current, 'transform', transform)
|
||||
setStyleProperty(rHtmlLayer2.current, 'transform', transform)
|
||||
})
|
||||
|
||||
const events = useCanvasEvents()
|
||||
|
@ -67,7 +64,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
const shapeSvgDefs = useValue(
|
||||
'shapeSvgDefs',
|
||||
() => {
|
||||
const shapeSvgDefsByKey = new Map<string, React.JSX.Element>()
|
||||
const shapeSvgDefsByKey = new Map<string, JSX.Element>()
|
||||
for (const util of objectMapValues(editor.shapeUtils)) {
|
||||
if (!util) return
|
||||
const defs = util.getCanvasSvgDefs()
|
||||
|
@ -98,10 +95,8 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<svg className="tl-svg-context">
|
||||
<defs>
|
||||
{shapeSvgDefs}
|
||||
{Cursor && <Cursor />}
|
||||
<CollaboratorHint />
|
||||
<ArrowheadDot />
|
||||
<ArrowheadCross />
|
||||
<CursorDef />
|
||||
<CollaboratorHintDef />
|
||||
{SvgDefs && <SvgDefs />}
|
||||
</defs>
|
||||
</svg>
|
||||
|
@ -330,13 +325,22 @@ function ShapesWithSVGs() {
|
|||
|
||||
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 (
|
||||
<>
|
||||
{renderingShapes.map((result) => (
|
||||
<React.Fragment key={result.id + '_fragment'}>
|
||||
<Shape {...result} />
|
||||
<Fragment key={result.id + '_fragment'}>
|
||||
<Shape {...result} dprMultiple={dprMultiple} />
|
||||
<DebugSvgCopy id={result.id} />
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
@ -347,10 +351,19 @@ function ShapesToDisplay() {
|
|||
|
||||
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 (
|
||||
<>
|
||||
{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} />
|
||||
}
|
||||
|
||||
const HintedShapeIndicator = track(function HintedShapeIndicator() {
|
||||
function HintedShapeIndicator() {
|
||||
const editor = useEditor()
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
|
||||
const ids = dedupe(editor.getHintingShapeIds())
|
||||
const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
|
||||
|
||||
if (!ids.length) return null
|
||||
if (!ShapeIndicator) return null
|
||||
|
@ -436,9 +449,9 @@ const HintedShapeIndicator = track(function HintedShapeIndicator() {
|
|||
))}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function Cursor() {
|
||||
function CursorDef() {
|
||||
return (
|
||||
<g id="cursor">
|
||||
<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" />
|
||||
}
|
||||
|
||||
function ArrowheadDot() {
|
||||
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 }) {
|
||||
function DebugSvgCopy({ id }: { id: TLShapeId }) {
|
||||
const editor = useEditor()
|
||||
|
||||
const [html, setHtml] = useState('')
|
||||
|
||||
const isInRoot = useValue(
|
||||
'is in root',
|
||||
() => {
|
||||
const shape = editor.getShape(id)
|
||||
return shape?.parentId === editor.getCurrentPageId()
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
||||
const [html, setHtml] = React.useState('')
|
||||
|
||||
const isInRoot = shape?.parentId === editor.getCurrentPageId()
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isInRoot) return
|
||||
|
||||
let latest = null
|
||||
|
@ -520,7 +522,7 @@ const DebugSvgCopy = track(function DupSvg({ id }: { id: TLShapeId }) {
|
|||
<div style={{ display: 'flex' }} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function SelectionForegroundWrapper() {
|
||||
const editor = useEditor()
|
||||
|
|
|
@ -80,3 +80,13 @@ export function releasePointerCapture(
|
|||
|
||||
/** @public */
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1014,7 +1014,17 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue