zoom to affected shapes after undo/redo (#2293)
This PR makes it so that any shapes affected by an undo/redo action, along with any shapes that are selected after an undo/redo action, are visible in the viewport. ### Change Type - [x] `patch` — Bug fix ### Release Notes - Make sure affected shapes are visible after undo/redo
This commit is contained in:
parent
e3d21e0b2b
commit
f7ae99dd1f
2 changed files with 117 additions and 2 deletions
|
@ -99,6 +99,7 @@ import {
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
} from '../utils/reordering/reordering'
|
} from '../utils/reordering/reordering'
|
||||||
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
||||||
|
import { uniq } from '../utils/uniq'
|
||||||
import { uniqueId } from '../utils/uniqueId'
|
import { uniqueId } from '../utils/uniqueId'
|
||||||
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
||||||
import { parentsToChildren } from './derivations/parentsToChildren'
|
import { parentsToChildren } from './derivations/parentsToChildren'
|
||||||
|
@ -781,6 +782,29 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private _ensureAffectedShapesAreMadeVisible(fn: () => void) {
|
||||||
|
// here we collect all the ids of shapes that are created or updated by the function
|
||||||
|
// along with the selectedIds of the current page
|
||||||
|
// then we attempt to make sure that they are all visible in the viewport after
|
||||||
|
// the function is run.
|
||||||
|
const changes = this.store.extractingChanges(fn)
|
||||||
|
const affectedRecordIds = uniq(
|
||||||
|
Object.keys(changes.added)
|
||||||
|
.concat(Object.keys(changes.updated))
|
||||||
|
.concat(this.getSelectedShapeIds())
|
||||||
|
)
|
||||||
|
const shapes = compact(
|
||||||
|
affectedRecordIds.map((id) => (isShapeId(id) ? this.getShape(id) : null))
|
||||||
|
)
|
||||||
|
if (!shapes.length) return this
|
||||||
|
const bounds = Box2d.Common(compact(shapes.map((shape) => this.getShapePageBounds(shape))))
|
||||||
|
const viewport = this.getViewportPageBounds()
|
||||||
|
if (!viewport.contains(bounds)) {
|
||||||
|
this.zoomToBounds(bounds, this.getCamera().z, { duration: 220 })
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo to the last mark.
|
* Undo to the last mark.
|
||||||
*
|
*
|
||||||
|
@ -792,7 +816,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
undo(): this {
|
undo(): this {
|
||||||
this.history.undo()
|
this._ensureAffectedShapesAreMadeVisible(() => {
|
||||||
|
this.history.undo()
|
||||||
|
})
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -825,7 +851,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
redo(): this {
|
redo(): this {
|
||||||
this.history.redo()
|
this._ensureAffectedShapesAreMadeVisible(() => {
|
||||||
|
this.history.redo()
|
||||||
|
})
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
AssetRecordType,
|
AssetRecordType,
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
|
Box2d,
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
TLShape,
|
TLShape,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
@ -645,3 +646,89 @@ describe('when the user prefers light UI', () => {
|
||||||
expect(editor.user.getIsDarkMode()).toBe(false)
|
expect(editor.user.getIsDarkMode()).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('undo and redo', () => {
|
||||||
|
test('cause the camera to move if the affected shapes are offscreen', () => {
|
||||||
|
editor = new TestEditor({})
|
||||||
|
editor.setScreenBounds(new Box2d(0, 0, 1000, 1000))
|
||||||
|
editor.user.updateUserPreferences({ animationSpeed: 0 })
|
||||||
|
|
||||||
|
const boxId = createShapeId('box')
|
||||||
|
editor.createShapes([{ id: boxId, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
|
||||||
|
editor.panZoomIntoView([boxId])
|
||||||
|
editor.mark()
|
||||||
|
const cameraBefore = editor.getCamera()
|
||||||
|
|
||||||
|
editor.updateShapes([
|
||||||
|
{
|
||||||
|
id: boxId,
|
||||||
|
type: 'geo',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
props: {
|
||||||
|
geo: 'cloud',
|
||||||
|
w: 100,
|
||||||
|
h: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"id": "camera:page:page",
|
||||||
|
"meta": Object {},
|
||||||
|
"typeName": "camera",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 1,
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
editor.undo()
|
||||||
|
expect(editor.getCamera()).toEqual(cameraBefore)
|
||||||
|
|
||||||
|
editor.updateShapes([
|
||||||
|
{
|
||||||
|
id: boxId,
|
||||||
|
type: 'geo',
|
||||||
|
x: -500,
|
||||||
|
y: -500,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
editor.mark()
|
||||||
|
editor.updateShapes([
|
||||||
|
{
|
||||||
|
id: boxId,
|
||||||
|
type: 'geo',
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
editor.undo()
|
||||||
|
|
||||||
|
expect(editor.getCamera()).not.toEqual(cameraBefore)
|
||||||
|
expect(editor.getCamera()).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"id": "camera:page:page",
|
||||||
|
"meta": Object {},
|
||||||
|
"typeName": "camera",
|
||||||
|
"x": 950,
|
||||||
|
"y": 950,
|
||||||
|
"z": 1,
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
editor.redo()
|
||||||
|
|
||||||
|
expect(editor.getCamera()).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"id": "camera:page:page",
|
||||||
|
"meta": Object {},
|
||||||
|
"typeName": "camera",
|
||||||
|
"x": -50,
|
||||||
|
"y": -50,
|
||||||
|
"z": 1,
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue