[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:
parent
f8dc5b3248
commit
a2fff9dca7
8 changed files with 101 additions and 29 deletions
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&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&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');</style></defs></svg>"`;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`,
|
||||
|
|
43
packages/tldraw/src/state/shapes/shared/getTextSvgElement.ts
Normal file
43
packages/tldraw/src/state/shapes/shared/getTextSvgElement.ts
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue