kopia lustrzana https://github.com/Tldraw/Tldraw
[Fix] Camera coordinate issues (#2719)
This PR fixes some bugs related to our coordinate systems. These bugs would appear when the editor was not full screen. ![Kapture 2024-02-04 at 11 53 37](https://github.com/tldraw/tldraw/assets/23072548/9c2199f3-b34d-4fe1-a3e5-d0c65fe5a11e) In short, we were being inconsistent with whether the `currentScreenPoint` was relative to the top left corner of the component or the top left corner of the page that contained the component. Here's the actual system: <img width="898" alt="image" src="https://github.com/tldraw/tldraw/assets/23072548/c49b3686-aeb6-4164-a55d-8639d40290a1"> The `viewportPageBounds` describes the bounds of the component within the browser's space. Its `x` and `y` describe the delta in browser space between the top left corner of the **page** and the component. This is not effected by scrolling. The use's `screenPoint` describes the user's cursor's location relative to the `viewportPageBounds`. Its `x` and `y` describe the delta in browser space between the top left corner of the **component** and the cursor. While this is a bug fix, I'm marking it as major as apps may be depending on the previous (broken) behavior. ### Change Type - [x] `major` — Breaking change ### Test Plan 1. Zoom in, out, and pinch on an editor that isn't full screen. 2. Zoom in, out, and pinch on an editor that is full screen. 3. Drag and scroll on an editor that isn't full screen. 4. Drag and scroll on an editor that is full screen. - [x] Unit Tests ### Release Notes - Fixed bugs with `getViewportScreenCenter` that could effect zooming and pinching on editors that aren't full screenpull/2721/head
rodzic
dee5d2928c
commit
647d03a173
|
@ -2074,12 +2074,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// Dispatch a new pointer move because the pointer's page will have changed
|
||||
// (its screen position will compute to a new page position given the new camera position)
|
||||
const { currentScreenPoint } = this.inputs
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
|
||||
this.dispatch({
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
point: currentScreenPoint,
|
||||
// weird but true: we need to put the screen point back into client space
|
||||
point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
|
||||
pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
|
||||
ctrlKey: this.inputs.ctrlKey,
|
||||
altKey: this.inputs.altKey,
|
||||
|
@ -2515,8 +2517,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const animationSpeed = this.user.getAnimationSpeed()
|
||||
const viewportPageBounds = this.getViewportPageBounds()
|
||||
|
||||
// If we have an existing animation, then stop it; also stop following any user
|
||||
// If we have an existing animation, then stop it
|
||||
this.stopCameraAnimation()
|
||||
|
||||
// also stop following any user
|
||||
if (this.getInstanceState().followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
|
@ -2526,7 +2530,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return this._setCamera({
|
||||
x: -targetViewportPage.x,
|
||||
y: -targetViewportPage.y,
|
||||
z: this.getViewportScreenBounds().width / targetViewportPage.width,
|
||||
z: viewportPageBounds.width / targetViewportPage.width,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2783,7 +2787,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
@computed getViewportScreenCenter() {
|
||||
return this.getViewportScreenBounds().center
|
||||
const viewportScreenBounds = this.getViewportScreenBounds()
|
||||
return new Vec(
|
||||
viewportScreenBounds.midX - viewportScreenBounds.minX,
|
||||
viewportScreenBounds.midY - viewportScreenBounds.minY
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8306,18 +8314,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this.inputs
|
||||
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x: sx, y: sy, z: sz } = info.point
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
|
||||
const sx = info.point.x - screenBounds.x
|
||||
const sy = info.point.y - screenBounds.y
|
||||
const sz = info.point.z
|
||||
|
||||
previousScreenPoint.setTo(currentScreenPoint)
|
||||
previousPagePoint.setTo(currentPagePoint)
|
||||
|
||||
// The "screen bounds" is relative to the user's actual screen.
|
||||
// The "screen point" is relative to the "screen bounds";
|
||||
// it will be 0,0 when its actual screen position is equal
|
||||
// to screenBounds.point. This is confusing!
|
||||
currentScreenPoint.set(sx, sy)
|
||||
currentPagePoint.set(
|
||||
(sx - screenBounds.x) / cz - cx,
|
||||
(sy - screenBounds.y) / cz - cy,
|
||||
sz ?? 0.5
|
||||
)
|
||||
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz ?? 0.5)
|
||||
|
||||
this.inputs.isPen = info.type === 'pointer' && info.isPen
|
||||
|
||||
|
@ -8577,10 +8588,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (!inputs.isPinching) return
|
||||
|
||||
const {
|
||||
point: { x, y, z = 1 },
|
||||
delta: { x: dx, y: dy },
|
||||
} = info
|
||||
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x, y, z = 1 } = Vec.SubXY(info.point, screenBounds.x, screenBounds.y)
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z))
|
||||
|
@ -8627,7 +8640,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// If the alt or ctrl keys are pressed,
|
||||
// zoom or pan the camera and then return.
|
||||
|
||||
// Subtract the top left offset from the user's point
|
||||
|
||||
const { x, y } = this.inputs.currentScreenPoint
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz))
|
||||
|
|
|
@ -52,6 +52,7 @@ export interface TLBaseEventInfo {
|
|||
export type TLPointerEventInfo = TLBaseEventInfo & {
|
||||
type: 'pointer'
|
||||
name: TLPointerEventName
|
||||
// The pointer position in client space, i.e. clientX / clientY
|
||||
point: VecLike
|
||||
pointerId: number
|
||||
button: number
|
||||
|
|
|
@ -185,6 +185,7 @@ export function useDocumentEvents() {
|
|||
// if the touch area overlaps with the screen edges
|
||||
// it's likely to trigger the navigation. We prevent the
|
||||
// touchstart event in that case.
|
||||
// todo: make this relative to the actual window, not the editor's screen bounds
|
||||
if (
|
||||
touchXPosition - touchXRadius < 10 ||
|
||||
touchXPosition + touchXRadius > editor.getViewportScreenBounds().width - 10
|
||||
|
|
|
@ -51,20 +51,8 @@ export function moveCameraWhenCloseToEdge(editor: Editor) {
|
|||
isCoarsePointer,
|
||||
insets: [t, r, b, l],
|
||||
} = editor.getInstanceState()
|
||||
const proximityFactorX = getEdgeProximityFactor(
|
||||
x - screenBounds.x,
|
||||
screenBounds.w,
|
||||
isCoarsePointer,
|
||||
l,
|
||||
r
|
||||
)
|
||||
const proximityFactorY = getEdgeProximityFactor(
|
||||
y - screenBounds.y,
|
||||
screenBounds.h,
|
||||
isCoarsePointer,
|
||||
t,
|
||||
b
|
||||
)
|
||||
const proximityFactorX = getEdgeProximityFactor(x, screenBounds.w, isCoarsePointer, l, r)
|
||||
const proximityFactorY = getEdgeProximityFactor(y, screenBounds.h, isCoarsePointer, t, b)
|
||||
|
||||
if (proximityFactorX === 0 && proximityFactorY === 0) return
|
||||
|
||||
|
|
|
@ -25,6 +25,28 @@ it('zooms by increments', () => {
|
|||
expect(editor.getZoomLevel()).toBe(ZOOMS[6])
|
||||
})
|
||||
|
||||
it('preserves the screen center', () => {
|
||||
const viewportCenter = editor.getViewportPageCenter().toJson()
|
||||
const screenCenter = editor.getViewportScreenCenter().toJson()
|
||||
|
||||
editor.zoomIn()
|
||||
|
||||
expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject(viewportCenter)
|
||||
expect(editor.getViewportScreenCenter().toJson()).toCloselyMatchObject(screenCenter)
|
||||
})
|
||||
|
||||
it('preserves the screen center when offset', () => {
|
||||
editor.setScreenBounds({ x: 100, y: 100, w: 1000, h: 1000 })
|
||||
|
||||
const viewportCenter = editor.getViewportPageCenter().toJson()
|
||||
const screenCenter = editor.getViewportScreenCenter().toJson()
|
||||
|
||||
editor.zoomIn()
|
||||
|
||||
expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject(viewportCenter)
|
||||
expect(editor.getViewportScreenCenter().toJson()).toCloselyMatchObject(screenCenter)
|
||||
})
|
||||
|
||||
it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => {
|
||||
editor.setCamera({ x: 0, y: 0, z: (ZOOMS[2] + ZOOMS[3]) / 2 })
|
||||
editor.zoomIn()
|
||||
|
|
Ładowanie…
Reference in New Issue