[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

@ -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 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
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()