[improvement] Add basic support for copying text (#354)

* Add getSvgElement

* Update TextUtil.tsx

* Add sticky svg

* Fix bounds bug, improve text export

* Include fonts
This commit is contained in:
Steve Ruiz 2021-11-22 16:15:51 +00:00 committed by GitHub
parent f8dc5b3248
commit a2fff9dca7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 29 deletions

View file

@ -9,7 +9,7 @@ export function useBoundsEvents() {
if (!inputs.pointerIsValid(e)) return
if (e.button === 2) {
callbacks.onRightPointShape?.(inputs.pointerDown(e, 'bounds'), e)
callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
return
}

View file

@ -1558,26 +1558,32 @@ export class TldrawApp extends StateManager<TDSnapshot> {
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
if (ids.length === 0) ids = Object.keys(this.page.shapes)
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const shapes = ids.map((id) => this.getShape(id, pageId))
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
const padding = 16
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
defs.appendChild(style)
svg.appendChild(defs)
function getSvgElementForShape(shape: TDShape) {
const elm = document.getElementById(shape.id + '_svg')
const util = TLDR.getShapeUtil(shape)
const element = util.getSvgElement(shape)
const bounds = util.getBounds(shape)
if (!elm) return
// TODO: Create SVG elements for text
const element = elm?.cloneNode(true) as SVGElement
const bounds = TLDR.getShapeUtil(shape).getBounds(shape)
if (!element) return
element.setAttribute(
'transform',
`translate(${shape.point[0]}, ${shape.point[1]}) rotate(${
((shape.rotation || 0) * 180) / Math.PI
}, ${bounds.width / 2}, ${bounds.height / 2})`
`translate(${padding + shape.point[0] - commonBounds.minX}, ${
padding + shape.point[1] - commonBounds.minY
}) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
bounds.height / 2
})`
)
return element
@ -1608,23 +1614,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}
})
const bounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
const padding = 16
// Resize the element to the bounding box
svg.setAttribute(
'viewBox',
[
bounds.minX - padding,
bounds.minY - padding,
bounds.width + padding * 2,
bounds.height + padding * 2,
].join(' ')
[0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
)
svg.setAttribute('width', String(bounds.width))
svg.setAttribute('height', String(bounds.height))
svg.setAttribute('width', String(commonBounds.width))
svg.setAttribute('height', String(commonBounds.height))
const s = new XMLSerializer()

View file

@ -202,6 +202,6 @@ Array [
]
`;
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-16 -16 232 232\\" width=\\"200\\" height=\\"200\\"><g/></svg>"`;
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs><g/></svg>"`;
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-20.741879096242684 -20.741879096242684 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"/>"`;
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs></svg>"`;

View file

@ -10,6 +10,7 @@ import { styled } from '~styles'
import { Vec } from '@tldraw/vec'
import { GHOSTED_OPACITY } from '~constants'
import { TLDR } from '~state/TLDR'
import { getTextSvgElement } from '../shared/getTextSvgElement'
type T = StickyShape
type E = HTMLDivElement
@ -232,6 +233,24 @@ export class StickyUtil extends TDShapeUtil<T, E> {
transformSingle = (shape: T): Partial<T> => {
return shape
}
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const textElm = getTextSvgElement(shape, bounds)
const style = getStickyShapeStyle(shape.style)
textElm.setAttribute('fill', style.color)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('width', bounds.width + '')
rect.setAttribute('height', bounds.height + '')
rect.setAttribute('fill', style.fill)
g.appendChild(rect)
g.appendChild(textElm)
return g
}
}
/* -------------------------------------------------- */

View file

@ -179,4 +179,8 @@ export abstract class TDShapeUtil<T extends TDShape, E extends Element = any> ex
onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void
onSessionComplete?: (shape: T) => Partial<T> | void
getSvgElement = (shape: T): SVGElement | void => {
return document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
}
}

View file

@ -10,6 +10,7 @@ import { styled } from '~styles'
import { Vec } from '@tldraw/vec'
import { TLDR } from '~state/TLDR'
import { getTextAlign } from '../shared/getTextAlign'
import { getTextSvgElement } from '../shared/getTextSvgElement'
type T = TextShape
type E = HTMLDivElement
@ -311,6 +312,13 @@ export class TextUtil extends TDShapeUtil<T, E> {
point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
}
}
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const elm = getTextSvgElement(shape, bounds)
elm.setAttribute('fill', getShapeStyle(shape.style).stroke)
return elm
}
}
/* -------------------------------------------------- */
@ -332,7 +340,7 @@ function getMeasurementDiv() {
Object.assign(pre.style, {
whiteSpace: 'pre',
width: 'auto',
border: '1px solid red',
border: '1px solid transparent',
padding: '4px',
margin: '0px',
letterSpacing: `${LETTER_SPACING}px`,

View file

@ -0,0 +1,43 @@
import type { TLBounds } from '@tldraw/core'
import { AlignStyle, StickyShape, TextShape } from '~types'
import { getFontFace, getFontSize } from './shape-styles'
import { getTextAlign } from './getTextAlign'
export function getTextSvgElement(shape: TextShape | StickyShape, bounds: TLBounds) {
const { text, style } = shape
const fontSize = getFontSize(shape.style.size, shape.style.font)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const LINE_HEIGHT = fontSize * 1.3
const textLines = text.split('\n').map((line, i) => {
const textElm = document.createElementNS('http://www.w3.org/2000/svg', 'text')
textElm.textContent = line
textElm.setAttribute('font-family', getFontFace(style.font))
textElm.setAttribute('font-size', fontSize + 'px')
textElm.setAttribute('text-anchor', 'start')
textElm.setAttribute('alignment-baseline', 'central')
textElm.setAttribute('text-align', getTextAlign(style.textAlign))
textElm.setAttribute('y', LINE_HEIGHT * (0.5 + i) + '')
g.appendChild(textElm)
return textElm
})
if (style.textAlign === AlignStyle.Middle) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width / 2 + '')
textElm.setAttribute('text-align', 'center')
textElm.setAttribute('text-anchor', 'middle')
})
} else if (style.textAlign === AlignStyle.End) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width + '')
textElm.setAttribute('text-align', 'right')
textElm.setAttribute('text-anchor', 'end')
})
}
return g
}

View file

@ -551,6 +551,7 @@ export class SelectTool extends BaseTool<Status> {
onRightPointShape: TLPointerEventHandler = (info) => {
if (!this.app.isSelected(info.target)) {
console.log(info.target)
this.app.select(info.target)
}
}