Camera options followups (#3701)

This PR adds a slideshow example (similar to @TodePond's slides but more
on rails) as a way to put some pressure on camera controls.

Along the way, it fixes some issues I found with animations and the new
camera controls.

- forced changes will continue to force through animations
- animations no longer set unnecessary additional listeners
- animations end correctly
- updating camera options does not immediately update the camera (to
allow for animations, etc.)

It also changes the location of the "in front of the canvas" element so
that it is not hidden by the hit test blocking element.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features
This commit is contained in:
Steve Ruiz 2024-05-07 11:06:35 +01:00 committed by GitHub
parent fabba66c0f
commit ebc892a1a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 636 additions and 277 deletions

View file

@ -44,7 +44,7 @@
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/react": "^18.2.47", "@types/react": "^18.2.47",
"@typescript-eslint/utils": "^5.59.0", "@typescript-eslint/utils": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.6.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.37.0", "eslint": "^8.37.0",
"fast-glob": "^3.3.1", "fast-glob": "^3.3.1",

View file

@ -50,7 +50,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.188", "@types/lodash": "^4.14.188",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react-swc": "^3.6.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"remark": "^15.0.1", "remark": "^15.0.1",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",

View file

@ -145,7 +145,7 @@ const CameraOptionsControlPanel = track(() => {
useEffect(() => { useEffect(() => {
if (!editor) return if (!editor) return
editor.batch(() => { editor.batch(() => {
editor.setCameraOptions(cameraOptions, { immediate: true }) editor.setCameraOptions(cameraOptions)
editor.setCamera(editor.getCamera(), { editor.setCamera(editor.getCamera(), {
immediate: true, immediate: true,
}) })

View file

@ -13,16 +13,26 @@ export default function EditorFocusExample() {
}, [focused]) }, [focused])
return ( return (
<div style={{ padding: 32 }}> <div
<div style={{ display: 'flex', gap: 4 }}> style={{ padding: 32 }}
<input onPointerDown={() => {
id={'focus'} const editor = rEditorRef.current
type={'checkbox'} if (editor && editor.getInstanceState().isFocused) {
onChange={(e) => { editor.updateInstanceState({ isFocused: false })
setFocused(e.target.checked) }
}} }}
/> >
<label htmlFor={'focus'}>Focus</label> <div>
<div style={{ display: 'flex', gap: 4 }}>
<input
id="focus"
type="checkbox"
onChange={(e) => {
setFocused(e.target.checked)
}}
/>
<label htmlFor="focus">Focus</label>
</div>
</div> </div>
<p> <p>
The checkbox controls the editor's <code>instanceState.isFocused</code> property. The checkbox controls the editor's <code>instanceState.isFocused</code> property.
@ -39,6 +49,7 @@ export default function EditorFocusExample() {
}} }}
/> />
</div> </div>
<input type="text" placeholder="Test me" />
</div> </div>
) )
} }

View file

@ -133,23 +133,21 @@ export function ImageAnnotationEditor({
* component hooks into camera updates to keep the camera constrained - try uploading a very long, * component hooks into camera updates to keep the camera constrained - try uploading a very long,
* thin image and seeing how the camera behaves. * thin image and seeing how the camera behaves.
*/ */
editor.setCameraOptions( editor.setCameraOptions({
{ constraints: {
constraints: { initialZoom: 'fit-max',
initialZoom: 'fit-max', baseZoom: 'default',
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 }, behavior: 'contain',
behavior: 'contain',
},
zoomSteps: [1, 2, 4, 8],
zoomSpeed: 1,
panSpeed: 1,
isLocked: false,
}, },
{ reset: true } zoomSteps: [1, 2, 4, 8],
) zoomSpeed: 1,
panSpeed: 1,
isLocked: false,
})
editor.setCamera(editor.getCamera(), { reset: true })
}, [editor, imageShapeId, image]) }, [editor, imageShapeId, image])
return ( return (

View file

@ -117,20 +117,18 @@ export function PdfEditor({ pdf }: { pdf: Pdf }) {
) )
function updateCameraBounds(isMobile: boolean) { function updateCameraBounds(isMobile: boolean) {
editor.setCameraOptions( editor.setCameraOptions({
{ ...DEFAULT_CAMERA_OPTIONS,
...DEFAULT_CAMERA_OPTIONS, constraints: {
constraints: { bounds: targetBounds,
bounds: targetBounds, padding: { x: isMobile ? 16 : 164, y: 64 },
padding: { x: isMobile ? 16 : 164, y: 64 }, origin: { x: 0.5, y: 0 },
origin: { x: 0.5, y: 0 }, initialZoom: 'fit-x-100',
initialZoom: 'fit-x-100', baseZoom: 'default',
baseZoom: 'default', behavior: 'contain',
behavior: 'contain',
},
}, },
{ reset: true } })
) editor.setCamera(editor.getCamera(), { reset: true })
} }
let isMobile = editor.getViewportScreenBounds().width < 840 let isMobile = editor.getViewportScreenBounds().width < 840

View file

@ -0,0 +1,11 @@
---
title: Slideshow with Camera
component: ./SlideShowExample.tsx
category: use-cases
---
---
The `Tldraw` component provides the tldraw editor as a regular React component. You can put this component anywhere in your React project. In this example, we make the component take up the height and width of the container.
By default, the component does not persist between refreshes or sync locally between tabs. To keep your work after a refresh, check the [`persistenceKey`](/peristence-key) example.

View file

@ -0,0 +1,264 @@
import { useEffect, useState } from 'react'
import {
DEFAULT_CAMERA_OPTIONS,
Editor,
TLFrameShape,
Tldraw,
createShapeId,
stopEventPropagation,
transact,
useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { SLIDE_MARGIN, SLIDE_SIZE, SlidesProvider, useSlides } from './SlidesManager'
export default function SlideShowExample() {
return (
<div className="tldraw__editor">
<SlidesProvider>
<InsideSlidesContext />
</SlidesProvider>
</div>
)
}
function InsideSlidesContext() {
const [editor, setEditor] = useState<Editor | null>(null)
const slides = useSlides()
const currentSlide = useValue('currentSlide', () => slides.getCurrentSlide(), [slides])
useEffect(() => {
if (!editor) return
const nextBounds = {
x: currentSlide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
y: 0,
w: SLIDE_SIZE.w,
h: SLIDE_SIZE.h,
}
editor.setCameraOptions({
...DEFAULT_CAMERA_OPTIONS,
constraints: {
bounds: nextBounds,
behavior: 'contain',
initialZoom: 'fit-max',
baseZoom: 'fit-max',
origin: { x: 0.5, y: 0.5 },
padding: { x: 50, y: 50 },
},
})
editor.zoomToBounds(nextBounds, { force: true, animation: { duration: 500 } })
}, [editor, currentSlide])
const currentSlides = useValue('slides', () => slides.getCurrentSlides(), [slides])
useEffect(() => {
if (!editor) return
const ids = currentSlides.map((slide) => createShapeId(slide.id))
transact(() => {
for (let i = 0; i < currentSlides.length; i++) {
const shapeId = ids[i]
const slide = currentSlides[i]
const shape = editor.getShape(shapeId)
if (shape) {
if (shape.x === slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN)) continue
// if name is still Slide and number, e.g Slide 1, update it. Use regex to test
const regex = /Slide \d+/
let name = (shape as TLFrameShape).props.name
if (regex.test((shape as TLFrameShape).props.name)) {
name = `Slide ${slide.index + 1}`
}
editor.updateShape<TLFrameShape>({
id: shapeId,
type: 'frame',
x: slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
props: {
name,
},
})
} else {
editor.createShape<TLFrameShape>({
id: shapeId,
parentId: editor.getCurrentPageId(),
type: 'frame',
x: slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
y: 0,
props: {
name: `Slide ${slide.index + 1}`,
w: SLIDE_SIZE.w,
h: SLIDE_SIZE.h,
},
})
}
}
})
const unsubs = [] as (() => void)[]
unsubs.push(
editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next) => {
if (
ids.includes(next.id) &&
(next as TLFrameShape).props.name === (prev as TLFrameShape).props.name
)
return prev
return next
})
)
unsubs.push(
editor.sideEffects.registerBeforeChangeHandler('instance_page_state', (prev, next) => {
next.selectedShapeIds = next.selectedShapeIds.filter((id) => !ids.includes(id))
if (next.hoveredShapeId && ids.includes(next.hoveredShapeId)) next.hoveredShapeId = null
return next
})
)
return () => {
unsubs.forEach((fn) => fn())
}
}, [currentSlides, editor])
const handleMount = (editor: Editor) => {
setEditor(editor)
}
return <Tldraw onMount={handleMount} components={components} />
}
function Slides() {
const slides = useSlides()
const currentSlides = useValue('slides', () => slides.getCurrentSlides(), [slides])
const lowestIndex = currentSlides[0].index
const highestIndex = currentSlides[currentSlides.length - 1].index
return (
<>
{/* {currentSlides.map((slide) => (
<div
key={slide.id}
style={{
position: 'absolute',
top: 0,
left: (SLIDE_SIZE.w + SLIDE_MARGIN) * slide.index,
width: SLIDE_SIZE.w,
height: SLIDE_SIZE.h,
backgroundColor: 'white',
border: '1px solid black',
pointerEvents: 'all',
}}
onPointerDown={(e) => {
if (slide.id !== slides.getCurrentSlideId()) {
stopEventPropagation(e)
slides.setCurrentSlide(slide.id)
}
}}
/>
))} */}
{currentSlides.slice(0, -1).map((slide) => (
<button
key={slide.id + 'between'}
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: (slide.index + 1) * (SLIDE_SIZE.w + SLIDE_MARGIN) - (SLIDE_MARGIN + 40) / 2,
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const newSlide = slides.newSlide(slide.index + 1)
slides.setCurrentSlide(newSlide.id)
}}
>
|
</button>
))}
<button
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: lowestIndex * (SLIDE_SIZE.w + SLIDE_MARGIN) - (40 + SLIDE_MARGIN * 0.1),
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const slide = slides.newSlide(lowestIndex - 1)
slides.setCurrentSlide(slide.id)
}}
>
{`+`}
</button>
<button
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: highestIndex * (SLIDE_SIZE.w + SLIDE_MARGIN) + (SLIDE_SIZE.w + SLIDE_MARGIN * 0.1),
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const slide = slides.newSlide(highestIndex + 1)
slides.setCurrentSlide(slide.id)
}}
>
{`+`}
</button>
</>
)
}
function SlideControls() {
const slides = useSlides()
return (
<>
<button
style={{
pointerEvents: 'all',
position: 'absolute',
top: '50%',
left: 0,
width: 50,
height: 50,
}}
onPointerDown={stopEventPropagation}
onClick={() => slides.prevSlide()}
>
{`<`}
</button>
<button
style={{
pointerEvents: 'all',
position: 'absolute',
top: '50%',
right: 0,
width: 50,
height: 50,
}}
onPointerDown={stopEventPropagation}
onClick={() => slides.nextSlide()}
>
{`>`}
</button>
</>
)
}
const components = {
OnTheCanvas: Slides,
InFrontOfTheCanvas: SlideControls,
}

View file

@ -0,0 +1,100 @@
import { createContext, ReactNode, useContext, useState } from 'react'
import { atom, computed, structuredClone, uniqueId } from 'tldraw'
export const SLIDE_SIZE = { x: 0, y: 0, w: 1600, h: 900 }
export const SLIDE_MARGIN = 100
type Slide = {
id: string
index: number
name: string
}
class SlidesManager {
private _slides = atom<Slide[]>('slide', [
{
id: '1',
index: 0,
name: 'Slide 1',
},
{
id: '2',
index: 1,
name: 'Slide 2',
},
{
id: '3',
index: 2,
name: 'Slide 3',
},
])
@computed getCurrentSlides() {
return this._slides.get().sort((a, b) => (a.index < b.index ? -1 : 1))
}
private _currentSlideId = atom('currentSlide', '1')
@computed getCurrentSlideId() {
return this._currentSlideId.get()
}
@computed getCurrentSlide() {
return this._slides.get().find((slide) => slide.id === this.getCurrentSlideId())!
}
setCurrentSlide(id: string) {
this._currentSlideId.set(id)
}
moveBy(delta: number) {
const slides = this.getCurrentSlides()
const currentIndex = slides.findIndex((slide) => slide.id === this.getCurrentSlideId())
const next = slides[currentIndex + delta]
if (!next) return
this._currentSlideId.set(next.id)
}
nextSlide() {
this.moveBy(1)
}
prevSlide() {
this.moveBy(-1)
}
newSlide(index: number) {
const slides = structuredClone(this.getCurrentSlides())
let bumping = false
for (const slide of slides) {
if (slide.index === index) {
bumping = true
}
if (bumping) {
slide.index++
}
}
const newSlide = {
id: uniqueId(),
index,
name: `Slide ${slides.length + 1}`,
}
this._slides.set([...slides, newSlide])
return newSlide
}
}
const slidesContext = createContext({} as SlidesManager)
export const SlidesProvider = ({ children }: { children: ReactNode }) => {
const [slideManager] = useState(() => new SlidesManager())
return <slidesContext.Provider value={slideManager}>{children}</slidesContext.Provider>
}
export function useSlides() {
return useContext(slidesContext)
}

View file

@ -3,7 +3,8 @@
"include": ["src", "e2e", "./vite.config.ts", "**/*.json"], "include": ["src", "e2e", "./vite.config.ts", "**/*.json"],
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"], "exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
"compilerOptions": { "compilerOptions": {
"outDir": "./.tsbuild" "outDir": "./.tsbuild",
"experimentalDecorators": true
}, },
"references": [ "references": [
{ {

View file

@ -1,9 +1,9 @@
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react-swc'
import path from 'path' import path from 'path'
import { PluginOption, defineConfig } from 'vite' import { PluginOption, defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
plugins: [react(), exampleReadmePlugin()], plugins: [react({ tsDecorators: true }), exampleReadmePlugin()],
root: path.join(__dirname, 'src'), root: path.join(__dirname, 'src'),
publicDir: path.join(__dirname, 'public'), publicDir: path.join(__dirname, 'public'),
build: { build: {

View file

@ -899,7 +899,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?: TLCameraMoveOptions): this; setCameraOptions(options: Partial<TLCameraOptions>): this;
setCroppingShape(shape: null | TLShape | TLShapeId): this; setCroppingShape(shape: null | TLShape | TLShapeId): this;
setCurrentPage(page: TLPage | TLPageId): this; setCurrentPage(page: TLPage | TLPageId): this;
setCurrentTool(id: string, info?: {}): this; setCurrentTool(id: string, info?: {}): this;

View file

@ -28,6 +28,7 @@
--layer-canvas: 200; --layer-canvas: 200;
--layer-shapes: 300; --layer-shapes: 300;
--layer-overlays: 400; --layer-overlays: 400;
--layer-in-front: 500;
--layer-following-indicator: 1000; --layer-following-indicator: 1000;
--layer-blocker: 10000; --layer-blocker: 10000;
@ -265,7 +266,8 @@ input,
.tl-overlays { .tl-overlays {
position: absolute; position: absolute;
inset: 0px; top: 0px;
left: 0px;
height: 100%; height: 100%;
width: 100%; width: 100%;
contain: strict; contain: strict;
@ -291,6 +293,17 @@ input,
pointer-events: none; pointer-events: none;
} }
.tl-front {
z-index: 600;
position: absolute;
inset: 0px;
height: 100%;
width: 100%;
overflow: clip;
content-visibility: auto;
touch-action: none;
pointer-events: none;
}
/* ------------------- Background ------------------- */ /* ------------------- Background ------------------- */
.tl-background__wrapper { .tl-background__wrapper {

View file

@ -367,7 +367,22 @@ function Layout({
useFocusEvents(autoFocus) useFocusEvents(autoFocus)
useOnMount(onMount) useOnMount(onMount)
return <>{children}</> return (
<>
{children}
<InFrontOfTheCanvasWrapper />
</>
)
}
function InFrontOfTheCanvasWrapper() {
const { InFrontOfTheCanvas } = useEditorComponents()
if (!InFrontOfTheCanvas) return null
return (
<div className="tl-front">
<InFrontOfTheCanvas />
</div>
)
} }
function Crash({ crashingError }: { crashingError: unknown }): null { function Crash({ crashingError }: { crashingError: unknown }): null {

View file

@ -169,7 +169,6 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
<SelectionForegroundWrapper /> <SelectionForegroundWrapper />
<LiveCollaborators /> <LiveCollaborators />
</div> </div>
<InFrontOfTheCanvasWrapper />
</div> </div>
<MovingCameraHitTestBlocker /> <MovingCameraHitTestBlocker />
</div> </div>
@ -647,12 +646,6 @@ function OnTheCanvasWrapper() {
return <OnTheCanvas /> return <OnTheCanvas />
} }
function InFrontOfTheCanvasWrapper() {
const { InFrontOfTheCanvas } = useEditorComponents()
if (!InFrontOfTheCanvas) return null
return <InFrontOfTheCanvas />
}
function MovingCameraHitTestBlocker() { function MovingCameraHitTestBlocker() {
const editor = useEditor() const editor = useEditor()
const cameraState = useValue('camera state', () => editor.getCameraState(), [editor]) const cameraState = useValue('camera state', () => editor.getCameraState(), [editor])

View file

@ -2148,26 +2148,24 @@ export class Editor extends EventEmitter<TLEventMap> {
} }
/** /**
* Set the camera options. * Set the camera options. Changing the options won't immediately change the camera itself, so you may want to call `setCamera` after changing the options.
* *
* @example * @example
* ```ts * ```ts
* editor.setCameraOptions(myCameraOptions) * editor.setCameraOptions(myCameraOptions)
* editor.setCameraOptions(myCameraOptions, { immediate: true, force: true, initial: false }) * editor.setCamera(editor.getCamera())
* ``` * ```
* *
* @param options - The camera options to set. * @param options - The camera options to set.
* @param opts - The options for the change.
* *
* @public */ * @public */
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions) { setCameraOptions(options: Partial<TLCameraOptions>) {
const next = structuredClone({ const next = structuredClone({
...this.getCameraOptions(), ...this._cameraOptions.__unsafe__getWithoutCapture(),
...options, ...options,
}) })
if (next.zoomSteps?.length < 1) next.zoomSteps = [1] if (next.zoomSteps?.length < 1) next.zoomSteps = [1]
this._cameraOptions.set(next) this._cameraOptions.set(next)
this.setCamera(this.getCamera(), opts)
return this return this
} }
@ -2637,16 +2635,16 @@ export class Editor extends EventEmitter<TLEventMap> {
bounds: BoxLike, bounds: BoxLike,
opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions
): this { ): this {
if (this.getCameraOptions().isLocked) return this const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture()
if (cameraOptions.isLocked) return this
const viewportScreenBounds = this.getViewportScreenBounds() const viewportScreenBounds = this.getViewportScreenBounds()
const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28) const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28)
const baseZoom = this.getBaseZoom() const baseZoom = this.getBaseZoom()
const { zoomSteps } = this.getCameraOptions() const zoomMin = cameraOptions.zoomSteps[0]
const zoomMin = zoomSteps[0] const zoomMax = last(cameraOptions.zoomSteps)!
const zoomMax = last(zoomSteps)!
let zoom = clamp( let zoom = clamp(
Math.min( Math.min(
@ -2701,19 +2699,13 @@ export class Editor extends EventEmitter<TLEventMap> {
private _animateViewport(ms: number): void { private _animateViewport(ms: number): void {
if (!this._viewportAnimation) return if (!this._viewportAnimation) return
const cancelAnimation = () => {
this.off('tick', this._animateViewport)
this.off('stop-camera-animation', cancelAnimation)
this._viewportAnimation = null
}
this.once('stop-camera-animation', cancelAnimation)
this._viewportAnimation.elapsed += ms this._viewportAnimation.elapsed += ms
const { elapsed, easing, duration, start, end } = this._viewportAnimation const { elapsed, easing, duration, start, end } = this._viewportAnimation
if (elapsed > duration) { if (elapsed > duration) {
this.off('tick', this._animateViewport)
this._viewportAnimation = null
this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width)) this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width))
return return
} }
@ -2725,7 +2717,9 @@ export class Editor extends EventEmitter<TLEventMap> {
const top = start.minY + (end.minY - start.minY) * t const top = start.minY + (end.minY - start.minY) * t
const right = start.maxX + (end.maxX - start.maxX) * t const right = start.maxX + (end.maxX - start.maxX) * t
this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left))) this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), {
force: true,
})
} }
/** @internal */ /** @internal */
@ -2733,8 +2727,9 @@ export class Editor extends EventEmitter<TLEventMap> {
targetViewportPage: Box, targetViewportPage: Box,
opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions
) { ) {
if (!opts.animation) return const { animation, ...rest } = opts
const { duration = 0, easing = EASINGS.easeInOutCubic } = opts.animation if (!animation) return
const { duration = 0, easing = EASINGS.easeInOutCubic } = animation
const animationSpeed = this.user.getAnimationSpeed() const animationSpeed = this.user.getAnimationSpeed()
const viewportPageBounds = this.getViewportPageBounds() const viewportPageBounds = this.getViewportPageBounds()
@ -2753,7 +2748,8 @@ export class Editor extends EventEmitter<TLEventMap> {
-targetViewportPage.x, -targetViewportPage.x,
-targetViewportPage.y, -targetViewportPage.y,
this.getViewportScreenBounds().width / targetViewportPage.width this.getViewportScreenBounds().width / targetViewportPage.width
) ),
{ ...rest }
) )
} }
@ -2766,6 +2762,12 @@ export class Editor extends EventEmitter<TLEventMap> {
end: targetViewportPage.clone(), end: targetViewportPage.clone(),
} }
// If we ever get a "stop-camera-animation" event, we stop
this.once('stop-camera-animation', () => {
this.off('tick', this._animateViewport)
this._viewportAnimation = null
})
// On each tick, animate the viewport // On each tick, animate the viewport
this.on('tick', this._animateViewport) this.on('tick', this._animateViewport)

View file

@ -56,6 +56,11 @@ export class Translating extends StateNode {
) => { ) => {
const { isCreating = false, onCreate = () => void null } = info const { isCreating = false, onCreate = () => void null } = info
if (!this.editor.getSelectedShapeIds()?.length) {
this.parent.transition('idle')
return
}
this.info = info this.info = info
this.parent.setCurrentToolIdMask(info.onInteractionEnd) this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.isCreating = isCreating this.isCreating = isCreating
@ -251,8 +256,17 @@ export class Translating extends StateNode {
} }
} }
protected handleChange() { protected updateShapes() {
const { movingShapes } = this.snapshot const { snapshot } = this
this.dragAndDropManager.updateDroppingNode(snapshot.movingShapes, this.updateParentTransforms)
moveShapesToPoint({
editor: this.editor,
snapshot,
})
const { movingShapes } = snapshot
const changes: TLShapePartial[] = [] const changes: TLShapePartial[] = []
@ -270,18 +284,6 @@ export class Translating extends StateNode {
} }
} }
protected updateShapes() {
const { snapshot } = this
this.dragAndDropManager.updateDroppingNode(snapshot.movingShapes, this.updateParentTransforms)
moveShapesToPoint({
editor: this.editor,
snapshot,
})
this.handleChange()
}
protected updateParentTransforms = () => { protected updateParentTransforms = () => {
const { const {
editor, editor,

View file

@ -361,8 +361,8 @@ describe('When constraints are contain', () => {
describe('Zoom reset positions based on origin', () => { describe('Zoom reset positions based on origin', () => {
it('Default .5, .5 origin', () => { it('Default .5, .5 origin', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -370,17 +370,17 @@ describe('Zoom reset positions based on origin', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'default', initialZoom: 'default',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
}) })
it('0 0 origin', () => { it('0 0 origin', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -388,17 +388,17 @@ describe('Zoom reset positions based on origin', () => {
origin: { x: 0, y: 0 }, origin: { x: 0, y: 0 },
initialZoom: 'default', initialZoom: 'default',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
}) })
it('1 1 origin', () => { it('1 1 origin', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -406,9 +406,9 @@ describe('Zoom reset positions based on origin', () => {
origin: { x: 1, y: 1 }, origin: { x: 1, y: 1 },
initialZoom: 'default', initialZoom: 'default',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toMatchObject({ x: 400, y: 100, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 400, y: 100, z: 1 })
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toMatchObject({ x: 400, y: 100, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 400, y: 100, z: 1 })
@ -417,8 +417,8 @@ describe('Zoom reset positions based on origin', () => {
describe('CameraOptions.constraints.initialZoom + behavior', () => { describe('CameraOptions.constraints.initialZoom + behavior', () => {
it('When fit is default', () => { it('When fit is default', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -426,17 +426,17 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'default', initialZoom: 'default',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
}) })
it('When fit is fit-max', () => { it('When fit is fit-max', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -444,17 +444,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// y should be 0 because the viewport width is bigger than the height // y should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -463,9 +462,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// y should be 0 because the viewport width is bigger than the height // y should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 666.66, y: 0, z: 0.75 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 666.66, y: 0, z: 0.75 }, 2)
@ -474,8 +472,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
// The proportions of the bounds don't matter, it's the proportion of the viewport that decides which axis gets fit // The proportions of the bounds don't matter, it's the proportion of the viewport that decides which axis gets fit
editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600)) editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600))
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -483,9 +481,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: 666.66, z: 0.75 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: 666.66, z: 0.75 }, 2)
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
@ -493,8 +490,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
}) })
it('When fit is fit-min', () => { it('When fit is fit-min', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -502,17 +499,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min', initialZoom: 'fit-min',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// x should be 0 because the viewport width is bigger than the height // x should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 1.333 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 1.333 }, 2)
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 1.333 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 1.333 }, 2)
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -521,9 +517,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min', initialZoom: 'fit-min',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// x should be 0 because the viewport width is bigger than the height // x should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -375, z: 2 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -375, z: 2 }, 2)
@ -532,8 +527,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
// The proportions of the bounds don't matter, it's the proportion of the viewport that decides which axis gets fit // The proportions of the bounds don't matter, it's the proportion of the viewport that decides which axis gets fit
editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600)) editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600))
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -541,9 +536,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min', initialZoom: 'fit-min',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toCloselyMatchObject({ x: -375, y: 0, z: 2 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: -375, y: 0, z: 2 }, 2)
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
@ -553,8 +547,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
it('When fit is fit-min-100', () => { it('When fit is fit-min-100', () => {
editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900)) editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900))
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -562,17 +556,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min-100', initialZoom: 'fit-min-100',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// Max 1 on initial / reset // Max 1 on initial / reset
expect(editor.getCamera()).toCloselyMatchObject({ x: 200, y: 50, z: 1 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 200, y: 50, z: 1 }, 2)
// Min is regular // Min is regular
editor.updateViewportScreenBounds(new Box(0, 0, 800, 450)) editor.updateViewportScreenBounds(new Box(0, 0, 800, 450))
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -580,9 +573,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
origin: { x: 0.5, y: 0.5 }, origin: { x: 0.5, y: 0.5 },
initialZoom: 'fit-min-100', initialZoom: 'fit-min-100',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 0.66 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 0.66 }, 2)
}) })
@ -590,8 +582,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
describe('Padding', () => { describe('Padding', () => {
it('sets when padding is zero', () => { it('sets when padding is zero', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -600,9 +592,8 @@ describe('Padding', () => {
padding: { x: 0, y: 0 }, padding: { x: 0, y: 0 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// y should be 0 because the viewport width is bigger than the height // y should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
@ -611,8 +602,8 @@ describe('Padding', () => {
}) })
it('sets when padding is 100, 0', () => { it('sets when padding is 100, 0', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -621,17 +612,16 @@ describe('Padding', () => {
padding: { x: 100, y: 0 }, padding: { x: 100, y: 0 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// no change because the horizontal axis has extra space available // no change because the horizontal axis has extra space available
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
editor.zoomIn().resetZoom().forceTick() editor.zoomIn().resetZoom().forceTick()
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -640,9 +630,8 @@ describe('Padding', () => {
padding: { x: 200, y: 0 }, padding: { x: 200, y: 0 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true })
)
// now we're pinching // now we're pinching
expect(editor.getCamera()).toCloselyMatchObject({ x: 200, y: 50, z: 1 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 200, y: 50, z: 1 }, 2)
@ -651,8 +640,8 @@ describe('Padding', () => {
}) })
it('sets when padding is 0 x 100', () => { it('sets when padding is 0 x 100', () => {
editor.setCameraOptions( editor
{ .setCameraOptions({
...DEFAULT_CAMERA_OPTIONS, ...DEFAULT_CAMERA_OPTIONS,
constraints: { constraints: {
...DEFAULT_CONSTRAINTS, ...DEFAULT_CONSTRAINTS,
@ -661,9 +650,8 @@ describe('Padding', () => {
padding: { x: 0, y: 100 }, padding: { x: 0, y: 100 },
initialZoom: 'fit-max', initialZoom: 'fit-max',
}, },
}, })
{ reset: true } .setCamera(editor.getCamera(), { reset: true, immediate: true })
)
// y should be 0 because the viewport width is bigger than the height // y should be 0 because the viewport width is bigger than the height
expect(editor.getCamera()).toCloselyMatchObject({ x: 314.28, y: 114.28, z: 0.875 }, 2) expect(editor.getCamera()).toCloselyMatchObject({ x: 314.28, y: 114.28, z: 0.875 }, 2)

View file

@ -97,22 +97,22 @@ export function getHashForObject(obj: any): string;
export function getHashForString(string: string): string; export function getHashForString(string: string): string;
// @public // @public
export function getIndexAbove(below: IndexKey): IndexKey; export function getIndexAbove(below?: IndexKey | undefined): IndexKey;
// @public // @public
export function getIndexBelow(above: IndexKey): IndexKey; export function getIndexBelow(above?: IndexKey | undefined): IndexKey;
// @public // @public
export function getIndexBetween(below: IndexKey, above?: IndexKey): IndexKey; export function getIndexBetween(below: IndexKey | undefined, above: IndexKey | undefined): IndexKey;
// @public // @public
export function getIndices(n: number, start?: IndexKey): IndexKey[]; export function getIndices(n: number, start?: IndexKey): IndexKey[];
// @public // @public
export function getIndicesAbove(below: IndexKey, n: number): IndexKey[]; export function getIndicesAbove(below: IndexKey | undefined, n: number): IndexKey[];
// @public // @public
export function getIndicesBelow(above: IndexKey, n: number): IndexKey[]; export function getIndicesBelow(above: IndexKey | undefined, n: number): IndexKey[];
// @public // @public
export function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[]; export function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[];

View file

@ -33,7 +33,7 @@ export function getIndicesBetween(
* @param n - The number of indices to get. * @param n - The number of indices to get.
* @public * @public
*/ */
export function getIndicesAbove(below: IndexKey, n: number) { export function getIndicesAbove(below: IndexKey | undefined, n: number) {
return generateNKeysBetween(below, undefined, n) return generateNKeysBetween(below, undefined, n)
} }
@ -43,7 +43,7 @@ export function getIndicesAbove(below: IndexKey, n: number) {
* @param n - The number of indices to get. * @param n - The number of indices to get.
* @public * @public
*/ */
export function getIndicesBelow(above: IndexKey, n: number) { export function getIndicesBelow(above: IndexKey | undefined, n: number) {
return generateNKeysBetween(undefined, above, n) return generateNKeysBetween(undefined, above, n)
} }
@ -53,7 +53,7 @@ export function getIndicesBelow(above: IndexKey, n: number) {
* @param above - The index above. * @param above - The index above.
* @public * @public
*/ */
export function getIndexBetween(below: IndexKey, above?: IndexKey) { export function getIndexBetween(below: IndexKey | undefined, above: IndexKey | undefined) {
return generateNKeysBetween(below, above, 1)[0] return generateNKeysBetween(below, above, 1)[0]
} }
@ -62,7 +62,7 @@ export function getIndexBetween(below: IndexKey, above?: IndexKey) {
* @param below - The index below. * @param below - The index below.
* @public * @public
*/ */
export function getIndexAbove(below: IndexKey) { export function getIndexAbove(below?: IndexKey | undefined) {
return generateNKeysBetween(below, undefined, 1)[0] return generateNKeysBetween(below, undefined, 1)[0]
} }
@ -71,7 +71,7 @@ export function getIndexAbove(below: IndexKey) {
* @param above - The index above. * @param above - The index above.
* @public * @public
*/ */
export function getIndexBelow(above: IndexKey) { export function getIndexBelow(above?: IndexKey | undefined) {
return generateNKeysBetween(undefined, above, 1)[0] return generateNKeysBetween(undefined, above, 1)[0]
} }

View file

@ -25,7 +25,7 @@
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0", "@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.0.0", "@vitejs/plugin-react-swc": "^3.6.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-react-refresh": "^0.3.4",

153
yarn.lock
View file

@ -898,7 +898,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.6, @babel/core@npm:^7.20.7, @babel/core@npm:^7.23.5": "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.6, @babel/core@npm:^7.20.7":
version: 7.23.9 version: 7.23.9
resolution: "@babel/core@npm:7.23.9" resolution: "@babel/core@npm:7.23.9"
dependencies: dependencies:
@ -1933,28 +1933,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/plugin-transform-react-jsx-self@npm:^7.23.3":
version: 7.23.3
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 882bf56bc932d015c2d83214133939ddcf342e5bcafa21f1a93b19f2e052145115e1e0351730897fd66e5f67cad7875b8a8d81ceb12b6e2a886ad0102cb4eb1f
languageName: node
linkType: hard
"@babel/plugin-transform-react-jsx-source@npm:^7.23.3":
version: 7.23.3
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.23.3"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.22.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 92287fb797e522d99bdc77eaa573ce79ff0ad9f1cf4e7df374645e28e51dce0adad129f6f075430b129b5bac8dad843f65021970e12e992d6d6671f0d65bb1e0
languageName: node
linkType: hard
"@babel/plugin-transform-regenerator@npm:^7.23.3": "@babel/plugin-transform-regenerator@npm:^7.23.3":
version: 7.23.3 version: 7.23.3
resolution: "@babel/plugin-transform-regenerator@npm:7.23.3" resolution: "@babel/plugin-transform-regenerator@npm:7.23.3"
@ -7164,91 +7142,91 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-darwin-arm64@npm:1.3.103": "@swc/core-darwin-arm64@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-darwin-arm64@npm:1.3.103" resolution: "@swc/core-darwin-arm64@npm:1.4.17"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-darwin-x64@npm:1.3.103": "@swc/core-darwin-x64@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-darwin-x64@npm:1.3.103" resolution: "@swc/core-darwin-x64@npm:1.4.17"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-linux-arm-gnueabihf@npm:1.3.103": "@swc/core-linux-arm-gnueabihf@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.103" resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.17"
conditions: os=linux & cpu=arm conditions: os=linux & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-linux-arm64-gnu@npm:1.3.103": "@swc/core-linux-arm64-gnu@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.103" resolution: "@swc/core-linux-arm64-gnu@npm:1.4.17"
conditions: os=linux & cpu=arm64 & libc=glibc conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-linux-arm64-musl@npm:1.3.103": "@swc/core-linux-arm64-musl@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-linux-arm64-musl@npm:1.3.103" resolution: "@swc/core-linux-arm64-musl@npm:1.4.17"
conditions: os=linux & cpu=arm64 & libc=musl conditions: os=linux & cpu=arm64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-linux-x64-gnu@npm:1.3.103": "@swc/core-linux-x64-gnu@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-linux-x64-gnu@npm:1.3.103" resolution: "@swc/core-linux-x64-gnu@npm:1.4.17"
conditions: os=linux & cpu=x64 & libc=glibc conditions: os=linux & cpu=x64 & libc=glibc
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-linux-x64-musl@npm:1.3.103": "@swc/core-linux-x64-musl@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-linux-x64-musl@npm:1.3.103" resolution: "@swc/core-linux-x64-musl@npm:1.4.17"
conditions: os=linux & cpu=x64 & libc=musl conditions: os=linux & cpu=x64 & libc=musl
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-win32-arm64-msvc@npm:1.3.103": "@swc/core-win32-arm64-msvc@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.103" resolution: "@swc/core-win32-arm64-msvc@npm:1.4.17"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-win32-ia32-msvc@npm:1.3.103": "@swc/core-win32-ia32-msvc@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.103" resolution: "@swc/core-win32-ia32-msvc@npm:1.4.17"
conditions: os=win32 & cpu=ia32 conditions: os=win32 & cpu=ia32
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core-win32-x64-msvc@npm:1.3.103": "@swc/core-win32-x64-msvc@npm:1.4.17":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core-win32-x64-msvc@npm:1.3.103" resolution: "@swc/core-win32-x64-msvc@npm:1.4.17"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core@npm:^1.3.55, @swc/core@npm:^1.3.96": "@swc/core@npm:^1.3.107, @swc/core@npm:^1.3.55":
version: 1.3.103 version: 1.4.17
resolution: "@swc/core@npm:1.3.103" resolution: "@swc/core@npm:1.4.17"
dependencies: dependencies:
"@swc/core-darwin-arm64": "npm:1.3.103" "@swc/core-darwin-arm64": "npm:1.4.17"
"@swc/core-darwin-x64": "npm:1.3.103" "@swc/core-darwin-x64": "npm:1.4.17"
"@swc/core-linux-arm-gnueabihf": "npm:1.3.103" "@swc/core-linux-arm-gnueabihf": "npm:1.4.17"
"@swc/core-linux-arm64-gnu": "npm:1.3.103" "@swc/core-linux-arm64-gnu": "npm:1.4.17"
"@swc/core-linux-arm64-musl": "npm:1.3.103" "@swc/core-linux-arm64-musl": "npm:1.4.17"
"@swc/core-linux-x64-gnu": "npm:1.3.103" "@swc/core-linux-x64-gnu": "npm:1.4.17"
"@swc/core-linux-x64-musl": "npm:1.3.103" "@swc/core-linux-x64-musl": "npm:1.4.17"
"@swc/core-win32-arm64-msvc": "npm:1.3.103" "@swc/core-win32-arm64-msvc": "npm:1.4.17"
"@swc/core-win32-ia32-msvc": "npm:1.3.103" "@swc/core-win32-ia32-msvc": "npm:1.4.17"
"@swc/core-win32-x64-msvc": "npm:1.3.103" "@swc/core-win32-x64-msvc": "npm:1.4.17"
"@swc/counter": "npm:^0.1.1" "@swc/counter": "npm:^0.1.2"
"@swc/types": "npm:^0.1.5" "@swc/types": "npm:^0.1.5"
peerDependencies: peerDependencies:
"@swc/helpers": ^0.5.0 "@swc/helpers": ^0.5.0
@ -7276,14 +7254,14 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
"@swc/helpers": "@swc/helpers":
optional: true optional: true
checksum: 65eff8264dfd73088b226091fc53d5242a8c9576caa76b27a91eeb30714a245ee4c92093ede50c3621dbd99315ca213e3d76ea73208eeacd3e4d0c1f32815309 checksum: 743da3648335b10901f9c2d6c7b332f90913f9ce0e09c040eb9b5cce71dde4e1c9dd6c78c05700433ffc173194f7857c5e0a6146c39ec4bf392f875397ed96d3
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/counter@npm:^0.1.1": "@swc/counter@npm:^0.1.2":
version: 0.1.2 version: 0.1.3
resolution: "@swc/counter@npm:0.1.2" resolution: "@swc/counter@npm:0.1.3"
checksum: 8427c594f1f0cf44b83885e9c8fe1e370c9db44ae96e07a37c117a6260ee97797d0709483efbcc244e77bac578690215f45b23254c4cd8a70fb25ddbb50bf33e checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598
languageName: node languageName: node
linkType: hard linkType: hard
@ -7830,7 +7808,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": "@types/babel__core@npm:^7.1.14":
version: 7.20.5 version: 7.20.5
resolution: "@types/babel__core@npm:7.20.5" resolution: "@types/babel__core@npm:7.20.5"
dependencies: dependencies:
@ -8996,29 +8974,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitejs/plugin-react-swc@npm:^3.5.0": "@vitejs/plugin-react-swc@npm:^3.6.0":
version: 3.5.0 version: 3.6.0
resolution: "@vitejs/plugin-react-swc@npm:3.5.0" resolution: "@vitejs/plugin-react-swc@npm:3.6.0"
dependencies: dependencies:
"@swc/core": "npm:^1.3.96" "@swc/core": "npm:^1.3.107"
peerDependencies: peerDependencies:
vite: ^4 || ^5 vite: ^4 || ^5
checksum: ca3315e2000303aa6da35b6bedc3a5c57550c5576dfa12e12d097a2f69f8c7bc68e6ce7a068685ae13fcbe121d43c133b47a0d4637ac58e366471dd6645bf8ac checksum: 8bff5065e9689d0b0405932b5f2483bd0c388812dc13219a1511023f7eaca7a53c43f75f3eae785e27f7ce5a60e99d5d32bac4845a63ab095d5562180f7efa7c
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^4.2.0":
version: 4.2.1
resolution: "@vitejs/plugin-react@npm:4.2.1"
dependencies:
"@babel/core": "npm:^7.23.5"
"@babel/plugin-transform-react-jsx-self": "npm:^7.23.3"
"@babel/plugin-transform-react-jsx-source": "npm:^7.23.3"
"@types/babel__core": "npm:^7.20.5"
react-refresh: "npm:^0.14.0"
peerDependencies:
vite: ^4.2.0 || ^5.0.0
checksum: d7fa6dacd3c246bcee482ff4b7037b2978b6ca002b79780ad4921e91ae4bc85ab234cfb94f8d4d825fed8488a0acdda2ff02b47c27b3055187c0727b18fc725e
languageName: node languageName: node
linkType: hard linkType: hard
@ -11934,7 +11897,7 @@ __metadata:
"@types/react": "npm:^18.2.47" "@types/react": "npm:^18.2.47"
"@typescript-eslint/utils": "npm:^5.59.0" "@typescript-eslint/utils": "npm:^5.59.0"
"@vercel/analytics": "npm:^1.1.1" "@vercel/analytics": "npm:^1.1.1"
"@vitejs/plugin-react-swc": "npm:^3.5.0" "@vitejs/plugin-react-swc": "npm:^3.6.0"
browser-fs-access: "npm:^0.35.0" browser-fs-access: "npm:^0.35.0"
dotenv: "npm:^16.3.1" dotenv: "npm:^16.3.1"
eslint: "npm:^8.37.0" eslint: "npm:^8.37.0"
@ -13628,7 +13591,7 @@ __metadata:
"@tldraw/assets": "workspace:*" "@tldraw/assets": "workspace:*"
"@types/lodash": "npm:^4.14.188" "@types/lodash": "npm:^4.14.188"
"@vercel/analytics": "npm:^1.1.1" "@vercel/analytics": "npm:^1.1.1"
"@vitejs/plugin-react": "npm:^4.2.0" "@vitejs/plugin-react-swc": "npm:^3.6.0"
classnames: "npm:^2.3.2" classnames: "npm:^2.3.2"
dotenv: "npm:^16.3.1" dotenv: "npm:^16.3.1"
lazyrepo: "npm:0.0.0-alpha.27" lazyrepo: "npm:0.0.0-alpha.27"