[fix] Fixes off-center bugs (#101)
* moves center from window center to center of element * Removes onMount in Renderer, adds onBoundsChange * Fix centered-g css * Fix zoom to fit
This commit is contained in:
parent
7d61d24398
commit
68efbf69fa
15 changed files with 125 additions and 104 deletions
|
@ -34,7 +34,7 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
inputs.size,
|
[inputs.bounds.width, inputs.bounds.height],
|
||||||
meta,
|
meta,
|
||||||
callbacks.onRenderCountChange
|
callbacks.onRenderCountChange
|
||||||
)
|
)
|
||||||
|
@ -59,7 +59,7 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
<Bounds
|
<Bounds
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
viewportWidth={inputs.size[0]}
|
viewportWidth={inputs.bounds.width}
|
||||||
isLocked={isLocked}
|
isLocked={isLocked}
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -56,9 +56,13 @@ export interface RendererProps<T extends TLShape, E extends Element = any, M = a
|
||||||
*/
|
*/
|
||||||
meta?: M
|
meta?: M
|
||||||
/**
|
/**
|
||||||
* A callback that receives the renderer's inputs manager.
|
* (optional) A callback that receives the renderer's inputs manager.
|
||||||
*/
|
*/
|
||||||
onMount?: (inputs: Inputs) => void
|
onMount?: (inputs: Inputs) => void
|
||||||
|
/**
|
||||||
|
* (optional) A callback that is fired when the editor's client bounding box changes.
|
||||||
|
*/
|
||||||
|
onBoundsChange?: (bounds: TLBounds) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,7 +87,7 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
||||||
}: RendererProps<T, E, M>): JSX.Element {
|
}: RendererProps<T, E, M>): JSX.Element {
|
||||||
useTLTheme(theme)
|
useTLTheme(theme)
|
||||||
|
|
||||||
const rScreenBounds = React.useRef<TLBounds>(null)
|
const rSelectionBounds = React.useRef<TLBounds>(null)
|
||||||
|
|
||||||
const rPageState = React.useRef<TLPageState>(pageState)
|
const rPageState = React.useRef<TLPageState>(pageState)
|
||||||
|
|
||||||
|
@ -96,7 +100,7 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
||||||
const [context] = React.useState<TLContextType<T, E, M>>(() => ({
|
const [context] = React.useState<TLContextType<T, E, M>>(() => ({
|
||||||
callbacks: rest,
|
callbacks: rest,
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
rScreenBounds,
|
rSelectionBounds,
|
||||||
rPageState,
|
rPageState,
|
||||||
inputs: new Inputs(),
|
inputs: new Inputs(),
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -15,17 +15,5 @@ export function useCameraCss(ref: React.RefObject<HTMLDivElement>, pageState: TL
|
||||||
ref.current!.style.setProperty('--tl-camera-y', pageState.camera.point[1] + 'px')
|
ref.current!.style.setProperty('--tl-camera-y', pageState.camera.point[1] + 'px')
|
||||||
}, [pageState.camera.point])
|
}, [pageState.camera.point])
|
||||||
|
|
||||||
// Update the group's position when the camera moves or zooms
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// const {
|
|
||||||
// zoom,
|
|
||||||
// point: [x = 0, y = 0],
|
|
||||||
// } = pageState.camera
|
|
||||||
// rLayer.current?.style.setProperty(
|
|
||||||
// 'transform',
|
|
||||||
// `scale(${zoom},${zoom}) translate(${x}px,${y}px)`
|
|
||||||
// )
|
|
||||||
// }, [pageState.camera])
|
|
||||||
|
|
||||||
return rLayer
|
return rLayer
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,18 @@ import type { TLBounds } from '+types'
|
||||||
export function usePosition(bounds: TLBounds, rotation = 0) {
|
export function usePosition(bounds: TLBounds, rotation = 0) {
|
||||||
const rBounds = React.useRef<HTMLDivElement>(null)
|
const rBounds = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Update the transform
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const elm = rBounds.current!
|
const elm = rBounds.current!
|
||||||
|
|
||||||
const transform = `
|
const transform = `
|
||||||
translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding)))
|
translate3d(
|
||||||
|
calc(${bounds.minX}px - var(--tl-padding)),
|
||||||
|
calc(${bounds.minY}px - var(--tl-padding)),
|
||||||
|
0px
|
||||||
|
)
|
||||||
rotate(${rotation + (bounds.rotation || 0)}rad)`
|
rotate(${rotation + (bounds.rotation || 0)}rad)`
|
||||||
|
|
||||||
elm.style.setProperty('transform', transform)
|
elm.style.setProperty('transform', transform)
|
||||||
|
|
||||||
elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`)
|
elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`)
|
||||||
|
|
|
@ -3,31 +3,48 @@ import * as React from 'react'
|
||||||
import { Utils } from '+utils'
|
import { Utils } from '+utils'
|
||||||
|
|
||||||
export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
|
export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
|
||||||
const { inputs } = useTLContext()
|
const { inputs, callbacks } = useTLContext()
|
||||||
|
|
||||||
const rIsMounted = React.useRef(false)
|
const rIsMounted = React.useRef(false)
|
||||||
|
|
||||||
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
|
const forceUpdate = React.useReducer((x) => x + 1, 0)[1]
|
||||||
|
|
||||||
const updateOffsets = React.useCallback(() => {
|
// When the element resizes, update the bounds (stored in inputs)
|
||||||
|
// and broadcast via the onBoundsChange callback prop.
|
||||||
|
const updateBounds = React.useCallback(() => {
|
||||||
if (rIsMounted.current) {
|
if (rIsMounted.current) {
|
||||||
const rect = ref.current?.getBoundingClientRect()
|
const rect = ref.current?.getBoundingClientRect()
|
||||||
|
|
||||||
if (rect) {
|
if (rect) {
|
||||||
inputs.offset = [rect.left, rect.top]
|
inputs.bounds = {
|
||||||
inputs.size = [rect.width, rect.height]
|
minX: rect.left,
|
||||||
|
maxX: rect.left + rect.width,
|
||||||
|
minY: rect.top,
|
||||||
|
maxY: rect.top + rect.height,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
}
|
||||||
|
|
||||||
|
callbacks.onBoundsChange?.(inputs.bounds)
|
||||||
|
|
||||||
|
// Force an update for a second mount
|
||||||
forceUpdate()
|
forceUpdate()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Skip the first mount
|
||||||
|
rIsMounted.current = true
|
||||||
}
|
}
|
||||||
rIsMounted.current = true
|
}, [ref, forceUpdate, inputs, callbacks.onBoundsChange])
|
||||||
}, [ref, forceUpdate])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)
|
const debouncedupdateBounds = Utils.debounce(updateBounds, 100)
|
||||||
window.addEventListener('scroll', debouncedUpdateOffsets)
|
window.addEventListener('scroll', debouncedupdateBounds)
|
||||||
window.addEventListener('resize', debouncedUpdateOffsets)
|
window.addEventListener('resize', debouncedupdateBounds)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', debouncedUpdateOffsets)
|
window.removeEventListener('scroll', debouncedupdateBounds)
|
||||||
window.removeEventListener('resize', debouncedUpdateOffsets)
|
window.removeEventListener('resize', debouncedupdateBounds)
|
||||||
}
|
}
|
||||||
}, [inputs])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
@ -36,7 +53,7 @@ export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entries[0].contentRect) {
|
if (entries[0].contentRect) {
|
||||||
updateOffsets()
|
updateBounds()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -50,6 +67,6 @@ export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
|
||||||
}, [ref, inputs])
|
}, [ref, inputs])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
updateOffsets()
|
updateBounds()
|
||||||
}, [ref])
|
}, [ref])
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export function useSelection<T extends TLShape, E extends Element>(
|
||||||
pageState: TLPageState,
|
pageState: TLPageState,
|
||||||
shapeUtils: TLShapeUtils<T, E>
|
shapeUtils: TLShapeUtils<T, E>
|
||||||
) {
|
) {
|
||||||
const { rScreenBounds } = useTLContext()
|
const { rSelectionBounds } = useTLContext()
|
||||||
const { selectedIds } = pageState
|
const { selectedIds } = pageState
|
||||||
|
|
||||||
let bounds: TLBounds | undefined = undefined
|
let bounds: TLBounds | undefined = undefined
|
||||||
|
@ -50,7 +50,7 @@ export function useSelection<T extends TLShape, E extends Element>(
|
||||||
const [minX, minY] = canvasToScreen([bounds.minX, bounds.minY], pageState.camera)
|
const [minX, minY] = canvasToScreen([bounds.minX, bounds.minY], pageState.camera)
|
||||||
const [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera)
|
const [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera)
|
||||||
|
|
||||||
rScreenBounds.current = {
|
rSelectionBounds.current = {
|
||||||
minX,
|
minX,
|
||||||
minY,
|
minY,
|
||||||
maxX,
|
maxX,
|
||||||
|
@ -59,7 +59,7 @@ export function useSelection<T extends TLShape, E extends Element>(
|
||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rScreenBounds.current = null
|
rSelectionBounds.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return { bounds, rotation, isLocked }
|
return { bounds, rotation, isLocked }
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Utils } from '+utils'
|
||||||
import { TLContext } from '+hooks'
|
import { TLContext } from '+hooks'
|
||||||
|
|
||||||
export function useShapeEvents(id: string, disable = false) {
|
export function useShapeEvents(id: string, disable = false) {
|
||||||
const { rPageState, rScreenBounds, callbacks, inputs } = React.useContext(TLContext)
|
const { rPageState, rSelectionBounds, callbacks, inputs } = React.useContext(TLContext)
|
||||||
|
|
||||||
const onPointerDown = React.useCallback(
|
const onPointerDown = React.useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
|
@ -26,8 +26,8 @@ export function useShapeEvents(id: string, disable = false) {
|
||||||
// treat the event as a bounding box click. Unfortunately there's no way I know to pipe
|
// treat the event as a bounding box click. Unfortunately there's no way I know to pipe
|
||||||
// the event to the actual bounds background element.
|
// the event to the actual bounds background element.
|
||||||
if (
|
if (
|
||||||
rScreenBounds.current &&
|
rSelectionBounds.current &&
|
||||||
Utils.pointInBounds(info.point, rScreenBounds.current) &&
|
Utils.pointInBounds(info.point, rSelectionBounds.current) &&
|
||||||
!rPageState.current.selectedIds.includes(id)
|
!rPageState.current.selectedIds.includes(id)
|
||||||
) {
|
) {
|
||||||
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
|
||||||
|
|
|
@ -151,8 +151,7 @@ const tlcss = css`
|
||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
contain: layout size;
|
contain: layout size;
|
||||||
transform: scale(var(--tl-zoom), var(--tl-zoom))
|
transform: scale(var(--tl-zoom)) translate3d(var(--tl-camera-x), var(--tl-camera-y), 0px);
|
||||||
translate(var(--tl-camera-x), var(--tl-camera-y));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-absolute {
|
.tl-absolute {
|
||||||
|
|
|
@ -2,12 +2,13 @@ import * as React from 'react'
|
||||||
import type { Inputs } from '+inputs'
|
import type { Inputs } from '+inputs'
|
||||||
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
|
import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export interface TLContextType<T extends TLShape, E extends Element, M = any> {
|
export interface TLContextType<T extends TLShape, E extends Element, M = any> {
|
||||||
id?: string
|
id?: string
|
||||||
callbacks: Partial<TLCallbacks<T>>
|
callbacks: Partial<TLCallbacks<T>>
|
||||||
shapeUtils: TLShapeUtils<T, E, M>
|
shapeUtils: TLShapeUtils<T, E, M>
|
||||||
rPageState: React.MutableRefObject<TLPageState>
|
rPageState: React.MutableRefObject<TLPageState>
|
||||||
rScreenBounds: React.MutableRefObject<TLBounds | null>
|
rSelectionBounds: React.MutableRefObject<TLBounds | null>
|
||||||
inputs: Inputs
|
inputs: Inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,27 @@ import type React from 'react'
|
||||||
import type { TLKeyboardInfo, TLPointerInfo } from './types'
|
import type { TLKeyboardInfo, TLPointerInfo } from './types'
|
||||||
import { Utils } from './utils'
|
import { Utils } from './utils'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
|
import type { TLBounds } from '+index'
|
||||||
|
|
||||||
const DOUBLE_CLICK_DURATION = 250
|
const DOUBLE_CLICK_DURATION = 250
|
||||||
|
|
||||||
export class Inputs {
|
export class Inputs {
|
||||||
pointer?: TLPointerInfo<string>
|
pointer?: TLPointerInfo<string>
|
||||||
|
|
||||||
keyboard?: TLKeyboardInfo
|
keyboard?: TLKeyboardInfo
|
||||||
|
|
||||||
keys: Record<string, boolean> = {}
|
keys: Record<string, boolean> = {}
|
||||||
|
|
||||||
isPinching = false
|
isPinching = false
|
||||||
|
|
||||||
offset = [0, 0]
|
bounds: TLBounds = {
|
||||||
size = [10, 10]
|
minX: 0,
|
||||||
|
maxX: 640,
|
||||||
|
minY: 0,
|
||||||
|
maxY: 480,
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
}
|
||||||
|
|
||||||
pointerUpTime = 0
|
pointerUpTime = 0
|
||||||
|
|
||||||
|
@ -41,9 +51,9 @@ export class Inputs {
|
||||||
const info: TLPointerInfo<T> = {
|
const info: TLPointerInfo<T> = {
|
||||||
target,
|
target,
|
||||||
pointerId: touch.identifier,
|
pointerId: touch.identifier,
|
||||||
origin: Inputs.getPoint(touch),
|
origin: Inputs.getPoint(touch, this.bounds),
|
||||||
delta: [0, 0],
|
delta: [0, 0],
|
||||||
point: Inputs.getPoint(touch),
|
point: Inputs.getPoint(touch, this.bounds),
|
||||||
pressure: Inputs.getPressure(touch),
|
pressure: Inputs.getPressure(touch),
|
||||||
shiftKey,
|
shiftKey,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
|
@ -64,9 +74,9 @@ export class Inputs {
|
||||||
const info: TLPointerInfo<T> = {
|
const info: TLPointerInfo<T> = {
|
||||||
target,
|
target,
|
||||||
pointerId: touch.identifier,
|
pointerId: touch.identifier,
|
||||||
origin: Inputs.getPoint(touch),
|
origin: Inputs.getPoint(touch, this.bounds),
|
||||||
delta: [0, 0],
|
delta: [0, 0],
|
||||||
point: Inputs.getPoint(touch),
|
point: Inputs.getPoint(touch, this.bounds),
|
||||||
pressure: Inputs.getPressure(touch),
|
pressure: Inputs.getPressure(touch),
|
||||||
shiftKey,
|
shiftKey,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
|
@ -88,7 +98,7 @@ export class Inputs {
|
||||||
|
|
||||||
const prev = this.pointer
|
const prev = this.pointer
|
||||||
|
|
||||||
const point = Inputs.getPoint(touch)
|
const point = Inputs.getPoint(touch, this.bounds)
|
||||||
|
|
||||||
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
||||||
|
|
||||||
|
@ -114,7 +124,7 @@ export class Inputs {
|
||||||
pointerDown<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
|
pointerDown<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
const point = Inputs.getPoint(e, this.offset)
|
const point = Inputs.getPoint(e, this.bounds)
|
||||||
|
|
||||||
this.activePointer = e.pointerId
|
this.activePointer = e.pointerId
|
||||||
|
|
||||||
|
@ -142,7 +152,7 @@ export class Inputs {
|
||||||
): TLPointerInfo<T> {
|
): TLPointerInfo<T> {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
const point = Inputs.getPoint(e, this.offset)
|
const point = Inputs.getPoint(e, this.bounds)
|
||||||
|
|
||||||
const info: TLPointerInfo<T> = {
|
const info: TLPointerInfo<T> = {
|
||||||
target,
|
target,
|
||||||
|
@ -167,7 +177,7 @@ export class Inputs {
|
||||||
|
|
||||||
const prev = this.pointer
|
const prev = this.pointer
|
||||||
|
|
||||||
const point = Inputs.getPoint(e, this.offset)
|
const point = Inputs.getPoint(e, this.bounds)
|
||||||
|
|
||||||
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
||||||
|
|
||||||
|
@ -195,7 +205,7 @@ export class Inputs {
|
||||||
|
|
||||||
const prev = this.pointer
|
const prev = this.pointer
|
||||||
|
|
||||||
const point = Inputs.getPoint(e, this.offset)
|
const point = Inputs.getPoint(e, this.bounds)
|
||||||
|
|
||||||
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
const delta = prev?.point ? Vec.sub(point, prev.point) : [0, 0]
|
||||||
|
|
||||||
|
@ -231,7 +241,7 @@ export class Inputs {
|
||||||
origin: this.pointer?.origin || [0, 0],
|
origin: this.pointer?.origin || [0, 0],
|
||||||
delta: [0, 0],
|
delta: [0, 0],
|
||||||
pressure: 0.5,
|
pressure: 0.5,
|
||||||
point: Inputs.getPoint(e, this.offset),
|
point: Inputs.getPoint(e, this.bounds),
|
||||||
shiftKey,
|
shiftKey,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
metaKey,
|
metaKey,
|
||||||
|
@ -252,7 +262,7 @@ export class Inputs {
|
||||||
|
|
||||||
const prev = this.pointer
|
const prev = this.pointer
|
||||||
|
|
||||||
const point = Inputs.getPoint(e, this.offset)
|
const point = Inputs.getPoint(e, this.bounds)
|
||||||
|
|
||||||
const info: TLPointerInfo<'wheel'> = {
|
const info: TLPointerInfo<'wheel'> = {
|
||||||
...prev,
|
...prev,
|
||||||
|
@ -330,7 +340,7 @@ export class Inputs {
|
||||||
target: 'pinch',
|
target: 'pinch',
|
||||||
origin,
|
origin,
|
||||||
delta: delta,
|
delta: delta,
|
||||||
point: Vec.sub(Vec.round(point), this.offset),
|
point: Vec.sub(Vec.round(point), [this.bounds.minX, this.bounds.minY]),
|
||||||
pressure: 0.5,
|
pressure: 0.5,
|
||||||
shiftKey,
|
shiftKey,
|
||||||
ctrlKey,
|
ctrlKey,
|
||||||
|
@ -353,9 +363,9 @@ export class Inputs {
|
||||||
|
|
||||||
static getPoint(
|
static getPoint(
|
||||||
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
|
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
|
||||||
offset = [0, 0]
|
bounds: TLBounds
|
||||||
): number[] {
|
): number[] {
|
||||||
return [+e.clientX.toFixed(2) - offset[0], +e.clientY.toFixed(2) - offset[1]]
|
return [+e.clientX.toFixed(2) - bounds.minX, +e.clientY.toFixed(2) - bounds.minY]
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) {
|
static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) {
|
||||||
|
|
|
@ -7,13 +7,13 @@ import { Inputs } from '+inputs'
|
||||||
|
|
||||||
export const ContextWrapper: React.FC = ({ children }) => {
|
export const ContextWrapper: React.FC = ({ children }) => {
|
||||||
useTLTheme()
|
useTLTheme()
|
||||||
const rScreenBounds = React.useRef<TLBounds>(null)
|
const rSelectionBounds = React.useRef<TLBounds>(null)
|
||||||
const rPageState = React.useRef<TLPageState>(mockDocument.pageState)
|
const rPageState = React.useRef<TLPageState>(mockDocument.pageState)
|
||||||
|
|
||||||
const [context] = React.useState(() => ({
|
const [context] = React.useState(() => ({
|
||||||
callbacks: {},
|
callbacks: {},
|
||||||
shapeUtils: mockUtils,
|
shapeUtils: mockUtils,
|
||||||
rScreenBounds,
|
rSelectionBounds,
|
||||||
rPageState,
|
rPageState,
|
||||||
inputs: new Inputs(),
|
inputs: new Inputs(),
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -205,6 +205,7 @@ export interface TLCallbacks<T extends TLShape> {
|
||||||
onShapeBlur: TLShapeBlurHandler<any>
|
onShapeBlur: TLShapeBlurHandler<any>
|
||||||
onRenderCountChange: (ids: string[]) => void
|
onRenderCountChange: (ids: string[]) => void
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
|
onBoundsChange: (bounds: TLBounds) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TLBounds {
|
export interface TLBounds {
|
||||||
|
|
|
@ -220,7 +220,7 @@ function InnerTldraw({
|
||||||
onRenderCountChange={tlstate.onRenderCountChange}
|
onRenderCountChange={tlstate.onRenderCountChange}
|
||||||
onShapeChange={tlstate.onShapeChange}
|
onShapeChange={tlstate.onShapeChange}
|
||||||
onShapeBlur={tlstate.onShapeBlur}
|
onShapeBlur={tlstate.onShapeBlur}
|
||||||
onMount={tlstate.handleMount}
|
onBoundsChange={tlstate.updateBounds}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<div className={menuButtons()}>
|
<div className={menuButtons()}>
|
||||||
|
|
|
@ -33,20 +33,6 @@ export class TLDR {
|
||||||
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getViewport(data: Data): TLBounds {
|
|
||||||
const [minX, minY] = TLDR.screenToWorld(data, [0, 0])
|
|
||||||
const [maxX, maxY] = TLDR.screenToWorld(data, [window.innerWidth, window.innerHeight])
|
|
||||||
|
|
||||||
return {
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY,
|
|
||||||
height: maxX - minX,
|
|
||||||
width: maxY - minY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getCameraZoom(zoom: number) {
|
static getCameraZoom(zoom: number) {
|
||||||
return Utils.clamp(zoom, 0.1, 5)
|
return Utils.clamp(zoom, 0.1, 5)
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,16 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
|
|
||||||
selectedGroupId?: string
|
selectedGroupId?: string
|
||||||
|
|
||||||
|
// The editor's bounding client rect
|
||||||
|
bounds: TLBounds = {
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 640,
|
||||||
|
maxY: 480,
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
}
|
||||||
|
|
||||||
private pasteInfo = {
|
private pasteInfo = {
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
offset: [0, 0],
|
offset: [0, 0],
|
||||||
|
@ -354,6 +364,14 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
this.inputs = inputs
|
this.inputs = inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the bounding box when the renderer's bounds change.
|
||||||
|
* @param bounds
|
||||||
|
*/
|
||||||
|
updateBounds = (bounds: TLBounds) => {
|
||||||
|
this.bounds = { ...bounds }
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Settings & UI */
|
/* Settings & UI */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -861,14 +879,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
|
|
||||||
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
||||||
|
|
||||||
let center = Vec.round(
|
let center = Vec.round(this.getPagePoint(point || this.centerPoint))
|
||||||
this.getPagePoint(
|
|
||||||
point ||
|
|
||||||
(this.inputs
|
|
||||||
? [this.inputs.size[0] / 2, this.inputs.size[1] / 2]
|
|
||||||
: [window.innerWidth / 2, window.innerHeight / 2])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
||||||
|
@ -914,10 +925,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
type: TLDrawShapeType.Text,
|
type: TLDrawShapeType.Text,
|
||||||
parentId: this.appState.currentPageId,
|
parentId: this.appState.currentPageId,
|
||||||
text: result,
|
text: result,
|
||||||
point: this.getPagePoint(
|
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
||||||
[window.innerWidth / 2, window.innerHeight / 2],
|
|
||||||
this.currentPageId
|
|
||||||
),
|
|
||||||
style: { ...this.appState.currentStyle },
|
style: { ...this.appState.currentStyle },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1030,11 +1038,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* Reset the camera to the default position
|
* Reset the camera to the default position
|
||||||
*/
|
*/
|
||||||
resetCamera = (): this => {
|
resetCamera = (): this => {
|
||||||
return this.setCamera(
|
return this.setCamera(this.centerPoint, 1, `reset_camera`)
|
||||||
Vec.round([window.innerWidth / 2, window.innerHeight / 2]),
|
|
||||||
1,
|
|
||||||
`reset_camera`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1066,7 +1070,7 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
* @param next The new zoom level.
|
* @param next The new zoom level.
|
||||||
* @param center The point to zoom towards (defaults to screen center).
|
* @param center The point to zoom towards (defaults to screen center).
|
||||||
*/
|
*/
|
||||||
zoomTo = (next: number, center = [window.innerWidth / 2, window.innerHeight / 2]): this => {
|
zoomTo = (next: number, center = this.centerPoint): this => {
|
||||||
const { zoom, point } = this.pageState.camera
|
const { zoom, point } = this.pageState.camera
|
||||||
const p0 = Vec.sub(Vec.div(center, zoom), point)
|
const p0 = Vec.sub(Vec.div(center, zoom), point)
|
||||||
const p1 = Vec.sub(Vec.div(center, next), point)
|
const p1 = Vec.sub(Vec.div(center, next), point)
|
||||||
|
@ -1102,13 +1106,13 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
||||||
|
|
||||||
const zoom = TLDR.getCameraZoom(
|
const zoom = TLDR.getCameraZoom(
|
||||||
window.innerWidth < window.innerHeight
|
this.bounds.width < this.bounds.height
|
||||||
? (window.innerWidth - 128) / bounds.width
|
? (this.bounds.width - 128) / bounds.width
|
||||||
: (window.innerHeight - 128) / bounds.height
|
: (this.bounds.height - 128) / bounds.height
|
||||||
)
|
)
|
||||||
|
|
||||||
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
|
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
|
||||||
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
|
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
|
||||||
|
|
||||||
return this.setCamera(
|
return this.setCamera(
|
||||||
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
||||||
|
@ -1126,13 +1130,13 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = TLDR.getSelectedBounds(this.state)
|
const bounds = TLDR.getSelectedBounds(this.state)
|
||||||
|
|
||||||
const zoom = TLDR.getCameraZoom(
|
const zoom = TLDR.getCameraZoom(
|
||||||
window.innerWidth < window.innerHeight
|
this.bounds.width < this.bounds.height
|
||||||
? (window.innerWidth - 128) / bounds.width
|
? (this.bounds.width - 128) / bounds.width
|
||||||
: (window.innerHeight - 128) / bounds.height
|
: (this.bounds.height - 128) / bounds.height
|
||||||
)
|
)
|
||||||
|
|
||||||
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
|
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
|
||||||
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
|
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
|
||||||
|
|
||||||
return this.setCamera(
|
return this.setCamera(
|
||||||
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
||||||
|
@ -1153,8 +1157,8 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
||||||
|
|
||||||
const { zoom } = pageState.camera
|
const { zoom } = pageState.camera
|
||||||
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
|
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
|
||||||
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
|
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
|
||||||
|
|
||||||
return this.setCamera(
|
return this.setCamera(
|
||||||
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
||||||
|
@ -2813,4 +2817,8 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
onError = () => {
|
onError = () => {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get centerPoint() {
|
||||||
|
return Vec.round(Utils.getBoundsCenter(this.bounds))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue