Merge branch 'main' into alex/auto-undo-redo

alex/no-batches
alex 2024-04-11 12:14:16 +01:00
commit e5bbaee67b
64 zmienionych plików z 974 dodań i 635 usunięć

Wyświetl plik

@ -31,7 +31,7 @@ To import fonts and CSS for tldraw:
- Copy and paste this into the file:
```CSS
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;700;&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap");
@import url("tldraw/tldraw.css");
body {

Wyświetl plik

@ -7,7 +7,7 @@ export function CursorChatMenuItem() {
const shouldShow = useValue(
'show cursor chat',
() => {
return editor.getInstanceState().isCoarsePointer && !editor.getSelectedShapes().length
return !editor.getInstanceState().isCoarsePointer
},
[editor]
)

Wyświetl plik

@ -108,34 +108,33 @@ export function getSaveFileCopyAction(
readonlyOk: true,
kbd: '$s',
async onSelect(source) {
handleUiEvent('save-project-to-file', { source })
const documentName =
editor.getDocumentSettings().name === ''
? defaultDocumentName
: editor.getDocumentSettings().name
const defaultName =
saveFileNames.get(editor.store) || `${documentName}${TLDRAW_FILE_EXTENSION}`
const blobToSave = serializeTldrawJsonBlob(editor.store)
let handle
try {
handleUiEvent('save-project-to-file', { source })
const documentName =
editor.getDocumentSettings().name === ''
? defaultDocumentName
: editor.getDocumentSettings().name
const defaultName =
saveFileNames.get(editor.store) || `${documentName}${TLDRAW_FILE_EXTENSION}`
const blobToSave = serializeTldrawJsonBlob(editor.store)
const handle = await fileSave(blobToSave, {
handle = await fileSave(blobToSave, {
fileName: defaultName,
extensions: [TLDRAW_FILE_EXTENSION],
description: 'tldraw project',
})
if (handle) {
// we deliberately don't store the handle for re-use
// next time. we always want to save a copy, but to
// help the user out we'll remember the last name
// they used
saveFileNames.set(editor.store, handle.name)
} else {
throw Error('Could not save file.')
}
} catch (e) {
console.error(e)
// user cancelled
return
}
if (handle) {
// we deliberately don't store the handle for re-use
// next time. we always want to save a copy, but to
// help the user out we'll remember the last name
// they used
saveFileNames.set(editor.store, handle.name)
}
},
}

Wyświetl plik

@ -0,0 +1,27 @@
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function BasicExample() {
return (
<div className="tldraw__editor">
<Tldraw
overrides={{
actions: (_editor, actions, _helpers) => {
const newActions = {
...actions,
delete: { ...actions['delete'], kbd: 'x' },
}
return newActions
},
}}
/>
</div>
)
}
/*
This example shows how you can override tldraw's actions object to change the keyboard shortcuts.
In this case we're changing the delete action's shortcut to 'x'. To customize the actions menu
please see the custom actions menu example. For more information on keyboard shortcuts see the
keyboard shortcuts example.
*/

Wyświetl plik

@ -0,0 +1,12 @@
---
title: Action overrides
component: ./ActionOverridesExample.tsx
category: ui
priority: 2
---
Override tldraw's actions
---
This example shows how you can override tldraw's actions object to change the keyboard shortcuts. In this case we're changing the delete action's shortcut to 'x'. To customize the actions menu please see the custom actions menu example. For more information on keyboard shortcuts see the keyboard shortcuts example.

Wyświetl plik

@ -36,7 +36,8 @@ export function CustomRenderer() {
const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() })
const currentPageId = editor.getCurrentPageId()
for (const { shape, maskedPageBounds, opacity } of renderingShapes) {
for (const { shape, opacity } of renderingShapes) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(shape)
if (!maskedPageBounds) continue
ctx.save()

Wyświetl plik

@ -1,6 +1,5 @@
import { TLUiActionsContextType, TLUiOverrides, TLUiToolsContextType, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
// There's a guide at the bottom of this file!
@ -8,13 +7,18 @@ import jsonSnapshot from './snapshot.json'
const overrides: TLUiOverrides = {
//[a]
actions(_editor, actions): TLUiActionsContextType {
actions['toggle-grid'].kbd = 'x'
return actions
const newActions = {
...actions,
'toggle-grid': { ...actions['toggle-grid'], kbd: 'x' },
'copy-as-png': { ...actions['copy-as-png'], kbd: '$1' },
}
return newActions
},
//[b]
tools(_editor, tools): TLUiToolsContextType {
tools['draw'].kbd = 'p'
return tools
const newTools = { ...tools, draw: { ...tools.draw, kbd: 'p' } }
return newTools
},
}
@ -22,7 +26,7 @@ const overrides: TLUiOverrides = {
export default function KeyboardShortcuts() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_kbd_shortcuts" overrides={overrides} snapshot={jsonSnapshot} />
<Tldraw overrides={overrides} />
</div>
)
}

Wyświetl plik

@ -5,8 +5,14 @@ category: ui
priority: 2
---
Override default keyboard shortcuts.
How to replace tldraw's default keyboard shortcuts with your own.
---
How to replace tldraw's default keyboard shortcuts with your own.
This example shows how you can replace tldraw's default keyboard shortcuts with your own,
or add a shortcut for an action that doesn't have one. An example of how to add shortcuts
for custom tools can be found in the custom-config example.
- Toggle show grid by pressing 'x'
- Select the Draw tool by pressing 'p'
- Copy as png by pressing 'ctrl/cmd + 1'

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -5,26 +5,34 @@ export function useChangedShapesReactor(
cb: (info: { culled: TLShape[]; restored: TLShape[] }) => void
) {
const editor = useEditor()
const rPrevShapes = useRef(editor.getRenderingShapes())
const rPrevShapes = useRef({
renderingShapes: editor.getRenderingShapes(),
culledShapes: editor.getCulledShapes(),
})
useEffect(() => {
return react('when rendering shapes change', () => {
const after = editor.getRenderingShapes()
const after = {
culledShapes: editor.getCulledShapes(),
renderingShapes: editor.getRenderingShapes(),
}
const before = rPrevShapes.current
const culled: TLShape[] = []
const restored: TLShape[] = []
const beforeToVisit = new Set(before)
const beforeToVisit = new Set(before.renderingShapes)
for (const afterInfo of after) {
const beforeInfo = before.find((s) => s.id === afterInfo.id)
for (const afterInfo of after.renderingShapes) {
const beforeInfo = before.renderingShapes.find((s) => s.id === afterInfo.id)
if (!beforeInfo) {
continue
} else {
if (afterInfo.isCulled && !beforeInfo.isCulled) {
const isAfterCulled = after.culledShapes.has(afterInfo.id)
const isBeforeCulled = before.culledShapes.has(beforeInfo.id)
if (isAfterCulled && !isBeforeCulled) {
culled.push(afterInfo.shape)
} else if (!afterInfo.isCulled && beforeInfo.isCulled) {
} else if (!isAfterCulled && isBeforeCulled) {
restored.push(afterInfo.shape)
}
beforeToVisit.delete(beforeInfo)

Wyświetl plik

@ -1,3 +1,7 @@
## 2.0.27
- Bug fixes and performance improvements.
## 2.0.26
- Bug fixes and performance improvements.

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "tldraw-vscode",
"description": "The tldraw extension for VS Code.",
"version": "2.0.26",
"version": "2.0.27",
"private": true,
"author": {
"name": "tldraw Inc.",

Wyświetl plik

@ -516,7 +516,7 @@ export function degreesToRadians(d: number): number;
export const DOUBLE_CLICK_DURATION = 450;
// @internal (undocumented)
export const DRAG_DISTANCE = 4;
export const DRAG_DISTANCE = 16;
// @public (undocumented)
export const EASINGS: {
@ -677,6 +677,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// @internal
getCrashingError(): unknown;
getCroppingShapeId(): null | TLShapeId;
getCulledShapes(): Set<TLShapeId>;
getCurrentPage(): TLPage;
getCurrentPageBounds(): Box | undefined;
getCurrentPageId(): TLPageId;
@ -714,7 +715,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
getRenderingBounds(): Box;
getRenderingBoundsExpanded(): Box;
getRenderingShapes(): {
id: TLShapeId;
shape: TLShape;
@ -722,8 +722,6 @@ export class Editor extends EventEmitter<TLEventMap> {
index: number;
backgroundIndex: number;
opacity: number;
isCulled: boolean;
maskedPageBounds: Box | undefined;
}[];
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined;
getSelectedShapeIds(): TLShapeId[];
@ -784,8 +782,6 @@ export class Editor extends EventEmitter<TLEventMap> {
index: number;
backgroundIndex: number;
opacity: number;
isCulled: boolean;
maskedPageBounds: Box | undefined;
}[];
getViewportPageBounds(): Box;
getViewportPageCenter(): Vec;
@ -1676,7 +1672,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canResize: TLShapeUtilFlag<Shape>;
canScroll: TLShapeUtilFlag<Shape>;
canSnap: TLShapeUtilFlag<Shape>;
canUnmount: TLShapeUtilFlag<Shape>;
abstract component(shape: Shape): any;
// (undocumented)
editor: Editor;
@ -1888,6 +1883,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
// (undocumented)
onKeyUp?: TLEventHandlers['onKeyUp'];
// (undocumented)
onLongPress?: TLEventHandlers['onLongPress'];
// (undocumented)
onMiddleClick?: TLEventHandlers['onMiddleClick'];
// (undocumented)
onPointerDown?: TLEventHandlers['onPointerDown'];
@ -2185,6 +2182,8 @@ export interface TLEventHandlers {
// (undocumented)
onKeyUp: TLKeyboardEvent;
// (undocumented)
onLongPress: TLPointerEvent;
// (undocumented)
onMiddleClick: TLPointerEvent;
// (undocumented)
onPointerDown: TLPointerEvent;
@ -2437,7 +2436,7 @@ export type TLPointerEventInfo = TLBaseEventInfo & {
} & TLPointerEventTarget;
// @public (undocumented)
export type TLPointerEventName = 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click';
export type TLPointerEventName = 'long_press' | 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click';
// @public (undocumented)
export type TLPointerEventTarget = {

Wyświetl plik

@ -10367,6 +10367,51 @@
"isAbstract": false,
"name": "getCroppingShapeId"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCulledShapes:member(1)",
"docComment": "/**\n * Get culled shapes.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getCulledShapes(): "
},
{
"kind": "Reference",
"text": "Set",
"canonicalReference": "!Set:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLShapeId",
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getCulledShapes"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getCurrentPage:member(1)",
@ -11959,38 +12004,6 @@
"isAbstract": false,
"name": "getRenderingBounds"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getRenderingBoundsExpanded:member(1)",
"docComment": "/**\n * The current rendering bounds in the current page space, expanded slightly. Used for determining which shapes to render and which to \"cull\".\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getRenderingBoundsExpanded(): "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getRenderingBoundsExpanded"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)",
@ -12038,16 +12051,7 @@
},
{
"kind": "Content",
"text": ">;\n index: number;\n backgroundIndex: number;\n opacity: number;\n isCulled: boolean;\n maskedPageBounds: "
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": " | undefined;\n }[]"
"text": ">;\n index: number;\n backgroundIndex: number;\n opacity: number;\n }[]"
},
{
"kind": "Content",
@ -12057,7 +12061,7 @@
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 12
"endIndex": 10
},
"releaseTag": "Public",
"isProtected": false,
@ -31139,41 +31143,6 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#canUnmount:member",
"docComment": "/**\n * Whether the shape should unmount when not visible in the editor. Consider keeping this to false if the shape's `component` has local state.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "canUnmount: "
},
{
"kind": "Reference",
"text": "TLShapeUtilFlag",
"canonicalReference": "@tldraw/editor!TLShapeUtilFlag:type"
},
{
"kind": "Content",
"text": "<Shape>"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "canUnmount",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!ShapeUtil#component:member(1)",
@ -35319,6 +35288,41 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!StateNode#onLongPress:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "onLongPress?: "
},
{
"kind": "Reference",
"text": "TLEventHandlers",
"canonicalReference": "@tldraw/editor!TLEventHandlers:interface"
},
{
"kind": "Content",
"text": "['onLongPress']"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onLongPress",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!StateNode#onMiddleClick:member",
@ -38590,6 +38594,34 @@
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/editor!TLEventHandlers#onLongPress:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "onLongPress: "
},
{
"kind": "Reference",
"text": "TLPointerEvent",
"canonicalReference": "@tldraw/editor!TLPointerEvent:type"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "onLongPress",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/editor!TLEventHandlers#onMiddleClick:member",
@ -41123,7 +41155,7 @@
},
{
"kind": "Content",
"text": "'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'"
"text": "'long_press' | 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'"
},
{
"kind": "Content",

Wyświetl plik

@ -24,10 +24,12 @@
/* Z Index */
--layer-background: 100;
--layer-grid: 150;
--layer-culled-shapes: 175;
--layer-canvas: 200;
--layer-shapes: 300;
--layer-overlays: 400;
--layer-following-indicator: 1000;
--layer-blocker: 10000;
/* Misc */
--tl-zoom: 1;
@ -236,6 +238,20 @@ input,
contain: strict;
}
.tl-culled-shapes {
width: 100%;
height: 100%;
z-index: var(--layer-culled-shapes);
position: absolute;
pointer-events: none;
contain: size layout;
}
.tl-culled-shapes__canvas {
width: 100%;
height: 100%;
}
.tl-shapes {
position: relative;
z-index: var(--layer-shapes);
@ -264,18 +280,25 @@ input,
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
}
/* ------------------- Background ------------------- */
.tl-background {
.tl-background__wrapper {
z-index: var(--layer-background);
position: absolute;
background-color: var(--color-background);
inset: 0px;
height: 100%;
width: 100%;
z-index: var(--layer-background);
}
.tl-background {
background-color: var(--color-background);
width: 100%;
height: 100%;
}
/* --------------------- Grid Layer --------------------- */
@ -338,10 +361,13 @@ input,
}
.tl-shape__culled {
position: relative;
position: absolute;
pointer-events: none;
overflow: visible;
transform-origin: top left;
contain: size layout;
background-color: var(--color-culled);
width: 100%;
height: 100%;
z-index: 0;
}
/* ---------------- Shape Containers ---------------- */
@ -1480,3 +1506,18 @@ it from receiving any pointer events or affecting the cursor. */
font-size: 12px;
font-family: monospace;
}
/* ---------------- Hit test blocker ---------------- */
.tl-hit-test-blocker {
position: absolute;
z-index: var(--layer-blocker);
inset: 0px;
width: 100%;
height: 100%;
pointer-events: all;
}
.tl-hit-test-blocker__hidden {
display: none;
}

Wyświetl plik

@ -1,12 +1,10 @@
import { useQuickReactor, useStateTracking } from '@tldraw/state'
import { IdOf } from '@tldraw/store'
import { TLShape, TLShapeId } from '@tldraw/tlschema'
import { memo, useCallback, useRef } from 'react'
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
import { useEditor } from '../hooks/useEditor'
import { useEditorComponents } from '../hooks/useEditorComponents'
import { Mat } from '../primitives/Mat'
import { toDomPrecision } from '../primitives/utils'
import { setStyleProperty } from '../utils/dom'
import { OptionalErrorBoundary } from './ErrorBoundary'
@ -28,7 +26,6 @@ export const Shape = memo(function Shape({
index,
backgroundIndex,
opacity,
isCulled,
dprMultiple,
}: {
id: TLShapeId
@ -37,7 +34,6 @@ export const Shape = memo(function Shape({
index: number
backgroundIndex: number
opacity: number
isCulled: boolean
dprMultiple: number
}) {
const editor = useEditor()
@ -52,6 +48,9 @@ export const Shape = memo(function Shape({
clipPath: 'none',
width: 0,
height: 0,
x: 0,
y: 0,
isCulled: false,
})
useQuickReactor(
@ -71,7 +70,11 @@ export const Shape = memo(function Shape({
}
// Page transform
const transform = Mat.toCssString(editor.getShapePageTransform(id))
const pageTransform = editor.getShapePageTransform(id)
const transform = Mat.toCssString(pageTransform)
const bounds = editor.getShapeGeometry(shape).bounds
// Update if the tranform has changed
if (transform !== prev.transform) {
setStyleProperty(containerRef.current, 'transform', transform)
setStyleProperty(bgContainerRef.current, 'transform', transform)
@ -81,7 +84,6 @@ export const Shape = memo(function Shape({
// Width / Height
// We round the shape width and height up to the nearest multiple of dprMultiple
// to avoid the browser making miscalculations when applying the transform.
const bounds = editor.getShapeGeometry(shape).bounds
const widthRemainder = bounds.w % dprMultiple
const heightRemainder = bounds.h % dprMultiple
const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
@ -117,6 +119,22 @@ export const Shape = memo(function Shape({
[opacity, index, backgroundIndex]
)
useQuickReactor(
'set display',
() => {
const shape = editor.getShape(id)
if (!shape) return // probably the shape was just deleted
const culledShapes = editor.getCulledShapes()
const isCulled = culledShapes.has(id)
if (isCulled !== memoizedStuffRef.current.isCulled) {
setStyleProperty(containerRef.current, 'display', isCulled ? 'none' : 'block')
setStyleProperty(bgContainerRef.current, 'display', isCulled ? 'none' : 'block')
memoizedStuffRef.current.isCulled = isCulled
}
},
[editor]
)
const annotateError = useCallback(
(error: any) => editor.annotateError(error, { origin: 'shape', willCrashApp: false }),
[editor]
@ -133,21 +151,15 @@ export const Shape = memo(function Shape({
data-shape-type={shape.type}
draggable={false}
>
{isCulled ? null : (
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
<InnerShapeBackground shape={shape} util={util} />
</OptionalErrorBoundary>
)}
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
<InnerShapeBackground shape={shape} util={util} />
</OptionalErrorBoundary>
</div>
)}
<div ref={containerRef} className="tl-shape" data-shape-type={shape.type} draggable={false}>
{isCulled ? (
<CulledShape shapeId={shape.id} />
) : (
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
<InnerShape shape={shape} util={util} />
</OptionalErrorBoundary>
)}
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
<InnerShape shape={shape} util={util} />
</OptionalErrorBoundary>
</div>
</>
)
@ -172,23 +184,3 @@ const InnerShapeBackground = memo(
},
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
)
const CulledShape = function CulledShape<T extends TLShape>({ shapeId }: { shapeId: IdOf<T> }) {
const editor = useEditor()
const culledRef = useRef<HTMLDivElement>(null)
useQuickReactor(
'set shape stuff',
() => {
const bounds = editor.getShapeGeometry(shapeId).bounds
setStyleProperty(
culledRef.current,
'transform',
`translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision(bounds.minY)}px)`
)
},
[editor]
)
return <div ref={culledRef} className="tl-shape__culled" />
}

Wyświetl plik

@ -3,9 +3,10 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
import classNames from 'classnames'
import { Fragment, JSX, useEffect, useRef, useState } from 'react'
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants'
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, TEXT_SHADOW_LOD } from '../../constants'
import { useCanvasEvents } from '../../hooks/useCanvasEvents'
import { useCoarsePointer } from '../../hooks/useCoarsePointer'
import { useContainer } from '../../hooks/useContainer'
import { useDocumentEvents } from '../../hooks/useDocumentEvents'
import { useEditor } from '../../hooks/useEditor'
import { useEditorComponents } from '../../hooks/useEditorComponents'
@ -36,6 +37,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const rCanvas = useRef<HTMLDivElement>(null)
const rHtmlLayer = useRef<HTMLDivElement>(null)
const rHtmlLayer2 = useRef<HTMLDivElement>(null)
const container = useContainer()
useScreenBounds(rCanvas)
useDocumentEvents()
@ -44,11 +46,37 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
useGestureEvents(rCanvas)
useFixSafariDoubleTapZoomPencilEvents(rCanvas)
const rMemoizedStuff = useRef({ lodDisableTextOutline: false, allowTextOutline: true })
useQuickReactor(
'position layers',
() => {
function positionLayersWhenCameraMoves() {
const { x, y, z } = editor.getCamera()
// This should only run once on first load
if (rMemoizedStuff.current.allowTextOutline && editor.environment.isSafari) {
container.style.setProperty('--tl-text-outline', 'none')
rMemoizedStuff.current.allowTextOutline = false
}
// And this should only run if we're not in Safari;
// If we're below the lod distance for text shadows, turn them off
if (
rMemoizedStuff.current.allowTextOutline &&
z < TEXT_SHADOW_LOD !== rMemoizedStuff.current.lodDisableTextOutline
) {
const lodDisableTextOutline = z < TEXT_SHADOW_LOD
container.style.setProperty(
'--tl-text-outline',
lodDisableTextOutline
? 'none'
: `0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background),
var(--b) var(--b) 0 var(--color-background), var(--a) var(--b) 0 var(--color-background),
var(--a) var(--a) 0 var(--color-background), var(--b) var(--a) 0 var(--color-background)`
)
rMemoizedStuff.current.lodDisableTextOutline = lodDisableTextOutline
}
// Because the html container has a width/height of 1px, we
// need to create a small offset when zoomed to ensure that
// the html container and svg container are lined up exactly.
@ -61,7 +89,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
setStyleProperty(rHtmlLayer.current, 'transform', transform)
setStyleProperty(rHtmlLayer2.current, 'transform', transform)
},
[editor]
[editor, container]
)
const events = useCanvasEvents()
@ -105,9 +133,12 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
{SvgDefs && <SvgDefs />}
</defs>
</svg>
{Background && <Background />}
{Background && (
<div className="tl-background__wrapper">
<Background />
</div>
)}
<GridWrapper />
<div ref={rHtmlLayer} className="tl-html-layer tl-shapes" draggable={false}>
<OnTheCanvasWrapper />
<SelectionBackgroundWrapper />
@ -129,6 +160,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
</div>
<InFrontOfTheCanvasWrapper />
</div>
<MovingCameraHitTestBlocker />
</div>
)
}
@ -350,6 +382,30 @@ function ShapesWithSVGs() {
</>
)
}
function ReflowIfNeeded() {
const editor = useEditor()
const culledShapesRef = useRef<Set<TLShapeId>>(new Set())
useQuickReactor(
'reflow for culled shapes',
() => {
const culledShapes = editor.getCulledShapes()
if (
culledShapesRef.current.size === culledShapes.size &&
[...culledShapes].every((id) => culledShapesRef.current.has(id))
)
return
culledShapesRef.current = culledShapes
const canvas = document.getElementsByClassName('tl-canvas')
if (canvas.length === 0) return
// This causes a reflow
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a
const _height = (canvas[0] as HTMLDivElement).offsetHeight
},
[editor]
)
return null
}
function ShapesToDisplay() {
const editor = useEditor()
@ -370,6 +426,7 @@ function ShapesToDisplay() {
{renderingShapes.map((result) => (
<Shape key={result.id + '_shape'} {...result} dprMultiple={dprMultiple} />
))}
{editor.environment.isSafari && <ReflowIfNeeded />}
</>
)
}
@ -581,3 +638,16 @@ function InFrontOfTheCanvasWrapper() {
if (!InFrontOfTheCanvas) return null
return <InFrontOfTheCanvas />
}
function MovingCameraHitTestBlocker() {
const editor = useEditor()
const cameraState = useValue('camera state', () => editor.getCameraState(), [editor])
return (
<div
className={classNames('tl-hit-test-blocker', {
'tl-hit-test-blocker__hidden': cameraState === 'idle',
})}
/>
)
}

Wyświetl plik

@ -34,10 +34,10 @@ export const DOUBLE_CLICK_DURATION = 450
export const MULTI_CLICK_DURATION = 200
/** @internal */
export const COARSE_DRAG_DISTANCE = 6
export const COARSE_DRAG_DISTANCE = 36 // 6 squared
/** @internal */
export const DRAG_DISTANCE = 4
export const DRAG_DISTANCE = 16 // 4 squared
/** @internal */
export const SVG_PADDING = 32
@ -104,3 +104,9 @@ export const COARSE_HANDLE_RADIUS = 20
/** @internal */
export const HANDLE_RADIUS = 12
/** @internal */
export const LONG_PRESS_DURATION = 500
/** @internal */
export const TEXT_SHADOW_LOD = 0.35

Wyświetl plik

@ -78,6 +78,7 @@ import {
FOLLOW_CHASE_ZOOM_UNSNAP,
HIT_TEST_MARGIN,
INTERNAL_POINTER_IDS,
LONG_PRESS_DURATION,
MAX_PAGES,
MAX_SHAPES_PER_PAGE,
MAX_ZOOM,
@ -100,6 +101,7 @@ import { getReorderingShapesChanges } from '../utils/reorderShapes'
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
import { uniqueId } from '../utils/uniqueId'
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
import { notVisibleShapes } from './derivations/notVisibleShapes'
import { parentsToChildren } from './derivations/parentsToChildren'
import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage'
import { getSvgJsx } from './getSvgJsx'
@ -3071,50 +3073,26 @@ export class Editor extends EventEmitter<TLEventMap> {
index: number
backgroundIndex: number
opacity: number
isCulled: boolean
maskedPageBounds: Box | undefined
}[] = []
let nextIndex = MAX_SHAPES_PER_PAGE * 2
let nextBackgroundIndex = MAX_SHAPES_PER_PAGE
// We only really need these if we're using editor state, but that's ok
const editingShapeId = this.getEditingShapeId()
const selectedShapeIds = this.getSelectedShapeIds()
const erasingShapeIds = this.getErasingShapeIds()
const renderingBoundsExpanded = this.getRenderingBoundsExpanded()
// If renderingBoundsMargin is set to Infinity, then we won't cull offscreen shapes
const isCullingOffScreenShapes = Number.isFinite(this.renderingBoundsMargin)
const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => {
const shape = this.getShape(id)
if (!shape) return
opacity *= shape.opacity
let isCulled = false
let isShapeErasing = false
const util = this.getShapeUtil(shape)
const maskedPageBounds = this.getShapeMaskedPageBounds(id)
if (useEditorState) {
isShapeErasing = !isAncestorErasing && erasingShapeIds.includes(id)
if (isShapeErasing) {
opacity *= 0.32
}
isCulled =
isCullingOffScreenShapes &&
// only cull shapes that allow unmounting, i.e. not stateful components
util.canUnmount(shape) &&
// never cull editingg shapes
editingShapeId !== id &&
// if the shape is fully outside of its parent's clipping bounds...
(maskedPageBounds === undefined ||
// ...or if the shape is outside of the expanded viewport bounds...
(!renderingBoundsExpanded.includes(maskedPageBounds) &&
// ...and if it's not selected... then cull it
!selectedShapeIds.includes(id)))
}
renderingShapes.push({
@ -3124,8 +3102,6 @@ export class Editor extends EventEmitter<TLEventMap> {
index: nextIndex,
backgroundIndex: nextBackgroundIndex,
opacity,
isCulled,
maskedPageBounds,
})
nextIndex += 1
@ -3195,19 +3171,6 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */
private readonly _renderingBounds = atom('rendering viewport', new Box())
/**
* The current rendering bounds in the current page space, expanded slightly. Used for determining which shapes
* to render and which to "cull".
*
* @public
*/
getRenderingBoundsExpanded() {
return this._renderingBoundsExpanded.get()
}
/** @internal */
private readonly _renderingBoundsExpanded = atom('rendering viewport expanded', new Box())
/**
* Update the rendering bounds. This should be called when the viewport has stopped changing, such
* as at the end of a pan, zoom, or animation.
@ -3225,13 +3188,6 @@ export class Editor extends EventEmitter<TLEventMap> {
if (viewportPageBounds.equals(this._renderingBounds.__unsafe__getWithoutCapture())) return this
this._renderingBounds.set(viewportPageBounds.clone())
if (Number.isFinite(this.renderingBoundsMargin)) {
this._renderingBoundsExpanded.set(
viewportPageBounds.clone().expandBy(this.renderingBoundsMargin / this.getZoomLevel())
)
} else {
this._renderingBoundsExpanded.set(viewportPageBounds)
}
return this
}
@ -3272,7 +3228,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getCurrentPageId(): TLPageId {
@computed getCurrentPageId(): TLPageId {
return this.getInstanceState().currentPageId
}
@ -4029,6 +3985,33 @@ export class Editor extends EventEmitter<TLEventMap> {
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
}
@computed
private _notVisibleShapes() {
return notVisibleShapes(this)
}
/**
* Get culled shapes.
*
* @public
*/
@computed
getCulledShapes() {
const notVisibleShapes = this._notVisibleShapes().get()
const selectedShapeIds = this.getSelectedShapeIds()
const editingId = this.getEditingShapeId()
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
// we don't cull the shape we are editing
if (editingId) {
culledShapes.delete(editingId)
}
// we also don't cull selected shapes
selectedShapeIds.forEach((id) => {
culledShapes.delete(id)
})
return culledShapes
}
/**
* The bounds of the current page (the common bounds of all of the shapes on the page).
*
@ -4111,7 +4094,6 @@ export class Editor extends EventEmitter<TLEventMap> {
if (filter) return filter(shape)
return true
})
for (let i = shapesToCheck.length - 1; i >= 0; i--) {
const shape = shapesToCheck[i]
const geometry = this.getShapeGeometry(shape)
@ -4400,10 +4382,8 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
@computed getCurrentPageRenderingShapesSorted(): TLShape[] {
return this.getRenderingShapes()
.filter(({ isCulled }) => !isCulled)
.sort((a, b) => a.index - b.index)
.map(({ shape }) => shape)
const culledShapes = this.getCulledShapes()
return this.getCurrentPageShapesSorted().filter(({ id }) => !culledShapes.has(id))
}
/**
@ -7987,6 +7967,9 @@ export class Editor extends EventEmitter<TLEventMap> {
/** @internal */
private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
/** @internal */
private _longPressTimeout = -1 as any
/** @internal */
capturedPointerId: number | null = null
@ -8004,7 +7987,13 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
dispatch = (info: TLEventInfo): this => {
this._pendingEventsForNextTick.push(info)
if (!(info.type === 'pointer' || info.type === 'wheel' || info.type === 'pinch')) {
if (
!(
(info.type === 'pointer' && info.name === 'pointer_move') ||
info.type === 'wheel' ||
info.type === 'pinch'
)
) {
this._flushEventsForTick(0)
}
return this
@ -8023,8 +8012,8 @@ export class Editor extends EventEmitter<TLEventMap> {
}
if (elapsed > 0) {
this.root.handleEvent({ type: 'misc', name: 'tick', elapsed })
this.scribbles.tick(elapsed)
}
this.scribbles.tick(elapsed)
})
}
@ -8084,6 +8073,7 @@ export class Editor extends EventEmitter<TLEventMap> {
switch (type) {
case 'pinch': {
if (!this.getInstanceState().canMoveCamera) return
clearTimeout(this._longPressTimeout)
this._updateInputsFromEvent(info)
switch (info.name) {
@ -8204,10 +8194,11 @@ export class Editor extends EventEmitter<TLEventMap> {
if (
!inputs.isDragging &&
inputs.isPointing &&
originPagePoint.dist(currentPagePoint) >
Vec.Dist2(originPagePoint, currentPagePoint) >
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
this.getZoomLevel()
) {
clearTimeout(this._longPressTimeout)
inputs.isDragging = true
}
}
@ -8225,6 +8216,10 @@ export class Editor extends EventEmitter<TLEventMap> {
case 'pointer_down': {
this.clearOpenMenus()
this._longPressTimeout = setTimeout(() => {
this.dispatch({ ...info, name: 'long_press' })
}, LONG_PRESS_DURATION)
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
// Firefox bug fix...
@ -8289,10 +8284,11 @@ export class Editor extends EventEmitter<TLEventMap> {
if (
!inputs.isDragging &&
inputs.isPointing &&
originPagePoint.dist(currentPagePoint) >
Vec.Dist2(originPagePoint, currentPagePoint) >
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
this.getZoomLevel()
) {
clearTimeout(this._longPressTimeout)
inputs.isDragging = true
}
break
@ -8435,6 +8431,8 @@ export class Editor extends EventEmitter<TLEventMap> {
break
}
case 'pointer_up': {
clearTimeout(this._longPressTimeout)
const otherEvent = this._clickManager.transformPointerUpEvent(info)
if (info.name !== otherEvent.name) {
this.root.handleEvent(info)

Wyświetl plik

@ -0,0 +1,105 @@
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
import { Box } from '../../primitives/Box'
import { Editor } from '../Editor'
function isShapeNotVisible(editor: Editor, id: TLShapeId, viewportPageBounds: Box): boolean {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
// if the shape is fully outside of its parent's clipping bounds...
if (maskedPageBounds === undefined) return true
// if the shape is fully outside of the viewport page bounds...
return !viewportPageBounds.includes(maskedPageBounds)
}
/**
* Incremental derivation of not visible shapes.
* Non visible shapes are shapes outside of the viewport page bounds and shapes outside of parent's clipping bounds.
*
* @param editor - Instance of the tldraw Editor.
* @returns Incremental derivation of non visible shapes.
*/
export const notVisibleShapes = (editor: Editor) => {
const isCullingOffScreenShapes = Number.isFinite(editor.renderingBoundsMargin)
const shapeHistory = editor.store.query.filterHistory('shape')
let lastPageId: TLPageId | null = null
let prevViewportPageBounds: Box
function fromScratch(editor: Editor): Set<TLShapeId> {
const shapes = editor.getCurrentPageShapeIds()
lastPageId = editor.getCurrentPageId()
const viewportPageBounds = editor.getViewportPageBounds()
prevViewportPageBounds = viewportPageBounds.clone()
const notVisibleShapes = new Set<TLShapeId>()
shapes.forEach((id) => {
if (isShapeNotVisible(editor, id, viewportPageBounds)) {
notVisibleShapes.add(id)
}
})
return notVisibleShapes
}
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue, lastComputedEpoch) => {
if (!isCullingOffScreenShapes) return new Set<TLShapeId>()
if (isUninitialized(prevValue)) {
return fromScratch(editor)
}
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
if (diff === RESET_VALUE) {
return fromScratch(editor)
}
const currentPageId = editor.getCurrentPageId()
if (lastPageId !== currentPageId) {
return fromScratch(editor)
}
const viewportPageBounds = editor.getViewportPageBounds()
if (!prevViewportPageBounds || !viewportPageBounds.equals(prevViewportPageBounds)) {
return fromScratch(editor)
}
let nextValue = null as null | Set<TLShapeId>
const addId = (id: TLShapeId) => {
// Already added
if (prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.add(id)
}
const deleteId = (id: TLShapeId) => {
// No need to delete since it's not there
if (!prevValue.has(id)) return
if (!nextValue) nextValue = new Set(prevValue)
nextValue.delete(id)
}
for (const changes of diff) {
for (const record of Object.values(changes.added)) {
if (isShape(record)) {
const isCulled = isShapeNotVisible(editor, record.id, viewportPageBounds)
if (isCulled) {
addId(record.id)
}
}
}
for (const [_from, to] of Object.values(changes.updated)) {
if (isShape(to)) {
const isCulled = isShapeNotVisible(editor, to.id, viewportPageBounds)
if (isCulled) {
addId(to.id)
} else {
deleteId(to.id)
}
}
}
for (const id of Object.keys(changes.removed)) {
if (isShapeId(id)) {
deleteId(id)
}
}
}
return nextValue ?? prevValue
})
}

Wyświetl plik

@ -38,7 +38,8 @@ export async function getSvgJsx(
if (opts.bounds) {
bbox = opts.bounds
} else {
for (const { maskedPageBounds } of renderingShapes) {
for (const { id } of renderingShapes) {
const maskedPageBounds = editor.getShapeMaskedPageBounds(id)
if (!maskedPageBounds) continue
if (bbox) {
bbox.union(maskedPageBounds)

Wyświetl plik

@ -227,7 +227,7 @@ export class ClickManager {
if (
this._clickState !== 'idle' &&
this._clickScreenPoint &&
this._clickScreenPoint.dist(this.editor.inputs.currentScreenPoint) >
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
) {
this.cancelDoubleClickTimeout()

Wyświetl plik

@ -89,13 +89,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/
canScroll: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape should unmount when not visible in the editor. Consider keeping this to false if the shape's `component` has local state.
*
* @public
*/
canUnmount: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape can be bound to by an arrow.
*

Wyświetl plik

@ -198,6 +198,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
onWheel?: TLEventHandlers['onWheel']
onPointerDown?: TLEventHandlers['onPointerDown']
onPointerMove?: TLEventHandlers['onPointerMove']
onLongPress?: TLEventHandlers['onLongPress']
onPointerUp?: TLEventHandlers['onPointerUp']
onDoubleClick?: TLEventHandlers['onDoubleClick']
onTripleClick?: TLEventHandlers['onTripleClick']

Wyświetl plik

@ -16,6 +16,7 @@ export type TLPointerEventTarget =
export type TLPointerEventName =
| 'pointer_down'
| 'pointer_move'
| 'long_press'
| 'pointer_up'
| 'right_click'
| 'middle_click'
@ -152,6 +153,7 @@ export type TLExitEventHandler = (info: any, to: string) => void
export interface TLEventHandlers {
onPointerDown: TLPointerEvent
onPointerMove: TLPointerEvent
onLongPress: TLPointerEvent
onRightClick: TLPointerEvent
onDoubleClick: TLClickEvent
onTripleClick: TLClickEvent
@ -176,6 +178,7 @@ export const EVENT_NAME_MAP: Record<
wheel: 'onWheel',
pointer_down: 'onPointerDown',
pointer_move: 'onPointerMove',
long_press: 'onLongPress',
pointer_up: 'onPointerUp',
right_click: 'onRightClick',
middle_click: 'onMiddleClick',

Wyświetl plik

@ -308,19 +308,20 @@ export class Vec {
static Per(A: VecLike): Vec {
return new Vec(A.y, -A.x)
}
static Dist2(A: VecLike, B: VecLike): number {
return Vec.Sub(A, B).len2()
}
static Abs(A: VecLike): Vec {
return new Vec(Math.abs(A.x), Math.abs(A.y))
}
// Get the distance between two points.
static Dist(A: VecLike, B: VecLike): number {
return Math.hypot(A.y - B.y, A.x - B.x)
}
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
static Dist2(A: VecLike, B: VecLike): number {
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)
}
/**
* Dot product of two vectors which is used to calculate the angle between them.
*/

Wyświetl plik

@ -52,15 +52,16 @@ export class Arc2d extends Geometry2d {
// Get the point (P) on the arc, then pick the nearest of A, B, and P
const P = _center.clone().add(point.clone().sub(_center).uni().mul(radius))
let distance = Infinity
let nearest: Vec | undefined
for (const pt of [A, B, P]) {
if (point.dist(pt) < distance) {
nearest = pt
distance = point.dist(pt)
let dist = Infinity
let d: number
for (const p of [A, B, P]) {
d = Vec.Dist2(point, p)
if (d < dist) {
nearest = p
dist = d
}
}
if (!nearest) throw Error('nearest point not found')
return nearest
}

Wyświetl plik

@ -55,9 +55,11 @@ export class CubicBezier2d extends Polyline2d {
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
let d: number
let p: Vec
for (const edge of this.segments) {
const p = edge.nearestPoint(A)
const d = p.dist(A)
p = edge.nearestPoint(A)
d = Vec.Dist2(p, A)
if (d < dist) {
nearest = p
dist = d

Wyświetl plik

@ -67,15 +67,16 @@ export class CubicSpline2d extends Geometry2d {
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
let d: number
let p: Vec
for (const segment of this.segments) {
const p = segment.nearestPoint(A)
const d = p.dist(A)
p = segment.nearestPoint(A)
d = Vec.Dist2(p, A)
if (d < dist) {
nearest = p
dist = d
}
}
if (!nearest) throw Error('nearest point not found')
return nearest
}

Wyświetl plik

@ -76,15 +76,16 @@ export class Ellipse2d extends Geometry2d {
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
let d: number
let p: Vec
for (const edge of this.edges) {
const p = edge.nearestPoint(A)
const d = p.dist(A)
p = edge.nearestPoint(A)
d = Vec.Dist2(p, A)
if (d < dist) {
nearest = p
dist = d
}
}
if (!nearest) throw Error('nearest point not found')
return nearest
}

Wyświetl plik

@ -55,14 +55,17 @@ export abstract class Geometry2d {
}
nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
let distance = Infinity
const { vertices } = this
let nearest: Vec | undefined
for (let i = 0; i < this.vertices.length; i++) {
const point = this.vertices[i]
const d = Vec.DistanceToLineSegment(A, B, point)
if (d < distance) {
distance = d
nearest = point
let dist = Infinity
let d: number
let p: Vec
for (let i = 0; i < vertices.length; i++) {
p = vertices[i]
d = Vec.DistanceToLineSegment(A, B, p)
if (d < dist) {
dist = d
nearest = p
}
}
if (!nearest) throw Error('nearest point not found')

Wyświetl plik

@ -30,8 +30,8 @@ export class Group2d extends Geometry2d {
}
override nearestPoint(point: Vec): Vec {
let d = Infinity
let p: Vec | undefined
let dist = Infinity
let nearest: Vec | undefined
const { children } = this
@ -39,16 +39,18 @@ export class Group2d extends Geometry2d {
throw Error('no children')
}
let p: Vec
let d: number
for (const child of children) {
const nearest = child.nearestPoint(point)
const dist = nearest.dist(point)
if (dist < d) {
d = dist
p = nearest
p = child.nearestPoint(point)
d = Vec.Dist2(p, point)
if (d < dist) {
dist = d
nearest = p
}
}
if (!p) throw Error('nearest point not found')
return p
if (!nearest) throw Error('nearest point not found')
return nearest
}
override distanceToPoint(point: Vec, hitInside = false) {

Wyświetl plik

@ -51,18 +51,17 @@ export class Polyline2d extends Geometry2d {
const { segments } = this
let nearest = this.points[0]
let dist = Infinity
let p: Vec // current point on segment
let d: number // distance from A to p
for (let i = 0; i < segments.length; i++) {
p = segments[i].nearestPoint(A)
d = p.dist(A)
d = Vec.Dist2(p, A)
if (d < dist) {
nearest = p
dist = d
}
}
if (!nearest) throw Error('nearest point not found')
return nearest
}

Wyświetl plik

@ -4201,7 +4201,7 @@
{
"kind": "Property",
"canonicalReference": "@tldraw/store!Store#onBeforeChange:member",
"docComment": "/**\n * A callback before after each record's change.\n *\n * @param prev - The previous value, if any.\n *\n * @param next - The next value.\n */\n",
"docComment": "/**\n * A callback fired before each record's change.\n *\n * @param prev - The previous value, if any.\n *\n * @param next - The next value.\n */\n",
"excerptTokens": [
{
"kind": "Content",

Wyświetl plik

@ -311,7 +311,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
onAfterCreate?: (record: R, source: 'remote' | 'user') => void
/**
* A callback before after each record's change.
* A callback fired before each record's change.
*
* @param prev - The previous value, if any.
* @param next - The next value.

Wyświetl plik

@ -15,6 +15,8 @@ import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from '@tldraw/utils'
* @public
*/
export function devFreeze<T>(object: T): T {
return object
if (process.env.NODE_ENV === 'production') {
return object
}

Wyświetl plik

@ -536,8 +536,6 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented)
canResize: (shape: TLEmbedShape) => boolean;
// (undocumented)
canUnmount: TLShapeUtilFlag<TLEmbedShape>;
// (undocumented)
component(shape: TLEmbedShape): JSX_2.Element;
// (undocumented)
getDefaultProps(): TLEmbedShape['props'];

Wyświetl plik

@ -5705,50 +5705,6 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "tldraw!EmbedShapeUtil#canUnmount:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "canUnmount: "
},
{
"kind": "Reference",
"text": "TLShapeUtilFlag",
"canonicalReference": "@tldraw/editor!TLShapeUtilFlag:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLEmbedShape",
"canonicalReference": "@tldraw/tlschema!TLEmbedShape:type"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "canUnmount",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Method",
"canonicalReference": "tldraw!EmbedShapeUtil#component:member(1)",

Wyświetl plik

@ -309,7 +309,7 @@ export class Drawing extends StateNode {
}
const hasMovedFarEnough =
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
// Find the distance from where the pointer was when shift was released and
// where it is now; if it's far enough away, then update the page point where
@ -380,7 +380,7 @@ export class Drawing extends StateNode {
}
const hasMovedFarEnough =
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
// Find the distance from where the pointer was when shift was released and
// where it is now; if it's far enough away, then update the page point where

Wyświetl plik

@ -34,9 +34,6 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
override hideSelectionBoundsFg: TLShapeUtilFlag<TLEmbedShape> = (shape) => !this.canResize(shape)
override canEdit: TLShapeUtilFlag<TLEmbedShape> = () => true
override canUnmount: TLShapeUtilFlag<TLEmbedShape> = (shape: TLEmbedShape) => {
return !!getEmbedInfo(shape.props.url)?.definition?.canUnmount
}
override canResize = (shape: TLEmbedShape) => {
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
}

Wyświetl plik

@ -179,6 +179,11 @@ function usePattern() {
const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(defaultPatterns)
useEffect(() => {
if (process.env.NODE_ENV === 'test') {
setIsReady(true)
return
}
const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = []
for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) {

Wyświetl plik

@ -4,6 +4,7 @@ import {
TLEventHandlers,
TLFrameShape,
TLGroupShape,
TLPointerEventInfo,
TLShapeId,
} from '@tldraw/editor'
@ -52,9 +53,13 @@ export class Pointing extends StateNode {
this.editor.setErasingShapes([...erasing])
}
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
this.startErasing(info)
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
this.parent.transition('erasing', info)
this.startErasing(info)
}
}
@ -74,6 +79,10 @@ export class Pointing extends StateNode {
this.cancel()
}
private startErasing(info: TLPointerEventInfo) {
this.parent.transition('erasing', info)
}
complete() {
const erasingShapeIds = this.editor.getErasingShapeIds()

Wyświetl plik

@ -3,7 +3,10 @@ import { CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec } from '@tldraw/
export class Dragging extends StateNode {
static override id = 'dragging'
initialCamera = new Vec()
override onEnter = () => {
this.initialCamera = Vec.From(this.editor.getCamera())
this.update()
}
@ -16,7 +19,7 @@ export class Dragging extends StateNode {
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.complete()
this.parent.transition('idle')
}
override onComplete = () => {
@ -24,21 +27,27 @@ export class Dragging extends StateNode {
}
private update() {
const { currentScreenPoint, previousScreenPoint } = this.editor.inputs
const { initialCamera, editor } = this
const { currentScreenPoint, originScreenPoint } = editor.inputs
const delta = Vec.Sub(currentScreenPoint, previousScreenPoint)
if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) {
this.editor.pan(delta)
}
const delta = Vec.Sub(currentScreenPoint, originScreenPoint).div(editor.getZoomLevel())
if (delta.len2() === 0) return
editor.setCamera(initialCamera.clone().add(delta))
}
private complete() {
this.editor.slideCamera({
speed: Math.min(2, this.editor.inputs.pointerVelocity.len()),
direction: this.editor.inputs.pointerVelocity,
friction: CAMERA_SLIDE_FRICTION,
})
const { editor } = this
const { pointerVelocity } = editor.inputs
const velocityAtPointerUp = Math.min(pointerVelocity.len(), 2)
if (velocityAtPointerUp > 0.1) {
this.editor.slideCamera({
speed: velocityAtPointerUp,
direction: pointerVelocity,
friction: CAMERA_SLIDE_FRICTION,
})
}
this.parent.transition('idle')
}

Wyświetl plik

@ -8,12 +8,20 @@ export class Pointing extends StateNode {
this.editor.setCursor({ type: 'grabbing', rotation: 0 })
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
override onLongPress: TLEventHandlers['onLongPress'] = () => {
this.startDragging()
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
if (this.editor.inputs.isDragging) {
this.parent.transition('dragging', info)
this.startDragging()
}
}
private startDragging() {
this.parent.transition('dragging')
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.complete()
}

Wyświetl plik

@ -29,16 +29,23 @@ export class PointingCropHandle extends StateNode {
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
const isDragging = this.editor.inputs.isDragging
if (isDragging) {
this.parent.transition('cropping', {
...this.info,
onInteractionEnd: this.info.onInteractionEnd,
})
if (this.editor.inputs.isDragging) {
this.startCropping()
}
}
override onLongPress: TLEventHandlers['onLongPress'] = () => {
this.startCropping()
}
private startCropping() {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('cropping', {
...this.info,
onInteractionEnd: this.info.onInteractionEnd,
})
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
if (this.info.onInteractionEnd) {
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)

Wyświetl plik

@ -31,10 +31,19 @@ export class PointingHandle extends StateNode {
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
if (this.editor.inputs.isDragging) {
this.parent.transition('dragging_handle', this.info)
this.startDraggingHandle()
}
}
override onLongPress: TLEventHandlers['onLongPress'] = () => {
this.startDraggingHandle()
}
private startDraggingHandle() {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('dragging_handle', this.info)
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.cancel()
}

Wyświetl plik

@ -46,13 +46,20 @@ export class PointingResizeHandle extends StateNode {
}
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
const isDragging = this.editor.inputs.isDragging
if (isDragging) {
this.parent.transition('resizing', this.info)
if (this.editor.inputs.isDragging) {
this.startResizing()
}
}
override onLongPress: TLEventHandlers['onLongPress'] = () => {
this.startResizing()
}
private startResizing() {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('resizing', this.info)
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.complete()
}

Wyświetl plik

@ -28,14 +28,21 @@ export class PointingRotateHandle extends StateNode {
this.editor.setCursor({ type: 'default', rotation: 0 })
}
override onPointerMove = () => {
const { isDragging } = this.editor.inputs
if (isDragging) {
this.parent.transition('rotating', this.info)
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
if (this.editor.inputs.isDragging) {
this.startRotating()
}
}
override onLongPress: TLEventHandlers['onLongPress'] = () => {
this.startRotating()
}
private startRotating() {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('rotating', this.info)
}
override onPointerUp = () => {
this.complete()
}

Wyświetl plik

@ -25,11 +25,19 @@ export class PointingSelection extends StateNode {
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('translating', info)
this.startTranslating(info)
}
}
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
this.startTranslating(info)
}
private startTranslating(info: TLPointerEventInfo) {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('translating', info)
}
override onDoubleClick?: TLClickEvent | undefined = (info) => {
const hoveredShape = this.editor.getHoveredShape()
const hitShape =

Wyświetl plik

@ -195,11 +195,19 @@ export class PointingShape extends StateNode {
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('translating', info)
this.startTranslating(info)
}
}
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
this.startTranslating(info)
}
private startTranslating(info: TLPointerEventInfo) {
if (this.editor.getInstanceState().isReadonly) return
this.parent.transition('translating', info)
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.cancel()
}

Wyświetl plik

@ -42,9 +42,7 @@ export class ScribbleBrushing extends StateNode {
this.updateScribbleSelection(true)
requestAnimationFrame(() => {
this.editor.updateInstanceState({ brush: null })
})
this.editor.updateInstanceState({ brush: null })
}
override onExit = () => {

Wyświetl plik

@ -1,6 +1,6 @@
import { Editor, HIT_TEST_MARGIN, TLShape } from '@tldraw/editor'
import { Editor, HIT_TEST_MARGIN, TLShape, throttle } from '@tldraw/editor'
export function updateHoveredId(editor: Editor) {
function _updateHoveredId(editor: Editor) {
// todo: consider replacing `get hoveredShapeId` with this; it would mean keeping hoveredShapeId in memory rather than in the store and possibly re-computing it more often than necessary
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint, {
hitInside: false,
@ -30,3 +30,6 @@ export function updateHoveredId(editor: Editor) {
return editor.setHoveredShape(shapeToHover.id)
}
export const updateHoveredId =
process.env.NODE_ENV === 'test' ? _updateHoveredId : throttle(_updateHoveredId, 32)

Wyświetl plik

@ -17,13 +17,12 @@ export function BackToContent() {
const renderingShapes = editor.getRenderingShapes()
const renderingBounds = editor.getRenderingBounds()
// renderingShapes will also include shapes that have the canUnmount flag
// set to true. These shapes will be on the canvas but may not be in the
// viewport... so we also need to narrow down the list to only shapes that
// are ALSO in the viewport.
const visibleShapes = renderingShapes.filter(
(s) => s.maskedPageBounds && renderingBounds.includes(s.maskedPageBounds)
)
// Rendering shapes includes all the shapes in the current page.
// We have to filter them down to just the shapes that are inside the renderingBounds.
const visibleShapes = renderingShapes.filter((s) => {
const maskedPageBounds = editor.getShapeMaskedPageBounds(s.id)
return maskedPageBounds && renderingBounds.includes(maskedPageBounds)
})
const showBackToContentNow =
visibleShapes.length === 0 && editor.getCurrentPageShapes().length > 0

Wyświetl plik

@ -189,7 +189,7 @@ export class MinimapManager {
this
const { width: cw, height: ch } = canvasScreenBounds
const selectedShapeIds = editor.getSelectedShapeIds()
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
const viewportPageBounds = editor.getViewportPageBounds()
if (!cvs || !pageBounds) {
@ -215,16 +215,6 @@ export class MinimapManager {
ctx.scale(sx, sy)
ctx.translate(-contentPageBounds.minX, -contentPageBounds.minY)
// Default radius for rounded rects
const rx = 8 / sx
const ry = 8 / sx
// Min radius
const ax = 1 / sx
const ay = 1 / sx
// Max radius factor
const bx = rx / 4
const by = ry / 4
// shapes
const shapesPath = new Path2D()
const selectedPath = new Path2D()
@ -237,14 +227,11 @@ export class MinimapManager {
let pb: Box & { id: TLShapeId }
for (let i = 0, n = pageBounds.length; i < n; i++) {
pb = pageBounds[i]
MinimapManager.roundedRect(
selectedShapeIds.includes(pb.id) ? selectedPath : shapesPath,
;(selectedShapeIds.has(pb.id) ? selectedPath : shapesPath).rect(
pb.minX,
pb.minY,
pb.width,
pb.height,
clamp(rx, ax, pb.width / bx),
clamp(ry, ay, pb.height / by)
pb.height
)
}

Wyświetl plik

@ -0,0 +1,138 @@
import { Box, TLShapeId, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
import { TL } from './test-jsx'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
editor.renderingBoundsMargin = 100
})
function createShapes() {
return editor.createShapesFromJsx([
<TL.geo ref="A" x={100} y={100} w={100} h={100} />,
<TL.frame ref="B" x={200} y={200} w={300} h={300}>
<TL.geo ref="C" x={200} y={200} w={50} h={50} />
{/* this is outside of the frames clipping bounds, so it should never be rendered */}
<TL.geo ref="D" x={1000} y={1000} w={50} h={50} />
</TL.frame>,
])
}
it('lists shapes in viewport', () => {
const ids = createShapes()
editor.selectNone()
// D is clipped and so should always be culled / outside of viewport
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.D]))
// Move the camera 201 pixels to the right and 201 pixels down
editor.pan({ x: -201, y: -201 })
jest.advanceTimersByTime(500)
// A is now outside of the viewport
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.D]))
editor.pan({ x: -900, y: -900 })
jest.advanceTimersByTime(500)
// Now all shapes are outside of the viewport
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.B, ids.C, ids.D]))
editor.select(ids.B)
// We don't cull selected shapes
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.C, ids.D]))
editor.setEditingShape(ids.C)
// or shapes being edited
expect(editor.getCulledShapes()).toStrictEqual(new Set([ids.A, ids.D]))
})
const shapeSize = 100
const numberOfShapes = 100
function getChangeOutsideBounds(viewportSize: number) {
const changeDirection = Math.random() > 0.5 ? 1 : -1
const maxChange = 1000
const changeAmount = 1 + Math.random() * maxChange
if (changeDirection === 1) {
// We need to get past the viewport size and then add a bit more
return viewportSize + changeAmount
} else {
// We also need to take the shape size into account
return -changeAmount - shapeSize
}
}
function getChangeInsideBounds(viewportSize: number) {
// We can go from -shapeSize to viewportSize
return -shapeSize + Math.random() * (viewportSize + shapeSize)
}
function createFuzzShape(viewport: Box) {
const id = createShapeId()
if (Math.random() > 0.5) {
const positionChange = Math.random()
// Should x, or y, or both go outside the bounds?
const dimensionChange = positionChange < 0.33 ? 'x' : positionChange < 0.66 ? 'y' : 'both'
const xOutsideBounds = dimensionChange === 'x' || dimensionChange === 'both'
const yOutsideBounds = dimensionChange === 'y' || dimensionChange === 'both'
// Create a shape outside the viewport
editor.createShape({
id,
type: 'geo',
x:
viewport.x +
(xOutsideBounds ? getChangeOutsideBounds(viewport.w) : getChangeInsideBounds(viewport.w)),
y:
viewport.y +
(yOutsideBounds ? getChangeOutsideBounds(viewport.h) : getChangeInsideBounds(viewport.h)),
props: { w: shapeSize, h: shapeSize },
})
return { isCulled: true, id }
} else {
// Create a shape inside the viewport
editor.createShape({
id,
type: 'geo',
x: viewport.x + getChangeInsideBounds(viewport.w),
y: viewport.y + getChangeInsideBounds(viewport.h),
props: { w: shapeSize, h: shapeSize },
})
return { isCulled: false, id }
}
}
it('correctly calculates the culled shapes when adding and deleting shapes', () => {
const viewport = editor.getViewportPageBounds()
const shapes: Array<TLShapeId | undefined> = []
for (let i = 0; i < numberOfShapes; i++) {
const { isCulled, id } = createFuzzShape(viewport)
shapes.push(id)
if (isCulled) {
expect(editor.getCulledShapes()).toContain(id)
} else {
expect(editor.getCulledShapes()).not.toContain(id)
}
}
const numberOfShapesToDelete = Math.floor((Math.random() * numberOfShapes) / 2)
for (let i = 0; i < numberOfShapesToDelete; i++) {
const index = Math.floor(Math.random() * (shapes.length - 1))
const id = shapes[index]
if (id) {
editor.deleteShape(id)
shapes[index] = undefined
expect(editor.getCulledShapes()).not.toContain(id)
}
}
const culledShapesIncremental = editor.getCulledShapes()
// force full refresh
editor.pan({ x: -1, y: 0 })
editor.pan({ x: 1, y: 0 })
const culledShapeFromScratch = editor.getCulledShapes()
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
})

Wyświetl plik

@ -60,47 +60,6 @@ it('updates the rendering viewport when the camera stops moving', () => {
expect(editor.getShapePageBounds(ids.A)).toMatchObject({ x: 100, y: 100, w: 100, h: 100 })
})
it('lists shapes in viewport', () => {
const ids = createShapes()
editor.selectNone()
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
[ids.A, false], // A is within the expanded rendering bounds, so should not be culled; and it's in the regular viewport too, so it's on screen.
[ids.B, false],
[ids.C, false],
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
])
// Move the camera 201 pixels to the right and 201 pixels down
editor.pan({ x: -201, y: -201 })
jest.advanceTimersByTime(500)
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
[ids.A, false], // A should not be culled, even though it's no longer in the viewport (because it's still in the EXPANDED viewport)
[ids.B, false],
[ids.C, false],
[ids.D, true], // D is clipped and so should always be culled / outside of viewport
])
editor.pan({ x: -100, y: -100 })
jest.advanceTimersByTime(500)
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
[ids.A, true], // A should be culled now that it's outside of the expanded viewport too
[ids.B, false],
[ids.C, false],
[ids.D, true], // D is clipped and so should always be culled, even if it's in the viewport
])
editor.pan({ x: -900, y: -900 })
jest.advanceTimersByTime(500)
expect(editor.getRenderingShapes().map(({ id, isCulled }) => [id, isCulled])).toStrictEqual([
[ids.A, true],
[ids.B, true],
[ids.C, true],
[ids.D, true],
])
})
it('lists shapes in viewport sorted by id with correct indexes & background indexes', () => {
const ids = createShapes()
// Expect the results to be sorted correctly by id

Wyświetl plik

@ -218,7 +218,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly overridePermissions: {
readonly 'allow-top-navigation': true;
};
@ -231,7 +230,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -241,7 +239,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -253,7 +250,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -265,7 +261,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -277,7 +272,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 520;
readonly height: 400;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -287,7 +281,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 520;
readonly height: 400;
readonly doesResize: false;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -297,7 +290,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 800;
readonly height: 450;
readonly doesResize: true;
readonly canUnmount: false;
readonly overridePermissions: {
readonly 'allow-presentation': true;
};
@ -313,7 +305,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly minWidth: 460;
readonly minHeight: 360;
readonly doesResize: true;
readonly canUnmount: false;
readonly instructionLink: "https://support.google.com/calendar/answer/41207?hl=en";
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
@ -326,7 +317,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly minWidth: 460;
readonly minHeight: 360;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -336,7 +326,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -346,7 +335,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -356,7 +344,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -368,7 +355,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly minHeight: 500;
readonly overrideOutlineRadius: 12;
readonly doesResize: true;
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
}, {
@ -378,7 +364,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 640;
readonly height: 360;
readonly doesResize: true;
readonly canUnmount: false;
readonly isAspectRatioLocked: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
@ -389,7 +374,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly isAspectRatioLocked: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
@ -400,7 +384,6 @@ export const EMBED_DEFINITIONS: readonly [{
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: false;
readonly isAspectRatioLocked: false;
readonly backgroundColor: "#fff";
readonly toEmbedUrl: (url: string) => string | undefined;
@ -417,7 +400,6 @@ export type EmbedDefinition = {
readonly width: number;
readonly height: number;
readonly doesResize: boolean;
readonly canUnmount: boolean;
readonly isAspectRatioLocked?: boolean;
readonly overridePermissions?: TLEmbedShapePermissions;
readonly instructionLink?: string;

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -2030,6 +2030,34 @@ describe('Fractional indexing for line points', () => {
})
})
describe('add white', () => {
const { up, down } = rootShapeMigrations.migrators[rootShapeVersions.AddWhite]
test('up works as expected', () => {
expect(
up({
props: {},
})
).toEqual({
props: {},
})
})
test('down works as expected', () => {
expect(
down({
props: {
color: 'white',
},
})
).toEqual({
props: {
color: 'black',
},
})
})
})
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
for (const migrator of allMigrators) {

Wyświetl plik

@ -87,11 +87,12 @@ export const rootShapeVersions = {
AddIsLocked: 1,
HoistOpacity: 2,
AddMeta: 3,
AddWhite: 4,
} as const
/** @internal */
export const rootShapeMigrations = defineMigrations({
currentVersion: rootShapeVersions.AddMeta,
currentVersion: rootShapeVersions.AddWhite,
migrators: {
[rootShapeVersions.AddIsLocked]: {
up: (record) => {
@ -147,6 +148,22 @@ export const rootShapeMigrations = defineMigrations({
}
},
},
[rootShapeVersions.AddWhite]: {
up: (record) => {
return {
...record,
}
},
down: (record) => {
return {
...record,
props: {
...record.props,
color: record.props.color === 'white' ? 'black' : record.props.color,
},
}
},
},
},
})

Wyświetl plik

@ -24,7 +24,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: true,
overridePermissions: {
'allow-top-navigation': true,
},
@ -50,7 +49,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: true,
toEmbedUrl: (url) => {
if (
!!url.match(
@ -81,7 +79,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
if (url.includes('/maps/')) {
const match = url.match(/@(.*),(.*),(.*)z/)
@ -120,7 +117,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
// e.g. extract "steveruizok.mathFact" from https://www.val.town/v/steveruizok.mathFact
@ -149,7 +145,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
const matches = urlObj && urlObj.pathname.match(/\/s\/([^/]+)\/?/)
@ -176,7 +171,6 @@ export const EMBED_DEFINITIONS = [
width: 520,
height: 400,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const CODEPEN_URL_REGEXP = /https:\/\/codepen.io\/([^/]+)\/pen\/([^/]+)/
const matches = url.match(CODEPEN_URL_REGEXP)
@ -203,7 +197,6 @@ export const EMBED_DEFINITIONS = [
width: 520,
height: 400,
doesResize: false,
canUnmount: false,
toEmbedUrl: (url) => {
const SCRATCH_URL_REGEXP = /https?:\/\/scratch.mit.edu\/projects\/([^/]+)/
const matches = url.match(SCRATCH_URL_REGEXP)
@ -230,7 +223,6 @@ export const EMBED_DEFINITIONS = [
width: 800,
height: 450,
doesResize: true,
canUnmount: false,
overridePermissions: {
'allow-presentation': true,
},
@ -275,7 +267,6 @@ export const EMBED_DEFINITIONS = [
minWidth: 460,
minHeight: 360,
doesResize: true,
canUnmount: false,
instructionLink: 'https://support.google.com/calendar/answer/41207?hl=en',
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
@ -318,7 +309,6 @@ export const EMBED_DEFINITIONS = [
minWidth: 460,
minHeight: 360,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
@ -353,7 +343,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: true,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
if (urlObj && urlObj.pathname.match(/\/([^/]+)\/([^/]+)/)) {
@ -378,7 +367,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
if (urlObj && urlObj.pathname.match(/\/@([^/]+)\/([^/]+)/)) {
@ -406,7 +394,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
if (urlObj && urlObj.pathname.match(/^\/map\//)) {
@ -432,7 +419,6 @@ export const EMBED_DEFINITIONS = [
minHeight: 500,
overrideOutlineRadius: 12,
doesResize: true,
canUnmount: false,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
if (urlObj && urlObj.pathname.match(/^\/(artist|album)\//)) {
@ -455,7 +441,6 @@ export const EMBED_DEFINITIONS = [
width: 640,
height: 360,
doesResize: true,
canUnmount: false,
isAspectRatioLocked: true,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
@ -486,7 +471,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
isAspectRatioLocked: true,
toEmbedUrl: (url) => {
const urlObj = safeParseUrl(url)
@ -510,7 +494,6 @@ export const EMBED_DEFINITIONS = [
width: 720,
height: 500,
doesResize: true,
canUnmount: false,
isAspectRatioLocked: false,
backgroundColor: '#fff',
toEmbedUrl: (url) => {
@ -619,7 +602,6 @@ export type EmbedDefinition = {
readonly width: number
readonly height: number
readonly doesResize: boolean
readonly canUnmount: boolean
readonly isAspectRatioLocked?: boolean
readonly overridePermissions?: TLEmbedShapePermissions
readonly instructionLink?: string

Wyświetl plik

@ -176,6 +176,15 @@ export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(
[K in Key]: ValueAfter;
};
// @internal (undocumented)
export function measureAverageDuration(_target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
// @internal (undocumented)
export function measureCbDuration(name: string, cb: () => any): any;
// @internal (undocumented)
export function measureDuration(_target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor;
// @public
export class MediaHelpers {
static getImageSize(blob: Blob): Promise<{

Wyświetl plik

@ -36,6 +36,7 @@ export {
objectMapKeys,
objectMapValues,
} from './lib/object'
export { measureAverageDuration, measureCbDuration, measureDuration } from './lib/perf'
export { PngHelpers } from './lib/png'
export { type IndexKey } from './lib/reordering/IndexKey'
export {

Wyświetl plik

@ -0,0 +1,50 @@
/** @internal */
export function measureCbDuration(name: string, cb: () => any) {
const now = performance.now()
const result = cb()
// eslint-disable-next-line no-console
console.log(`${name} took`, performance.now() - now, 'ms')
return result
}
/** @internal */
export function measureDuration(_target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
const start = performance.now()
const result = originalMethod.apply(this, args)
const end = performance.now()
// eslint-disable-next-line no-console
console.log(`${propertyKey} took ${end - start}ms `)
return result
}
return descriptor
}
const averages = new Map<any, { total: number; count: number }>()
/** @internal */
export function measureAverageDuration(
_target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
const start = performance.now()
const result = originalMethod.apply(this, args)
const end = performance.now()
const value = averages.get(descriptor.value)!
const length = end - start
const total = value.total + length
const count = value.count + 1
averages.set(descriptor.value, { total, count })
// eslint-disable-next-line no-console
console.log(
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
)
return result
}
averages.set(descriptor.value, { total: 0, count: 0 })
return descriptor
}