[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:
Steve Ruiz 2021-09-22 09:45:09 +01:00 committed by GitHub
parent 7d61d24398
commit 68efbf69fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 125 additions and 104 deletions

View file

@ -34,7 +34,7 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
page,
pageState,
shapeUtils,
inputs.size,
[inputs.bounds.width, inputs.bounds.height],
meta,
callbacks.onRenderCountChange
)
@ -59,7 +59,7 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
<Bounds
zoom={zoom}
bounds={bounds}
viewportWidth={inputs.size[0]}
viewportWidth={inputs.bounds.width}
isLocked={isLocked}
rotation={rotation}
/>

View file

@ -56,9 +56,13 @@ export interface RendererProps<T extends TLShape, E extends Element = any, M = a
*/
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
/**
* (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 {
useTLTheme(theme)
const rScreenBounds = React.useRef<TLBounds>(null)
const rSelectionBounds = React.useRef<TLBounds>(null)
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>>(() => ({
callbacks: rest,
shapeUtils,
rScreenBounds,
rSelectionBounds,
rPageState,
inputs: new Inputs(),
}))

View file

@ -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')
}, [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
}

View file

@ -5,11 +5,18 @@ import type { TLBounds } from '+types'
export function usePosition(bounds: TLBounds, rotation = 0) {
const rBounds = React.useRef<HTMLDivElement>(null)
// Update the transform
React.useLayoutEffect(() => {
const elm = rBounds.current!
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)`
elm.style.setProperty('transform', transform)
elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`)

View file

@ -3,31 +3,48 @@ import * as React from 'react'
import { Utils } from '+utils'
export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
const { inputs } = useTLContext()
const { inputs, callbacks } = useTLContext()
const rIsMounted = React.useRef(false)
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) {
const rect = ref.current?.getBoundingClientRect()
if (rect) {
inputs.offset = [rect.left, rect.top]
inputs.size = [rect.width, rect.height]
inputs.bounds = {
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()
}
} else {
// Skip the first mount
rIsMounted.current = true
}
rIsMounted.current = true
}, [ref, forceUpdate])
}, [ref, forceUpdate, inputs, callbacks.onBoundsChange])
React.useEffect(() => {
const debouncedUpdateOffsets = Utils.debounce(updateOffsets, 100)
window.addEventListener('scroll', debouncedUpdateOffsets)
window.addEventListener('resize', debouncedUpdateOffsets)
const debouncedupdateBounds = Utils.debounce(updateBounds, 100)
window.addEventListener('scroll', debouncedupdateBounds)
window.addEventListener('resize', debouncedupdateBounds)
return () => {
window.removeEventListener('scroll', debouncedUpdateOffsets)
window.removeEventListener('resize', debouncedUpdateOffsets)
window.removeEventListener('scroll', debouncedupdateBounds)
window.removeEventListener('resize', debouncedupdateBounds)
}
}, [inputs])
}, [])
React.useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
@ -36,7 +53,7 @@ export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
}
if (entries[0].contentRect) {
updateOffsets()
updateBounds()
}
})
@ -50,6 +67,6 @@ export function useResizeObserver<T extends Element>(ref: React.RefObject<T>) {
}, [ref, inputs])
React.useEffect(() => {
updateOffsets()
updateBounds()
}, [ref])
}

View file

@ -11,7 +11,7 @@ export function useSelection<T extends TLShape, E extends Element>(
pageState: TLPageState,
shapeUtils: TLShapeUtils<T, E>
) {
const { rScreenBounds } = useTLContext()
const { rSelectionBounds } = useTLContext()
const { selectedIds } = pageState
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 [maxX, maxY] = canvasToScreen([bounds.maxX, bounds.maxY], pageState.camera)
rScreenBounds.current = {
rSelectionBounds.current = {
minX,
minY,
maxX,
@ -59,7 +59,7 @@ export function useSelection<T extends TLShape, E extends Element>(
height: maxY - minY,
}
} else {
rScreenBounds.current = null
rSelectionBounds.current = null
}
return { bounds, rotation, isLocked }

View file

@ -3,7 +3,7 @@ import { Utils } from '+utils'
import { TLContext } from '+hooks'
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(
(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
// the event to the actual bounds background element.
if (
rScreenBounds.current &&
Utils.pointInBounds(info.point, rScreenBounds.current) &&
rSelectionBounds.current &&
Utils.pointInBounds(info.point, rSelectionBounds.current) &&
!rPageState.current.selectedIds.includes(id)
) {
callbacks.onPointBounds?.(inputs.pointerDown(e, 'bounds'), e)

View file

@ -151,8 +151,7 @@ const tlcss = css`
height: 0;
width: 0;
contain: layout size;
transform: scale(var(--tl-zoom), var(--tl-zoom))
translate(var(--tl-camera-x), var(--tl-camera-y));
transform: scale(var(--tl-zoom)) translate3d(var(--tl-camera-x), var(--tl-camera-y), 0px);
}
.tl-absolute {

View file

@ -2,12 +2,13 @@ import * as React from 'react'
import type { Inputs } from '+inputs'
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> {
id?: string
callbacks: Partial<TLCallbacks<T>>
shapeUtils: TLShapeUtils<T, E, M>
rPageState: React.MutableRefObject<TLPageState>
rScreenBounds: React.MutableRefObject<TLBounds | null>
rSelectionBounds: React.MutableRefObject<TLBounds | null>
inputs: Inputs
}

View file

@ -2,17 +2,27 @@ import type React from 'react'
import type { TLKeyboardInfo, TLPointerInfo } from './types'
import { Utils } from './utils'
import { Vec } from '@tldraw/vec'
import type { TLBounds } from '+index'
const DOUBLE_CLICK_DURATION = 250
export class Inputs {
pointer?: TLPointerInfo<string>
keyboard?: TLKeyboardInfo
keys: Record<string, boolean> = {}
isPinching = false
offset = [0, 0]
size = [10, 10]
bounds: TLBounds = {
minX: 0,
maxX: 640,
minY: 0,
maxY: 480,
width: 640,
height: 480,
}
pointerUpTime = 0
@ -41,9 +51,9 @@ export class Inputs {
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
origin: Inputs.getPoint(touch),
origin: Inputs.getPoint(touch, this.bounds),
delta: [0, 0],
point: Inputs.getPoint(touch),
point: Inputs.getPoint(touch, this.bounds),
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
@ -64,9 +74,9 @@ export class Inputs {
const info: TLPointerInfo<T> = {
target,
pointerId: touch.identifier,
origin: Inputs.getPoint(touch),
origin: Inputs.getPoint(touch, this.bounds),
delta: [0, 0],
point: Inputs.getPoint(touch),
point: Inputs.getPoint(touch, this.bounds),
pressure: Inputs.getPressure(touch),
shiftKey,
ctrlKey,
@ -88,7 +98,7 @@ export class Inputs {
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]
@ -114,7 +124,7 @@ export class Inputs {
pointerDown<T extends string>(e: PointerEvent | React.PointerEvent, target: T): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const point = Inputs.getPoint(e, this.offset)
const point = Inputs.getPoint(e, this.bounds)
this.activePointer = e.pointerId
@ -142,7 +152,7 @@ export class Inputs {
): TLPointerInfo<T> {
const { shiftKey, ctrlKey, metaKey, altKey } = e
const point = Inputs.getPoint(e, this.offset)
const point = Inputs.getPoint(e, this.bounds)
const info: TLPointerInfo<T> = {
target,
@ -167,7 +177,7 @@ export class Inputs {
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]
@ -195,7 +205,7 @@ export class Inputs {
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]
@ -231,7 +241,7 @@ export class Inputs {
origin: this.pointer?.origin || [0, 0],
delta: [0, 0],
pressure: 0.5,
point: Inputs.getPoint(e, this.offset),
point: Inputs.getPoint(e, this.bounds),
shiftKey,
ctrlKey,
metaKey,
@ -252,7 +262,7 @@ export class Inputs {
const prev = this.pointer
const point = Inputs.getPoint(e, this.offset)
const point = Inputs.getPoint(e, this.bounds)
const info: TLPointerInfo<'wheel'> = {
...prev,
@ -330,7 +340,7 @@ export class Inputs {
target: 'pinch',
origin,
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,
shiftKey,
ctrlKey,
@ -353,9 +363,9 @@ export class Inputs {
static getPoint(
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
offset = [0, 0]
bounds: TLBounds
): 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) {

View file

@ -7,13 +7,13 @@ import { Inputs } from '+inputs'
export const ContextWrapper: React.FC = ({ children }) => {
useTLTheme()
const rScreenBounds = React.useRef<TLBounds>(null)
const rSelectionBounds = React.useRef<TLBounds>(null)
const rPageState = React.useRef<TLPageState>(mockDocument.pageState)
const [context] = React.useState(() => ({
callbacks: {},
shapeUtils: mockUtils,
rScreenBounds,
rSelectionBounds,
rPageState,
inputs: new Inputs(),
}))

View file

@ -205,6 +205,7 @@ export interface TLCallbacks<T extends TLShape> {
onShapeBlur: TLShapeBlurHandler<any>
onRenderCountChange: (ids: string[]) => void
onError: (error: Error) => void
onBoundsChange: (bounds: TLBounds) => void
}
export interface TLBounds {

View file

@ -220,7 +220,7 @@ function InnerTldraw({
onRenderCountChange={tlstate.onRenderCountChange}
onShapeChange={tlstate.onShapeChange}
onShapeBlur={tlstate.onShapeBlur}
onMount={tlstate.handleMount}
onBoundsChange={tlstate.updateBounds}
/>
</ContextMenu>
<div className={menuButtons()}>

View file

@ -33,20 +33,6 @@ export class TLDR {
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) {
return Utils.clamp(zoom, 0.1, 5)
}

View file

@ -119,6 +119,16 @@ export class TLDrawState extends StateManager<Data> {
selectedGroupId?: string
// The editor's bounding client rect
bounds: TLBounds = {
minX: 0,
minY: 0,
maxX: 640,
maxY: 480,
width: 640,
height: 480,
}
private pasteInfo = {
center: [0, 0],
offset: [0, 0],
@ -354,6 +364,14 @@ export class TLDrawState extends StateManager<Data> {
this.inputs = inputs
}
/**
* Update the bounding box when the renderer's bounds change.
* @param bounds
*/
updateBounds = (bounds: TLBounds) => {
this.bounds = { ...bounds }
}
/* -------------------------------------------------- */
/* Settings & UI */
/* -------------------------------------------------- */
@ -861,14 +879,7 @@ export class TLDrawState extends StateManager<Data> {
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
let center = Vec.round(
this.getPagePoint(
point ||
(this.inputs
? [this.inputs.size[0] / 2, this.inputs.size[1] / 2]
: [window.innerWidth / 2, window.innerHeight / 2])
)
)
let center = Vec.round(this.getPagePoint(point || this.centerPoint))
if (
Vec.dist(center, this.pasteInfo.center) < 2 ||
@ -914,10 +925,7 @@ export class TLDrawState extends StateManager<Data> {
type: TLDrawShapeType.Text,
parentId: this.appState.currentPageId,
text: result,
point: this.getPagePoint(
[window.innerWidth / 2, window.innerHeight / 2],
this.currentPageId
),
point: this.getPagePoint(this.centerPoint, this.currentPageId),
style: { ...this.appState.currentStyle },
})
@ -1030,11 +1038,7 @@ export class TLDrawState extends StateManager<Data> {
* Reset the camera to the default position
*/
resetCamera = (): this => {
return this.setCamera(
Vec.round([window.innerWidth / 2, window.innerHeight / 2]),
1,
`reset_camera`
)
return this.setCamera(this.centerPoint, 1, `reset_camera`)
}
/**
@ -1066,7 +1070,7 @@ export class TLDrawState extends StateManager<Data> {
* @param next The new zoom level.
* @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 p0 = Vec.sub(Vec.div(center, zoom), 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 zoom = TLDR.getCameraZoom(
window.innerWidth < window.innerHeight
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
this.bounds.width < this.bounds.height
? (this.bounds.width - 128) / bounds.width
: (this.bounds.height - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
return this.setCamera(
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 zoom = TLDR.getCameraZoom(
window.innerWidth < window.innerHeight
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
this.bounds.width < this.bounds.height
? (this.bounds.width - 128) / bounds.width
: (this.bounds.height - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
return this.setCamera(
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 { zoom } = pageState.camera
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
const mx = (this.bounds.width - bounds.width * zoom) / 2 / zoom
const my = (this.bounds.height - bounds.height * zoom) / 2 / zoom
return this.setCamera(
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
@ -2813,4 +2817,8 @@ export class TLDrawState extends StateManager<Data> {
onError = () => {
// TODO
}
get centerPoint() {
return Vec.round(Utils.getBoundsCenter(this.bounds))
}
}