瀏覽代碼

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

* Add getSvgElement

* Update TextUtil.tsx

* Add sticky svg

* Fix bounds bug, improve text export

* Include fonts
main
Steve Ruiz 3 年之前
父節點
當前提交
a2fff9dca7
No account linked to committer's email address

+ 1
- 1
packages/core/src/hooks/useBoundsEvents.tsx 查看文件

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

+ 21
- 24
packages/tldraw/src/state/TldrawApp.ts 查看文件

@@ -1558,26 +1558,32 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1558 1558
   copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
1559 1559
     if (ids.length === 0) ids = Object.keys(this.page.shapes)
1560 1560
 
1561
-    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1562
-
1563 1561
     const shapes = ids.map((id) => this.getShape(id, pageId))
1562
+    const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
1563
+    const padding = 16
1564 1564
 
1565
-    function getSvgElementForShape(shape: TDShape) {
1566
-      const elm = document.getElementById(shape.id + '_svg')
1567
-
1568
-      if (!elm) return
1565
+    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1566
+    const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
1567
+    const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
1569 1568
 
1570
-      // TODO: Create SVG elements for text
1569
+    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');`
1570
+    defs.appendChild(style)
1571
+    svg.appendChild(defs)
1571 1572
 
1572
-      const element = elm?.cloneNode(true) as SVGElement
1573
+    function getSvgElementForShape(shape: TDShape) {
1574
+      const util = TLDR.getShapeUtil(shape)
1575
+      const element = util.getSvgElement(shape)
1576
+      const bounds = util.getBounds(shape)
1573 1577
 
1574
-      const bounds = TLDR.getShapeUtil(shape).getBounds(shape)
1578
+      if (!element) return
1575 1579
 
1576 1580
       element.setAttribute(
1577 1581
         'transform',
1578
-        `translate(${shape.point[0]}, ${shape.point[1]}) rotate(${
1579
-          ((shape.rotation || 0) * 180) / Math.PI
1580
-        }, ${bounds.width / 2}, ${bounds.height / 2})`
1582
+        `translate(${padding + shape.point[0] - commonBounds.minX}, ${
1583
+          padding + shape.point[1] - commonBounds.minY
1584
+        }) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
1585
+          bounds.height / 2
1586
+        })`
1581 1587
       )
1582 1588
 
1583 1589
       return element
@@ -1608,23 +1614,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
1608 1614
       }
1609 1615
     })
1610 1616
 
1611
-    const bounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
1612
-    const padding = 16
1613
-
1614 1617
     // Resize the element to the bounding box
1615 1618
     svg.setAttribute(
1616 1619
       'viewBox',
1617
-      [
1618
-        bounds.minX - padding,
1619
-        bounds.minY - padding,
1620
-        bounds.width + padding * 2,
1621
-        bounds.height + padding * 2,
1622
-      ].join(' ')
1620
+      [0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
1623 1621
     )
1624 1622
 
1625
-    svg.setAttribute('width', String(bounds.width))
1626
-
1627
-    svg.setAttribute('height', String(bounds.height))
1623
+    svg.setAttribute('width', String(commonBounds.width))
1624
+    svg.setAttribute('height', String(commonBounds.height))
1628 1625
 
1629 1626
     const s = new XMLSerializer()
1630 1627
 

+ 2
- 2
packages/tldraw/src/state/__snapshots__/TLDrawApp.spec.ts.snap 查看文件

@@ -202,6 +202,6 @@ Array [
202 202
 ]
203 203
 `;
204 204
 
205
-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>"`;
205
+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>"`;
206 206
 
207
-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\\"/>"`;
207
+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>"`;

+ 19
- 0
packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx 查看文件

@@ -10,6 +10,7 @@ import { styled } from '~styles'
10 10
 import { Vec } from '@tldraw/vec'
11 11
 import { GHOSTED_OPACITY } from '~constants'
12 12
 import { TLDR } from '~state/TLDR'
13
+import { getTextSvgElement } from '../shared/getTextSvgElement'
13 14
 
14 15
 type T = StickyShape
15 16
 type E = HTMLDivElement
@@ -232,6 +233,24 @@ export class StickyUtil extends TDShapeUtil<T, E> {
232 233
   transformSingle = (shape: T): Partial<T> => {
233 234
     return shape
234 235
   }
236
+
237
+  getSvgElement = (shape: T): SVGElement | void => {
238
+    const bounds = this.getBounds(shape)
239
+    const textElm = getTextSvgElement(shape, bounds)
240
+    const style = getStickyShapeStyle(shape.style)
241
+    textElm.setAttribute('fill', style.color)
242
+
243
+    const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
244
+    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
245
+    rect.setAttribute('width', bounds.width + '')
246
+    rect.setAttribute('height', bounds.height + '')
247
+    rect.setAttribute('fill', style.fill)
248
+
249
+    g.appendChild(rect)
250
+    g.appendChild(textElm)
251
+
252
+    return g
253
+  }
235 254
 }
236 255
 
237 256
 /* -------------------------------------------------- */

+ 4
- 0
packages/tldraw/src/state/shapes/TDShapeUtil.tsx 查看文件

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

+ 9
- 1
packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx 查看文件

@@ -10,6 +10,7 @@ import { styled } from '~styles'
10 10
 import { Vec } from '@tldraw/vec'
11 11
 import { TLDR } from '~state/TLDR'
12 12
 import { getTextAlign } from '../shared/getTextAlign'
13
+import { getTextSvgElement } from '../shared/getTextSvgElement'
13 14
 
14 15
 type T = TextShape
15 16
 type E = HTMLDivElement
@@ -311,6 +312,13 @@ export class TextUtil extends TDShapeUtil<T, E> {
311 312
       point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
312 313
     }
313 314
   }
