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:
parent
fabba66c0f
commit
ebc892a1a6
22 changed files with 636 additions and 277 deletions
|
@ -44,7 +44,7 @@
|
|||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "^18.2.47",
|
||||
"@typescript-eslint/utils": "^5.59.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.37.0",
|
||||
"fast-glob": "^3.3.1",
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.188",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
|
|
|
@ -145,7 +145,7 @@ const CameraOptionsControlPanel = track(() => {
|
|||
useEffect(() => {
|
||||
if (!editor) return
|
||||
editor.batch(() => {
|
||||
editor.setCameraOptions(cameraOptions, { immediate: true })
|
||||
editor.setCameraOptions(cameraOptions)
|
||||
editor.setCamera(editor.getCamera(), {
|
||||
immediate: true,
|
||||
})
|
||||
|
|
|
@ -13,16 +13,26 @@ export default function EditorFocusExample() {
|
|||
}, [focused])
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32 }}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input
|
||||
id={'focus'}
|
||||
type={'checkbox'}
|
||||
onChange={(e) => {
|
||||
setFocused(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={'focus'}>Focus</label>
|
||||
<div
|
||||
style={{ padding: 32 }}
|
||||
onPointerDown={() => {
|
||||
const editor = rEditorRef.current
|
||||
if (editor && editor.getInstanceState().isFocused) {
|
||||
editor.updateInstanceState({ isFocused: false })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<p>
|
||||
The checkbox controls the editor's <code>instanceState.isFocused</code> property.
|
||||
|
@ -39,6 +49,7 @@ export default function EditorFocusExample() {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<input type="text" placeholder="Test me" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -133,23 +133,21 @@ export function ImageAnnotationEditor({
|
|||
* component hooks into camera updates to keep the camera constrained - try uploading a very long,
|
||||
* thin image and seeing how the camera behaves.
|
||||
*/
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
constraints: {
|
||||
initialZoom: 'fit-max',
|
||||
baseZoom: 'default',
|
||||
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
|
||||
padding: { x: 32, y: 64 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
behavior: 'contain',
|
||||
},
|
||||
zoomSteps: [1, 2, 4, 8],
|
||||
zoomSpeed: 1,
|
||||
panSpeed: 1,
|
||||
isLocked: false,
|
||||
editor.setCameraOptions({
|
||||
constraints: {
|
||||
initialZoom: 'fit-max',
|
||||
baseZoom: 'default',
|
||||
bounds: { w: image.width, h: image.height, x: 0, y: 0 },
|
||||
padding: { x: 32, y: 64 },
|
||||
origin: { x: 0.5, y: 0.5 },
|
||||
behavior: 'contain',
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
zoomSteps: [1, 2, 4, 8],
|
||||
zoomSpeed: 1,
|
||||
panSpeed: 1,
|
||||
isLocked: false,
|
||||
})
|
||||
editor.setCamera(editor.getCamera(), { reset: true })
|
||||
}, [editor, imageShapeId, image])
|
||||
|
||||
return (
|
||||
|
|
|
@ -117,20 +117,18 @@ export function PdfEditor({ pdf }: { pdf: Pdf }) {
|
|||
)
|
||||
|
||||
function updateCameraBounds(isMobile: boolean) {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
bounds: targetBounds,
|
||||
padding: { x: isMobile ? 16 : 164, y: 64 },
|
||||
origin: { x: 0.5, y: 0 },
|
||||
initialZoom: 'fit-x-100',
|
||||
baseZoom: 'default',
|
||||
behavior: 'contain',
|
||||
},
|
||||
editor.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
bounds: targetBounds,
|
||||
padding: { x: isMobile ? 16 : 164, y: 64 },
|
||||
origin: { x: 0.5, y: 0 },
|
||||
initialZoom: 'fit-x-100',
|
||||
baseZoom: 'default',
|
||||
behavior: 'contain',
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
editor.setCamera(editor.getCamera(), { reset: true })
|
||||
}
|
||||
|
||||
let isMobile = editor.getViewportScreenBounds().width < 840
|
||||
|
|
11
apps/examples/src/examples/slideshow/README.md
Normal file
11
apps/examples/src/examples/slideshow/README.md
Normal 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.
|
264
apps/examples/src/examples/slideshow/SlideShowExample.tsx
Normal file
264
apps/examples/src/examples/slideshow/SlideShowExample.tsx
Normal 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,
|
||||
}
|
100
apps/examples/src/examples/slideshow/SlidesManager.tsx
Normal file
100
apps/examples/src/examples/slideshow/SlidesManager.tsx
Normal 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)
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
"include": ["src", "e2e", "./vite.config.ts", "**/*.json"],
|
||||
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./.tsbuild"
|
||||
"outDir": "./.tsbuild",
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import react from '@vitejs/plugin-react'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import path from 'path'
|
||||
import { PluginOption, defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), exampleReadmePlugin()],
|
||||
plugins: [react({ tsDecorators: true }), exampleReadmePlugin()],
|
||||
root: path.join(__dirname, 'src'),
|
||||
publicDir: path.join(__dirname, 'public'),
|
||||
build: {
|
||||
|
|
|
@ -899,7 +899,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
sendBackward(shapes: TLShape[] | TLShapeId[]): this;
|
||||
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
|
||||
setCamera(point: VecLike, opts?: TLCameraMoveOptions): this;
|
||||
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions): this;
|
||||
setCameraOptions(options: Partial<TLCameraOptions>): this;
|
||||
setCroppingShape(shape: null | TLShape | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage | TLPageId): this;
|
||||
setCurrentTool(id: string, info?: {}): this;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
--layer-canvas: 200;
|
||||
--layer-shapes: 300;
|
||||
--layer-overlays: 400;
|
||||
--layer-in-front: 500;
|
||||
--layer-following-indicator: 1000;
|
||||
--layer-blocker: 10000;
|
||||
|
||||
|
@ -265,7 +266,8 @@ input,
|
|||
|
||||
.tl-overlays {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
contain: strict;
|
||||
|
@ -291,6 +293,17 @@ input,
|
|||
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 ------------------- */
|
||||
|
||||
.tl-background__wrapper {
|
||||
|
|
|
@ -367,7 +367,22 @@ function Layout({
|
|||
useFocusEvents(autoFocus)
|
||||
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 {
|
||||
|
|
|
@ -169,7 +169,6 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<SelectionForegroundWrapper />
|
||||
<LiveCollaborators />
|
||||
</div>
|
||||
<InFrontOfTheCanvasWrapper />
|
||||
</div>
|
||||
<MovingCameraHitTestBlocker />
|
||||
</div>
|
||||
|
@ -647,12 +646,6 @@ function OnTheCanvasWrapper() {
|
|||
return <OnTheCanvas />
|
||||
}
|
||||
|
||||
function InFrontOfTheCanvasWrapper() {
|
||||
const { InFrontOfTheCanvas } = useEditorComponents()
|
||||
if (!InFrontOfTheCanvas) return null
|
||||
return <InFrontOfTheCanvas />
|
||||
}
|
||||
|
||||
function MovingCameraHitTestBlocker() {
|
||||
const editor = useEditor()
|
||||
const cameraState = useValue('camera state', () => editor.getCameraState(), [editor])
|
||||
|
|
|
@ -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
|
||||
* ```ts
|
||||
* editor.setCameraOptions(myCameraOptions)
|
||||
* editor.setCameraOptions(myCameraOptions, { immediate: true, force: true, initial: false })
|
||||
* editor.setCamera(editor.getCamera())
|
||||
* ```
|
||||
*
|
||||
* @param options - The camera options to set.
|
||||
* @param opts - The options for the change.
|
||||
*
|
||||
* @public */
|
||||
setCameraOptions(options: Partial<TLCameraOptions>, opts?: TLCameraMoveOptions) {
|
||||
setCameraOptions(options: Partial<TLCameraOptions>) {
|
||||
const next = structuredClone({
|
||||
...this.getCameraOptions(),
|
||||
...this._cameraOptions.__unsafe__getWithoutCapture(),
|
||||
...options,
|
||||
})
|
||||
if (next.zoomSteps?.length < 1) next.zoomSteps = [1]
|
||||
this._cameraOptions.set(next)
|
||||
this.setCamera(this.getCamera(), opts)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2637,16 +2635,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
bounds: BoxLike,
|
||||
opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions
|
||||
): this {
|
||||
if (this.getCameraOptions().isLocked) return this
|
||||
const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture()
|
||||
if (cameraOptions.isLocked) return this
|
||||
|
||||
const viewportScreenBounds = this.getViewportScreenBounds()
|
||||
|
||||
const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28)
|
||||
|
||||
const baseZoom = this.getBaseZoom()
|
||||
const { zoomSteps } = this.getCameraOptions()
|
||||
const zoomMin = zoomSteps[0]
|
||||
const zoomMax = last(zoomSteps)!
|
||||
const zoomMin = cameraOptions.zoomSteps[0]
|
||||
const zoomMax = last(cameraOptions.zoomSteps)!
|
||||
|
||||
let zoom = clamp(
|
||||
Math.min(
|
||||
|
@ -2701,19 +2699,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _animateViewport(ms: number): void {
|
||||
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
|
||||
|
||||
const { elapsed, easing, duration, start, end } = this._viewportAnimation
|
||||
|
||||
if (elapsed > duration) {
|
||||
this.off('tick', this._animateViewport)
|
||||
this._viewportAnimation = null
|
||||
this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width))
|
||||
return
|
||||
}
|
||||
|
@ -2725,7 +2717,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const top = start.minY + (end.minY - start.minY) * 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 */
|
||||
|
@ -2733,8 +2727,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
targetViewportPage: Box,
|
||||
opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions
|
||||
) {
|
||||
if (!opts.animation) return
|
||||
const { duration = 0, easing = EASINGS.easeInOutCubic } = opts.animation
|
||||
const { animation, ...rest } = opts
|
||||
if (!animation) return
|
||||
const { duration = 0, easing = EASINGS.easeInOutCubic } = animation
|
||||
const animationSpeed = this.user.getAnimationSpeed()
|
||||
const viewportPageBounds = this.getViewportPageBounds()
|
||||
|
||||
|
@ -2753,7 +2748,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
-targetViewportPage.x,
|
||||
-targetViewportPage.y,
|
||||
this.getViewportScreenBounds().width / targetViewportPage.width
|
||||
)
|
||||
),
|
||||
{ ...rest }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2766,6 +2762,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
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
|
||||
this.on('tick', this._animateViewport)
|
||||
|
||||
|
|
|
@ -56,6 +56,11 @@ export class Translating extends StateNode {
|
|||
) => {
|
||||
const { isCreating = false, onCreate = () => void null } = info
|
||||
|
||||
if (!this.editor.getSelectedShapeIds()?.length) {
|
||||
this.parent.transition('idle')
|
||||
return
|
||||
}
|
||||
|
||||
this.info = info
|
||||
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
|
||||
this.isCreating = isCreating
|
||||
|
@ -251,8 +256,17 @@ export class Translating extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
protected handleChange() {
|
||||
const { movingShapes } = this.snapshot
|
||||
protected updateShapes() {
|
||||
const { snapshot } = this
|
||||
|
||||
this.dragAndDropManager.updateDroppingNode(snapshot.movingShapes, this.updateParentTransforms)
|
||||
|
||||
moveShapesToPoint({
|
||||
editor: this.editor,
|
||||
snapshot,
|
||||
})
|
||||
|
||||
const { movingShapes } = snapshot
|
||||
|
||||
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 = () => {
|
||||
const {
|
||||
editor,
|
||||
|
|
|
@ -361,8 +361,8 @@ describe('When constraints are contain', () => {
|
|||
|
||||
describe('Zoom reset positions based on origin', () => {
|
||||
it('Default .5, .5 origin', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -370,17 +370,17 @@ describe('Zoom reset positions based on origin', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'default',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
|
||||
})
|
||||
|
||||
it('0 0 origin', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -388,17 +388,17 @@ describe('Zoom reset positions based on origin', () => {
|
|||
origin: { x: 0, y: 0 },
|
||||
initialZoom: 'default',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 })
|
||||
})
|
||||
|
||||
it('1 1 origin', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -406,9 +406,9 @@ describe('Zoom reset positions based on origin', () => {
|
|||
origin: { x: 1, y: 1 },
|
||||
initialZoom: 'default',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toMatchObject({ x: 400, y: 100, z: 1 })
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
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', () => {
|
||||
it('When fit is default', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -426,17 +426,17 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'default',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toMatchObject({ x: 200, y: 50, z: 1 })
|
||||
})
|
||||
|
||||
it('When fit is fit-max', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -444,17 +444,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// 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)
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
|
||||
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -463,9 +462,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// 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)
|
||||
|
@ -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
|
||||
editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600))
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -483,9 +481,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: 666.66, z: 0.75 }, 2)
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
|
@ -493,8 +490,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
})
|
||||
|
||||
it('When fit is fit-min', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -502,17 +499,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-min',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// 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)
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 1.333 }, 2)
|
||||
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -521,9 +517,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-min',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// x should be 0 because the viewport width is bigger than the height
|
||||
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
|
||||
editor.updateViewportScreenBounds(new Box(0, 0, 900, 1600))
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -541,9 +536,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-min',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: -375, y: 0, z: 2 }, 2)
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
|
@ -553,8 +547,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
it('When fit is fit-min-100', () => {
|
||||
editor.updateViewportScreenBounds(new Box(0, 0, 1600, 900))
|
||||
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -562,17 +556,16 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-min-100',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// Max 1 on initial / reset
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 200, y: 50, z: 1 }, 2)
|
||||
|
||||
// Min is regular
|
||||
editor.updateViewportScreenBounds(new Box(0, 0, 800, 450))
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -580,9 +573,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
origin: { x: 0.5, y: 0.5 },
|
||||
initialZoom: 'fit-min-100',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 0, y: -62.5, z: 0.66 }, 2)
|
||||
})
|
||||
|
@ -590,8 +582,8 @@ describe('CameraOptions.constraints.initialZoom + behavior', () => {
|
|||
|
||||
describe('Padding', () => {
|
||||
it('sets when padding is zero', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -600,9 +592,8 @@ describe('Padding', () => {
|
|||
padding: { x: 0, y: 0 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// 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)
|
||||
|
@ -611,8 +602,8 @@ describe('Padding', () => {
|
|||
})
|
||||
|
||||
it('sets when padding is 100, 0', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -621,17 +612,16 @@ describe('Padding', () => {
|
|||
padding: { x: 100, y: 0 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// no change because the horizontal axis has extra space available
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
|
||||
editor.zoomIn().resetZoom().forceTick()
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 111.11, y: 0, z: 1.125 }, 2)
|
||||
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -640,9 +630,8 @@ describe('Padding', () => {
|
|||
padding: { x: 200, y: 0 },
|
||||
initialZoom: 'fit-max',
|
||||
},
|
||||
},
|
||||
{ reset: true }
|
||||
)
|
||||
})
|
||||
.setCamera(editor.getCamera(), { reset: true })
|
||||
|
||||
// now we're pinching
|
||||
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', () => {
|
||||
editor.setCameraOptions(
|
||||
{
|
||||
editor
|
||||
.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
...DEFAULT_CONSTRAINTS,
|
||||
|
@ -661,9 +650,8 @@ describe('Padding', () => {
|
|||
padding: { x: 0, y: 100 },
|
||||
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
|
||||
expect(editor.getCamera()).toCloselyMatchObject({ x: 314.28, y: 114.28, z: 0.875 }, 2)
|
||||
|
|
|
@ -97,22 +97,22 @@ export function getHashForObject(obj: any): string;
|
|||
export function getHashForString(string: string): string;
|
||||
|
||||
// @public
|
||||
export function getIndexAbove(below: IndexKey): IndexKey;
|
||||
export function getIndexAbove(below?: IndexKey | undefined): IndexKey;
|
||||
|
||||
// @public
|
||||
export function getIndexBelow(above: IndexKey): IndexKey;
|
||||
export function getIndexBelow(above?: IndexKey | undefined): IndexKey;
|
||||
|
||||
// @public
|
||||
export function getIndexBetween(below: IndexKey, above?: IndexKey): IndexKey;
|
||||
export function getIndexBetween(below: IndexKey | undefined, above: IndexKey | undefined): IndexKey;
|
||||
|
||||
// @public
|
||||
export function getIndices(n: number, start?: IndexKey): IndexKey[];
|
||||
|
||||
// @public
|
||||
export function getIndicesAbove(below: IndexKey, n: number): IndexKey[];
|
||||
export function getIndicesAbove(below: IndexKey | undefined, n: number): IndexKey[];
|
||||
|
||||
// @public
|
||||
export function getIndicesBelow(above: IndexKey, n: number): IndexKey[];
|
||||
export function getIndicesBelow(above: IndexKey | undefined, n: number): IndexKey[];
|
||||
|
||||
// @public
|
||||
export function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[];
|
||||
|
|
|
@ -33,7 +33,7 @@ export function getIndicesBetween(
|
|||
* @param n - The number of indices to get.
|
||||
* @public
|
||||
*/
|
||||
export function getIndicesAbove(below: IndexKey, n: number) {
|
||||
export function getIndicesAbove(below: IndexKey | undefined, n: number) {
|
||||
return generateNKeysBetween(below, undefined, n)
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export function getIndicesAbove(below: IndexKey, n: number) {
|
|||
* @param n - The number of indices to get.
|
||||
* @public
|
||||
*/
|
||||
export function getIndicesBelow(above: IndexKey, n: number) {
|
||||
export function getIndicesBelow(above: IndexKey | undefined, n: number) {
|
||||
return generateNKeysBetween(undefined, above, n)
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ export function getIndicesBelow(above: IndexKey, n: number) {
|
|||
* @param above - The index above.
|
||||
* @public
|
||||
*/
|
||||
export function getIndexBetween(below: IndexKey, above?: IndexKey) {
|
||||
export function getIndexBetween(below: IndexKey | undefined, above: IndexKey | undefined) {
|
||||
return generateNKeysBetween(below, above, 1)[0]
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ export function getIndexBetween(below: IndexKey, above?: IndexKey) {
|
|||
* @param below - The index below.
|
||||
* @public
|
||||
*/
|
||||
export function getIndexAbove(below: IndexKey) {
|
||||
export function getIndexAbove(below?: IndexKey | undefined) {
|
||||
return generateNKeysBetween(below, undefined, 1)[0]
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ export function getIndexAbove(below: IndexKey) {
|
|||
* @param above - The index above.
|
||||
* @public
|
||||
*/
|
||||
export function getIndexBelow(above: IndexKey) {
|
||||
export function getIndexBelow(above?: IndexKey | undefined) {
|
||||
return generateNKeysBetween(undefined, above, 1)[0]
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^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-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
|
|
153
yarn.lock
153
yarn.lock
|
@ -898,7 +898,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "@babel/core@npm:7.23.9"
|
||||
dependencies:
|
||||
|
@ -1933,28 +1933,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.23.3
|
||||
resolution: "@babel/plugin-transform-regenerator@npm:7.23.3"
|
||||
|
@ -7164,91 +7142,91 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-arm64@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.3.103"
|
||||
"@swc/core-darwin-arm64@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.4.17"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-x64@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-darwin-x64@npm:1.3.103"
|
||||
"@swc/core-darwin-x64@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-darwin-x64@npm:1.4.17"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.103"
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.17"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-gnu@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.3.103"
|
||||
"@swc/core-linux-arm64-gnu@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.4.17"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-musl@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.3.103"
|
||||
"@swc/core-linux-arm64-musl@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.4.17"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-gnu@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.3.103"
|
||||
"@swc/core-linux-x64-gnu@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.4.17"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-musl@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.3.103"
|
||||
"@swc/core-linux-x64-musl@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.4.17"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-arm64-msvc@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.3.103"
|
||||
"@swc/core-win32-arm64-msvc@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.4.17"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-ia32-msvc@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.3.103"
|
||||
"@swc/core-win32-ia32-msvc@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.4.17"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-x64-msvc@npm:1.3.103":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.3.103"
|
||||
"@swc/core-win32-x64-msvc@npm:1.4.17":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.4.17"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core@npm:^1.3.55, @swc/core@npm:^1.3.96":
|
||||
version: 1.3.103
|
||||
resolution: "@swc/core@npm:1.3.103"
|
||||
"@swc/core@npm:^1.3.107, @swc/core@npm:^1.3.55":
|
||||
version: 1.4.17
|
||||
resolution: "@swc/core@npm:1.4.17"
|
||||
dependencies:
|
||||
"@swc/core-darwin-arm64": "npm:1.3.103"
|
||||
"@swc/core-darwin-x64": "npm:1.3.103"
|
||||
"@swc/core-linux-arm-gnueabihf": "npm:1.3.103"
|
||||
"@swc/core-linux-arm64-gnu": "npm:1.3.103"
|
||||
"@swc/core-linux-arm64-musl": "npm:1.3.103"
|
||||
"@swc/core-linux-x64-gnu": "npm:1.3.103"
|
||||
"@swc/core-linux-x64-musl": "npm:1.3.103"
|
||||
"@swc/core-win32-arm64-msvc": "npm:1.3.103"
|
||||
"@swc/core-win32-ia32-msvc": "npm:1.3.103"
|
||||
"@swc/core-win32-x64-msvc": "npm:1.3.103"
|
||||
"@swc/counter": "npm:^0.1.1"
|
||||
"@swc/core-darwin-arm64": "npm:1.4.17"
|
||||
"@swc/core-darwin-x64": "npm:1.4.17"
|
||||
"@swc/core-linux-arm-gnueabihf": "npm:1.4.17"
|
||||
"@swc/core-linux-arm64-gnu": "npm:1.4.17"
|
||||
"@swc/core-linux-arm64-musl": "npm:1.4.17"
|
||||
"@swc/core-linux-x64-gnu": "npm:1.4.17"
|
||||
"@swc/core-linux-x64-musl": "npm:1.4.17"
|
||||
"@swc/core-win32-arm64-msvc": "npm:1.4.17"
|
||||
"@swc/core-win32-ia32-msvc": "npm:1.4.17"
|
||||
"@swc/core-win32-x64-msvc": "npm:1.4.17"
|
||||
"@swc/counter": "npm:^0.1.2"
|
||||
"@swc/types": "npm:^0.1.5"
|
||||
peerDependencies:
|
||||
"@swc/helpers": ^0.5.0
|
||||
|
@ -7276,14 +7254,14 @@ __metadata:
|
|||
peerDependenciesMeta:
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 65eff8264dfd73088b226091fc53d5242a8c9576caa76b27a91eeb30714a245ee4c92093ede50c3621dbd99315ca213e3d76ea73208eeacd3e4d0c1f32815309
|
||||
checksum: 743da3648335b10901f9c2d6c7b332f90913f9ce0e09c040eb9b5cce71dde4e1c9dd6c78c05700433ffc173194f7857c5e0a6146c39ec4bf392f875397ed96d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/counter@npm:^0.1.1":
|
||||
version: 0.1.2
|
||||
resolution: "@swc/counter@npm:0.1.2"
|
||||
checksum: 8427c594f1f0cf44b83885e9c8fe1e370c9db44ae96e07a37c117a6260ee97797d0709483efbcc244e77bac578690215f45b23254c4cd8a70fb25ddbb50bf33e
|
||||
"@swc/counter@npm:^0.1.2":
|
||||
version: 0.1.3
|
||||
resolution: "@swc/counter@npm:0.1.3"
|
||||
checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7830,7 +7808,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "@types/babel__core@npm:7.20.5"
|
||||
dependencies:
|
||||
|
@ -8996,29 +8974,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react-swc@npm:^3.5.0":
|
||||
version: 3.5.0
|
||||
resolution: "@vitejs/plugin-react-swc@npm:3.5.0"
|
||||
"@vitejs/plugin-react-swc@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "@vitejs/plugin-react-swc@npm:3.6.0"
|
||||
dependencies:
|
||||
"@swc/core": "npm:^1.3.96"
|
||||
"@swc/core": "npm:^1.3.107"
|
||||
peerDependencies:
|
||||
vite: ^4 || ^5
|
||||
checksum: ca3315e2000303aa6da35b6bedc3a5c57550c5576dfa12e12d097a2f69f8c7bc68e6ce7a068685ae13fcbe121d43c133b47a0d4637ac58e366471dd6645bf8ac
|
||||
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
|
||||
checksum: 8bff5065e9689d0b0405932b5f2483bd0c388812dc13219a1511023f7eaca7a53c43f75f3eae785e27f7ce5a60e99d5d32bac4845a63ab095d5562180f7efa7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -11934,7 +11897,7 @@ __metadata:
|
|||
"@types/react": "npm:^18.2.47"
|
||||
"@typescript-eslint/utils": "npm:^5.59.0"
|
||||
"@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"
|
||||
dotenv: "npm:^16.3.1"
|
||||
eslint: "npm:^8.37.0"
|
||||
|
@ -13628,7 +13591,7 @@ __metadata:
|
|||
"@tldraw/assets": "workspace:*"
|
||||
"@types/lodash": "npm:^4.14.188"
|
||||
"@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"
|
||||
dotenv: "npm:^16.3.1"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
|
|
Loading…
Reference in a new issue