[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 {
|
.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 ---------------- */
|
||||||
|
|
|
@ -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" />
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue