This PR is intended to do some housecleaning ahead of our developer
release.

It:
- co-locates code in the `Editor` class, i.e. moving shape-related
methods next to other shape-related methods
- renames `cullingBounds` and other culling-related names to
`renderingBounds`
- renames `Editor.getParentPageId` to `Editor.getAncestorPageId`
- renames `Editor.shapeIds` to `Editor.currentPageShapeIds`

### Change Type

- [x] `major` — api changes
This commit is contained in:
Steve Ruiz 2023-06-16 12:27:47 +01:00 committed by GitHub
parent b88a2370b3
commit 271d0088e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 2165 additions and 2108 deletions

View file

@ -404,14 +404,9 @@ export class Editor extends EventEmitter<TLEventMap> {
createPage(title: string, id?: TLPageId, belowPageIndex?: string): this;
createShapes<T extends TLUnknownShape>(partials: TLShapePartial<T>[], select?: boolean): this;
get croppingId(): null | TLShapeId;
get cullingBounds(): Box2d;
// @internal (undocumented)
readonly _cullingBounds: Atom<Box2d, unknown>;
get cullingBoundsExpanded(): Box2d;
// @internal (undocumented)
readonly _cullingBoundsExpanded: Atom<Box2d, unknown>;
get currentPage(): TLPage;
get currentPageId(): TLPageId;
get currentPageShapeIds(): Set<TLShapeId>;
get currentToolId(): string;
get cursor(): TLCursor;
deleteAssets(ids: TLAssetId[]): this;
@ -440,6 +435,7 @@ export class Editor extends EventEmitter<TLEventMap> {
focus(): this;
get focusLayerId(): TLPageId | TLShapeId;
get focusLayerShape(): TLShape | undefined;
getAncestorPageId(shape?: TLShape): TLPageId | undefined;
getAncestors(shape: TLShape, acc?: TLShape[]): TLShape[];
getAncestorsById(id: TLShapeId, acc?: TLShape[]): TLShape[];
getArrowsBoundTo(shapeId: TLShapeId): {
@ -477,7 +473,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getPageTransform(shape: TLShape): Matrix2d | undefined;
getPageTransformById(id: TLShapeId): Matrix2d | undefined;
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
getParentPageId(shape?: TLShape): TLPageId | undefined;
getParentShape(shape?: TLShape): TLShape | undefined;
getParentsMappedToChildren(ids: TLShapeId[]): Map<TLParentId, Set<TLShape>>;
getParentTransform(shape: TLShape): Matrix2d;
@ -588,6 +583,12 @@ export class Editor extends EventEmitter<TLEventMap> {
putExternalContent(info: TLExternalContent): Promise<void>;
redo(): this;
renamePage(id: TLPageId, name: string, squashing?: boolean): this;
get renderingBounds(): Box2d;
// @internal (undocumented)
readonly _renderingBounds: Atom<Box2d, unknown>;
get renderingBoundsExpanded(): Box2d;
// @internal (undocumented)
readonly _renderingBoundsExpanded: Atom<Box2d, unknown>;
get renderingShapes(): {
id: TLShapeId;
index: number;
@ -658,7 +659,6 @@ export class Editor extends EventEmitter<TLEventMap> {
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
setToolLocked(isToolLocked: boolean): this;
setZoomBrush(zoomBrush?: Box2dModel | null): this;
get shapeIds(): Set<TLShapeId>;
get shapesArray(): TLShape[];
shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
@ -684,11 +684,11 @@ export class Editor extends EventEmitter<TLEventMap> {
undo(): HistoryManager<this>;
ungroupShapes(ids?: TLShapeId[]): this;
updateAssets(assets: TLAssetPartial[]): this;
// @internal
updateCullingBounds(): this;
updateDocumentSettings(settings: Partial<TLDocument>): void;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, ephemeral?: boolean, squashing?: boolean): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
// @internal
updateRenderingBounds(): this;
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], squashing?: boolean): this;
updateViewportScreenBounds(center?: boolean): this;
readonly user: UserPreferencesManager;

View file

@ -107,3 +107,6 @@ export const COLLABORATOR_CHECK_INTERVAL = 1200
export const INTERNAL_POINTER_IDS = {
CAMERA_MOVE: -10,
} as const
/** @internal */
export const CAMERA_MOVING_TIMEOUT = 64

File diff suppressed because it is too large Load diff

View file

@ -19,30 +19,30 @@ const ids = {
describe('shapeIdsInCurrentPage', () => {
it('keeps the shape ids in the current page', () => {
expect(new Set(editor.shapeIds)).toEqual(new Set([]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([]))
editor.createShapes([{ type: 'geo', id: ids.box1 }])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box1]))
editor.createShapes([{ type: 'geo', id: ids.box2 }])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box1, ids.box2]))
editor.createShapes([{ type: 'geo', id: ids.box3 }])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
editor.deleteShapes([ids.box2])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box3]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box1, ids.box3]))
editor.deleteShapes([ids.box1])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box3]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box3]))
editor.deleteShapes([ids.box3])
expect(new Set(editor.shapeIds)).toEqual(new Set([]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([]))
})
it('changes when the current page changes', () => {
@ -60,10 +60,10 @@ describe('shapeIdsInCurrentPage', () => {
{ type: 'geo', id: ids.box6 },
])
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box4, ids.box5, ids.box6]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box4, ids.box5, ids.box6]))
editor.setCurrentPageId(editor.pages[0].id)
expect(new Set(editor.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
expect(new Set(editor.currentPageShapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3]))
})
})

View file

@ -33,7 +33,7 @@ const isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolea
* @param store - The tldraw store.
* @param getCurrentPageId - A function that returns the current page id.
*/
export const shapeIdsInCurrentPage = (store: TLStore, getCurrentPageId: () => TLPageId) => {
export const deriveShapeIdsInCurrentPage = (store: TLStore, getCurrentPageId: () => TLPageId) => {
const shapesIndex = store.query.ids('shape')
let lastPageId: null | TLPageId = null
function fromScratch() {

View file

@ -15,7 +15,7 @@ export class CameraManager {
if (this.timeoutRemaining <= 0) {
this.state.set('idle')
this.editor.off('tick', this.decay)
this.editor.updateCullingBounds()
this.editor.updateRenderingBounds()
}
}

View file

@ -248,7 +248,7 @@ export class SnapManager {
// TODO: make this an incremental derivation
@computed get snappableShapes(): GapNode[] {
const { editor } = this
const { selectedIds, cullingBounds } = editor
const { selectedIds, renderingBounds: renderingBounds } = editor
const snappableShapes: GapNode[] = []
@ -264,7 +264,7 @@ export class SnapManager {
if (!util.canSnap(childShape)) continue
// Only consider shapes if they're inside of the viewport page bounds
const pageBounds = editor.getPageBoundsById(childId)
if (!(pageBounds && cullingBounds.includes(pageBounds))) continue
if (!(pageBounds && renderingBounds.includes(pageBounds))) continue
// Snap to children of groups but not group itself
if (editor.isShapeOfType(childShape, GroupShapeUtil)) {
collectSnappableShapesFromParent(childId)

View file

@ -196,7 +196,7 @@ describe('Misc', () => {
editor.pointerMove(50, 50) // Move shape by 25, 25
editor.pointerUp().keyUp('Alt')
expect(Array.from(editor.shapeIds.values()).length).toEqual(2)
expect(Array.from(editor.currentPageShapeIds.values()).length).toEqual(2)
})
it('deletes', () => {
@ -208,7 +208,7 @@ describe('Misc', () => {
editor.pointerMove(50, 50) // Move shape by 25, 25
editor.pointerUp().keyUp('Alt')
let ids = Array.from(editor.shapeIds.values())
let ids = Array.from(editor.currentPageShapeIds.values())
expect(ids.length).toEqual(2)
const duplicate = ids.filter((i) => i !== id)[0]
@ -216,7 +216,7 @@ describe('Misc', () => {
editor.deleteShapes()
ids = Array.from(editor.shapeIds.values())
ids = Array.from(editor.currentPageShapeIds.values())
expect(ids.length).toEqual(1)
expect(ids[0]).toEqual(id)
})

View file

@ -46,7 +46,7 @@ describe('TLSelectTool.Translating', () => {
expect(editor.shapesArray.length).toBe(1)
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
const t1 = [...editor.shapeIds.values()]
const t1 = [...editor.currentPageShapeIds.values()]
editor.keyDown('Alt')
expect(editor.shapesArray.length).toBe(2)
@ -66,7 +66,7 @@ describe('TLSelectTool.Translating', () => {
editor.expectShapeToMatch({ id: ids.box1, x: 150, y: 150 })
expect([...editor.shapeIds.values()]).toMatchObject(t1)
expect([...editor.currentPageShapeIds.values()]).toMatchObject(t1)
// todo: Should cloning again duplicate new shapes, or restore the last clone?
// editor.keyDown('Alt')

View file

@ -13,7 +13,7 @@ export function useZoomCss() {
const setScaleDebounced = debounce(setScale, 100)
const scheduler = new EffectScheduler('useZoomCss', () => {
const numShapes = editor.shapeIds.size
const numShapes = editor.currentPageShapeIds.size
if (numShapes < 300) {
setScale(editor.zoomLevel)
} else {

View file

@ -125,7 +125,7 @@ export class TestEditor extends Editor {
this.bounds.bottom = bounds.y + bounds.h
this.updateViewportScreenBounds(center)
this.updateCullingBounds()
this.updateRenderingBounds()
return this
}

View file

@ -117,13 +117,13 @@ it('Creates shapes at the correct index', () => {
})
it('Throws out all shapes if any shape is invalid', () => {
const n = editor.shapeIds.size
const n = editor.currentPageShapeIds.size
expect(() => {
editor.createShapes([{ id: ids.box1, type: 'geo' }])
}).not.toThrow()
expect(editor.shapeIds.size).toBe(n + 1)
expect(editor.currentPageShapeIds.size).toBe(n + 1)
console.error = jest.fn()
@ -136,5 +136,5 @@ it('Throws out all shapes if any shape is invalid', () => {
])
}).toThrow()
expect(editor.shapeIds.size).toBe(n + 1)
expect(editor.currentPageShapeIds.size).toBe(n + 1)
})

View file

@ -38,13 +38,13 @@ describe('Editor.moveShapesToPage', () => {
// box1 didn't get moved, still on page 1
expect(editor.getShapeById(ids.box1)!.parentId).toBe(ids.page1)
expect([...editor.shapeIds].sort()).toMatchObject([ids.box2, ids.ellipse1])
expect([...editor.currentPageShapeIds].sort()).toMatchObject([ids.box2, ids.ellipse1])
expect(editor.currentPageId).toBe(ids.page2)
editor.setCurrentPageId(ids.page1)
expect([...editor.shapeIds]).toEqual([ids.box1])
expect([...editor.currentPageShapeIds]).toEqual([ids.box1])
})
it('Moves children to page', () => {
@ -80,23 +80,23 @@ describe('Editor.moveShapesToPage', () => {
it('Restores on undo / redo', () => {
expect(editor.currentPageId).toBe(ids.page1)
expect([...editor.shapeIds].sort()).toMatchObject([ids.box1, ids.box2, ids.ellipse1])
expect([...editor.currentPageShapeIds].sort()).toMatchObject([ids.box1, ids.box2, ids.ellipse1])
editor.mark('move shapes to page')
editor.moveShapesToPage([ids.box2], ids.page2)
expect(editor.currentPageId).toBe(ids.page2)
expect([...editor.shapeIds].sort()).toMatchObject([ids.box2])
expect([...editor.currentPageShapeIds].sort()).toMatchObject([ids.box2])
editor.undo()
expect(editor.currentPageId).toBe(ids.page1)
expect([...editor.shapeIds].sort()).toMatchObject([ids.box1, ids.box2, ids.ellipse1])
expect([...editor.currentPageShapeIds].sort()).toMatchObject([ids.box1, ids.box2, ids.ellipse1])
editor.redo()
expect(editor.currentPageId).toBe(ids.page2)
expect([...editor.shapeIds].sort()).toMatchObject([ids.box2])
expect([...editor.currentPageShapeIds].sort()).toMatchObject([ids.box2])
})
it('Sets the correct indices', () => {

View file

@ -417,7 +417,7 @@ describe('When pasting into frames...', () => {
.bringToFront()
editor.setCamera(-2000, -2000, 1)
editor.updateCullingBounds()
editor.updateRenderingBounds()
// Copy box 1 (should be out of viewport)
editor.select(ids.box1).copy()

View file

@ -48,13 +48,15 @@ function createShapes() {
])
}
it('updates the culling viewport', () => {
it('updates the rendering viewport when the camera stops moving', () => {
const ids = createShapes()
editor.updateCullingBounds = jest.fn(editor.updateCullingBounds)
editor.updateRenderingBounds = jest.fn(editor.updateRenderingBounds)
editor.pan(-201, -201)
jest.advanceTimersByTime(500)
expect(editor.updateCullingBounds).toHaveBeenCalledTimes(1)
expect(editor.cullingBounds).toMatchObject({ x: 201, y: 201, w: 1800, h: 900 })
expect(editor.updateRenderingBounds).toHaveBeenCalledTimes(1)
expect(editor.renderingBounds).toMatchObject({ x: 201, y: 201, w: 1800, h: 900 })
expect(editor.getPageBoundsById(ids.A)).toMatchObject({ x: 100, y: 100, w: 100, h: 100 })
})
@ -63,7 +65,7 @@ it('lists shapes in viewport', () => {
expect(
editor.renderingShapes.map(({ id, isCulled, isInViewport }) => [id, isCulled, isInViewport])
).toStrictEqual([
[ids.A, false, true], // A is within the expanded culling bounds, so should not be culled; and it's in the regular viewport too, so it's on screen.
[ids.A, false, true], // A is within the expanded rendering bounds, so should not be culled; and it's in the regular viewport too, so it's on screen.
[ids.B, false, true],
[ids.C, false, true],
[ids.D, true, false], // D is clipped and so should always be culled / outside of viewport

View file

@ -211,15 +211,15 @@ describe('When cloning...', () => {
])
})
it('clones a single shape and restores when stopping cloning', () => {
expect(editor.shapeIds.size).toBe(3)
expect(editor.shapeIds.size).toBe(3)
expect(editor.currentPageShapeIds.size).toBe(3)
expect(editor.currentPageShapeIds.size).toBe(3)
editor.select(ids.box1).pointerDown(50, 50, ids.box1).pointerMove(50, 40) // [0, -10]
expect(editor.shapeIds.size).toBe(3)
expect(editor.currentPageShapeIds.size).toBe(3)
editor.expectShapeToMatch({ id: ids.box1, x: 10, y: 0 }) // Translated A...
// Start cloning!
editor.keyDown('Alt')
expect(editor.shapeIds.size).toBe(4)
expect(editor.currentPageShapeIds.size).toBe(4)
const newShape = editor.selectedShapes[0]
expect(newShape.id).not.toBe(ids.box1)
@ -240,13 +240,13 @@ describe('When cloning...', () => {
it('clones multiple single shape and restores when stopping cloning', () => {
editor.select(ids.box1, ids.box2).pointerDown(50, 50, ids.box1).pointerMove(50, 40) // [0, -10]
expect(editor.shapeIds.size).toBe(3)
expect(editor.currentPageShapeIds.size).toBe(3)
editor.expectShapeToMatch({ id: ids.box1, x: 10, y: 0 }) // Translated A...
editor.expectShapeToMatch({ id: ids.box2, x: 200, y: 190 }) // Translated B...
// Start cloning!
editor.keyDown('Alt')
expect(editor.shapeIds.size).toBe(5) // Two new shapes!
expect(editor.currentPageShapeIds.size).toBe(5) // Two new shapes!
const newShapeA = editor.getShapeById(editor.selectedIds[0])!
const newShapeB = editor.getShapeById(editor.selectedIds[1])!
expect(newShapeA).toBeDefined()
@ -280,9 +280,9 @@ describe('When cloning...', () => {
expect(editor.getShapeById(ids.line1)!.parentId).toBe(ids.box2)
editor.select(ids.box2).pointerDown(250, 250, ids.box2).pointerMove(250, 240) // [0, -10]
expect(editor.shapeIds.size).toBe(3)
expect(editor.currentPageShapeIds.size).toBe(3)
editor.keyDown('Alt', { altKey: true })
expect(editor.shapeIds.size).toBe(5) // Creates a clone of B and C (its descendant)
expect(editor.currentPageShapeIds.size).toBe(5) // Creates a clone of B and C (its descendant)
const newShapeA = editor.getShapeById(editor.selectedIds[0])!
const newShapeB = editor.getShapeById(editor.getSortedChildIds(newShapeA.id)[0])!

View file

@ -55,7 +55,7 @@ export const Minimap = track(function Minimap({
const onDoubleClick = React.useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
if (!editor.shapeIds.size) return
if (!editor.currentPageShapeIds.size) return
const { x, y } = minimap.minimapScreenPointToPagePoint(e.clientX, e.clientY, false, false)
@ -72,7 +72,7 @@ export const Minimap = track(function Minimap({
const onPointerDown = React.useCallback(
(e: React.PointerEvent<HTMLCanvasElement>) => {
e.currentTarget.setPointerCapture(e.pointerId)
if (!editor.shapeIds.size) return
if (!editor.currentPageShapeIds.size) return
rPointing.current = true
@ -197,7 +197,7 @@ export const Minimap = track(function Minimap({
const allShapeBounds = [] as (Box2d & { id: TLShapeId })[]
editor.shapeIds.forEach((id) => {
editor.currentPageShapeIds.forEach((id) => {
let pageBounds = editor.getPageBoundsById(id)! as Box2d & { id: TLShapeId }
const pageMask = editor.getPageMaskById(id)

View file

@ -13,7 +13,7 @@ export const ZoomMenu = track(function ZoomMenu() {
const breakpoint = useBreakpoint()
const zoom = editor.zoomLevel
const hasShapes = editor.shapeIds.size > 0
const hasShapes = editor.currentPageShapeIds.size > 0
const hasSelected = editor.selectedIds.length > 0
const isZoomedTo100 = editor.zoomLevel === 1

View file

@ -92,7 +92,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem
const threeStackableItems = useThreeStackableItems()
const atLeastOneShapeOnPage = useValue(
'atLeastOneShapeOnPage',
() => editor.shapeIds.size > 0,
() => editor.currentPageShapeIds.size > 0,
[]
)
const isTransparentBg = useValue(

View file

@ -26,7 +26,7 @@ export function useCopyAs() {
// little awkward.
function copyAs(ids: TLShapeId[] = editor.selectedIds, format: TLCopyType = 'svg') {
if (ids.length === 0) {
ids = [...editor.shapeIds]
ids = [...editor.currentPageShapeIds]
}
if (ids.length === 0) {

View file

@ -20,7 +20,7 @@ export function useExportAs() {
return useCallback(
async function exportAs(ids: TLShapeId[] = editor.selectedIds, format: TLExportType = 'png') {
if (ids.length === 0) {
ids = [...editor.shapeIds]
ids = [...editor.currentPageShapeIds]
}
if (ids.length === 0) {

View file

@ -62,7 +62,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
[editor]
)
const emptyPage = useValue('emptyPage', () => editor.shapeIds.size === 0, [editor])
const emptyPage = useValue('emptyPage', () => editor.currentPageShapeIds.size === 0, [editor])
const selectedCount = useValue('selectedCount', () => editor.selectedIds.length, [editor])
const noneSelected = selectedCount === 0