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
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: How can we reproduce the bug?
|
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:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: dropdown
|
- 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.
|
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
|
### 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 { LoadingScreen } from 'tldraw'
|
||||||
|
import { version } from '../../version'
|
||||||
import { useUrl } from '../hooks/useUrl'
|
import { useUrl } from '../hooks/useUrl'
|
||||||
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
import { trackAnalyticsEvent } from '../utils/trackAnalyticsEvent'
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ export function IFrameProtector({
|
||||||
<div className="tldraw__editor tl-container">
|
<div className="tldraw__editor tl-container">
|
||||||
<div className="iframe-warning__container">
|
<div className="iframe-warning__container">
|
||||||
<a className="iframe-warning__link" href={url} target="_blank">
|
<a className="iframe-warning__link" href={url} target="_blank">
|
||||||
{'Visit this page on tldraw.com '}
|
{'Visit this page on tldraw.com'}
|
||||||
<svg
|
<svg
|
||||||
width="15"
|
width="15"
|
||||||
height="15"
|
height="15"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { default as React, useEffect } from 'react'
|
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 = {
|
const PARAMS = {
|
||||||
// deprecated
|
// deprecated
|
||||||
|
@ -68,19 +68,13 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) {
|
||||||
const viewport = viewportFromString(newViewportRaw)
|
const viewport = viewportFromString(newViewportRaw)
|
||||||
const { x, y, w, h } = viewport
|
const { x, y, w, h } = viewport
|
||||||
const { w: sw, h: sh } = editor.getViewportScreenBounds()
|
const { w: sw, h: sh } = editor.getViewportScreenBounds()
|
||||||
const fitZoom = editor.getCameraFitZoom()
|
const initialZoom = editor.getInitialZoom()
|
||||||
const { zoomSteps } = editor.getCameraOptions()
|
const { zoomSteps } = editor.getCameraOptions()
|
||||||
const zoomMin = zoomSteps[0]
|
const zoomMin = zoomSteps[0]
|
||||||
const zoomMax = zoomSteps[zoomSteps.length - 1]
|
const zoomMax = zoomSteps[zoomSteps.length - 1]
|
||||||
|
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * initialZoom, zoomMax * initialZoom)
|
||||||
const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * fitZoom, zoomMax * fitZoom)
|
|
||||||
|
|
||||||
editor.setCamera(
|
editor.setCamera(
|
||||||
{
|
new Vec(-x + (sw - w * zoom) / 2 / zoom, -y + (sh - h * zoom) / 2 / zoom, zoom),
|
||||||
x: -x + (sw - w * zoom) / 2 / zoom,
|
|
||||||
y: -y + (sh - h * zoom) / 2 / zoom,
|
|
||||||
z: zoom,
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -86,7 +86,6 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
|
||||||
editor.history.clear()
|
editor.history.clear()
|
||||||
// Put the old bounds back in place
|
// Put the old bounds back in place
|
||||||
editor.updateViewportScreenBounds(bounds)
|
editor.updateViewportScreenBounds(bounds)
|
||||||
editor.updateRenderingBounds()
|
|
||||||
editor.updateInstanceState({ isFocused })
|
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 = {
|
const CAMERA_OPTIONS: TLCameraOptions = {
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
|
wheelBehavior: 'pan',
|
||||||
panSpeed: 1,
|
panSpeed: 1,
|
||||||
zoomSpeed: 1,
|
zoomSpeed: 1,
|
||||||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
||||||
constraints: {
|
constraints: {
|
||||||
fit: 'max',
|
initialZoom: 'fit-max',
|
||||||
|
baseZoom: 'fit-max',
|
||||||
bounds: {
|
bounds: {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 1200,
|
w: 1600,
|
||||||
h: 800,
|
h: 900,
|
||||||
},
|
},
|
||||||
fitX: 'contain',
|
behavior: { x: 'contain', y: 'contain' },
|
||||||
fitY: 'contain',
|
|
||||||
padding: { x: 100, y: 100 },
|
padding: { x: 100, y: 100 },
|
||||||
origin: { x: 0.5, y: 0.5 },
|
origin: { x: 0.5, y: 0.5 },
|
||||||
},
|
},
|
||||||
|
@ -139,11 +140,16 @@ const components = {
|
||||||
const CameraOptionsControlPanel = track(() => {
|
const CameraOptionsControlPanel = track(() => {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex', CAMERA_OPTIONS)
|
const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex1', CAMERA_OPTIONS)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
editor.setCameraOptions(cameraOptions, { immediate: true })
|
editor.batch(() => {
|
||||||
|
editor.setCameraOptions(cameraOptions, { immediate: true })
|
||||||
|
editor.setCamera(editor.getCamera(), {
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
}, [editor, cameraOptions])
|
}, [editor, cameraOptions])
|
||||||
|
|
||||||
const { constraints } = cameraOptions
|
const { constraints } = cameraOptions
|
||||||
|
@ -234,10 +240,12 @@ const CameraOptionsControlPanel = track(() => {
|
||||||
<input
|
<input
|
||||||
name="zoomsteps"
|
name="zoomsteps"
|
||||||
type="text"
|
type="text"
|
||||||
value={cameraOptions.zoomSteps.join(', ')}
|
defaultValue={cameraOptions.zoomSteps.join(', ')}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value.split(', ').map((v) => Number(v))
|
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>
|
<label htmlFor="bounds">Bounds</label>
|
||||||
|
@ -275,24 +283,43 @@ const CameraOptionsControlPanel = track(() => {
|
||||||
</select>
|
</select>
|
||||||
{constraints ? (
|
{constraints ? (
|
||||||
<>
|
<>
|
||||||
<label htmlFor="fit">Fit</label>
|
<label htmlFor="initialZoom">Initial Zoom</label>
|
||||||
<select
|
<select
|
||||||
name="fit"
|
name="initialZoom"
|
||||||
value={constraints.fit}
|
value={constraints.initialZoom}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateOptions({
|
updateOptions({
|
||||||
constraints: {
|
constraints: {
|
||||||
...constraints,
|
...constraints,
|
||||||
fit: e.target.value as any,
|
initialZoom: e.target.value as any,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option>min</option>
|
<option>fit-min</option>
|
||||||
<option>max</option>
|
<option>fit-max</option>
|
||||||
<option> x</option>
|
<option>fit-x</option>
|
||||||
<option> y</option>
|
<option>fit-y</option>
|
||||||
<option>none</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>
|
</select>
|
||||||
<label htmlFor="originX">Origin X</label>
|
<label htmlFor="originX">Origin X</label>
|
||||||
<input
|
<input
|
||||||
|
@ -368,16 +395,19 @@ const CameraOptionsControlPanel = track(() => {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="fitx">Fit X</label>
|
<label htmlFor="behaviorX">Behavior X</label>
|
||||||
<select
|
<select
|
||||||
name="fitx"
|
name="behaviorX"
|
||||||
value={constraints.fitX}
|
value={(constraints.behavior as { x: any; y: any }).x}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCameraOptions({
|
setCameraOptions({
|
||||||
...cameraOptions,
|
...cameraOptions,
|
||||||
constraints: {
|
constraints: {
|
||||||
...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>outside</option>
|
||||||
<option>lock</option>
|
<option>lock</option>
|
||||||
</select>
|
</select>
|
||||||
<label htmlFor="fity">Fit Y</label>
|
<label htmlFor="behaviorY">Behavior Y</label>
|
||||||
<select
|
<select
|
||||||
name="fity"
|
name="behaviorY"
|
||||||
value={constraints.fitY}
|
value={(constraints.behavior as { x: any; y: any }).y}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCameraOptions({
|
setCameraOptions({
|
||||||
...cameraOptions,
|
...cameraOptions,
|
||||||
constraints: {
|
constraints: {
|
||||||
...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 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.setCamera(editor.getCamera(), { initial: true })
|
editor.setCamera(editor.getCamera(), { reset: true })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Camera
|
Reset Camera
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default function ExternalContentSourcesExample() {
|
||||||
const htmlSource = sources?.find((s) => s.type === 'text' && s.subtype === 'html')
|
const htmlSource = sources?.find((s) => s.type === 'text' && s.subtype === 'html')
|
||||||
|
|
||||||
if (htmlSource) {
|
if (htmlSource) {
|
||||||
const center = point ?? editor.getViewportPageCenter()
|
const center = point ?? editor.getViewportPageBounds().center
|
||||||
|
|
||||||
editor.createShape({
|
editor.createShape({
|
||||||
type: 'html',
|
type: 'html',
|
||||||
|
|
|
@ -133,19 +133,19 @@ export function ImageAnnotationEditor({
|
||||||
editor.setCameraOptions(
|
editor.setCameraOptions(
|
||||||
{
|
{
|
||||||
constraints: {
|
constraints: {
|
||||||
fit: 'max',
|
initialZoom: 'fit-max',
|
||||||
|
baseZoom: 'default',
|
||||||
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
|
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
|
||||||
padding: { x: 32, y: 64 },
|
padding: { x: 32, y: 64 },
|
||||||
origin: { x: 0.5, y: 0.5 },
|
origin: { x: 0.5, y: 0.5 },
|
||||||
fitX: 'inside',
|
behavior: 'inside',
|
||||||
fitY: 'inside',
|
|
||||||
},
|
},
|
||||||
zoomSteps: [1, 2, 4, 8],
|
zoomSteps: [1, 2, 4, 8],
|
||||||
zoomSpeed: 1,
|
zoomSpeed: 1,
|
||||||
panSpeed: 1,
|
panSpeed: 1,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
},
|
},
|
||||||
{ initial: isInitial }
|
{ reset: isInitial }
|
||||||
)
|
)
|
||||||
|
|
||||||
isInitial = false
|
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}
|
text={text}
|
||||||
labelColor={theme[color].solid}
|
labelColor={theme[color].solid}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
disableTab
|
|
||||||
wrap
|
wrap
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -50,7 +50,6 @@ const ZOOM_EVENT = {
|
||||||
'reset-zoom': 'resetZoom',
|
'reset-zoom': 'resetZoom',
|
||||||
'zoom-to-fit': 'zoomToFit',
|
'zoom-to-fit': 'zoomToFit',
|
||||||
'zoom-to-selection': 'zoomToSelection',
|
'zoom-to-selection': 'zoomToSelection',
|
||||||
'zoom-to-content': 'zoomToContent',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCodeSnippet(name: string, data: any) {
|
export function getCodeSnippet(name: string, data: any) {
|
||||||
|
@ -136,15 +135,11 @@ if (updates.length > 0) {
|
||||||
} else if (name === 'fit-frame-to-content') {
|
} else if (name === 'fit-frame-to-content') {
|
||||||
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
|
codeSnippet = `fitFrameToContent(editor, editor.getOnlySelectedShape().id)`
|
||||||
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
|
} else if (name.startsWith('zoom-') || name === 'reset-zoom') {
|
||||||
if (name === 'zoom-to-content') {
|
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
|
||||||
codeSnippet = 'editor.zoomToContent()'
|
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
|
||||||
} else {
|
? 'editor.getViewportScreenCenter(), '
|
||||||
codeSnippet = `editor.${ZOOM_EVENT[name as keyof typeof ZOOM_EVENT]}(${
|
: ''
|
||||||
name !== 'zoom-to-fit' && name !== 'zoom-to-selection'
|
}{ duration: 320 })`
|
||||||
? 'editor.getViewportScreenCenter(), '
|
|
||||||
: ''
|
|
||||||
}{ duration: 320 })`
|
|
||||||
}
|
|
||||||
} else if (name.startsWith('toggle-')) {
|
} else if (name.startsWith('toggle-')) {
|
||||||
if (name === 'toggle-lock') {
|
if (name === 'toggle-lock') {
|
||||||
codeSnippet = `editor.toggleLock(editor.getSelectedShapeIds())`
|
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
|
## 2.0.28
|
||||||
|
|
||||||
- Fix an issue with panning the canvas.
|
- Fix an issue with panning the canvas.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "tldraw-vscode",
|
"name": "tldraw-vscode",
|
||||||
"description": "The tldraw extension for VS Code.",
|
"description": "The tldraw extension for VS Code.",
|
||||||
"version": "2.0.28",
|
"version": "2.0.30",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "tldraw Inc.",
|
"name": "tldraw Inc.",
|
||||||
|
|
|
@ -11,6 +11,10 @@ import { TextDecoder, TextEncoder } from 'util'
|
||||||
global.TextEncoder = TextEncoder
|
global.TextEncoder = TextEncoder
|
||||||
global.TextDecoder = TextDecoder
|
global.TextDecoder = TextDecoder
|
||||||
|
|
||||||
|
Image.prototype.decode = async function () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function convertNumbersInObject(obj: any, roundToNearest: number) {
|
function convertNumbersInObject(obj: any, roundToNearest: number) {
|
||||||
if (!obj) return obj
|
if (!obj) return obj
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
|
|
|
@ -459,6 +459,9 @@ export const DEFAULT_ANIMATION_OPTIONS: {
|
||||||
easing: (t: number) => number;
|
easing: (t: number) => number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @internal (undocumented)
|
||||||
|
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function DefaultBackground(): JSX_2.Element;
|
export function DefaultBackground(): JSX_2.Element;
|
||||||
|
|
||||||
|
@ -593,7 +596,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}>;
|
}>;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
immediate: boolean;
|
immediate: boolean;
|
||||||
initial: boolean;
|
reset: boolean;
|
||||||
}>): this;
|
}>): this;
|
||||||
animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: Partial<{
|
animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: Partial<{
|
||||||
animation: Partial<{
|
animation: Partial<{
|
||||||
|
@ -602,7 +605,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}>;
|
}>;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
immediate: boolean;
|
immediate: boolean;
|
||||||
initial: boolean;
|
reset: boolean;
|
||||||
}>): this;
|
}>): this;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: {
|
annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: {
|
||||||
|
@ -689,12 +692,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
|
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
|
||||||
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
||||||
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
||||||
|
getBaseZoom(): number;
|
||||||
getCamera(): TLCamera;
|
getCamera(): TLCamera;
|
||||||
getCameraFitZoom(): number;
|
|
||||||
getCameraOptions(): TLCameraOptions;
|
getCameraOptions(): TLCameraOptions;
|
||||||
getCameraState(): "idle" | "moving";
|
getCameraState(): "idle" | "moving";
|
||||||
getCanRedo(): boolean;
|
getCanRedo(): boolean;
|
||||||
getCanUndo(): boolean;
|
getCanUndo(): boolean;
|
||||||
|
getCollaborators(): TLInstancePresence[];
|
||||||
|
getCollaboratorsOnCurrentPage(): TLInstancePresence[];
|
||||||
getContainer: () => HTMLElement;
|
getContainer: () => HTMLElement;
|
||||||
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
|
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
|
||||||
// @internal
|
// @internal
|
||||||
|
@ -706,6 +711,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getCurrentPageId(): TLPageId;
|
getCurrentPageId(): TLPageId;
|
||||||
getCurrentPageRenderingShapesSorted(): TLShape[];
|
getCurrentPageRenderingShapesSorted(): TLShape[];
|
||||||
getCurrentPageShapeIds(): Set<TLShapeId>;
|
getCurrentPageShapeIds(): Set<TLShapeId>;
|
||||||
|
// @internal (undocumented)
|
||||||
|
getCurrentPageShapeIdsSorted(): TLShapeId[];
|
||||||
getCurrentPageShapes(): TLShape[];
|
getCurrentPageShapes(): TLShape[];
|
||||||
getCurrentPageShapesSorted(): TLShape[];
|
getCurrentPageShapesSorted(): TLShape[];
|
||||||
getCurrentPageState(): TLInstancePageState;
|
getCurrentPageState(): TLInstancePageState;
|
||||||
|
@ -725,6 +732,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getHoveredShape(): TLShape | undefined;
|
getHoveredShape(): TLShape | undefined;
|
||||||
getHoveredShapeId(): null | TLShapeId;
|
getHoveredShapeId(): null | TLShapeId;
|
||||||
getInitialMetaForShape(_shape: TLShape): JsonObject;
|
getInitialMetaForShape(_shape: TLShape): JsonObject;
|
||||||
|
getInitialZoom(): number;
|
||||||
getInstanceState(): TLInstance;
|
getInstanceState(): TLInstance;
|
||||||
getIsMenuOpen(): boolean;
|
getIsMenuOpen(): boolean;
|
||||||
getOnlySelectedShape(): null | TLShape;
|
getOnlySelectedShape(): null | TLShape;
|
||||||
|
@ -738,7 +746,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getPath(): string;
|
getPath(): string;
|
||||||
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
||||||
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec;
|
||||||
getRenderingBounds(): Box;
|
|
||||||
getRenderingShapes(): {
|
getRenderingShapes(): {
|
||||||
backgroundIndex: number;
|
backgroundIndex: number;
|
||||||
id: TLShapeId;
|
id: TLShapeId;
|
||||||
|
@ -813,7 +820,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
util: ShapeUtil;
|
util: ShapeUtil;
|
||||||
}[];
|
}[];
|
||||||
getViewportPageBounds(): Box;
|
getViewportPageBounds(): Box;
|
||||||
getViewportPageCenter(): Vec;
|
|
||||||
getViewportScreenBounds(): Box;
|
getViewportScreenBounds(): Box;
|
||||||
getViewportScreenCenter(): Vec;
|
getViewportScreenCenter(): Vec;
|
||||||
getZoomLevel(): number;
|
getZoomLevel(): number;
|
||||||
|
@ -860,8 +866,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
|
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
|
||||||
pageToScreen(point: VecLike): Vec;
|
pageToScreen(point: VecLike): Vec;
|
||||||
pageToViewport(point: VecLike): Vec;
|
pageToViewport(point: VecLike): Vec;
|
||||||
pan(offset: VecLike, opts?: TLCameraMoveOptions): this;
|
|
||||||
panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this;
|
|
||||||
popFocusedGroupId(): this;
|
popFocusedGroupId(): this;
|
||||||
putContentOntoCurrentPage(content: TLContent, options?: {
|
putContentOntoCurrentPage(content: TLContent, options?: {
|
||||||
point?: VecLike;
|
point?: VecLike;
|
||||||
|
@ -878,7 +882,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
} : TLExternalContent) => void) | null): this;
|
} : TLExternalContent) => void) | null): this;
|
||||||
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
|
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
renderingBoundsMargin: number;
|
|
||||||
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
|
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this;
|
||||||
resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this;
|
resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this;
|
||||||
resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): 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;
|
sendBackward(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
|
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
setCamera(point: VecLike, opts?: TLCameraMoveOptions): this;
|
setCamera(point: VecLike, opts?: TLCameraMoveOptions): this;
|
||||||
setCameraOptions(options: Partial<TLCameraOptions>, opts?: {
|
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions): this;
|
||||||
force?: boolean;
|
|
||||||
immediate?: boolean;
|
|
||||||
initial?: boolean;
|
|
||||||
}): this;
|
|
||||||
setCroppingShape(shape: null | TLShape | TLShapeId): this;
|
setCroppingShape(shape: null | TLShape | TLShapeId): this;
|
||||||
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
|
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
setCurrentTool(id: string, info?: {}): this;
|
setCurrentTool(id: string, info?: {}): this;
|
||||||
|
@ -943,8 +942,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
||||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
|
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, 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;
|
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
updateShapes<T extends TLUnknownShape>(partials: (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;
|
updateViewportScreenBounds(screenBounds: Box, center?: boolean): this;
|
||||||
|
@ -956,10 +953,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
inset?: number;
|
inset?: number;
|
||||||
targetZoom?: number;
|
targetZoom?: number;
|
||||||
} & TLCameraMoveOptions): this;
|
} & TLCameraMoveOptions): this;
|
||||||
zoomToContent(opts?: TLCameraMoveOptions): this;
|
|
||||||
zoomToFit(opts?: TLCameraMoveOptions): this;
|
zoomToFit(opts?: TLCameraMoveOptions): this;
|
||||||
zoomToSelection(opts?: TLCameraMoveOptions): this;
|
zoomToSelection(opts?: TLCameraMoveOptions): this;
|
||||||
zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this;
|
|
||||||
zoomToUser(userId: string, opts?: TLCameraMoveOptions): this;
|
zoomToUser(userId: string, opts?: TLCameraMoveOptions): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1093,9 +1088,6 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string;
|
export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string;
|
||||||
|
|
||||||
// @internal (undocumented)
|
|
||||||
export const getDefaultCameraOptions: (cameraOptions?: Partial<TLCameraOptions>) => TLCameraOptions;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getFreshUserPreferences(): TLUserPreferences;
|
export function getFreshUserPreferences(): TLUserPreferences;
|
||||||
|
|
||||||
|
@ -1997,23 +1989,27 @@ export type TLBrushProps = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLCameraMoveOptions = Partial<{
|
export type TLCameraMoveOptions = Partial<{
|
||||||
animation: Partial<{
|
animation: Partial<{
|
||||||
duration: number;
|
|
||||||
easing: (t: number) => number;
|
easing: (t: number) => number;
|
||||||
|
duration: number;
|
||||||
}>;
|
}>;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
immediate: boolean;
|
immediate: boolean;
|
||||||
initial: boolean;
|
reset: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLCameraOptions = {
|
export type TLCameraOptions = {
|
||||||
|
wheelBehavior: 'none' | 'pan' | 'zoom';
|
||||||
constraints?: {
|
constraints?: {
|
||||||
fitX: 'contain' | 'inside' | 'lock' | 'outside';
|
behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | {
|
||||||
fitY: 'contain' | 'inside' | 'lock' | 'outside';
|
x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
|
||||||
|
y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside';
|
||||||
|
};
|
||||||
bounds: BoxModel;
|
bounds: BoxModel;
|
||||||
|
baseZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
|
||||||
|
initialZoom: 'default' | 'fit-max' | 'fit-min' | 'fit-x' | 'fit-y';
|
||||||
origin: VecLike;
|
origin: VecLike;
|
||||||
padding: VecLike;
|
padding: VecLike;
|
||||||
fit: 'max' | 'min' | 'none' | 'x' | 'y';
|
|
||||||
};
|
};
|
||||||
panSpeed: number;
|
panSpeed: number;
|
||||||
zoomSpeed: number;
|
zoomSpeed: number;
|
||||||
|
|
|
@ -7554,7 +7554,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Content",
|
||||||
|
@ -7641,7 +7641,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Content",
|
||||||
|
@ -9838,6 +9838,37 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getAssets"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getCamera:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getCamera:member(1)",
|
||||||
|
@ -9874,37 +9905,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getCamera"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)",
|
||||||
|
@ -10030,6 +10030,86 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getCanUndo"
|
"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",
|
"kind": "Property",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getContainer:member",
|
"canonicalReference": "@tldraw/editor!Editor#getContainer:member",
|
||||||
|
@ -11169,6 +11249,37 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getInitialMetaForShape"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getInstanceState:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getInstanceState:member(1)",
|
||||||
|
@ -11809,38 +11920,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getPointInShapeSpace"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)",
|
||||||
|
@ -14146,38 +14225,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getViewportPageBounds"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#getViewportScreenBounds:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#getViewportScreenBounds:member(1)",
|
||||||
|
@ -15589,142 +15636,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "pageToViewport"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#popFocusedGroupId:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#popFocusedGroupId:member(1)",
|
||||||
|
@ -16240,36 +16151,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "renamePage"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#reparentShapes:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#reparentShapes:member(1)",
|
||||||
|
@ -17060,8 +16941,9 @@
|
||||||
"text": ", opts?: "
|
"text": ", opts?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Reference",
|
||||||
"text": "{\n force?: boolean;\n immediate?: boolean;\n initial?: boolean;\n }"
|
"text": "TLCameraMoveOptions",
|
||||||
|
"canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -19799,55 +19681,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "zoomToBounds"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)",
|
||||||
|
@ -19946,72 +19779,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "zoomToSelection"
|
"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",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)",
|
"canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)",
|
||||||
|
@ -36940,14 +36707,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Content",
|
||||||
"text": ";"
|
"text": ";"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fileUrlPath": "packages/editor/src/lib/editor/Editor.ts",
|
"fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts",
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"name": "TLCameraMoveOptions",
|
"name": "TLCameraMoveOptions",
|
||||||
"typeTokenRange": {
|
"typeTokenRange": {
|
||||||
|
@ -36966,7 +36733,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Reference",
|
||||||
|
@ -36975,7 +36742,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Reference",
|
||||||
|
@ -36993,7 +36760,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Content",
|
||||||
|
|
|
@ -111,6 +111,7 @@ export {
|
||||||
ANIMATION_SHORT_MS,
|
ANIMATION_SHORT_MS,
|
||||||
CAMERA_SLIDE_FRICTION,
|
CAMERA_SLIDE_FRICTION,
|
||||||
DEFAULT_ANIMATION_OPTIONS,
|
DEFAULT_ANIMATION_OPTIONS,
|
||||||
|
DEFAULT_CAMERA_OPTIONS,
|
||||||
DOUBLE_CLICK_DURATION,
|
DOUBLE_CLICK_DURATION,
|
||||||
DRAG_DISTANCE,
|
DRAG_DISTANCE,
|
||||||
GRID_STEPS,
|
GRID_STEPS,
|
||||||
|
@ -120,14 +121,8 @@ export {
|
||||||
MULTI_CLICK_DURATION,
|
MULTI_CLICK_DURATION,
|
||||||
SIDES,
|
SIDES,
|
||||||
SVG_PADDING,
|
SVG_PADDING,
|
||||||
getDefaultCameraOptions,
|
|
||||||
} from './lib/constants'
|
} from './lib/constants'
|
||||||
export {
|
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
||||||
Editor,
|
|
||||||
type TLCameraMoveOptions,
|
|
||||||
type TLEditorOptions,
|
|
||||||
type TLResizeShapeOptions,
|
|
||||||
} from './lib/editor/Editor'
|
|
||||||
export type {
|
export type {
|
||||||
SideEffectManager,
|
SideEffectManager,
|
||||||
TLAfterChangeHandler,
|
TLAfterChangeHandler,
|
||||||
|
@ -240,6 +235,7 @@ export {
|
||||||
} from './lib/editor/types/history-types'
|
} from './lib/editor/types/history-types'
|
||||||
export {
|
export {
|
||||||
type RequiredKeys,
|
type RequiredKeys,
|
||||||
|
type TLCameraMoveOptions,
|
||||||
type TLCameraOptions,
|
type TLCameraOptions,
|
||||||
type TLSvgOptions,
|
type TLSvgOptions,
|
||||||
} from './lib/editor/types/misc-types'
|
} from './lib/editor/types/misc-types'
|
||||||
|
|
|
@ -11,24 +11,15 @@ export const ANIMATION_SHORT_MS = 80
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const ANIMATION_MEDIUM_MS = 320
|
export const ANIMATION_MEDIUM_MS = 320
|
||||||
|
|
||||||
const DEFAULT_COMMON_CAMERA_OPTIONS = {
|
/** @internal */
|
||||||
zoomMax: 8,
|
export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
|
||||||
zoomMin: 0.1,
|
|
||||||
zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
|
|
||||||
zoomSpeed: 1,
|
|
||||||
panSpeed: 1,
|
|
||||||
isLocked: false,
|
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
|
export const FOLLOW_CHASE_PROPORTION = 0.5
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const FOLLOW_CHASE_PAN_SNAP = 0.1
|
export const FOLLOW_CHASE_PAN_SNAP = 0.1
|
||||||
|
@ -116,3 +107,8 @@ export const LONG_PRESS_DURATION = 500
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const TEXT_SHADOW_LOD = 0.35
|
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 { computed, isUninitialized } from '@tldraw/state'
|
||||||
import { TLPageId, TLShapeId, isShape, isShapeId } from '@tldraw/tlschema'
|
import { TLShapeId } from '@tldraw/tlschema'
|
||||||
import { Box } from '../../primitives/Box'
|
import { Box } from '../../primitives/Box'
|
||||||
import { Editor } from '../Editor'
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
|
@ -20,16 +20,9 @@ function isShapeNotVisible(editor: Editor, id: TLShapeId, viewportPageBounds: Bo
|
||||||
* @returns Incremental derivation of non visible shapes.
|
* @returns Incremental derivation of non visible shapes.
|
||||||
*/
|
*/
|
||||||
export const notVisibleShapes = (editor: Editor) => {
|
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> {
|
function fromScratch(editor: Editor): Set<TLShapeId> {
|
||||||
const shapes = editor.getCurrentPageShapeIds()
|
const shapes = editor.getCurrentPageShapeIds()
|
||||||
lastPageId = editor.getCurrentPageId()
|
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
const viewportPageBounds = editor.getViewportPageBounds()
|
||||||
prevViewportPageBounds = viewportPageBounds.clone()
|
|
||||||
const notVisibleShapes = new Set<TLShapeId>()
|
const notVisibleShapes = new Set<TLShapeId>()
|
||||||
shapes.forEach((id) => {
|
shapes.forEach((id) => {
|
||||||
if (isShapeNotVisible(editor, id, viewportPageBounds)) {
|
if (isShapeNotVisible(editor, id, viewportPageBounds)) {
|
||||||
|
@ -38,68 +31,19 @@ export const notVisibleShapes = (editor: Editor) => {
|
||||||
})
|
})
|
||||||
return notVisibleShapes
|
return notVisibleShapes
|
||||||
}
|
}
|
||||||
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue, lastComputedEpoch) => {
|
return computed<Set<TLShapeId>>('getCulledShapes', (prevValue) => {
|
||||||
if (!isCullingOffScreenShapes) return new Set<TLShapeId>()
|
|
||||||
|
|
||||||
if (isUninitialized(prevValue)) {
|
if (isUninitialized(prevValue)) {
|
||||||
return fromScratch(editor)
|
return fromScratch(editor)
|
||||||
}
|
}
|
||||||
const diff = shapeHistory.getDiffSince(lastComputedEpoch)
|
|
||||||
|
|
||||||
if (diff === RESET_VALUE) {
|
const nextValue = fromScratch(editor)
|
||||||
return fromScratch(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPageId = editor.getCurrentPageId()
|
if (prevValue.size !== nextValue.size) return nextValue
|
||||||
if (lastPageId !== currentPageId) {
|
for (const prev of prevValue) {
|
||||||
return fromScratch(editor)
|
if (!nextValue.has(prev)) {
|
||||||
}
|
return nextValue
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
|
||||||
if (!prevViewportPageBounds || !viewportPageBounds.equals(prevViewportPageBounds)) {
|
|
||||||
return fromScratch(editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextValue = null as null | Set<TLShapeId>
|
|
||||||
const addId = (id: TLShapeId) => {
|
|
||||||
// Already added
|
|
||||||
if (prevValue.has(id)) return
|
|
||||||
if (!nextValue) nextValue = new Set(prevValue)
|
|
||||||
nextValue.add(id)
|
|
||||||
}
|
|
||||||
const deleteId = (id: TLShapeId) => {
|
|
||||||
// No need to delete since it's not there
|
|
||||||
if (!prevValue.has(id)) return
|
|
||||||
if (!nextValue) nextValue = new Set(prevValue)
|
|
||||||
nextValue.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const changes of diff) {
|
|
||||||
for (const record of Object.values(changes.added)) {
|
|
||||||
if (isShape(record)) {
|
|
||||||
const isCulled = isShapeNotVisible(editor, record.id, viewportPageBounds)
|
|
||||||
if (isCulled) {
|
|
||||||
addId(record.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [_from, to] of Object.values(changes.updated)) {
|
|
||||||
if (isShape(to)) {
|
|
||||||
const isCulled = isShapeNotVisible(editor, to.id, viewportPageBounds)
|
|
||||||
if (isCulled) {
|
|
||||||
addId(to.id)
|
|
||||||
} else {
|
|
||||||
deleteId(to.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id of Object.keys(changes.removed)) {
|
|
||||||
if (isShapeId(id)) {
|
|
||||||
deleteId(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return prevValue
|
||||||
return nextValue ?? prevValue
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,116 +95,118 @@ export class ClickManager {
|
||||||
|
|
||||||
lastPointerInfo = {} as TLPointerEventInfo
|
lastPointerInfo = {} as TLPointerEventInfo
|
||||||
|
|
||||||
/**
|
handlePointerEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
||||||
* Start the double click timeout.
|
switch (info.name) {
|
||||||
*
|
case 'pointer_down': {
|
||||||
* @param info - The event info.
|
if (!this._clickState) return info
|
||||||
*/
|
this._clickScreenPoint = Vec.From(info.point)
|
||||||
transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
|
||||||
if (!this._clickState) return info
|
|
||||||
|
|
||||||
this._clickScreenPoint = Vec.From(info.point)
|
if (
|
||||||
|
this._previousScreenPoint &&
|
||||||
if (
|
Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2
|
||||||
this._previousScreenPoint &&
|
) {
|
||||||
this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE
|
this._clickState = 'idle'
|
||||||
) {
|
|
||||||
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',
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case 'pendingTriple': {
|
this._previousScreenPoint = this._clickScreenPoint
|
||||||
this._clickState = 'pendingQuadruple'
|
|
||||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
this.lastPointerInfo = info
|
||||||
return {
|
|
||||||
...info,
|
switch (this._clickState) {
|
||||||
type: 'click',
|
case 'pendingDouble': {
|
||||||
name: 'triple_click',
|
this._clickState = 'pendingTriple'
|
||||||
phase: 'down',
|
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)
|
this._clickTimeout = this._getClickTimeout(this._clickState)
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
default: {
|
case 'pointer_up': {
|
||||||
// overflow
|
if (!this._clickState) return info
|
||||||
this._clickTimeout = this._getClickTimeout(this._clickState)
|
this._clickScreenPoint = Vec.From(info.point)
|
||||||
return info
|
|
||||||
}
|
switch (this._clickState) {
|
||||||
}
|
case 'pendingTriple': {
|
||||||
}
|
return {
|
||||||
|
...this.lastPointerInfo,
|
||||||
/**
|
type: 'click',
|
||||||
* Emit click_up events on pointer up.
|
name: 'double_click',
|
||||||
*
|
phase: 'up',
|
||||||
* @param info - The event info.
|
}
|
||||||
*/
|
}
|
||||||
transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
|
case 'pendingQuadruple': {
|
||||||
if (!this._clickState) return info
|
return {
|
||||||
|
...this.lastPointerInfo,
|
||||||
this._clickScreenPoint = Vec.From(info.point)
|
type: 'click',
|
||||||
|
name: 'triple_click',
|
||||||
switch (this._clickState) {
|
phase: 'up',
|
||||||
case 'pendingTriple': {
|
}
|
||||||
return {
|
}
|
||||||
...this.lastPointerInfo,
|
case 'pendingOverflow': {
|
||||||
type: 'click',
|
return {
|
||||||
name: 'double_click',
|
...this.lastPointerInfo,
|
||||||
phase: 'up',
|
type: 'click',
|
||||||
}
|
name: 'quadruple_click',
|
||||||
}
|
phase: 'up',
|
||||||
case 'pendingQuadruple': {
|
}
|
||||||
return {
|
}
|
||||||
...this.lastPointerInfo,
|
default: {
|
||||||
type: 'click',
|
// idle, pendingDouble, overflow
|
||||||
name: 'triple_click',
|
}
|
||||||
phase: 'up',
|
}
|
||||||
}
|
|
||||||
}
|
return info
|
||||||
case 'pendingOverflow': {
|
}
|
||||||
return {
|
case 'pointer_move': {
|
||||||
...this.lastPointerInfo,
|
if (
|
||||||
type: 'click',
|
this._clickState !== 'idle' &&
|
||||||
name: 'quadruple_click',
|
this._clickScreenPoint &&
|
||||||
phase: 'up',
|
Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) >
|
||||||
}
|
(this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
|
||||||
}
|
) {
|
||||||
default: {
|
this.cancelDoubleClickTimeout()
|
||||||
// idle, pendingDouble, overflow
|
}
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -216,21 +218,4 @@ export class ClickManager {
|
||||||
this._clickTimeout = clearTimeout(this._clickTimeout)
|
this._clickTimeout = clearTimeout(this._clickTimeout)
|
||||||
this._clickState = 'idle'
|
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
|
// TODO: make this an incremental derivation
|
||||||
@computed getSnappableShapes(): Set<TLShapeId> {
|
@computed getSnappableShapes(): Set<TLShapeId> {
|
||||||
const { editor } = this
|
const { editor } = this
|
||||||
const renderingBounds = editor.getRenderingBounds()
|
const renderingBounds = editor.getViewportPageBounds()
|
||||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||||
|
|
||||||
const snappableShapes: Set<TLShapeId> = new Set()
|
const snappableShapes: Set<TLShapeId> = new Set()
|
||||||
|
|
|
@ -17,8 +17,27 @@ export type TLSvgOptions = {
|
||||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
|
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 */
|
/** @public */
|
||||||
export type TLCameraOptions = {
|
export type TLCameraOptions = {
|
||||||
|
/** Controls whether the wheel pans or zooms. */
|
||||||
|
wheelBehavior: 'zoom' | 'pan' | 'none'
|
||||||
/** The speed of a scroll wheel / trackpad pan */
|
/** The speed of a scroll wheel / trackpad pan */
|
||||||
panSpeed: number
|
panSpeed: number
|
||||||
/** The speed of a scroll wheel / trackpad zoom */
|
/** The speed of a scroll wheel / trackpad zoom */
|
||||||
|
@ -29,17 +48,26 @@ export type TLCameraOptions = {
|
||||||
isLocked: boolean
|
isLocked: boolean
|
||||||
/** The camera constraints */
|
/** The camera constraints */
|
||||||
constraints?: {
|
constraints?: {
|
||||||
/** The type of constraint behavior. */
|
/** The bounds (in page space) of the constrained space */
|
||||||
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) */
|
|
||||||
bounds: BoxModel
|
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
|
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
|
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 React, { useMemo } from 'react'
|
||||||
|
import { RIGHT_MOUSE_BUTTON } from '../constants'
|
||||||
import {
|
import {
|
||||||
preventDefault,
|
preventDefault,
|
||||||
releasePointerCapture,
|
releasePointerCapture,
|
||||||
|
@ -19,7 +20,7 @@ export function useCanvasEvents() {
|
||||||
function onPointerDown(e: React.PointerEvent) {
|
function onPointerDown(e: React.PointerEvent) {
|
||||||
if ((e as any).isKilled) return
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
if (e.button === 2) {
|
if (e.button === RIGHT_MOUSE_BUTTON) {
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
type: 'pointer',
|
type: 'pointer',
|
||||||
target: 'canvas',
|
target: 'canvas',
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useComputed, useValue } from '@tldraw/state'
|
import { useComputed, useValue } from '@tldraw/state'
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { uniq } from '../utils/uniq'
|
import { uniq } from '../utils/uniq'
|
||||||
import { useEditor } from './useEditor'
|
import { useEditor } from './useEditor'
|
||||||
|
|
||||||
|
@ -10,17 +9,12 @@ import { useEditor } from './useEditor'
|
||||||
*/
|
*/
|
||||||
export function usePeerIds() {
|
export function usePeerIds() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const $presences = useMemo(() => {
|
|
||||||
return editor.store.query.records('instance_presence', () => ({
|
|
||||||
userId: { neq: editor.user.getId() },
|
|
||||||
}))
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
const $userIds = useComputed(
|
const $userIds = useComputed(
|
||||||
'userIds',
|
'userIds',
|
||||||
() => uniq($presences.get().map((p) => p.userId)).sort(),
|
() => uniq(editor.getCollaborators().map((p) => p.userId)).sort(),
|
||||||
{ isEqual: (a, b) => a.join(',') === b.join?.(',') },
|
{ isEqual: (a, b) => a.join(',') === b.join?.(',') },
|
||||||
[$presences]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
return useValue($userIds)
|
return useValue($userIds)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useValue } from '@tldraw/state'
|
import { useValue } from '@tldraw/state'
|
||||||
import { TLInstancePresence } from '@tldraw/tlschema'
|
import { TLInstancePresence } from '@tldraw/tlschema'
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { useEditor } from './useEditor'
|
import { useEditor } from './useEditor'
|
||||||
|
|
||||||
// TODO: maybe move this to a computed property on the App class?
|
// 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 {
|
export function usePresence(userId: string): TLInstancePresence | null {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const $presences = useMemo(() => {
|
|
||||||
return editor.store.query.records('instance_presence', () => ({
|
|
||||||
userId: { eq: userId },
|
|
||||||
}))
|
|
||||||
}, [editor, userId])
|
|
||||||
|
|
||||||
const latestPresence = useValue(
|
const latestPresence = useValue(
|
||||||
`latestPresence:${userId}`,
|
`latestPresence:${userId}`,
|
||||||
() => {
|
() => {
|
||||||
return $presences
|
return editor.getCollaborators().find((c) => c.userId === userId)
|
||||||
.get()
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => b.lastActivityTimestamp - a.lastActivityTimestamp)[0]
|
|
||||||
},
|
},
|
||||||
[]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
return latestPresence ?? null
|
return latestPresence ?? null
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { RIGHT_MOUSE_BUTTON } from '../constants'
|
||||||
import { TLSelectionHandle } from '../editor/types/selection-types'
|
import { TLSelectionHandle } from '../editor/types/selection-types'
|
||||||
import {
|
import {
|
||||||
loopToHtmlElement,
|
loopToHtmlElement,
|
||||||
|
@ -18,7 +19,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
||||||
const onPointerDown: React.PointerEventHandler = (e) => {
|
const onPointerDown: React.PointerEventHandler = (e) => {
|
||||||
if ((e as any).isKilled) return
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
if (e.button === 2) {
|
if (e.button === RIGHT_MOUSE_BUTTON) {
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
type: 'pointer',
|
type: 'pointer',
|
||||||
target: 'selection',
|
target: 'selection',
|
||||||
|
|
|
@ -39,12 +39,13 @@ export class Mat {
|
||||||
|
|
||||||
equals(m: Mat | MatModel) {
|
equals(m: Mat | MatModel) {
|
||||||
return (
|
return (
|
||||||
this.a === m.a &&
|
this === m ||
|
||||||
this.b === m.b &&
|
(this.a === m.a &&
|
||||||
this.c === m.c &&
|
this.b === m.b &&
|
||||||
this.d === m.d &&
|
this.c === m.c &&
|
||||||
this.e === m.e &&
|
this.d === m.d &&
|
||||||
this.f === m.f
|
this.e === m.e &&
|
||||||
|
this.f === m.f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { HistoryBuffer } from './HistoryBuffer'
|
||||||
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
|
import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'
|
||||||
import { GLOBAL_START_EPOCH } from './constants'
|
import { GLOBAL_START_EPOCH } from './constants'
|
||||||
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'
|
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 { Child, ComputeDiff, RESET_VALUE, Signal } from './types'
|
||||||
import { logComputedGetterWarning } from './warnings'
|
import { logComputedGetterWarning } from './warnings'
|
||||||
|
|
||||||
|
@ -189,8 +189,15 @@ class __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff>
|
||||||
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {
|
__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {
|
||||||
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
|
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH
|
||||||
|
|
||||||
if (!isNew && (this.lastCheckedEpoch === getGlobalEpoch() || !haveParentsChanged(this))) {
|
const globalEpoch = getGlobalEpoch()
|
||||||
this.lastCheckedEpoch = getGlobalEpoch()
|
|
||||||
|
if (
|
||||||
|
!isNew &&
|
||||||
|
(this.lastCheckedEpoch === globalEpoch ||
|
||||||
|
(this.isActivelyListening && getIsReacting() && this.lastTraversedEpoch < globalEpoch) ||
|
||||||
|
!haveParentsChanged(this))
|
||||||
|
) {
|
||||||
|
this.lastCheckedEpoch = globalEpoch
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
if (!ignoreErrors) {
|
if (!ignoreErrors) {
|
||||||
throw this.error.thrownValue
|
throw this.error.thrownValue
|
||||||
|
|
|
@ -70,6 +70,10 @@ export function getGlobalEpoch() {
|
||||||
return inst.globalEpoch
|
return inst.globalEpoch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIsReacting() {
|
||||||
|
return inst.globalIsReacting
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect all of the reactors that need to run for an atom and run them.
|
* 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;
|
export function useDialogs(): TLUiDialogsContextType;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useEditableText(id: TLShapeId, type: string, text: string, opts?: {
|
export function useEditableText(id: TLShapeId, type: string, text: string): {
|
||||||
disableTab: boolean;
|
|
||||||
}): {
|
|
||||||
handleBlur: () => void;
|
handleBlur: () => void;
|
||||||
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
|
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
handleDoubleClick: (e: any) => any;
|
handleDoubleClick: (e: any) => any;
|
||||||
|
|
|
@ -15735,7 +15735,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
"text": ") => {\n id: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -15819,7 +15819,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
"text": ") => {\n id: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -15894,7 +15894,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ") => {\n id: import(\"@tldraw/editor\")."
|
"text": ") => {\n id: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -15903,7 +15903,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"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",
|
"kind": "Reference",
|
||||||
|
@ -27480,14 +27480,6 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "string"
|
"text": "string"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ", opts?: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "{\n disableTab: boolean;\n}"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "): "
|
"text": "): "
|
||||||
|
@ -27575,8 +27567,8 @@
|
||||||
],
|
],
|
||||||
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
|
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 9,
|
"startIndex": 7,
|
||||||
"endIndex": 26
|
"endIndex": 24
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -27604,14 +27596,6 @@
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameterName": "opts",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 7,
|
|
||||||
"endIndex": 8
|
|
||||||
},
|
|
||||||
"isOptional": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "useEditableText"
|
"name": "useEditableText"
|
||||||
|
|
|
@ -109,13 +109,10 @@ export function Tldraw(props: TldrawProps) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls)
|
||||||
|
|
||||||
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
|
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
|
||||||
|
|
||||||
if (preloadingError) {
|
if (preloadingError) {
|
||||||
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preloadingComplete) {
|
if (!preloadingComplete) {
|
||||||
return <LoadingScreen>Loading assets...</LoadingScreen>
|
return <LoadingScreen>Loading assets...</LoadingScreen>
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,9 @@ export function registerDefaultExternalContentHandlers(
|
||||||
editor.registerExternalContentHandler('svg-text', async ({ point, text }) => {
|
editor.registerExternalContentHandler('svg-text', async ({ point, text }) => {
|
||||||
const position =
|
const position =
|
||||||
point ??
|
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')
|
const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
|
||||||
if (!svg) {
|
if (!svg) {
|
||||||
|
@ -185,7 +187,9 @@ export function registerDefaultExternalContentHandlers(
|
||||||
editor.registerExternalContentHandler('embed', ({ point, url, embed }) => {
|
editor.registerExternalContentHandler('embed', ({ point, url, embed }) => {
|
||||||
const position =
|
const position =
|
||||||
point ??
|
point ??
|
||||||
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
|
(editor.inputs.shiftKey
|
||||||
|
? editor.inputs.currentPagePoint
|
||||||
|
: editor.getViewportPageBounds().center)
|
||||||
|
|
||||||
const { width, height } = embed
|
const { width, height } = embed
|
||||||
|
|
||||||
|
@ -210,7 +214,9 @@ export function registerDefaultExternalContentHandlers(
|
||||||
editor.registerExternalContentHandler('files', async ({ point, files }) => {
|
editor.registerExternalContentHandler('files', async ({ point, files }) => {
|
||||||
const position =
|
const position =
|
||||||
point ??
|
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)
|
const pagePoint = new Vec(position.x, position.y)
|
||||||
|
|
||||||
|
@ -266,7 +272,9 @@ export function registerDefaultExternalContentHandlers(
|
||||||
editor.registerExternalContentHandler('text', async ({ point, text }) => {
|
editor.registerExternalContentHandler('text', async ({ point, text }) => {
|
||||||
const p =
|
const p =
|
||||||
point ??
|
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()
|
const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()
|
||||||
|
|
||||||
|
@ -370,7 +378,9 @@ export function registerDefaultExternalContentHandlers(
|
||||||
|
|
||||||
const position =
|
const position =
|
||||||
point ??
|
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 assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
||||||
const shape = createEmptyBookmarkShape(editor, url, position)
|
const shape = createEmptyBookmarkShape(editor, url, position)
|
||||||
|
|
|
@ -268,14 +268,16 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
|
||||||
const debugGeom: Geometry2d[] = []
|
const debugGeom: Geometry2d[] = []
|
||||||
const info = editor.getArrowInfo(shape)!
|
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 hasStartArrowhead = info.start.arrowhead !== 'none'
|
||||||
const hasEndArrowhead = info.end.arrowhead !== 'none'
|
const hasEndArrowhead = info.end.arrowhead !== 'none'
|
||||||
if (info.isStraight) {
|
if (info.isStraight) {
|
||||||
const range = getStraightArrowLabelRange(editor, shape, info)
|
const range = getStraightArrowLabelRange(editor, shape, info)
|
||||||
let clampedPosition = clamp(
|
let clampedPosition = clamp(
|
||||||
shape.props.labelPosition,
|
shape.props.labelPosition,
|
||||||
hasStartArrowhead ? range.start : 0,
|
hasStartArrowhead || hasStartBinding ? range.start : 0,
|
||||||
hasEndArrowhead ? range.end : 1
|
hasEndArrowhead || hasEndBinding ? range.end : 1
|
||||||
)
|
)
|
||||||
// This makes the position snap in the middle.
|
// This makes the position snap in the middle.
|
||||||
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
|
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)
|
if (range.dbg) debugGeom.push(...range.dbg)
|
||||||
let clampedPosition = clamp(
|
let clampedPosition = clamp(
|
||||||
shape.props.labelPosition,
|
shape.props.labelPosition,
|
||||||
hasStartArrowhead ? range.start : 0,
|
hasStartArrowhead || hasStartBinding ? range.start : 0,
|
||||||
hasEndArrowhead ? range.end : 1
|
hasEndArrowhead || hasEndBinding ? range.end : 1
|
||||||
)
|
)
|
||||||
// This makes the position snap in the middle.
|
// This makes the position snap in the middle.
|
||||||
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
|
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}
|
labelColor={theme[labelColor].solid}
|
||||||
textWidth={width}
|
textWidth={width}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
disableTab
|
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class Drawing extends StateNode {
|
||||||
this.mergeNextPoint = false
|
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) => {
|
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||||
|
@ -137,7 +137,7 @@ export class Drawing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateShapes()
|
this.updateDrawingShape()
|
||||||
}
|
}
|
||||||
|
|
||||||
override onExit? = () => {
|
override onExit? = () => {
|
||||||
|
@ -281,7 +281,7 @@ export class Drawing extends StateNode {
|
||||||
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
this.initialShape = this.editor.getShape<DrawableShape>(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateShapes() {
|
private updateDrawingShape() {
|
||||||
const { initialShape } = this
|
const { initialShape } = this
|
||||||
const { inputs } = this.editor
|
const { inputs } = this.editor
|
||||||
|
|
||||||
|
|
|
@ -402,7 +402,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
{showHtmlContainer && (
|
{showHtmlContainer && (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
id={shape.id}
|
|
||||||
style={{
|
style={{
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
width: shape.props.w,
|
width: shape.props.w,
|
||||||
|
@ -421,7 +420,6 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
text={text}
|
text={text}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
labelColor={theme[props.labelColor].solid}
|
labelColor={theme[props.labelColor].solid}
|
||||||
disableTab
|
|
||||||
wrap
|
wrap
|
||||||
/>
|
/>
|
||||||
</HTMLContainer>
|
</HTMLContainer>
|
||||||
|
|
|
@ -190,7 +190,6 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
isNote
|
isNote
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
labelColor={theme[color].note.text}
|
labelColor={theme[color].note.text}
|
||||||
disableTab
|
|
||||||
wrap
|
wrap
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,7 +27,6 @@ type TextLabelProps = {
|
||||||
bounds?: Box
|
bounds?: Box
|
||||||
isNote?: boolean
|
isNote?: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
disableTab?: boolean
|
|
||||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||||
classNamePrefix?: string
|
classNamePrefix?: string
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
|
@ -51,15 +50,13 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
onKeyDown: handleKeyDownCustom,
|
onKeyDown: handleKeyDownCustom,
|
||||||
classNamePrefix,
|
classNamePrefix,
|
||||||
style,
|
style,
|
||||||
disableTab = false,
|
|
||||||
textWidth,
|
textWidth,
|
||||||
textHeight,
|
textHeight,
|
||||||
}: TextLabelProps) {
|
}: TextLabelProps) {
|
||||||
const { rInput, isEmpty, isEditing, isEditingAnything, ...editableTextRest } = useEditableText(
|
const { rInput, isEmpty, isEditing, isEditingAnything, ...editableTextRest } = useEditableText(
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
text,
|
text
|
||||||
{ disableTab }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [initialText, setInitialText] = useState(text)
|
const [initialText, setInitialText] = useState(text)
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import {
|
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
|
||||||
Vec,
|
|
||||||
VecLike,
|
|
||||||
assert,
|
|
||||||
average,
|
|
||||||
precise,
|
|
||||||
shortAngleDist,
|
|
||||||
toDomPrecision,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
||||||
import { getStrokePoints } from './getStrokePoints'
|
import { getStrokePoints } from './getStrokePoints'
|
||||||
import { setStrokePointRadii } from './setStrokePointRadii'
|
import { setStrokePointRadii } from './setStrokePointRadii'
|
||||||
|
@ -36,17 +28,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
|
|
||||||
const result: StrokePoint[][] = []
|
const result: StrokePoint[][] = []
|
||||||
let currentPartition: StrokePoint[] = [points[0]]
|
let currentPartition: StrokePoint[] = [points[0]]
|
||||||
for (let i = 1; i < points.length - 1; i++) {
|
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
|
||||||
const prevPoint = points[i - 1]
|
let nextV: Vec
|
||||||
const thisPoint = points[i]
|
let dpr: number
|
||||||
const nextPoint = points[i + 1]
|
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
|
||||||
const prevAngle = Vec.Angle(prevPoint.point, thisPoint.point)
|
for (let i = 1, n = points.length; i < n - 1; i++) {
|
||||||
const nextAngle = Vec.Angle(thisPoint.point, nextPoint.point)
|
prevPoint = points[i - 1]
|
||||||
// acuteness is a normalized representation of how acute the angle is.
|
thisPoint = points[i]
|
||||||
// 1 is an infinitely thin wedge
|
nextPoint = points[i + 1]
|
||||||
// 0 is a straight line
|
|
||||||
const acuteness = Math.abs(shortAngleDist(prevAngle, nextAngle)) / Math.PI
|
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
|
||||||
if (acuteness > 0.8) {
|
dpr = Vec.Dpr(prevV, nextV)
|
||||||
|
prevV = nextV
|
||||||
|
|
||||||
|
if (dpr < -0.8) {
|
||||||
// always treat such acute angles as elbows
|
// always treat such acute angles as elbows
|
||||||
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
||||||
const elbowPoint = {
|
const elbowPoint = {
|
||||||
|
@ -59,19 +54,20 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
currentPartition.push(thisPoint)
|
currentPartition.push(thisPoint)
|
||||||
if (acuteness < 0.25) {
|
|
||||||
// this is not an elbow, bail out
|
if (dpr > 0.7) {
|
||||||
|
// Not an elbow
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
||||||
// away 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
|
||||||
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
|
|
||||||
// (normalized by the radius)
|
// (normalized by the radius)
|
||||||
const angularDist = incomingNormalizedDist + outgoingNormalizedDist
|
if (
|
||||||
if (angularDist < 1.5) {
|
(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
|
// if this point is kinda close to its neighbors and it has a reasonably
|
||||||
// acute angle, it's probably a hard elbow
|
// acute angle, it's probably a hard elbow
|
||||||
currentPartition.push(thisPoint)
|
currentPartition.push(thisPoint)
|
||||||
|
@ -89,11 +85,13 @@ function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
||||||
function cleanUpPartition(partition: StrokePoint[]) {
|
function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
// clean up start of partition (remove points that are too close to the start)
|
// clean up start of partition (remove points that are too close to the start)
|
||||||
const startPoint = partition[0]
|
const startPoint = partition[0]
|
||||||
|
let nextPoint: StrokePoint
|
||||||
while (partition.length > 2) {
|
while (partition.length > 2) {
|
||||||
const nextPoint = partition[1]
|
nextPoint = partition[1]
|
||||||
const dist = Vec.Dist(startPoint.point, nextPoint.point)
|
if (
|
||||||
const avgRadius = (startPoint.radius + nextPoint.radius) / 2
|
Vec.Dist2(startPoint.point, nextPoint.point) <
|
||||||
if (dist < avgRadius * 0.5) {
|
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
|
||||||
|
) {
|
||||||
partition.splice(1, 1)
|
partition.splice(1, 1)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -101,11 +99,13 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
}
|
}
|
||||||
// clean up end of partition in the same fashion
|
// clean up end of partition in the same fashion
|
||||||
const endPoint = partition[partition.length - 1]
|
const endPoint = partition[partition.length - 1]
|
||||||
|
let prevPoint: StrokePoint
|
||||||
while (partition.length > 2) {
|
while (partition.length > 2) {
|
||||||
const prevPoint = partition[partition.length - 2]
|
prevPoint = partition[partition.length - 2]
|
||||||
const dist = Vec.Dist(endPoint.point, prevPoint.point)
|
if (
|
||||||
const avgRadius = (endPoint.radius + prevPoint.radius) / 2
|
Vec.Dist2(endPoint.point, prevPoint.point) <
|
||||||
if (dist < avgRadius * 0.5) {
|
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
|
||||||
|
) {
|
||||||
partition.splice(partition.length - 2, 1)
|
partition.splice(partition.length - 2, 1)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
@ -115,13 +115,14 @@ function cleanUpPartition(partition: StrokePoint[]) {
|
||||||
if (partition.length > 1) {
|
if (partition.length > 1) {
|
||||||
partition[0] = {
|
partition[0] = {
|
||||||
...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] = {
|
||||||
...partition[partition.length - 1],
|
...partition[partition.length - 1],
|
||||||
vector: Vec.FromAngle(
|
vector: Vec.Sub(
|
||||||
Vec.Angle(partition[partition.length - 1].point, partition[partition.length - 2].point)
|
partition[partition.length - 2].point,
|
||||||
),
|
partition[partition.length - 1].point
|
||||||
|
).uni(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return partition
|
return partition
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLUnknownShape,
|
TLUnknownShape,
|
||||||
getPointerInfo,
|
getPointerInfo,
|
||||||
preventDefault,
|
|
||||||
stopEventPropagation,
|
stopEventPropagation,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
|
@ -11,31 +10,14 @@ import React, { useCallback, useEffect, useRef } from 'react'
|
||||||
import { INDENT, TextHelpers } from './TextHelpers'
|
import { INDENT, TextHelpers } from './TextHelpers'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function useEditableText(
|
export function useEditableText(id: TLShapeId, type: string, text: string) {
|
||||||
id: TLShapeId,
|
|
||||||
type: string,
|
|
||||||
text: string,
|
|
||||||
opts = { disableTab: false } as { disableTab: boolean }
|
|
||||||
) {
|
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||||
|
const rSelectionRanges = useRef<Range[] | null>()
|
||||||
const isEditing = useValue(
|
const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor])
|
||||||
'isEditing',
|
const isEditingAnything = useValue('isEditingAnything', () => !!editor.getEditingShapeId(), [
|
||||||
() => {
|
editor,
|
||||||
return editor.getEditingShapeId() === id
|
])
|
||||||
},
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isEditingAnything = useValue(
|
|
||||||
'isEditingAnything',
|
|
||||||
() => {
|
|
||||||
return editor.getEditingShapeId() !== null
|
|
||||||
},
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function selectAllIfEditing({ shapeId }: { shapeId: TLShapeId }) {
|
function selectAllIfEditing({ shapeId }: { shapeId: TLShapeId }) {
|
||||||
|
@ -52,14 +34,13 @@ export function useEditableText(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.on('select-all-text', selectAllIfEditing)
|
editor.on('select-all-text', selectAllIfEditing)
|
||||||
return () => {
|
return () => {
|
||||||
editor.off('select-all-text', selectAllIfEditing)
|
editor.off('select-all-text', selectAllIfEditing)
|
||||||
}
|
}
|
||||||
}, [editor, id])
|
}, [editor, id])
|
||||||
|
|
||||||
const rSelectionRanges = useRef<Range[] | null>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditing) return
|
if (!isEditing) return
|
||||||
|
|
||||||
|
@ -69,10 +50,18 @@ export function useEditableText(
|
||||||
// Focus if we're not already focused
|
// Focus if we're not already focused
|
||||||
if (document.activeElement !== elm) {
|
if (document.activeElement !== elm) {
|
||||||
elm.focus()
|
elm.focus()
|
||||||
|
|
||||||
// On mobile etc, just select all the text when we start focusing
|
// On mobile etc, just select all the text when we start focusing
|
||||||
if (editor.getInstanceState().isCoarsePointer) {
|
if (editor.getInstanceState().isCoarsePointer) {
|
||||||
elm.select()
|
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
|
// When the selection changes, save the selection ranges
|
||||||
|
@ -103,12 +92,14 @@ export function useEditableText(
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const elm = rInput.current
|
const elm = rInput.current
|
||||||
const editingShapeId = editor.getEditingShapeId()
|
const editingShapeId = editor.getEditingShapeId()
|
||||||
|
|
||||||
// Did we move to a different shape?
|
// Did we move to a different shape?
|
||||||
if (editingShapeId) {
|
if (editingShapeId) {
|
||||||
// important! these ^v are two different things
|
// important! these ^v are two different things
|
||||||
// is that shape OUR shape?
|
// is that shape OUR shape?
|
||||||
if (elm && editingShapeId === id) {
|
if (elm && editingShapeId === id) {
|
||||||
elm.focus()
|
elm.focus()
|
||||||
|
|
||||||
if (ranges && ranges.length) {
|
if (ranges && ranges.length) {
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
if (selection) {
|
if (selection) {
|
||||||
|
@ -134,20 +125,9 @@ export function useEditableText(
|
||||||
}
|
}
|
||||||
break
|
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.
|
// When the text changes, update the text value.
|
||||||
|
@ -198,8 +178,6 @@ export function useEditableText(
|
||||||
[editor, id, isEditing]
|
[editor, id, isEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDoubleClick = stopEventPropagation
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rInput,
|
rInput,
|
||||||
handleFocus: noop,
|
handleFocus: noop,
|
||||||
|
@ -207,7 +185,7 @@ export function useEditableText(
|
||||||
handleKeyDown,
|
handleKeyDown,
|
||||||
handleChange,
|
handleChange,
|
||||||
handleInputPointerDown,
|
handleInputPointerDown,
|
||||||
handleDoubleClick,
|
handleDoubleClick: stopEventPropagation,
|
||||||
isEmpty: text.trim().length === 0,
|
isEmpty: text.trim().length === 0,
|
||||||
isEditing,
|
isEditing,
|
||||||
isEditingAnything,
|
isEditingAnything,
|
||||||
|
|
|
@ -7,18 +7,22 @@ import {
|
||||||
SvgExportContext,
|
SvgExportContext,
|
||||||
TLOnEditEndHandler,
|
TLOnEditEndHandler,
|
||||||
TLOnResizeHandler,
|
TLOnResizeHandler,
|
||||||
|
TLShapeId,
|
||||||
TLShapeUtilFlag,
|
TLShapeUtilFlag,
|
||||||
TLTextShape,
|
TLTextShape,
|
||||||
Vec,
|
Vec,
|
||||||
WeakMapCache,
|
WeakMapCache,
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
|
preventDefault,
|
||||||
textShapeMigrations,
|
textShapeMigrations,
|
||||||
textShapeProps,
|
textShapeProps,
|
||||||
toDomPrecision,
|
toDomPrecision,
|
||||||
useEditor,
|
useEditor,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
import { useCallback } from 'react'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||||
|
import { TextHelpers } from '../shared/TextHelpers'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||||
|
@ -73,6 +77,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
const { width, height } = this.getMinDimensions(shape)
|
const { width, height } = this.getMinDimensions(shape)
|
||||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
|
const handleKeyDown = useTextShapeKeydownHandler(id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextLabel
|
<TextLabel
|
||||||
|
@ -94,6 +99,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
}}
|
}}
|
||||||
wrap
|
wrap
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -332,3 +338,32 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
||||||
height: Math.max(fontSize, result.h),
|
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
|
let nextLabelPosition
|
||||||
if (info.isStraight) {
|
if (info.isStraight) {
|
||||||
// straight arrows
|
// straight arrows
|
||||||
const lineLength = Vec.Dist2(info.start.point, info.end.point)
|
const lineLength = Vec.Dist(info.start.point, info.end.point)
|
||||||
const segmentLength = Vec.Dist2(info.end.point, nearestPoint)
|
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
|
||||||
nextLabelPosition = 1 - segmentLength / lineLength
|
nextLabelPosition = 1 - segmentLength / lineLength
|
||||||
} else {
|
} else {
|
||||||
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
||||||
|
|
|
@ -117,7 +117,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
|
||||||
editor.putExternalContent({
|
editor.putExternalContent({
|
||||||
type: 'embed',
|
type: 'embed',
|
||||||
url,
|
url,
|
||||||
point: editor.getViewportPageCenter(),
|
point: editor.getViewportPageBounds().center,
|
||||||
embed: embedInfoForUrl.definition,
|
embed: embedInfoForUrl.definition,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEditor } from '@tldraw/editor'
|
import { useEditor, useQuickReactor } from '@tldraw/editor'
|
||||||
import { useEffect, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { useActions } from '../../context/actions'
|
import { useActions } from '../../context/actions'
|
||||||
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
||||||
|
|
||||||
|
@ -9,33 +9,25 @@ export function BackToContent() {
|
||||||
const actions = useActions()
|
const actions = useActions()
|
||||||
|
|
||||||
const [showBackToContent, setShowBackToContent] = useState(false)
|
const [showBackToContent, setShowBackToContent] = useState(false)
|
||||||
|
const rIsShowing = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useQuickReactor(
|
||||||
let showBackToContentPrev = false
|
'toggle showback to content',
|
||||||
|
() => {
|
||||||
const interval = setInterval(() => {
|
const showBackToContentPrev = rIsShowing.current
|
||||||
const renderingShapes = editor.getRenderingShapes()
|
const shapeIds = editor.getCurrentPageShapeIds()
|
||||||
const renderingBounds = editor.getRenderingBounds()
|
let showBackToContentNow = false
|
||||||
|
if (shapeIds.size) {
|
||||||
// Rendering shapes includes all the shapes in the current page.
|
showBackToContentNow = shapeIds.size === editor.getCulledShapes().size
|
||||||
// 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
|
|
||||||
|
|
||||||
if (showBackToContentPrev !== showBackToContentNow) {
|
if (showBackToContentPrev !== showBackToContentNow) {
|
||||||
setShowBackToContent(showBackToContentNow)
|
setShowBackToContent(showBackToContentNow)
|
||||||
showBackToContentPrev = showBackToContentNow
|
rIsShowing.current = showBackToContentNow
|
||||||
}
|
}
|
||||||
}, 1000)
|
},
|
||||||
|
[editor]
|
||||||
return () => {
|
)
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
if (!showBackToContent) return null
|
if (!showBackToContent) return null
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import {
|
import {
|
||||||
ANIMATION_MEDIUM_MS,
|
ANIMATION_MEDIUM_MS,
|
||||||
Box,
|
|
||||||
TLPointerEventInfo,
|
TLPointerEventInfo,
|
||||||
TLShapeId,
|
|
||||||
Vec,
|
Vec,
|
||||||
getPointerInfo,
|
getPointerInfo,
|
||||||
intersectPolygonPolygon,
|
|
||||||
normalizeWheel,
|
normalizeWheel,
|
||||||
releasePointerCapture,
|
releasePointerCapture,
|
||||||
setPointerCapture,
|
setPointerCapture,
|
||||||
useComputed,
|
|
||||||
useEditor,
|
useEditor,
|
||||||
useIsDarkMode,
|
useIsDarkMode,
|
||||||
useQuickReactor,
|
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { MinimapManager } from './MinimapManager'
|
import { MinimapManager } from './MinimapManager'
|
||||||
|
@ -24,67 +19,78 @@ export function DefaultMinimap() {
|
||||||
const rCanvas = React.useRef<HTMLCanvasElement>(null!)
|
const rCanvas = React.useRef<HTMLCanvasElement>(null!)
|
||||||
const rPointing = React.useRef(false)
|
const rPointing = React.useRef(false)
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode()
|
const minimapRef = React.useRef<MinimapManager>()
|
||||||
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])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Must check after render
|
const minimap = new MinimapManager(editor, rCanvas.current)
|
||||||
const raf = requestAnimationFrame(() => {
|
minimapRef.current = minimap
|
||||||
minimap.updateColors()
|
return minimapRef.current.close
|
||||||
minimap.render()
|
}, [editor])
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelAnimationFrame(raf)
|
|
||||||
}
|
|
||||||
}, [editor, minimap, isDarkMode])
|
|
||||||
|
|
||||||
const onDoubleClick = React.useCallback(
|
const onDoubleClick = React.useCallback(
|
||||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (!editor.getCurrentPageShapeIds().size) return
|
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)
|
minimapRef.current.originPagePoint.setTo(clampedPoint)
|
||||||
minimap.originPageCenter.setTo(editor.getViewportPageBounds().center)
|
minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center)
|
||||||
|
|
||||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||||
},
|
},
|
||||||
[editor, minimap]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPointerDown = React.useCallback(
|
const onPointerDown = React.useCallback(
|
||||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!minimapRef.current) return
|
||||||
const elm = e.currentTarget
|
const elm = e.currentTarget
|
||||||
setPointerCapture(elm, e)
|
setPointerCapture(elm, e)
|
||||||
if (!editor.getCurrentPageShapeIds().size) return
|
if (!editor.getCurrentPageShapeIds().size) return
|
||||||
|
|
||||||
rPointing.current = true
|
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()
|
const _vpPageBounds = editor.getViewportPageBounds()
|
||||||
|
|
||||||
minimap.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
minimapRef.current.isInViewport = _vpPageBounds.containsPoint(clampedPoint)
|
||||||
|
|
||||||
if (minimap.isInViewport) {
|
if (minimapRef.current.isInViewport) {
|
||||||
minimap.originPagePoint.setTo(clampedPoint)
|
minimapRef.current.originPagePoint.setTo(clampedPoint)
|
||||||
minimap.originPageCenter.setTo(_vpPageBounds.center)
|
minimapRef.current.originPageCenter.setTo(_vpPageBounds.center)
|
||||||
} else {
|
} else {
|
||||||
const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point)
|
const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point)
|
||||||
const pagePoint = Vec.Add(point, delta)
|
const pagePoint = Vec.Add(point, delta)
|
||||||
minimap.originPagePoint.setTo(pagePoint)
|
minimapRef.current.originPagePoint.setTo(pagePoint)
|
||||||
minimap.originPageCenter.setTo(point)
|
minimapRef.current.originPageCenter.setTo(point)
|
||||||
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,16 +104,24 @@ export function DefaultMinimap() {
|
||||||
|
|
||||||
document.body.addEventListener('pointerup', release)
|
document.body.addEventListener('pointerup', release)
|
||||||
},
|
},
|
||||||
[editor, minimap]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPointerMove = React.useCallback(
|
const onPointerMove = React.useCallback(
|
||||||
(e: React.PointerEvent<HTMLCanvasElement>) => {
|
(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 (rPointing.current) {
|
||||||
if (minimap.isInViewport) {
|
if (minimapRef.current.isInViewport) {
|
||||||
const delta = minimap.originPagePoint.clone().sub(minimap.originPageCenter)
|
const delta = minimapRef.current.originPagePoint
|
||||||
|
.clone()
|
||||||
|
.sub(minimapRef.current.originPageCenter)
|
||||||
editor.centerOnPoint(Vec.Sub(point, delta))
|
editor.centerOnPoint(Vec.Sub(point, delta))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -115,7 +129,7 @@ export function DefaultMinimap() {
|
||||||
editor.centerOnPoint(point)
|
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)
|
const screenPoint = editor.pageToScreen(pagePoint)
|
||||||
|
|
||||||
|
@ -130,7 +144,7 @@ export function DefaultMinimap() {
|
||||||
|
|
||||||
editor.dispatch(info)
|
editor.dispatch(info)
|
||||||
},
|
},
|
||||||
[editor, minimap]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onWheel = React.useCallback(
|
const onWheel = React.useCallback(
|
||||||
|
@ -150,73 +164,16 @@ export function DefaultMinimap() {
|
||||||
[editor]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update the minimap's dpr when the dpr changes
|
const isDarkMode = useIsDarkMode()
|
||||||
useQuickReactor(
|
|
||||||
'update when dpr changes',
|
|
||||||
() => {
|
|
||||||
const dpr = devicePixelRatio.get()
|
|
||||||
minimap.setDpr(dpr)
|
|
||||||
|
|
||||||
const canvas = rCanvas.current as HTMLCanvasElement
|
React.useEffect(() => {
|
||||||
const rect = canvas.getBoundingClientRect()
|
// need to wait a tick for next theme css to be applied
|
||||||
const width = rect.width * dpr
|
// otherwise the minimap will render with the wrong colors
|
||||||
const height = rect.height * dpr
|
setTimeout(() => {
|
||||||
|
minimapRef.current?.updateColors()
|
||||||
// These must happen in order
|
minimapRef.current?.render()
|
||||||
canvas.width = width
|
})
|
||||||
canvas.height = height
|
}, [isDarkMode])
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-minimap">
|
<div className="tlui-minimap">
|
||||||
|
|
|
@ -1,114 +1,159 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
ComputedCache,
|
||||||
Editor,
|
Editor,
|
||||||
PI2,
|
TLShape,
|
||||||
TLInstancePresence,
|
|
||||||
TLShapeId,
|
|
||||||
Vec,
|
Vec,
|
||||||
|
atom,
|
||||||
clamp,
|
clamp,
|
||||||
|
computed,
|
||||||
|
react,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
} from '@tldraw/editor'
|
} 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 {
|
export class MinimapManager {
|
||||||
constructor(public editor: Editor) {}
|
disposables = [] as (() => void)[]
|
||||||
|
close = () => this.disposables.forEach((d) => d())
|
||||||
dpr = 1
|
gl: ReturnType<typeof setupWebGl>
|
||||||
|
shapeGeometryCache: ComputedCache<Float32Array | null, TLShape>
|
||||||
colors = {
|
constructor(
|
||||||
shapeFill: 'rgba(144, 144, 144, .1)',
|
public editor: Editor,
|
||||||
selectFill: '#2f80ed',
|
public readonly elem: HTMLCanvasElement
|
||||||
viewportFill: 'rgba(144, 144, 144, .1)',
|
) {
|
||||||
|
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()
|
private _getColors() {
|
||||||
cvs: HTMLCanvasElement | null = null
|
const style = getComputedStyle(this.editor.getContainer())
|
||||||
pageBounds: (Box & { id: TLShapeId })[] = []
|
|
||||||
collaborators: TLInstancePresence[] = []
|
|
||||||
|
|
||||||
canvasScreenBounds = new Box()
|
return {
|
||||||
canvasPageBounds = new Box()
|
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()
|
private colors: ReturnType<MinimapManager['_getColors']>
|
||||||
contentScreenBounds = new Box()
|
// 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()
|
originPagePoint = new Vec()
|
||||||
originPageCenter = new Vec()
|
originPageCenter = new Vec()
|
||||||
|
|
||||||
isInViewport = false
|
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) {
|
const aspectRatio = canvasScreenBounds.width / canvasScreenBounds.height
|
||||||
this.dpr = +dpr.toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateContentScreenBounds = () => {
|
let targetWidth = contentPageBounds.width
|
||||||
const { contentScreenBounds, contentPageBounds: content, canvasScreenBounds: canvas } = this
|
let targetHeight = targetWidth / aspectRatio
|
||||||
|
if (targetHeight < contentPageBounds.height) {
|
||||||
let { x, y, w, h } = contentScreenBounds
|
targetHeight = contentPageBounds.height
|
||||||
|
targetWidth = targetHeight * aspectRatio
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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. */
|
@computed getCanvasPageBoundsArray() {
|
||||||
updateCanvasPageBounds = () => {
|
const { x, y, w, h } = this.getCanvasPageBounds()
|
||||||
const { canvasPageBounds, canvasScreenBounds, contentPageBounds, contentScreenBounds } = this
|
return new Float32Array([x, y, w, h])
|
||||||
|
|
||||||
canvasPageBounds.set(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
contentPageBounds.width / (contentScreenBounds.width / canvasScreenBounds.width),
|
|
||||||
contentPageBounds.height / (contentScreenBounds.height / canvasScreenBounds.height)
|
|
||||||
)
|
|
||||||
|
|
||||||
canvasPageBounds.center = contentPageBounds.center
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getScreenPoint = (x: number, y: number) => {
|
getPagePoint = (clientX: number, clientY: number) => {
|
||||||
const { canvasScreenBounds } = this
|
const canvasPageBounds = this.getCanvasPageBounds()
|
||||||
|
const canvasScreenBounds = this.getCanvasScreenBounds()
|
||||||
|
|
||||||
const screenX = (x - canvasScreenBounds.minX) * this.dpr
|
// first offset the canvas position
|
||||||
const screenY = (y - canvasScreenBounds.minY) * this.dpr
|
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) => {
|
// then add the canvas page bounds' offset
|
||||||
const { contentPageBounds, contentScreenBounds, canvasPageBounds } = this
|
x += canvasPageBounds.minX
|
||||||
|
y += canvasPageBounds.minY
|
||||||
|
|
||||||
const { x: screenX, y: screenY } = this.getScreenPoint(x, y)
|
return new Vec(x, y, 1)
|
||||||
|
|
||||||
return new Vec(
|
|
||||||
canvasPageBounds.minX + (screenX * contentPageBounds.width) / contentScreenBounds.width,
|
|
||||||
canvasPageBounds.minY + (screenY * contentPageBounds.height) / contentScreenBounds.height,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
minimapScreenPointToPagePoint = (
|
minimapScreenPointToPagePoint = (
|
||||||
|
@ -123,13 +168,13 @@ export class MinimapManager {
|
||||||
let { x: px, y: py } = this.getPagePoint(x, y)
|
let { x: px, y: py } = this.getPagePoint(x, y)
|
||||||
|
|
||||||
if (clampToBounds) {
|
if (clampToBounds) {
|
||||||
const shapesPageBounds = this.editor.getCurrentPageBounds()
|
const shapesPageBounds = this.editor.getCurrentPageBounds() ?? new Box()
|
||||||
const vpPageBounds = viewportPageBounds
|
const vpPageBounds = viewportPageBounds
|
||||||
|
|
||||||
const minX = (shapesPageBounds?.minX ?? 0) - vpPageBounds.width / 2
|
const minX = shapesPageBounds.minX - vpPageBounds.width / 2
|
||||||
const maxX = (shapesPageBounds?.maxX ?? 0) + vpPageBounds.width / 2
|
const maxX = shapesPageBounds.maxX + vpPageBounds.width / 2
|
||||||
const minY = (shapesPageBounds?.minY ?? 0) - vpPageBounds.height / 2
|
const minY = shapesPageBounds.minY - vpPageBounds.height / 2
|
||||||
const maxY = (shapesPageBounds?.maxY ?? 0) + vpPageBounds.height / 2
|
const maxY = shapesPageBounds.maxY + vpPageBounds.height / 2
|
||||||
|
|
||||||
const lx = Math.max(0, minX + vpPageBounds.width - px)
|
const lx = Math.max(0, minX + vpPageBounds.width - px)
|
||||||
const rx = Math.max(0, -(maxX - vpPageBounds.width - px))
|
const rx = Math.max(0, -(maxX - vpPageBounds.width - px))
|
||||||
|
@ -171,209 +216,110 @@ export class MinimapManager {
|
||||||
return new Vec(px, py)
|
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 = () => {
|
render = () => {
|
||||||
const { cvs, pageBounds } = this
|
// make sure we update when dark mode switches
|
||||||
this.updateCanvasPageBounds()
|
const context = this.gl.context
|
||||||
|
const canvasSize = this.getCanvasSize()
|
||||||
|
|
||||||
const { editor, canvasScreenBounds, canvasPageBounds, contentPageBounds, contentScreenBounds } =
|
this.gl.setCanvasPageBounds(this.getCanvasPageBoundsArray())
|
||||||
this
|
|
||||||
const { width: cw, height: ch } = canvasScreenBounds
|
|
||||||
|
|
||||||
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
|
this.elem.width = canvasSize.x
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
this.elem.height = canvasSize.y
|
||||||
|
context.viewport(0, 0, canvasSize.x, canvasSize.y)
|
||||||
|
|
||||||
if (!cvs || !pageBounds) {
|
// this affects which color transparent shapes are blended with
|
||||||
return
|
// 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) {
|
const selectedShapes = new Set(this.editor.getSelectedShapeIds())
|
||||||
throw new Error('Minimap (shapes): Could not get context')
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.resetTransform()
|
const colors = this.colors
|
||||||
ctx.globalAlpha = 1
|
let selectedShapeOffset = 0
|
||||||
ctx.clearRect(0, 0, cw, ch)
|
let unselectedShapeOffset = 0
|
||||||
|
|
||||||
// Transform canvas
|
const ids = this.editor.getCurrentPageShapeIdsSorted()
|
||||||
|
|
||||||
const sx = contentScreenBounds.width / contentPageBounds.width
|
for (let i = 0, len = ids.length; i < len; i++) {
|
||||||
const sy = contentScreenBounds.height / contentPageBounds.height
|
const shapeId = ids[i]
|
||||||
|
const geometry = this.shapeGeometryCache.get(shapeId)
|
||||||
|
if (!geometry) continue
|
||||||
|
|
||||||
ctx.translate((cw - contentScreenBounds.width) / 2, (ch - contentScreenBounds.height) / 2)
|
const len = geometry.length
|
||||||
ctx.scale(sx, sy)
|
|
||||||
ctx.translate(-contentPageBounds.minX, -contentPageBounds.minY)
|
|
||||||
|
|
||||||
// shapes
|
if (selectedShapes.has(shapeId)) {
|
||||||
const shapesPath = new Path2D()
|
appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry)
|
||||||
const selectedPath = new Path2D()
|
selectedShapeOffset += len
|
||||||
|
} else {
|
||||||
const { shapeFill, selectFill, viewportFill } = this.colors
|
appendVertices(this.gl.unselectedShapes, unselectedShapeOffset, geometry)
|
||||||
|
unselectedShapeOffset += len
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Viewport
|
this.drawViewport()
|
||||||
{
|
this.drawShapes(this.gl.unselectedShapes, unselectedShapeOffset, colors.shapeFill)
|
||||||
const { minX, minY, width, height } = viewportPageBounds
|
this.drawShapes(this.gl.selectedShapes, selectedShapeOffset, colors.selectFill)
|
||||||
|
this.drawCollaborators()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static roundedRect(
|
private drawShapes(stuff: BufferStuff, len: number, color: Float32Array) {
|
||||||
ctx: CanvasRenderingContext2D | Path2D,
|
this.gl.prepareTriangles(stuff, len)
|
||||||
x: number,
|
this.gl.setFillColor(color)
|
||||||
y: number,
|
this.gl.drawTriangles(len)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static sharpRect(
|
private drawViewport() {
|
||||||
ctx: CanvasRenderingContext2D | Path2D,
|
const viewport = this.editor.getViewportPageBounds()
|
||||||
x: number,
|
const zoom = this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width
|
||||||
y: number,
|
const len = roundedRectangle(this.gl.viewport.vertices, viewport, 4 * zoom)
|
||||||
width: number,
|
|
||||||
height: number,
|
this.gl.prepareTriangles(this.gl.viewport, len)
|
||||||
_rx?: number,
|
this.gl.setFillColor(this.colors.viewportFill)
|
||||||
_ry?: number
|
this.gl.drawTriangles(len)
|
||||||
) {
|
}
|
||||||
ctx.rect(x, y, width, height)
|
|
||||||
|
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,
|
useEditor,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { ADJACENT_NOTE_MARGIN } from '../../shapes/note/noteHelpers'
|
||||||
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
|
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
|
||||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||||
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
||||||
|
@ -818,7 +819,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
trackEvent('pack-shapes', { source })
|
trackEvent('pack-shapes', { source })
|
||||||
editor.mark('pack')
|
editor.mark('pack')
|
||||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||||
editor.packShapes(selectedShapeIds, 16)
|
editor.packShapes(selectedShapeIds, ADJACENT_NOTE_MARGIN)
|
||||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1036,7 +1037,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
trackEvent('zoom-in', { source })
|
trackEvent('zoom-in', { source })
|
||||||
editor.zoomIn(editor.getViewportScreenCenter(), {
|
editor.zoomIn(undefined, {
|
||||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -1048,7 +1049,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
trackEvent('zoom-out', { source })
|
trackEvent('zoom-out', { source })
|
||||||
editor.zoomOut(editor.getViewportScreenCenter(), {
|
editor.zoomOut(undefined, {
|
||||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -1061,7 +1062,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
trackEvent('reset-zoom', { source })
|
trackEvent('reset-zoom', { source })
|
||||||
editor.resetZoom(editor.getViewportScreenCenter(), {
|
editor.resetZoom(undefined, {
|
||||||
animation: { duration: ANIMATION_MEDIUM_MS },
|
animation: { duration: ANIMATION_MEDIUM_MS },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -1296,7 +1297,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
trackEvent('zoom-to-content', { 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'
|
import { TLUiAssetUrls } from '../assetUrls'
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -14,6 +14,19 @@ export function AssetUrlsProvider({
|
||||||
assetUrls: TLUiAssetUrls
|
assetUrls: TLUiAssetUrls
|
||||||
children: React.ReactNode
|
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>
|
return <AssetUrlsContext.Provider value={assetUrls}>{children}</AssetUrlsContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
TLTextShape,
|
TLTextShape,
|
||||||
VecLike,
|
VecLike,
|
||||||
isNonNull,
|
isNonNull,
|
||||||
|
preventDefault,
|
||||||
|
stopEventPropagation,
|
||||||
uniq,
|
uniq,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
|
@ -615,24 +617,29 @@ export function useNativeClipboardEvents() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appIsFocused) return
|
if (!appIsFocused) return
|
||||||
const copy = () => {
|
const copy = (e: ClipboardEvent) => {
|
||||||
if (
|
if (
|
||||||
editor.getSelectedShapeIds().length === 0 ||
|
editor.getSelectedShapeIds().length === 0 ||
|
||||||
editor.getEditingShapeId() !== null ||
|
editor.getEditingShapeId() !== null ||
|
||||||
disallowClipboardEvents(editor)
|
disallowClipboardEvents(editor)
|
||||||
)
|
) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preventDefault(e)
|
||||||
handleNativeOrMenuCopy(editor)
|
handleNativeOrMenuCopy(editor)
|
||||||
trackEvent('copy', { source: 'kbd' })
|
trackEvent('copy', { source: 'kbd' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function cut() {
|
function cut(e: ClipboardEvent) {
|
||||||
if (
|
if (
|
||||||
editor.getSelectedShapeIds().length === 0 ||
|
editor.getSelectedShapeIds().length === 0 ||
|
||||||
editor.getEditingShapeId() !== null ||
|
editor.getEditingShapeId() !== null ||
|
||||||
disallowClipboardEvents(editor)
|
disallowClipboardEvents(editor)
|
||||||
)
|
) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
preventDefault(e)
|
||||||
handleNativeOrMenuCopy(editor)
|
handleNativeOrMenuCopy(editor)
|
||||||
editor.deleteShapes(editor.getSelectedShapeIds())
|
editor.deleteShapes(editor.getSelectedShapeIds())
|
||||||
trackEvent('cut', { source: 'kbd' })
|
trackEvent('cut', { source: 'kbd' })
|
||||||
|
@ -641,6 +648,7 @@ export function useNativeClipboardEvents() {
|
||||||
let disablingMiddleClickPaste = false
|
let disablingMiddleClickPaste = false
|
||||||
const pointerUpHandler = (e: PointerEvent) => {
|
const pointerUpHandler = (e: PointerEvent) => {
|
||||||
if (e.button === 1) {
|
if (e.button === 1) {
|
||||||
|
// middle mouse button
|
||||||
disablingMiddleClickPaste = true
|
disablingMiddleClickPaste = true
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
disablingMiddleClickPaste = false
|
disablingMiddleClickPaste = false
|
||||||
|
@ -648,9 +656,9 @@ export function useNativeClipboardEvents() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paste = (event: ClipboardEvent) => {
|
const paste = (e: ClipboardEvent) => {
|
||||||
if (disablingMiddleClickPaste) {
|
if (disablingMiddleClickPaste) {
|
||||||
event.stopPropagation()
|
stopEventPropagation(e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,8 +668,8 @@ export function useNativeClipboardEvents() {
|
||||||
if (editor.getEditingShapeId() !== null || disallowClipboardEvents(editor)) return
|
if (editor.getEditingShapeId() !== null || disallowClipboardEvents(editor)) return
|
||||||
|
|
||||||
// First try to use the clipboard data on the event
|
// First try to use the clipboard data on the event
|
||||||
if (event.clipboardData && !editor.inputs.shiftKey) {
|
if (e.clipboardData && !editor.inputs.shiftKey) {
|
||||||
handlePasteFromEventClipboardData(editor, event.clipboardData)
|
handlePasteFromEventClipboardData(editor, e.clipboardData)
|
||||||
} else {
|
} else {
|
||||||
// Or else use the clipboard API
|
// Or else use the clipboard API
|
||||||
navigator.clipboard.read().then((clipboardItems) => {
|
navigator.clipboard.read().then((clipboardItems) => {
|
||||||
|
@ -671,6 +679,7 @@ export function useNativeClipboardEvents() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preventDefault(e)
|
||||||
trackEvent('paste', { source: 'kbd' })
|
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({
|
const tldrawFileValidator: T.Validator<TldrawFile> = T.object({
|
||||||
tldrawFileFormatVersion: T.nonZeroInteger,
|
tldrawFileFormatVersion: T.nonZeroInteger,
|
||||||
schema: T.union('schemaVersion', {
|
schema: T.numberUnion('schemaVersion', {
|
||||||
1: schemaV1,
|
1: schemaV1,
|
||||||
2: schemaV2,
|
2: schemaV2,
|
||||||
}),
|
}),
|
||||||
|
@ -305,7 +305,6 @@ export async function parseAndLoadDocument(
|
||||||
editor.history.clear()
|
editor.history.clear()
|
||||||
// Put the old bounds back in place
|
// Put the old bounds back in place
|
||||||
editor.updateViewportScreenBounds(initialBounds)
|
editor.updateViewportScreenBounds(initialBounds)
|
||||||
editor.updateRenderingBounds()
|
|
||||||
|
|
||||||
const bounds = editor.getCurrentPageBounds()
|
const bounds = editor.getCurrentPageBounds()
|
||||||
if (bounds) {
|
if (bounds) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
TLWheelEventInfo,
|
TLWheelEventInfo,
|
||||||
Vec,
|
Vec,
|
||||||
VecLike,
|
VecLike,
|
||||||
|
computed,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
createTLStore,
|
createTLStore,
|
||||||
rotateSelectionHandle,
|
rotateSelectionHandle,
|
||||||
|
@ -143,6 +144,15 @@ export class TestEditor extends Editor {
|
||||||
elm: HTMLDivElement
|
elm: HTMLDivElement
|
||||||
bounds = { x: 0, y: 0, top: 0, left: 0, width: 1080, height: 720, bottom: 720, right: 1080 }
|
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) {
|
setScreenBounds(bounds: BoxModel, center = false) {
|
||||||
this.bounds.x = bounds.x
|
this.bounds.x = bounds.x
|
||||||
this.bounds.y = bounds.y
|
this.bounds.y = bounds.y
|
||||||
|
@ -154,7 +164,6 @@ export class TestEditor extends Editor {
|
||||||
this.bounds.bottom = bounds.y + bounds.h
|
this.bounds.bottom = bounds.y + bounds.h
|
||||||
|
|
||||||
this.updateViewportScreenBounds(Box.From(bounds), center)
|
this.updateViewportScreenBounds(Box.From(bounds), center)
|
||||||
this.updateRenderingBounds()
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,12 +209,12 @@ export class TestEditor extends Editor {
|
||||||
* _transformPointerDownSpy.mockRestore())
|
* _transformPointerDownSpy.mockRestore())
|
||||||
*/
|
*/
|
||||||
_transformPointerDownSpy = jest
|
_transformPointerDownSpy = jest
|
||||||
.spyOn(this._clickManager, 'transformPointerDownEvent')
|
.spyOn(this._clickManager, 'handlePointerEvent')
|
||||||
.mockImplementation((info) => {
|
.mockImplementation((info) => {
|
||||||
return info
|
return info
|
||||||
})
|
})
|
||||||
_transformPointerUpSpy = jest
|
_transformPointerUpSpy = jest
|
||||||
.spyOn(this._clickManager, 'transformPointerDownEvent')
|
.spyOn(this._clickManager, 'handlePointerEvent')
|
||||||
.mockImplementation((info) => {
|
.mockImplementation((info) => {
|
||||||
return info
|
return info
|
||||||
})
|
})
|
||||||
|
@ -455,6 +464,16 @@ export class TestEditor extends Editor {
|
||||||
return this
|
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 = (
|
pinchStart = (
|
||||||
x = this.inputs.currentScreenPoint.x,
|
x = this.inputs.currentScreenPoint.x,
|
||||||
y = this.inputs.currentScreenPoint.y,
|
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'
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -8,7 +8,7 @@ beforeEach(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('zooms by increments', () => {
|
it('zooms by increments', () => {
|
||||||
const cameraOptions = getDefaultCameraOptions()
|
const cameraOptions = DEFAULT_CAMERA_OPTIONS
|
||||||
|
|
||||||
// Starts at 1
|
// Starts at 1
|
||||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
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', () => {
|
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.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 })
|
||||||
editor.zoomIn()
|
editor.zoomIn()
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getDefaultCameraOptions } from '@tldraw/editor'
|
|
||||||
import { TestEditor } from '../TestEditor'
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
@ -8,7 +7,7 @@ beforeEach(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('zooms out and in by increments', () => {
|
it('zooms out and in by increments', () => {
|
||||||
const cameraOptions = getDefaultCameraOptions()
|
const cameraOptions = editor.getCameraOptions()
|
||||||
|
|
||||||
// Starts at 1
|
// Starts at 1
|
||||||
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3])
|
||||||
|
@ -30,3 +29,87 @@ it('does not zoom out when camera is frozen', () => {
|
||||||
editor.zoomOut()
|
editor.zoomOut()
|
||||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
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 { TLDrawShape, TLHighlightShape, last } from '@tldraw/editor'
|
||||||
import { TestEditor } from './TestEditor'
|
import { TestEditor } from './TestEditor'
|
||||||
|
import { TEST_DRAW_SHAPE_SCREEN_POINTS } from './drawing.data'
|
||||||
|
|
||||||
jest.useFakeTimers()
|
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(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||||
editor.renderingBoundsMargin = 100
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function createShapes() {
|
function createShapes() {
|
||||||
|
@ -136,3 +135,56 @@ it('correctly calculates the culled shapes when adding and deleting shapes', ()
|
||||||
const culledShapeFromScratch = editor.getCulledShapes()
|
const culledShapeFromScratch = editor.getCulledShapes()
|
||||||
expect(culledShapesIncremental).toEqual(culledShapeFromScratch)
|
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())
|
.bringToFront(editor.getSelectedShapeIds())
|
||||||
|
|
||||||
editor.setCamera({ x: -2000, y: -2000, z: 1 })
|
editor.setCamera({ x: -2000, y: -2000, z: 1 })
|
||||||
editor.updateRenderingBounds()
|
|
||||||
|
|
||||||
// Copy box 1 (should be out of viewport)
|
// Copy box 1 (should be out of viewport)
|
||||||
editor.select(ids.box1).copy()
|
editor.select(ids.box1).copy()
|
||||||
|
|
|
@ -34,7 +34,6 @@ function normalizeIndexes(
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
editor.setScreenBounds({ x: 0, y: 0, w: 1800, h: 900 })
|
||||||
editor.renderingBoundsMargin = 100
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function createShapes() {
|
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', () => {
|
it('lists shapes in viewport sorted by id with correct indexes & background indexes', () => {
|
||||||
const ids = createShapes()
|
const ids = createShapes()
|
||||||
// Expect the results to be sorted correctly by id
|
// Expect the results to be sorted correctly by id
|
||||||
|
|
|
@ -34,15 +34,17 @@ export function measureAverageDuration(
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
const result = originalMethod.apply(this, args)
|
const result = originalMethod.apply(this, args)
|
||||||
const end = performance.now()
|
const end = performance.now()
|
||||||
const value = averages.get(descriptor.value)!
|
|
||||||
const length = end - start
|
const length = end - start
|
||||||
const total = value.total + length
|
if (length !== 0) {
|
||||||
const count = value.count + 1
|
const value = averages.get(descriptor.value)!
|
||||||
averages.set(descriptor.value, { total, count })
|
const total = value.total + length
|
||||||
// eslint-disable-next-line no-console
|
const count = value.count + 1
|
||||||
console.log(
|
averages.set(descriptor.value, { total, count })
|
||||||
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
// eslint-disable-next-line no-console
|
||||||
)
|
console.log(
|
||||||
|
`${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`
|
||||||
|
)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
averages.set(descriptor.value, { total: 0, count: 0 })
|
averages.set(descriptor.value, { total: 0, count: 0 })
|
||||||
|
|
|
@ -83,6 +83,9 @@ function nullable<T>(validator: Validatable<T>): Validator<null | T>;
|
||||||
// @public
|
// @public
|
||||||
const number: Validator<number>;
|
const number: Validator<number>;
|
||||||
|
|
||||||
|
// @internal (undocumented)
|
||||||
|
function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
function object<Shape extends object>(config: {
|
function object<Shape extends object>(config: {
|
||||||
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
||||||
|
@ -134,6 +137,7 @@ declare namespace T {
|
||||||
jsonDict,
|
jsonDict,
|
||||||
dict,
|
dict,
|
||||||
union,
|
union,
|
||||||
|
numberUnion,
|
||||||
model,
|
model,
|
||||||
setEnum,
|
setEnum,
|
||||||
optional,
|
optional,
|
||||||
|
@ -178,7 +182,7 @@ function union<Key extends string, Config extends UnionValidatorConfig<Key, Conf
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class UnionValidator<Key extends string, Config extends UnionValidatorConfig<Key, Config>, UnknownValue = never> extends Validator<TypeOf<Config[keyof Config]> | UnknownValue> {
|
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)
|
// (undocumented)
|
||||||
validateUnknownVariants<Unknown>(unknownValueValidation: (value: object, variant: string) => Unknown): UnionValidator<Key, Config, Unknown>;
|
validateUnknownVariants<Unknown>(unknownValueValidation: (value: object, variant: string) => Unknown): UnionValidator<Key, Config, Unknown>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3027,6 +3027,14 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "(value: object, variant: string) => UnknownValue"
|
"text": "(value: object, variant: string) => UnknownValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", useNumberKeys: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "boolean"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ");"
|
"text": ");"
|
||||||
|
@ -3059,6 +3067,14 @@
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "useNumberKeys",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 7,
|
||||||
|
"endIndex": 8
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4260,6 +4276,14 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "(value: object, variant: string) => UnknownValue"
|
"text": "(value: object, variant: string) => UnknownValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", useNumberKeys: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "boolean"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ");"
|
"text": ");"
|
||||||
|
@ -4292,6 +4316,14 @@
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "useNumberKeys",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 7,
|
||||||
|
"endIndex": 8
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -394,7 +394,8 @@ export class UnionValidator<
|
||||||
constructor(
|
constructor(
|
||||||
private readonly key: Key,
|
private readonly key: Key,
|
||||||
private readonly config: Config,
|
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(
|
super(
|
||||||
(input) => {
|
(input) => {
|
||||||
|
@ -442,11 +443,13 @@ export class UnionValidator<
|
||||||
matchingSchema: Validatable<any> | undefined
|
matchingSchema: Validatable<any> | undefined
|
||||||
variant: string
|
variant: string
|
||||||
} {
|
} {
|
||||||
const variant = getOwnProperty(object, this.key) as keyof Config | undefined
|
const variant = getOwnProperty(object, this.key) as string & keyof Config
|
||||||
if (typeof variant !== 'string') {
|
if (!this.useNumberKeys && typeof variant !== 'string') {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
|
`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
|
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
|
||||||
|
@ -456,7 +459,7 @@ export class UnionValidator<
|
||||||
validateUnknownVariants<Unknown>(
|
validateUnknownVariants<Unknown>(
|
||||||
unknownValueValidation: (value: object, variant: string) => Unknown
|
unknownValueValidation: (value: object, variant: string) => Unknown
|
||||||
): UnionValidator<Key, Config, 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,
|
key: Key,
|
||||||
config: Config
|
config: Config
|
||||||
): UnionValidator<Key, Config> {
|
): UnionValidator<Key, Config> {
|
||||||
return new UnionValidator(key, config, (unknownValue, unknownVariant) => {
|
return new UnionValidator(
|
||||||
throw new ValidationError(
|
key,
|
||||||
`Expected one of ${Object.keys(config)
|
config,
|
||||||
.map((key) => JSON.stringify(key))
|
(unknownValue, unknownVariant) => {
|
||||||
.join(' or ')}, got ${JSON.stringify(unknownVariant)}`,
|
throw new ValidationError(
|
||||||
[key]
|
`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.
|
// and it will mess up the inline source viewer on sentry errors.
|
||||||
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
|
const out = tar.x({ cwd: assetsDir, 'keep-existing': true })
|
||||||
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable<Uint8Array>) {
|
for await (const chunk of Body?.transformToWebStream() as any as AsyncIterable<Uint8Array>) {
|
||||||
out.write(chunk)
|
out.write(Buffer.from(chunk.buffer))
|
||||||
}
|
}
|
||||||
out.end()
|
out.end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,12 @@ async function hasPackageChanged(pkg: PackageDetails) {
|
||||||
}
|
}
|
||||||
const publishedTarballPath = `${dirPath}/published-package.tgz`
|
const publishedTarballPath = `${dirPath}/published-package.tgz`
|
||||||
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
|
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
|
||||||
const publishedManifest = await getTarballManifest(publishedTarballPath)
|
const publishedManifest = getTarballManifestSync(publishedTarballPath)
|
||||||
|
|
||||||
const localTarballPath = `${dirPath}/local-package.tgz`
|
const localTarballPath = `${dirPath}/local-package.tgz`
|
||||||
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir })
|
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir })
|
||||||
|
|
||||||
const localManifest = await getTarballManifest(localTarballPath)
|
const localManifest = getTarballManifestSync(localTarballPath)
|
||||||
|
|
||||||
return !manifestsAreEqual(publishedManifest, localManifest)
|
return !manifestsAreEqual(publishedManifest, localManifest)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -48,34 +48,25 @@ function manifestsAreEqual(a: Record<string, Buffer>, b: Record<string, Buffer>)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTarballManifest(tarballPath: string): Promise<Record<string, Buffer>> {
|
function getTarballManifestSync(tarballPath: string) {
|
||||||
const manifest: Record<string, Buffer> = {}
|
const manifest: Record<string, Buffer> = {}
|
||||||
return new Promise((resolve, reject) =>
|
tar.list({
|
||||||
tar.list(
|
file: tarballPath,
|
||||||
{
|
onentry: (entry) => {
|
||||||
// @ts-expect-error bad typings
|
entry.on('data', (data) => {
|
||||||
file: tarballPath,
|
// we could hash these to reduce memory but it's probably fine
|
||||||
onentry: (entry) => {
|
const existing = manifest[entry.path]
|
||||||
entry.on('data', (data) => {
|
if (existing) {
|
||||||
// we could hash these to reduce memory but it's probably fine
|
manifest[entry.path] = Buffer.concat([existing, data])
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
resolve(manifest)
|
manifest[entry.path] = data
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
},
|
||||||
)
|
sync: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function didAnyPackageChange() {
|
export async function didAnyPackageChange() {
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
"@types/tmp": "^0.2.6",
|
"@types/tmp": "^0.2.6",
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"tar": "^6.2.0",
|
"tar": "^7.0.1",
|
||||||
"tmp": "^0.2.3"
|
"tmp": "^0.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
113
yarn.lock
113
yarn.lock
|
@ -3680,6 +3680,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@istanbuljs/load-nyc-config@npm:^1.0.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
|
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
|
||||||
|
@ -7589,7 +7598,7 @@ __metadata:
|
||||||
rimraf: "npm:^4.4.0"
|
rimraf: "npm:^4.4.0"
|
||||||
semver: "npm:^7.3.8"
|
semver: "npm:^7.3.8"
|
||||||
svgo: "npm:^3.0.2"
|
svgo: "npm:^3.0.2"
|
||||||
tar: "npm:^6.2.0"
|
tar: "npm:^7.0.1"
|
||||||
tmp: "npm:^0.2.3"
|
tmp: "npm:^0.2.3"
|
||||||
typescript: "npm:^5.3.3"
|
typescript: "npm:^5.3.3"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
@ -10700,6 +10709,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"chrome-trace-event@npm:^1.0.2":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "chrome-trace-event@npm:1.0.3"
|
resolution: "chrome-trace-event@npm:1.0.3"
|
||||||
|
@ -14645,18 +14661,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"glob@npm:^10.2.2, glob@npm:^10.3.10":
|
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
|
||||||
version: 10.3.10
|
version: 10.3.12
|
||||||
resolution: "glob@npm:10.3.10"
|
resolution: "glob@npm:10.3.12"
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: "npm:^3.1.0"
|
foreground-child: "npm:^3.1.0"
|
||||||
jackspeak: "npm:^2.3.5"
|
jackspeak: "npm:^2.3.6"
|
||||||
minimatch: "npm:^9.0.1"
|
minimatch: "npm:^9.0.1"
|
||||||
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
|
minipass: "npm:^7.0.4"
|
||||||
path-scurry: "npm:^1.10.1"
|
path-scurry: "npm:^1.10.2"
|
||||||
bin:
|
bin:
|
||||||
glob: dist/esm/bin.mjs
|
glob: dist/esm/bin.mjs
|
||||||
checksum: 38bdb2c9ce75eb5ed168f309d4ed05b0798f640b637034800a6bf306f39d35409bf278b0eaaffaec07591085d3acb7184a201eae791468f0f617771c2486a6a8
|
checksum: 9e8186abc22dc824b5dd86cefd8e6b5621a72d1be7f68bacc0fd681e8c162ec5546660a6ec0553d6a74757a585e655956c7f8f1a6d24570e8d865c307323d178
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -16275,7 +16291,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"jackspeak@npm:^2.3.5":
|
"jackspeak@npm:^2.3.6":
|
||||||
version: 2.3.6
|
version: 2.3.6
|
||||||
resolution: "jackspeak@npm:2.3.6"
|
resolution: "jackspeak@npm:2.3.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -17721,10 +17737,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0":
|
"lru-cache@npm:^10.0.0, lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||||
version: 10.1.0
|
version: 10.2.0
|
||||||
resolution: "lru-cache@npm:10.1.0"
|
resolution: "lru-cache@npm:10.2.0"
|
||||||
checksum: 207278d6fa711fb1f94a0835d4d4737441d2475302482a14785b10515e4c906a57ebf9f35bf060740c9560e91c7c1ad5a04fd7ed030972a9ba18bce2a228e95b
|
checksum: 502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -19131,7 +19147,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 7.0.4
|
||||||
resolution: "minipass@npm:7.0.4"
|
resolution: "minipass@npm:7.0.4"
|
||||||
checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18
|
checksum: e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18
|
||||||
|
@ -19148,6 +19164,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
|
||||||
version: 0.5.3
|
version: 0.5.3
|
||||||
resolution: "mkdirp-classic@npm:0.5.3"
|
resolution: "mkdirp-classic@npm:0.5.3"
|
||||||
|
@ -19164,6 +19190,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mlly@npm:^1.1.0, mlly@npm:^1.2.0":
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
resolution: "mlly@npm:1.5.0"
|
resolution: "mlly@npm:1.5.0"
|
||||||
|
@ -20327,13 +20362,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"path-scurry@npm:^1.10.1, path-scurry@npm:^1.6.1":
|
"path-scurry@npm:^1.10.2, path-scurry@npm:^1.6.1":
|
||||||
version: 1.10.1
|
version: 1.10.2
|
||||||
resolution: "path-scurry@npm:1.10.1"
|
resolution: "path-scurry@npm:1.10.2"
|
||||||
dependencies:
|
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"
|
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||||
checksum: eebfb8304fef1d4f7e1486df987e4fd77413de4fce16508dea69fcf8eb318c09a6b15a7a2f4c22877cec1cb7ecbd3071d18ca9de79eeece0df874a00f1f0bdc8
|
checksum: a2bbbe8dc284c49dd9be78ca25f3a8b89300e0acc24a77e6c74824d353ef50efbf163e64a69f4330b301afca42d0e2229be0560d6d616ac4e99d48b4062016b1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -22045,6 +22080,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"rollup-plugin-inject@npm:^3.0.0":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "rollup-plugin-inject@npm:3.0.2"
|
resolution: "rollup-plugin-inject@npm:3.0.2"
|
||||||
|
@ -23378,7 +23424,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 6.2.1
|
||||||
resolution: "tar@npm:6.2.1"
|
resolution: "tar@npm:6.2.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -23392,6 +23438,20 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"terminal-link@npm:^2.1.1":
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
resolution: "terminal-link@npm:2.1.1"
|
resolution: "terminal-link@npm:2.1.1"
|
||||||
|
@ -24964,8 +25024,8 @@ __metadata:
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vite@npm:^5.0.0":
|
"vite@npm:^5.0.0":
|
||||||
version: 5.2.8
|
version: 5.2.9
|
||||||
resolution: "vite@npm:5.2.8"
|
resolution: "vite@npm:5.2.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: "npm:^0.20.1"
|
esbuild: "npm:^0.20.1"
|
||||||
fsevents: "npm:~2.3.3"
|
fsevents: "npm:~2.3.3"
|
||||||
|
@ -24999,7 +25059,7 @@ __metadata:
|
||||||
optional: true
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
vite: bin/vite.js
|
vite: bin/vite.js
|
||||||
checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
|
checksum: 26342c8dde540e4161fdad2c9c8f2f0e23567f051c7a40abb8e4796d6c4292fbd118ab7a4ac252515e78c4f99525b557731e6117287b2bccde0ea61d73bcff27
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -25666,6 +25726,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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
|
version: 2.3.4
|
||||||
resolution: "yaml@npm:2.3.4"
|
resolution: "yaml@npm:2.3.4"
|
||||||
|
|
Ładowanie…
Reference in New Issue