315
+
316
+  getSvgElement = (shape: T): SVGElement | void => {
317
+    const bounds = this.getBounds(shape)
318
+    const elm = getTextSvgElement(shape, bounds)
319
+    elm.setAttribute('fill', getShapeStyle(shape.style).stroke)
320
+    return elm
321
+  }
314 322
 }
315 323
 
316 324
 /* -------------------------------------------------- */
@@ -332,7 +340,7 @@ function getMeasurementDiv() {
332 340
   Object.assign(pre.style, {
333 341
     whiteSpace: 'pre',
334 342
     width: 'auto',
335
-    border: '1px solid red',
343
+    border: '1px solid transparent',
336 344
     padding: '4px',
337 345
     margin: '0px',
338 346
     letterSpacing: `${LETTER_SPACING}px`,

+ 43
- 0
packages/tldraw/src/state/shapes/shared/getTextSvgElement.ts 查看文件

@@ -0,0 +1,43 @@
1
+import type { TLBounds } from '@tldraw/core'
2
+import { AlignStyle, StickyShape, TextShape } from '~types'
3
+import { getFontFace, getFontSize } from './shape-styles'
4
+import { getTextAlign } from './getTextAlign'
5
+
6
+export function getTextSvgElement(shape: TextShape | StickyShape, bounds: TLBounds) {
7
+  const { text, style } = shape
8
+  const fontSize = getFontSize(shape.style.size, shape.style.font)
9
+
10
+  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
11
+
12
+  const LINE_HEIGHT = fontSize * 1.3
13
+
14
+  const textLines = text.split('\n').map((line, i) => {
15
+    const textElm = document.createElementNS('http://www.w3.org/2000/svg', 'text')
16
+    textElm.textContent = line
17
+    textElm.setAttribute('font-family', getFontFace(style.font))
18
+    textElm.setAttribute('font-size', fontSize + 'px')
19
+    textElm.setAttribute('text-anchor', 'start')
20
+    textElm.setAttribute('alignment-baseline', 'central')
21
+    textElm.setAttribute('text-align', getTextAlign(style.textAlign))
22
+    textElm.setAttribute('y', LINE_HEIGHT * (0.5 + i) + '')
23
+    g.appendChild(textElm)
24
+
25
+    return textElm
26
+  })
27
+
28
+  if (style.textAlign === AlignStyle.Middle) {
29
+    textLines.forEach((textElm) => {
30
+      textElm.setAttribute('x', bounds.width / 2 + '')
31
+      textElm.setAttribute('text-align', 'center')
32
+      textElm.setAttribute('text-anchor', 'middle')
33
+    })
34
+  } else if (style.textAlign === AlignStyle.End) {
35
+    textLines.forEach((textElm) => {
36
+      textElm.setAttribute('x', bounds.width + '')
37
+      textElm.setAttribute('text-align', 'right')
38
+      textElm.setAttribute('text-anchor', 'end')
39
+    })
40
+  }
41
+
42
+  return g
43
+}

+ 1
- 0
packages/tldraw/src/state/tools/SelectTool/SelectTool.ts 查看文件

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

Loading…
取消
儲存