React-powered SVG exports (#3117)

## Migration path
1. If any of your shapes implement `toSvg` for exports, you'll need to
replace your implementation with a new version that returns JSX (it's a
react component) instead of manually constructing SVG DOM nodes
2. `editor.getSvg` is deprecated. It still works, but will be going away
in a future release. If you still need SVGs as DOM elements rather than
strings, use `new DOMParser().parseFromString(svgString,
'image/svg+xml').firstElementChild`

## The change in detail
At the moment, our SVG exports very carefully try to recreate the
visuals of our shapes by manually constructing SVG DOM nodes. On its own
this is really painful, but it also results in a lot of duplicated logic
between the `component` and `getSvg` methods of shape utils.

In #3020, we looked at using string concatenation & DOMParser to make
this a bit less painful. This works, but requires specifying namespaces
everywhere, is still pretty painful (no syntax highlighting or
formatting), and still results in all that duplicated logic.

I briefly experimented with creating my own version of the javascript
language that let you embed XML like syntax directly. I was going to
call it EXTREME JAVASCRIPT or XJS for short, but then I noticed that we
already wrote the whole of tldraw in this thing called react and a (imo
much worse named) version of the javascript xml thing already existed.

Given the entire library already depends on react, what would it look
like if we just used react directly for these exports? Turns out things
get a lot simpler! Take a look at lmk what you think

This diff was intended as a proof of concept, but is actually pretty
close to being landable. The main thing is that here, I've deliberately
leant into this being a big breaking change to see just how much code we
could delete (turns out: lots). We could if we wanted to make this
without making it a breaking change at all, but it would add back a lot
of complexity on our side and run a fair bit slower

---------

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
This commit is contained in:
alex 2024-03-25 14:16:55 +00:00 committed by GitHub
parent 016dcdc56a
commit 05f58f7c2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 1804 additions and 3465 deletions

View file

@ -481,7 +481,7 @@ function CollaboratorHintDef() {
function DebugSvgCopy({ id }: { id: TLShapeId }) {
const editor = useEditor()
const [html, setHtml] = useState('')
const [src, setSrc] = useState<string | null>(null)
const isInRoot = useValue(
'is in root',
@ -499,18 +499,15 @@ function DebugSvgCopy({ id }: { id: TLShapeId }) {
const unsubscribe = react('shape to svg', async () => {
const renderId = Math.random()
latest = renderId
const bb = editor.getShapePageBounds(id)
const el = await editor.getSvg([id], {
const result = await editor.getSvgString([id], {
padding: 0,
background: editor.getInstanceState().exportBackground,
})
if (el && bb && latest === renderId) {
el.style.setProperty('overflow', 'visible')
el.setAttribute('preserveAspectRatio', 'xMidYMin slice')
el.style.setProperty('transform', `translate(${bb.x}px, ${bb.y + bb.h + 12}px)`)
el.style.setProperty('border', '1px solid black')
setHtml(el?.outerHTML)
}
if (latest !== renderId || !result) return
const svgDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(result.svg)}`
setSrc(svgDataUrl)
})
return () => {
@ -518,13 +515,24 @@ function DebugSvgCopy({ id }: { id: TLShapeId }) {
unsubscribe()
}
}, [editor, id, isInRoot])
const bb = editor.getShapePageBounds(id)
if (!isInRoot) return null
if (!isInRoot || !src || !bb) return null
return (
<div style={{ paddingTop: 12, position: 'absolute' }}>
<div style={{ display: 'flex' }} dangerouslySetInnerHTML={{ __html: html }} />
</div>
<img
src={src}
width={bb.width}
height={bb.height}
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translate(${bb.x}px, ${bb.y + bb.h + 12}px)`,
border: '1px solid black',
maxWidth: 'none',
}}
/>
)
}