kopia lustrzana https://github.com/Tldraw/Tldraw
Porównaj commity
45 Commity
27c5c1a520
...
64e4f8e16d
Autor | SHA1 | Data |
---|---|---|
Steve Ruiz | 64e4f8e16d | |
Steve Ruiz | cb99199f24 | |
Steve Ruiz | 8565e0ad29 | |
Steve Ruiz | 0e50cd4f36 | |
Steve Ruiz | 849f94851d | |
Steve Ruiz | 10f94eafe9 | |
Steve Ruiz | 7230a90e59 | |
Steve Ruiz | 1d237b1dd6 | |
Steve Ruiz | 04218652b3 | |
Steve Ruiz | e9b13ab9cf | |
Steve Ruiz | 219e31ada3 | |
dependabot[bot] | 4507ce6378 | |
Steve Ruiz | a6f3241c8f | |
Steve Ruiz | a6d2ab05d2 | |
Steve Ruiz | b5fab15c6d | |
David Sheldrick | b5dfd81540 | |
Steve Ruiz | f6a2e352de | |
Steve Ruiz | b356cb1165 | |
Steve Ruiz | afeaae83d5 | |
Steve Ruiz | e929bdd133 | |
Steve Ruiz | caf9ec4ee1 | |
Steve Ruiz | cefe694798 | |
Steve Ruiz | 4a5271ac3d | |
Steve Ruiz | d1b270b4a3 | |
Steve Ruiz | 3389687581 | |
Mitja Bezenšek | 1fc68975e2 | |
Steve Ruiz | 009ecc2b5a | |
Steve Ruiz | e209256084 | |
Steve Ruiz | 6d03c73443 | |
Steve Ruiz | 8aca34fd9a | |
Steve Ruiz | ced1eadd8c | |
Steve Ruiz | 20cbb6d4bf | |
Steve Ruiz | a9d1c921c7 | |
Mitja Bezenšek | 47070ec109 | |
David Sheldrick | 741ed00bda | |
Mitja Bezenšek | dd0b7b882d | |
David Sheldrick | 625f4abc3b | |
Mitja Bezenšek | f70fd2729d | |
Mime Čuvalo | d247b5dc53 | |
Mime Čuvalo | f9bafb2f8a | |
Mime Čuvalo | f754bebc32 | |
Mime Čuvalo | f44ea90da6 | |
Mitja Bezenšek | 0b44a8b47a | |
Mime Čuvalo | 34ad856873 | |
Steve Ruiz | 1450454873 |
|
@ -19,7 +19,7 @@ body:
|
|||
id: reproduction
|
||||
attributes:
|
||||
label: How can we reproduce the bug?
|
||||
description: If you can make the bug happen again, please share the steps involved.
|
||||
description: If you can make the bug happen again, please share the steps involved. You can [fork this CodeSandbox](https://codesandbox.io/p/sandbox/tldraw-example-n539u) to make a reproduction.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
|
|
|
@ -201,7 +201,152 @@ The [Editor#getInstanceState](?) method returns settings that relate to each ind
|
|||
|
||||
The editor's user preferences are shared between all instances. See the [TLUserPreferences](?) docs for more about the user preferences.
|
||||
|
||||
## Common things to do with the editor
|
||||
# Camera and coordinates
|
||||
|
||||
The editor offers many methods and properties relating to the part of the infinite canvas that is displayed in the component. This section includes key concepts and methods that you can use to change or control which parts of the canvas are visible.
|
||||
|
||||
## Viewport
|
||||
|
||||
The viewport is the rectangular area contained by the editor.
|
||||
|
||||
| Method | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Editor#getViewportScreenBounds] | A [Box](?) that describes the size and position of the component's canvas in actual screen pixels. |
|
||||
| [Editor#getViewportPageBounds](?) | A [Box](?) that describes the size and position of the part of the current page that is displayed in the viewport. |
|
||||
|
||||
## Screen vs. page coordinates
|
||||
|
||||
In tldraw, coordinates are either be in page or screen space.
|
||||
|
||||
A "screen point" refers to the point's distance from the top left corner of the component.
|
||||
|
||||
A "page point" refers to the point's distance from the "zero point" of the canvas.
|
||||
|
||||
When the camera is at `{x: 0, y: 0, z: 0}`, the screen point and page point will be identical. As the camera moves, however, the viewport will display a different part of the page; and so a screen point will correspond to a different page point.
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------ | ---------------------------------------------- |
|
||||
| [Editor#screenToPage](?) | Convert a point in screen space to page space. |
|
||||
| [Editor#pageToScreen](?) | Convert a point in page space to screen space. |
|
||||
|
||||
You can get the user's pointer position in both screen and page space.
|
||||
|
||||
```ts
|
||||
const {
|
||||
// The user's most recent page / screen points
|
||||
currentPagePoint,
|
||||
currentScreenPoint,
|
||||
// The user's previous page / screen points
|
||||
previousPagePoint,
|
||||
previousScreenPoint,
|
||||
// The last place where the most recent pointer down occurred
|
||||
originPagePoint,
|
||||
originScreenPoint,
|
||||
} = editor.inputs
|
||||
```
|
||||
|
||||
## Camera options
|
||||
|
||||
You can use the editor's camera options to configure the behavior of the editor's camera. There are many options available.
|
||||
|
||||
### `wheelBehavior`
|
||||
|
||||
When set to `'pan'`, scrolling the mousewheel will pan the camera. When set to `'zoom'`, scrolling the mousewheel will zoom the camera. When set to `none`, it will have no effect.
|
||||
|
||||
### `panSpeed`
|
||||
|
||||
The speed at which the camera pans. A pan can occur when the user holds the spacebar and drags, holds the middle mouse button and drags, drags while using the hand tool, or scrolls the mousewheel. The default value is `1`. A value of `0.5` would be twice as slow as default. A value of `2` would be twice as fast. When set to `0`, the camera will not pan.
|
||||
|
||||
### `zoomSpeed`
|
||||
|
||||
The speed at which the camera zooms. A zoom can occur when the user pinches or scrolls the mouse wheel. The default value is `1`. A value of `0.5` would be twice as slow as default. A value of `2` would be twice as fast. When set to `0`, the camera will not zoom.
|
||||
|
||||
### `zoomSteps`
|
||||
|
||||
The camera's "zoom steps" are an array of discrete zoom levels that the camera will move between when using the "zoom in" or "zoom out" controls.
|
||||
|
||||
The first number in the `zoomSteps` array defines the camera's minimum zoom level. The last number in the `zoomSteps` array defines the camera's maximum zoom level.
|
||||
|
||||
If the `constraints` are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the `zoomSteps` array with the value from the `baseZoom`. See the `baseZoom` property for more information.
|
||||
|
||||
### `isLocked`
|
||||
|
||||
Whether the camera is locked. When the camera is locked, the camera will not move.
|
||||
|
||||
### `constraints`
|
||||
|
||||
By default the camera is free to move anywhere on the infinite canvas. However, you may provide the camera with a `constraints` object that constrains the camera based on a relationship between a `bounds` (in page space) and the viewport (in screen space).
|
||||
|
||||
### `constraints.bounds`
|
||||
|
||||
A box model describing the bounds in page space.
|
||||
|
||||
### `constraints.padding`
|
||||
|
||||
An object with padding to apply to the `x` and `y` dimensions of the viewport. The padding is in screen space.
|
||||
|
||||
### `constraints.origin`
|
||||
|
||||
An object with an origin for the `x` and `y` dimensions. Depending on the `behavior`, the origin may be used to position the bounds within the viewport.
|
||||
|
||||
For example, when the `behavior` is `fixed` and the `origin.x` is `0`, the bounds will be placed with its left side touching the left side of the viewport. When `origin.x` is `1` the bounds will be placed with its right side touching the right side of the viewport. By default the origin for each dimension is .5. This places the bounds in the center of the viewport.
|
||||
|
||||
### `constraints.initialZoom`
|
||||
|
||||
The `initialZoom` option defines the camera's initial zoom level and what the zoom should be when when the camera is reset. The zoom it produces is based on the value provided:
|
||||
|
||||
| Value | Description |
|
||||
| --------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| 'default' | 100%. |
|
||||
| 'fit-x' | The zoom at which the constraint's `bounds.width` exactly fits within the viewport. |
|
||||
| 'fit-y' | The zoom at which the constraint's `bounds.height` exactly fits within the viewport. |
|
||||
| 'fit-min' | The zoom at which the _smaller_ of the constraint's `bounds.width` or `bounds.height` exactly fits within the viewport. |
|
||||
| 'fit-max' | The zoom at which the _larger_ of the constraint's `bounds.width` or `bounds.height` exactly fits within the viewport. |
|
||||
|
||||
### `constraints.baseZoom`
|
||||
|
||||
The `baseZoom` property defines the base property for the camera's zoom steps. It accepts the same values as `initialZoom`.
|
||||
|
||||
When `constraints` are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the `zoomSteps` array with the value from the `baseZoom`.
|
||||
|
||||
For example, if the `baseZoom` is set to `default`, then a zoom step of 2 will be 200%. However, if the `baseZoom` is set to `fit-x`, then a zoom step value of 2 will be twice the zoom level at which the bounds width exactly fits within the viewport.
|
||||
|
||||
### `constraints.behavior`
|
||||
|
||||
The `behavior` property defines which logic should be used when calculating the bounds position.
|
||||
|
||||
| Value | Description |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 'free' | The bounds may be placed anywhere relative to the viewport. This is the default "infinite canvas" experience. |
|
||||
| 'inside' | The bounds must stay entirely within the viewport. |
|
||||
| 'outside' | The bounds may partially leave the viewport but must never leave it completely. |
|
||||
| 'fixed' | The bounds are placed in the viewport at a fixed location according to the `'origin'`. |
|
||||
| 'contain' | When the zoom is below the "fit zoom" for an axis, the bounds use the `'fixed'` behavior; when above, the bounds use the `inside` behavior. |
|
||||
|
||||
## Controlling the camera
|
||||
|
||||
There are several `Editor` methods available for controlling the camera.
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| [Editor#setCamera](?) | Moves the camera to the provided coordinates. |
|
||||
| [Editor#zoomIn](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
|
||||
| [Editor#zoomOut](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
|
||||
| [Editor#zoomToFit](?) | Zooms the camera in to the nearest zoom step. See the `constraints.zoomSteps` for more information. |
|
||||
| [Editor#zoomToBounds](?) | Moves the camera to fit the given bounding box. |
|
||||
| [Editor#zoomToSelection](?) | Moves the camera to fit the current selection. |
|
||||
| [Editor#zoomToUser](?) | Moves the camera to center on a user's cursor. |
|
||||
| [Editor#resetZoom](?) | Resets the zoom to 100% or to the `initialZoom` zoom level. |
|
||||
| [Editor#centerOnPoint](?) | Centers the camera on the given point. |
|
||||
| [Editor#stopCameraAnimation](?) | Stops any camera animation. |
|
||||
|
||||
## Camera state
|
||||
|
||||
The camera may be in two states, `idle` or `moving`.
|
||||
|
||||
You can get the current camera state with [Editor#getCameraState](?).
|
||||
|
||||
# Common things to do with the editor
|
||||
|
||||
### Create a shape id
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ReactNode, useEffect, useState, version } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { LoadingScreen } from 'tldraw'
|
||||
import { version } from '../../version'
|
||||
import { useUrl } from '../hooks/useUrl'
|
||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||
|
||||
|
@ -113,7 +114,7 @@ export function IFrameProtector({
|
|||
<div className="tldraw__editor tl-container">
|
||||
<div className="iframe-warning__container">
|
||||
<a className="iframe-warning__link" href={url} target="_blank">
|
||||
{'Visit this page on tldraw.com '}
|
||||
{'Visit this page on tldraw.com'}
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { default as React, useEffect } from 'react'
|
||||
import { Editor, TLPageId, clamp, debounce, react, useEditor } from 'tldraw'
|
||||
import { Editor, TLPageId, Vec, clamp, debounce, react, useEditor } from 'tldraw'
|
||||
|
||||
const PARAMS = {
|
||||
// deprecated
|
||||
|
@ -68,19 +68,13 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) {
|
|||
const viewport = viewportFromString(newViewportRaw)
|
||||
const { x, y, w, h } = viewport
|
||||
const { w: sw, h: sh } = editor.getViewportScreenBounds()
|
||||
const fitZoom = editor.getCameraFitZoom()
|
||||
const initialZoom = editor.getInitialZoom()
|
||||
const { zoomSteps } = editor.getCameraOptions()
|
||||
const zoomMin = zoomSteps[0]
|
||||
const zoomMax = zoomSteps[zoomSteps.length - 1]
|
||||
|
||||
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * fitZoom, zoomMax * fitZoom)
|
||||
|
||||
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * initialZoom, zoomMax * initialZoom)
|
||||
editor.setCamera(
|
||||
{
|
||||
x: -x + (sw - w * zoom) / 2 / zoom,
|
||||
y: -y + (sh - h * zoom) / 2 / zoom,
|
||||
z: zoom,
|
||||
},
|
||||
new Vec(-x + (sw - w * zoom) / 2 / zoom, -y + (sh - h * zoom) / 2 / zoom, zoom),
|
||||
{ immediate: true }
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -86,7 +86,6 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
|
|||
editor.history.clear()
|
||||
// Put the old bounds back in place
|
||||
editor.updateViewportScreenBounds(bounds)
|
||||
editor.updateRenderingBounds()
|
||||
editor.updateInstanceState({ isFocused })
|
||||
})
|
||||
},
|
||||
|
|
|
@ -68,5 +68,5 @@ function createDemoShapes(editor: Editor) {
|
|||
},
|
||||
}))
|
||||
)
|
||||
.zoomToContent({ animation: { duration: 0 } })
|
||||
.zoomToFit({ animation: { duration: 0 } })
|
||||
}
|
||||
|
|
|
@ -58,5 +58,5 @@ function createDemoShapes(editor: Editor) {
|
|||
})),
|
||||
])
|
||||
|
||||
editor.zoomToContent({ animation: { duration: 0 } })
|
||||
editor.zoomToFit({ animation: { duration: 0 } })
|
||||
}
|
||||
|
|
|
@ -44,5 +44,5 @@ function createDemoShapes(editor: Editor) {
|
|||
},
|
||||
},
|
||||
])
|
||||
.zoomToContent({ animation: { duration: 0 } })
|
||||
.zoomToFit({ animation: { duration: 0 } })
|
||||
}
|
||||
|
|
|
@ -13,19 +13,20 @@ import 'tldraw/tldraw.css'
|
|||
|
||||
const CAMERA_OPTIONS: TLCameraOptions = {
|
||||
isLocked: false,
|
||||
wheelBehavior: 'pan',
|
||||
panSpeed: 1,
|
||||
zoomSpeed: 1,
|
||||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
||||
constraints: {
|
||||
fit: 'max',
|
||||
initialZoom: 'fit-max',
|
||||
baseZoom: 'fit-max',
|
||||
bounds: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1200,
|
||||
h: 800,
|
||||
w: 1600,
|
||||
h: 900,
|
||||
},
|
||||
fitX: 'contain',
|
||||
fitY: 'contain',
|
||||
behavior: { x: 'contain', y: 'contain' },
|
||||
padding: { x: 100, y: 100 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
|
@ -139,11 +140,16 @@ const components = {
|
|||
const CameraOptionsControlPanel = track(() => {
|
||||
const editor = useEditor()
|
||||
|
||||
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex', CAMERA_OPTIONS)
|
||||
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex1', CAMERA_OPTIONS)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return
|
||||
editor.setCameraOptions(cameraOptions, { immediate: true })
|
||||
editor.batch(() => {
|
||||
editor.setCameraOptions(cameraOptions, { immediate: true })
|
||||
editor.setCamera(editor.getCamera(), {
|
||||
immediate: true,
|
||||
})
|
||||
})
|
||||
}, [editor, cameraOptions])
|
||||
|
||||
const { constraints } = cameraOptions
|
||||
|
@ -234,10 +240,12 @@ const CameraOptionsControlPanel = track(() => {
|
|||
<input
|
||||
name="zoomsteps"
|
||||
type="text"
|
||||
value={cameraOptions.zoomSteps.join(', ')}
|
||||
defaultValue={cameraOptions.zoomSteps.join(', ')}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.split(', ').map((v) => Number(v))
|
||||
updateOptions({ zoomSteps: val })
|
||||
if (val.every((v) => typeof v === 'number' && Number.isFinite(v))) {
|
||||
updateOptions({ zoomSteps: val })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="bounds">Bounds</label>
|
||||
|
@ -275,24 +283,43 @@ const CameraOptionsControlPanel = track(() => {
|
|||
</select>
|
||||
{constraints ? (
|
||||
<>
|
||||
<label htmlFor="fit">Fit</label>
|
||||
<label htmlFor="initialZoom">Initial Zoom</label>
|
||||
<select
|
||||
name="fit"
|
||||
value={constraints.fit}
|
||||
name="initialZoom"
|
||||
value={constraints.initialZoom}
|
||||
onChange={(e) => {
|
||||
updateOptions({
|
||||
constraints: {
|
||||
...constraints,
|
||||
fit: e.target.value as any,
|
||||
initialZoom: e.target.value as any,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option>min</option>
|
||||
<option>max</option>
|
||||
<option> x</option>
|
||||
<option> y</option>
|
||||
<option>none</option>
|
||||
<option>fit-min</option>
|
||||
<option>fit-max</option>
|
||||
<option>fit-x</option>
|
||||
<option>fit-y</option>
|
||||
<option>default</option>
|
||||
</select>
|
||||
<label htmlFor="zoomBehavior">Base Zoom</label>
|
||||
<select
|
||||
name="zoomBehavior"
|
||||
value={constraints.baseZoom}
|
||||
onChange={(e) => {
|
||||
updateOptions({
|
||||
constraints: {
|
||||
...constraints,
|
||||
baseZoom: e.target.value as any,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option>fit-min</option>
|
||||
<option>fit-max</option>
|
||||
<option>fit-x</option>
|
||||
<option>fit-y</option>
|
||||
<option>default</option>
|
||||
</select>
|
||||
<label htmlFor="originX">Origin X</label>
|
||||
<input
|
||||
|
@ -368,16 +395,19 @@ const CameraOptionsControlPanel = track(() => {
|
|||
})
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="fitx">Fit X</label>
|
||||
<label htmlFor="behaviorX">Behavior X</label>
|
||||
<select
|
||||
name="fitx"
|
||||
value={constraints.fitX}
|
||||
name="behaviorX"
|
||||
value={(constraints.behavior as { x: any; y: any }).x}
|
||||
onChange={(e) => {
|
||||
setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...constraints,
|
||||
fitX: e.target.value as any,
|
||||
behavior: {
|
||||
...(constraints.behavior as { x: any; y: any }),
|
||||
x: e.target.value as any,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
|
@ -387,16 +417,19 @@ const CameraOptionsControlPanel = track(() => {
|
|||
<option>outside</option>
|
||||
<option>lock</option>
|
||||
</select>
|
||||
<label htmlFor="fity">Fit Y</label>
|
||||
<label htmlFor="behaviorY">Behavior Y</label>
|
||||
<select
|
||||
name="fity"
|
||||
value={constraints.fitY}
|
||||
name="behaviorY"
|
||||
value={(constraints.behavior as { x: any; y: any }).y}
|
||||
onChange={(e) => {
|
||||
setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...constraints,
|
||||
fitY: e.target.value as any,
|
||||
behavior: {
|
||||
...(constraints.behavior as { x: any; y: any }),
|
||||
y: e.target.value as any,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
|
@ -412,7 +445,7 @@ const CameraOptionsControlPanel = track(() => {
|
|||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.setCamera(editor.getCamera(), { initial: true })
|
||||
editor.setCamera(editor.getCamera(), { reset: true })
|
||||
}}
|
||||
>
|
||||
Reset Camera
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function ExternalContentSourcesExample() {
|
|||
const htmlSource = sources?.find((s) => s.type === 'text' && s.subtype === 'html')
|
||||
|
||||
if (htmlSource) {
|
||||
const center = point ?? editor.getViewportPageCenter()
|
||||
const center = point ?? editor.getViewportPageBounds().center
|
||||
|
||||
editor.createShape({
|
||||
type: 'html',
|
||||
|
|
|
@ -133,19 +133,19 @@ export function ImageAnnotationEditor({
|
|||
editor.setCameraOptions(
|
||||
{
|
||||
constraints: {
|
||||
fit: 'max',
|
||||
initialZoom: 'fit-max',
|
||||
baseZoom: 'default',
|
||||
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
|
||||
padding: { x: 32, y: 64 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
fitX: 'inside',
|
||||
fitY: 'inside',
|
||||
behavior: 'inside',
|
||||
},
|
||||
zoomSteps: [1, 2, 4, 8],
|
||||
zoomSpeed: 1,
|
||||
panSpeed: 1,
|
||||
isLocked: false,
|
||||
},
|
||||
{ initial: isInitial }
|
||||
{ reset: isInitial }
|
||||
)
|
||||
|
||||
isInitial = false
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
title: Rendering shapes change
|
||||
component: ./RenderingShapesChangeExample.tsx
|
||||
category: basic
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
Do something when the rendering shapes change.
|
|
@ -1,27 +0,0 @@
|
|||
import { useCallback } from 'react'
|
||||
import { TLShape, Tldraw } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
import { useChangedShapesReactor } from './useRenderingShapesChange'
|
||||
|
||||
const components = {
|
||||
InFrontOfTheCanvas: () => {
|
||||
const onShapesChanged = useCallback((info: { culled: TLShape[]; restored: TLShape[] }) => {
|
||||
// eslint-disable-next-line no-console
|
||||
for (const shape of info.culled) console.log('culled: ' + shape.id)
|
||||
// eslint-disable-next-line no-console
|
||||
for (const shape of info.restored) console.log('restored: ' + shape.id)
|
||||
}, [])
|
||||
|
||||
useChangedShapesReactor(onShapesChanged)
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
export default function RenderingShapesChangeExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw persistenceKey="example" components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import { TLShape, react, useEditor } from 'tldraw'
|
||||
|
||||
export function useChangedShapesReactor(
|
||||
cb: (info: { culled: TLShape[]; restored: TLShape[] }) => void
|
||||
) {
|
||||
const editor = useEditor()
|
||||
const rPrevShapes = useRef({
|
||||
renderingShapes: editor.getRenderingShapes(),
|
||||
culledShapes: editor.getCulledShapes(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
return react('when rendering shapes change', () => {
|
||||
const after = {
|
||||
culledShapes: editor.getCulledShapes(),
|
||||
renderingShapes: editor.getRenderingShapes(),
|
||||
}
|
||||
const before = rPrevShapes.current
|
||||
|
||||
const culled: TLShape[] = []
|
||||
const restored: TLShape[] = []
|
||||
|
||||
const beforeToVisit = new Set(before.renderingShapes)
|
||||
|
||||
for (const afterInfo of after.renderingShapes) {
|
||||
const beforeInfo = before.renderingShapes.find((s) => s.id === afterInfo.id)
|
||||
if (!beforeInfo) {
|
||||
continue
|
||||
} else {
|
||||
const isAfterCulled = after.culledShapes.has(afterInfo.id)
|
||||
const isBeforeCulled = before.culledShapes.has(beforeInfo.id)
|
||||
if (isAfterCulled && !isBeforeCulled) {
|
||||
culled.push(afterInfo.shape)
|
||||
} else if (!isAfterCulled && isBeforeCulled) {
|
||||
restored.push(afterInfo.shape)
|
||||
}
|
||||
beforeToVisit.delete(beforeInfo)
|
||||
}
|
||||
}
|
||||
|
||||
rPrevShapes.current = after
|
||||
|
||||
cb({
|
||||
culled,
|
||||
restored,
|
||||
})
|
||||
})
|
||||
}, [cb, editor])
|
||||
}
|
|
@ -203,7 +203,6 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
text={text}
|
||||
labelColor={theme[color].solid}
|
||||
isSelected={isSelected}
|
||||
disableTab
|
||||
wrap
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -50,7 +50,6 @@ const ZOOM_EVENT = {
|
|||
'reset-zoom': 'resetZoom',
|
||||
'zoom-to-fit': 'zoomToFit',
|
||||
'zoom-to-selection': 'zoomToSelection',
|
||||
'zoom-to-content': 'zoomToContent',
|
||||
}
|
||||
|
||||
export function getCodeSnippet(name: string, data: any) {
|
||||
|
@ -136,15 +135,11 @@ if (updates.length > 0) {
|
|||
} else if (name === 'fit-frame-to-content') {
|
||||
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
|
||||
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
|
||||
if (name === 'zoom-to-content') {
|
||||
codeSnippet = 'editor.zoomToContent()'
|
||||
} else {
|
||||
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
|
||||
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
|
||||
? 'editor.getViewportScreenCenter(), '
|
||||
: ''
|
||||
}{ duration: 320 })`
|
||||
}
|
||||
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
|
||||
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
|
||||
? 'editor.getViewportScreenCenter(), '
|
||||
: ''
|
||||
}{ duration: 320 })`
|
||||
} else if (name.startsWith('toggle-')) {
|
||||
if (name === 'toggle-lock') {
|
||||
codeSnippet = `editor.toggleLock(editor.getSelectedShapeIds())`
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
## 2.0.30
|
||||
|
||||
- Fixes a bug that prevented opening some files.
|
||||
|
||||
## 2.0.29
|
||||
|
||||
- Improved note shapes.
|
||||
- Color improvements for both light and dark mode.
|
||||
- Bug fixes and performance improvements.
|
||||
|
||||
## 2.0.28
|
||||
|
||||
- Fix an issue with panning the canvas.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "tldraw-vscode",
|
||||
"description": "The tldraw extension for VS Code.",
|
||||
"version": "2.0.28",
|
||||
"version": "2.0.30",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "tldraw Inc.",
|
||||
|
|
|
@ -11,6 +11,10 @@ import { TextDecoder, TextEncoder } from 'util'
|
|||
global.TextEncoder = TextEncoder
|
||||
global.TextDecoder = TextDecoder
|
||||
|
||||
Image.prototype.decode = async function () {
|
||||
return true
|
||||
}
|
||||
|
||||
function convertNumbersInObject(obj: any, roundToNearest: number) {
|
||||
if (!obj) return obj
|
||||
if (Array.isArray(obj)) {
|
||||
|
|
|
@ -459,6 +459,9 @@ export const DEFAULT_ANIMATION_OPTIONS: {
|
|||
easing: (t: number) => number;
|
||||
};
|
||||
|
||||
// @internal (undocumented)
|
||||
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
export function DefaultBackground(): JSX_2.Element;
|
||||
|
||||
|
@ -593,7 +596,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}>;
|
||||
force: boolean;
|
||||
immediate: boolean;
|
||||
initial: boolean;
|
||||
reset: boolean;
|
||||
}>): this;
|
||||
animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: Partial<{
|
||||
animation: Partial<{
|
||||
|
@ -602,7 +605,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}>;
|
||||
force: boolean;
|
||||
immediate: boolean;
|
||||
initial: boolean;
|
||||
reset: boolean;
|
||||
}>): this;
|
||||
// @internal (undocumented)
|
||||
annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: {
|
||||
|
@ -689,12 +692,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
|
||||
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
||||
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
||||
getBaseZoom(): number;
|
||||
getCamera(): TLCamera;
|
||||
getCameraFitZoom(): number;
|
||||
getCameraOptions(): TLCameraOptions;
|
||||
getCameraState(): "idle" | "moving";
|
||||
getCanRedo(): boolean;
|
||||
getCanUndo(): boolean;
|
||||
getCollaborators(): TLInstancePresence[];
|
||||
getCollaboratorsOnCurrentPage(): TLInstancePresence[];
|
||||
getContainer: () => HTMLElement;
|
||||
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
|
||||
// @internal
|
||||
|
@ -706,6 +711,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getCurrentPageId(): TLPageId;
|
||||
getCurrentPageRenderingShapesSorted(): TLShape[];
|
||||
getCurrentPageShapeIds(): Set<TLShapeId>;
|
||||
// @internal (undocumented)
|
||||
getCurrentPageShapeIdsSorted(): TLShapeId[];
|
||||
getCurrentPageShapes(): TLShape[];
|
||||
getCurrentPageShapesSorted(): TLShape[];
|
||||
getCurrentPageState(): TLInstancePageState;
|
||||
|
@ -725,6 +732,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getHoveredShape(): TLShape | undefined;
|
||||
getHoveredShapeId(): null | TLShapeId;
|
||||
getInitialMetaForShape(_shape: TLShape): JsonObject;
|
||||
getInitialZoom(): number;
|
||||
getInstanceState(): TLInstance;
|
||||
getIsMenuOpen(): boolean;
|
||||
getOnlySelectedShape(): null | TLShape;
|
||||
|
@ -738,7 +746,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getPath(): string;
|
||||
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
||||
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
||||
getRenderingBounds(): Box;
|
||||
getRenderingShapes(): {
|
||||
backgroundIndex: number;
|
||||
id: TLShapeId;
|
||||
|
@ -813,7 +820,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
util: ShapeUtil;
|
||||
}[];
|
||||
getViewportPageBounds(): Box;
|
||||
getViewportPageCenter(): Vec;
|
||||
getViewportScreenBounds(): Box;
|
||||
getViewportScreenCenter(): Vec;
|
||||
getZoomLevel(): number;
|
||||
|
@ -860,8 +866,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
|
||||
pageToScreen(point: VecLike): Vec;
|
||||
pageToViewport(point: VecLike): Vec;
|
||||
pan(offset: VecLike, opts?: TLCameraMoveOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this;
|
||||
popFocusedGroupId(): this;
|
||||
putContentOntoCurrentPage(content: TLContent, options?: {
|
||||
point?: VecLike;
|
||||
|
@ -878,7 +882,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
type: T;
|
||||
} : TLExternalContent) => void) | null): this;
|
||||
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
|
||||
renderingBoundsMargin: number;
|
||||
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
|
||||
resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this;
|
||||
resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
|
||||
|
@ -892,11 +895,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
sendBackward(shapes: TLShape[] | TLShapeId[]): this;
|
||||
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
|
||||
setCamera(point: VecLike, opts?: TLCameraMoveOptions): this;
|
||||
setCameraOptions(options: Partial<TLCameraOptions>, opts?: {
|
||||
force?: boolean;
|
||||
immediate?: boolean;
|
||||
initial?: boolean;
|
||||
}): this;
|
||||
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions): this;
|
||||
setCroppingShape(shape: null | TLShape | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
|
||||
setCurrentTool(id: string, info?: {}): this;
|
||||
|
@ -943,8 +942,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
|
||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: TLCommandHistoryOptions): this;
|
||||
// @internal
|
||||
updateRenderingBounds(): this;
|
||||
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: TLCommandHistoryOptions): this;
|
||||
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: TLCommandHistoryOptions): this;
|
||||
updateViewportScreenBounds(screenBounds: Box, center?: boolean): this;
|
||||
|
@ -956,10 +953,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
inset?: number;
|
||||
targetZoom?: number;
|
||||
} & TLCameraMoveOptions): this;
|
||||
zoomToContent(opts?: TLCameraMoveOptions): this;
|
||||
zoomToFit(opts?: TLCameraMoveOptions): this;
|
||||
zoomToSelection(opts?: TLCameraMoveOptions): this;
|
||||
zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this;
|
||||
zoomToUser(userId: string, opts?: TLCameraMoveOptions): this;
|
||||
}
|
||||
|
||||
|
@ -1093,9 +1088,6 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap
|
|||
// @public (undocumented)
|
||||
export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const getDefaultCameraOptions: (cameraOptions?: Partial<TLCameraOptions>) => TLCameraOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getFreshUserPreferences(): TLUserPreferences;
|
||||
|
||||
|
@ -1997,23 +1989,27 @@ export type TLBrushProps = {
|
|||
// @public (undocumented)
|
||||
export type TLCameraMoveOptions = Partial<{
|
||||
animation: Partial<{
|
||||
duration: number;
|
||||
easing: (t: number) => number;
|
||||
duration: number;
|
||||
}>;
|
||||
force: boolean;
|
||||
immediate: boolean;
|
||||
initial: boolean;
|
||||
reset: boolean;
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLCameraOptions = {
|
||||
wheelBehavior: 'none' | 'pan' | 'zoom';
|
||||
constraints?: {
|
||||
fitX: 'contain' | 'inside' | 'lock' | 'outside';
|
||||
fitY: 'contain' | 'inside' | 'lock' | 'outside';
|
||||
behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {
|
||||
x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
|
||||
y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
|
||||
};
|
||||
bounds: BoxModel;
|
||||
baseZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
|
||||
initialZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
|
||||
origin: VecLike;
|
||||
padding: VecLike;
|
||||
fit: 'max' | 'min' | 'none' | 'x' | 'y';
|
||||
};
|
||||
panSpeed: number;
|
||||
zoomSpeed: number;
|
||||
|
|
|
@ -7554,7 +7554,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>"
|
||||
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n }>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -7641,7 +7641,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>"
|
||||
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n }>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -9838,6 +9838,37 @@
|
|||
"isAbstract": false,
|
||||
"name": "getAssets"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getBaseZoom:member(1)",
|
||||
"docComment": "/**\n * Get the camera's base level for calculating actual zoom levels based on the zoom steps.\n *\n * @example\n * ```ts\n * editor.getBaseZoom()\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getBaseZoom(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getBaseZoom"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getCamera:member(1)",
|
||||
|
@ -9874,37 +9905,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "getCamera"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getCameraFitZoom:member(1)",
|
||||
"docComment": "/**\n * Get the zoom level that would fit the camera to the current constraints.\n *\n * @example\n * ```ts\n * editor.getCameraFitZoom()\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getCameraFitZoom(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getCameraFitZoom"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)",
|
||||
|
@ -10030,6 +10030,86 @@
|
|||
"isAbstract": false,
|
||||
"name": "getCanUndo"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getCollaborators:member(1)",
|
||||
"docComment": "/**\n * Returns a list of presence records for all peer collaborators. This will return the latest presence record for each connected user.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getCollaborators(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "import(\"@tldraw/tlschema\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLInstancePresence",
|
||||
"canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getCollaborators"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getCollaboratorsOnCurrentPage:member(1)",
|
||||
"docComment": "/**\n * Returns a list of presence records for all peer collaborators on the current page. This will return the latest presence record for each connected user.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getCollaboratorsOnCurrentPage(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "import(\"@tldraw/tlschema\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLInstancePresence",
|
||||
"canonicalReference": "@tldraw/tlschema!TLInstancePresence:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getCollaboratorsOnCurrentPage"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getContainer:member",
|
||||
|
@ -11169,6 +11249,37 @@
|
|||
"isAbstract": false,
|
||||
"name": "getInitialMetaForShape"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getInitialZoom:member(1)",
|
||||
"docComment": "/**\n * Get the camera's initial or reset zoom level.\n *\n * @example\n * ```ts\n * editor.getInitialZoom()\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getInitialZoom(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getInitialZoom"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getInstanceState:member(1)",
|
||||
|
@ -11809,38 +11920,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "getPointInShapeSpace"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getRenderingBounds:member(1)",
|
||||
"docComment": "/**\n * The current rendering bounds in the current page space, used for checking which shapes are \"on screen\".\n *\n * @example\n * ```ts\n * editor.getRenderingBounds()\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getRenderingBounds(): "
|
||||
},
|
||||
{
|
||||
"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": "getRenderingBounds"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)",
|
||||
|
@ -14146,38 +14225,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "getViewportPageBounds"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getViewportPageCenter:member(1)",
|
||||
"docComment": "/**\n * The center of the viewport in the current page space.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getViewportPageCenter(): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Vec",
|
||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getViewportPageCenter"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getViewportScreenBounds:member(1)",
|
||||
|
@ -15589,142 +15636,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "pageToViewport"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#pan:member(1)",
|
||||
"docComment": "/**\n * Pan the camera.\n *\n * @param offset - The offset in the current page space.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.pan({ x: 100, y: 100 })\n * editor.pan({ x: 100, y: 100 }, { animation: { duration: 1000 } })\n * ```\n *\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "pan(offset: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLCameraMoveOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "this"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "offset",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "pan"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#panZoomIntoView:member(1)",
|
||||
"docComment": "/**\n * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.\n *\n * @param ids - The ids of the shapes to pan and zoom into view.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.panZoomIntoView([myShape.id])\n * editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "panZoomIntoView(ids: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLCameraMoveOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "this"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 6,
|
||||
"endIndex": 7
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "ids",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 5
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "panZoomIntoView"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#popFocusedGroupId:member(1)",
|
||||
|
@ -16240,36 +16151,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "renamePage"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Editor#renderingBoundsMargin:member",
|
||||
"docComment": "/**\n * The distance to expand the viewport when measuring culling. A larger distance will mean that shapes near to the viewport (but still outside of it) will not be culled.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "renderingBoundsMargin: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "renderingBoundsMargin",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#reparentShapes:member(1)",
|
||||
|
@ -17060,8 +16941,9 @@
|
|||
"text": ", opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n force?: boolean;\n immediate?: boolean;\n initial?: boolean;\n }"
|
||||
"kind": "Reference",
|
||||
"text": "TLCameraMoveOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -19799,55 +19681,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "zoomToBounds"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#zoomToContent:member(1)",
|
||||
"docComment": "/**\n * Move the camera to the nearest content.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToContent()\n * editor.zoomToContent({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "zoomToContent(opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLCameraMoveOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "this"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "zoomToContent"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)",
|
||||
|
@ -19946,72 +19779,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "zoomToSelection"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#zoomToShape:member(1)",
|
||||
"docComment": "/**\n * Animate the camera to a shape.\n *\n * @param shapeId - The id of the shape to animate to.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToShape(myShape.id)\n * editor.zoomToShape(myShape.id, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "zoomToShape(shapeId: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLCameraMoveOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "this"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shapeId",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "zoomToShape"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)",
|
||||
|
@ -36940,14 +36707,14 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n}>"
|
||||
"text": "<{\n easing: (t: number) => number;\n duration: number;\n }>;\n force: boolean;\n immediate: boolean;\n reset: boolean;\n}>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/editor/Editor.ts",
|
||||
"fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "TLCameraMoveOptions",
|
||||
"typeTokenRange": {
|
||||
|
@ -36966,7 +36733,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n constraints?: {\n fitX: 'contain' | 'inside' | 'lock' | 'outside';\n fitY: 'contain' | 'inside' | 'lock' | 'outside';\n bounds: "
|
||||
"text": "{\n wheelBehavior: 'none' | 'pan' | 'zoom';\n constraints?: {\n behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {\n x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';\n y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';\n };\n bounds: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -36975,7 +36742,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n origin: "
|
||||
"text": ";\n baseZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';\n initialZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';\n origin: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -36993,7 +36760,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n}"
|
||||
"text": ";\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -111,6 +111,7 @@ export {
|
|||
ANIMATION_SHORT_MS,
|
||||
CAMERA_SLIDE_FRICTION,
|
||||
DEFAULT_ANIMATION_OPTIONS,
|
||||
DEFAULT_CAMERA_OPTIONS,
|
||||
DOUBLE_CLICK_DURATION,
|
||||
DRAG_DISTANCE,
|
||||
GRID_STEPS,
|
||||
|
@ -120,14 +121,8 @@ export {
|
|||
MULTI_CLICK_DURATION,
|
||||
SIDES,
|
||||
SVG_PADDING,
|
||||
getDefaultCameraOptions,
|
||||
} from './lib/constants'
|
||||
export {
|
||||
Editor,
|
||||
type TLCameraMoveOptions,
|
||||
type TLEditorOptions,
|
||||
type TLResizeShapeOptions,
|
||||
} from './lib/editor/Editor'
|
||||
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
||||
export type {
|
||||
SideEffectManager,
|
||||
TLAfterChangeHandler,
|
||||
|
@ -240,6 +235,7 @@ export {
|
|||
} from './lib/editor/types/history-types'
|
||||
export {
|
||||
type RequiredKeys,
|
||||
type TLCameraMoveOptions,
|
||||
type TLCameraOptions,
|
||||
type TLSvgOptions,
|
||||
} from './lib/editor/types/misc-types'
|
||||
|
|
|
@ -11,24 +11,15 @@ export const ANIMATION_SHORT_MS = 80
|
|||
/** @internal */
|
||||
export const ANIMATION_MEDIUM_MS = 320
|
||||
|
||||
const DEFAULT_COMMON_CAMERA_OPTIONS = {
|
||||
zoomMax: 8,
|
||||
zoomMin: 0.1,
|
||||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
||||
zoomSpeed: 1,
|
||||
panSpeed: 1,
|
||||
/** @internal */
|
||||
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
|
||||
isLocked: false,
|
||||
wheelBehavior: 'pan',
|
||||
panSpeed: 1,
|
||||
zoomSpeed: 1,
|
||||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const getDefaultCameraOptions = (
|
||||
cameraOptions: Partial<TLCameraOptions> = {}
|
||||
): TLCameraOptions => ({
|
||||
...DEFAULT_COMMON_CAMERA_OPTIONS,
|
||||
...cameraOptions,
|
||||
})
|
||||
|
||||
/** @internal */
|
||||
export const FOLLOW_CHASE_PROPORTION = 0.5
|
||||
/** @internal */
|
||||
export const FOLLOW_CHASE_PAN_SNAP = 0.1
|
||||
|
@ -116,3 +107,8 @@ export const LONG_PRESS_DURATION = 500
|
|||
|
||||
/** @internal */
|
||||
export const TEXT_SHADOW_LOD = 0.35
|
||||
|
||||
export const LEFT_MOUSE_BUTTON = 0
|
||||
export const RIGHT_MOUSE_BUTTON = 2
|
||||
export const MIDDLE_MOUSE_BUTTON = 1
|
||||
export const STYLUS_ERASER_BUTTON = 5
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,5 +1,5 @@
|
|||
import { RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
|
||||
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
|
||||
import { computed, isUninitialized } from '@tldraw/state'
|
||||
import { TLShapeId } from '@tldraw/tlschema'
|
||||
import { Box } from '../../primitives/Box'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
|
@ -20,16 +20,9 @@ function isShapeNotVisible(editor: Editor, id: TLShapeId, viewportPageBounds: Bo
|
|||
* @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)) {
|
||||
|
@ -38,68 +31,19 @@ export const notVisibleShapes = (editor: Editor) => {
|
|||
})
|
||||
return notVisibleShapes
|
||||
}
|
||||
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue, lastComputedEpoch) => {
|
||||
if (!isCullingOffScreenShapes) return new Set<TLShapeId>()
|
||||
|
||||
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue) => {
|
||||
if (isUninitialized(prevValue)) {
|
||||
return fromScratch(editor)
|
||||
}
|
||||
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
|
||||
|
||||
if (diff === RESET_VALUE) {
|
||||
return fromScratch(editor)
|
||||
}
|
||||
const nextValue = 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)
|
||||
}
|
||||
if (prevValue.size !== nextValue.size) return nextValue
|
||||
for (const prev of prevValue) {
|
||||
if (!nextValue.has(prev)) {
|
||||
return nextValue
|
||||
}
|
||||
}
|
||||
|
||||
return nextValue ?? prevValue
|
||||
return prevValue
|
||||
})
|
||||
}
|
||||
|
|
|
@ -95,116 +95,118 @@ export class ClickManager {
|
|||
|
||||
lastPointerInfo = {} as TLPointerEventInfo
|
||||
|
||||
/**
|
||||
* Start the double click timeout.
|
||||
*
|
||||
* @param info - The event info.
|
||||
*/
|
||||
transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
||||
if (!this._clickState) return info
|
||||
handlePointerEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
||||
switch (info.name) {
|
||||
case 'pointer_down': {
|
||||
if (!this._clickState) return info
|
||||
this._clickScreenPoint = Vec.From(info.point)
|
||||
|
||||
this._clickScreenPoint = Vec.From(info.point)
|
||||
|
||||
if (
|
||||
this._previousScreenPoint &&
|
||||
this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE
|
||||
) {
|
||||
this._clickState = 'idle'
|
||||
}
|
||||
|
||||
this._previousScreenPoint = this._clickScreenPoint
|
||||
|
||||
this.lastPointerInfo = info
|
||||
|
||||
switch (this._clickState) {
|
||||
case 'idle': {
|
||||
this._clickState = 'pendingDouble'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return info // returns the pointer event
|
||||
}
|
||||
case 'pendingDouble': {
|
||||
this._clickState = 'pendingTriple'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'down',
|
||||
if (
|
||||
this._previousScreenPoint &&
|
||||
Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
|
||||
) {
|
||||
this._clickState = 'idle'
|
||||
}
|
||||
}
|
||||
case 'pendingTriple': {
|
||||
this._clickState = 'pendingQuadruple'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'triple_click',
|
||||
phase: 'down',
|
||||
|
||||
this._previousScreenPoint = this._clickScreenPoint
|
||||
|
||||
this.lastPointerInfo = info
|
||||
|
||||
switch (this._clickState) {
|
||||
case 'pendingDouble': {
|
||||
this._clickState = 'pendingTriple'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'down',
|
||||
}
|
||||
}
|
||||
case 'pendingTriple': {
|
||||
this._clickState = 'pendingQuadruple'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'triple_click',
|
||||
phase: 'down',
|
||||
}
|
||||
}
|
||||
case 'pendingQuadruple': {
|
||||
this._clickState = 'pendingOverflow'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'quadruple_click',
|
||||
phase: 'down',
|
||||
}
|
||||
}
|
||||
case 'idle': {
|
||||
this._clickState = 'pendingDouble'
|
||||
break
|
||||
}
|
||||
case 'pendingOverflow': {
|
||||
this._clickState = 'overflow'
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// overflow
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'pendingQuadruple': {
|
||||
this._clickState = 'pendingOverflow'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return {
|
||||
...info,
|
||||
type: 'click',
|
||||
name: 'quadruple_click',
|
||||
phase: 'down',
|
||||
}
|
||||
}
|
||||
case 'pendingOverflow': {
|
||||
this._clickState = 'overflow'
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return info
|
||||
}
|
||||
default: {
|
||||
// overflow
|
||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||
return info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit click_up events on pointer up.
|
||||
*
|
||||
* @param info - The event info.
|
||||
*/
|
||||
transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
||||
if (!this._clickState) return info
|
||||
|
||||
this._clickScreenPoint = Vec.From(info.point)
|
||||
|
||||
switch (this._clickState) {
|
||||
case 'pendingTriple': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
case 'pendingQuadruple': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'triple_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
case 'pendingOverflow': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'quadruple_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
default: {
|
||||
// idle, pendingDouble, overflow
|
||||
case 'pointer_up': {
|
||||
if (!this._clickState) return info
|
||||
this._clickScreenPoint = Vec.From(info.point)
|
||||
|
||||
switch (this._clickState) {
|
||||
case 'pendingTriple': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
case 'pendingQuadruple': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'triple_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
case 'pendingOverflow': {
|
||||
return {
|
||||
...this.lastPointerInfo,
|
||||
type: 'click',
|
||||
name: 'quadruple_click',
|
||||
phase: 'up',
|
||||
}
|
||||
}
|
||||
default: {
|
||||
// idle, pendingDouble, overflow
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
case 'pointer_move': {
|
||||
if (
|
||||
this._clickState !== 'idle' &&
|
||||
this._clickScreenPoint &&
|
||||
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
|
||||
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
) {
|
||||
this.cancelDoubleClickTimeout()
|
||||
}
|
||||
return info
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,21 +218,4 @@ export class ClickManager {
|
|||
this._clickTimeout = clearTimeout(this._clickTimeout)
|
||||
this._clickState = 'idle'
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a move event, possibly cancelling the click timeout.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
handleMove = () => {
|
||||
// Cancel a double click event if the user has started dragging.
|
||||
if (
|
||||
this._clickState !== 'idle' &&
|
||||
this._clickScreenPoint &&
|
||||
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
|
||||
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||
) {
|
||||
this.cancelDoubleClickTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ export class SnapManager {
|
|||
// TODO: make this an incremental derivation
|
||||
@computed getSnappableShapes(): Set<TLShapeId> {
|
||||
const { editor } = this
|
||||
const renderingBounds = editor.getRenderingBounds()
|
||||
const renderingBounds = editor.getViewportPageBounds()
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
|
||||
const snappableShapes: Set<TLShapeId> = new Set()
|
||||
|
|
|
@ -17,8 +17,27 @@ export type TLSvgOptions = {
|
|||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TLCameraMoveOptions = Partial<{
|
||||
/** Whether to move the camera immediately, rather than on the next tick. */
|
||||
immediate: boolean
|
||||
/** Whether to force the camera to move, even if the user's camera options have locked the camera. */
|
||||
force: boolean
|
||||
/** Whether to reset the camera to its default position and zoom. */
|
||||
reset: boolean
|
||||
/** An (optional) animation to use. */
|
||||
animation: Partial<{
|
||||
/** The time the animation should take to arrive at the specified camera coordinates. */
|
||||
duration: number
|
||||
/** An easing function to apply to the animation's progress from start to end. */
|
||||
easing: (t: number) => number
|
||||
}>
|
||||
}>
|
||||
|
||||
/** @public */
|
||||
export type TLCameraOptions = {
|
||||
/** Controls whether the wheel pans or zooms. */
|
||||
wheelBehavior: 'zoom' | 'pan' | 'none'
|
||||
/** The speed of a scroll wheel / trackpad pan */
|
||||
panSpeed: number
|
||||
/** The speed of a scroll wheel / trackpad zoom */
|
||||
|
@ -29,17 +48,26 @@ export type TLCameraOptions = {
|
|||
isLocked: boolean
|
||||
/** The camera constraints */
|
||||
constraints?: {
|
||||
/** The type of constraint behavior. */
|
||||
fit: 'min' | 'max' | 'x' | 'y' | 'none'
|
||||
/** The behavior for the constraints on the x axis. */
|
||||
fitX: 'contain' | 'inside' | 'outside' | 'lock'
|
||||
/** The behavior for the constraints on the y axis. */
|
||||
fitY: 'contain' | 'inside' | 'outside' | 'lock'
|
||||
/** The bounds of the content (in page space) */
|
||||
/** The bounds (in page space) of the constrained space */
|
||||
bounds: BoxModel
|
||||
/** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */
|
||||
/** The padding inside of the viewport (in screen space) */
|
||||
padding: VecLike
|
||||
/** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/
|
||||
/** The origin for placement. Used to position the bounds within the viewport when an axis is fixed or contained and zoom is below the axis fit. */
|
||||
origin: VecLike
|
||||
/** The camera's initial zoom, used also when the camera is reset. */
|
||||
initialZoom: 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'default'
|
||||
/** The camera's base for its zoom steps. */
|
||||
baseZoom: 'fit-min' | 'fit-max' | 'fit-x' | 'fit-y' | 'default'
|
||||
/** The behavior for the constraints on the x axis. */
|
||||
behavior:
|
||||
| 'free'
|
||||
| 'contain'
|
||||
| 'inside'
|
||||
| 'outside'
|
||||
| 'fixed'
|
||||
| {
|
||||
x: 'contain' | 'inside' | 'outside' | 'fixed' | 'free'
|
||||
y: 'contain' | 'inside' | 'outside' | 'fixed' | 'free'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import { RIGHT_MOUSE_BUTTON } from '../constants'
|
||||
import {
|
||||
preventDefault,
|
||||
releasePointerCapture,
|
||||
|
@ -19,7 +20,7 @@ export function useCanvasEvents() {
|
|||
function onPointerDown(e: React.PointerEvent) {
|
||||
if ((e as any).isKilled) return
|
||||
|
||||
if (e.button === 2) {
|
||||
if (e.button === RIGHT_MOUSE_BUTTON) {
|
||||
editor.dispatch({
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useComputed, useValue } from '@tldraw/state'
|
||||
import { useMemo } from 'react'
|
||||
import { uniq } from '../utils/uniq'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
|
@ -10,17 +9,12 @@ import { useEditor } from './useEditor'
|
|||
*/
|
||||
export function usePeerIds() {
|
||||
const editor = useEditor()
|
||||
const $presences = useMemo(() => {
|
||||
return editor.store.query.records('instance_presence', () => ({
|
||||
userId: { neq: editor.user.getId() },
|
||||
}))
|
||||
}, [editor])
|
||||
|
||||
const $userIds = useComputed(
|
||||
'userIds',
|
||||
() => uniq($presences.get().map((p) => p.userId)).sort(),
|
||||
() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),
|
||||
{ isEqual: (a, b) => a.join(',') === b.join?.(',') },
|
||||
[$presences]
|
||||
[editor]
|
||||
)
|
||||
|
||||
return useValue($userIds)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useValue } from '@tldraw/state'
|
||||
import { TLInstancePresence } from '@tldraw/tlschema'
|
||||
import { useMemo } from 'react'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
// TODO: maybe move this to a computed property on the App class?
|
||||
|
@ -11,21 +10,12 @@ import { useEditor } from './useEditor'
|
|||
export function usePresence(userId: string): TLInstancePresence | null {
|
||||
const editor = useEditor()
|
||||
|
||||
const $presences = useMemo(() => {
|
||||
return editor.store.query.records('instance_presence', () => ({
|
||||
userId: { eq: userId },
|
||||
}))
|
||||
}, [editor, userId])
|
||||
|
||||
const latestPresence = useValue(
|
||||
`latestPresence:${userId}`,
|
||||
() => {
|
||||
return $presences
|
||||
.get()
|
||||
.slice()
|
||||
.sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0]
|
||||
return editor.getCollaborators().find((c) => c.userId === userId)
|
||||
},
|
||||
[]
|
||||
[editor]
|
||||
)
|
||||
|
||||
return latestPresence ?? null
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from 'react'
|
||||
import { RIGHT_MOUSE_BUTTON } from '../constants'
|
||||
import { TLSelectionHandle } from '../editor/types/selection-types'
|
||||
import {
|
||||
loopToHtmlElement,
|
||||
|
@ -18,7 +19,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
|||
const onPointerDown: React.PointerEventHandler = (e) => {
|
||||
if ((e as any).isKilled) return
|
||||
|
||||
if (e.button === 2) {
|
||||
if (e.button === RIGHT_MOUSE_BUTTON) {
|
||||
editor.dispatch({
|
||||
type: 'pointer',
|
||||
target: 'selection',
|
||||
|
|
|
@ -39,12 +39,13 @@ export class Mat {
|
|||
|
||||
equals(m: Mat | MatModel) {
|
||||
return (
|
||||
this.a === m.a &&
|
||||
this.b === m.b &&
|
||||
this.c === m.c &&
|
||||
this.d === m.d &&
|
||||
this.e === m.e &&
|
||||
this.f === m.f
|
||||
this === m ||
|
||||
(this.a === m.a &&
|
||||
this.b === m.b &&
|
||||
this.c === m.c &&
|
||||
this.d === m.d &&
|
||||
this.e === m.e &&
|
||||
this.f === m.f)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HistoryBuffer } from './HistoryBuffer'
|
|||
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
|
||||
import { GLOBAL_START_EPOCH } from './constants'
|
||||
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'
|
||||
import { getGlobalEpoch } from './transactions'
|
||||
import { getGlobalEpoch, getIsReacting } from './transactions'
|
||||
import { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
|
||||
import { logComputedGetterWarning } from './warnings'
|
||||
|
||||
|
@ -189,8 +189,15 @@ class __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff>
|
|||
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {
|
||||
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
|
||||
|
||||
if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) {
|
||||
this.lastCheckedEpoch = getGlobalEpoch()
|
||||
const globalEpoch = getGlobalEpoch()
|
||||
|
||||
if (
|
||||
!isNew &&
|
||||
(this.lastCheckedEpoch === globalEpoch ||
|
||||
(this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < globalEpoch) ||
|
||||
!haveParentsChanged(this))
|
||||
) {
|
||||
this.lastCheckedEpoch = globalEpoch
|
||||
if (this.error) {
|
||||
if (!ignoreErrors) {
|
||||
throw this.error.thrownValue
|
||||
|
|
|
@ -70,6 +70,10 @@ export function getGlobalEpoch() {
|
|||
return inst.globalEpoch
|
||||
}
|
||||
|
||||
export function getIsReacting() {
|
||||
return inst.globalIsReacting
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all of the reactors that need to run for an atom and run them.
|
||||
*
|
||||
|
|
|
@ -2507,9 +2507,7 @@ export function useDefaultHelpers(): {
|
|||
export function useDialogs(): TLUiDialogsContextType;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useEditableText(id: TLShapeId, type: string, text: string, opts?: {
|
||||
disableTab: boolean;
|
||||
}): {
|
||||
export function useEditableText(id: TLShapeId, type: string, text: string): {
|
||||
handleBlur: () => void;
|
||||
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
handleDoubleClick: (e: any) => any;
|
||||
|
|
|
@ -15735,7 +15735,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
||||
"text": ") => {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -15819,7 +15819,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
||||
"text": ") => {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -15894,7 +15894,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
||||
"text": ") => {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -15903,7 +15903,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n props: {\n autoSize: boolean;\n scale?: undefined;\n };\n type: \"text\";\n } | {\n id: import(\"@tldraw/editor\")."
|
||||
"text": ";\n props: {\n autoSize: boolean;\n scale?: undefined;\n };\n type: \"text\";\n } | {\n id: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -27480,14 +27480,6 @@
|
|||
"kind": "Content",
|
||||
"text": "string"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", opts?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n disableTab: boolean;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
|
@ -27575,8 +27567,8 @@
|
|||
],
|
||||
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 9,
|
||||
"endIndex": 26
|
||||
"startIndex": 7,
|
||||
"endIndex": 24
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -27604,14 +27596,6 @@
|
|||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"name": "useEditableText"
|
||||
|
|
|
@ -109,13 +109,10 @@ export function Tldraw(props: TldrawProps) {
|
|||
)
|
||||
|
||||
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
||||
|
||||
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
|
||||
|
||||
if (preloadingError) {
|
||||
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
||||
}
|
||||
|
||||
if (!preloadingComplete) {
|
||||
return <LoadingScreen>Loading assets...</LoadingScreen>
|
||||
}
|
||||
|
|
|
@ -152,7 +152,9 @@ export function registerDefaultExternalContentHandlers(
|
|||
editor.registerExternalContentHandler('svg-text', async ({ point, text }) => {
|
||||
const position =
|
||||
point ??
|
||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
||||
(editor.inputs.shiftKey
|
||||
? editor.inputs.currentPagePoint
|
||||
: editor.getViewportPageBounds().center)
|
||||
|
||||
const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
|
||||
if (!svg) {
|
||||
|
@ -185,7 +187,9 @@ export function registerDefaultExternalContentHandlers(
|
|||
editor.registerExternalContentHandler('embed', ({ point, url, embed }) => {
|
||||
const position =
|
||||
point ??
|
||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
||||
(editor.inputs.shiftKey
|
||||
? editor.inputs.currentPagePoint
|
||||
: editor.getViewportPageBounds().center)
|
||||
|
||||
const { width, height } = embed
|
||||
|
||||
|
@ -210,7 +214,9 @@ export function registerDefaultExternalContentHandlers(
|
|||
editor.registerExternalContentHandler('files', async ({ point, files }) => {
|
||||
const position =
|
||||
point ??
|
||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
||||
(editor.inputs.shiftKey
|
||||
? editor.inputs.currentPagePoint
|
||||
: editor.getViewportPageBounds().center)
|
||||
|
||||
const pagePoint = new Vec(position.x, position.y)
|
||||
|
||||
|
@ -266,7 +272,9 @@ export function registerDefaultExternalContentHandlers(
|
|||
editor.registerExternalContentHandler('text', async ({ point, text }) => {
|
||||
const p =
|
||||
point ??
|
||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
||||
(editor.inputs.shiftKey
|
||||
? editor.inputs.currentPagePoint
|
||||
: editor.getViewportPageBounds().center)
|
||||
|
||||
const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()
|
||||
|
||||
|
@ -370,7 +378,9 @@ export function registerDefaultExternalContentHandlers(
|
|||
|
||||
const position =
|
||||
point ??
|
||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
||||
(editor.inputs.shiftKey
|
||||
? editor.inputs.currentPagePoint
|
||||
: editor.getViewportPageBounds().center)
|
||||
|
||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
||||
const shape = createEmptyBookmarkShape(editor, url, position)
|
||||
|
|
|
@ -268,14 +268,16 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
|
|||
const debugGeom: Geometry2d[] = []
|
||||
const info = editor.getArrowInfo(shape)!
|
||||
|
||||
const hasStartBinding = shape.props.start.type === 'binding'
|
||||
const hasEndBinding = shape.props.end.type === 'binding'
|
||||
const hasStartArrowhead = info.start.arrowhead !== 'none'
|
||||
const hasEndArrowhead = info.end.arrowhead !== 'none'
|
||||
if (info.isStraight) {
|
||||
const range = getStraightArrowLabelRange(editor, shape, info)
|
||||
let clampedPosition = clamp(
|
||||
shape.props.labelPosition,
|
||||
hasStartArrowhead ? range.start : 0,
|
||||
hasEndArrowhead ? range.end : 1
|
||||
hasStartArrowhead || hasStartBinding ? range.start : 0,
|
||||
hasEndArrowhead || hasEndBinding ? range.end : 1
|
||||
)
|
||||
// This makes the position snap in the middle.
|
||||
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
|
||||
|
@ -285,8 +287,8 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
|
|||
if (range.dbg) debugGeom.push(...range.dbg)
|
||||
let clampedPosition = clamp(
|
||||
shape.props.labelPosition,
|
||||
hasStartArrowhead ? range.start : 0,
|
||||
hasEndArrowhead ? range.end : 1
|
||||
hasStartArrowhead || hasStartBinding ? range.start : 0,
|
||||
hasEndArrowhead || hasEndBinding ? range.end : 1
|
||||
)
|
||||
// This makes the position snap in the middle.
|
||||
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
|
||||
|
|
|
@ -35,7 +35,6 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
|
|||
labelColor={theme[labelColor].solid}
|
||||
textWidth={width}
|
||||
isSelected={isSelected}
|
||||
disableTab
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
}}
|
||||
|
|
|
@ -97,7 +97,7 @@ export class Drawing extends StateNode {
|
|||
this.mergeNextPoint = false
|
||||
}
|
||||
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
|
||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
|
@ -137,7 +137,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.updateShapes()
|
||||
this.updateDrawingShape()
|
||||
}
|
||||
|
||||
override onExit? = () => {
|
||||
|
@ -281,7 +281,7 @@ export class Drawing extends StateNode {
|
|||
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
||||
}
|
||||
|
||||
private updateShapes() {
|
||||
private updateDrawingShape() {
|
||||
const { initialShape } = this
|
||||
const { inputs } = this.editor
|
||||
|
||||
|
|
|
@ -402,7 +402,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
</SVGContainer>
|
||||
{showHtmlContainer && (
|
||||
<HTMLContainer
|
||||
id={shape.id}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
width: shape.props.w,
|
||||
|
@ -421,7 +420,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
text={text}
|
||||
isSelected={isSelected}
|
||||
labelColor={theme[props.labelColor].solid}
|
||||
disableTab
|
||||
wrap
|
||||
/>
|
||||
</HTMLContainer>
|
||||
|
|
|
@ -190,7 +190,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
isNote
|
||||
isSelected={isSelected}
|
||||
labelColor={theme[color].note.text}
|
||||
disableTab
|
||||
wrap
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
|
|
@ -27,7 +27,6 @@ type TextLabelProps = {
|
|||
bounds?: Box
|
||||
isNote?: boolean
|
||||
isSelected: boolean
|
||||
disableTab?: boolean
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
classNamePrefix?: string
|
||||
style?: React.CSSProperties
|
||||
|
@ -51,15 +50,13 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
onKeyDown: handleKeyDownCustom,
|
||||
classNamePrefix,
|
||||
style,
|
||||
disableTab = false,
|
||||
textWidth,
|
||||
textHeight,
|
||||
}: TextLabelProps) {
|
||||
const { rInput, isEmpty, isEditing, isEditingAnything, ...editableTextRest } = useEditableText(
|
||||
id,
|
||||
type,
|
||||
text,
|
||||
{ disableTab }
|
||||
text
|
||||
)
|
||||
|
||||
const [initialText, setInitialText] = useState(text)
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
Vec,
|
||||
VecLike,
|
||||
assert,
|
||||
average,
|
||||
precise,
|
||||
shortAngleDist,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
|
||||
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
||||
import { getStrokePoints } from './getStrokePoints'
|
||||
import { setStrokePointRadii } from './setStrokePointRadii'
|
||||
|
@ -36,17 +28,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
|
||||
const result: StrokePoint[][] = []
|
||||
let currentPartition: StrokePoint[] = [points[0]]
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const prevPoint = points[i - 1]
|
||||
const thisPoint = points[i]
|
||||
const nextPoint = points[i + 1]
|
||||
const prevAngle = Vec.Angle(prevPoint.point, thisPoint.point)
|
||||
const nextAngle = Vec.Angle(thisPoint.point, nextPoint.point)
|
||||
// acuteness is a normalized representation of how acute the angle is.
|
||||
// 1 is an infinitely thin wedge
|
||||
// 0 is a straight line
|
||||
const acuteness = Math.abs(shortAngleDist(prevAngle, nextAngle)) / Math.PI
|
||||
if (acuteness > 0.8) {
|
||||
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
|
||||
let nextV: Vec
|
||||
let dpr: number
|
||||
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
|
||||
for (let i = 1, n = points.length; i < n - 1; i++) {
|
||||
prevPoint = points[i - 1]
|
||||
thisPoint = points[i]
|
||||
nextPoint = points[i + 1]
|
||||
|
||||
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
|
||||
dpr = Vec.Dpr(prevV, nextV)
|
||||
prevV = nextV
|
||||
|
||||
if (dpr < -0.8) {
|
||||
// always treat such acute angles as elbows
|
||||
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
||||
const elbowPoint = {
|
||||
|
@ -59,19 +54,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
continue
|
||||
}
|
||||
currentPartition.push(thisPoint)
|
||||
if (acuteness < 0.25) {
|
||||
// this is not an elbow, bail out
|
||||
|
||||
if (dpr > 0.7) {
|
||||
// Not an elbow
|
||||
continue
|
||||
}
|
||||
|
||||
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
||||
// away from it's neighbors
|
||||
const avgRadius = (prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3
|
||||
const incomingNormalizedDist = Vec.Dist(prevPoint.point, thisPoint.point) / avgRadius
|
||||
const outgoingNormalizedDist = Vec.Dist(thisPoint.point, nextPoint.point) / avgRadius
|
||||
// angular dist is a normalized representation of how far away the point is from it's neighbors
|
||||
// away from it's neighbors, angular dist is a normalized representation of how far away the point is from it's neighbors
|
||||
// (normalized by the radius)
|
||||
const angularDist = incomingNormalizedDist + outgoingNormalizedDist
|
||||
if (angularDist < 1.5) {
|
||||
if (
|
||||
(Vec.Dist2(prevPoint.point, thisPoint.point) + Vec.Dist2(thisPoint.point, nextPoint.point)) /
|
||||
((prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3) ** 2 <
|
||||
1.5
|
||||
) {
|
||||
// if this point is kinda close to its neighbors and it has a reasonably
|
||||
// acute angle, it's probably a hard elbow
|
||||
currentPartition.push(thisPoint)
|
||||
|
@ -89,11 +85,13 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|||
function cleanUpPartition(partition: StrokePoint[]) {
|
||||
// clean up start of partition (remove points that are too close to the start)
|
||||
const startPoint = partition[0]
|
||||
let nextPoint: StrokePoint
|
||||
while (partition.length > 2) {
|
||||
const nextPoint = partition[1]
|
||||
const dist = Vec.Dist(startPoint.point, nextPoint.point)
|
||||
const avgRadius = (startPoint.radius + nextPoint.radius) / 2
|
||||
if (dist < avgRadius * 0.5) {
|
||||
nextPoint = partition[1]
|
||||
if (
|
||||
Vec.Dist2(startPoint.point, nextPoint.point) <
|
||||
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
|
||||
) {
|
||||
partition.splice(1, 1)
|
||||
} else {
|
||||
break
|
||||
|
@ -101,11 +99,13 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
|||
}
|
||||
// clean up end of partition in the same fashion
|
||||
const endPoint = partition[partition.length - 1]
|
||||
let prevPoint: StrokePoint
|
||||
while (partition.length > 2) {
|
||||
const prevPoint = partition[partition.length - 2]
|
||||
const dist = Vec.Dist(endPoint.point, prevPoint.point)
|
||||
const avgRadius = (endPoint.radius + prevPoint.radius) / 2
|
||||
if (dist < avgRadius * 0.5) {
|
||||
prevPoint = partition[partition.length - 2]
|
||||
if (
|
||||
Vec.Dist2(endPoint.point, prevPoint.point) <
|
||||
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
|
||||
) {
|
||||
partition.splice(partition.length - 2, 1)
|
||||
} else {
|
||||
break
|
||||
|
@ -115,13 +115,14 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
|||
if (partition.length > 1) {
|
||||
partition[0] = {
|
||||
...partition[0],
|
||||
vector: Vec.FromAngle(Vec.Angle(partition[1].point, partition[0].point)),
|
||||
vector: Vec.Sub(partition[0].point, partition[1].point).uni(),
|
||||
}
|
||||
partition[partition.length - 1] = {
|
||||
...partition[partition.length - 1],
|
||||
vector: Vec.FromAngle(
|
||||
Vec.Angle(partition[partition.length - 1].point, partition[partition.length - 2].point)
|
||||
),
|
||||
vector: Vec.Sub(
|
||||
partition[partition.length - 2].point,
|
||||
partition[partition.length - 1].point
|
||||
).uni(),
|
||||
}
|
||||
}
|
||||
return partition
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
TLShapeId,
|
||||
TLUnknownShape,
|
||||
getPointerInfo,
|
||||
preventDefault,
|
||||
stopEventPropagation,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -11,31 +10,14 @@ import React, { useCallback, useEffect, useRef } from 'react'
|
|||
import { INDENT, TextHelpers } from './TextHelpers'
|
||||
|
||||
/** @public */
|
||||
export function useEditableText(
|
||||
id: TLShapeId,
|
||||
type: string,
|
||||
text: string,
|
||||
opts = { disableTab: false } as { disableTab: boolean }
|
||||
) {
|
||||
export function useEditableText(id: TLShapeId, type: string, text: string) {
|
||||
const editor = useEditor()
|
||||
|
||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const isEditing = useValue(
|
||||
'isEditing',
|
||||
() => {
|
||||
return editor.getEditingShapeId() === id
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
const isEditingAnything = useValue(
|
||||
'isEditingAnything',
|
||||
() => {
|
||||
return editor.getEditingShapeId() !== null
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
const rSelectionRanges = useRef<Range[] | null>()
|
||||
const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor])
|
||||
const isEditingAnything = useValue('isEditingAnything', () => !!editor.getEditingShapeId(), [
|
||||
editor,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
function selectAllIfEditing({ shapeId }: { shapeId: TLShapeId }) {
|
||||
|
@ -52,14 +34,13 @@ export function useEditableText(
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
editor.on('select-all-text', selectAllIfEditing)
|
||||
return () => {
|
||||
editor.off('select-all-text', selectAllIfEditing)
|
||||
}
|
||||
}, [editor, id])
|
||||
|
||||
const rSelectionRanges = useRef<Range[] | null>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) return
|
||||
|
||||
|
@ -69,10 +50,18 @@ export function useEditableText(
|
|||
// Focus if we're not already focused
|
||||
if (document.activeElement !== elm) {
|
||||
elm.focus()
|
||||
|
||||
// On mobile etc, just select all the text when we start focusing
|
||||
if (editor.getInstanceState().isCoarsePointer) {
|
||||
elm.select()
|
||||
}
|
||||
} else {
|
||||
// This fixes iOS not showing the cursor sometimes. This "shakes" the cursor
|
||||
// awake.
|
||||
if (editor.environment.isSafari) {
|
||||
elm.blur()
|
||||
elm.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// When the selection changes, save the selection ranges
|
||||
|
@ -103,12 +92,14 @@ export function useEditableText(
|
|||
requestAnimationFrame(() => {
|
||||
const elm = rInput.current
|
||||
const editingShapeId = editor.getEditingShapeId()
|
||||
|
||||
// Did we move to a different shape?
|
||||
if (editingShapeId) {
|
||||
// important! these ^v are two different things
|
||||
// is that shape OUR shape?
|
||||
if (elm && editingShapeId === id) {
|
||||
elm.focus()
|
||||
|
||||
if (ranges && ranges.length) {
|
||||
const selection = window.getSelection()
|
||||
if (selection) {
|
||||
|
@ -134,20 +125,9 @@ export function useEditableText(
|
|||
}
|
||||
break
|
||||
}
|
||||
case 'Tab': {
|
||||
if (!opts.disableTab) {
|
||||
preventDefault(e)
|
||||
if (e.shiftKey) {
|
||||
TextHelpers.unindent(e.currentTarget)
|
||||
} else {
|
||||
TextHelpers.indent(e.currentTarget)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, id, opts.disableTab]
|
||||
[editor, id]
|
||||
)
|
||||
|
||||
// When the text changes, update the text value.
|
||||
|
@ -198,8 +178,6 @@ export function useEditableText(
|
|||
[editor, id, isEditing]
|
||||
)
|
||||
|
||||
const handleDoubleClick = stopEventPropagation
|
||||
|
||||
return {
|
||||
rInput,
|
||||
handleFocus: noop,
|
||||
|
@ -207,7 +185,7 @@ export function useEditableText(
|
|||
handleKeyDown,
|
||||
handleChange,
|
||||
handleInputPointerDown,
|
||||
handleDoubleClick,
|
||||
handleDoubleClick: stopEventPropagation,
|
||||
isEmpty: text.trim().length === 0,
|
||||
isEditing,
|
||||
isEditingAnything,
|
||||
|
|
|
@ -7,18 +7,22 @@ import {
|
|||
SvgExportContext,
|
||||
TLOnEditEndHandler,
|
||||
TLOnResizeHandler,
|
||||
TLShapeId,
|
||||
TLShapeUtilFlag,
|
||||
TLTextShape,
|
||||
Vec,
|
||||
WeakMapCache,
|
||||
getDefaultColorTheme,
|
||||
preventDefault,
|
||||
textShapeMigrations,
|
||||
textShapeProps,
|
||||
toDomPrecision,
|
||||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import { useCallback } from 'react'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||
import { TextHelpers } from '../shared/TextHelpers'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
|
@ -73,6 +77,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
const { width, height } = this.getMinDimensions(shape)
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
const theme = useDefaultColorTheme()
|
||||
const handleKeyDown = useTextShapeKeydownHandler(id)
|
||||
|
||||
return (
|
||||
<TextLabel
|
||||
|
@ -94,6 +99,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
transformOrigin: 'top left',
|
||||
}}
|
||||
wrap
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -332,3 +338,32 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
|||
height: Math.max(fontSize, result.h),
|
||||
}
|
||||
}
|
||||
|
||||
function useTextShapeKeydownHandler(id: TLShapeId) {
|
||||
const editor = useEditor()
|
||||
|
||||
return useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (editor.getEditingShapeId() !== id) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter': {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
editor.complete()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Tab': {
|
||||
preventDefault(e)
|
||||
if (e.shiftKey) {
|
||||
TextHelpers.unindent(e.currentTarget)
|
||||
} else {
|
||||
TextHelpers.indent(e.currentTarget)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -92,8 +92,8 @@ export class PointingArrowLabel extends StateNode {
|
|||
let nextLabelPosition
|
||||
if (info.isStraight) {
|
||||
// straight arrows
|
||||
const lineLength = Vec.Dist2(info.start.point, info.end.point)
|
||||
const segmentLength = Vec.Dist2(info.end.point, nearestPoint)
|
||||
const lineLength = Vec.Dist(info.start.point, info.end.point)
|
||||
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
|
||||
nextLabelPosition = 1 - segmentLength / lineLength
|
||||
} else {
|
||||
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
||||
|
|
|
@ -117,7 +117,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
|||
editor.putExternalContent({
|
||||
type: 'embed',
|
||||
url,
|
||||
point: editor.getViewportPageCenter(),
|
||||
point: editor.getViewportPageBounds().center,
|
||||
embed: embedInfoForUrl.definition,
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEditor } from '@tldraw/editor'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEditor, useQuickReactor } from '@tldraw/editor'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useActions } from '../../context/actions'
|
||||
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
||||
|
||||
|
@ -9,33 +9,25 @@ export function BackToContent() {
|
|||
const actions = useActions()
|
||||
|
||||
const [showBackToContent, setShowBackToContent] = useState(false)
|
||||
const rIsShowing = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
let showBackToContentPrev = false
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const renderingShapes = editor.getRenderingShapes()
|
||||
const renderingBounds = editor.getRenderingBounds()
|
||||
|
||||
// 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
|
||||
useQuickReactor(
|
||||
'toggle showback to content',
|
||||
() => {
|
||||
const showBackToContentPrev = rIsShowing.current
|
||||
const shapeIds = editor.getCurrentPageShapeIds()
|
||||
let showBackToContentNow = false
|
||||
if (shapeIds.size) {
|
||||
showBackToContentNow = shapeIds.size === editor.getCulledShapes().size
|
||||
}
|
||||
|
||||
if (showBackToContentPrev !== showBackToContentNow) {
|
||||
setShowBackToContent(showBackToContentNow)
|
||||
showBackToContentPrev = showBackToContentNow
|
||||
rIsShowing.current = showBackToContentNow
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [editor])
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
if (!showBackToContent) return null
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
Box,
|
||||
TLPointerEventInfo,
|
||||
TLShapeId,
|
||||
Vec,
|
||||
getPointerInfo,
|
||||
intersectPolygonPolygon,
|
||||
normalizeWheel,
|
||||
releasePointerCapture,
|
||||
setPointerCapture,
|
||||
useComputed,
|
||||
useEditor,
|
||||
useIsDarkMode,
|
||||
useQuickReactor,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { MinimapManager } from './MinimapManager'
|
||||
|
@ -24,67 +19,78 @@ export function DefaultMinimap() {
|
|||
const rCanvas = React.useRef<HTMLCanvasElement>(null!)
|
||||
const rPointing = React.useRef(false)
|
||||
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const devicePixelRatio = useComputed('dpr', () => editor.getInstanceState().devicePixelRatio, [
|
||||
editor,
|
||||
])
|
||||
const presences = React.useMemo(() => editor.store.query.records('instance_presence'), [editor])
|
||||
|
||||
const minimap = React.useMemo(() => new MinimapManager(editor), [editor])
|
||||
const minimapRef = React.useRef<MinimapManager>()
|
||||
|
||||
React.useEffect(() => {
|
||||
// Must check after render
|
||||
const raf = requestAnimationFrame(() => {
|
||||
minimap.updateColors()
|
||||
minimap.render()
|
||||
})
|
||||
return () => {
|
||||
cancelAnimationFrame(raf)
|
||||
}
|
||||
}, [editor, minimap, isDarkMode])
|
||||
const minimap = new MinimapManager(editor, rCanvas.current)
|
||||
minimapRef.current = minimap
|
||||
return minimapRef.current.close
|
||||
}, [editor])
|
||||
|
||||
const onDoubleClick = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!editor.getCurrentPageShapeIds().size) return
|
||||
if (!minimapRef.current) return
|
||||
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
const point = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
false,
|
||||
false
|
||||
)
|
||||
|
||||
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
|
||||
const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
false,
|
||||
true
|
||||
)
|
||||
|
||||
minimap.originPagePoint.setTo(clampedPoint)
|
||||
minimap.originPageCenter.setTo(editor.getViewportPageBounds().center)
|
||||
minimapRef.current.originPagePoint.setTo(clampedPoint)
|
||||
minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center)
|
||||
|
||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
},
|
||||
[editor, minimap]
|
||||
[editor]
|
||||
)
|
||||
|
||||
const onPointerDown = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
if (!minimapRef.current) return
|
||||
const elm = e.currentTarget
|
||||
setPointerCapture(elm, e)
|
||||
if (!editor.getCurrentPageShapeIds().size) return
|
||||
|
||||
rPointing.current = true
|
||||
|
||||
minimap.isInViewport = false
|
||||
minimapRef.current.isInViewport = false
|
||||
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
|
||||
const point = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
false,
|
||||
false
|
||||
)
|
||||
|
||||
const clampedPoint = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, true)
|
||||
const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
false,
|
||||
true
|
||||
)
|
||||
|
||||
const _vpPageBounds = editor.getViewportPageBounds()
|
||||
|
||||
minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
||||
minimapRef.current.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
||||
|
||||
if (minimap.isInViewport) {
|
||||
minimap.originPagePoint.setTo(clampedPoint)
|
||||
minimap.originPageCenter.setTo(_vpPageBounds.center)
|
||||
if (minimapRef.current.isInViewport) {
|
||||
minimapRef.current.originPagePoint.setTo(clampedPoint)
|
||||
minimapRef.current.originPageCenter.setTo(_vpPageBounds.center)
|
||||
} else {
|
||||
const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point)
|
||||
const pagePoint = Vec.Add(point, delta)
|
||||
minimap.originPagePoint.setTo(pagePoint)
|
||||
minimap.originPageCenter.setTo(point)
|
||||
minimapRef.current.originPagePoint.setTo(pagePoint)
|
||||
minimapRef.current.originPageCenter.setTo(point)
|
||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||
}
|
||||
|
||||
|
@ -98,16 +104,24 @@ export function DefaultMinimap() {
|
|||
|
||||
document.body.addEventListener('pointerup', release)
|
||||
},
|
||||
[editor, minimap]
|
||||
[editor]
|
||||
)
|
||||
|
||||
const onPointerMove = React.useCallback(
|
||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||
const point = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, e.shiftKey, true)
|
||||
if (!minimapRef.current) return
|
||||
const point = minimapRef.current.minimapScreenPointToPagePoint(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
e.shiftKey,
|
||||
true
|
||||
)
|
||||
|
||||
if (rPointing.current) {
|
||||
if (minimap.isInViewport) {
|
||||
const delta = minimap.originPagePoint.clone().sub(minimap.originPageCenter)
|
||||
if (minimapRef.current.isInViewport) {
|
||||
const delta = minimapRef.current.originPagePoint
|
||||
.clone()
|
||||
.sub(minimapRef.current.originPageCenter)
|
||||
editor.centerOnPoint(Vec.Sub(point, delta))
|
||||
return
|
||||
}
|
||||
|
@ -115,7 +129,7 @@ export function DefaultMinimap() {
|
|||
editor.centerOnPoint(point)
|
||||
}
|
||||
|
||||
const pagePoint = minimap.getPagePoint(e.clientX, e.clientY)
|
||||
const pagePoint = minimapRef.current.getPagePoint(e.clientX, e.clientY)
|
||||
|
||||
const screenPoint = editor.pageToScreen(pagePoint)
|
||||
|
||||
|
@ -130,7 +144,7 @@ export function DefaultMinimap() {
|
|||
|
||||
editor.dispatch(info)
|
||||
},
|
||||
[editor, minimap]
|
||||
[editor]
|
||||
)
|
||||
|
||||
const onWheel = React.useCallback(
|
||||
|
@ -150,73 +164,16 @@ export function DefaultMinimap() {
|
|||
[editor]
|
||||
)
|
||||
|
||||
// Update the minimap's dpr when the dpr changes
|
||||
useQuickReactor(
|
||||
'update when dpr changes',
|
||||
() => {
|
||||
const dpr = devicePixelRatio.get()
|
||||
minimap.setDpr(dpr)
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const canvas = rCanvas.current as HTMLCanvasElement
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const width = rect.width * dpr
|
||||
const height = rect.height * dpr
|
||||
|
||||
// These must happen in order
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
minimap.canvasScreenBounds.set(rect.x, rect.y, width, height)
|
||||
|
||||
minimap.cvs = rCanvas.current
|
||||
},
|
||||
[devicePixelRatio, minimap]
|
||||
)
|
||||
|
||||
useQuickReactor(
|
||||
'minimap render when pagebounds or collaborators changes',
|
||||
() => {
|
||||
const shapeIdsOnCurrentPage = editor.getCurrentPageShapeIds()
|
||||
const commonBoundsOfAllShapesOnCurrentPage = editor.getCurrentPageBounds()
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
|
||||
const _dpr = devicePixelRatio.get() // dereference
|
||||
|
||||
minimap.contentPageBounds = commonBoundsOfAllShapesOnCurrentPage
|
||||
? Box.Expand(commonBoundsOfAllShapesOnCurrentPage, viewportPageBounds)
|
||||
: viewportPageBounds
|
||||
|
||||
minimap.updateContentScreenBounds()
|
||||
|
||||
// All shape bounds
|
||||
|
||||
const allShapeBounds = [] as (Box & { id: TLShapeId })[]
|
||||
|
||||
shapeIdsOnCurrentPage.forEach((id) => {
|
||||
let pageBounds = editor.getShapePageBounds(id) as Box & { id: TLShapeId }
|
||||
if (!pageBounds) return
|
||||
|
||||
const pageMask = editor.getShapeMask(id)
|
||||
|
||||
if (pageMask) {
|
||||
const intersection = intersectPolygonPolygon(pageMask, pageBounds.corners)
|
||||
if (!intersection) {
|
||||
return
|
||||
}
|
||||
pageBounds = Box.FromPoints(intersection) as Box & { id: TLShapeId }
|
||||
}
|
||||
|
||||
if (pageBounds) {
|
||||
pageBounds.id = id // kinda dirty but we want to include the id here
|
||||
allShapeBounds.push(pageBounds)
|
||||
}
|
||||
})
|
||||
|
||||
minimap.pageBounds = allShapeBounds
|
||||
minimap.collaborators = presences.get()
|
||||
minimap.render()
|
||||
},
|
||||
[editor, minimap]
|
||||
)
|
||||
React.useEffect(() => {
|
||||
// need to wait a tick for next theme css to be applied
|
||||
// otherwise the minimap will render with the wrong colors
|
||||
setTimeout(() => {
|
||||
minimapRef.current?.updateColors()
|
||||
minimapRef.current?.render()
|
||||
})
|
||||
}, [isDarkMode])
|
||||
|
||||
return (
|
||||
<div className="tlui-minimap">
|
||||
|
|
|
@ -1,114 +1,159 @@
|
|||
import {
|
||||
Box,
|
||||
ComputedCache,
|
||||
Editor,
|
||||
PI2,
|
||||
TLInstancePresence,
|
||||
TLShapeId,
|
||||
TLShape,
|
||||
Vec,
|
||||
atom,
|
||||
clamp,
|
||||
computed,
|
||||
react,
|
||||
uniqueId,
|
||||
} from '@tldraw/editor'
|
||||
import { getRgba } from './getRgba'
|
||||
import { BufferStuff, appendVertices, setupWebGl } from './minimap-webgl-setup'
|
||||
import { pie, rectangle, roundedRectangle } from './minimap-webgl-shapes'
|
||||
|
||||
export class MinimapManager {
|
||||
constructor(public editor: Editor) {}
|
||||
|
||||
dpr = 1
|
||||
|
||||
colors = {
|
||||
shapeFill: 'rgba(144, 144, 144, .1)',
|
||||
selectFill: '#2f80ed',
|
||||
viewportFill: 'rgba(144, 144, 144, .1)',
|
||||
disposables = [] as (() => void)[]
|
||||
close = () => this.disposables.forEach((d) => d())
|
||||
gl: ReturnType<typeof setupWebGl>
|
||||
shapeGeometryCache: ComputedCache<Float32Array | null, TLShape>
|
||||
constructor(
|
||||
public editor: Editor,
|
||||
public readonly elem: HTMLCanvasElement
|
||||
) {
|
||||
this.gl = setupWebGl(elem)
|
||||
this.shapeGeometryCache = editor.store.createComputedCache('webgl-geometry', (r: TLShape) => {
|
||||
const bounds = editor.getShapeMaskedPageBounds(r.id)
|
||||
if (!bounds) return null
|
||||
const arr = new Float32Array(12)
|
||||
rectangle(arr, 0, bounds.x, bounds.y, bounds.w, bounds.h)
|
||||
return arr
|
||||
})
|
||||
this.colors = this._getColors()
|
||||
this.disposables.push(this._listenForCanvasResize(), react('minimap render', this.render))
|
||||
}
|
||||
|
||||
id = uniqueId()
|
||||
cvs: HTMLCanvasElement | null = null
|
||||
pageBounds: (Box & { id: TLShapeId })[] = []
|
||||
collaborators: TLInstancePresence[] = []
|
||||
private _getColors() {
|
||||
const style = getComputedStyle(this.editor.getContainer())
|
||||
|
||||
canvasScreenBounds = new Box()
|
||||
canvasPageBounds = new Box()
|
||||
return {
|
||||
shapeFill: getRgba(style.getPropertyValue('--color-text-3').trim()),
|
||||
selectFill: getRgba(style.getPropertyValue('--color-selected').trim()),
|
||||
viewportFill: getRgba(style.getPropertyValue('--color-muted-1').trim()),
|
||||
}
|
||||
}
|
||||
|
||||
contentPageBounds = new Box()
|
||||
contentScreenBounds = new Box()
|
||||
private colors: ReturnType<MinimapManager['_getColors']>
|
||||
// this should be called after dark/light mode changes have propagated to the dom
|
||||
updateColors() {
|
||||
this.colors = this._getColors()
|
||||
}
|
||||
|
||||
readonly id = uniqueId()
|
||||
@computed
|
||||
getDpr() {
|
||||
return this.editor.getInstanceState().devicePixelRatio
|
||||
}
|
||||
|
||||
@computed
|
||||
getContentPageBounds() {
|
||||
const viewportPageBounds = this.editor.getViewportPageBounds()
|
||||
const commonShapeBounds = this.editor.getCurrentPageBounds()
|
||||
return commonShapeBounds
|
||||
? Box.Expand(commonShapeBounds, viewportPageBounds)
|
||||
: viewportPageBounds
|
||||
}
|
||||
|
||||
@computed
|
||||
getContentScreenBounds() {
|
||||
const contentPageBounds = this.getContentPageBounds()
|
||||
const topLeft = this.editor.pageToScreen(contentPageBounds.point)
|
||||
const bottomRight = this.editor.pageToScreen(
|
||||
new Vec(contentPageBounds.maxX, contentPageBounds.maxY)
|
||||
)
|
||||
return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
|
||||
}
|
||||
|
||||
private _getCanvasBoundingRect() {
|
||||
const { x, y, width, height } = this.elem.getBoundingClientRect()
|
||||
return new Box(x, y, width, height)
|
||||
}
|
||||
|
||||
private readonly canvasBoundingClientRect = atom('canvasBoundingClientRect', new Box())
|
||||
|
||||
getCanvasScreenBounds() {
|
||||
return this.canvasBoundingClientRect.get()
|
||||
}
|
||||
|
||||
private _listenForCanvasResize() {
|
||||
const observer = new ResizeObserver(() => {
|
||||
const rect = this._getCanvasBoundingRect()
|
||||
this.canvasBoundingClientRect.set(rect)
|
||||
})
|
||||
observer.observe(this.elem)
|
||||
return () => observer.disconnect()
|
||||
}
|
||||
|
||||
@computed
|
||||
getCanvasSize() {
|
||||
const rect = this.canvasBoundingClientRect.get()
|
||||
const dpr = this.getDpr()
|
||||
return new Vec(rect.width * dpr, rect.height * dpr)
|
||||
}
|
||||
|
||||
@computed
|
||||
getCanvasClientPosition() {
|
||||
return this.canvasBoundingClientRect.get().point
|
||||
}
|
||||
|
||||
originPagePoint = new Vec()
|
||||
originPageCenter = new Vec()
|
||||
|
||||
isInViewport = false
|
||||
|
||||
debug = false
|
||||
/** Get the canvas's true bounds converted to page bounds. */
|
||||
@computed getCanvasPageBounds() {
|
||||
const canvasScreenBounds = this.getCanvasScreenBounds()
|
||||
const contentPageBounds = this.getContentPageBounds()
|
||||
|
||||
setDpr(dpr: number) {
|
||||
this.dpr = +dpr.toFixed(2)
|
||||
}
|
||||
const aspectRatio = canvasScreenBounds.width / canvasScreenBounds.height
|
||||
|
||||
updateContentScreenBounds = () => {
|
||||
const { contentScreenBounds, contentPageBounds: content, canvasScreenBounds: canvas } = this
|
||||
|
||||
let { x, y, w, h } = contentScreenBounds
|
||||
|
||||
if (content.w > content.h) {
|
||||
const sh = canvas.w / (content.w / content.h)
|
||||
if (sh > canvas.h) {
|
||||
x = (canvas.w - canvas.w * (canvas.h / sh)) / 2
|
||||
y = 0
|
||||
w = canvas.w * (canvas.h / sh)
|
||||
h = canvas.h
|
||||
} else {
|
||||
x = 0
|
||||
y = (canvas.h - sh) / 2
|
||||
w = canvas.w
|
||||
h = sh
|
||||
}
|
||||
} else if (content.w < content.h) {
|
||||
const sw = canvas.h / (content.h / content.w)
|
||||
x = (canvas.w - sw) / 2
|
||||
y = 0
|
||||
w = sw
|
||||
h = canvas.h
|
||||
} else {
|
||||
x = canvas.h / 2
|
||||
y = 0
|
||||
w = canvas.h
|
||||
h = canvas.h
|
||||
let targetWidth = contentPageBounds.width
|
||||
let targetHeight = targetWidth / aspectRatio
|
||||
if (targetHeight < contentPageBounds.height) {
|
||||
targetHeight = contentPageBounds.height
|
||||
targetWidth = targetHeight * aspectRatio
|
||||
}
|
||||
|
||||
contentScreenBounds.set(x, y, w, h)
|
||||
const box = new Box(0, 0, targetWidth, targetHeight)
|
||||
box.center = contentPageBounds.center
|
||||
return box
|
||||
}
|
||||
|
||||
/** Get the canvas's true bounds converted to page bounds. */
|
||||
updateCanvasPageBounds = () => {
|
||||
const { canvasPageBounds, canvasScreenBounds, contentPageBounds, contentScreenBounds } = this
|
||||
|
||||
canvasPageBounds.set(
|
||||
0,
|
||||
0,
|
||||
contentPageBounds.width / (contentScreenBounds.width / canvasScreenBounds.width),
|
||||
contentPageBounds.height / (contentScreenBounds.height / canvasScreenBounds.height)
|
||||
)
|
||||
|
||||
canvasPageBounds.center = contentPageBounds.center
|
||||
@computed getCanvasPageBoundsArray() {
|
||||
const { x, y, w, h } = this.getCanvasPageBounds()
|
||||
return new Float32Array([x, y, w, h])
|
||||
}
|
||||
|
||||
getScreenPoint = (x: number, y: number) => {
|
||||
const { canvasScreenBounds } = this
|
||||
getPagePoint = (clientX: number, clientY: number) => {
|
||||
const canvasPageBounds = this.getCanvasPageBounds()
|
||||
const canvasScreenBounds = this.getCanvasScreenBounds()
|
||||
|
||||
const screenX = (x - canvasScreenBounds.minX) * this.dpr
|
||||
const screenY = (y - canvasScreenBounds.minY) * this.dpr
|
||||
// first offset the canvas position
|
||||
let x = clientX - canvasScreenBounds.x
|
||||
let y = clientY - canvasScreenBounds.y
|
||||
|
||||
return { x: screenX, y: screenY }
|
||||
}
|
||||
// then multiply by the ratio between the page and screen bounds
|
||||
x *= canvasPageBounds.width / canvasScreenBounds.width
|
||||
y *= canvasPageBounds.height / canvasScreenBounds.height
|
||||
|
||||
getPagePoint = (x: number, y: number) => {
|
||||
const { contentPageBounds, contentScreenBounds, canvasPageBounds } = this
|
||||
// then add the canvas page bounds' offset
|
||||
x += canvasPageBounds.minX
|
||||
y += canvasPageBounds.minY
|
||||
|
||||
const { x: screenX, y: screenY } = this.getScreenPoint(x, y)
|
||||
|
||||
return new Vec(
|
||||
canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
|
||||
canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
|
||||
1
|
||||
)
|
||||
return new Vec(x, y, 1)
|
||||
}
|
||||
|
||||
minimapScreenPointToPagePoint = (
|
||||
|
@ -123,13 +168,13 @@ export class MinimapManager {
|
|||
let { x: px, y: py } = this.getPagePoint(x, y)
|
||||
|
||||
if (clampToBounds) {
|
||||
const shapesPageBounds = this.editor.getCurrentPageBounds()
|
||||
const shapesPageBounds = this.editor.getCurrentPageBounds() ?? new Box()
|
||||
const vpPageBounds = viewportPageBounds
|
||||
|
||||
const minX = (shapesPageBounds?.minX ?? 0) - vpPageBounds.width / 2
|
||||
const maxX = (shapesPageBounds?.maxX ?? 0) + vpPageBounds.width / 2
|
||||
const minY = (shapesPageBounds?.minY ?? 0) - vpPageBounds.height / 2
|
||||
const maxY = (shapesPageBounds?.maxY ?? 0) + vpPageBounds.height / 2
|
||||
const minX = shapesPageBounds.minX - vpPageBounds.width / 2
|
||||
const maxX = shapesPageBounds.maxX + vpPageBounds.width / 2
|
||||
const minY = shapesPageBounds.minY - vpPageBounds.height / 2
|
||||
const maxY = shapesPageBounds.maxY + vpPageBounds.height / 2
|
||||
|
||||
const lx = Math.max(0, minX + vpPageBounds.width - px)
|
||||
const rx = Math.max(0, -(maxX - vpPageBounds.width - px))
|
||||
|
@ -171,209 +216,110 @@ export class MinimapManager {
|
|||
return new Vec(px, py)
|
||||
}
|
||||
|
||||
updateColors = () => {
|
||||
const style = getComputedStyle(this.editor.getContainer())
|
||||
|
||||
this.colors = {
|
||||
shapeFill: style.getPropertyValue('--color-text-3').trim(),
|
||||
selectFill: style.getPropertyValue('--color-selected').trim(),
|
||||
viewportFill: style.getPropertyValue('--color-muted-1').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { cvs, pageBounds } = this
|
||||
this.updateCanvasPageBounds()
|
||||
// make sure we update when dark mode switches
|
||||
const context = this.gl.context
|
||||
const canvasSize = this.getCanvasSize()
|
||||
|
||||
const { editor, canvasScreenBounds, canvasPageBounds, contentPageBounds, contentScreenBounds } =
|
||||
this
|
||||
const { width: cw, height: ch } = canvasScreenBounds
|
||||
this.gl.setCanvasPageBounds(this.getCanvasPageBoundsArray())
|
||||
|
||||
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
this.elem.width = canvasSize.x
|
||||
this.elem.height = canvasSize.y
|
||||
context.viewport(0, 0, canvasSize.x, canvasSize.y)
|
||||
|
||||
if (!cvs || !pageBounds) {
|
||||
return
|
||||
// this affects which color transparent shapes are blended with
|
||||
// during rendering. If we were to invert this any shapes narrower
|
||||
// than 1 px in screen space would have much lower contrast. e.g.
|
||||
// draw shapes on a large canvas.
|
||||
if (this.editor.user.getIsDarkMode()) {
|
||||
context.clearColor(1, 1, 1, 0)
|
||||
} else {
|
||||
context.clearColor(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
const ctx = cvs.getContext('2d')!
|
||||
context.clear(context.COLOR_BUFFER_BIT)
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Minimap (shapes): Could not get context')
|
||||
}
|
||||
const selectedShapes = new Set(this.editor.getSelectedShapeIds())
|
||||
|
||||
ctx.resetTransform()
|
||||
ctx.globalAlpha = 1
|
||||
ctx.clearRect(0, 0, cw, ch)
|
||||
const colors = this.colors
|
||||
let selectedShapeOffset = 0
|
||||
let unselectedShapeOffset = 0
|
||||
|
||||
// Transform canvas
|
||||
const ids = this.editor.getCurrentPageShapeIdsSorted()
|
||||
|
||||
const sx = contentScreenBounds.width / contentPageBounds.width
|
||||
const sy = contentScreenBounds.height / contentPageBounds.height
|
||||
for (let i = 0, len = ids.length; i < len; i++) {
|
||||
const shapeId = ids[i]
|
||||
const geometry = this.shapeGeometryCache.get(shapeId)
|
||||
if (!geometry) continue
|
||||
|
||||
ctx.translate((cw - contentScreenBounds.width) / 2, (ch - contentScreenBounds.height) / 2)
|
||||
ctx.scale(sx, sy)
|
||||
ctx.translate(-contentPageBounds.minX, -contentPageBounds.minY)
|
||||
const len = geometry.length
|
||||
|
||||
// shapes
|
||||
const shapesPath = new Path2D()
|
||||
const selectedPath = new Path2D()
|
||||
|
||||
const { shapeFill, selectFill, viewportFill } = this.colors
|
||||
|
||||
// When there are many shapes, don't draw rounded rectangles;
|
||||
// consider using the shape's size instead.
|
||||
|
||||
let pb: Box & { id: TLShapeId }
|
||||
for (let i = 0, n = pageBounds.length; i < n; i++) {
|
||||
pb = pageBounds[i]
|
||||
;(selectedShapeIds.has(pb.id) ? selectedPath : shapesPath).rect(
|
||||
pb.minX,
|
||||
pb.minY,
|
||||
pb.width,
|
||||
pb.height
|
||||
)
|
||||
}
|
||||
|
||||
// Fill the shapes paths
|
||||
ctx.fillStyle = shapeFill
|
||||
ctx.fill(shapesPath)
|
||||
|
||||
// Fill the selected paths
|
||||
ctx.fillStyle = selectFill
|
||||
ctx.fill(selectedPath)
|
||||
|
||||
if (this.debug) {
|
||||
// Page bounds
|
||||
const commonBounds = Box.Common(pageBounds)
|
||||
const { minX, minY, width, height } = commonBounds
|
||||
ctx.strokeStyle = 'green'
|
||||
ctx.lineWidth = 2 / sx
|
||||
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
|
||||
}
|
||||
|
||||
// Brush
|
||||
{
|
||||
const { brush } = editor.getInstanceState()
|
||||
if (brush) {
|
||||
const { x, y, w, h } = brush
|
||||
ctx.beginPath()
|
||||
MinimapManager.sharpRect(ctx, x, y, w, h)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = viewportFill
|
||||
ctx.fill()
|
||||
if (selectedShapes.has(shapeId)) {
|
||||
appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry)
|
||||
selectedShapeOffset += len
|
||||
} else {
|
||||
appendVertices(this.gl.unselectedShapes, unselectedShapeOffset, geometry)
|
||||
unselectedShapeOffset += len
|
||||
}
|
||||
}
|
||||
|
||||
// Viewport
|
||||
{
|
||||
const { minX, minY, width, height } = viewportPageBounds
|
||||
|
||||
ctx.beginPath()
|
||||
|
||||
const rx = 12 / sx
|
||||
const ry = 12 / sx
|
||||
MinimapManager.roundedRect(
|
||||
ctx,
|
||||
minX,
|
||||
minY,
|
||||
width,
|
||||
height,
|
||||
Math.min(width / 4, rx),
|
||||
Math.min(height / 4, ry)
|
||||
)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = viewportFill
|
||||
ctx.fill()
|
||||
|
||||
if (this.debug) {
|
||||
ctx.strokeStyle = 'orange'
|
||||
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
|
||||
}
|
||||
}
|
||||
|
||||
// Show collaborator cursors
|
||||
|
||||
// Padding for canvas bounds edges
|
||||
const px = 2.5 / sx
|
||||
const py = 2.5 / sy
|
||||
|
||||
const currentPageId = editor.getCurrentPageId()
|
||||
|
||||
let collaborator: TLInstancePresence
|
||||
for (let i = 0; i < this.collaborators.length; i++) {
|
||||
collaborator = this.collaborators[i]
|
||||
if (collaborator.currentPageId !== currentPageId) {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.ellipse(
|
||||
clamp(collaborator.cursor.x, canvasPageBounds.minX + px, canvasPageBounds.maxX - px),
|
||||
clamp(collaborator.cursor.y, canvasPageBounds.minY + py, canvasPageBounds.maxY - py),
|
||||
5 / sx,
|
||||
5 / sy,
|
||||
0,
|
||||
0,
|
||||
PI2
|
||||
)
|
||||
ctx.fillStyle = collaborator.color
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
ctx.lineWidth = 2 / sx
|
||||
|
||||
{
|
||||
// Minimap Bounds
|
||||
const { minX, minY, width, height } = contentPageBounds
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
|
||||
}
|
||||
|
||||
{
|
||||
// Canvas Bounds
|
||||
const { minX, minY, width, height } = canvasPageBounds
|
||||
ctx.strokeStyle = 'blue'
|
||||
ctx.strokeRect(minX + 1 / sx, minY + 1 / sy, width - 2 / sx, height - 2 / sy)
|
||||
}
|
||||
}
|
||||
this.drawViewport()
|
||||
this.drawShapes(this.gl.unselectedShapes, unselectedShapeOffset, colors.shapeFill)
|
||||
this.drawShapes(this.gl.selectedShapes, selectedShapeOffset, colors.selectFill)
|
||||
this.drawCollaborators()
|
||||
}
|
||||
|
||||
static roundedRect(
|
||||
ctx: CanvasRenderingContext2D | Path2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
rx: number,
|
||||
ry: number
|
||||
) {
|
||||
if (rx < 1 && ry < 1) {
|
||||
ctx.rect(x, y, width, height)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.moveTo(x + rx, y)
|
||||
ctx.lineTo(x + width - rx, y)
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + ry)
|
||||
ctx.lineTo(x + width, y + height - ry)
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height)
|
||||
ctx.lineTo(x + rx, y + height)
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - ry)
|
||||
ctx.lineTo(x, y + ry)
|
||||
ctx.quadraticCurveTo(x, y, x + rx, y)
|
||||
private drawShapes(stuff: BufferStuff, len: number, color: Float32Array) {
|
||||
this.gl.prepareTriangles(stuff, len)
|
||||
this.gl.setFillColor(color)
|
||||
this.gl.drawTriangles(len)
|
||||
}
|
||||
|
||||
static sharpRect(
|
||||
ctx: CanvasRenderingContext2D | Path2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
_rx?: number,
|
||||
_ry?: number
|
||||
) {
|
||||
ctx.rect(x, y, width, height)
|
||||
private drawViewport() {
|
||||
const viewport = this.editor.getViewportPageBounds()
|
||||
const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
|
||||
const len = roundedRectangle(this.gl.viewport.vertices, viewport, 4 * zoom)
|
||||
|
||||
this.gl.prepareTriangles(this.gl.viewport, len)
|
||||
this.gl.setFillColor(this.colors.viewportFill)
|
||||
this.gl.drawTriangles(len)
|
||||
}
|
||||
|
||||
drawCollaborators() {
|
||||
const collaborators = this.editor.getCollaboratorsOnCurrentPage()
|
||||
if (!collaborators.length) return
|
||||
|
||||
const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
|
||||
|
||||
// just draw a little circle for each collaborator
|
||||
const numSegmentsPerCircle = 20
|
||||
const dataSizePerCircle = numSegmentsPerCircle * 6
|
||||
const totalSize = dataSizePerCircle * collaborators.length
|
||||
|
||||
// expand vertex array if needed
|
||||
if (this.gl.collaborators.vertices.length < totalSize) {
|
||||
this.gl.collaborators.vertices = new Float32Array(totalSize)
|
||||
}
|
||||
|
||||
const vertices = this.gl.collaborators.vertices
|
||||
let offset = 0
|
||||
for (const { cursor } of collaborators) {
|
||||
pie(vertices, {
|
||||
center: Vec.From(cursor),
|
||||
radius: 2 * zoom,
|
||||
offset,
|
||||
numArcSegments: numSegmentsPerCircle,
|
||||
})
|
||||
offset += dataSizePerCircle
|
||||
}
|
||||
|
||||
this.gl.prepareTriangles(this.gl.collaborators, totalSize)
|
||||
|
||||
offset = 0
|
||||
for (const { color } of collaborators) {
|
||||
this.gl.setFillColor(getRgba(color))
|
||||
this.gl.context.drawArrays(this.gl.context.TRIANGLES, offset / 2, dataSizePerCircle / 2)
|
||||
offset += dataSizePerCircle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
const memo = {} as Record<string, Float32Array>
|
||||
|
||||
export function getRgba(colorString: string) {
|
||||
if (memo[colorString]) {
|
||||
return memo[colorString]
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
context!.fillStyle = colorString
|
||||
context!.fillRect(0, 0, 1, 1)
|
||||
const [r, g, b, a] = context!.getImageData(0, 0, 1, 1).data
|
||||
const result = new Float32Array([r / 255, g / 255, b / 255, a / 255])
|
||||
|
||||
memo[colorString] = result
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import { roundedRectangleDataSize } from './minimap-webgl-shapes'
|
||||
|
||||
export function setupWebGl(canvas: HTMLCanvasElement | null) {
|
||||
if (!canvas) throw new Error('Canvas element not found')
|
||||
|
||||
const context = canvas.getContext('webgl2', {
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
if (!context) throw new Error('Failed to get webgl2 context')
|
||||
|
||||
const vertexShaderSourceCode = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
in vec2 shapeVertexPosition;
|
||||
|
||||
uniform vec4 canvasPageBounds;
|
||||
|
||||
// taken (with thanks) from
|
||||
// https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
|
||||
void main() {
|
||||
// convert the position from pixels to 0.0 to 1.0
|
||||
vec2 zeroToOne = (shapeVertexPosition - canvasPageBounds.xy) / canvasPageBounds.zw;
|
||||
|
||||
// convert from 0->1 to 0->2
|
||||
vec2 zeroToTwo = zeroToOne * 2.0;
|
||||
|
||||
// convert from 0->2 to -1->+1 (clipspace)
|
||||
vec2 clipSpace = zeroToTwo - 1.0;
|
||||
|
||||
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
|
||||
}`
|
||||
|
||||
const vertexShader = context.createShader(context.VERTEX_SHADER)
|
||||
if (!vertexShader) {
|
||||
throw new Error('Failed to create vertex shader')
|
||||
}
|
||||
context.shaderSource(vertexShader, vertexShaderSourceCode)
|
||||
context.compileShader(vertexShader)
|
||||
if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) {
|
||||
throw new Error('Failed to compile vertex shader')
|
||||
}
|
||||
|
||||
const fragmentShaderSourceCode = `#version 300 es
|
||||
precision mediump float;
|
||||
|
||||
uniform vec4 fillColor;
|
||||
out vec4 outputColor;
|
||||
|
||||
void main() {
|
||||
outputColor = fillColor;
|
||||
}`
|
||||
|
||||
const fragmentShader = context.createShader(context.FRAGMENT_SHADER)
|
||||
if (!fragmentShader) {
|
||||
throw new Error('Failed to create fragment shader')
|
||||
}
|
||||
context.shaderSource(fragmentShader, fragmentShaderSourceCode)
|
||||
context.compileShader(fragmentShader)
|
||||
if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) {
|
||||
throw new Error('Failed to compile fragment shader')
|
||||
}
|
||||
|
||||
const program = context.createProgram()
|
||||
if (!program) {
|
||||
throw new Error('Failed to create program')
|
||||
}
|
||||
context.attachShader(program, vertexShader)
|
||||
context.attachShader(program, fragmentShader)
|
||||
context.linkProgram(program)
|
||||
if (!context.getProgramParameter(program, context.LINK_STATUS)) {
|
||||
throw new Error('Failed to link program')
|
||||
}
|
||||
context.useProgram(program)
|
||||
|
||||
const shapeVertexPositionAttributeLocation = context.getAttribLocation(
|
||||
program,
|
||||
'shapeVertexPosition'
|
||||
)
|
||||
if (shapeVertexPositionAttributeLocation < 0) {
|
||||
throw new Error('Failed to get shapeVertexPosition attribute location')
|
||||
}
|
||||
context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
|
||||
|
||||
const canvasPageBoundsLocation = context.getUniformLocation(program, 'canvasPageBounds')
|
||||
const fillColorLocation = context.getUniformLocation(program, 'fillColor')
|
||||
|
||||
const selectedShapesBuffer = context.createBuffer()
|
||||
if (!selectedShapesBuffer) throw new Error('Failed to create buffer')
|
||||
|
||||
const unselectedShapesBuffer = context.createBuffer()
|
||||
if (!unselectedShapesBuffer) throw new Error('Failed to create buffer')
|
||||
|
||||
return {
|
||||
context,
|
||||
selectedShapes: allocateBuffer(context, 1024),
|
||||
unselectedShapes: allocateBuffer(context, 4096),
|
||||
viewport: allocateBuffer(context, roundedRectangleDataSize),
|
||||
collaborators: allocateBuffer(context, 1024),
|
||||
|
||||
prepareTriangles(stuff: BufferStuff, len: number) {
|
||||
context.bindBuffer(context.ARRAY_BUFFER, stuff.buffer)
|
||||
context.bufferData(context.ARRAY_BUFFER, stuff.vertices, context.STATIC_DRAW, 0, len)
|
||||
context.enableVertexAttribArray(shapeVertexPositionAttributeLocation)
|
||||
context.vertexAttribPointer(
|
||||
shapeVertexPositionAttributeLocation,
|
||||
2,
|
||||
context.FLOAT,
|
||||
false,
|
||||
0,
|
||||
0
|
||||
)
|
||||
},
|
||||
|
||||
drawTriangles(len: number) {
|
||||
context.drawArrays(context.TRIANGLES, 0, len / 2)
|
||||
},
|
||||
|
||||
setFillColor(color: Float32Array) {
|
||||
context.uniform4fv(fillColorLocation, color)
|
||||
},
|
||||
|
||||
setCanvasPageBounds(bounds: Float32Array) {
|
||||
context.uniform4fv(canvasPageBoundsLocation, bounds)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type BufferStuff = ReturnType<typeof allocateBuffer>
|
||||
|
||||
function allocateBuffer(context: WebGL2RenderingContext, size: number) {
|
||||
const buffer = context.createBuffer()
|
||||
if (!buffer) throw new Error('Failed to create buffer')
|
||||
return { buffer, vertices: new Float32Array(size) }
|
||||
}
|
||||
|
||||
export function appendVertices(bufferStuff: BufferStuff, offset: number, data: Float32Array) {
|
||||
let len = bufferStuff.vertices.length
|
||||
while (len < offset + data.length) {
|
||||
len *= 2
|
||||
}
|
||||
if (len != bufferStuff.vertices.length) {
|
||||
const newVertices = new Float32Array(len)
|
||||
newVertices.set(bufferStuff.vertices)
|
||||
bufferStuff.vertices = newVertices
|
||||
}
|
||||
|
||||
bufferStuff.vertices.set(data, offset)
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import { Box, HALF_PI, PI, PI2, Vec } from '@tldraw/editor'
|
||||
|
||||
export const numArcSegmentsPerCorner = 10
|
||||
|
||||
export const roundedRectangleDataSize =
|
||||
// num triangles in corners
|
||||
4 * 6 * numArcSegmentsPerCorner +
|
||||
// num triangles in center rect
|
||||
12 +
|
||||
// num triangles in outer rects
|
||||
4 * 12
|
||||
|
||||
export function pie(
|
||||
array: Float32Array,
|
||||
{
|
||||
center,
|
||||
radius,
|
||||
numArcSegments = 20,
|
||||
startAngle = 0,
|
||||
endAngle = PI2,
|
||||
offset = 0,
|
||||
}: {
|
||||
center: Vec
|
||||
radius: number
|
||||
numArcSegments?: number
|
||||
startAngle?: number
|
||||
endAngle?: number
|
||||
offset?: number
|
||||
}
|
||||
) {
|
||||
const angle = (endAngle - startAngle) / numArcSegments
|
||||
let i = offset
|
||||
for (let a = startAngle; a < endAngle; a += angle) {
|
||||
array[i++] = center.x
|
||||
array[i++] = center.y
|
||||
array[i++] = center.x + Math.cos(a) * radius
|
||||
array[i++] = center.y + Math.sin(a) * radius
|
||||
array[i++] = center.x + Math.cos(a + angle) * radius
|
||||
array[i++] = center.y + Math.sin(a + angle) * radius
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
export function rectangle(
|
||||
array: Float32Array,
|
||||
offset: number,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number
|
||||
) {
|
||||
array[offset++] = x
|
||||
array[offset++] = y
|
||||
array[offset++] = x
|
||||
array[offset++] = y + h
|
||||
array[offset++] = x + w
|
||||
array[offset++] = y
|
||||
|
||||
array[offset++] = x + w
|
||||
array[offset++] = y
|
||||
array[offset++] = x
|
||||
array[offset++] = y + h
|
||||
array[offset++] = x + w
|
||||
array[offset++] = y + h
|
||||
}
|
||||
|
||||
export function roundedRectangle(data: Float32Array, box: Box, radius: number): number {
|
||||
const numArcSegments = numArcSegmentsPerCorner
|
||||
radius = Math.min(radius, Math.min(box.w, box.h) / 2)
|
||||
// first draw the inner box
|
||||
const innerBox = Box.ExpandBy(box, -radius)
|
||||
if (innerBox.w <= 0 || innerBox.h <= 0) {
|
||||
// just draw a circle
|
||||
pie(data, { center: box.center, radius: radius, numArcSegments: numArcSegmentsPerCorner * 4 })
|
||||
return numArcSegmentsPerCorner * 4 * 6
|
||||
}
|
||||
let offset = 0
|
||||
// draw center rect first
|
||||
rectangle(data, offset, innerBox.minX, innerBox.minY, innerBox.w, innerBox.h)
|
||||
offset += 12
|
||||
// then top rect
|
||||
rectangle(data, offset, innerBox.minX, box.minY, innerBox.w, radius)
|
||||
offset += 12
|
||||
// then right rect
|
||||
rectangle(data, offset, innerBox.maxX, innerBox.minY, radius, innerBox.h)
|
||||
offset += 12
|
||||
// then bottom rect
|
||||
rectangle(data, offset, innerBox.minX, innerBox.maxY, innerBox.w, radius)
|
||||
offset += 12
|
||||
// then left rect
|
||||
rectangle(data, offset, box.minX, innerBox.minY, radius, innerBox.h)
|
||||
offset += 12
|
||||
|
||||
// draw the corners
|
||||
|
||||
// top left
|
||||
pie(data, {
|
||||
numArcSegments,
|
||||
offset,
|
||||
center: innerBox.point,
|
||||
radius,
|
||||
startAngle: PI,
|
||||
endAngle: PI * 1.5,
|
||||
})
|
||||
|
||||
offset += numArcSegments * 6
|
||||
|
||||
// top right
|
||||
pie(data, {
|
||||
numArcSegments,
|
||||
offset,
|
||||
center: Vec.Add(innerBox.point, new Vec(innerBox.w, 0)),
|
||||
radius,
|
||||
startAngle: PI * 1.5,
|
||||
endAngle: PI2,
|
||||
})
|
||||
|
||||
offset += numArcSegments * 6
|
||||
|
||||
// bottom right
|
||||
pie(data, {
|
||||
numArcSegments,
|
||||
offset,
|
||||
center: Vec.Add(innerBox.point, innerBox.size),
|
||||
radius,
|
||||
startAngle: 0,
|
||||
endAngle: HALF_PI,
|
||||
})
|
||||
|
||||
offset += numArcSegments * 6
|
||||
|
||||
// bottom left
|
||||
pie(data, {
|
||||
numArcSegments,
|
||||
offset,
|
||||
center: Vec.Add(innerBox.point, new Vec(0, innerBox.h)),
|
||||
radius,
|
||||
startAngle: HALF_PI,
|
||||
endAngle: PI,
|
||||
})
|
||||
|
||||
return roundedRectangleDataSize
|
||||
}
|
|
@ -20,6 +20,7 @@ import {
|
|||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { ADJACENT_NOTE_MARGIN } from '../../shapes/note/noteHelpers'
|
||||
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
|
||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
||||
|
@ -818,7 +819,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
trackEvent('pack-shapes', { source })
|
||||
editor.mark('pack')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.packShapes(selectedShapeIds, 16)
|
||||
editor.packShapes(selectedShapeIds, ADJACENT_NOTE_MARGIN)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
|
@ -1036,7 +1037,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('zoom-in', { source })
|
||||
editor.zoomIn(editor.getViewportScreenCenter(), {
|
||||
editor.zoomIn(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
})
|
||||
},
|
||||
|
@ -1048,7 +1049,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('zoom-out', { source })
|
||||
editor.zoomOut(editor.getViewportScreenCenter(), {
|
||||
editor.zoomOut(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
})
|
||||
},
|
||||
|
@ -1061,7 +1062,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('reset-zoom', { source })
|
||||
editor.resetZoom(editor.getViewportScreenCenter(), {
|
||||
editor.resetZoom(undefined, {
|
||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||
})
|
||||
},
|
||||
|
@ -1296,7 +1297,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
trackEvent('zoom-to-content', { source })
|
||||
editor.zoomToContent()
|
||||
const bounds = editor.getSelectionPageBounds() ?? editor.getCurrentPageBounds()
|
||||
if (!bounds) return
|
||||
editor.zoomToBounds(bounds, {
|
||||
targetZoom: Math.min(1, editor.getZoomLevel()),
|
||||
animation: { duration: 220 },
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import { createContext, useContext, useEffect } from 'react'
|
||||
import { TLUiAssetUrls } from '../assetUrls'
|
||||
|
||||
/** @internal */
|
||||
|
@ -14,6 +14,19 @@ export function AssetUrlsProvider({
|
|||
assetUrls: TLUiAssetUrls
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
useEffect(() => {
|
||||
for (const src of Object.values(assetUrls.icons)) {
|
||||
const image = new Image()
|
||||
image.src = src
|
||||
image.decode()
|
||||
}
|
||||
for (const src of Object.values(assetUrls.embedIcons)) {
|
||||
const image = new Image()
|
||||
image.src = src
|
||||
image.decode()
|
||||
}
|
||||
}, [assetUrls])
|
||||
|
||||
return <AssetUrlsContext.Provider value={assetUrls}>{children}</AssetUrlsContext.Provider>
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
TLTextShape,
|
||||
VecLike,
|
||||
isNonNull,
|
||||
preventDefault,
|
||||
stopEventPropagation,
|
||||
uniq,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -615,24 +617,29 @@ export function useNativeClipboardEvents() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!appIsFocused) return
|
||||
const copy = () => {
|
||||
const copy = (e: ClipboardEvent) => {
|
||||
if (
|
||||
editor.getSelectedShapeIds().length === 0 ||
|
||||
editor.getEditingShapeId() !== null ||
|
||||
disallowClipboardEvents(editor)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
preventDefault(e)
|
||||
handleNativeOrMenuCopy(editor)
|
||||
trackEvent('copy', { source: 'kbd' })
|
||||
}
|
||||
|
||||
function cut() {
|
||||
function cut(e: ClipboardEvent) {
|
||||
if (
|
||||
editor.getSelectedShapeIds().length === 0 ||
|
||||
editor.getEditingShapeId() !== null ||
|
||||
disallowClipboardEvents(editor)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
preventDefault(e)
|
||||
handleNativeOrMenuCopy(editor)
|
||||
editor.deleteShapes(editor.getSelectedShapeIds())
|
||||
trackEvent('cut', { source: 'kbd' })
|
||||
|
@ -641,6 +648,7 @@ export function useNativeClipboardEvents() {
|
|||
let disablingMiddleClickPaste = false
|
||||
const pointerUpHandler = (e: PointerEvent) => {
|
||||
if (e.button === 1) {
|
||||
// middle mouse button
|
||||
disablingMiddleClickPaste = true
|
||||
requestAnimationFrame(() => {
|
||||
disablingMiddleClickPaste = false
|
||||
|
@ -648,9 +656,9 @@ export function useNativeClipboardEvents() {
|
|||
}
|
||||
}
|
||||
|
||||
const paste = (event: ClipboardEvent) => {
|
||||
const paste = (e: ClipboardEvent) => {
|
||||
if (disablingMiddleClickPaste) {
|
||||
event.stopPropagation()
|
||||
stopEventPropagation(e)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -660,8 +668,8 @@ export function useNativeClipboardEvents() {
|
|||
if (editor.getEditingShapeId() !== null || disallowClipboardEvents(editor)) return
|
||||
|
||||
// First try to use the clipboard data on the event
|
||||
if (event.clipboardData && !editor.inputs.shiftKey) {
|
||||
handlePasteFromEventClipboardData(editor, event.clipboardData)
|
||||
if (e.clipboardData && !editor.inputs.shiftKey) {
|
||||
handlePasteFromEventClipboardData(editor, e.clipboardData)
|
||||
} else {
|
||||
// Or else use the clipboard API
|
||||
navigator.clipboard.read().then((clipboardItems) => {
|
||||
|
@ -671,6 +679,7 @@ export function useNativeClipboardEvents() {
|
|||
})
|
||||
}
|
||||
|
||||
preventDefault(e)
|
||||
trackEvent('paste', { source: 'kbd' })
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useAssetUrls } from '../context/asset-urls'
|
||||
import { iconTypes } from '../icon-types'
|
||||
|
||||
/** @internal */
|
||||
export function usePreloadIcons(): boolean {
|
||||
const [isLoaded, setIsLoaded] = useState<boolean>(false)
|
||||
const assetUrls = useAssetUrls()
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function loadImages() {
|
||||
// Run through all of the icons and load them. It doesn't matter
|
||||
// if any of the images don't load; though we expect that they would.
|
||||
// Instead, we just want to make sure that the browser has cached
|
||||
// all of the icons it can so that they're available when we need them.
|
||||
|
||||
await Promise.allSettled(
|
||||
iconTypes.map((icon) => {
|
||||
const image = new Image()
|
||||
image.src = assetUrls.icons[icon]
|
||||
return image.decode()
|
||||
})
|
||||
)
|
||||
|
||||
if (cancelled) return
|
||||
setIsLoaded(true)
|
||||
}
|
||||
|
||||
loadImages()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isLoaded, assetUrls])
|
||||
|
||||
return isLoaded
|
||||
}
|
|
@ -62,7 +62,7 @@ const schemaV2 = T.object<SerializedSchemaV2>({
|
|||
|
||||
const tldrawFileValidator: T.Validator<TldrawFile> = T.object({
|
||||
tldrawFileFormatVersion: T.nonZeroInteger,
|
||||
schema: T.union('schemaVersion', {
|
||||
schema: T.numberUnion('schemaVersion', {
|
||||
1: schemaV1,
|
||||
2: schemaV2,
|
||||
}),
|
||||
|
@ -305,7 +305,6 @@ export async function parseAndLoadDocument(
|
|||
editor.history.clear()
|
||||
// Put the old bounds back in place
|
||||
editor.updateViewportScreenBounds(initialBounds)
|
||||
editor.updateRenderingBounds()
|
||||
|
||||
const bounds = editor.getCurrentPageBounds()
|
||||
if (bounds) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
TLWheelEventInfo,
|
||||
Vec,
|
||||
VecLike,
|
||||
computed,
|
||||
createShapeId,
|
||||
createTLStore,
|
||||
rotateSelectionHandle,
|
||||
|
@ -143,6 +144,15 @@ export class TestEditor extends Editor {
|
|||
elm: HTMLDivElement
|
||||
bounds = { x: 0, y: 0, top: 0, left: 0, width: 1080, height: 720, bottom: 720, right: 1080 }
|
||||
|
||||
/**
|
||||
* The center of the viewport in the current page space.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@computed getViewportPageCenter() {
|
||||
return this.getViewportPageBounds().center
|
||||
}
|
||||
|
||||
setScreenBounds(bounds: BoxModel, center = false) {
|
||||
this.bounds.x = bounds.x
|
||||
this.bounds.y = bounds.y
|
||||
|
@ -154,7 +164,6 @@ export class TestEditor extends Editor {
|
|||
this.bounds.bottom = bounds.y + bounds.h
|
||||
|
||||
this.updateViewportScreenBounds(Box.From(bounds), center)
|
||||
this.updateRenderingBounds()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -200,12 +209,12 @@ export class TestEditor extends Editor {
|
|||
* _transformPointerDownSpy.mockRestore())
|
||||
*/
|
||||
_transformPointerDownSpy = jest
|
||||
.spyOn(this._clickManager, 'transformPointerDownEvent')
|
||||
.spyOn(this._clickManager, 'handlePointerEvent')
|
||||
.mockImplementation((info) => {
|
||||
return info
|
||||
})
|
||||
_transformPointerUpSpy = jest
|
||||
.spyOn(this._clickManager, 'transformPointerDownEvent')
|
||||
.spyOn(this._clickManager, 'handlePointerEvent')
|
||||
.mockImplementation((info) => {
|
||||
return info
|
||||
})
|
||||
|
@ -455,6 +464,16 @@ export class TestEditor extends Editor {
|
|||
return this
|
||||
}
|
||||
|
||||
pan(offset: VecLike): this {
|
||||
const { isLocked, panSpeed } = this.getCameraOptions()
|
||||
if (isLocked) return this
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
this.setCamera(new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz), {
|
||||
immediate: true,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
pinchStart = (
|
||||
x = this.inputs.currentScreenPoint.x,
|
||||
y = this.inputs.currentScreenPoint.y,
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,89 @@
|
|||
import { TLCameraOptions } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
describe('getBaseZoom', () => {
|
||||
it('gets initial zoom with default options', () => {
|
||||
expect(editor.getBaseZoom()).toBe(1)
|
||||
})
|
||||
|
||||
it('gets initial zoom based on constraints', () => {
|
||||
const vsb = editor.getViewportScreenBounds()
|
||||
let cameraOptions: TLCameraOptions
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
|
||||
padding: { x: 0, y: 0 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'default',
|
||||
baseZoom: 'default',
|
||||
behavior: 'free',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(1)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
baseZoom: 'fit-x',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(0.5)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
baseZoom: 'fit-y',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(0.25)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
baseZoom: 'fit-min',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(0.5)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
baseZoom: 'fit-max',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(0.25)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
baseZoom: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getBaseZoom()).toBe(1)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,89 @@
|
|||
import { TLCameraOptions } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
describe('getInitialZoom', () => {
|
||||
it('gets initial zoom with default options', () => {
|
||||
expect(editor.getInitialZoom()).toBe(1)
|
||||
})
|
||||
|
||||
it('gets initial zoom based on constraints', () => {
|
||||
const vsb = editor.getViewportScreenBounds()
|
||||
let cameraOptions: TLCameraOptions
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
|
||||
padding: { x: 0, y: 0 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'default',
|
||||
baseZoom: 'default',
|
||||
behavior: 'free',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(1)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
initialZoom: 'fit-x',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.5)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
initialZoom: 'fit-y',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.25)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
initialZoom: 'fit-min',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.5)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.25)
|
||||
|
||||
cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
...(cameraOptions.constraints as any),
|
||||
initialZoom: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(1)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import { getDefaultCameraOptions } from '@tldraw/editor'
|
||||
import { DEFAULT_CAMERA_OPTIONS } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -8,7 +8,7 @@ beforeEach(() => {
|
|||
})
|
||||
|
||||
it('zooms by increments', () => {
|
||||
const cameraOptions = getDefaultCameraOptions()
|
||||
const cameraOptions = DEFAULT_CAMERA_OPTIONS
|
||||
|
||||
// Starts at 1
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
||||
|
@ -46,7 +46,7 @@ it('preserves the screen center when offset', () => {
|
|||
})
|
||||
|
||||
it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => {
|
||||
const cameraOptions = getDefaultCameraOptions()
|
||||
const cameraOptions = DEFAULT_CAMERA_OPTIONS
|
||||
|
||||
editor.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 })
|
||||
editor.zoomIn()
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { getDefaultCameraOptions } from '@tldraw/editor'
|
||||
import { TestEditor } from '../TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
@ -8,7 +7,7 @@ beforeEach(() => {
|
|||
})
|
||||
|
||||
it('zooms out and in by increments', () => {
|
||||
const cameraOptions = getDefaultCameraOptions()
|
||||
const cameraOptions = editor.getCameraOptions()
|
||||
|
||||
// Starts at 1
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
||||
|
@ -30,3 +29,87 @@ it('does not zoom out when camera is frozen', () => {
|
|||
editor.zoomOut()
|
||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||
})
|
||||
|
||||
it('zooms out and in by increments when the camera options have constraints but no base zoom', () => {
|
||||
const cameraOptions = editor.getCameraOptions()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
bounds: { x: 0, y: 0, w: 1600, h: 900 },
|
||||
padding: { x: 0, y: 0 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'default',
|
||||
baseZoom: 'default',
|
||||
behavior: 'free',
|
||||
},
|
||||
})
|
||||
// Starts at 1
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2])
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1])
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0])
|
||||
// does not zoom out past min
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0])
|
||||
})
|
||||
|
||||
it('zooms out and in by increments when the camera options have constraints and a base zoom', () => {
|
||||
const cameraOptions = editor.getCameraOptions()
|
||||
const vsb = editor.getViewportScreenBounds()
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
|
||||
padding: { x: 0, y: 0 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-x',
|
||||
baseZoom: 'fit-x',
|
||||
behavior: 'free',
|
||||
},
|
||||
})
|
||||
// And reset the zoom to its initial value
|
||||
editor.resetZoom()
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.5) // fitting the x axis
|
||||
// Starts at 1
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3] * 0.5)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2] * 0.5)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1] * 0.5)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.5)
|
||||
// does not zoom out past min
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.5)
|
||||
|
||||
editor.setCameraOptions({
|
||||
...cameraOptions,
|
||||
constraints: {
|
||||
bounds: { x: 0, y: 0, w: vsb.w * 2, h: vsb.h * 4 },
|
||||
padding: { x: 0, y: 0 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-y',
|
||||
baseZoom: 'fit-y',
|
||||
behavior: 'free',
|
||||
},
|
||||
})
|
||||
// And reset the zoom to its initial value
|
||||
editor.resetZoom()
|
||||
|
||||
expect(editor.getInitialZoom()).toBe(0.25) // fitting the y axis
|
||||
// Starts at 1
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3] * 0.25)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2] * 0.25)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1] * 0.25)
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.25)
|
||||
// does not zoom out past min
|
||||
editor.zoomOut()
|
||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0] * 0.25)
|
||||
})
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,5 +1,6 @@
|
|||
import { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
||||
import { TestEditor } from './TestEditor'
|
||||
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
|
@ -260,3 +261,22 @@ for (const toolType of ['draw', 'highlight'] as const) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
it('Draws a bunch', () => {
|
||||
editor.setCurrentTool('draw').setCamera({ x: 0, y: 0, z: 1 })
|
||||
|
||||
const [first, ...rest] = TEST_DRAW_SHAPE_SCREEN_POINTS
|
||||
editor.pointerMove(first.x, first.y).pointerDown()
|
||||
|
||||
for (const point of rest) {
|
||||
editor.pointerMove(point.x, point.y)
|
||||
}
|
||||
|
||||
editor.pointerUp()
|
||||
editor.selectAll()
|
||||
|
||||
const shape = { ...editor.getLastCreatedShape() }
|
||||
// @ts-expect-error
|
||||
delete shape.id
|
||||
expect(shape).toMatchSnapshot('draw shape')
|
||||
})
|
||||
|
|
|
@ -7,7 +7,6 @@ let editor: TestEditor
|
|||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||
editor.renderingBoundsMargin = 100
|
||||
})
|
||||
|
||||
function createShapes() {
|
||||
|
@ -136,3 +135,56 @@ it('correctly calculates the culled shapes when adding and deleting shapes', ()
|
|||
const culledShapeFromScratch = editor.getCulledShapes()
|
||||
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
|
||||
})
|
||||
|
||||
it('works for shapes that are outside of the viewport, but are then moved inside it', () => {
|
||||
const box1Id = createShapeId()
|
||||
const box2Id = createShapeId()
|
||||
const arrowId = createShapeId()
|
||||
|
||||
editor.createShapes([
|
||||
{
|
||||
id: box1Id,
|
||||
props: { w: 100, h: 100, geo: 'rectangle' },
|
||||
type: 'geo',
|
||||
x: -500,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
id: box2Id,
|
||||
type: 'geo',
|
||||
x: -1000,
|
||||
y: 200,
|
||||
props: { w: 100, h: 100, geo: 'rectangle' },
|
||||
},
|
||||
{
|
||||
id: arrowId,
|
||||
type: 'arrow',
|
||||
props: {
|
||||
start: {
|
||||
type: 'binding',
|
||||
isExact: true,
|
||||
boundShapeId: box1Id,
|
||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||
isPrecise: false,
|
||||
},
|
||||
end: {
|
||||
type: 'binding',
|
||||
isExact: true,
|
||||
boundShapeId: box2Id,
|
||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||
isPrecise: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(editor.getCulledShapes()).toEqual(new Set([box1Id, box2Id, arrowId]))
|
||||
|
||||
// Move box1 and box2 inside the viewport
|
||||
editor.updateShapes([
|
||||
{ id: box1Id, type: 'geo', x: 100 },
|
||||
{ id: box2Id, type: 'geo', x: 200 },
|
||||
])
|
||||
// Arrow should also not be culled
|
||||
expect(editor.getCulledShapes()).toEqual(new Set())
|
||||
})
|
||||
|
|
|
@ -419,7 +419,6 @@ describe('When pasting into frames...', () => {
|
|||
.bringToFront(editor.getSelectedShapeIds())
|
||||
|
||||
editor.setCamera({ x: -2000, y: -2000, z: 1 })
|
||||
editor.updateRenderingBounds()
|
||||
|
||||
// Copy box 1 (should be out of viewport)
|
||||
editor.select(ids.box1).copy()
|
||||
|
|
|
@ -34,7 +34,6 @@ function normalizeIndexes(
|
|||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||
editor.renderingBoundsMargin = 100
|
||||
})
|
||||
|
||||
function createShapes() {
|
||||
|
@ -48,18 +47,6 @@ function createShapes() {
|
|||
])
|
||||
}
|
||||
|
||||
it('updates the rendering viewport when the camera stops moving', () => {
|
||||
const ids = createShapes()
|
||||
|
||||
editor.updateRenderingBounds = jest.fn(editor.updateRenderingBounds)
|
||||
editor.pan({ x: -201, y: -201 })
|
||||
jest.advanceTimersByTime(500)
|
||||
|
||||
expect(editor.updateRenderingBounds).toHaveBeenCalledTimes(1)
|
||||
expect(editor.getRenderingBounds()).toMatchObject({ x: 201, y: 201, w: 1800, h: 900 })
|
||||
expect(editor.getShapePageBounds(ids.A)).toMatchObject({ x: 100, y: 100, w: 100, h: 100 })
|
||||
})
|
||||
|
||||
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
|
||||
|
|
|
@ -34,15 +34,17 @@ export function measureAverageDuration(
|
|||
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`
|
||||
)
|
||||
if (length !== 0) {
|
||||
const value = averages.get(descriptor.value)!
|
||||
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 })
|
||||
|
|
|
@ -83,6 +83,9 @@ function nullable<T>(validator: Validatable<T>): Validator<null | T>;
|
|||
// @public
|
||||
const number: Validator<number>;
|
||||
|
||||
// @internal (undocumented)
|
||||
function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
|
||||
|
||||
// @public
|
||||
function object<Shape extends object>(config: {
|
||||
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
||||
|
@ -134,6 +137,7 @@ declare namespace T {
|
|||
jsonDict,
|
||||
dict,
|
||||
union,
|
||||
numberUnion,
|
||||
model,
|
||||
setEnum,
|
||||
optional,
|
||||
|
@ -178,7 +182,7 @@ function union<Key extends string, Config extends UnionValidatorConfig<Key, Conf
|
|||
|
||||
// @public (undocumented)
|
||||
export class UnionValidator<Key extends string, Config extends UnionValidatorConfig<Key, Config>, UnknownValue = never> extends Validator<TypeOf<Config[keyof Config]> | UnknownValue> {
|
||||
constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue);
|
||||
constructor(key: Key, config: Config, unknownValueValidation: (value: object, variant: string) => UnknownValue, useNumberKeys: boolean);
|
||||
// (undocumented)
|
||||
validateUnknownVariants<Unknown>(unknownValueValidation: (value: object, variant: string) => Unknown): UnionValidator<Key, Config, Unknown>;
|
||||
}
|
||||
|
|
|
@ -3027,6 +3027,14 @@
|
|||
"kind": "Content",
|
||||
"text": "(value: object, variant: string) => UnknownValue"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", useNumberKeys: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ");"
|
||||
|
@ -3059,6 +3067,14 @@
|
|||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "useNumberKeys",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4260,6 +4276,14 @@
|
|||
"kind": "Content",
|
||||
"text": "(value: object, variant: string) => UnknownValue"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", useNumberKeys: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ");"
|
||||
|
@ -4292,6 +4316,14 @@
|
|||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "useNumberKeys",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -394,7 +394,8 @@ export class UnionValidator<
|
|||
constructor(
|
||||
private readonly key: Key,
|
||||
private readonly config: Config,
|
||||
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue
|
||||
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue,
|
||||
private readonly useNumberKeys: boolean
|
||||
) {
|
||||
super(
|
||||
(input) => {
|
||||
|
@ -442,11 +443,13 @@ export class UnionValidator<
|
|||
matchingSchema: Validatable<any> | undefined
|
||||
variant: string
|
||||
} {
|
||||
const variant = getOwnProperty(object, this.key) as keyof Config | undefined
|
||||
if (typeof variant !== 'string') {
|
||||
const variant = getOwnProperty(object, this.key) as string & keyof Config
|
||||
if (!this.useNumberKeys && typeof variant !== 'string') {
|
||||
throw new ValidationError(
|
||||
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
|
||||
)
|
||||
} else if (this.useNumberKeys && !Number.isFinite(Number(variant))) {
|
||||
throw new ValidationError(`Expected a number for key "${this.key}", got "${variant as any}"`)
|
||||
}
|
||||
|
||||
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
|
||||
|
@ -456,7 +459,7 @@ export class UnionValidator<
|
|||
validateUnknownVariants<Unknown>(
|
||||
unknownValueValidation: (value: object, variant: string) => Unknown
|
||||
): UnionValidator<Key, Config, Unknown> {
|
||||
return new UnionValidator(this.key, this.config, unknownValueValidation)
|
||||
return new UnionValidator(this.key, this.config, unknownValueValidation, this.useNumberKeys)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -829,14 +832,41 @@ export function union<Key extends string, Config extends UnionValidatorConfig<Ke
|
|||
key: Key,
|
||||
config: Config
|
||||
): UnionValidator<Key, Config> {
|
||||
return new UnionValidator(key, config, (unknownValue, unknownVariant) => {
|
||||
throw new ValidationError(
|
||||
`Expected one of ${Object.keys(config)
|
||||
.map((key) => JSON.stringify(key))
|
||||
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
|
||||
[key]
|
||||
)
|
||||
})
|
||||
return new UnionValidator(
|
||||
key,
|
||||
config,
|
||||
(unknownValue, unknownVariant) => {
|
||||
throw new ValidationError(
|
||||
`Expected one of ${Object.keys(config)
|
||||
.map((key) => JSON.stringify(key))
|
||||
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
|
||||
[key]
|
||||
)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(
|
||||
key: Key,
|
||||
config: Config
|
||||
): UnionValidator<Key, Config> {
|
||||
return new UnionValidator(
|
||||
key,
|
||||
config,
|
||||
(unknownValue, unknownVariant) => {
|
||||
throw new ValidationError(
|
||||
`Expected one of ${Object.keys(config)
|
||||
.map((key) => JSON.stringify(key))
|
||||
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
|
||||
[key]
|
||||
)
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -515,7 +515,7 @@ async function coalesceWithPreviousAssets(assetsDir: string) {
|
|||
// and it will mess up the inline source viewer on sentry errors.
|
||||
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
|
||||
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable<Uint8Array>) {
|
||||
out.write(chunk)
|
||||
out.write(Buffer.from(chunk.buffer))
|
||||
}
|
||||
out.end()
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ async function hasPackageChanged(pkg: PackageDetails) {
|
|||
}
|
||||
const publishedTarballPath = `${dirPath}/published-package.tgz`
|
||||
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
|
||||
const publishedManifest = await getTarballManifest(publishedTarballPath)
|
||||
const publishedManifest = getTarballManifestSync(publishedTarballPath)
|
||||
|
||||
const localTarballPath = `${dirPath}/local-package.tgz`
|
||||
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir })
|
||||
|
||||
const localManifest = await getTarballManifest(localTarballPath)
|
||||
const localManifest = getTarballManifestSync(localTarballPath)
|
||||
|
||||
return !manifestsAreEqual(publishedManifest, localManifest)
|
||||
} finally {
|
||||
|
@ -48,34 +48,25 @@ function manifestsAreEqual(a: Record<string, Buffer>, b: Record<string, Buffer>)
|
|||
return true
|
||||
}
|
||||
|
||||
function getTarballManifest(tarballPath: string): Promise<Record<string, Buffer>> {
|
||||
function getTarballManifestSync(tarballPath: string) {
|
||||
const manifest: Record<string, Buffer> = {}
|
||||
return new Promise((resolve, reject) =>
|
||||
tar.list(
|
||||
{
|
||||
// @ts-expect-error bad typings
|
||||
file: tarballPath,
|
||||
onentry: (entry) => {
|
||||
entry.on('data', (data) => {
|
||||
// we could hash these to reduce memory but it's probably fine
|
||||
const existing = manifest[entry.path]
|
||||
if (existing) {
|
||||
manifest[entry.path] = Buffer.concat([existing, data])
|
||||
} else {
|
||||
manifest[entry.path] = data
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
(err: any) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
tar.list({
|
||||
file: tarballPath,
|
||||
onentry: (entry) => {
|
||||
entry.on('data', (data) => {
|
||||
// we could hash these to reduce memory but it's probably fine
|
||||
const existing = manifest[entry.path]
|
||||
if (existing) {
|
||||
manifest[entry.path] = Buffer.concat([existing, data])
|
||||
} else {
|
||||
resolve(manifest)
|
||||
manifest[entry.path] = data
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
sync: true,
|
||||
})
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
export async function didAnyPackageChange() {
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
"@types/tmp": "^0.2.6",
|
||||
"ignore": "^5.2.4",
|
||||
"minimist": "^1.2.8",
|
||||
"tar": "^6.2.0",
|
||||
"tar": "^7.0.1",
|
||||
"tmp": "^0.2.3"
|
||||
}
|
||||
}
|
||||
|
|
113
yarn.lock
113
yarn.lock
|
@ -3680,6 +3680,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/fs-minipass@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@isaacs/fs-minipass@npm:4.0.0"
|
||||
dependencies:
|
||||
minipass: "npm:^7.0.4"
|
||||
checksum: 7444d7a3c9211c27494630e2bff8545e3494a1598624a4871ee7ef3a9e592a61fed3abd85d118f966673bd0b4401c266d45441f89c00c420e9d0cfbf1042dbd5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@istanbuljs/load-nyc-config@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
|
||||
|
@ -7589,7 +7598,7 @@ __metadata:
|
|||
rimraf: "npm:^4.4.0"
|
||||
semver: "npm:^7.3.8"
|
||||
svgo: "npm:^3.0.2"
|
||||
tar: "npm:^6.2.0"
|
||||
tar: "npm:^7.0.1"
|
||||
tmp: "npm:^0.2.3"
|
||||
typescript: "npm:^5.3.3"
|
||||
languageName: unknown
|
||||
|
@ -10700,6 +10709,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "chownr@npm:3.0.0"
|
||||
checksum: b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chrome-trace-event@npm:^1.0.2":
|
||||
version: 1.0.3
|
||||
resolution: "chrome-trace-event@npm:1.0.3"
|
||||
|
@ -14645,18 +14661,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2, glob@npm:^10.3.10":
|
||||
version: 10.3.10
|
||||
resolution: "glob@npm:10.3.10"
|
||||
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
|
||||
version: 10.3.12
|
||||
resolution: "glob@npm:10.3.12"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.1.0"
|
||||
jackspeak: "npm:^2.3.5"
|
||||
jackspeak: "npm:^2.3.6"
|
||||
minimatch: "npm:^9.0.1"
|
||||
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
path-scurry: "npm:^1.10.1"
|
||||
minipass: "npm:^7.0.4"
|
||||
path-scurry: "npm:^1.10.2"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8
|
||||
checksum: 9e8186abc22dc824b5dd86cefd8e6b5621a72d1be7f68bacc0fd681e8c162ec5546660a6ec0553d6a74757a585e655956c7f8f1a6d24570e8d865c307323d178
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -16275,7 +16291,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jackspeak@npm:^2.3.5":
|
||||
"jackspeak@npm:^2.3.6":
|
||||
version: 2.3.6
|
||||
resolution: "jackspeak@npm:2.3.6"
|
||||
dependencies:
|
||||
|
@ -17721,10 +17737,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0":
|
||||
version: 10.1.0
|
||||
resolution: "lru-cache@npm:10.1.0"
|
||||
checksum: 207278d6fa711fb1f94a0835d4d4737441d2475302482a14785b10515e4c906a57ebf9f35bf060740c9560e91c7c1ad5a04fd7ed030972a9ba18bce2a228e95b
|
||||
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||
version: 10.2.0
|
||||
resolution: "lru-cache@npm:10.2.0"
|
||||
checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -19131,7 +19147,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3":
|
||||
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4":
|
||||
version: 7.0.4
|
||||
resolution: "minipass@npm:7.0.4"
|
||||
checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18
|
||||
|
@ -19148,6 +19164,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "minizlib@npm:3.0.1"
|
||||
dependencies:
|
||||
minipass: "npm:^7.0.4"
|
||||
rimraf: "npm:^5.0.5"
|
||||
checksum: 622cb85f51e5c206a080a62d20db0d7b4066f308cb6ce82a9644da112367c3416ae7062017e631eb7ac8588191cfa4a9a279b8651c399265202b298e98c4acef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
|
||||
version: 0.5.3
|
||||
resolution: "mkdirp-classic@npm:0.5.3"
|
||||
|
@ -19164,6 +19190,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "mkdirp@npm:3.0.1"
|
||||
bin:
|
||||
mkdirp: dist/cjs/src/bin.js
|
||||
checksum: 16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mlly@npm:^1.1.0, mlly@npm:^1.2.0":
|
||||
version: 1.5.0
|
||||
resolution: "mlly@npm:1.5.0"
|
||||
|
@ -20327,13 +20362,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1":
|
||||
version: 1.10.1
|
||||
resolution: "path-scurry@npm:1.10.1"
|
||||
"path-scurry@npm:^1.10.2, path-scurry@npm:^1.6.1":
|
||||
version: 1.10.2
|
||||
resolution: "path-scurry@npm:1.10.2"
|
||||
dependencies:
|
||||
lru-cache: "npm:^9.1.1 || ^10.0.0"
|
||||
lru-cache: "npm:^10.2.0"
|
||||
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
checksum: eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8
|
||||
checksum: a2bbbe8dc284c49dd9be78ca25f3a8b89300e0acc24a77e6c74824d353ef50efbf163e64a69f4330b301afca42d0e2229be0560d6d616ac4e99d48b4062016b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -22045,6 +22080,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^5.0.5":
|
||||
version: 5.0.5
|
||||
resolution: "rimraf@npm:5.0.5"
|
||||
dependencies:
|
||||
glob: "npm:^10.3.7"
|
||||
bin:
|
||||
rimraf: dist/esm/bin.mjs
|
||||
checksum: a612c7184f96258b7d1328c486b12ca7b60aa30e04229a08bbfa7e964486deb1e9a1b52d917809311bdc39a808a4055c0f950c0280fba194ba0a09e6f0d404f6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup-plugin-inject@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "rollup-plugin-inject@npm:3.0.2"
|
||||
|
@ -23378,7 +23424,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2, tar@npm:^6.2.0":
|
||||
"tar@npm:^6.0.2, tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||
version: 6.2.1
|
||||
resolution: "tar@npm:6.2.1"
|
||||
dependencies:
|
||||
|
@ -23392,6 +23438,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^7.0.1":
|
||||
version: 7.0.1
|
||||
resolution: "tar@npm:7.0.1"
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass": "npm:^4.0.0"
|
||||
chownr: "npm:^3.0.0"
|
||||
minipass: "npm:^5.0.0"
|
||||
minizlib: "npm:^3.0.1"
|
||||
mkdirp: "npm:^3.0.1"
|
||||
yallist: "npm:^5.0.0"
|
||||
checksum: 6fd89ef8051d12975f66a2f3932a80479bdc6c9f3bcdf04b8b57784e942ed860708ccecf79bcbb30659b14ab52eef2095d2c3af377545ff9df30de28036671dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"terminal-link@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "terminal-link@npm:2.1.1"
|
||||
|
@ -24964,8 +25024,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"vite@npm:^5.0.0":
|
||||
version: 5.2.8
|
||||
resolution: "vite@npm:5.2.8"
|
||||
version: 5.2.9
|
||||
resolution: "vite@npm:5.2.9"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.20.1"
|
||||
fsevents: "npm:~2.3.3"
|
||||
|
@ -24999,7 +25059,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
|
||||
checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -25666,6 +25726,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "yallist@npm:5.0.0"
|
||||
checksum: 1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:2.3.4, yaml@npm:^2.0.0, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.4":
|
||||
version: 2.3.4
|
||||
resolution: "yaml@npm:2.3.4"
|
||||
|
|
Ładowanie…
Reference in New Issue