kopia lustrzana https://github.com/Tldraw/Tldraw
Merge branch 'main' into alex/auto-undo-redo
commit
e5bbaee67b
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
|
@ -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.
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
## 2.0.27
|
||||
|
||||
- Bug fixes and performance improvements.
|
||||
|
||||
## 2.0.26
|
||||
|
||||
- Bug fixes and performance improvements.
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
Ładowanie…
Reference in New Issue