@tldraw/editor 3.12.0-internal.624e32507d98 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/dist-cjs/index.d.ts +162 -17
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +5 -0
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/GeometryDebuggingView.js +2 -2
- package/dist-cjs/lib/components/GeometryDebuggingView.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +10 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +208 -18
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager.js +1 -1
- package/dist-cjs/lib/editor/managers/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +12 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +4 -13
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/selection-types.js.map +1 -1
- package/dist-cjs/lib/exports/StyleEmbedder.js +31 -60
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/cssRules.js +3 -9
- package/dist-cjs/lib/exports/cssRules.js.map +2 -2
- package/dist-cjs/lib/exports/exportToSvg.js +1 -4
- package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +16 -0
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +10 -20
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +128 -16
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +3 -3
- package/dist-cjs/lib/primitives/geometry/Group2d.js +54 -11
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +20 -0
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/utils/reorderShapes.js +2 -8
- package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +162 -17
- package/dist-esm/index.mjs +8 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +5 -0
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/GeometryDebuggingView.mjs +3 -3
- package/dist-esm/lib/components/GeometryDebuggingView.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +10 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +209 -18
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +12 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +4 -13
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +32 -61
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/cssRules.mjs +3 -9
- package/dist-esm/lib/exports/cssRules.mjs.map +2 -2
- package/dist-esm/lib/exports/exportToSvg.mjs +1 -4
- package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +16 -0
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +10 -20
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +132 -14
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -12
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +20 -0
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/utils/reorderShapes.mjs +2 -8
- package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +34 -19
- package/package.json +7 -7
- package/src/index.ts +11 -2
- package/src/lib/TldrawEditor.tsx +30 -3
- package/src/lib/components/GeometryDebuggingView.tsx +3 -3
- package/src/lib/components/default-components/DefaultCanvas.tsx +6 -1
- package/src/lib/editor/Editor.ts +315 -24
- package/src/lib/editor/managers/FocusManager.ts +1 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +7 -15
- package/src/lib/editor/types/selection-types.ts +3 -0
- package/src/lib/exports/StyleEmbedder.ts +34 -81
- package/src/lib/exports/cssRules.ts +5 -11
- package/src/lib/exports/exportToSvg.tsx +1 -5
- package/src/lib/hooks/useCanvasEvents.ts +6 -2
- package/src/lib/hooks/useDocumentEvents.ts +18 -0
- package/src/lib/license/Watermark.tsx +18 -29
- package/src/lib/primitives/geometry/Geometry2d.ts +196 -16
- package/src/lib/primitives/geometry/Group2d.ts +76 -13
- package/src/lib/primitives/intersect.ts +41 -0
- package/src/lib/utils/reorderShapes.ts +2 -9
- package/src/version.ts +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertExists, getOwnProperty, objectMapValues, uniqueId } from '@tldraw/utils'
|
|
2
2
|
import { FontEmbedder } from './FontEmbedder'
|
|
3
3
|
import { ReadonlyStyles, Styles, cssRules } from './cssRules'
|
|
4
4
|
import {
|
|
@@ -23,18 +23,6 @@ export class StyleEmbedder {
|
|
|
23
23
|
private readonly styles = new Map<Element, ElementStyleInfo>()
|
|
24
24
|
readonly fonts = new FontEmbedder()
|
|
25
25
|
|
|
26
|
-
async collectDefaultStyles(elements: Element[]) {
|
|
27
|
-
const collected = new Set<string>()
|
|
28
|
-
const promises = []
|
|
29
|
-
for (const element of elements) {
|
|
30
|
-
const tagName = element.tagName.toLowerCase()
|
|
31
|
-
if (collected.has(tagName)) continue
|
|
32
|
-
collected.add(tagName)
|
|
33
|
-
promises.push(populateDefaultStylesForTagName(tagName))
|
|
34
|
-
}
|
|
35
|
-
await Promise.all(promises)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
26
|
readRootElementStyles(rootElement: Element) {
|
|
39
27
|
// when reading a root, we always apply _all_ the styles, even if they match the defaults
|
|
40
28
|
this.readElementStyles(rootElement, {
|
|
@@ -246,7 +234,12 @@ function styleFromComputedStyleMap(
|
|
|
246
234
|
) {
|
|
247
235
|
const styles: Record<string, string> = {}
|
|
248
236
|
const currentColor = style.get('color')?.toString() || ''
|
|
249
|
-
const ruleOptions = {
|
|
237
|
+
const ruleOptions = {
|
|
238
|
+
currentColor,
|
|
239
|
+
parentStyles,
|
|
240
|
+
defaultStyles,
|
|
241
|
+
getStyle: (property: string) => style.get(property)?.toString() ?? '',
|
|
242
|
+
}
|
|
250
243
|
for (const property of style.keys()) {
|
|
251
244
|
if (!shouldIncludeCssProperty(property)) continue
|
|
252
245
|
|
|
@@ -269,7 +262,12 @@ function styleFromComputedStyle(
|
|
|
269
262
|
) {
|
|
270
263
|
const styles: Record<string, string> = {}
|
|
271
264
|
const currentColor = style.color
|
|
272
|
-
const ruleOptions = {
|
|
265
|
+
const ruleOptions = {
|
|
266
|
+
currentColor,
|
|
267
|
+
parentStyles,
|
|
268
|
+
defaultStyles,
|
|
269
|
+
getStyle: (property: string) => style.getPropertyValue(property),
|
|
270
|
+
}
|
|
273
271
|
|
|
274
272
|
for (const property in style) {
|
|
275
273
|
if (!shouldIncludeCssProperty(property)) continue
|
|
@@ -297,88 +295,43 @@ function formatCss(style: ReadonlyStyles) {
|
|
|
297
295
|
// when we're figuring out the default values for a tag, we need read them from a separate document
|
|
298
296
|
// so they're not affected by the current document's styles
|
|
299
297
|
let defaultStyleFrame:
|
|
300
|
-
|
|
|
301
|
-
iframe: HTMLIFrameElement
|
|
302
|
-
foreignObject: SVGForeignObjectElement
|
|
303
|
-
document: Document
|
|
304
|
-
}>
|
|
298
|
+
| { iframe: HTMLIFrameElement; foreignObject: SVGForeignObjectElement; document: Document }
|
|
305
299
|
| undefined
|
|
306
|
-
|
|
307
|
-
const defaultStylesByTagName: Record<
|
|
308
|
-
string,
|
|
309
|
-
| { type: 'resolved'; styles: ReadonlyStyles; promise: Promise<ReadonlyStyles> }
|
|
310
|
-
| { type: 'pending'; promise: Promise<ReadonlyStyles> }
|
|
311
|
-
> = {}
|
|
312
|
-
|
|
313
|
-
const emptyFrameBlob = new Blob(
|
|
314
|
-
['<svg xmlns="http://www.w3.org/2000/svg"><foreignObject/></svg>'],
|
|
315
|
-
{ type: 'image/svg+xml' }
|
|
316
|
-
)
|
|
317
|
-
const emptyFrameUrl = URL.createObjectURL(emptyFrameBlob)
|
|
318
|
-
|
|
300
|
+
const defaultStylesByTagName: Record<string, ReadonlyStyles> = {}
|
|
319
301
|
function getDefaultStyleFrame() {
|
|
320
302
|
if (!defaultStyleFrame) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
pointerEvents: 'none',
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
frame.onload = () => {
|
|
334
|
-
const contentDocument = frame.contentDocument!
|
|
335
|
-
const foreignObject = contentDocument.querySelector('foreignObject')!
|
|
336
|
-
resolve({ iframe: frame, foreignObject, document: contentDocument })
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
frame.src = emptyFrameUrl
|
|
340
|
-
document.body.appendChild(frame)
|
|
341
|
-
})
|
|
303
|
+
const frame = document.createElement('iframe')
|
|
304
|
+
frame.style.display = 'none'
|
|
305
|
+
document.body.appendChild(frame)
|
|
306
|
+
const frameDocument = assertExists(frame.contentDocument, 'frame must have a document')
|
|
307
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
308
|
+
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
|
|
309
|
+
svg.appendChild(foreignObject)
|
|
310
|
+
frameDocument.body.appendChild(svg)
|
|
311
|
+
defaultStyleFrame = { iframe: frame, foreignObject, document: frameDocument }
|
|
342
312
|
}
|
|
343
313
|
return defaultStyleFrame
|
|
344
314
|
}
|
|
345
315
|
|
|
346
316
|
function destroyDefaultStyleFrame() {
|
|
347
317
|
if (defaultStyleFrame) {
|
|
348
|
-
|
|
318
|
+
document.body.removeChild(defaultStyleFrame.iframe)
|
|
349
319
|
defaultStyleFrame = undefined
|
|
350
320
|
}
|
|
351
321
|
}
|
|
352
322
|
|
|
353
323
|
const defaultStyleReadOptions: ReadStyleOpts = { defaultStyles: NO_STYLES, parentStyles: NO_STYLES }
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
if (existing
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (existing && existing.type === 'pending') {
|
|
361
|
-
return existing.promise
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const promise = getDefaultStyleFrame().then(({ foreignObject, document }) => {
|
|
365
|
-
const element = document.createElementNS('http://www.w3.org/1999/xhtml', tagName)
|
|
366
|
-
element.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
|
|
324
|
+
function getDefaultStylesForTagName(tagName: string) {
|
|
325
|
+
let existing = defaultStylesByTagName[tagName]
|
|
326
|
+
if (!existing) {
|
|
327
|
+
const { foreignObject, document } = getDefaultStyleFrame()
|
|
328
|
+
const element = document.createElement(tagName)
|
|
367
329
|
foreignObject.appendChild(element)
|
|
368
|
-
|
|
330
|
+
existing = element.computedStyleMap
|
|
369
331
|
? styleFromComputedStyleMap(element.computedStyleMap(), defaultStyleReadOptions)
|
|
370
332
|
: styleFromComputedStyle(getComputedStyle(element), defaultStyleReadOptions)
|
|
371
333
|
foreignObject.removeChild(element)
|
|
372
|
-
defaultStylesByTagName[tagName] =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
defaultStylesByTagName[tagName] = { type: 'pending', promise }
|
|
377
|
-
return promise
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function getDefaultStylesForTagName(tagName: string) {
|
|
381
|
-
const existing = defaultStylesByTagName[tagName]
|
|
382
|
-
assert(existing && existing.type === 'resolved', 'default styles must be populated & resolved')
|
|
383
|
-
return existing.styles
|
|
334
|
+
defaultStylesByTagName[tagName] = existing
|
|
335
|
+
}
|
|
336
|
+
return existing
|
|
384
337
|
}
|
|
@@ -5,20 +5,13 @@ type CanSkipRule = (
|
|
|
5
5
|
value: string,
|
|
6
6
|
property: string,
|
|
7
7
|
options: {
|
|
8
|
-
|
|
8
|
+
getStyle(property: string): string
|
|
9
9
|
parentStyles: ReadonlyStyles
|
|
10
10
|
defaultStyles: ReadonlyStyles
|
|
11
11
|
currentColor: string
|
|
12
12
|
}
|
|
13
13
|
) => boolean
|
|
14
14
|
|
|
15
|
-
const getStyle = (styles: StylePropertyMapReadOnly | CSSStyleDeclaration, property: string) => {
|
|
16
|
-
if (styles instanceof CSSStyleDeclaration) {
|
|
17
|
-
return styles.getPropertyValue(property)
|
|
18
|
-
}
|
|
19
|
-
return styles.get(property)?.toString()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
|
|
23
16
|
return value === 'currentColor' || value === currentColor
|
|
24
17
|
}
|
|
@@ -27,11 +20,12 @@ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
|
|
|
27
20
|
return parentStyles[property] === value
|
|
28
21
|
}
|
|
29
22
|
|
|
23
|
+
// see comment below about why we exclude border styles
|
|
30
24
|
const isExcludedBorder =
|
|
31
25
|
(borderDirection: string): CanSkipRule =>
|
|
32
|
-
(value, property, {
|
|
33
|
-
const borderWidth = getStyle(
|
|
34
|
-
const borderStyle = getStyle(
|
|
26
|
+
(value, property, { getStyle }) => {
|
|
27
|
+
const borderWidth = getStyle(`border-${borderDirection}-width`)
|
|
28
|
+
const borderStyle = getStyle(`border-${borderDirection}-style`)
|
|
35
29
|
|
|
36
30
|
if (borderWidth === '0px') return true
|
|
37
31
|
if (borderStyle === 'none') return true
|
|
@@ -101,15 +101,11 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
|
|
|
101
101
|
// urls, and things like videos will be converted to images.
|
|
102
102
|
await Promise.all(foreignObjectChildren.map((el) => embedMedia(el as HTMLElement)))
|
|
103
103
|
|
|
104
|
-
await styleEmbedder.collectDefaultStyles([
|
|
105
|
-
...svg.querySelectorAll('foreignObject.tl-export-embed-styles *'),
|
|
106
|
-
])
|
|
107
|
-
|
|
108
104
|
// read the computed styles of every element (+ it's children & pseudo-elements) in the
|
|
109
105
|
// document. we do this in a single pass before we start embedding any CSS stuff to avoid
|
|
110
106
|
// constantly forcing the browser to recompute styles & layout.
|
|
111
107
|
for (const el of foreignObjectChildren) {
|
|
112
|
-
|
|
108
|
+
styleEmbedder.readRootElementStyles(el as HTMLElement)
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
// fetch any resources that we need to embed in the CSS, like background images.
|
|
@@ -53,7 +53,11 @@ export function useCanvasEvents() {
|
|
|
53
53
|
|
|
54
54
|
// For tools that benefit from a higher fidelity of events,
|
|
55
55
|
// we dispatch the coalesced events.
|
|
56
|
-
|
|
56
|
+
// N.B. Sometimes getCoalescedEvents isn't present on iOS, ugh.
|
|
57
|
+
const events =
|
|
58
|
+
currentTool.useCoalescedEvents && e.nativeEvent.getCoalescedEvents
|
|
59
|
+
? e.nativeEvent.getCoalescedEvents()
|
|
60
|
+
: [e]
|
|
57
61
|
for (const singleEvent of events) {
|
|
58
62
|
editor.dispatch({
|
|
59
63
|
type: 'pointer',
|
|
@@ -107,7 +111,7 @@ export function useCanvasEvents() {
|
|
|
107
111
|
if (
|
|
108
112
|
e.target.tagName !== 'A' &&
|
|
109
113
|
e.target.tagName !== 'TEXTAREA' &&
|
|
110
|
-
e.target.isContentEditable &&
|
|
114
|
+
!e.target.isContentEditable &&
|
|
111
115
|
// When in EditingShape state, we are actually clicking on a 'DIV'
|
|
112
116
|
// not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
|
|
113
117
|
// for edit mode on mobile we need to not preventDefault.
|
|
@@ -104,6 +104,7 @@ export function useDocumentEvents() {
|
|
|
104
104
|
|
|
105
105
|
if ((e as any).isKilled) return
|
|
106
106
|
;(e as any).isKilled = true
|
|
107
|
+
const hasSelectedShapes = !!editor.getSelectedShapeIds().length
|
|
107
108
|
|
|
108
109
|
switch (e.key) {
|
|
109
110
|
case '=':
|
|
@@ -124,6 +125,23 @@ export function useDocumentEvents() {
|
|
|
124
125
|
if (areShortcutsDisabled(editor)) {
|
|
125
126
|
return
|
|
126
127
|
}
|
|
128
|
+
if (hasSelectedShapes) {
|
|
129
|
+
// This is used in tandem with shape navigation.
|
|
130
|
+
preventDefault(e)
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
case 'ArrowLeft':
|
|
135
|
+
case 'ArrowRight':
|
|
136
|
+
case 'ArrowUp':
|
|
137
|
+
case 'ArrowDown': {
|
|
138
|
+
if (areShortcutsDisabled(editor)) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (hasSelectedShapes && (e.metaKey || e.ctrlKey)) {
|
|
142
|
+
// This is used in tandem with shape navigation.
|
|
143
|
+
preventDefault(e)
|
|
144
|
+
}
|
|
127
145
|
break
|
|
128
146
|
}
|
|
129
147
|
case ',': {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
2
|
import { memo, useRef } from 'react'
|
|
3
|
-
import { tlenv } from '../globals/environment'
|
|
4
3
|
import { useCanvasEvents } from '../hooks/useCanvasEvents'
|
|
5
4
|
import { useEditor } from '../hooks/useEditor'
|
|
6
5
|
import { usePassThroughWheelEvents } from '../hooks/usePassThroughWheelEvents'
|
|
@@ -57,29 +56,17 @@ const WatermarkInner = memo(function WatermarkInner({ src }: { src: string }) {
|
|
|
57
56
|
draggable={false}
|
|
58
57
|
{...events}
|
|
59
58
|
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
) : (
|
|
72
|
-
<a
|
|
73
|
-
href={url}
|
|
74
|
-
target="_blank"
|
|
75
|
-
rel="noreferrer"
|
|
76
|
-
draggable={false}
|
|
77
|
-
onPointerDown={(e) => {
|
|
78
|
-
stopEventPropagation(e)
|
|
79
|
-
}}
|
|
80
|
-
style={{ mask: maskCss, WebkitMask: maskCss }}
|
|
81
|
-
/>
|
|
82
|
-
)}
|
|
59
|
+
<button
|
|
60
|
+
draggable={false}
|
|
61
|
+
role="button"
|
|
62
|
+
onPointerDown={(e) => {
|
|
63
|
+
stopEventPropagation(e)
|
|
64
|
+
preventDefault(e)
|
|
65
|
+
}}
|
|
66
|
+
title="made with tldraw"
|
|
67
|
+
onClick={() => runtime.openWindow(url, '_blank')}
|
|
68
|
+
style={{ mask: maskCss, WebkitMask: maskCss }}
|
|
69
|
+
/>
|
|
83
70
|
</div>
|
|
84
71
|
)
|
|
85
72
|
})
|
|
@@ -115,7 +102,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
115
102
|
box-sizing: content-box;
|
|
116
103
|
}
|
|
117
104
|
|
|
118
|
-
.${className} >
|
|
105
|
+
.${className} > button {
|
|
119
106
|
position: absolute;
|
|
120
107
|
width: 96px;
|
|
121
108
|
height: 32px;
|
|
@@ -123,6 +110,8 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
123
110
|
cursor: inherit;
|
|
124
111
|
color: var(--color-text);
|
|
125
112
|
opacity: .38;
|
|
113
|
+
border: 0;
|
|
114
|
+
padding: 0;
|
|
126
115
|
background-color: currentColor;
|
|
127
116
|
}
|
|
128
117
|
|
|
@@ -137,13 +126,13 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
137
126
|
height: 48px;
|
|
138
127
|
}
|
|
139
128
|
|
|
140
|
-
.${className}[data-mobile='true'] >
|
|
129
|
+
.${className}[data-mobile='true'] > button {
|
|
141
130
|
width: 8px;
|
|
142
131
|
height: 32px;
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
@media (hover: hover) {
|
|
146
|
-
.${className} >
|
|
135
|
+
.${className} > button {
|
|
147
136
|
pointer-events: none;
|
|
148
137
|
}
|
|
149
138
|
|
|
@@ -153,12 +142,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
153
142
|
transition-delay: 0.32s;
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
.${className}:hover >
|
|
145
|
+
.${className}:hover > button {
|
|
157
146
|
animation: delayed_link 0.2s forwards ease-in-out;
|
|
158
147
|
animation-delay: 0.32s;
|
|
159
148
|
}
|
|
160
149
|
|
|
161
|
-
.${className} >
|
|
150
|
+
.${className} > button:focus-visible {
|
|
162
151
|
opacity: 1;
|
|
163
152
|
}
|
|
164
153
|
}
|
|
@@ -1,12 +1,51 @@
|
|
|
1
|
+
import { assert } from '@tldraw/utils'
|
|
1
2
|
import { Box } from '../Box'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { Mat, MatModel } from '../Mat'
|
|
4
|
+
import { Vec, VecLike } from '../Vec'
|
|
5
|
+
import {
|
|
6
|
+
intersectCirclePolygon,
|
|
7
|
+
intersectCirclePolyline,
|
|
8
|
+
intersectLineSegmentPolygon,
|
|
9
|
+
intersectLineSegmentPolyline,
|
|
10
|
+
intersectPolys,
|
|
11
|
+
} from '../intersect'
|
|
12
|
+
import { approximately, pointInPolygon } from '../utils'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Filter geometry within a group.
|
|
16
|
+
*
|
|
17
|
+
* Filters are ignored when called directly on primitive geometries, but can be used to narrow down
|
|
18
|
+
* the results of an operation on `Group2d` geometries.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface Geometry2dFilters {
|
|
23
|
+
readonly includeLabels?: boolean
|
|
24
|
+
readonly includeInternal?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @public */
|
|
28
|
+
export const Geometry2dFilters: {
|
|
29
|
+
EXCLUDE_NON_STANDARD: Geometry2dFilters
|
|
30
|
+
INCLUDE_ALL: Geometry2dFilters
|
|
31
|
+
EXCLUDE_LABELS: Geometry2dFilters
|
|
32
|
+
EXCLUDE_INTERNAL: Geometry2dFilters
|
|
33
|
+
} = {
|
|
34
|
+
EXCLUDE_NON_STANDARD: {
|
|
35
|
+
includeLabels: false,
|
|
36
|
+
includeInternal: false,
|
|
37
|
+
},
|
|
38
|
+
INCLUDE_ALL: { includeLabels: true, includeInternal: true },
|
|
39
|
+
EXCLUDE_LABELS: { includeLabels: false, includeInternal: true },
|
|
40
|
+
EXCLUDE_INTERNAL: { includeLabels: true, includeInternal: false },
|
|
41
|
+
}
|
|
4
42
|
|
|
5
43
|
/** @public */
|
|
6
44
|
export interface Geometry2dOptions {
|
|
7
45
|
isFilled: boolean
|
|
8
46
|
isClosed: boolean
|
|
9
47
|
isLabel?: boolean
|
|
48
|
+
isInternal?: boolean
|
|
10
49
|
debugColor?: string
|
|
11
50
|
ignore?: boolean
|
|
12
51
|
}
|
|
@@ -16,6 +55,7 @@ export abstract class Geometry2d {
|
|
|
16
55
|
isFilled = false
|
|
17
56
|
isClosed = true
|
|
18
57
|
isLabel = false
|
|
58
|
+
isInternal = false
|
|
19
59
|
debugColor?: string
|
|
20
60
|
ignore?: boolean
|
|
21
61
|
|
|
@@ -23,20 +63,23 @@ export abstract class Geometry2d {
|
|
|
23
63
|
this.isFilled = opts.isFilled
|
|
24
64
|
this.isClosed = opts.isClosed
|
|
25
65
|
this.isLabel = opts.isLabel ?? false
|
|
66
|
+
this.isInternal = opts.isInternal ?? false
|
|
26
67
|
this.debugColor = opts.debugColor
|
|
27
68
|
this.ignore = opts.ignore
|
|
28
69
|
}
|
|
29
70
|
|
|
30
|
-
|
|
71
|
+
isExcludedByFilter(filters?: Geometry2dFilters) {
|
|
72
|
+
if (!filters) return false
|
|
73
|
+
if (this.isLabel && !filters.includeLabels) return true
|
|
74
|
+
if (this.isInternal && !filters.includeInternal) return true
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
31
77
|
|
|
32
|
-
abstract
|
|
78
|
+
abstract getVertices(filters: Geometry2dFilters): Vec[]
|
|
33
79
|
|
|
34
|
-
|
|
35
|
-
// // We've removed the broad phase here; that should be done outside of the call
|
|
36
|
-
// return this.distanceToPoint(point, hitInside) <= margin
|
|
37
|
-
// }
|
|
80
|
+
abstract nearestPoint(point: Vec, _filters?: Geometry2dFilters): Vec
|
|
38
81
|
|
|
39
|
-
hitTestPoint(point: Vec, margin = 0, hitInside = false) {
|
|
82
|
+
hitTestPoint(point: Vec, margin = 0, hitInside = false, _filters?: Geometry2dFilters) {
|
|
40
83
|
// First check whether the point is inside
|
|
41
84
|
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
|
|
42
85
|
return true
|
|
@@ -45,17 +88,17 @@ export abstract class Geometry2d {
|
|
|
45
88
|
return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin
|
|
46
89
|
}
|
|
47
90
|
|
|
48
|
-
distanceToPoint(point: Vec, hitInside = false) {
|
|
91
|
+
distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
|
|
49
92
|
return (
|
|
50
|
-
point.dist(this.nearestPoint(point)) *
|
|
93
|
+
point.dist(this.nearestPoint(point, filters)) *
|
|
51
94
|
(this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
|
|
52
95
|
? -1
|
|
53
96
|
: 1)
|
|
54
97
|
)
|
|
55
98
|
}
|
|
56
99
|
|
|
57
|
-
distanceToLineSegment(A: Vec, B: Vec) {
|
|
58
|
-
if (A.equals(B)) return this.distanceToPoint(A)
|
|
100
|
+
distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
|
|
101
|
+
if (A.equals(B)) return this.distanceToPoint(A, false, filters)
|
|
59
102
|
const { vertices } = this
|
|
60
103
|
let nearest: Vec | undefined
|
|
61
104
|
let dist = Infinity
|
|
@@ -73,10 +116,35 @@ export abstract class Geometry2d {
|
|
|
73
116
|
return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
|
|
74
117
|
}
|
|
75
118
|
|
|
76
|
-
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
|
77
|
-
return this.distanceToLineSegment(A, B) <= distance
|
|
119
|
+
hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
|
|
120
|
+
return this.distanceToLineSegment(A, B, filters) <= distance
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
intersectLineSegment(A: VecLike, B: VecLike, _filters?: Geometry2dFilters): VecLike[] {
|
|
124
|
+
const intersections = this.isClosed
|
|
125
|
+
? intersectLineSegmentPolygon(A, B, this.vertices)
|
|
126
|
+
: intersectLineSegmentPolyline(A, B, this.vertices)
|
|
127
|
+
|
|
128
|
+
return intersections ?? []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
intersectCircle(center: VecLike, radius: number, _filters?: Geometry2dFilters): VecLike[] {
|
|
132
|
+
const intersections = this.isClosed
|
|
133
|
+
? intersectCirclePolygon(center, radius, this.vertices)
|
|
134
|
+
: intersectCirclePolyline(center, radius, this.vertices)
|
|
135
|
+
|
|
136
|
+
return intersections ?? []
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
intersectPolygon(polygon: VecLike[], _filters?: Geometry2dFilters): VecLike[] {
|
|
140
|
+
return intersectPolys(polygon, this.vertices, true, this.isClosed)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
intersectPolyline(polyline: VecLike[], _filters?: Geometry2dFilters): VecLike[] {
|
|
144
|
+
return intersectPolys(polyline, this.vertices, false, this.isClosed)
|
|
78
145
|
}
|
|
79
146
|
|
|
147
|
+
/** @deprecated Iterate the vertices instead. */
|
|
80
148
|
nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
|
|
81
149
|
const { vertices } = this
|
|
82
150
|
let nearest: Vec | undefined
|
|
@@ -105,12 +173,16 @@ export abstract class Geometry2d {
|
|
|
105
173
|
)
|
|
106
174
|
}
|
|
107
175
|
|
|
176
|
+
transform(transform: MatModel): Geometry2d {
|
|
177
|
+
return new TransformedGeometry2d(this, transform)
|
|
178
|
+
}
|
|
179
|
+
|
|
108
180
|
private _vertices: Vec[] | undefined
|
|
109
181
|
|
|
110
182
|
// eslint-disable-next-line no-restricted-syntax
|
|
111
183
|
get vertices(): Vec[] {
|
|
112
184
|
if (!this._vertices) {
|
|
113
|
-
this._vertices = this.getVertices()
|
|
185
|
+
this._vertices = this.getVertices(Geometry2dFilters.EXCLUDE_LABELS)
|
|
114
186
|
}
|
|
115
187
|
|
|
116
188
|
return this._vertices
|
|
@@ -204,3 +276,111 @@ export abstract class Geometry2d {
|
|
|
204
276
|
|
|
205
277
|
abstract getSvgPathData(first: boolean): string
|
|
206
278
|
}
|
|
279
|
+
|
|
280
|
+
// =================================================================================================
|
|
281
|
+
// Because Geometry2d.transform depends on TransformedGeometry2d, we need to define it here instead
|
|
282
|
+
// of in its own files. This prevents a circular import error.
|
|
283
|
+
// =================================================================================================
|
|
284
|
+
|
|
285
|
+
/** @public */
|
|
286
|
+
export class TransformedGeometry2d extends Geometry2d {
|
|
287
|
+
private readonly inverse: MatModel
|
|
288
|
+
private readonly decomposed
|
|
289
|
+
|
|
290
|
+
constructor(
|
|
291
|
+
private readonly geometry: Geometry2d,
|
|
292
|
+
private readonly matrix: MatModel
|
|
293
|
+
) {
|
|
294
|
+
super(geometry)
|
|
295
|
+
this.inverse = Mat.Inverse(matrix)
|
|
296
|
+
this.decomposed = Mat.Decompose(matrix)
|
|
297
|
+
|
|
298
|
+
assert(
|
|
299
|
+
approximately(this.decomposed.scaleX, this.decomposed.scaleY),
|
|
300
|
+
'non-uniform scaling is not yet supported'
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getVertices(filters: Geometry2dFilters): Vec[] {
|
|
305
|
+
return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
nearestPoint(point: Vec, filters?: Geometry2dFilters): Vec {
|
|
309
|
+
return Mat.applyToPoint(
|
|
310
|
+
this.matrix,
|
|
311
|
+
this.geometry.nearestPoint(Mat.applyToPoint(this.inverse, point), filters)
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
override hitTestPoint(
|
|
316
|
+
point: Vec,
|
|
317
|
+
margin = 0,
|
|
318
|
+
hitInside?: boolean,
|
|
319
|
+
filters?: Geometry2dFilters
|
|
320
|
+
): boolean {
|
|
321
|
+
return this.geometry.hitTestPoint(
|
|
322
|
+
Mat.applyToPoint(this.inverse, point),
|
|
323
|
+
margin / this.decomposed.scaleX,
|
|
324
|
+
hitInside,
|
|
325
|
+
filters
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
override distanceToPoint(point: Vec, hitInside = false, filters?: Geometry2dFilters) {
|
|
330
|
+
return (
|
|
331
|
+
this.geometry.distanceToPoint(Mat.applyToPoint(this.inverse, point), hitInside, filters) *
|
|
332
|
+
this.decomposed.scaleX
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
override distanceToLineSegment(A: Vec, B: Vec, filters?: Geometry2dFilters) {
|
|
337
|
+
return (
|
|
338
|
+
this.geometry.distanceToLineSegment(
|
|
339
|
+
Mat.applyToPoint(this.inverse, A),
|
|
340
|
+
Mat.applyToPoint(this.inverse, B),
|
|
341
|
+
filters
|
|
342
|
+
) * this.decomposed.scaleX
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
override hitTestLineSegment(A: Vec, B: Vec, distance = 0, filters?: Geometry2dFilters): boolean {
|
|
347
|
+
return this.geometry.hitTestLineSegment(
|
|
348
|
+
Mat.applyToPoint(this.inverse, A),
|
|
349
|
+
Mat.applyToPoint(this.inverse, B),
|
|
350
|
+
distance / this.decomposed.scaleX,
|
|
351
|
+
filters
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
|
|
356
|
+
return this.geometry.intersectLineSegment(
|
|
357
|
+
Mat.applyToPoint(this.inverse, A),
|
|
358
|
+
Mat.applyToPoint(this.inverse, B),
|
|
359
|
+
filters
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
|
|
364
|
+
return this.geometry.intersectCircle(
|
|
365
|
+
Mat.applyToPoint(this.inverse, center),
|
|
366
|
+
radius / this.decomposed.scaleX,
|
|
367
|
+
filters
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[] {
|
|
372
|
+
return this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[] {
|
|
376
|
+
return this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
override transform(transform: MatModel): Geometry2d {
|
|
380
|
+
return new TransformedGeometry2d(this.geometry, Mat.Multiply(transform, this.matrix))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getSvgPathData(): string {
|
|
384
|
+
throw new Error('Cannot get SVG path data for transformed geometry.')
|
|
385
|
+
}
|
|
386
|
+
}
|