From 3db207bca3bce8d3db96316ebf80ae1bc4578b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 10:38:04 +0100 Subject: [PATCH 01/82] Initial implementation. --- packages/editor/api-report.md | 2 + packages/editor/api/api.json | 41 +- packages/editor/src/lib/editor/Editor.ts | 776 ++++++++++++----------- 3 files changed, 452 insertions(+), 367 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 4317f6d42..6fa9770c6 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -669,6 +669,8 @@ export class Editor extends EventEmitter { getCameraState(): "idle" | "moving"; getCanRedo(): boolean; getCanUndo(): boolean; + // (undocumented) + getCoalescedEvents: () => TLEventInfo[]; getContainer: () => HTMLElement; getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; // @internal diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index bda751816..6c451d030 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -9026,7 +9026,7 @@ { "kind": "Property", "canonicalReference": "@tldraw/editor!Editor#dispatch:member", - "docComment": "/**\n * Dispatch an event to the editor.\n *\n * @param info - The event info.\n *\n * @example\n * ```ts\n * editor.dispatch(myPointerEvent)\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Dispatch an event to the editor.\n *\n * @param info - The event info. h*\n *\n * @example\n * ```ts\n * editor.dispatch(myPointerEvent)\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10146,6 +10146,45 @@ "isAbstract": false, "name": "getCanUndo" }, + { + "kind": "Property", + "canonicalReference": "@tldraw/editor!Editor#getCoalescedEvents:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getCoalescedEvents: " + }, + { + "kind": "Content", + "text": "() => " + }, + { + "kind": "Reference", + "text": "TLEventInfo", + "canonicalReference": "@tldraw/editor!TLEventInfo:type" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "getCoalescedEvents", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "@tldraw/editor!Editor#getContainer:member", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 6b2be38b6..2a765181b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -637,7 +637,7 @@ export class Editor extends EventEmitter { this.updateRenderingBounds() - this.on('tick', this.tick) + this.on('tick', this._flushEventsForTick) requestAnimationFrame(() => { this._tickManager.start() @@ -2085,7 +2085,7 @@ export class Editor extends EventEmitter { } /** @internal */ - private _setCamera(point: VecLike): this { + private _setCamera(point: VecLike, immediate = false): this { const currentCamera = this.getCamera() if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) { @@ -2100,7 +2100,7 @@ export class Editor extends EventEmitter { const { currentScreenPoint } = this.inputs const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! - this.dispatch({ + const event: TLPointerEventInfo = { type: 'pointer', target: 'canvas', name: 'pointer_move', @@ -2112,7 +2112,12 @@ export class Editor extends EventEmitter { shiftKey: this.inputs.shiftKey, button: 0, isPen: this.getInstanceState().isPenMode ?? false, - }) + } + if (immediate) { + this._flushEventForTick(event) + } else { + this.dispatch(event) + } this._tickCameraState() }) @@ -8404,12 +8409,6 @@ export class Editor extends EventEmitter { ]) } - /** @internal */ - private tick = (elapsed = 0) => { - this.dispatch({ type: 'misc', name: 'tick', elapsed }) - this.scribbles.tick(elapsed) - } - /** * Dispatch a cancel event. * @@ -8553,10 +8552,57 @@ export class Editor extends EventEmitter { * ``` * * @param info - The event info. - * + h* * @public */ dispatch = (info: TLEventInfo): this => { + console.log('adding to pending events', info.name) + // We already have an event in the queue. If the current event is the same as the last one + // we will coalesce them. If it's different, we will flush the queue and start a new one. + this._allEventsSinceLastTick.push(info) + if (this._pendingEventsForNextTick.length === 1) { + const eventInQueue = this._pendingEventsForNextTick[0] + if (eventInQueue.name === info.name) { + console.log('coalescing') + this._pendingEventsForNextTick[0] = info + } else { + console.log('different event type') + // Event has changed. We flush the currently pending events + this._flushEventsForTick(0) + // Then we add the new event to the queue + this._pendingEventsForNextTick.push(info) + } + } else { + console.log('adding the first event') + this._pendingEventsForNextTick.push(info) + } + return this + } + + private _pendingEventsForNextTick: TLEventInfo[] = [] + private _allEventsSinceLastTick: TLEventInfo[] = [] + + getCoalescedEvents = () => { + return this._allEventsSinceLastTick + } + + private _flushEventsForTick = (elapsed: number) => { + this.batch(() => { + if (this._pendingEventsForNextTick.length > 0) { + console.log('flushing', this._pendingEventsForNextTick.length, this._allEventsSinceLastTick) + const events = [...this._pendingEventsForNextTick] + this._pendingEventsForNextTick.length = 0 + for (const info of events) { + this._flushEventForTick(info) + } + } + this.root.handleEvent({ type: 'misc', name: 'tick', elapsed }) + this.scribbles.tick(elapsed) + }) + this._allEventsSinceLastTick.length = 0 + } + + private _flushEventForTick = (info: TLEventInfo) => { // prevent us from spamming similar event errors if we're crashed. // todo: replace with new readonly mode? if (this.getCrashingError()) return this @@ -8564,161 +8610,250 @@ export class Editor extends EventEmitter { const { inputs } = this const { type } = info - this.batch(() => { - if (info.type === 'misc') { - // stop panning if the interaction is cancelled or completed - if (info.name === 'cancel' || info.name === 'complete') { - this.inputs.isDragging = false + if (info.type === 'misc') { + // stop panning if the interaction is cancelled or completed + if (info.name === 'cancel' || info.name === 'complete') { + this.inputs.isDragging = false - if (this.inputs.isPanning) { - this.inputs.isPanning = false - this.updateInstanceState({ - cursor: { - type: this._prevCursor, - rotation: 0, - }, + if (this.inputs.isPanning) { + this.inputs.isPanning = false + this.updateInstanceState({ + cursor: { + type: this._prevCursor, + rotation: 0, + }, + }) + } + } + + this.root.handleEvent(info) + return + } + + if (info.shiftKey) { + clearInterval(this._shiftKeyTimeout) + this._shiftKeyTimeout = -1 + inputs.shiftKey = true + } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) { + this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150) + } + + if (info.altKey) { + clearInterval(this._altKeyTimeout) + this._altKeyTimeout = -1 + inputs.altKey = true + } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) { + this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150) + } + + if (info.ctrlKey) { + clearInterval(this._ctrlKeyTimeout) + this._ctrlKeyTimeout = -1 + inputs.ctrlKey = true /** @internal */ /** @internal */ /** @internal */ + } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) { + this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150) + } + + const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs + + if (!inputs.isPointing) { + inputs.isDragging = false + } + + switch (type) { + case 'pinch': { + if (!this.getInstanceState().canMoveCamera) return + this._updateInputsFromEvent(info) + + switch (info.name) { + case 'pinch_start': { + if (inputs.isPinching) return + + if (!inputs.isEditing) { + this._pinchStart = this.getCamera().z + if (!this._selectedShapeIdsAtPointerDown.length) { + this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() + } + + this._didPinch = true + + inputs.isPinching = true + + this.interrupt() + } + + return // Stop here! + } + case 'pinch': { + if (!inputs.isPinching) return + + const { + point: { z = 1 }, + delta: { x: dx, y: dy }, + } = info + + const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + const { x, y } = 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)) + + this.setCamera({ + x: cx + dx / cz - x / cz + x / zoom, + y: cy + dy / cz - y / cz + y / zoom, + z: zoom, }) + + return // Stop here! } - } + case 'pinch_end': { + if (!inputs.isPinching) return this - this.root.handleEvent(info) - return - } + inputs.isPinching = false + const { _selectedShapeIdsAtPointerDown } = this + this.setSelectedShapes(this._selectedShapeIdsAtPointerDown, { squashing: true }) + this._selectedShapeIdsAtPointerDown = [] - if (info.shiftKey) { - clearInterval(this._shiftKeyTimeout) - this._shiftKeyTimeout = -1 - inputs.shiftKey = true - } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) { - this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150) - } - - if (info.altKey) { - clearInterval(this._altKeyTimeout) - this._altKeyTimeout = -1 - inputs.altKey = true - } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) { - this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150) - } - - if (info.ctrlKey) { - clearInterval(this._ctrlKeyTimeout) - this._ctrlKeyTimeout = -1 - inputs.ctrlKey = true /** @internal */ /** @internal */ /** @internal */ - } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) { - this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150) - } - - const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs - - if (!inputs.isPointing) { - inputs.isDragging = false - } - - switch (type) { - case 'pinch': { - if (!this.getInstanceState().canMoveCamera) return - this._updateInputsFromEvent(info) - - switch (info.name) { - case 'pinch_start': { - if (inputs.isPinching) return - - if (!inputs.isEditing) { - this._pinchStart = this.getCamera().z - if (!this._selectedShapeIdsAtPointerDown.length) { - this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() + if (this._didPinch) { + this._didPinch = false + requestAnimationFrame(() => { + if (!this._didPinch) { + this.setSelectedShapes(_selectedShapeIdsAtPointerDown, { squashing: true }) } - - this._didPinch = true - - inputs.isPinching = true - - this.interrupt() - } - - return // Stop here! - } - case 'pinch': { - if (!inputs.isPinching) return - - const { - point: { z = 1 }, - delta: { x: dx, y: dy }, - } = info - - const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! - const { x, y } = 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)) - - this.setCamera({ - x: cx + dx / cz - x / cz + x / zoom, - y: cy + dy / cz - y / cz + y / zoom, - z: zoom, }) - - return // Stop here! } - case 'pinch_end': { - if (!inputs.isPinching) return this - inputs.isPinching = false - const { _selectedShapeIdsAtPointerDown } = this - this.setSelectedShapes(this._selectedShapeIdsAtPointerDown, { squashing: true }) - this._selectedShapeIdsAtPointerDown = [] - - if (this._didPinch) { - this._didPinch = false - requestAnimationFrame(() => { - if (!this._didPinch) { - this.setSelectedShapes(_selectedShapeIdsAtPointerDown, { squashing: true }) - } - }) - } - - return // Stop here! - } + return // Stop here! } } - case 'wheel': { - if (!this.getInstanceState().canMoveCamera) return + } + case 'wheel': { + if (!this.getInstanceState().canMoveCamera) return - this._updateInputsFromEvent(info) + this._updateInputsFromEvent(info) - if (this.getIsMenuOpen()) { - // noop - } else { - if (inputs.ctrlKey) { - // todo: Start or update the zoom end interval + if (this.getIsMenuOpen()) { + // noop + } else { + if (inputs.ctrlKey) { + // todo: Start or update the zoom end interval - // If the alt or ctrl keys are pressed, - // zoom or pan the camera and then return. + // 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 + // Subtract the top left offset from the user's point - const { x, y } = this.inputs.currentScreenPoint + const { x, y } = this.inputs.currentScreenPoint - const { x: cx, y: cy, z: cz } = this.getCamera() + 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)) + const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz)) - this.setCamera({ - x: cx + (x / zoom - x) - (x / cz - x), - y: cy + (y / zoom - y) - (y / cz - y), - z: zoom, + this.setCamera({ + x: cx + (x / zoom - x) - (x / cz - x), + y: cy + (y / zoom - y) - (y / cz - y), + z: zoom, + }) + + // We want to return here because none of the states in our + // statechart should respond to this event (a camera zoom) + return + } + + // Update the camera here, which will dispatch a pointer move... + // this will also update the pointer position, etc + this.pan(info.delta) + + if ( + !inputs.isDragging && + inputs.isPointing && + originPagePoint.dist(currentPagePoint) > + (this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / + this.getZoomLevel() + ) { + inputs.isDragging = true + } + } + break + } + case 'pointer': { + // If we're pinching, return + if (inputs.isPinching) return + + this._updateInputsFromEvent(info) + + const { isPen } = info + + switch (info.name) { + case 'pointer_down': { + this.clearOpenMenus() + + this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() + + // Firefox bug fix... + // If it's a left-mouse-click, we store the pointer id for later user + if (info.button === 0) { + this.capturedPointerId = info.pointerId + } + + // Add the button from the buttons set + inputs.buttons.add(info.button) + + inputs.isPointing = true + inputs.isDragging = false + + if (this.getInstanceState().isPenMode) { + if (!isPen) { + return + } + } else { + if (isPen) { + this.updateInstanceState({ isPenMode: true }) + } + } + + if (info.button === 5) { + // Eraser button activates eraser + this._restoreToolId = this.getCurrentToolId() + this.complete() + this.setCurrentTool('eraser') + } else if (info.button === 1) { + // Middle mouse pan activates panning + if (!this.inputs.isPanning) { + this._prevCursor = this.getInstanceState().cursor.type + } + + this.inputs.isPanning = true + } + + if (this.inputs.isPanning) { + this.stopCameraAnimation() + this.updateInstanceState({ + cursor: { + type: 'grabbing', + rotation: 0, + }, }) + return this + } - // We want to return here because none of the states in our - // statechart should respond to this event (a camera zoom) + originScreenPoint.setTo(currentScreenPoint) + originPagePoint.setTo(currentPagePoint) + break + } + case 'pointer_move': { + // If the user is in pen mode, but the pointer is not a pen, stop here. + if (!isPen && this.getInstanceState().isPenMode) { return } - // Update the camera here, which will dispatch a pointer move... - // this will also update the pointer position, etc - this.pan(info.delta) + if (this.inputs.isPanning && this.inputs.isPointing) { + // Handle panning + const { currentScreenPoint, previousScreenPoint } = this.inputs + this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint)) + return + } if ( !inputs.isDragging && @@ -8729,149 +8864,46 @@ export class Editor extends EventEmitter { ) { inputs.isDragging = true } + break } - break - } - case 'pointer': { - // If we're pinching, return - if (inputs.isPinching) return + case 'pointer_up': { + // Remove the button from the buttons set + inputs.buttons.delete(info.button) - this._updateInputsFromEvent(info) + inputs.isPointing = false + inputs.isDragging = false - const { isPen } = info - - switch (info.name) { - case 'pointer_down': { - this.clearOpenMenus() - - this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() - - // Firefox bug fix... - // If it's a left-mouse-click, we store the pointer id for later user - if (info.button === 0) { - this.capturedPointerId = info.pointerId - } - - // Add the button from the buttons set - inputs.buttons.add(info.button) - - inputs.isPointing = true - inputs.isDragging = false - - if (this.getInstanceState().isPenMode) { - if (!isPen) { - return - } - } else { - if (isPen) { - this.updateInstanceState({ isPenMode: true }) - } - } - - if (info.button === 5) { - // Eraser button activates eraser - this._restoreToolId = this.getCurrentToolId() - this.complete() - this.setCurrentTool('eraser') - } else if (info.button === 1) { - // Middle mouse pan activates panning - if (!this.inputs.isPanning) { - this._prevCursor = this.getInstanceState().cursor.type - } - - this.inputs.isPanning = true - } - - if (this.inputs.isPanning) { - this.stopCameraAnimation() - this.updateInstanceState({ - cursor: { - type: 'grabbing', - rotation: 0, - }, - }) - return this - } - - originScreenPoint.setTo(currentScreenPoint) - originPagePoint.setTo(currentPagePoint) - break + if (this.getIsMenuOpen()) { + // Suppressing pointerup here as doesn't seem to do what we what here. + return } - case 'pointer_move': { - // If the user is in pen mode, but the pointer is not a pen, stop here. - if (!isPen && this.getInstanceState().isPenMode) { - return - } - if (this.inputs.isPanning && this.inputs.isPointing) { - // Handle panning - const { currentScreenPoint, previousScreenPoint } = this.inputs - this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint)) - return - } - - if ( - !inputs.isDragging && - inputs.isPointing && - originPagePoint.dist(currentPagePoint) > - (this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / - this.getZoomLevel() - ) { - inputs.isDragging = true - } - break + if (!isPen && this.getInstanceState().isPenMode) { + return } - case 'pointer_up': { - // Remove the button from the buttons set - inputs.buttons.delete(info.button) - inputs.isPointing = false - inputs.isDragging = false + // Firefox bug fix... + // If it's the same pointer that we stored earlier... + // ... then it's probably still a left-mouse-click! + if (this.capturedPointerId === info.pointerId) { + this.capturedPointerId = null + info.button = 0 + } - if (this.getIsMenuOpen()) { - // Suppressing pointerup here as doesn't seem to do what we what here. - return - } + if (inputs.isPanning) { + if (info.button === 1) { + if (!this.inputs.keys.has(' ')) { + inputs.isPanning = false - if (!isPen && this.getInstanceState().isPenMode) { - return - } - - // Firefox bug fix... - // If it's the same pointer that we stored earlier... - // ... then it's probably still a left-mouse-click! - if (this.capturedPointerId === info.pointerId) { - this.capturedPointerId = null - info.button = 0 - } - - if (inputs.isPanning) { - if (info.button === 1) { - if (!this.inputs.keys.has(' ')) { - inputs.isPanning = false - - this.slideCamera({ - speed: Math.min(2, this.inputs.pointerVelocity.len()), - direction: this.inputs.pointerVelocity, - friction: CAMERA_SLIDE_FRICTION, - }) - this.updateInstanceState({ - cursor: { type: this._prevCursor, rotation: 0 }, - }) - } else { - this.slideCamera({ - speed: Math.min(2, this.inputs.pointerVelocity.len()), - direction: this.inputs.pointerVelocity, - friction: CAMERA_SLIDE_FRICTION, - }) - this.updateInstanceState({ - cursor: { - type: 'grab', - rotation: 0, - }, - }) - } - } else if (info.button === 0) { + this.slideCamera({ + speed: Math.min(2, this.inputs.pointerVelocity.len()), + direction: this.inputs.pointerVelocity, + friction: CAMERA_SLIDE_FRICTION, + }) + this.updateInstanceState({ + cursor: { type: this._prevCursor, rotation: 0 }, + }) + } else { this.slideCamera({ speed: Math.min(2, this.inputs.pointerVelocity.len()), direction: this.inputs.pointerVelocity, @@ -8884,115 +8916,127 @@ export class Editor extends EventEmitter { }, }) } - } else { - if (info.button === 5) { - // Eraser button activates eraser - this.complete() - this.setCurrentTool(this._restoreToolId) - } - } - - break - } - } - - break - } - case 'keyboard': { - // please, please - if (info.key === 'ShiftRight') info.key = 'ShiftLeft' - if (info.key === 'AltRight') info.key = 'AltLeft' - if (info.code === 'ControlRight') info.code = 'ControlLeft' - - switch (info.name) { - case 'key_down': { - // Add the key from the keys set - inputs.keys.add(info.code) - - // If the space key is pressed (but meta / control isn't!) activate panning - if (!info.ctrlKey && info.code === 'Space') { - if (!this.inputs.isPanning) { - this._prevCursor = this.getInstanceState().cursor.type - } - - this.inputs.isPanning = true + } else if (info.button === 0) { + this.slideCamera({ + speed: Math.min(2, this.inputs.pointerVelocity.len()), + direction: this.inputs.pointerVelocity, + friction: CAMERA_SLIDE_FRICTION, + }) this.updateInstanceState({ - cursor: { type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 }, + cursor: { + type: 'grab', + rotation: 0, + }, }) } - - break + } else { + if (info.button === 5) { + // Eraser button activates eraser + this.complete() + this.setCurrentTool(this._restoreToolId) + } } - case 'key_up': { - // Remove the key from the keys set - inputs.keys.delete(info.code) - if (info.code === 'Space' && !this.inputs.buttons.has(1)) { - this.inputs.isPanning = false - this.updateInstanceState({ - cursor: { type: this._prevCursor, rotation: 0 }, - }) + break + } + } + + break + } + case 'keyboard': { + // please, please + if (info.key === 'ShiftRight') info.key = 'ShiftLeft' + if (info.key === 'AltRight') info.key = 'AltLeft' + if (info.code === 'ControlRight') info.code = 'ControlLeft' + + switch (info.name) { + case 'key_down': { + // Add the key from the keys set + inputs.keys.add(info.code) + + // If the space key is pressed (but meta / control isn't!) activate panning + if (!info.ctrlKey && info.code === 'Space') { + if (!this.inputs.isPanning) { + this._prevCursor = this.getInstanceState().cursor.type } - break - } - case 'key_repeat': { - // noop - break + this.inputs.isPanning = true + this.updateInstanceState({ + cursor: { type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 }, + }) } + + break + } + case 'key_up': { + // Remove the key from the keys set + inputs.keys.delete(info.code) + + if (info.code === 'Space' && !this.inputs.buttons.has(1)) { + this.inputs.isPanning = false + this.updateInstanceState({ + cursor: { type: this._prevCursor, rotation: 0 }, + }) + } + + break + } + case 'key_repeat': { + // noop + break } - break } + break + } + } + + // Correct the info name for right / middle clicks + if (info.type === 'pointer') { + if (info.button === 1) { + info.name = 'middle_click' + } else if (info.button === 2) { + info.name = 'right_click' } - // Correct the info name for right / middle clicks - if (info.type === 'pointer') { - if (info.button === 1) { - info.name = 'middle_click' - } else if (info.button === 2) { - info.name = 'right_click' - } - - // If a pointer event, send the event to the click manager. - if (info.isPen === this.getInstanceState().isPenMode) { - switch (info.name) { - case 'pointer_down': { - const otherEvent = this._clickManager.transformPointerDownEvent(info) - if (info.name !== otherEvent.name) { - this.root.handleEvent(info) - this.emit('event', info) - this.root.handleEvent(otherEvent) - this.emit('event', otherEvent) - return - } - - break + // If a pointer event, send the event to the click manager. + if (info.isPen === this.getInstanceState().isPenMode) { + switch (info.name) { + case 'pointer_down': { + const otherEvent = this._clickManager.transformPointerDownEvent(info) + if (info.name !== otherEvent.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(otherEvent) + this.emit('event', otherEvent) + return } - case 'pointer_up': { - const otherEvent = this._clickManager.transformPointerUpEvent(info) - if (info.name !== otherEvent.name) { - this.root.handleEvent(info) - this.emit('event', info) - this.root.handleEvent(otherEvent) - this.emit('event', otherEvent) - return - } - break - } - case 'pointer_move': { - this._clickManager.handleMove() - break + break + } + case 'pointer_up': { + const otherEvent = this._clickManager.transformPointerUpEvent(info) + if (info.name !== otherEvent.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(otherEvent) + this.emit('event', otherEvent) + return } + + break + } + case 'pointer_move': { + this._clickManager.handleMove() + break } } } + } - // Send the event to the statechart. It will be handled by all - // active states, starting at the root. - this.root.handleEvent(info) - this.emit('event', info) - }) + // Send the event to the statechart. It will be handled by all + // active states, starting at the root. + this.root.handleEvent(info) + this.emit('event', info) return this } From 832770001474d041350634d0f5e125f6afa14f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 11:24:06 +0100 Subject: [PATCH 02/82] Initial prototype of using coalesced events for drawing. --- packages/editor/src/lib/editor/Editor.ts | 31 +++++++++++-------- packages/tldraw/api-report.md | 9 +++--- packages/tldraw/api/api.json | 24 +++++++------- .../src/lib/shapes/draw/toolStates/Drawing.ts | 26 +++++++++++----- 4 files changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 2a765181b..50cfa0280 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8556,25 +8556,28 @@ export class Editor extends EventEmitter { * @public */ dispatch = (info: TLEventInfo): this => { - console.log('adding to pending events', info.name) - // We already have an event in the queue. If the current event is the same as the last one - // we will coalesce them. If it's different, we will flush the queue and start a new one. - this._allEventsSinceLastTick.push(info) - if (this._pendingEventsForNextTick.length === 1) { + // console.log('adding to pending events', info.name) + if (info.name === 'pointer_move') { + this._updateInputsFromEvent(info) + ;(info as any).inputs = structuredClone(this.inputs) + } + if (this._pendingEventsForNextTick.length === 0) { + // console.log('adding the first event') + this._pendingEventsForNextTick.push(info) + this._allEventsSinceLastTick.push(info) + } else { const eventInQueue = this._pendingEventsForNextTick[0] if (eventInQueue.name === info.name) { - console.log('coalescing') + // console.log('coalescing') this._pendingEventsForNextTick[0] = info + this._allEventsSinceLastTick.push(info) } else { - console.log('different event type') + // console.log('different event type') // Event has changed. We flush the currently pending events this._flushEventsForTick(0) - // Then we add the new event to the queue - this._pendingEventsForNextTick.push(info) + // Then we add the new event to the queue (flushing clears the queue) + this._pendingEventsForNextTick = [info] } - } else { - console.log('adding the first event') - this._pendingEventsForNextTick.push(info) } return this } @@ -8587,9 +8590,10 @@ export class Editor extends EventEmitter { } private _flushEventsForTick = (elapsed: number) => { + // const now = Date.now() this.batch(() => { if (this._pendingEventsForNextTick.length > 0) { - console.log('flushing', this._pendingEventsForNextTick.length, this._allEventsSinceLastTick) + // console.log('flushing', this._pendingEventsForNextTick.length, this._allEventsSinceLastTick) const events = [...this._pendingEventsForNextTick] this._pendingEventsForNextTick.length = 0 for (const info of events) { @@ -8600,6 +8604,7 @@ export class Editor extends EventEmitter { this.scribbles.tick(elapsed) }) this._allEventsSinceLastTick.length = 0 + // console.log('flushed', Date.now() - now) } private _flushEventForTick = (info: TLEventInfo) => { diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 5b45dafe6..338a6b2f6 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -71,6 +71,7 @@ import { TLEditorComponents } from '@tldraw/editor'; import { TLEmbedShape } from '@tldraw/editor'; import { TLEnterEventHandler } from '@tldraw/editor'; import { TLEventHandlers } from '@tldraw/editor'; +import { TLEventInfo } from '@tldraw/editor'; import { TLExitEventHandler } from '@tldraw/editor'; import { TLFrameShape } from '@tldraw/editor'; import { TLGeoShape } from '@tldraw/editor'; @@ -403,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -449,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) @@ -675,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_3 | typeof Pointing_2)[]; + static children: () => (typeof Idle_2 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -870,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 48b0f4a74..24aa6c0af 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,15 +3799,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "DrawShapeTool", - "canonicalReference": "tldraw!DrawShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3826,6 +3817,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "HighlightShapeTool", + "canonicalReference": "tldraw!HighlightShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", diff --git a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts index 499c19713..83f712904 100644 --- a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts +++ b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts @@ -6,6 +6,7 @@ import { TLDrawShape, TLDrawShapeSegment, TLEventHandlers, + TLEventInfo, TLHighlightShape, TLPointerEventInfo, TLShapePartial, @@ -62,9 +63,18 @@ export class Drawing extends StateNode { } override onPointerMove: TLEventHandlers['onPointerMove'] = () => { - const { - editor: { inputs }, - } = this + // console.log('drawing: pointer move') + const coallesced = this.editor.getCoalescedEvents() + // console.log({ coallesced }) + console.log('number of events', coallesced.length) + coallesced.forEach((info, i) => { + // console.log(i, (info as any).inputs) + this.processEvents(info) + }) + } + + processEvents(info: TLEventInfo) { + const inputs = (info as any).inputs if (this.isPen !== inputs.isPen) { // The user made a palm gesture before starting a pen gesture; @@ -99,7 +109,7 @@ export class Drawing extends StateNode { this.mergeNextPoint = false } - this.updateShapes() + this.updateShapes(inputs) } } @@ -117,7 +127,7 @@ export class Drawing extends StateNode { } } } - this.updateShapes() + this.updateShapes(this.editor.inputs) } override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => { @@ -139,7 +149,7 @@ export class Drawing extends StateNode { } } - this.updateShapes() + this.updateShapes(this.editor.inputs) } override onExit? = () => { @@ -281,8 +291,8 @@ export class Drawing extends StateNode { this.initialShape = this.editor.getShape(id) } - private updateShapes() { - const { inputs } = this.editor + private updateShapes(inputs: any) { + // console.log('update shapes', inputs) const { initialShape } = this if (!initialShape) return From 920b6fc7ceea2d4975378e60ce9f0f8dc8d6e5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 12:07:22 +0100 Subject: [PATCH 03/82] Improve coalescing. --- packages/editor/src/lib/editor/Editor.ts | 25 ++++++++++++++++++------ packages/tldraw/api-report.md | 8 ++++---- packages/tldraw/api/api.json | 24 +++++++++++------------ 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 50cfa0280..dac17ae65 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8543,6 +8543,20 @@ export class Editor extends EventEmitter { /** @internal */ capturedPointerId: number | null = null + private _eventsToCoalesce = ['pointer_move'] + + private _shouldCoallesce = (info: TLEventInfo) => { + if (!this._isCoalesableEvent(info)) return false + return ( + this._isCoalesableEvent(this._pendingEventsForNextTick[0]) && + this._pendingEventsForNextTick[0].name === info.name + ) + } + + private _isCoalesableEvent = (info: TLEventInfo) => { + return this._eventsToCoalesce.includes(info.name) + } + /** * Dispatch an event to the editor. * @@ -8557,8 +8571,8 @@ export class Editor extends EventEmitter { */ dispatch = (info: TLEventInfo): this => { // console.log('adding to pending events', info.name) - if (info.name === 'pointer_move') { - this._updateInputsFromEvent(info) + if (this._isCoalesableEvent(info)) { + this._updateInputsFromEvent(info as TLPointerEventInfo) ;(info as any).inputs = structuredClone(this.inputs) } if (this._pendingEventsForNextTick.length === 0) { @@ -8566,8 +8580,7 @@ export class Editor extends EventEmitter { this._pendingEventsForNextTick.push(info) this._allEventsSinceLastTick.push(info) } else { - const eventInQueue = this._pendingEventsForNextTick[0] - if (eventInQueue.name === info.name) { + if (this._shouldCoallesce(info)) { // console.log('coalescing') this._pendingEventsForNextTick[0] = info this._allEventsSinceLastTick.push(info) @@ -8590,7 +8603,7 @@ export class Editor extends EventEmitter { } private _flushEventsForTick = (elapsed: number) => { - // const now = Date.now() + const now = Date.now() this.batch(() => { if (this._pendingEventsForNextTick.length > 0) { // console.log('flushing', this._pendingEventsForNextTick.length, this._allEventsSinceLastTick) @@ -8604,7 +8617,7 @@ export class Editor extends EventEmitter { this.scribbles.tick(elapsed) }) this._allEventsSinceLastTick.length = 0 - // console.log('flushed', Date.now() - now) + console.log('flusing took', Date.now() - now) } private _flushEventForTick = (info: TLEventInfo) => { diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 338a6b2f6..fb82cdb5e 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_2 | typeof Pointing_2)[]; + static children: () => (typeof Idle_3 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 24aa6c0af..48b0f4a74 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,6 +3799,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "DrawShapeTool", + "canonicalReference": "tldraw!DrawShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3817,15 +3826,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "HighlightShapeTool", - "canonicalReference": "tldraw!HighlightShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", From 9831a435036e8844e780f3806459f82e9904cb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 12:12:11 +0100 Subject: [PATCH 04/82] Simplify. --- packages/editor/src/lib/editor/Editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index dac17ae65..dfd1200ac 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8548,7 +8548,7 @@ export class Editor extends EventEmitter { private _shouldCoallesce = (info: TLEventInfo) => { if (!this._isCoalesableEvent(info)) return false return ( - this._isCoalesableEvent(this._pendingEventsForNextTick[0]) && + this._pendingEventsForNextTick.length === 1 && this._pendingEventsForNextTick[0].name === info.name ) } From 608e832b93915a6a8675749339226399a377ceb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 17:31:37 +0100 Subject: [PATCH 05/82] Improve coalescing. --- packages/editor/api-report.md | 5 +- packages/editor/api/api.json | 21 +++++--- packages/editor/src/lib/editor/Editor.ts | 48 +++++++++---------- .../editor/src/lib/editor/tools/StateNode.ts | 6 ++- .../src/lib/editor/types/event-types.ts | 10 +++- .../editor/src/lib/hooks/useCanvasEvents.ts | 12 +++++ .../editor/src/lib/hooks/useHandleEvents.ts | 4 ++ .../src/lib/hooks/useSelectionEvents.ts | 5 ++ packages/tldraw/api-report.md | 1 - .../src/lib/shapes/draw/toolStates/Drawing.ts | 42 ++++++---------- .../shapes/frame/components/FrameHeading.tsx | 3 ++ .../src/lib/shapes/shared/useEditableText.ts | 2 + .../ui/components/Minimap/DefaultMinimap.tsx | 2 + .../src/lib/ui/hooks/useKeyboardShortcuts.ts | 3 ++ .../tldraw/src/test/commands/penmode.test.ts | 1 + 15 files changed, 102 insertions(+), 63 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 6fa9770c6..5e26a8e8d 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -670,7 +670,7 @@ export class Editor extends EventEmitter { getCanRedo(): boolean; getCanUndo(): boolean; // (undocumented) - getCoalescedEvents: () => TLEventInfo[]; + getCoalescedEvents: () => TLPointerEventInfo[]; getContainer: () => HTMLElement; getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; // @internal @@ -2101,7 +2101,7 @@ export interface TLEventHandlers { // (undocumented) onPointerDown: TLPointerEvent; // (undocumented) - onPointerMove: TLPointerEvent; + onPointerMove: TLPointerMoveEvent; // (undocumented) onPointerUp: TLPointerEvent; // (undocumented) @@ -2368,6 +2368,7 @@ export type TLPointerEventInfo = TLBaseEventInfo & { pointerId: number; button: number; isPen: boolean; + pagePoint: Vec; } & TLPointerEventTarget; // @public (undocumented) diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 6c451d030..70f3f63c8 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10161,8 +10161,8 @@ }, { "kind": "Reference", - "text": "TLEventInfo", - "canonicalReference": "@tldraw/editor!TLEventInfo:type" + "text": "TLPointerEventInfo", + "canonicalReference": "@tldraw/editor!TLPointerEventInfo:type" }, { "kind": "Content", @@ -37525,8 +37525,8 @@ }, { "kind": "Reference", - "text": "TLPointerEvent", - "canonicalReference": "@tldraw/editor!TLPointerEvent:type" + "text": "TLPointerMoveEvent", + "canonicalReference": "@tldraw/editor!~TLPointerMoveEvent:type" }, { "kind": "Content", @@ -40076,7 +40076,16 @@ }, { "kind": "Content", - "text": ";\n pointerId: number;\n button: number;\n isPen: boolean;\n} & " + "text": ";\n pointerId: number;\n button: number;\n isPen: boolean;\n pagePoint: " + }, + { + "kind": "Reference", + "text": "Vec", + "canonicalReference": "@tldraw/editor!Vec:class" + }, + { + "kind": "Content", + "text": ";\n} & " }, { "kind": "Reference", @@ -40093,7 +40102,7 @@ "name": "TLPointerEventInfo", "typeTokenRange": { "startIndex": 1, - "endIndex": 8 + "endIndex": 10 } }, { diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index dfd1200ac..324f27158 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2106,6 +2106,7 @@ export class Editor extends EventEmitter { name: 'pointer_move', // weird but true: we need to put the screen point back into client space point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y), + pagePoint: this.inputs.currentPagePoint, pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE, ctrlKey: this.inputs.ctrlKey, altKey: this.inputs.altKey, @@ -8570,22 +8571,18 @@ export class Editor extends EventEmitter { * @public */ dispatch = (info: TLEventInfo): this => { - // console.log('adding to pending events', info.name) - if (this._isCoalesableEvent(info)) { - this._updateInputsFromEvent(info as TLPointerEventInfo) - ;(info as any).inputs = structuredClone(this.inputs) - } if (this._pendingEventsForNextTick.length === 0) { - // console.log('adding the first event') this._pendingEventsForNextTick.push(info) - this._allEventsSinceLastTick.push(info) + if (info.name === 'pointer_move') { + this._allEventsSinceLastTick.push(info) + } } else { if (this._shouldCoallesce(info)) { - // console.log('coalescing') this._pendingEventsForNextTick[0] = info - this._allEventsSinceLastTick.push(info) + if (info.name === 'pointer_move') { + this._allEventsSinceLastTick.push(info) + } } else { - // console.log('different event type') // Event has changed. We flush the currently pending events this._flushEventsForTick(0) // Then we add the new event to the queue (flushing clears the queue) @@ -8596,17 +8593,15 @@ export class Editor extends EventEmitter { } private _pendingEventsForNextTick: TLEventInfo[] = [] - private _allEventsSinceLastTick: TLEventInfo[] = [] + private _allEventsSinceLastTick: TLPointerEventInfo[] = [] getCoalescedEvents = () => { return this._allEventsSinceLastTick } private _flushEventsForTick = (elapsed: number) => { - const now = Date.now() this.batch(() => { if (this._pendingEventsForNextTick.length > 0) { - // console.log('flushing', this._pendingEventsForNextTick.length, this._allEventsSinceLastTick) const events = [...this._pendingEventsForNextTick] this._pendingEventsForNextTick.length = 0 for (const info of events) { @@ -8617,7 +8612,6 @@ export class Editor extends EventEmitter { this.scribbles.tick(elapsed) }) this._allEventsSinceLastTick.length = 0 - console.log('flusing took', Date.now() - now) } private _flushEventForTick = (info: TLEventInfo) => { @@ -8717,11 +8711,14 @@ export class Editor extends EventEmitter { const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)) - this.setCamera({ - x: cx + dx / cz - x / cz + x / zoom, - y: cy + dy / cz - y / cz + y / zoom, - z: zoom, - }) + this._setCamera( + { + x: cx + dx / cz - x / cz + x / zoom, + y: cy + dy / cz - y / cz + y / zoom, + z: zoom, + }, + true + ) return // Stop here! } @@ -8768,11 +8765,14 @@ export class Editor extends EventEmitter { const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz)) - this.setCamera({ - x: cx + (x / zoom - x) - (x / cz - x), - y: cy + (y / zoom - y) - (y / cz - y), - z: zoom, - }) + this._setCamera( + { + x: cx + (x / zoom - x) - (x / cz - x), + y: cy + (y / zoom - y) - (y / cz - y), + z: zoom, + }, + true + ) // We want to return here because none of the states in our // statechart should respond to this event (a camera zoom) diff --git a/packages/editor/src/lib/editor/tools/StateNode.ts b/packages/editor/src/lib/editor/tools/StateNode.ts index 5c5378b63..55fcb63a6 100644 --- a/packages/editor/src/lib/editor/tools/StateNode.ts +++ b/packages/editor/src/lib/editor/tools/StateNode.ts @@ -147,7 +147,11 @@ export abstract class StateNode implements Partial { handleEvent = (info: Exclude) => { const cbName = EVENT_NAME_MAP[info.name] const x = this.getCurrent() - this[cbName]?.(info as any) + if (cbName === 'onPointerMove') { + this[cbName]?.(info as any, this.editor.getCoalescedEvents()) + } else { + this[cbName]?.(info as any) + } if (this.getCurrent() === x && this.getIsActive()) { x?.handleEvent(info) } diff --git a/packages/editor/src/lib/editor/types/event-types.ts b/packages/editor/src/lib/editor/types/event-types.ts index 5fa41deb8..77881bc89 100644 --- a/packages/editor/src/lib/editor/types/event-types.ts +++ b/packages/editor/src/lib/editor/types/event-types.ts @@ -1,5 +1,5 @@ import { TLHandle, TLShape, VecModel } from '@tldraw/tlschema' -import { VecLike } from '../../primitives/Vec' +import { Vec, VecLike } from '../../primitives/Vec' import { TLSelectionHandle } from './selection-types' /** @public */ @@ -58,6 +58,7 @@ export type TLPointerEventInfo = TLBaseEventInfo & { pointerId: number button: number isPen: boolean + pagePoint: Vec } & TLPointerEventTarget /** @public */ @@ -118,6 +119,11 @@ export type TLEventInfo = /** @public */ export type TLPointerEvent = (info: TLPointerEventInfo) => void /** @public */ +export type TLPointerMoveEvent = ( + info: TLPointerEventInfo, + coalescedInfo: TLPointerEventInfo[] +) => void +/** @public */ export type TLClickEvent = (info: TLClickEventInfo) => void /** @public */ export type TLKeyboardEvent = (info: TLKeyboardEventInfo) => void @@ -151,7 +157,7 @@ export type TLExitEventHandler = (info: any, to: string) => void /** @public */ export interface TLEventHandlers { onPointerDown: TLPointerEvent - onPointerMove: TLPointerEvent + onPointerMove: TLPointerMoveEvent onRightClick: TLPointerEvent onDoubleClick: TLClickEvent onTripleClick: TLClickEvent diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 1633708c1..9cc53213e 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -1,4 +1,6 @@ +import { structuredClone } from '@tldraw/utils' import React, { useMemo } from 'react' +import { Vec } from '../primitives/Vec' import { preventDefault, releasePointerCapture, @@ -23,6 +25,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'right_click', ...getPointerInfo(e), }) @@ -36,6 +39,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'pointer_down', ...getPointerInfo(e), }) @@ -57,10 +61,17 @@ export function useCanvasEvents() { lastX = e.clientX lastY = e.clientY + const { screenBounds } = editor.getInstanceState() + const { x: cx, y: cy, z: cz } = editor.getCamera() + const sx = lastX - screenBounds.x + const sy = lastY - screenBounds.y + const sz = e.pressure + editor.dispatch({ type: 'pointer', target: 'canvas', name: 'pointer_move', + pagePoint: new Vec(sx / cz - cx, sy / cz - cy, sz ?? 0.5), ...getPointerInfo(e), }) } @@ -76,6 +87,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useHandleEvents.ts b/packages/editor/src/lib/hooks/useHandleEvents.ts index 3fb42597e..7a85c4d82 100644 --- a/packages/editor/src/lib/hooks/useHandleEvents.ts +++ b/packages/editor/src/lib/hooks/useHandleEvents.ts @@ -1,4 +1,5 @@ import { TLArrowShape, TLLineShape, TLShapeId } from '@tldraw/tlschema' +import { structuredClone } from '@tldraw/utils' import * as React from 'react' import { Editor } from '../editor/Editor' import { loopToHtmlElement, releasePointerCapture, setPointerCapture } from '../utils/dom' @@ -31,6 +32,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'pointer_down', ...getPointerInfo(e), }) @@ -54,6 +56,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'pointer_move', ...getPointerInfo(e), }) @@ -74,6 +77,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, + pagePoint: structuredClone(editor.inputs.currentPagePoint), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index 993330b2e..6c7fccebd 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -1,3 +1,4 @@ +import { structuredClone } from '@tldraw/utils' import { useMemo } from 'react' import { TLSelectionHandle } from '../editor/types/selection-types' import { @@ -24,6 +25,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { target: 'selection', handle, name: 'right_click', + pagePoint: structuredClone(editor.inputs.currentPagePoint), ...getPointerInfo(e), }) return @@ -51,6 +53,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, + pagePoint: structuredClone(editor.inputs.currentPagePoint), ...getPointerInfo(e), }) stopEventPropagation(e) @@ -71,6 +74,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, + pagePoint: structuredClone(editor.inputs.currentPagePoint), ...getPointerInfo(e), }) } @@ -84,6 +88,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, + pagePoint: structuredClone(editor.inputs.currentPagePoint), ...getPointerInfo(e), }) } diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index fb82cdb5e..5b45dafe6 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -71,7 +71,6 @@ import { TLEditorComponents } from '@tldraw/editor'; import { TLEmbedShape } from '@tldraw/editor'; import { TLEnterEventHandler } from '@tldraw/editor'; import { TLEventHandlers } from '@tldraw/editor'; -import { TLEventInfo } from '@tldraw/editor'; import { TLExitEventHandler } from '@tldraw/editor'; import { TLFrameShape } from '@tldraw/editor'; import { TLGeoShape } from '@tldraw/editor'; diff --git a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts index 83f712904..0107b1cf8 100644 --- a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts +++ b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts @@ -6,7 +6,6 @@ import { TLDrawShape, TLDrawShapeSegment, TLEventHandlers, - TLEventInfo, TLHighlightShape, TLPointerEventInfo, TLShapePartial, @@ -62,19 +61,14 @@ export class Drawing extends StateNode { } } - override onPointerMove: TLEventHandlers['onPointerMove'] = () => { - // console.log('drawing: pointer move') - const coallesced = this.editor.getCoalescedEvents() - // console.log({ coallesced }) - console.log('number of events', coallesced.length) - coallesced.forEach((info, i) => { - // console.log(i, (info as any).inputs) + override onPointerMove: TLEventHandlers['onPointerMove'] = (_info, coallescedInfo) => { + coallescedInfo.forEach((info) => { this.processEvents(info) }) } - processEvents(info: TLEventInfo) { - const inputs = (info as any).inputs + processEvents(info: TLPointerEventInfo) { + const { inputs } = this.editor if (this.isPen !== inputs.isPen) { // The user made a palm gesture before starting a pen gesture; @@ -96,11 +90,8 @@ export class Drawing extends StateNode { if (this.canDraw) { if (inputs.isPen) { // Don't update the shape if we haven't moved far enough from the last time we recorded a point - if ( - Vec.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >= - 1 / this.editor.getZoomLevel() - ) { - this.lastRecordedPoint = inputs.currentPagePoint.clone() + if (Vec.Dist(info.pagePoint, this.lastRecordedPoint) >= 1 / this.editor.getZoomLevel()) { + this.lastRecordedPoint = info.pagePoint.clone() this.mergeNextPoint = false } else { this.mergeNextPoint = true @@ -109,7 +100,7 @@ export class Drawing extends StateNode { this.mergeNextPoint = false } - this.updateShapes(inputs) + this.updateShapes(info.pagePoint) } } @@ -127,7 +118,7 @@ export class Drawing extends StateNode { } } } - this.updateShapes(this.editor.inputs) + this.updateShapes(this.editor.inputs.currentPagePoint) } override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => { @@ -149,7 +140,7 @@ export class Drawing extends StateNode { } } - this.updateShapes(this.editor.inputs) + this.updateShapes(this.editor.inputs.currentPagePoint) } override onExit? = () => { @@ -291,8 +282,7 @@ export class Drawing extends StateNode { this.initialShape = this.editor.getShape(id) } - private updateShapes(inputs: any) { - // console.log('update shapes', inputs) + private updateShapes(currentPagePoint: Vec) { const { initialShape } = this if (!initialShape) return @@ -308,7 +298,7 @@ export class Drawing extends StateNode { const { segments } = shape.props - const { x, y, z } = this.editor.getPointInShapeSpace(shape, inputs.currentPagePoint).toFixed() + const { x, y, z } = this.editor.getPointInShapeSpace(shape, currentPagePoint).toFixed() const newPoint = { x, y, z: this.isPen ? +(z! * 1.25).toFixed(2) : 0.5 } @@ -321,7 +311,7 @@ export class Drawing extends StateNode { } const hasMovedFarEnough = - Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE + Vec.Dist(pagePointWhereNextSegmentChanged, currentPagePoint) > DRAG_DISTANCE // Find the distance from where the pointer was when shift was released and // where it is now; if it's far enough away, then update the page point where @@ -394,7 +384,7 @@ export class Drawing extends StateNode { } const hasMovedFarEnough = - Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE + Vec.Dist(pagePointWhereNextSegmentChanged, currentPagePoint) > DRAG_DISTANCE // Find the distance from where the pointer was when shift was released and // where it is now; if it's far enough away, then update the page point where @@ -450,7 +440,7 @@ export class Drawing extends StateNode { const newSegment = newSegments[newSegments.length - 1] const { pagePointWhereCurrentSegmentChanged } = this - const { currentPagePoint, ctrlKey } = this.editor.inputs + const { ctrlKey } = this.editor.inputs if (!pagePointWhereCurrentSegmentChanged) throw Error('We should have a point where the segment changed') @@ -633,8 +623,6 @@ export class Drawing extends StateNode { if (newPoints.length > 500) { this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }]) - const { currentPagePoint } = this.editor.inputs - const newShapeId = createShapeId() this.editor.createShapes([ @@ -657,7 +645,7 @@ export class Drawing extends StateNode { this.initialShape = structuredClone(this.editor.getShape(newShapeId)!) this.mergeNextPoint = false - this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone() + this.lastRecordedPoint = currentPagePoint.clone() this.currentLineLength = 0 } diff --git a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx index b03c5fe7e..15ac76b85 100644 --- a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx +++ b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx @@ -8,6 +8,7 @@ import { useIsEditing, useValue, } from '@tldraw/editor' +import { structuredClone } from '@tldraw/utils' import { useCallback, useEffect, useRef } from 'react' import { FrameLabelInput } from './FrameLabelInput' @@ -45,6 +46,8 @@ export const FrameHeading = function FrameHeading({ name: 'pointer_down', target: 'shape', shape: editor.getShape(id)!, + pagePoint: structuredClone(editor.inputs.currentPagePoint), + ...event, }) e.preventDefault() diff --git a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts index 40a776cdf..5d0dc3668 100644 --- a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts +++ b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts @@ -9,6 +9,7 @@ import { useEditor, useValue, } from '@tldraw/editor' +import { structuredClone } from '@tldraw/utils' import React, { useCallback, useEffect, useRef } from 'react' import { INDENT, TextHelpers } from './TextHelpers' @@ -185,6 +186,7 @@ export function useEditableText { ctrlKey: false, button: 1, target: 'canvas', + pagePoint: new Vec(0, 0), }) expect(editor.getCurrentPageShapes().length).toBe(0) From 568368b6902c957469657e5df199f7cb2600246d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 17:32:53 +0100 Subject: [PATCH 06/82] Fix imports. --- packages/tldraw/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 288cc2773..20aa5b163 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -54,6 +54,7 @@ "@radix-ui/react-slider": "^1.1.0", "@radix-ui/react-toast": "^1.1.1", "@tldraw/editor": "workspace:*", + "@tldraw/utils": "workspace:*", "canvas-size": "^1.2.6", "classnames": "^2.3.2", "hotkeys-js": "^3.11.2", diff --git a/yarn.lock b/yarn.lock index c81a8d937..846a946e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23067,6 +23067,7 @@ __metadata: "@testing-library/jest-dom": "npm:^5.16.5" "@testing-library/react": "npm:^14.0.0" "@tldraw/editor": "workspace:*" + "@tldraw/utils": "workspace:*" "@types/canvas-size": "npm:^1.2.0" "@types/classnames": "npm:^2.3.1" "@types/lz-string": "npm:^1.3.34" From dce67dbc0ca85c58582d208dca3862a037996bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 20 Mar 2024 17:42:57 +0100 Subject: [PATCH 07/82] Use clone from vec instead of structured clone. --- packages/editor/src/lib/hooks/useCanvasEvents.ts | 7 +++---- packages/editor/src/lib/hooks/useHandleEvents.ts | 7 +++---- packages/editor/src/lib/hooks/useSelectionEvents.ts | 9 ++++----- packages/tldraw/package.json | 1 - .../src/lib/shapes/frame/components/FrameHeading.tsx | 3 +-- packages/tldraw/src/lib/shapes/shared/useEditableText.ts | 3 +-- .../src/lib/ui/components/Minimap/DefaultMinimap.tsx | 3 +-- packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts | 5 ++--- yarn.lock | 1 - 9 files changed, 15 insertions(+), 24 deletions(-) diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 9cc53213e..55d27117f 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -1,4 +1,3 @@ -import { structuredClone } from '@tldraw/utils' import React, { useMemo } from 'react' import { Vec } from '../primitives/Vec' import { @@ -25,7 +24,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), name: 'right_click', ...getPointerInfo(e), }) @@ -39,7 +38,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_down', ...getPointerInfo(e), }) @@ -87,7 +86,7 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useHandleEvents.ts b/packages/editor/src/lib/hooks/useHandleEvents.ts index 7a85c4d82..c10cfa440 100644 --- a/packages/editor/src/lib/hooks/useHandleEvents.ts +++ b/packages/editor/src/lib/hooks/useHandleEvents.ts @@ -1,5 +1,4 @@ import { TLArrowShape, TLLineShape, TLShapeId } from '@tldraw/tlschema' -import { structuredClone } from '@tldraw/utils' import * as React from 'react' import { Editor } from '../editor/Editor' import { loopToHtmlElement, releasePointerCapture, setPointerCapture } from '../utils/dom' @@ -32,7 +31,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint, name: 'pointer_down', ...getPointerInfo(e), }) @@ -56,7 +55,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_move', ...getPointerInfo(e), }) @@ -77,7 +76,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index 6c7fccebd..b21edc8eb 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -1,4 +1,3 @@ -import { structuredClone } from '@tldraw/utils' import { useMemo } from 'react' import { TLSelectionHandle } from '../editor/types/selection-types' import { @@ -25,7 +24,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { target: 'selection', handle, name: 'right_click', - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) return @@ -53,7 +52,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) stopEventPropagation(e) @@ -74,7 +73,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) } @@ -88,7 +87,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) } diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index 20aa5b163..288cc2773 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -54,7 +54,6 @@ "@radix-ui/react-slider": "^1.1.0", "@radix-ui/react-toast": "^1.1.1", "@tldraw/editor": "workspace:*", - "@tldraw/utils": "workspace:*", "canvas-size": "^1.2.6", "classnames": "^2.3.2", "hotkeys-js": "^3.11.2", diff --git a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx index 15ac76b85..dc0266558 100644 --- a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx +++ b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx @@ -8,7 +8,6 @@ import { useIsEditing, useValue, } from '@tldraw/editor' -import { structuredClone } from '@tldraw/utils' import { useCallback, useEffect, useRef } from 'react' import { FrameLabelInput } from './FrameLabelInput' @@ -46,7 +45,7 @@ export const FrameHeading = function FrameHeading({ name: 'pointer_down', target: 'shape', shape: editor.getShape(id)!, - pagePoint: structuredClone(editor.inputs.currentPagePoint), + pagePoint: editor.inputs.currentPagePoint.clone(), ...event, }) diff --git a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts index 5d0dc3668..2bdd46865 100644 --- a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts +++ b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts @@ -9,7 +9,6 @@ import { useEditor, useValue, } from '@tldraw/editor' -import { structuredClone } from '@tldraw/utils' import React, { useCallback, useEffect, useRef } from 'react' import { INDENT, TextHelpers } from './TextHelpers' @@ -186,7 +185,7 @@ export function useEditableText Date: Thu, 21 Mar 2024 09:18:33 +0100 Subject: [PATCH 08/82] Refactor. --- packages/editor/api-report.md | 18 ++-- packages/editor/api/api.json | 92 ++++++++++++------- packages/editor/src/index.ts | 1 + packages/editor/src/lib/editor/Editor.ts | 22 +++-- .../editor/src/lib/editor/tools/StateNode.ts | 6 +- .../src/lib/editor/types/event-types.ts | 17 ++-- .../editor/src/lib/hooks/useCanvasEvents.ts | 3 - .../editor/src/lib/hooks/useHandleEvents.ts | 2 +- .../src/lib/hooks/useSelectionEvents.ts | 3 +- packages/tldraw/api-report.md | 9 +- packages/tldraw/api/api.json | 24 ++--- .../src/lib/shapes/draw/toolStates/Drawing.ts | 15 +-- .../shapes/frame/components/FrameHeading.tsx | 2 - .../src/lib/shapes/shared/useEditableText.ts | 1 - .../ui/components/Minimap/DefaultMinimap.tsx | 1 + .../src/lib/ui/hooks/useKeyboardShortcuts.ts | 1 - 16 files changed, 120 insertions(+), 97 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 5e26a8e8d..367ea2bf4 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -670,7 +670,7 @@ export class Editor extends EventEmitter { getCanRedo(): boolean; getCanUndo(): boolean; // (undocumented) - getCoalescedEvents: () => TLPointerEventInfo[]; + getCoalescedEvents: () => TLPointerMoveEventInfo[]; getContainer: () => HTMLElement; getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; // @internal @@ -2361,15 +2361,7 @@ export type TLPinchEventName = 'pinch_end' | 'pinch_start' | 'pinch'; export type TLPointerEvent = (info: TLPointerEventInfo) => void; // @public (undocumented) -export type TLPointerEventInfo = TLBaseEventInfo & { - type: 'pointer'; - name: TLPointerEventName; - point: VecLike; - pointerId: number; - button: number; - isPen: boolean; - pagePoint: Vec; -} & TLPointerEventTarget; +export type TLPointerEventInfo = TLBasePointerEventInfo | TLPointerMoveEventInfo; // @public (undocumented) export type TLPointerEventName = 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'; @@ -2391,6 +2383,12 @@ export type TLPointerEventTarget = { shape: TLShape; }; +// @public (undocumented) +export type TLPointerMoveEventInfo = TLBasePointerEventInfo & { + coalescedInfo: TLPointerMoveEventInfo[]; + pagePoint: Vec; +}; + // @public (undocumented) export type TLResizeHandle = SelectionCorner | SelectionEdge; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 70f3f63c8..dfd37ed4b 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10161,8 +10161,8 @@ }, { "kind": "Reference", - "text": "TLPointerEventInfo", - "canonicalReference": "@tldraw/editor!TLPointerEventInfo:type" + "text": "TLPointerMoveEventInfo", + "canonicalReference": "@tldraw/editor!TLPointerMoveEventInfo:type" }, { "kind": "Content", @@ -40053,44 +40053,17 @@ }, { "kind": "Reference", - "text": "TLBaseEventInfo", - "canonicalReference": "@tldraw/editor!TLBaseEventInfo:interface" + "text": "TLBasePointerEventInfo", + "canonicalReference": "@tldraw/editor!~TLBasePointerEventInfo:type" }, { "kind": "Content", - "text": " & {\n type: 'pointer';\n name: " + "text": " | " }, { "kind": "Reference", - "text": "TLPointerEventName", - "canonicalReference": "@tldraw/editor!TLPointerEventName:type" - }, - { - "kind": "Content", - "text": ";\n point: " - }, - { - "kind": "Reference", - "text": "VecLike", - "canonicalReference": "@tldraw/editor!VecLike:type" - }, - { - "kind": "Content", - "text": ";\n pointerId: number;\n button: number;\n isPen: boolean;\n pagePoint: " - }, - { - "kind": "Reference", - "text": "Vec", - "canonicalReference": "@tldraw/editor!Vec:class" - }, - { - "kind": "Content", - "text": ";\n} & " - }, - { - "kind": "Reference", - "text": "TLPointerEventTarget", - "canonicalReference": "@tldraw/editor!TLPointerEventTarget:type" + "text": "TLPointerMoveEventInfo", + "canonicalReference": "@tldraw/editor!TLPointerMoveEventInfo:type" }, { "kind": "Content", @@ -40102,7 +40075,7 @@ "name": "TLPointerEventInfo", "typeTokenRange": { "startIndex": 1, - "endIndex": 10 + "endIndex": 4 } }, { @@ -40193,6 +40166,55 @@ "endIndex": 10 } }, + { + "kind": "TypeAlias", + "canonicalReference": "@tldraw/editor!TLPointerMoveEventInfo:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type TLPointerMoveEventInfo = " + }, + { + "kind": "Reference", + "text": "TLBasePointerEventInfo", + "canonicalReference": "@tldraw/editor!~TLBasePointerEventInfo:type" + }, + { + "kind": "Content", + "text": " & {\n coalescedInfo: " + }, + { + "kind": "Reference", + "text": "TLPointerMoveEventInfo", + "canonicalReference": "@tldraw/editor!TLPointerMoveEventInfo:type" + }, + { + "kind": "Content", + "text": "[];\n pagePoint: " + }, + { + "kind": "Reference", + "text": "Vec", + "canonicalReference": "@tldraw/editor!Vec:class" + }, + { + "kind": "Content", + "text": ";\n}" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/editor/src/lib/editor/types/event-types.ts", + "releaseTag": "Public", + "name": "TLPointerMoveEventInfo", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 7 + } + }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/editor!TLResizeHandle:type", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 8f7f42c9f..50351db3a 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -222,6 +222,7 @@ export { type TLPointerEventInfo, type TLPointerEventName, type TLPointerEventTarget, + type TLPointerMoveEventInfo, type TLTickEvent, type TLWheelEvent, type TLWheelEventInfo, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 324f27158..c4f6b1a0b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -126,6 +126,7 @@ import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo, + TLPointerMoveEventInfo, TLWheelEventInfo, } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' @@ -8544,8 +8545,6 @@ export class Editor extends EventEmitter { /** @internal */ capturedPointerId: number | null = null - private _eventsToCoalesce = ['pointer_move'] - private _shouldCoallesce = (info: TLEventInfo) => { if (!this._isCoalesableEvent(info)) return false return ( @@ -8554,8 +8553,11 @@ export class Editor extends EventEmitter { ) } - private _isCoalesableEvent = (info: TLEventInfo) => { - return this._eventsToCoalesce.includes(info.name) + private _isCoalesableEvent = (info: TLEventInfo): info is TLPointerMoveEventInfo => { + if ((info as any).coalescedInfo) { + return true + } + return false } /** @@ -8573,13 +8575,13 @@ export class Editor extends EventEmitter { dispatch = (info: TLEventInfo): this => { if (this._pendingEventsForNextTick.length === 0) { this._pendingEventsForNextTick.push(info) - if (info.name === 'pointer_move') { + if (this._isCoalesableEvent(info)) { this._allEventsSinceLastTick.push(info) } } else { if (this._shouldCoallesce(info)) { this._pendingEventsForNextTick[0] = info - if (info.name === 'pointer_move') { + if (this._isCoalesableEvent(info)) { this._allEventsSinceLastTick.push(info) } } else { @@ -8593,7 +8595,7 @@ export class Editor extends EventEmitter { } private _pendingEventsForNextTick: TLEventInfo[] = [] - private _allEventsSinceLastTick: TLPointerEventInfo[] = [] + private _allEventsSinceLastTick: TLPointerMoveEventInfo[] = [] getCoalescedEvents = () => { return this._allEventsSinceLastTick @@ -9053,7 +9055,11 @@ export class Editor extends EventEmitter { // Send the event to the statechart. It will be handled by all // active states, starting at the root. - this.root.handleEvent(info) + if (this._isCoalesableEvent(info)) { + info.coalescedInfo = this._allEventsSinceLastTick + } else { + this.root.handleEvent(info) + } this.emit('event', info) return this diff --git a/packages/editor/src/lib/editor/tools/StateNode.ts b/packages/editor/src/lib/editor/tools/StateNode.ts index 55fcb63a6..5c5378b63 100644 --- a/packages/editor/src/lib/editor/tools/StateNode.ts +++ b/packages/editor/src/lib/editor/tools/StateNode.ts @@ -147,11 +147,7 @@ export abstract class StateNode implements Partial { handleEvent = (info: Exclude) => { const cbName = EVENT_NAME_MAP[info.name] const x = this.getCurrent() - if (cbName === 'onPointerMove') { - this[cbName]?.(info as any, this.editor.getCoalescedEvents()) - } else { - this[cbName]?.(info as any) - } + this[cbName]?.(info as any) if (this.getCurrent() === x && this.getIsActive()) { x?.handleEvent(info) } diff --git a/packages/editor/src/lib/editor/types/event-types.ts b/packages/editor/src/lib/editor/types/event-types.ts index 77881bc89..35ce80bf1 100644 --- a/packages/editor/src/lib/editor/types/event-types.ts +++ b/packages/editor/src/lib/editor/types/event-types.ts @@ -50,7 +50,7 @@ export interface TLBaseEventInfo { } /** @public */ -export type TLPointerEventInfo = TLBaseEventInfo & { +export type TLBasePointerEventInfo = TLBaseEventInfo & { type: 'pointer' name: TLPointerEventName // The pointer position in client space, i.e. clientX / clientY @@ -58,9 +58,17 @@ export type TLPointerEventInfo = TLBaseEventInfo & { pointerId: number button: number isPen: boolean - pagePoint: Vec } & TLPointerEventTarget +/** @public */ +export type TLPointerMoveEventInfo = TLBasePointerEventInfo & { + coalescedInfo: TLPointerMoveEventInfo[] + pagePoint: Vec +} + +/** @public */ +export type TLPointerEventInfo = TLBasePointerEventInfo | TLPointerMoveEventInfo + /** @public */ export type TLClickEventInfo = TLBaseEventInfo & { type: 'click' @@ -119,10 +127,7 @@ export type TLEventInfo = /** @public */ export type TLPointerEvent = (info: TLPointerEventInfo) => void /** @public */ -export type TLPointerMoveEvent = ( - info: TLPointerEventInfo, - coalescedInfo: TLPointerEventInfo[] -) => void +export type TLPointerMoveEvent = (info: TLPointerMoveEventInfo) => void /** @public */ export type TLClickEvent = (info: TLClickEventInfo) => void /** @public */ diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 55d27117f..93da36eb7 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -24,7 +24,6 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: editor.inputs.currentPagePoint.clone(), name: 'right_click', ...getPointerInfo(e), }) @@ -38,7 +37,6 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_down', ...getPointerInfo(e), }) @@ -86,7 +84,6 @@ export function useCanvasEvents() { editor.dispatch({ type: 'pointer', target: 'canvas', - pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useHandleEvents.ts b/packages/editor/src/lib/hooks/useHandleEvents.ts index c10cfa440..524c00e7c 100644 --- a/packages/editor/src/lib/hooks/useHandleEvents.ts +++ b/packages/editor/src/lib/hooks/useHandleEvents.ts @@ -56,6 +56,7 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { handle, shape, pagePoint: editor.inputs.currentPagePoint.clone(), + coalescedInfo: [], name: 'pointer_move', ...getPointerInfo(e), }) @@ -76,7 +77,6 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: editor.inputs.currentPagePoint.clone(), name: 'pointer_up', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index b21edc8eb..3c324572e 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -24,7 +24,6 @@ export function useSelectionEvents(handle: TLSelectionHandle) { target: 'selection', handle, name: 'right_click', - pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) return @@ -52,7 +51,6 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) stopEventPropagation(e) @@ -74,6 +72,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { target: 'selection', handle, pagePoint: editor.inputs.currentPagePoint.clone(), + coalescedInfo: [], ...getPointerInfo(e), }) } diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 5b45dafe6..ec764b4de 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -98,6 +98,7 @@ import { TLParentId } from '@tldraw/editor'; import { TLPointerEvent } from '@tldraw/editor'; import { TLPointerEventInfo } from '@tldraw/editor'; import { TLPointerEventName } from '@tldraw/editor'; +import { TLPointerMoveEventInfo } from '@tldraw/editor'; import { TLRecord } from '@tldraw/editor'; import { TLRotationSnapshot } from '@tldraw/editor'; import { TLSchema } from '@tldraw/editor'; @@ -403,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -449,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) @@ -675,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_3 | typeof Pointing_2)[]; + static children: () => (typeof Idle_2 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -870,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 48b0f4a74..24aa6c0af 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,15 +3799,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "DrawShapeTool", - "canonicalReference": "tldraw!DrawShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3826,6 +3817,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "HighlightShapeTool", + "canonicalReference": "tldraw!HighlightShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", diff --git a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts index 0107b1cf8..c3534f05c 100644 --- a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts +++ b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts @@ -7,7 +7,7 @@ import { TLDrawShapeSegment, TLEventHandlers, TLHighlightShape, - TLPointerEventInfo, + TLPointerMoveEventInfo, TLShapePartial, Vec, VecModel, @@ -25,7 +25,7 @@ type DrawableShape = TLDrawShape | TLHighlightShape export class Drawing extends StateNode { static override id = 'drawing' - info = {} as TLPointerEventInfo + info = {} as TLPointerMoveEventInfo initialShape?: DrawableShape @@ -51,7 +51,7 @@ export class Drawing extends StateNode { markId = null as null | string - override onEnter = (info: TLPointerEventInfo) => { + override onEnter = (info: TLPointerMoveEventInfo) => { this.markId = null this.info = info this.canDraw = !this.editor.getIsMenuOpen() @@ -61,13 +61,14 @@ export class Drawing extends StateNode { } } - override onPointerMove: TLEventHandlers['onPointerMove'] = (_info, coallescedInfo) => { - coallescedInfo.forEach((info) => { - this.processEvents(info) + override onPointerMove: TLEventHandlers['onPointerMove'] = (info: TLPointerMoveEventInfo) => { + const coallescedInfo = info.coalescedInfo + coallescedInfo.forEach((ci: TLPointerMoveEventInfo) => { + this.processEvents(ci) }) } - processEvents(info: TLPointerEventInfo) { + processEvents(info: TLPointerMoveEventInfo) { const { inputs } = this.editor if (this.isPen !== inputs.isPen) { diff --git a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx index dc0266558..b03c5fe7e 100644 --- a/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx +++ b/packages/tldraw/src/lib/shapes/frame/components/FrameHeading.tsx @@ -45,8 +45,6 @@ export const FrameHeading = function FrameHeading({ name: 'pointer_down', target: 'shape', shape: editor.getShape(id)!, - pagePoint: editor.inputs.currentPagePoint.clone(), - ...event, }) e.preventDefault() diff --git a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts index 2bdd46865..40a776cdf 100644 --- a/packages/tldraw/src/lib/shapes/shared/useEditableText.ts +++ b/packages/tldraw/src/lib/shapes/shared/useEditableText.ts @@ -185,7 +185,6 @@ export function useEditableText Date: Thu, 21 Mar 2024 09:19:24 +0100 Subject: [PATCH 09/82] Remove uneeded function. --- packages/editor/api-report.md | 2 -- packages/editor/api/api.json | 39 ------------------------ packages/editor/src/lib/editor/Editor.ts | 4 --- packages/tldraw/api-report.md | 8 ++--- packages/tldraw/api/api.json | 24 +++++++-------- 5 files changed, 16 insertions(+), 61 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 367ea2bf4..d2308d84a 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -669,8 +669,6 @@ export class Editor extends EventEmitter { getCameraState(): "idle" | "moving"; getCanRedo(): boolean; getCanUndo(): boolean; - // (undocumented) - getCoalescedEvents: () => TLPointerMoveEventInfo[]; getContainer: () => HTMLElement; getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; // @internal diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index dfd37ed4b..6ff9338f4 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10146,45 +10146,6 @@ "isAbstract": false, "name": "getCanUndo" }, - { - "kind": "Property", - "canonicalReference": "@tldraw/editor!Editor#getCoalescedEvents:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "getCoalescedEvents: " - }, - { - "kind": "Content", - "text": "() => " - }, - { - "kind": "Reference", - "text": "TLPointerMoveEventInfo", - "canonicalReference": "@tldraw/editor!TLPointerMoveEventInfo:type" - }, - { - "kind": "Content", - "text": "[]" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "getCoalescedEvents", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 4 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, { "kind": "Property", "canonicalReference": "@tldraw/editor!Editor#getContainer:member", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index c4f6b1a0b..320e3d4ca 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8597,10 +8597,6 @@ export class Editor extends EventEmitter { private _pendingEventsForNextTick: TLEventInfo[] = [] private _allEventsSinceLastTick: TLPointerMoveEventInfo[] = [] - getCoalescedEvents = () => { - return this._allEventsSinceLastTick - } - private _flushEventsForTick = (elapsed: number) => { this.batch(() => { if (this._pendingEventsForNextTick.length > 0) { diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index ec764b4de..2beab6449 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_2 | typeof Pointing_2)[]; + static children: () => (typeof Idle_3 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 24aa6c0af..48b0f4a74 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,6 +3799,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "DrawShapeTool", + "canonicalReference": "tldraw!DrawShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3817,15 +3826,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "HighlightShapeTool", - "canonicalReference": "tldraw!HighlightShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", From 7987c1e4de232b956df0ebf76f46cdfe08d8a07d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:22:43 +0100 Subject: [PATCH 10/82] Refactor. --- packages/editor/src/lib/editor/Editor.ts | 1 + packages/editor/src/lib/hooks/useCanvasEvents.ts | 1 + .../tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 320e3d4ca..ebc581e02 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2108,6 +2108,7 @@ export class Editor extends EventEmitter { // weird but true: we need to put the screen point back into client space point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y), pagePoint: this.inputs.currentPagePoint, + coalescedInfo: [], pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE, ctrlKey: this.inputs.ctrlKey, altKey: this.inputs.altKey, diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 93da36eb7..80ca666d1 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -69,6 +69,7 @@ export function useCanvasEvents() { target: 'canvas', name: 'pointer_move', pagePoint: new Vec(sx / cz - cx, sy / cz - cy, sz ?? 0.5), + coalescedInfo: [], ...getPointerInfo(e), }) } diff --git a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx index 875cef47d..89965f9e5 100644 --- a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx +++ b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx @@ -123,11 +123,11 @@ export function DefaultMinimap() { type: 'pointer', target: 'canvas', name: 'pointer_move', - pagePoint: editor.inputs.currentPagePoint.clone(), - coalescedInfo: [], ...getPointerInfo(e), point: screenPoint, isPen: editor.getInstanceState().isPenMode, + pagePoint: editor.inputs.currentPagePoint.clone(), + coalescedInfo: [], } editor.dispatch(info) From f8d0db5eb433c8646afd7472c0b996b7f32a2b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:25:17 +0100 Subject: [PATCH 11/82] Remove pagepoint. --- packages/editor/src/lib/hooks/useHandleEvents.ts | 1 - packages/editor/src/lib/hooks/useSelectionEvents.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/editor/src/lib/hooks/useHandleEvents.ts b/packages/editor/src/lib/hooks/useHandleEvents.ts index 524c00e7c..94f651b24 100644 --- a/packages/editor/src/lib/hooks/useHandleEvents.ts +++ b/packages/editor/src/lib/hooks/useHandleEvents.ts @@ -31,7 +31,6 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: editor.inputs.currentPagePoint, name: 'pointer_down', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index 3c324572e..ec6c457f0 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -86,7 +86,6 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: editor.inputs.currentPagePoint.clone(), ...getPointerInfo(e), }) } From 91d031d7095be31831e64de8a2ed71f61cfdb944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:26:57 +0100 Subject: [PATCH 12/82] Typo. --- packages/tldraw/api-report.md | 8 +++---- packages/tldraw/api/api.json | 24 +++++++++---------- .../src/lib/shapes/draw/toolStates/Drawing.ts | 4 ++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 2beab6449..ec764b4de 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_3 | typeof Pointing_2)[]; + static children: () => (typeof Idle_2 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 48b0f4a74..24aa6c0af 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,15 +3799,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "DrawShapeTool", - "canonicalReference": "tldraw!DrawShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3826,6 +3817,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "HighlightShapeTool", + "canonicalReference": "tldraw!HighlightShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", diff --git a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts index c3534f05c..c48021491 100644 --- a/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts +++ b/packages/tldraw/src/lib/shapes/draw/toolStates/Drawing.ts @@ -62,8 +62,8 @@ export class Drawing extends StateNode { } override onPointerMove: TLEventHandlers['onPointerMove'] = (info: TLPointerMoveEventInfo) => { - const coallescedInfo = info.coalescedInfo - coallescedInfo.forEach((ci: TLPointerMoveEventInfo) => { + const coalescedInfo = info.coalescedInfo + coalescedInfo.forEach((ci: TLPointerMoveEventInfo) => { this.processEvents(ci) }) } From 48e004e9561e450c4f9495152e50fa534cd22783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:28:22 +0100 Subject: [PATCH 13/82] Remove pagepoint. --- packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts | 1 - packages/tldraw/src/test/commands/penmode.test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts index 4e35829ef..fb10a306e 100644 --- a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts +++ b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts @@ -94,7 +94,6 @@ export function useKeyboardShortcuts() { button: 0, isPen: editor.getInstanceState().isPenMode, target: 'canvas', - pagePoint: editor.inputs.currentPagePoint.clone(), } editor.dispatch(info) diff --git a/packages/tldraw/src/test/commands/penmode.test.ts b/packages/tldraw/src/test/commands/penmode.test.ts index 6ab741304..b60e5b388 100644 --- a/packages/tldraw/src/test/commands/penmode.test.ts +++ b/packages/tldraw/src/test/commands/penmode.test.ts @@ -21,7 +21,6 @@ it('ignores touch events while in pen mode', async () => { ctrlKey: false, button: 1, target: 'canvas', - pagePoint: new Vec(0, 0), }) expect(editor.getCurrentPageShapes().length).toBe(0) From 38701a89cf72e4bc6a7a99645afd2af61acdb31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:44:38 +0100 Subject: [PATCH 14/82] Fix handling of events. --- packages/editor/src/lib/editor/Editor.ts | 7 +++---- packages/tldraw/api-report.md | 8 ++++---- packages/tldraw/api/api.json | 24 ++++++++++++------------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index ebc581e02..3acab6a10 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -9050,13 +9050,12 @@ export class Editor extends EventEmitter { } } - // Send the event to the statechart. It will be handled by all - // active states, starting at the root. if (this._isCoalesableEvent(info)) { info.coalescedInfo = this._allEventsSinceLastTick - } else { - this.root.handleEvent(info) } + // Send the event to the statechart. It will be handled by all + // active states, starting at the root. + this.root.handleEvent(info) this.emit('event', info) return this diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index ec764b4de..2beab6449 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_2 | typeof Pointing_2)[]; + static children: () => (typeof Idle_3 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 24aa6c0af..48b0f4a74 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,6 +3799,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "DrawShapeTool", + "canonicalReference": "tldraw!DrawShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3817,15 +3826,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "HighlightShapeTool", - "canonicalReference": "tldraw!HighlightShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", From 27a1c9b38135365edcb6d11f27661cf12b925927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:48:55 +0100 Subject: [PATCH 15/82] :) --- packages/editor/api/api.json | 2 +- packages/editor/src/lib/editor/Editor.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 6ff9338f4..e70da2900 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -9026,7 +9026,7 @@ { "kind": "Property", "canonicalReference": "@tldraw/editor!Editor#dispatch:member", - "docComment": "/**\n * Dispatch an event to the editor.\n *\n * @param info - The event info. h*\n *\n * @example\n * ```ts\n * editor.dispatch(myPointerEvent)\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Dispatch an event to the editor.\n *\n * @param info - The event info.\n *\n * @example\n * ```ts\n * editor.dispatch(myPointerEvent)\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 3acab6a10..e0f2da278 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8570,7 +8570,6 @@ export class Editor extends EventEmitter { * ``` * * @param info - The event info. - h* * @public */ dispatch = (info: TLEventInfo): this => { From 98b202c8c411fea22aa05a3dc9ea881092665793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:50:12 +0100 Subject: [PATCH 16/82] Typo. --- packages/editor/src/lib/editor/Editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index e0f2da278..db3b23d79 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8546,7 +8546,7 @@ export class Editor extends EventEmitter { /** @internal */ capturedPointerId: number | null = null - private _shouldCoallesce = (info: TLEventInfo) => { + private _shouldCoalesce = (info: TLEventInfo) => { if (!this._isCoalesableEvent(info)) return false return ( this._pendingEventsForNextTick.length === 1 && @@ -8579,7 +8579,7 @@ export class Editor extends EventEmitter { this._allEventsSinceLastTick.push(info) } } else { - if (this._shouldCoallesce(info)) { + if (this._shouldCoalesce(info)) { this._pendingEventsForNextTick[0] = info if (this._isCoalesableEvent(info)) { this._allEventsSinceLastTick.push(info) From 8515ec311d6ec562b08802acd022bd4bb6ce2f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 09:52:34 +0100 Subject: [PATCH 17/82] Add comments. --- packages/editor/src/lib/editor/Editor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index db3b23d79..53b1d16c0 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8573,6 +8573,7 @@ export class Editor extends EventEmitter { * @public */ dispatch = (info: TLEventInfo): this => { + // Adding the first event to the queue if (this._pendingEventsForNextTick.length === 0) { this._pendingEventsForNextTick.push(info) if (this._isCoalesableEvent(info)) { @@ -8580,7 +8581,9 @@ export class Editor extends EventEmitter { } } else { if (this._shouldCoalesce(info)) { - this._pendingEventsForNextTick[0] = info + // We only care for the last event + this._pendingEventsForNextTick = [info] + // But we also store all the other events if we have an coalesable event if (this._isCoalesableEvent(info)) { this._allEventsSinceLastTick.push(info) } From ae6a5f3812fe69bff383e65e809a09f70308a8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 13:29:40 +0100 Subject: [PATCH 18/82] Fix tests. --- packages/editor/src/lib/editor/Editor.ts | 2 + packages/tldraw/src/test/TestEditor.ts | 39 +++++++++++++++----- packages/tldraw/src/test/translating.test.ts | 4 +- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 53b1d16c0..d5d971e03 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8424,6 +8424,7 @@ export class Editor extends EventEmitter { */ cancel(): this { this.dispatch({ type: 'misc', name: 'cancel' }) + this._tickManager.tick() return this } @@ -8439,6 +8440,7 @@ export class Editor extends EventEmitter { */ interrupt(): this { this.dispatch({ type: 'misc', name: 'interrupt' }) + this._tickManager.tick() return this } diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index b1d204c6d..59afd5532 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -311,16 +311,35 @@ export class TestEditor extends Editor { /* ------------------ Input Events ------------------ */ + /** + Some of our updates are not synchronous any longer. For example, drawing happens on tick instead of on pointer move. + You can use this helper to force the tick, which will then process all the updates. + */ + forceTick = (count = 1) => { + for (let i = 0; i < count; i++) { + this.emit('tick', 16) + } + return this + } + pointerMove = ( x = this.inputs.currentScreenPoint.x, y = this.inputs.currentScreenPoint.y, options?: PointerEventInit, modifiers?: EventModifiers ) => { + const { screenBounds } = this.getInstanceState() + const { x: cx, y: cy, z: cz } = this.getCamera() + const sx = x - screenBounds.x + const sy = y - screenBounds.y + this.dispatch({ ...this.getPointerEventInfo(x, y, options, modifiers), name: 'pointer_move', - }) + pagePoint: new Vec(sx / cz - cx, sy / cz - cy), + + coalescedInfo: [], + }).forceTick() return this } @@ -333,7 +352,7 @@ export class TestEditor extends Editor { this.dispatch({ ...this.getPointerEventInfo(x, y, options, modifiers), name: 'pointer_down', - }) + }).forceTick() return this } @@ -346,7 +365,7 @@ export class TestEditor extends Editor { this.dispatch({ ...this.getPointerEventInfo(x, y, options, modifiers), name: 'pointer_up', - }) + }).forceTick() return this } @@ -380,17 +399,17 @@ export class TestEditor extends Editor { type: 'click', name: 'double_click', phase: 'up', - }) + }).forceTick() return this } keyDown = (key: string, options = {} as Partial>) => { - this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_down', options) }) + this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_down', options) }).forceTick() return this } keyRepeat = (key: string, options = {} as Partial>) => { - this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_repeat', options) }) + this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_repeat', options) }).forceTick() return this } @@ -402,7 +421,7 @@ export class TestEditor extends Editor { altKey: this.inputs.altKey && key !== 'Alt', ...options, }), - }) + }).forceTick() return this } @@ -416,7 +435,7 @@ export class TestEditor extends Editor { altKey: this.inputs.altKey, ...options, delta: { x: dx, y: dy }, - }) + }).forceTick(2) return this } @@ -438,7 +457,7 @@ export class TestEditor extends Editor { ...options, point: { x, y, z }, delta: { x: dx, y: dy, z: dz }, - }) + }).forceTick() return this } @@ -482,7 +501,7 @@ export class TestEditor extends Editor { ...options, point: { x, y, z }, delta: { x: dx, y: dy, z: dz }, - }) + }).forceTick() return this } /* ------ Interaction Helpers ------ */ diff --git a/packages/tldraw/src/test/translating.test.ts b/packages/tldraw/src/test/translating.test.ts index a0292426e..e5fd235ee 100644 --- a/packages/tldraw/src/test/translating.test.ts +++ b/packages/tldraw/src/test/translating.test.ts @@ -166,9 +166,9 @@ describe('When translating...', () => { .pointerMove(1080, 800) jest.advanceTimersByTime(100) editor - .expectShapeToMatch({ id: ids.box1, x: 1300, y: 845.68 }) + .expectShapeToMatch({ id: ids.box1, x: 1320, y: 845.68 }) .pointerUp() - .expectShapeToMatch({ id: ids.box1, x: 1300, y: 845.68 }) + .expectShapeToMatch({ id: ids.box1, x: 1360, y: 870.16 }) }) it('translates multiple shapes', () => { From 02bfcda0fc08cfa59625ecbedb4058bf8f20a089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 13:59:07 +0100 Subject: [PATCH 19/82] Fix e2e tests. --- apps/examples/e2e/tests/test-canvas-events.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/examples/e2e/tests/test-canvas-events.spec.ts b/apps/examples/e2e/tests/test-canvas-events.spec.ts index 072870629..ba610ae66 100644 --- a/apps/examples/e2e/tests/test-canvas-events.spec.ts +++ b/apps/examples/e2e/tests/test-canvas-events.spec.ts @@ -21,6 +21,7 @@ test.describe('Canvas events', () => { await page.mouse.move(200, 200) // to kill any double clicks await page.mouse.move(100, 100) await page.mouse.down() + await page.waitForTimeout(100) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -46,6 +47,7 @@ test.describe('Canvas events', () => { await page.mouse.down() await page.mouse.move(101, 101) await page.mouse.up() + await page.waitForTimeout(100) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -118,6 +120,7 @@ test.describe('Shape events', () => { test('pointer down', async () => { await page.mouse.move(51, 51) await page.mouse.down() + await page.waitForTimeout(100) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -128,6 +131,7 @@ test.describe('Shape events', () => { test('pointer move', async () => { await page.mouse.move(51, 51) await page.mouse.move(52, 52) + await page.waitForTimeout(100) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -139,6 +143,7 @@ test.describe('Shape events', () => { await page.mouse.move(51, 51) await page.mouse.down() await page.mouse.up() + await page.waitForTimeout(100) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', From 612e2833f92ee2e2f6b700c2b5b4bf1ca52cf92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 14:07:53 +0100 Subject: [PATCH 20/82] Fix test. --- apps/examples/e2e/tests/test-shapes.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/examples/e2e/tests/test-shapes.spec.ts b/apps/examples/e2e/tests/test-shapes.spec.ts index b47d20d6b..8e0d1d1f3 100644 --- a/apps/examples/e2e/tests/test-shapes.spec.ts +++ b/apps/examples/e2e/tests/test-shapes.spec.ts @@ -112,6 +112,7 @@ test.describe('Shape Tools', () => { // Click on the page await page.mouse.click(200, 200) + await page.waitForTimeout(100) // We should have a corresponding shape in the page expect(await getAllShapeTypes(page)).toEqual([shape]) From 1a8948c0737d99c0bdd9c0370c91c66da2d30e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 21 Mar 2024 15:03:08 +0100 Subject: [PATCH 21/82] Round. --- packages/tldraw/api-report.md | 8 ++++---- packages/tldraw/api/api.json | 24 ++++++++++++------------ packages/utils/src/lib/throttle.ts | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 2beab6449..ec764b4de 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_3 | typeof Pointing_2)[]; + static children: () => (typeof Idle_2 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_2)[]; + static children: () => (typeof Drawing | typeof Idle_3)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 48b0f4a74..24aa6c0af 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,15 +3799,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "DrawShapeTool", - "canonicalReference": "tldraw!DrawShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3826,6 +3817,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "HighlightShapeTool", + "canonicalReference": "tldraw!HighlightShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", diff --git a/packages/utils/src/lib/throttle.ts b/packages/utils/src/lib/throttle.ts index 323236ec4..4733a47d9 100644 --- a/packages/utils/src/lib/throttle.ts +++ b/packages/utils/src/lib/throttle.ts @@ -6,7 +6,7 @@ const isTest = () => const fpsQueue: Array<() => void> = [] const targetFps = 60 -const targetTimePerFrame = 1000 / targetFps +const targetTimePerFrame = Math.ceil(1000 / targetFps) let frame: number | undefined let time = 0 let last = 0 From 976572312428e18919bec34f6b940c5566948e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 22 Mar 2024 09:56:50 +0100 Subject: [PATCH 22/82] Api update? --- packages/tldraw/api-report.md | 8 ++++---- packages/tldraw/api/api.json | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index ec764b4de..2beab6449 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -404,7 +404,7 @@ export const DefaultQuickActions: NamedExoticComponent; export function DefaultQuickActionsContent(): JSX_2.Element | undefined; // @public (undocumented) -export const defaultShapeTools: (typeof ArrowShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof HighlightShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; +export const defaultShapeTools: (typeof ArrowShapeTool | typeof DrawShapeTool | typeof FrameShapeTool | typeof GeoShapeTool | typeof LineShapeTool | typeof NoteShapeTool | typeof TextShapeTool)[]; // @public (undocumented) export const defaultShapeUtils: TLAnyShapeUtilConstructor[]; @@ -450,7 +450,7 @@ export function downsizeImage(blob: Blob, width: number, height: number, opts?: // @public (undocumented) export class DrawShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) @@ -676,7 +676,7 @@ export function FrameToolbarItem(): JSX_2.Element; // @public (undocumented) export class GeoShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Idle_2 | typeof Pointing_2)[]; + static children: () => (typeof Idle_3 | typeof Pointing_2)[]; // (undocumented) static id: string; // (undocumented) @@ -871,7 +871,7 @@ export function HexagonToolbarItem(): JSX_2.Element; // @public (undocumented) export class HighlightShapeTool extends StateNode { // (undocumented) - static children: () => (typeof Drawing | typeof Idle_3)[]; + static children: () => (typeof Drawing | typeof Idle_2)[]; // (undocumented) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 24aa6c0af..48b0f4a74 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -3799,6 +3799,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "DrawShapeTool", + "canonicalReference": "tldraw!DrawShapeTool:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "FrameShapeTool", @@ -3817,15 +3826,6 @@ "kind": "Content", "text": " | typeof " }, - { - "kind": "Reference", - "text": "HighlightShapeTool", - "canonicalReference": "tldraw!HighlightShapeTool:class" - }, - { - "kind": "Content", - "text": " | typeof " - }, { "kind": "Reference", "text": "LineShapeTool", @@ -4460,7 +4460,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", @@ -7922,7 +7922,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_2:class" + "canonicalReference": "tldraw!~Idle_3:class" }, { "kind": "Content", @@ -9732,7 +9732,7 @@ { "kind": "Reference", "text": "Idle", - "canonicalReference": "tldraw!~Idle_3:class" + "canonicalReference": "tldraw!~Idle_2:class" }, { "kind": "Content", From e84f3ce0fa5a1e5ceb8775c98ed8d6cf80a18a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 22 Mar 2024 10:06:34 +0100 Subject: [PATCH 23/82] Reduce timeout. --- apps/examples/e2e/tests/test-canvas-events.spec.ts | 10 +++++----- apps/examples/e2e/tests/test-shapes.spec.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/examples/e2e/tests/test-canvas-events.spec.ts b/apps/examples/e2e/tests/test-canvas-events.spec.ts index ba610ae66..5f5537e68 100644 --- a/apps/examples/e2e/tests/test-canvas-events.spec.ts +++ b/apps/examples/e2e/tests/test-canvas-events.spec.ts @@ -21,7 +21,7 @@ test.describe('Canvas events', () => { await page.mouse.move(200, 200) // to kill any double clicks await page.mouse.move(100, 100) await page.mouse.down() - await page.waitForTimeout(100) + await page.waitForTimeout(20) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -47,7 +47,7 @@ test.describe('Canvas events', () => { await page.mouse.down() await page.mouse.move(101, 101) await page.mouse.up() - await page.waitForTimeout(100) + await page.waitForTimeout(20) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -120,7 +120,7 @@ test.describe('Shape events', () => { test('pointer down', async () => { await page.mouse.move(51, 51) await page.mouse.down() - await page.waitForTimeout(100) + await page.waitForTimeout(20) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -131,7 +131,7 @@ test.describe('Shape events', () => { test('pointer move', async () => { await page.mouse.move(51, 51) await page.mouse.move(52, 52) - await page.waitForTimeout(100) + await page.waitForTimeout(20) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', @@ -143,7 +143,7 @@ test.describe('Shape events', () => { await page.mouse.move(51, 51) await page.mouse.down() await page.mouse.up() - await page.waitForTimeout(100) + await page.waitForTimeout(20) expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({ target: 'canvas', type: 'pointer', diff --git a/apps/examples/e2e/tests/test-shapes.spec.ts b/apps/examples/e2e/tests/test-shapes.spec.ts index 8e0d1d1f3..7602c1bc4 100644 --- a/apps/examples/e2e/tests/test-shapes.spec.ts +++ b/apps/examples/e2e/tests/test-shapes.spec.ts @@ -112,7 +112,7 @@ test.describe('Shape Tools', () => { // Click on the page await page.mouse.click(200, 200) - await page.waitForTimeout(100) + await page.waitForTimeout(20) // We should have a corresponding shape in the page expect(await getAllShapeTypes(page)).toEqual([shape]) From c41adb4845e86a8710c12f03e5adb5af2ae301de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 22 Mar 2024 10:07:20 +0100 Subject: [PATCH 24/82] Restore line. --- packages/editor/src/lib/editor/Editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d5d971e03..0a891ee09 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8572,6 +8572,7 @@ export class Editor extends EventEmitter { * ``` * * @param info - The event info. + * * @public */ dispatch = (info: TLEventInfo): this => { From d21ef7d8b71bc5d32ef02263cf4bc2d7069c6df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 22 Mar 2024 10:09:08 +0100 Subject: [PATCH 25/82] Remove empty line. --- packages/tldraw/src/test/TestEditor.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index 59afd5532..54514c840 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -337,7 +337,6 @@ export class TestEditor extends Editor { ...this.getPointerEventInfo(x, y, options, modifiers), name: 'pointer_move', pagePoint: new Vec(sx / cz - cx, sy / cz - cy), - coalescedInfo: [], }).forceTick() return this From 5c29ce380f7657681dd46ff9a2837050c2283b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Fri, 22 Mar 2024 10:46:13 +0100 Subject: [PATCH 26/82] Fix panning while translating. --- packages/editor/src/lib/editor/Editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 0a891ee09..eb4f52127 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2495,6 +2495,7 @@ export class Editor extends EventEmitter { if (!this.getInstanceState().canMoveCamera) return this const { x: cx, y: cy, z: cz } = this.getCamera() this.setCamera({ x: cx + offset.x / cz, y: cy + offset.y / cz, z: cz }, animation) + this._flushEventsForTick(0) return this } From 585a939d1ada1f45174e267c476329207896eba9 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 23 Mar 2024 17:03:41 +0000 Subject: [PATCH 27/82] oko --- apps/examples/src/misc/develop.tsx | 29 ++- packages/editor/api-report.md | 11 +- packages/editor/api/api.json | 198 +++++++++++++++- packages/editor/src/lib/TldrawEditor.tsx | 7 +- packages/editor/src/lib/constants.ts | 13 ++ packages/editor/src/lib/editor/Editor.ts | 211 +++++++++++++++--- .../editor/src/lib/editor/types/misc-types.ts | 21 ++ 7 files changed, 453 insertions(+), 37 deletions(-) diff --git a/apps/examples/src/misc/develop.tsx b/apps/examples/src/misc/develop.tsx index db14ab8cd..00400f6d7 100644 --- a/apps/examples/src/misc/develop.tsx +++ b/apps/examples/src/misc/develop.tsx @@ -1,14 +1,39 @@ -import { Tldraw } from 'tldraw' +import { TLGeoShape, Tldraw } from 'tldraw' import 'tldraw/tldraw.css' export default function Develop() { return (
{ ;(window as any).app = editor ;(window as any).editor = editor + if (editor.getCurrentPageShapeIds().size === 0) { + editor.createShape({ + type: 'geo', + x: 0, + y: 0, + props: { + w: 800, + h: 800, + }, + }) + } + }} + cameraOptions={{ + bounds: { + x: 0, + y: 0, + w: 800, + h: 800, + }, + padding: [0, 100], + panSpeed: 1, + zoomSteps: [0.5, 0.75, 1, 1.5, 2], + zoomMax: 2, + zoomMin: 0.5, + zoomSpeed: 1, }} />
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 4317f6d42..fbb62a299 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -570,7 +570,7 @@ export class Edge2d extends Geometry2d { // @public (undocumented) export class Editor extends EventEmitter { - constructor({ store, user, shapeUtils, tools, getContainer, initialState, inferDarkMode, }: TLEditorOptions); + constructor({ store, user, shapeUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: TLEditorOptions); addOpenMenu(id: string): this; alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this; animateShape(partial: null | TLShapePartial | undefined, animationOptions?: TLAnimationOptions): this; @@ -666,6 +666,8 @@ export class Editor extends EventEmitter { getAssetForExternalContent(info: TLExternalAssetContent): Promise; getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[]; getCamera(): TLCamera; + // (undocumented) + getCameraOptions(): TLCameraOptions; getCameraState(): "idle" | "moving"; getCanRedo(): boolean; getCanUndo(): boolean; @@ -860,6 +862,8 @@ export class Editor extends EventEmitter { sendBackward(shapes: TLShape[] | TLShapeId[]): this; sendToBack(shapes: TLShape[] | TLShapeId[]): this; setCamera(point: VecLike, animation?: TLAnimationOptions): this; + // (undocumented) + setCameraOptions(options: TLCameraOptions): void; setCroppingShape(shape: null | TLShape | TLShapeId): this; setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this; setCurrentTool(id: string, info?: {}): this; @@ -902,6 +906,8 @@ export class Editor extends EventEmitter { // (undocumented) ungroupShapes(ids: TLShape[]): this; updateAssets(assets: TLAssetPartial[]): this; + // (undocumented) + updateCameraOnMount(): void; updateCurrentPageState(partial: Partial>, historyOptions?: TLCommandHistoryOptions): this; updateDocumentSettings(settings: Partial): this; updateInstanceState(partial: Partial>, historyOptions?: TLCommandHistoryOptions): this; @@ -2026,6 +2032,8 @@ export const TldrawEditor: React_2.NamedExoticComponent; // @public export interface TldrawEditorBaseProps { autoFocus?: boolean; + // (undocumented) + cameraOptions?: Partial; children?: ReactNode; className?: string; components?: TLEditorComponents; @@ -2056,6 +2064,7 @@ export type TLEditorComponents = Partial<{ // @public (undocumented) export interface TLEditorOptions { + cameraOptions?: Partial; getContainer: () => HTMLElement; inferDarkMode?: boolean; initialState?: string; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index bda751816..29f2a4adb 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -7447,7 +7447,7 @@ "excerptTokens": [ { "kind": "Content", - "text": "constructor({ store, user, shapeUtils, tools, getContainer, initialState, inferDarkMode, }: " + "text": "constructor({ store, user, shapeUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: " }, { "kind": "Reference", @@ -7464,7 +7464,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "{ store, user, shapeUtils, tools, getContainer, initialState, inferDarkMode, }", + "parameterName": "{ store, user, shapeUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -10053,6 +10053,38 @@ "isAbstract": false, "name": "getCamera" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "getCameraOptions(): " + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getCameraOptions" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCameraState:member(1)", @@ -16956,6 +16988,55 @@ "isAbstract": false, "name": "setCamera" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#setCameraOptions:member(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "setCameraOptions(options: " + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "options", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "setCameraOptions" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#setCroppingShape:member(1)", @@ -18750,6 +18831,37 @@ "isAbstract": false, "name": "updateAssets" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#updateCameraOnMount:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "updateCameraOnMount(): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "updateCameraOnMount" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#updateCurrentPageState:member(1)", @@ -36420,6 +36532,47 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!TldrawEditorBaseProps#cameraOptions:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "cameraOptions?: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "cameraOptions", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TldrawEditorBaseProps#children:member", @@ -36855,6 +37008,47 @@ "name": "TLEditorOptions", "preserveMemberOrder": false, "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!TLEditorOptions#cameraOptions:member", + "docComment": "/**\n * Options for the editor's camera.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "cameraOptions?: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "cameraOptions", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TLEditorOptions#getContainer:member", diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 976415fa5..5bd615b3a 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -18,6 +18,7 @@ import { TLUser, createTLUser } from './config/createTLUser' import { TLAnyShapeUtilConstructor } from './config/defaultShapes' import { Editor } from './editor/Editor' import { TLStateNodeConstructor } from './editor/tools/StateNode' +import { TLCameraOptions } from './editor/types/misc-types' import { ContainerProvider, useContainer } from './hooks/useContainer' import { useCursor } from './hooks/useCursor' import { useDarkMode } from './hooks/useDarkMode' @@ -113,6 +114,8 @@ export interface TldrawEditorBaseProps { * Whether to infer dark mode from the user's OS. Defaults to false. */ inferDarkMode?: boolean + + cameraOptions?: Partial } /** @@ -265,6 +268,7 @@ function TldrawEditorWithReadyStore({ initialState, autoFocus = true, inferDarkMode, + cameraOptions, }: Required< TldrawEditorProps & { store: TLStore @@ -285,13 +289,14 @@ function TldrawEditorWithReadyStore({ user, initialState, inferDarkMode, + cameraOptions, }) setEditor(editor) return () => { editor.dispose() } - }, [container, shapeUtils, tools, store, user, initialState, inferDarkMode]) + }, [container, shapeUtils, tools, store, user, initialState, inferDarkMode, cameraOptions]) const crashingError = useSyncExternalStore( useCallback( diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 2fb880568..ae34d2444 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -1,3 +1,4 @@ +import { TLCameraOptions } from './editor/types/misc-types' import { EASINGS } from './primitives/easings' /** @internal */ @@ -17,6 +18,18 @@ export const MIN_ZOOM = 0.1 /** @internal */ export const MAX_ZOOM = 8 +/** @internal */ +export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { + bounds: null, + padding: 0, + zoomMax: 8, + zoomMin: 0.1, + zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], + zoomSpeed: 1, + panSpeed: 1, + isLocked: false, +} + /** @internal */ export const FOLLOW_CHASE_PROPORTION = 0.5 /** @internal */ diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 6b2be38b6..27aecee83 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -59,6 +59,7 @@ import { sortByIndex, structuredClone, } from '@tldraw/utils' +import { Number } from 'core-js' import { EventEmitter } from 'eventemitter3' import { TLUser, createTLUser } from '../config/createTLUser' import { checkShapesAndAddCore } from '../config/defaultShapes' @@ -70,6 +71,7 @@ import { COARSE_DRAG_DISTANCE, COLLABORATOR_IDLE_TIMEOUT, DEFAULT_ANIMATION_OPTIONS, + DEFAULT_CAMERA_OPTIONS, DRAG_DISTANCE, FOLLOW_CHASE_PAN_SNAP, FOLLOW_CHASE_PAN_UNSNAP, @@ -83,7 +85,6 @@ import { MAX_ZOOM, MIN_ZOOM, SVG_PADDING, - ZOOMS, } from '../constants' import { Box } from '../primitives/Box' import { Mat, MatLike, MatModel } from '../primitives/Mat' @@ -130,7 +131,7 @@ import { } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' import { TLCommandHistoryOptions } from './types/history-types' -import { OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types' +import { OptionalKeys, RequiredKeys, TLCameraOptions, TLSvgOptions } from './types/misc-types' import { TLResizeHandle } from './types/selection-types' /** @public */ @@ -182,6 +183,10 @@ export interface TLEditorOptions { * Whether to infer dark mode from the user's system preferences. Defaults to false. */ inferDarkMode?: boolean + /** + * Options for the editor's camera. + */ + cameraOptions?: Partial } /** @public */ @@ -192,6 +197,7 @@ export class Editor extends EventEmitter { shapeUtils, tools, getContainer, + cameraOptions, initialState, inferDarkMode, }: TLEditorOptions) { @@ -201,6 +207,8 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) + this._cameraOptions = { ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions } + this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) this.getContainer = getContainer ?? (() => document.body) @@ -2060,6 +2068,37 @@ export class Editor extends EventEmitter { /* --------------------- Camera --------------------- */ + private _cameraOptions: TLCameraOptions + + /** @public */ + getCameraOptions() { + return this._cameraOptions + } + + private getNaturalZoom() { + const cameraOptions = this.getCameraOptions() + let naturalZoom = 1 + if (cameraOptions.bounds) { + const { padding } = cameraOptions + const vsb = this.getViewportScreenBounds() + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) + const bounds = Box.From(cameraOptions.bounds) + bounds.x -= px + bounds.y -= py + bounds.w += px * 2 + bounds.h += py * 2 + naturalZoom = Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + } + return naturalZoom + } + + /** @public */ + setCameraOptions(options: TLCameraOptions) { + this._cameraOptions = options + } + /** @internal */ @computed private getCameraId() { @@ -2084,10 +2123,78 @@ export class Editor extends EventEmitter { return this.getCamera().z } + private fixCamera(point: VecLike) { + const currentCamera = this.getCamera() + if (!Number.isFinite(point.x)) point.x = 0 + if (!Number.isFinite(point.y)) point.y = 0 + if (point.z === undefined || !Number.isFinite(point.z)) point.z = currentCamera.z + + const cameraOptions = this.getCameraOptions() + + // If bounds are provided, then we'll keep those bounds on screen + if (cameraOptions.bounds) { + const { zoomMax, zoomMin, padding } = cameraOptions + + const vsb = this.getViewportScreenBounds() + const vpb = this.getViewportPageBounds() + + // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + // Clamp padding to half the viewport size on either dimension + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) + + // Expand the bounds by the padding + const bounds = Box.From(cameraOptions.bounds) + bounds.x -= px + bounds.y -= py + bounds.w += px * 2 + bounds.h += py * 2 + + // The natural zoom is the zoom at which the expanded bounds (with padding) would fit the viewport + const zx = vsb.w / bounds.width + const zy = vsb.h / bounds.height + const naturalZoom = Math.min(zx, zy) + const maxZ = naturalZoom * zoomMax + const minZ = naturalZoom * zoomMin + + if (point.z < minZ || point.z > maxZ) { + // We're trying to zoom out past the minimum zoom level, + // or in past the maximum zoom level, so stop the camera + point.x = currentCamera.x + point.y = currentCamera.y + point.z = point.z < minZ ? minZ : maxZ + } + + if (point.z > zx) { + const l = Math.max(0, bounds.minX - vpb.minX) + const r = Math.max(0, vpb.maxX - bounds.maxX) + if (l > 0) point.x -= l + if (r > 0) point.x += r + } else { + point.x -= bounds.midX - vpb.midX + } + + if (point.z > zy) { + const t = Math.max(0, bounds.minY - vpb.minY) + const b = Math.max(0, vpb.maxY - bounds.maxY) + if (t > 0) point.y -= t + if (b > 0) point.y += b + } else { + point.y -= bounds.midY - vpb.midY + } + } + + return point + } + /** @internal */ private _setCamera(point: VecLike): this { const currentCamera = this.getCamera() + // Apply any adjustments based on the camera options + point = this.fixCamera(point) + if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) { return this } @@ -2136,10 +2243,6 @@ export class Editor extends EventEmitter { * @public */ setCamera(point: VecLike, animation?: TLAnimationOptions): this { - const x = Number.isFinite(point.x) ? point.x : 0 - const y = Number.isFinite(point.y) ? point.y : 0 - const z = Number.isFinite(point.z) ? point.z! : this.getZoomLevel() - // Stop any camera animations this.stopCameraAnimation() @@ -2150,9 +2253,12 @@ export class Editor extends EventEmitter { if (animation) { const { width, height } = this.getViewportScreenBounds() - return this._animateToViewport(new Box(-x, -y, width / z, height / z), animation) + return this._animateToViewport( + new Box(-point.x, -point.y, width / point.z!, height / point.z!), + animation + ) } else { - this._setCamera({ x, y, z }) + this._setCamera(point) } return this @@ -2173,7 +2279,7 @@ export class Editor extends EventEmitter { * @public */ centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const { width: pw, height: ph } = this.getViewportPageBounds() @@ -2221,8 +2327,6 @@ export class Editor extends EventEmitter { * @public */ zoomToFit(animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this - const ids = [...this.getCurrentPageShapeIds()] if (ids.length <= 0) return this @@ -2247,7 +2351,7 @@ export class Editor extends EventEmitter { * @public */ resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() const { x, y } = point @@ -2274,15 +2378,20 @@ export class Editor extends EventEmitter { * @public */ zoomIn(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() - let zoom = MAX_ZOOM + const { zoomMax, zoomSteps } = this.getCameraOptions() + if (zoomSteps === null || zoomSteps.length <= 1) return this - for (let i = 1; i < ZOOMS.length; i++) { - const z1 = ZOOMS[i - 1] - const z2 = ZOOMS[i] + const naturalZoom = this.getNaturalZoom() + + let zoom = zoomMax * naturalZoom + + for (let i = 1; i < zoomSteps.length; i++) { + const z1 = zoomSteps[i - 1] * naturalZoom + const z2 = zoomSteps[i] * naturalZoom if (z2 - cz <= (z2 - z1) / 2) continue zoom = z2 break @@ -2312,15 +2421,20 @@ export class Editor extends EventEmitter { * @public */ zoomOut(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this + + const { zoomMin, zoomSteps } = this.getCameraOptions() + if (zoomSteps === null || zoomSteps.length <= 1) return this + + const naturalZoom = this.getNaturalZoom() const { x: cx, y: cy, z: cz } = this.getCamera() - let zoom = MIN_ZOOM + let zoom = zoomMin * naturalZoom - for (let i = ZOOMS.length - 1; i > 0; i--) { - const z1 = ZOOMS[i - 1] - const z2 = ZOOMS[i] + for (let i = zoomSteps.length - 1; i > 0; i--) { + const z1 = zoomSteps[i - 1] * naturalZoom + const z2 = zoomSteps[i] * naturalZoom if (z2 - cz >= (z2 - z1) / 2) continue zoom = z1 break @@ -2353,7 +2467,7 @@ export class Editor extends EventEmitter { * @public */ zoomToSelection(animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const selectionPageBounds = this.getSelectionPageBounds() if (!selectionPageBounds) return this @@ -2375,7 +2489,7 @@ export class Editor extends EventEmitter { * @public */ panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this if (ids.length <= 0) return this const selectionBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id)))) @@ -2440,7 +2554,7 @@ export class Editor extends EventEmitter { bounds: Box, opts?: { targetZoom?: number; inset?: number } & TLAnimationOptions ): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const viewportScreenBounds = this.getViewportScreenBounds() @@ -2484,7 +2598,7 @@ export class Editor extends EventEmitter { * @param animation - The animation options. */ pan(offset: VecLike, animation?: TLAnimationOptions): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() this.setCamera({ x: cx + offset.x / cz, y: cy + offset.y / cz, z: cz }, animation) return this @@ -2593,7 +2707,7 @@ export class Editor extends EventEmitter { speedThreshold?: number } ): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this this.stopCameraAnimation() @@ -2689,7 +2803,7 @@ export class Editor extends EventEmitter { * @public */ animateToShape(shapeId: TLShapeId, opts: TLAnimationOptions = DEFAULT_ANIMATION_OPTIONS): this { - if (!this.getInstanceState().canMoveCamera) return this + if (this.getCameraOptions().isLocked) return this const activeArea = this.getViewportScreenBounds().clone().expandBy(-32) const viewportAspectRatio = activeArea.width / activeArea.height @@ -2768,6 +2882,7 @@ export class Editor extends EventEmitter { { screenBounds: screenBounds.toJson(), insets }, { squashing: true, ephemeral: true } ) + this.updateCameraOnMount() } else { if (center && !this.getInstanceState().followingUserId) { // Get the page center before the change, make the change, and restore it @@ -2778,11 +2893,12 @@ export class Editor extends EventEmitter { ) this.centerOnPoint(before) } else { - // Otherwise, + // Otherwise... this.updateInstanceState( { screenBounds: screenBounds.toJson(), insets }, { squashing: true, ephemeral: true } ) + this._setCamera({ ...this.getCamera() }) } } } @@ -2793,6 +2909,39 @@ export class Editor extends EventEmitter { return this } + updateCameraOnMount() { + const vsb = this.getViewportScreenBounds() + const cameraOptions = this.getCameraOptions() + + if (cameraOptions.bounds) { + // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) + const { padding } = cameraOptions + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + // Clamp padding to half the viewport size on either dimension + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) + + // Expand the bounds by the padding + const bounds = Box.From(cameraOptions.bounds) + bounds.x -= px + bounds.y -= py + bounds.w += px * 2 + bounds.h += py * 2 + + // The natural zoom is the zoom at which the expanded bounds (with padding) would fit the viewport + const zoom = Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + + // Keeping the current screen center, adjust zoom to the natural zoom + const { x, y } = vsb.center + const { x: cx, y: cy, z: cz } = this.getCamera() + this.setCamera({ + x: cx + (x / zoom - x) - (x / cz - x), + y: cy + (y / zoom - y) - (y / cz - y), + z: zoom, + }) + } + } + /** * The bounds of the editor's viewport in screen space. * @@ -8617,7 +8766,7 @@ export class Editor extends EventEmitter { switch (type) { case 'pinch': { - if (!this.getInstanceState().canMoveCamera) return + if (this.getCameraOptions().isLocked) return this._updateInputsFromEvent(info) switch (info.name) { @@ -8684,7 +8833,7 @@ export class Editor extends EventEmitter { } } case 'wheel': { - if (!this.getInstanceState().canMoveCamera) return + if (this.getCameraOptions().isLocked) return this._updateInputsFromEvent(info) diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 6851e726d..fdf92c509 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -1,3 +1,4 @@ +import { BoxModel } from '@tldraw/tlschema' import { Box } from '../../primitives/Box' /** @public */ @@ -14,3 +15,23 @@ export type TLSvgOptions = { darkMode?: boolean preserveAspectRatio: React.SVGAttributes['preserveAspectRatio'] } + +/** @public */ +export type TLCameraOptions = { + /** The bounds of the content */ + bounds: BoxModel | null + /** The padding around the bounds */ + padding: number | number[] + /** The speed of a scroll wheel / trackpad pan */ + panSpeed: number + /** The speed of a scroll wheel / trackpad zoom */ + zoomSpeed: number + /** The steps that a user can zoom between with zoom in / zoom out (zoom factors) */ + zoomSteps: number[] | null + /** A minimum zoom factor (e.g. .5x of the zoom at which the shape is fully on screen) */ + zoomMin: number + /** A maximum zoom factor (e.g. 2x of the zoom at which the shape is fully on screen) */ + zoomMax: number + /** Whether the camera is locked */ + isLocked: boolean +} From 96c7ff4255abf9dc9508ef03d1aa32e8358c5ea8 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 24 Mar 2024 11:16:09 +0000 Subject: [PATCH 28/82] add elastic option --- packages/editor/src/lib/constants.ts | 1 + packages/editor/src/lib/editor/types/misc-types.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index ae34d2444..fd322328c 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -28,6 +28,7 @@ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { zoomSpeed: 1, panSpeed: 1, isLocked: false, + elastic: 0, } /** @internal */ diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index fdf92c509..a9b3a6fe0 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -22,6 +22,8 @@ export type TLCameraOptions = { bounds: BoxModel | null /** The padding around the bounds */ padding: number | number[] + /** The degree of elasticity */ + elastic: number /** The speed of a scroll wheel / trackpad pan */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom */ From a147591bc44639127e5b241a2d890d06b18e0923 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 24 Mar 2024 12:00:18 +0000 Subject: [PATCH 29/82] fix zoom --- packages/editor/src/lib/constants.ts | 1 - packages/editor/src/lib/editor/Editor.ts | 126 ++++++++++-------- .../editor/src/lib/editor/types/misc-types.ts | 2 - 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index fd322328c..ae34d2444 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -28,7 +28,6 @@ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { zoomSpeed: 1, panSpeed: 1, isLocked: false, - elastic: 0, } /** @internal */ diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 27aecee83..686f3cc10 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2123,78 +2123,86 @@ export class Editor extends EventEmitter { return this.getCamera().z } - private fixCamera(point: VecLike) { + /** @internal */ + private _setCamera(point: VecLike, opts?: { force: boolean }): this { const currentCamera = this.getCamera() - if (!Number.isFinite(point.x)) point.x = 0 - if (!Number.isFinite(point.y)) point.y = 0 - if (point.z === undefined || !Number.isFinite(point.z)) point.z = currentCamera.z - const cameraOptions = this.getCameraOptions() + // If force is true, then we'll set the camera to the point regardless of + // the camera options, so that we can handle gestures that permit elasticity + // or decay. + if (!opts?.force) { + // Apply any adjustments based on the camera options + const currentCamera = this.getCamera() + if (!Number.isFinite(point.x)) point.x = 0 + if (!Number.isFinite(point.y)) point.y = 0 + if (point.z === undefined || !Number.isFinite(point.z)) point.z = currentCamera.z - // If bounds are provided, then we'll keep those bounds on screen - if (cameraOptions.bounds) { - const { zoomMax, zoomMin, padding } = cameraOptions + const cameraOptions = this.getCameraOptions() - const vsb = this.getViewportScreenBounds() - const vpb = this.getViewportPageBounds() + // If bounds are provided, then we'll keep those bounds on screen + if (cameraOptions.bounds) { + const { zoomMax, zoomMin, padding } = cameraOptions - // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] - // Clamp padding to half the viewport size on either dimension - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) + const vsb = this.getViewportScreenBounds() + const vpb = this.getViewportPageBounds() - // Expand the bounds by the padding - const bounds = Box.From(cameraOptions.bounds) - bounds.x -= px - bounds.y -= py - bounds.w += px * 2 - bounds.h += py * 2 + // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + // Clamp padding to half the viewport size on either dimension + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) - // The natural zoom is the zoom at which the expanded bounds (with padding) would fit the viewport - const zx = vsb.w / bounds.width - const zy = vsb.h / bounds.height - const naturalZoom = Math.min(zx, zy) - const maxZ = naturalZoom * zoomMax - const minZ = naturalZoom * zoomMin + // Expand the bounds by the padding + const bounds = Box.From(cameraOptions.bounds) + bounds.x -= px + bounds.y -= py + bounds.w += px * 2 + bounds.h += py * 2 - if (point.z < minZ || point.z > maxZ) { - // We're trying to zoom out past the minimum zoom level, - // or in past the maximum zoom level, so stop the camera - point.x = currentCamera.x - point.y = currentCamera.y - point.z = point.z < minZ ? minZ : maxZ - } + // For each axis, the "natural zoom" is the zoom at + // which the expanded bounds (with padding) would fit + // the current viewport screen bounds. + const zx = vsb.w / bounds.width + const zy = vsb.h / bounds.height - if (point.z > zx) { - const l = Math.max(0, bounds.minX - vpb.minX) - const r = Math.max(0, vpb.maxX - bounds.maxX) - if (l > 0) point.x -= l - if (r > 0) point.x += r - } else { - point.x -= bounds.midX - vpb.midX - } + // The min and max zooms are factors of the smaller natural zoom axis + const minNaturalZoom = Math.min(zx, zy) + const maxZ = zoomMax * minNaturalZoom + const minZ = zoomMin * minNaturalZoom - if (point.z > zy) { - const t = Math.max(0, bounds.minY - vpb.minY) - const b = Math.max(0, vpb.maxY - bounds.maxY) - if (t > 0) point.y -= t - if (b > 0) point.y += b - } else { - point.y -= bounds.midY - vpb.midY + if (point.z < minZ || point.z > maxZ) { + // We're trying to zoom out past the minimum zoom level, + // or in past the maximum zoom level, so stop the camera + // but keep the current center + + const cxA = -currentCamera.x + vsb.w / currentCamera.z / 2 + const cyA = -currentCamera.y + vsb.h / currentCamera.z / 2 + + point.z = point.z < minZ ? minZ : maxZ + + const cxB = -currentCamera.x + vsb.w / point.z / 2 + const cyB = -currentCamera.y + vsb.h / point.z / 2 + + point.x = currentCamera.x + cxB - cxA + point.y = currentCamera.y + cyB - cyA + } + + point.x += + point.z > zx + ? // We're past the natural zoom for the x axis, so cancel any overlaps + Math.max(0, vpb.maxX - bounds.maxX) - Math.max(0, bounds.minX - vpb.minX) + : // We're below the natural zoom for the x axis, so center it + vpb.midX - bounds.midX + + point.y += + point.z > zy + ? // We're past the natural zoom for the y axis, so cancel any overlaps + Math.max(0, vpb.maxY - bounds.maxY) - Math.max(0, bounds.minY - vpb.minY) + : // We're below the natural zoom for the y axis, so center it + vpb.midY - bounds.midY } } - return point - } - - /** @internal */ - private _setCamera(point: VecLike): this { - const currentCamera = this.getCamera() - - // Apply any adjustments based on the camera options - point = this.fixCamera(point) - if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) { return this } diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index a9b3a6fe0..fdf92c509 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -22,8 +22,6 @@ export type TLCameraOptions = { bounds: BoxModel | null /** The padding around the bounds */ padding: number | number[] - /** The degree of elasticity */ - elastic: number /** The speed of a scroll wheel / trackpad pan */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom */ From c2eeb6d3ebe0da12708a77fd3ba0b3ce05fb820f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 24 Mar 2024 17:32:02 +0000 Subject: [PATCH 30/82] hand tool --- packages/editor/api-report.md | 5 +- packages/editor/api/api.json | 14 +++-- packages/editor/src/lib/constants.ts | 1 + packages/editor/src/lib/editor/Editor.ts | 55 ++++++++-------- .../editor/src/lib/editor/types/misc-types.ts | 6 +- .../tools/HandTool/childStates/Dragging.ts | 63 +++++++++++++++++-- 6 files changed, 105 insertions(+), 39 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index a0e64f698..5f41c426b 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -861,7 +861,10 @@ export class Editor extends EventEmitter { selectNone(): this; sendBackward(shapes: TLShape[] | TLShapeId[]): this; sendToBack(shapes: TLShape[] | TLShapeId[]): this; - setCamera(point: VecLike, animation?: TLAnimationOptions): this; + setCamera(point: VecLike, opts?: TLAnimationOptions & { + immediate?: boolean; + force?: boolean; + }): this; // (undocumented) setCameraOptions(options: TLCameraOptions): void; setCroppingShape(shape: null | TLShape | TLShapeId): this; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 212c6ff6f..905480727 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -16938,13 +16938,17 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", "text": "TLAnimationOptions", "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" }, + { + "kind": "Content", + "text": " & {\n immediate?: boolean;\n force?: boolean;\n }" + }, { "kind": "Content", "text": "): " @@ -16960,8 +16964,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 6, + "endIndex": 7 }, "releaseTag": "Public", "isProtected": false, @@ -16976,10 +16980,10 @@ "isOptional": false }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, - "endIndex": 4 + "endIndex": 5 }, "isOptional": true } diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index ae34d2444..c848502e4 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -22,6 +22,7 @@ export const MAX_ZOOM = 8 export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { bounds: null, padding: 0, + elastic: 40, zoomMax: 8, zoomMin: 0.1, zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 8789683f4..bc035fb71 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -60,7 +60,6 @@ import { structuredClone, } from '@tldraw/utils' import { Number } from 'core-js' -import immediate from 'core-js/web/immediate' import { EventEmitter } from 'eventemitter3' import { TLUser, createTLUser } from '../config/createTLUser' import { checkShapesAndAddCore } from '../config/defaultShapes' @@ -2126,8 +2125,9 @@ export class Editor extends EventEmitter { } /** @internal */ - private _setCamera(point: VecLike, opts?: { immediate?: boolean; force?: boolean }): this { + private _setCamera(_point: VecLike, opts?: { immediate?: boolean; force?: boolean }): this { const currentCamera = this.getCamera() + const point = Vec.From(_point) // If force is true, then we'll set the camera to the point regardless of // the camera options, so that we can handle gestures that permit elasticity @@ -2146,7 +2146,6 @@ export class Editor extends EventEmitter { const { zoomMax, zoomMin, padding } = cameraOptions const vsb = this.getViewportScreenBounds() - const vpb = this.getViewportPageBounds() // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) let [py, px] = Array.isArray(padding) ? padding : [padding, padding] @@ -2156,16 +2155,13 @@ export class Editor extends EventEmitter { // Expand the bounds by the padding const bounds = Box.From(cameraOptions.bounds) - bounds.x -= px - bounds.y -= py - bounds.w += px * 2 - bounds.h += py * 2 // For each axis, the "natural zoom" is the zoom at // which the expanded bounds (with padding) would fit - // the current viewport screen bounds. - const zx = vsb.w / bounds.width - const zy = vsb.h / bounds.height + // the current viewport screen bounds. Paddings are + // equal to screen pixels at 100% + const zx = (vsb.w - px * 2) / bounds.width + const zy = (vsb.h - py * 2) / bounds.height // The min and max zooms are factors of the smaller natural zoom axis const minNaturalZoom = Math.min(zx, zy) @@ -2189,19 +2185,19 @@ export class Editor extends EventEmitter { point.y = currentCamera.y + cyB - cyA } - point.x += - point.z > zx - ? // We're past the natural zoom for the x axis, so cancel any overlaps - Math.max(0, vpb.maxX - bounds.maxX) - Math.max(0, bounds.minX - vpb.minX) - : // We're below the natural zoom for the x axis, so center it - vpb.midX - bounds.midX + // We're past the natural zoom for the x axis, so clamp it with bounds + padding + // We're below the natural zoom for the x axis, so center it + if (point.z > zx) { + point.x = clamp(point.x, -bounds.maxX + (vsb.w - px) / point.z, bounds.x + px / point.z) + } else { + point.x = vsb.midX / point.z - bounds.midX + } - point.y += - point.z > zy - ? // We're past the natural zoom for the y axis, so cancel any overlaps - Math.max(0, vpb.maxY - bounds.maxY) - Math.max(0, bounds.minY - vpb.minY) - : // We're below the natural zoom for the y axis, so center it - vpb.midY - bounds.midY + if (point.z > zy) { + point.y = clamp(point.y, -bounds.maxY + (vsb.h - py) / point.z, bounds.y + py / point.z) + } else { + point.y = vsb.midY / point.z - bounds.midY + } } } @@ -2232,7 +2228,8 @@ export class Editor extends EventEmitter { button: 0, isPen: this.getInstanceState().isPenMode ?? false, } - if (immediate) { + + if (opts?.immediate) { this._flushEventForTick(event) } else { this.dispatch(event) @@ -2259,7 +2256,10 @@ export class Editor extends EventEmitter { * * @public */ - setCamera(point: VecLike, animation?: TLAnimationOptions): this { + setCamera( + point: VecLike, + opts?: TLAnimationOptions & { immediate?: boolean; force?: boolean } + ): this { // Stop any camera animations this.stopCameraAnimation() @@ -2268,14 +2268,15 @@ export class Editor extends EventEmitter { this.stopFollowingUser() } - if (animation) { + if (opts && (opts.duration || opts.easing)) { const { width, height } = this.getViewportScreenBounds() + // todo: animate this return this._animateToViewport( new Box(-point.x, -point.y, width / point.z!, height / point.z!), - animation + opts ) } else { - this._setCamera(point) + this._setCamera(point, opts) } return this diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index fdf92c509..601c4fef7 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -18,10 +18,12 @@ export type TLSvgOptions = { /** @public */ export type TLCameraOptions = { - /** The bounds of the content */ + /** The bounds of the content (in page space) */ bounds: BoxModel | null - /** The padding around the bounds */ + /** The padding around the bounds (in screen space) */ padding: number | number[] + /** The maximum elastic distance around the bounds */ + elastic: number /** The speed of a scroll wheel / trackpad pan */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom */ diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 8d6e60553..281829225 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -1,9 +1,18 @@ -import { CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec } from '@tldraw/editor' +import { Box, CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec, clamp } from '@tldraw/editor' export class Dragging extends StateNode { static override id = 'dragging' + camera = new Vec() + override onEnter = () => { + const { editor } = this + this.camera = Vec.From(editor.getCamera()) + + editor.stopCameraAnimation() + if (editor.getInstanceState().followingUserId) { + editor.stopFollowingUser() + } this.update() } @@ -24,16 +33,62 @@ export class Dragging extends StateNode { } private update() { - const { currentScreenPoint, previousScreenPoint } = this.editor.inputs + const { editor } = this + const { currentScreenPoint, originScreenPoint } = editor.inputs - const delta = Vec.Sub(currentScreenPoint, previousScreenPoint) + const delta = Vec.Sub(currentScreenPoint, originScreenPoint) - if (Math.abs(delta.x) > 0 || Math.abs(delta.y) > 0) { + const cameraOptions = editor.getCameraOptions() + if (cameraOptions?.bounds) { + const { x: cx, y: cy, z: cz } = this.camera + const point = { x: cx + delta.x / cz, y: cy + delta.y / cz, z: cz } + + const vsb = editor.getViewportScreenBounds() + + const { padding, elastic } = cameraOptions + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) + + const bounds = Box.From(cameraOptions.bounds) + + const zx = (vsb.w - px * 2) / bounds.width + const zy = (vsb.h - py * 2) / bounds.height + + if (point.z > zx) { + const minX = -bounds.maxX + (vsb.w - px) / point.z + const maxX = bounds.x + px / point.z + point.x = clamp( + point.x, + elastic ? minX - (minX - point.x) / 10 : minX, + elastic ? maxX + (point.x - maxX) / 10 : maxX + ) + } else { + const cx = vsb.midX / point.z - bounds.midX + point.x = elastic ? cx - (cx - point.x) / 10 : cx + } + + if (point.z > zy) { + const minY = -bounds.maxY + (vsb.h - py) / point.z + const maxY = bounds.y + py / point.z + point.y = clamp( + point.y, + elastic ? minY - (minY - point.y) / 10 : minY, + elastic ? maxY + (point.y - maxY) / 10 : maxY + ) + } else { + const cy = vsb.midY / point.z - bounds.midY + point.y = elastic ? cy - (cy - point.y) / 10 : cy + } + + this.editor.setCamera(point, { force: true }) + } else { this.editor.pan(delta) } } private complete() { + this.editor.setCamera(this.editor.getCamera()) this.editor.slideCamera({ speed: Math.min(2, this.editor.inputs.pointerVelocity.len()), direction: this.editor.inputs.pointerVelocity, From ae275cf10b41c13345d109be687312ceefb77de6 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 24 Mar 2024 19:04:50 +0000 Subject: [PATCH 31/82] add elastic --- packages/editor/src/lib/constants.ts | 2 +- .../src/lib/tools/HandTool/childStates/Dragging.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index c848502e4..99ba93d58 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -22,7 +22,7 @@ export const MAX_ZOOM = 8 export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { bounds: null, padding: 0, - elastic: 40, + elastic: 0.1, zoomMax: 8, zoomMin: 0.1, zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 281829225..9a662bb5e 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -60,12 +60,12 @@ export class Dragging extends StateNode { const maxX = bounds.x + px / point.z point.x = clamp( point.x, - elastic ? minX - (minX - point.x) / 10 : minX, - elastic ? maxX + (point.x - maxX) / 10 : maxX + elastic ? minX - (minX - point.x) * elastic : minX, + elastic ? maxX + (point.x - maxX) * elastic : maxX ) } else { const cx = vsb.midX / point.z - bounds.midX - point.x = elastic ? cx - (cx - point.x) / 10 : cx + point.x = elastic ? cx - (cx - point.x) * elastic : cx } if (point.z > zy) { @@ -73,12 +73,12 @@ export class Dragging extends StateNode { const maxY = bounds.y + py / point.z point.y = clamp( point.y, - elastic ? minY - (minY - point.y) / 10 : minY, - elastic ? maxY + (point.y - maxY) / 10 : maxY + elastic ? minY - (minY - point.y) * elastic : minY, + elastic ? maxY + (point.y - maxY) * elastic : maxY ) } else { const cy = vsb.midY / point.z - bounds.midY - point.y = elastic ? cy - (cy - point.y) / 10 : cy + point.y = elastic ? cy - (cy - point.y) * elastic : cy } this.editor.setCamera(point, { force: true }) From ab9fe4ed5a9d617c44c6b5e431172d4b8a25d82f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 08:35:11 +0000 Subject: [PATCH 32/82] add fit --- apps/examples/src/misc/develop.tsx | 1 + packages/editor/api-report.md | 8 +++++-- packages/editor/api/api.json | 24 ++++++++++++++++--- packages/editor/src/lib/TldrawEditor.tsx | 2 +- packages/editor/src/lib/constants.ts | 4 +--- packages/editor/src/lib/editor/Editor.ts | 23 ++++++++++++------ .../editor/src/lib/editor/types/misc-types.ts | 24 ++++++++++++------- .../tools/HandTool/childStates/Dragging.ts | 2 +- 8 files changed, 63 insertions(+), 25 deletions(-) diff --git a/apps/examples/src/misc/develop.tsx b/apps/examples/src/misc/develop.tsx index 00400f6d7..617e2ac05 100644 --- a/apps/examples/src/misc/develop.tsx +++ b/apps/examples/src/misc/develop.tsx @@ -22,6 +22,7 @@ export default function Develop() { } }} cameraOptions={{ + fit: 'cover', bounds: { x: 0, y: 0, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 5f41c426b..0a23a054b 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2036,7 +2036,9 @@ export const TldrawEditor: React_2.NamedExoticComponent; export interface TldrawEditorBaseProps { autoFocus?: boolean; // (undocumented) - cameraOptions?: Partial; + cameraOptions?: Partial & { + fit: 'contain' | 'cover' | 'infinite'; + }; children?: ReactNode; className?: string; components?: TLEditorComponents; @@ -2067,7 +2069,9 @@ export type TLEditorComponents = Partial<{ // @public (undocumented) export interface TLEditorOptions { - cameraOptions?: Partial; + cameraOptions?: Partial> & { + fit: TLCameraOptions['fit']; + }; getContainer: () => HTMLElement; inferDarkMode?: boolean; initialState?: string; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 905480727..afeeff052 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36561,7 +36561,7 @@ }, { "kind": "Content", - "text": ">" + "text": "> & {\n fit: 'contain' | 'cover' | 'infinite';\n }" }, { "kind": "Content", @@ -37030,6 +37030,15 @@ "kind": "Content", "text": "<" }, + { + "kind": "Reference", + "text": "Exclude", + "canonicalReference": "!Exclude:type" + }, + { + "kind": "Content", + "text": "<" + }, { "kind": "Reference", "text": "TLCameraOptions", @@ -37037,7 +37046,16 @@ }, { "kind": "Content", - "text": ">" + "text": ", 'fit'>> & {\n fit: " + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + }, + { + "kind": "Content", + "text": "['fit'];\n }" }, { "kind": "Content", @@ -37050,7 +37068,7 @@ "name": "cameraOptions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 9 } }, { diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 5bd615b3a..a4aa3436f 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -115,7 +115,7 @@ export interface TldrawEditorBaseProps { */ inferDarkMode?: boolean - cameraOptions?: Partial + cameraOptions?: Partial & { fit: 'infinite' | 'contain' | 'cover' } } /** diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 99ba93d58..4b5ed0128 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -20,9 +20,7 @@ export const MAX_ZOOM = 8 /** @internal */ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { - bounds: null, - padding: 0, - elastic: 0.1, + fit: 'infinite', zoomMax: 8, zoomMin: 0.1, zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index bc035fb71..d7c309524 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -187,7 +187,7 @@ export interface TLEditorOptions { /** * Options for the editor's camera. */ - cameraOptions?: Partial + cameraOptions?: Partial> & { fit: TLCameraOptions['fit'] } } /** @public */ @@ -208,7 +208,7 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions = { ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions } + this._cameraOptions = { ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions } as TLCameraOptions this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2079,7 +2079,7 @@ export class Editor extends EventEmitter { private getNaturalZoom() { const cameraOptions = this.getCameraOptions() let naturalZoom = 1 - if (cameraOptions.bounds) { + if (cameraOptions.fit !== 'infinite') { const { padding } = cameraOptions const vsb = this.getViewportScreenBounds() let [py, px] = Array.isArray(padding) ? padding : [padding, padding] @@ -2142,7 +2142,11 @@ export class Editor extends EventEmitter { const cameraOptions = this.getCameraOptions() // If bounds are provided, then we'll keep those bounds on screen - if (cameraOptions.bounds) { + if (cameraOptions.fit === 'infinite') { + // constrain the zoom + const { zoomMax, zoomMin } = cameraOptions + point.z = clamp(point.z, zoomMin, zoomMax) + } else { const { zoomMax, zoomMin, padding } = cameraOptions const vsb = this.getViewportScreenBounds() @@ -2164,7 +2168,7 @@ export class Editor extends EventEmitter { const zy = (vsb.h - py * 2) / bounds.height // The min and max zooms are factors of the smaller natural zoom axis - const minNaturalZoom = Math.min(zx, zy) + const minNaturalZoom = cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) const maxZ = zoomMax * minNaturalZoom const minZ = zoomMin * minNaturalZoom @@ -2932,7 +2936,9 @@ export class Editor extends EventEmitter { const vsb = this.getViewportScreenBounds() const cameraOptions = this.getCameraOptions() - if (cameraOptions.bounds) { + if (cameraOptions.fit === 'infinite') { + // noop + } else { // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) const { padding } = cameraOptions let [py, px] = Array.isArray(padding) ? padding : [padding, padding] @@ -2948,7 +2954,10 @@ export class Editor extends EventEmitter { bounds.h += py * 2 // The natural zoom is the zoom at which the expanded bounds (with padding) would fit the viewport - const zoom = Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + const zoom = + cameraOptions.fit === 'contain' + ? Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + : Math.max(vsb.w / bounds.width, vsb.h / bounds.height) // Keeping the current screen center, adjust zoom to the natural zoom const { x, y } = vsb.center diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 601c4fef7..4e11a428d 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -18,22 +18,30 @@ export type TLSvgOptions = { /** @public */ export type TLCameraOptions = { - /** The bounds of the content (in page space) */ - bounds: BoxModel | null - /** The padding around the bounds (in screen space) */ - padding: number | number[] - /** The maximum elastic distance around the bounds */ - elastic: number /** The speed of a scroll wheel / trackpad pan */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom */ zoomSpeed: number /** The steps that a user can zoom between with zoom in / zoom out (zoom factors) */ - zoomSteps: number[] | null + zoomSteps: number[] /** A minimum zoom factor (e.g. .5x of the zoom at which the shape is fully on screen) */ zoomMin: number /** A maximum zoom factor (e.g. 2x of the zoom at which the shape is fully on screen) */ zoomMax: number /** Whether the camera is locked */ isLocked: boolean -} +} & ( + | { + fit: 'infinite' + } + | { + /** The fit logic for the camera. */ + fit: 'contain' | 'cover' + /** The bounds of the content (in page space) */ + bounds: BoxModel + /** The padding around the bounds (in screen space) */ + padding: number | number[] + /** The maximum elastic distance around the bounds */ + elastic: number + } +) diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 9a662bb5e..52bd5a1cc 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -39,7 +39,7 @@ export class Dragging extends StateNode { const delta = Vec.Sub(currentScreenPoint, originScreenPoint) const cameraOptions = editor.getCameraOptions() - if (cameraOptions?.bounds) { + if (cameraOptions.fit !== 'infinite') { const { x: cx, y: cy, z: cz } = this.camera const point = { x: cx + delta.x / cz, y: cy + delta.y / cz, z: cz } From c8864aabb4f910e11d4fa18dedf2438274215e6d Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 10:03:41 +0000 Subject: [PATCH 33/82] ok --- apps/examples/src/misc/develop.tsx | 5 +- packages/editor/api-report.md | 1 + packages/editor/src/lib/constants.ts | 32 ++++++- packages/editor/src/lib/editor/Editor.ts | 92 +++++++++++-------- .../editor/src/lib/editor/types/misc-types.ts | 6 +- 5 files changed, 91 insertions(+), 45 deletions(-) diff --git a/apps/examples/src/misc/develop.tsx b/apps/examples/src/misc/develop.tsx index 617e2ac05..934db8006 100644 --- a/apps/examples/src/misc/develop.tsx +++ b/apps/examples/src/misc/develop.tsx @@ -22,14 +22,15 @@ export default function Develop() { } }} cameraOptions={{ - fit: 'cover', + fit: 'contain', bounds: { x: 0, y: 0, w: 800, h: 800, }, - padding: [0, 100], + origin: [0.5, 0.5], + padding: [50, 100], panSpeed: 1, zoomSteps: [0.5, 0.75, 1, 1.5, 2], zoomMax: 2, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 0a23a054b..cc746f961 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -4,6 +4,7 @@ ```ts +/// /// import { Atom } from '@tldraw/state'; diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 4b5ed0128..5e1e2bc5b 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -18,9 +18,7 @@ export const MIN_ZOOM = 0.1 /** @internal */ export const MAX_ZOOM = 8 -/** @internal */ -export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { - fit: 'infinite', +const DEFAULT_COMMON_CAMERA_OPTIONS = { zoomMax: 8, zoomMin: 0.1, zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], @@ -29,6 +27,34 @@ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { isLocked: false, } +const DEFAULT_FIT_CONTAIN_CAMERA_OPTIONS = { + bounds: { x: 0, y: 0, w: 1200, h: 800 }, + padding: [0, 0], + origin: [0.5, 0.5], + elastic: 0, +} + +/** @internal */ +export const getDefaultCameraOptions = ( + cameraOptions: Partial> & { fit: TLCameraOptions['fit'] } +): TLCameraOptions => { + switch (cameraOptions.fit) { + case 'infinite': { + return { + ...DEFAULT_COMMON_CAMERA_OPTIONS, + ...cameraOptions, + } + } + default: { + return { + ...DEFAULT_COMMON_CAMERA_OPTIONS, + ...DEFAULT_FIT_CONTAIN_CAMERA_OPTIONS, + ...cameraOptions, + } + } + } +} + /** @internal */ export const FOLLOW_CHASE_PROPORTION = 0.5 /** @internal */ diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d7c309524..6177de129 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -71,7 +71,6 @@ import { COARSE_DRAG_DISTANCE, COLLABORATOR_IDLE_TIMEOUT, DEFAULT_ANIMATION_OPTIONS, - DEFAULT_CAMERA_OPTIONS, DRAG_DISTANCE, FOLLOW_CHASE_PAN_SNAP, FOLLOW_CHASE_PAN_UNSNAP, @@ -85,6 +84,7 @@ import { MAX_ZOOM, MIN_ZOOM, SVG_PADDING, + getDefaultCameraOptions, } from '../constants' import { Box } from '../primitives/Box' import { Mat, MatLike, MatModel } from '../primitives/Mat' @@ -208,7 +208,9 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions = { ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions } as TLCameraOptions + this._cameraOptions = cameraOptions?.fit + ? getDefaultCameraOptions(cameraOptions) + : getDefaultCameraOptions({ fit: 'infinite' }) this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2078,21 +2080,23 @@ export class Editor extends EventEmitter { private getNaturalZoom() { const cameraOptions = this.getCameraOptions() - let naturalZoom = 1 - if (cameraOptions.fit !== 'infinite') { - const { padding } = cameraOptions - const vsb = this.getViewportScreenBounds() - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) - const bounds = Box.From(cameraOptions.bounds) - bounds.x -= px - bounds.y -= py - bounds.w += px * 2 - bounds.h += py * 2 - naturalZoom = Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + if (cameraOptions.fit === 'infinite') { + return 1 } - return naturalZoom + + const { padding } = cameraOptions + const vsb = this.getViewportScreenBounds() + let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + py = Math.min(py, vsb.w / 2) + px = Math.min(px, vsb.h / 2) + const bounds = Box.From(cameraOptions.bounds) + bounds.x -= px + bounds.y -= py + bounds.w += px * 2 + bounds.h += py * 2 + return cameraOptions.fit === 'contain' + ? Math.min(vsb.w / bounds.width, vsb.h / bounds.height) + : Math.max(vsb.w / bounds.width, vsb.h / bounds.height) } /** @public */ @@ -2125,7 +2129,10 @@ export class Editor extends EventEmitter { } /** @internal */ - private _setCamera(_point: VecLike, opts?: { immediate?: boolean; force?: boolean }): this { + private _setCamera( + _point: VecLike, + opts?: { immediate?: boolean; force?: boolean; initial?: boolean } + ): this { const currentCamera = this.getCamera() const point = Vec.From(_point) @@ -2147,15 +2154,14 @@ export class Editor extends EventEmitter { const { zoomMax, zoomMin } = cameraOptions point.z = clamp(point.z, zoomMin, zoomMax) } else { - const { zoomMax, zoomMin, padding } = cameraOptions + const { zoomMax, zoomMin, padding, origin } = cameraOptions const vsb = this.getViewportScreenBounds() // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] // Clamp padding to half the viewport size on either dimension - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) + const py = Math.min(padding[0], vsb.w / 2) + const px = Math.min(padding[1], vsb.h / 2) // Expand the bounds by the padding const bounds = Box.From(cameraOptions.bounds) @@ -2168,9 +2174,13 @@ export class Editor extends EventEmitter { const zy = (vsb.h - py * 2) / bounds.height // The min and max zooms are factors of the smaller natural zoom axis - const minNaturalZoom = cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) - const maxZ = zoomMax * minNaturalZoom - const minZ = zoomMin * minNaturalZoom + const fitZoom = cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + const maxZ = zoomMax * fitZoom + const minZ = zoomMin * fitZoom + + if (opts?.initial) { + point.z = fitZoom + } if (point.z < minZ || point.z > maxZ) { // We're trying to zoom out past the minimum zoom level, @@ -2189,18 +2199,20 @@ export class Editor extends EventEmitter { point.y = currentCamera.y + cyB - cyA } + const [oy, ox] = origin + // We're past the natural zoom for the x axis, so clamp it with bounds + padding - // We're below the natural zoom for the x axis, so center it if (point.z > zx) { point.x = clamp(point.x, -bounds.maxX + (vsb.w - px) / point.z, bounds.x + px / point.z) } else { - point.x = vsb.midX / point.z - bounds.midX + // We're below the natural zoom for the x axis, so apply the origin + point.x = (vsb.x + px) / point.z + ((vsb.w - px * 2) / point.z - bounds.width) * ox } if (point.z > zy) { point.y = clamp(point.y, -bounds.maxY + (vsb.h - py) / point.z, bounds.y + py / point.z) } else { - point.y = vsb.midY / point.z - bounds.midY + point.y = (vsb.y + py) / point.z + ((vsb.h - py * 2) / point.z - bounds.height) * oy } } } @@ -2940,11 +2952,10 @@ export class Editor extends EventEmitter { // noop } else { // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) - const { padding } = cameraOptions - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] + const { padding, origin } = cameraOptions // Clamp padding to half the viewport size on either dimension - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) + const py = Math.min(padding[0], vsb.w / 2) + const px = Math.min(padding[0], vsb.h / 2) // Expand the bounds by the padding const bounds = Box.From(cameraOptions.bounds) @@ -2959,14 +2970,19 @@ export class Editor extends EventEmitter { ? Math.min(vsb.w / bounds.width, vsb.h / bounds.height) : Math.max(vsb.w / bounds.width, vsb.h / bounds.height) + const [oy, ox] = origin + const cx = vsb.midX / zoom - (bounds.minX + bounds.width * ox) + const cy = vsb.midY / zoom - (bounds.minY + bounds.height * oy) + // Keeping the current screen center, adjust zoom to the natural zoom - const { x, y } = vsb.center - const { x: cx, y: cy, z: cz } = this.getCamera() - this.setCamera({ - x: cx + (x / zoom - x) - (x / cz - x), - y: cy + (y / zoom - y) - (y / cz - y), - z: zoom, - }) + this._setCamera( + { + x: cx, + y: cy, + z: zoom, + }, + { initial: true } + ) } } diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 4e11a428d..0c95a1891 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -39,8 +39,10 @@ export type TLCameraOptions = { fit: 'contain' | 'cover' /** The bounds of the content (in page space) */ bounds: BoxModel - /** The padding around the bounds (in screen space) */ - padding: number | number[] + /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ + padding: number[] + /** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/ + origin: number[] /** The maximum elastic distance around the bounds */ elastic: number } From 897a8684354661ba678f524ad6817ca2fcec6f29 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 12:03:30 +0000 Subject: [PATCH 34/82] tighten up math, example --- .../camera-options/CameraOptionsExample.tsx | 46 +++++++++++++++++++ .../src/examples/camera-options/README.md | 11 +++++ apps/examples/src/misc/develop.tsx | 31 +------------ packages/editor/api-report.md | 18 ++++++++ packages/editor/api/api.json | 45 ++++++++++++++++-- packages/editor/src/index.ts | 6 ++- packages/editor/src/lib/editor/Editor.ts | 45 +++++++++--------- 7 files changed, 145 insertions(+), 57 deletions(-) create mode 100644 apps/examples/src/examples/camera-options/CameraOptionsExample.tsx create mode 100644 apps/examples/src/examples/camera-options/README.md diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx new file mode 100644 index 000000000..1e967f6ab --- /dev/null +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -0,0 +1,46 @@ +import { TLCameraOptions, TLGeoShape, Tldraw } from 'tldraw' +import 'tldraw/tldraw.css' + +const CAMERA_OPTIONS: TLCameraOptions = { + fit: 'cover', + bounds: { + x: 100, + y: 100, + w: 2400, + h: 800, + }, + origin: [0.5, 0.5], + padding: [50, 50], + panSpeed: 1, + zoomSteps: [0.5, 0.75, 1, 1.5, 2], + zoomMax: 2, + zoomMin: 0.5, + zoomSpeed: 1, + isLocked: false, + elastic: 0, +} + +export default function CameraOptionsExample() { + return ( +
+ { + if (editor.getCurrentPageShapeIds().size === 0) { + if (CAMERA_OPTIONS.fit === 'infinite') return + + const { + bounds: { x, y, w, h }, + } = CAMERA_OPTIONS + editor.createShape({ + type: 'geo', + x, + y, + props: { w, h }, + }) + } + }} + cameraOptions={CAMERA_OPTIONS} + /> +
+ ) +} diff --git a/apps/examples/src/examples/camera-options/README.md b/apps/examples/src/examples/camera-options/README.md new file mode 100644 index 000000000..08c7b5364 --- /dev/null +++ b/apps/examples/src/examples/camera-options/README.md @@ -0,0 +1,11 @@ +--- +title: Camera options +component: ./CameraOptionsExample.tsx +category: basic +--- + +You can set the camera's options and constraints. + +--- + +The `Tldraw` component provides a prop, `cameraOptions`, that can be used to set the camera's constraints, zoom behavior, and other options. diff --git a/apps/examples/src/misc/develop.tsx b/apps/examples/src/misc/develop.tsx index 934db8006..db14ab8cd 100644 --- a/apps/examples/src/misc/develop.tsx +++ b/apps/examples/src/misc/develop.tsx @@ -1,41 +1,14 @@ -import { TLGeoShape, Tldraw } from 'tldraw' +import { Tldraw } from 'tldraw' import 'tldraw/tldraw.css' export default function Develop() { return (
{ ;(window as any).app = editor ;(window as any).editor = editor - if (editor.getCurrentPageShapeIds().size === 0) { - editor.createShape({ - type: 'geo', - x: 0, - y: 0, - props: { - w: 800, - h: 800, - }, - }) - } - }} - cameraOptions={{ - fit: 'contain', - bounds: { - x: 0, - y: 0, - w: 800, - h: 800, - }, - origin: [0.5, 0.5], - padding: [50, 100], - panSpeed: 1, - zoomSteps: [0.5, 0.75, 1, 1.5, 2], - zoomMax: 2, - zoomMin: 0.5, - zoomSpeed: 1, }} />
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index cc746f961..f5c3d9795 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -1941,6 +1941,24 @@ export type TLBrushProps = { className?: string; }; +// @public (undocumented) +export type TLCameraOptions = { + panSpeed: number; + zoomSpeed: number; + zoomSteps: number[]; + zoomMin: number; + zoomMax: number; + isLocked: boolean; +} & ({ + fit: 'contain' | 'cover'; + bounds: BoxModel; + padding: number[]; + origin: number[]; + elastic: number; +} | { + fit: 'infinite'; +}); + // @public (undocumented) export type TLCancelEvent = (info: TLCancelEventInfo) => void; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index afeeff052..432f7f961 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10065,7 +10065,7 @@ { "kind": "Reference", "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, { "kind": "Content", @@ -17004,7 +17004,7 @@ { "kind": "Reference", "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, { "kind": "Content", @@ -35855,6 +35855,41 @@ "endIndex": 4 } }, + { + "kind": "TypeAlias", + "canonicalReference": "@tldraw/editor!TLCameraOptions:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type TLCameraOptions = " + }, + { + "kind": "Content", + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n fit: 'contain' | 'cover';\n bounds: " + }, + { + "kind": "Reference", + "text": "BoxModel", + "canonicalReference": "@tldraw/tlschema!BoxModel:interface" + }, + { + "kind": "Content", + "text": ";\n padding: number[];\n origin: number[];\n elastic: number;\n} | {\n fit: 'infinite';\n})" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts", + "releaseTag": "Public", + "name": "TLCameraOptions", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 4 + } + }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/editor!TLCancelEvent:type", @@ -36557,7 +36592,7 @@ { "kind": "Reference", "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, { "kind": "Content", @@ -37042,7 +37077,7 @@ { "kind": "Reference", "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, { "kind": "Content", @@ -37051,7 +37086,7 @@ { "kind": "Reference", "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!~TLCameraOptions:type" + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, { "kind": "Content", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 50351db3a..2a5f13232 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -240,7 +240,11 @@ export { type TLHistoryEntry, type TLHistoryMark, } from './lib/editor/types/history-types' -export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types' +export { + type RequiredKeys, + type TLCameraOptions, + type TLSvgOptions, +} from './lib/editor/types/misc-types' export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types' export { ContainerProvider, useContainer } from './lib/hooks/useContainer' export { getCursor } from './lib/hooks/useCursor' diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 6177de129..3d35eb03d 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2170,10 +2170,10 @@ export class Editor extends EventEmitter { // which the expanded bounds (with padding) would fit // the current viewport screen bounds. Paddings are // equal to screen pixels at 100% - const zx = (vsb.w - px * 2) / bounds.width - const zy = (vsb.h - py * 2) / bounds.height - // The min and max zooms are factors of the smaller natural zoom axis + + const zx = (vsb.w - px * 2) / bounds.w + const zy = (vsb.h - py * 2) / bounds.h const fitZoom = cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) const maxZ = zoomMax * fitZoom const minZ = zoomMin * fitZoom @@ -2186,34 +2186,35 @@ export class Editor extends EventEmitter { // We're trying to zoom out past the minimum zoom level, // or in past the maximum zoom level, so stop the camera // but keep the current center - const cxA = -currentCamera.x + vsb.w / currentCamera.z / 2 const cyA = -currentCamera.y + vsb.h / currentCamera.z / 2 - point.z = point.z < minZ ? minZ : maxZ - const cxB = -currentCamera.x + vsb.w / point.z / 2 const cyB = -currentCamera.y + vsb.h / point.z / 2 - point.x = currentCamera.x + cxB - cxA point.y = currentCamera.y + cyB - cyA } - const [oy, ox] = origin - - // We're past the natural zoom for the x axis, so clamp it with bounds + padding - if (point.z > zx) { - point.x = clamp(point.x, -bounds.maxX + (vsb.w - px) / point.z, bounds.x + px / point.z) - } else { - // We're below the natural zoom for the x axis, so apply the origin - point.x = (vsb.x + px) / point.z + ((vsb.w - px * 2) / point.z - bounds.width) * ox - } - - if (point.z > zy) { - point.y = clamp(point.y, -bounds.maxY + (vsb.h - py) / point.z, bounds.y + py / point.z) - } else { - point.y = (vsb.y + py) / point.z + ((vsb.h - py * 2) / point.z - bounds.height) * oy - } + // For each axis... + // If we're doing the initial camera position, or if we're below the + // natural zoom for the axis, clamp it with bounds + padding; or else + // use the origin for that axis to decide where to put the content. + // min = padding in page space + // max = padding in page space + (free space in page space * (zoomed out ? origin : 1)) + point.x = clamp( + point.x, + px / point.z + + ((vsb.w - px * 2) / point.z - bounds.w) * + (point.z < zx || opts?.initial ? origin[1] : 1), + px / point.z + ) + point.y = clamp( + point.y, + py / point.z + + ((vsb.h - py * 2) / point.z - bounds.h) * + (point.z < zy || opts?.initial ? origin[0] : 1), + py / point.z + ) } } From 7de976af41ad811f56f9efc8c41b2315fddbd23a Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 13:04:31 +0000 Subject: [PATCH 35/82] _setCamera takes a Vec --- .../camera-options/CameraOptionsExample.tsx | 72 +++++-- packages/editor/api-report.md | 2 - packages/editor/api/api.json | 31 --- packages/editor/src/lib/editor/Editor.ts | 199 +++++++----------- .../editor/src/lib/utils/edgeScrolling.ts | 1 + 5 files changed, 134 insertions(+), 171 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index 1e967f6ab..a27ff4276 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -1,16 +1,16 @@ -import { TLCameraOptions, TLGeoShape, Tldraw } from 'tldraw' +import { TLCameraOptions, Tldraw } from 'tldraw' import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { - fit: 'cover', + fit: 'contain', bounds: { - x: 100, - y: 100, - w: 2400, + x: 0, + y: 0, + w: 1200, h: 800, }, origin: [0.5, 0.5], - padding: [50, 50], + padding: [200, 50], panSpeed: 1, zoomSteps: [0.5, 0.75, 1, 1.5, 2], zoomMax: 2, @@ -24,22 +24,60 @@ export default function CameraOptionsExample() { return (
{ - if (editor.getCurrentPageShapeIds().size === 0) { - if (CAMERA_OPTIONS.fit === 'infinite') return + persistenceKey="camera-options" + cameraOptions={CAMERA_OPTIONS} + components={{ + // These components are just included for debugging / visualization! + OnTheCanvas: () => { + // This component shows the bounds (in page space) + if (CAMERA_OPTIONS.fit === 'infinite') return null const { bounds: { x, y, w, h }, } = CAMERA_OPTIONS - editor.createShape({ - type: 'geo', - x, - y, - props: { w, h }, - }) - } + + return ( + <> +
+ + ) + }, + InFrontOfTheCanvas: () => { + // This component shows the padding (in screen space) + if (CAMERA_OPTIONS.fit === 'infinite') return null + + const { + padding: [py, px], + } = CAMERA_OPTIONS + + if (!px && !py) return null + + return ( + <> +
+ + ) + }, }} - cameraOptions={CAMERA_OPTIONS} />
) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index f5c3d9795..ddf2bcf3e 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -910,8 +910,6 @@ export class Editor extends EventEmitter { // (undocumented) ungroupShapes(ids: TLShape[]): this; updateAssets(assets: TLAssetPartial[]): this; - // (undocumented) - updateCameraOnMount(): void; updateCurrentPageState(partial: Partial>, historyOptions?: TLCommandHistoryOptions): this; updateDocumentSettings(settings: Partial): this; updateInstanceState(partial: Partial>, historyOptions?: TLCommandHistoryOptions): this; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 432f7f961..7e34163d0 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -18835,37 +18835,6 @@ "isAbstract": false, "name": "updateAssets" }, - { - "kind": "Method", - "canonicalReference": "@tldraw/editor!Editor#updateCameraOnMount:member(1)", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "updateCameraOnMount(): " - }, - { - "kind": "Content", - "text": "void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "updateCameraOnMount" - }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#updateCurrentPageState:member(1)", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 3d35eb03d..357ba559b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2080,23 +2080,15 @@ export class Editor extends EventEmitter { private getNaturalZoom() { const cameraOptions = this.getCameraOptions() - if (cameraOptions.fit === 'infinite') { - return 1 - } - + if (cameraOptions.fit === 'infinite') return 1 const { padding } = cameraOptions const vsb = this.getViewportScreenBounds() - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) + const py = Math.min(padding[0], vsb.w / 2) + const px = Math.min(padding[1], vsb.h / 2) const bounds = Box.From(cameraOptions.bounds) - bounds.x -= px - bounds.y -= py - bounds.w += px * 2 - bounds.h += py * 2 - return cameraOptions.fit === 'contain' - ? Math.min(vsb.w / bounds.width, vsb.h / bounds.height) - : Math.max(vsb.w / bounds.width, vsb.h / bounds.height) + const zx = (vsb.w - px * 2) / bounds.w + const zy = (vsb.h - py * 2) / bounds.h + return cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) } /** @public */ @@ -2130,21 +2122,18 @@ export class Editor extends EventEmitter { /** @internal */ private _setCamera( - _point: VecLike, + point: Vec, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } ): this { const currentCamera = this.getCamera() - const point = Vec.From(_point) + + let { x, y, z } = point // If force is true, then we'll set the camera to the point regardless of // the camera options, so that we can handle gestures that permit elasticity // or decay. if (!opts?.force) { // Apply any adjustments based on the camera options - const currentCamera = this.getCamera() - if (!Number.isFinite(point.x)) point.x = 0 - if (!Number.isFinite(point.y)) point.y = 0 - if (point.z === undefined || !Number.isFinite(point.z)) point.z = currentCamera.z const cameraOptions = this.getCameraOptions() @@ -2152,7 +2141,7 @@ export class Editor extends EventEmitter { if (cameraOptions.fit === 'infinite') { // constrain the zoom const { zoomMax, zoomMin } = cameraOptions - point.z = clamp(point.z, zoomMin, zoomMax) + z = clamp(point.z, zoomMin, zoomMax) } else { const { zoomMax, zoomMin, padding, origin } = cameraOptions @@ -2179,44 +2168,39 @@ export class Editor extends EventEmitter { const minZ = zoomMin * fitZoom if (opts?.initial) { - point.z = fitZoom + z = fitZoom } - if (point.z < minZ || point.z > maxZ) { + if (z < minZ || z > maxZ) { // We're trying to zoom out past the minimum zoom level, // or in past the maximum zoom level, so stop the camera // but keep the current center const cxA = -currentCamera.x + vsb.w / currentCamera.z / 2 const cyA = -currentCamera.y + vsb.h / currentCamera.z / 2 - point.z = point.z < minZ ? minZ : maxZ - const cxB = -currentCamera.x + vsb.w / point.z / 2 - const cyB = -currentCamera.y + vsb.h / point.z / 2 - point.x = currentCamera.x + cxB - cxA - point.y = currentCamera.y + cyB - cyA + z = clamp(z, minZ, maxZ) + const cxB = -currentCamera.x + vsb.w / z / 2 + const cyB = -currentCamera.y + vsb.h / z / 2 + x = currentCamera.x + cxB - cxA + y = currentCamera.y + cyB - cyA } - // For each axis... - // If we're doing the initial camera position, or if we're below the - // natural zoom for the axis, clamp it with bounds + padding; or else - // use the origin for that axis to decide where to put the content. - // min = padding in page space - // max = padding in page space + (free space in page space * (zoomed out ? origin : 1)) - point.x = clamp( - point.x, - px / point.z + - ((vsb.w - px * 2) / point.z - bounds.w) * - (point.z < zx || opts?.initial ? origin[1] : 1), - px / point.z - ) - point.y = clamp( - point.y, - py / point.z + - ((vsb.h - py * 2) / point.z - bounds.h) * - (point.z < zy || opts?.initial ? origin[0] : 1), - py / point.z - ) + // Math salad time. (ง •̀_•́)ง + // For each axis... if we're doing the initial camera position, or + // if we're below the natural zoom for the axis: clamp it with bounds + // plus padding in page space; or else use the origin for that axis + // to decide where to put the content. + + const minX = px / z - bounds.x + const minY = py / z - bounds.y + const freeW = (vsb.w - px * 2) / z - bounds.w + const freeH = (vsb.h - py * 2) / z - bounds.h + x = clamp(x, minX + freeW * (z < zx || opts?.initial ? origin[1] : 1), minX) + y = clamp(y, minY + freeH * (z < zy || opts?.initial ? origin[0] : 1), minY) + + // Update the point } } + point.set(x, y, z) if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) { return this @@ -2285,15 +2269,21 @@ export class Editor extends EventEmitter { this.stopFollowingUser() } + const _point = Vec.From(point) + + if (!Number.isFinite(_point.x)) _point.x = 0 + if (!Number.isFinite(_point.y)) _point.y = 0 + if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel() + if (opts && (opts.duration || opts.easing)) { const { width, height } = this.getViewportScreenBounds() // todo: animate this return this._animateToViewport( - new Box(-point.x, -point.y, width / point.z!, height / point.z!), + new Box(-point.x, -point.y, width / _point.z, height / _point.z), opts ) } else { - this._setCamera(point, opts) + this._setCamera(_point, opts) } return this @@ -2386,15 +2376,28 @@ export class Editor extends EventEmitter { * @public */ resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - if (this.getCameraOptions().isLocked) return this + const { isLocked, fit } = this.getCameraOptions() + if (isLocked) return this - const { x: cx, y: cy, z: cz } = this.getCamera() + const currentCamera = this.getCamera() + const { x: cx, y: cy, z: cz } = currentCamera const { x, y } = point + + let z = 1 + + if (fit !== 'infinite') { + // For non-infinite fit, we'll set the camera to the natural zoom level... + // unless it's already there, in which case we'll set zoom to 100% + const naturalZoom = this.getNaturalZoom() + if (cz !== naturalZoom) { + z = naturalZoom + } + } + this.setCamera( - { x: cx + (x / 1 - x) - (x / cz - x), y: cy + (y / 1 - y) - (y / cz - y), z: 1 }, + { x: cx + (x / z - x) - (x / cz - x), y: cy + (y / z - y) - (y / cz - y), z }, animation ) - return this } @@ -2676,7 +2679,7 @@ export class Editor extends EventEmitter { const { elapsed, easing, duration, start, end } = this._viewportAnimation if (elapsed > duration) { - this._setCamera({ x: -end.x, y: -end.y, z: this.getViewportScreenBounds().width / end.width }) + this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width)) cancel() return } @@ -2688,7 +2691,7 @@ export class Editor extends EventEmitter { const top = start.minY + (end.minY - start.minY) * t const right = start.maxX + (end.maxX - start.maxX) * t - this._setCamera({ x: -left, y: -top, z: this.getViewportScreenBounds().width / (right - left) }) + this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left))) } /** @internal */ @@ -2707,11 +2710,13 @@ export class Editor extends EventEmitter { if (duration === 0 || animationSpeed === 0) { // If we have no animation, then skip the animation and just set the camera - return this._setCamera({ - x: -targetViewportPage.x, - y: -targetViewportPage.y, - z: this.getViewportScreenBounds().width / targetViewportPage.width, - }) + return this._setCamera( + new Vec( + -targetViewportPage.x, + -targetViewportPage.y, + this.getViewportScreenBounds().width / targetViewportPage.width + ) + ) } // Set our viewport animation @@ -2770,7 +2775,7 @@ export class Editor extends EventEmitter { if (currentSpeed < speedThreshold) { cancel() } else { - this._setCamera({ x: cx + movementVec.x, y: cy + movementVec.y, z: cz }) + this._setCamera(new Vec(cx + movementVec.x, cy + movementVec.y, cz)) } } @@ -2918,7 +2923,7 @@ export class Editor extends EventEmitter { { screenBounds: screenBounds.toJson(), insets }, { squashing: true, ephemeral: true } ) - this.updateCameraOnMount() + this.setCamera(this.getCamera()) } else { if (center && !this.getInstanceState().followingUserId) { // Get the page center before the change, make the change, and restore it @@ -2934,7 +2939,7 @@ export class Editor extends EventEmitter { { screenBounds: screenBounds.toJson(), insets }, { squashing: true, ephemeral: true } ) - this._setCamera({ ...this.getCamera() }) + this._setCamera(Vec.From({ ...this.getCamera() })) } } } @@ -2945,48 +2950,6 @@ export class Editor extends EventEmitter { return this } - updateCameraOnMount() { - const vsb = this.getViewportScreenBounds() - const cameraOptions = this.getCameraOptions() - - if (cameraOptions.fit === 'infinite') { - // noop - } else { - // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) - const { padding, origin } = cameraOptions - // Clamp padding to half the viewport size on either dimension - const py = Math.min(padding[0], vsb.w / 2) - const px = Math.min(padding[0], vsb.h / 2) - - // Expand the bounds by the padding - const bounds = Box.From(cameraOptions.bounds) - bounds.x -= px - bounds.y -= py - bounds.w += px * 2 - bounds.h += py * 2 - - // The natural zoom is the zoom at which the expanded bounds (with padding) would fit the viewport - const zoom = - cameraOptions.fit === 'contain' - ? Math.min(vsb.w / bounds.width, vsb.h / bounds.height) - : Math.max(vsb.w / bounds.width, vsb.h / bounds.height) - - const [oy, ox] = origin - const cx = vsb.midX / zoom - (bounds.minX + bounds.width * ox) - const cy = vsb.midY / zoom - (bounds.minY + bounds.height * oy) - - // Keeping the current screen center, adjust zoom to the natural zoom - this._setCamera( - { - x: cx, - y: cy, - z: zoom, - }, - { initial: true } - ) - } - } - /** * The bounds of the editor's viewport in screen space. * @@ -3210,11 +3173,13 @@ export class Editor extends EventEmitter { // Update the camera! isCaughtUp = false this.stopCameraAnimation() - this._setCamera({ - x: -(targetCenter.x - targetWidth / 2), - y: -(targetCenter.y - targetHeight / 2), - z: targetZoom, - }) + this._setCamera( + new Vec( + -(targetCenter.x - targetWidth / 2), + -(targetCenter.y - targetHeight / 2), + targetZoom + ) + ) } this.once('stop-following', cancel) @@ -8899,11 +8864,7 @@ export class Editor extends EventEmitter { const { x: cx, y: cy, z: cz } = this.getCamera() this._setCamera( - { - x: cx + dx / cz - x / cz + x / z, - y: cy + dy / cz - y / cz + y / z, - z, - }, + new Vec(cx + dx / cz - x / cz + x / z, cy + dy / cz - y / cz + y / z, z), { immediate: true } ) @@ -8952,11 +8913,7 @@ export class Editor extends EventEmitter { const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz)) this._setCamera( - { - x: cx + (x / zoom - x) - (x / cz - x), - y: cy + (y / zoom - y) - (y / cz - y), - z: zoom, - }, + new Vec(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom), { immediate: true } ) diff --git a/packages/editor/src/lib/utils/edgeScrolling.ts b/packages/editor/src/lib/utils/edgeScrolling.ts index b89e804fe..a5f1926df 100644 --- a/packages/editor/src/lib/utils/edgeScrolling.ts +++ b/packages/editor/src/lib/utils/edgeScrolling.ts @@ -71,5 +71,6 @@ export function moveCameraWhenCloseToEdge(editor: Editor) { editor.setCamera({ x: camera.x + scrollDeltaX, y: camera.y + scrollDeltaY, + z: camera.z, }) } From db6bdaae522da6d1182853a7b195bf307b0bbfc5 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 13:16:52 +0000 Subject: [PATCH 36/82] remove elastic --- .../camera-options/CameraOptionsExample.tsx | 7 ++- packages/editor/api-report.md | 16 ++---- packages/editor/api/api.json | 52 ++++++++++++++++--- packages/editor/src/lib/TldrawEditor.tsx | 2 +- packages/editor/src/lib/constants.ts | 5 +- packages/editor/src/lib/editor/Editor.ts | 20 +++---- .../editor/src/lib/editor/types/misc-types.ts | 9 ++-- .../tools/HandTool/childStates/Dragging.ts | 52 +------------------ 8 files changed, 72 insertions(+), 91 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index a27ff4276..b8be85de1 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -2,7 +2,7 @@ import { TLCameraOptions, Tldraw } from 'tldraw' import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { - fit: 'contain', + type: 'contain', bounds: { x: 0, y: 0, @@ -17,7 +17,6 @@ const CAMERA_OPTIONS: TLCameraOptions = { zoomMin: 0.5, zoomSpeed: 1, isLocked: false, - elastic: 0, } export default function CameraOptionsExample() { @@ -30,7 +29,7 @@ export default function CameraOptionsExample() { // These components are just included for debugging / visualization! OnTheCanvas: () => { // This component shows the bounds (in page space) - if (CAMERA_OPTIONS.fit === 'infinite') return null + if (CAMERA_OPTIONS.type === 'infinite') return null const { bounds: { x, y, w, h }, @@ -54,7 +53,7 @@ export default function CameraOptionsExample() { }, InFrontOfTheCanvas: () => { // This component shows the padding (in screen space) - if (CAMERA_OPTIONS.fit === 'infinite') return null + if (CAMERA_OPTIONS.type === 'infinite') return null const { padding: [py, px], diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index ddf2bcf3e..3ba922453 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -4,7 +4,6 @@ ```ts -/// /// import { Atom } from '@tldraw/state'; @@ -867,7 +866,7 @@ export class Editor extends EventEmitter { force?: boolean; }): this; // (undocumented) - setCameraOptions(options: TLCameraOptions): void; + setCameraOptions(options: TLCameraOptions): this; setCroppingShape(shape: null | TLShape | TLShapeId): this; setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this; setCurrentTool(id: string, info?: {}): this; @@ -1948,13 +1947,12 @@ export type TLCameraOptions = { zoomMax: number; isLocked: boolean; } & ({ - fit: 'contain' | 'cover'; + type: 'contain' | 'cover'; bounds: BoxModel; padding: number[]; origin: number[]; - elastic: number; } | { - fit: 'infinite'; + type: 'infinite'; }); // @public (undocumented) @@ -2053,9 +2051,7 @@ export const TldrawEditor: React_2.NamedExoticComponent; export interface TldrawEditorBaseProps { autoFocus?: boolean; // (undocumented) - cameraOptions?: Partial & { - fit: 'contain' | 'cover' | 'infinite'; - }; + cameraOptions?: Partial> & Pick; children?: ReactNode; className?: string; components?: TLEditorComponents; @@ -2086,9 +2082,7 @@ export type TLEditorComponents = Partial<{ // @public (undocumented) export interface TLEditorOptions { - cameraOptions?: Partial> & { - fit: TLCameraOptions['fit']; - }; + cameraOptions?: Partial> & Pick; getContainer: () => HTMLElement; inferDarkMode?: boolean; initialState?: string; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 7e34163d0..04b394b45 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -17012,7 +17012,7 @@ }, { "kind": "Content", - "text": "void" + "text": "this" }, { "kind": "Content", @@ -35835,7 +35835,7 @@ }, { "kind": "Content", - "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n fit: 'contain' | 'cover';\n bounds: " + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n type: 'contain' | 'cover';\n bounds: " }, { "kind": "Reference", @@ -35844,7 +35844,7 @@ }, { "kind": "Content", - "text": ";\n padding: number[];\n origin: number[];\n elastic: number;\n} | {\n fit: 'infinite';\n})" + "text": ";\n padding: number[];\n origin: number[];\n} | {\n type: 'infinite';\n})" }, { "kind": "Content", @@ -36558,6 +36558,15 @@ "kind": "Content", "text": "<" }, + { + "kind": "Reference", + "text": "Exclude", + "canonicalReference": "!Exclude:type" + }, + { + "kind": "Content", + "text": "<" + }, { "kind": "Reference", "text": "TLCameraOptions", @@ -36565,7 +36574,25 @@ }, { "kind": "Content", - "text": "> & {\n fit: 'contain' | 'cover' | 'infinite';\n }" + "text": ", 'type'>> & " + }, + { + "kind": "Reference", + "text": "Pick", + "canonicalReference": "!Pick:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLCameraOptions", + "canonicalReference": "@tldraw/editor!TLCameraOptions:type" + }, + { + "kind": "Content", + "text": ", 'type'>" }, { "kind": "Content", @@ -36578,7 +36605,7 @@ "name": "cameraOptions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 5 + "endIndex": 11 } }, { @@ -37050,7 +37077,16 @@ }, { "kind": "Content", - "text": ", 'fit'>> & {\n fit: " + "text": ", 'type'>> & " + }, + { + "kind": "Reference", + "text": "Pick", + "canonicalReference": "!Pick:type" + }, + { + "kind": "Content", + "text": "<" }, { "kind": "Reference", @@ -37059,7 +37095,7 @@ }, { "kind": "Content", - "text": "['fit'];\n }" + "text": ", 'type'>" }, { "kind": "Content", @@ -37072,7 +37108,7 @@ "name": "cameraOptions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 9 + "endIndex": 11 } }, { diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index a4aa3436f..a758959ef 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -115,7 +115,7 @@ export interface TldrawEditorBaseProps { */ inferDarkMode?: boolean - cameraOptions?: Partial & { fit: 'infinite' | 'contain' | 'cover' } + cameraOptions?: Partial> & Pick } /** diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 5e1e2bc5b..aa3f8df89 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -31,14 +31,13 @@ const DEFAULT_FIT_CONTAIN_CAMERA_OPTIONS = { bounds: { x: 0, y: 0, w: 1200, h: 800 }, padding: [0, 0], origin: [0.5, 0.5], - elastic: 0, } /** @internal */ export const getDefaultCameraOptions = ( - cameraOptions: Partial> & { fit: TLCameraOptions['fit'] } + cameraOptions: Partial> & { type: TLCameraOptions['type'] } ): TLCameraOptions => { - switch (cameraOptions.fit) { + switch (cameraOptions.type) { case 'infinite': { return { ...DEFAULT_COMMON_CAMERA_OPTIONS, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 357ba559b..5cc92c200 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -187,7 +187,7 @@ export interface TLEditorOptions { /** * Options for the editor's camera. */ - cameraOptions?: Partial> & { fit: TLCameraOptions['fit'] } + cameraOptions?: Partial> & Pick } /** @public */ @@ -208,9 +208,9 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions = cameraOptions?.fit + this._cameraOptions = cameraOptions?.type ? getDefaultCameraOptions(cameraOptions) - : getDefaultCameraOptions({ fit: 'infinite' }) + : getDefaultCameraOptions({ type: 'infinite' }) this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2080,7 +2080,7 @@ export class Editor extends EventEmitter { private getNaturalZoom() { const cameraOptions = this.getCameraOptions() - if (cameraOptions.fit === 'infinite') return 1 + if (cameraOptions.type === 'infinite') return 1 const { padding } = cameraOptions const vsb = this.getViewportScreenBounds() const py = Math.min(padding[0], vsb.w / 2) @@ -2088,12 +2088,14 @@ export class Editor extends EventEmitter { const bounds = Box.From(cameraOptions.bounds) const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - return cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + return cameraOptions.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) } /** @public */ setCameraOptions(options: TLCameraOptions) { this._cameraOptions = options + this.setCamera(this.getCamera()) + return this } /** @internal */ @@ -2138,7 +2140,7 @@ export class Editor extends EventEmitter { const cameraOptions = this.getCameraOptions() // If bounds are provided, then we'll keep those bounds on screen - if (cameraOptions.fit === 'infinite') { + if (cameraOptions.type === 'infinite') { // constrain the zoom const { zoomMax, zoomMin } = cameraOptions z = clamp(point.z, zoomMin, zoomMax) @@ -2163,7 +2165,7 @@ export class Editor extends EventEmitter { const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - const fitZoom = cameraOptions.fit === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + const fitZoom = cameraOptions.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) const maxZ = zoomMax * fitZoom const minZ = zoomMin * fitZoom @@ -2376,7 +2378,7 @@ export class Editor extends EventEmitter { * @public */ resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - const { isLocked, fit } = this.getCameraOptions() + const { isLocked, type } = this.getCameraOptions() if (isLocked) return this const currentCamera = this.getCamera() @@ -2385,7 +2387,7 @@ export class Editor extends EventEmitter { let z = 1 - if (fit !== 'infinite') { + if (type !== 'infinite') { // For non-infinite fit, we'll set the camera to the natural zoom level... // unless it's already there, in which case we'll set zoom to 100% const naturalZoom = this.getNaturalZoom() diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 0c95a1891..487e4c0d4 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -32,18 +32,17 @@ export type TLCameraOptions = { isLocked: boolean } & ( | { - fit: 'infinite' + /** The type of behavior. */ + type: 'infinite' } | { - /** The fit logic for the camera. */ - fit: 'contain' | 'cover' + /** The type of behavior. */ + type: 'contain' | 'cover' /** The bounds of the content (in page space) */ bounds: BoxModel /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ padding: number[] /** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/ origin: number[] - /** The maximum elastic distance around the bounds */ - elastic: number } ) diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 52bd5a1cc..aaa12fd18 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -1,4 +1,4 @@ -import { Box, CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec, clamp } from '@tldraw/editor' +import { CAMERA_SLIDE_FRICTION, StateNode, TLEventHandlers, Vec } from '@tldraw/editor' export class Dragging extends StateNode { static override id = 'dragging' @@ -35,56 +35,8 @@ export class Dragging extends StateNode { private update() { const { editor } = this const { currentScreenPoint, originScreenPoint } = editor.inputs - const delta = Vec.Sub(currentScreenPoint, originScreenPoint) - - const cameraOptions = editor.getCameraOptions() - if (cameraOptions.fit !== 'infinite') { - const { x: cx, y: cy, z: cz } = this.camera - const point = { x: cx + delta.x / cz, y: cy + delta.y / cz, z: cz } - - const vsb = editor.getViewportScreenBounds() - - const { padding, elastic } = cameraOptions - let [py, px] = Array.isArray(padding) ? padding : [padding, padding] - py = Math.min(py, vsb.w / 2) - px = Math.min(px, vsb.h / 2) - - const bounds = Box.From(cameraOptions.bounds) - - const zx = (vsb.w - px * 2) / bounds.width - const zy = (vsb.h - py * 2) / bounds.height - - if (point.z > zx) { - const minX = -bounds.maxX + (vsb.w - px) / point.z - const maxX = bounds.x + px / point.z - point.x = clamp( - point.x, - elastic ? minX - (minX - point.x) * elastic : minX, - elastic ? maxX + (point.x - maxX) * elastic : maxX - ) - } else { - const cx = vsb.midX / point.z - bounds.midX - point.x = elastic ? cx - (cx - point.x) * elastic : cx - } - - if (point.z > zy) { - const minY = -bounds.maxY + (vsb.h - py) / point.z - const maxY = bounds.y + py / point.z - point.y = clamp( - point.y, - elastic ? minY - (minY - point.y) * elastic : minY, - elastic ? maxY + (point.y - maxY) * elastic : maxY - ) - } else { - const cy = vsb.midY / point.z - bounds.midY - point.y = elastic ? cy - (cy - point.y) * elastic : cy - } - - this.editor.setCamera(point, { force: true }) - } else { - this.editor.pan(delta) - } + this.editor.pan(delta) } private complete() { From 9990f509add2a8f92d9f4f0b58a2c2790d18ad1f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 13:48:24 +0000 Subject: [PATCH 37/82] panspeed / zoomspeed --- apps/docs/content/docs/editor.mdx | 2 +- packages/editor/src/lib/editor/Editor.ts | 19 ++++++++++--- .../editor/src/lib/utils/edgeScrolling.ts | 6 +---- .../tldraw/src/lib/ui/context/actions.tsx | 2 +- .../tldraw/src/test/commands/zoomIn.test.ts | 2 +- .../tldraw/src/test/commands/zoomOut.test.ts | 2 +- .../src/test/commands/zoomToBounds.test.ts | 2 +- .../src/test/commands/zoomToFit.test.ts | 2 +- .../src/test/commands/zoomToSelection.test.ts | 2 +- packages/tlschema/api-report.md | 2 -- packages/tlschema/api/api.json | 27 ------------------- packages/tlschema/src/migrations.test.ts | 12 +++++++++ packages/tlschema/src/records/TLInstance.ts | 14 +++++++--- 13 files changed, 45 insertions(+), 49 deletions(-) diff --git a/apps/docs/content/docs/editor.mdx b/apps/docs/content/docs/editor.mdx index dfd3f4192..764c608ad 100644 --- a/apps/docs/content/docs/editor.mdx +++ b/apps/docs/content/docs/editor.mdx @@ -289,7 +289,7 @@ editor.setCamera(0, 0, 1) You can prevent the user from changing the camera using the [Editor#updateInstanceState](?) method. ```ts -editor.updateInstanceState({ canMoveCamera: false }) +editor.setCameraOptions({ ...editor.getCameraOptions(), canMoveCamera: false }) ``` ### Turn on dark mode diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 5cc92c200..16be9d403 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2638,9 +2638,13 @@ export class Editor extends EventEmitter { * @param animation - The animation options. */ pan(offset: VecLike, animation?: TLAnimationOptions): this { - if (this.getCameraOptions().isLocked) return this + const { isLocked, panSpeed } = this.getCameraOptions() + if (isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() - this.setCamera({ x: cx + offset.x / cz, y: cy + offset.y / cz, z: cz }, animation) + this.setCamera( + { x: (cx + offset.x * panSpeed) / cz, y: (cy + offset.y * panSpeed) / cz, z: cz }, + animation + ) this._flushEventsForTick(0) return this } @@ -8865,8 +8869,14 @@ export class Editor extends EventEmitter { const { x: cx, y: cy, z: cz } = this.getCamera() + const { panSpeed, zoomSpeed } = this.getCameraOptions() + this._setCamera( - new Vec(cx + dx / cz - x / cz + x / z, cy + dy / cz - y / cz + y / z, z), + new Vec( + cx + (dx * panSpeed) / cz - x / cz + x / (z * zoomSpeed), + cy + (dy * panSpeed) / cz - y / cz + y / (z * zoomSpeed), + z * zoomSpeed + ), { immediate: true } ) @@ -8912,7 +8922,8 @@ export class Editor extends EventEmitter { 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)) + const { zoomSpeed } = this.getCameraOptions() + const zoom = cz + (info.delta.z ?? 0) * zoomSpeed * cz this._setCamera( new Vec(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom), diff --git a/packages/editor/src/lib/utils/edgeScrolling.ts b/packages/editor/src/lib/utils/edgeScrolling.ts index a5f1926df..e58699675 100644 --- a/packages/editor/src/lib/utils/edgeScrolling.ts +++ b/packages/editor/src/lib/utils/edgeScrolling.ts @@ -33,11 +33,7 @@ function getEdgeProximityFactor( * @public */ export function moveCameraWhenCloseToEdge(editor: Editor) { - if ( - !editor.inputs.isDragging || - editor.inputs.isPanning || - !editor.getInstanceState().canMoveCamera - ) + if (!editor.inputs.isDragging || editor.inputs.isPanning || editor.getCameraOptions().isLocked) return const { diff --git a/packages/tldraw/src/lib/ui/context/actions.tsx b/packages/tldraw/src/lib/ui/context/actions.tsx index 9fef8816e..b7bc31540 100644 --- a/packages/tldraw/src/lib/ui/context/actions.tsx +++ b/packages/tldraw/src/lib/ui/context/actions.tsx @@ -497,7 +497,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { } else { ids = editor.getSelectedShapeIds() const commonBounds = Box.Common(compact(ids.map((id) => editor.getShapePageBounds(id)))) - offset = instanceState.canMoveCamera + offset = !editor.getCameraOptions().isLocked ? { x: commonBounds.width + 10, y: 0, diff --git a/packages/tldraw/src/test/commands/zoomIn.test.ts b/packages/tldraw/src/test/commands/zoomIn.test.ts index f3cca8314..5864d24b2 100644 --- a/packages/tldraw/src/test/commands/zoomIn.test.ts +++ b/packages/tldraw/src/test/commands/zoomIn.test.ts @@ -59,7 +59,7 @@ it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => { it('does not zoom when camera is frozen', () => { editor.setCamera({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) editor.zoomIn() expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) }) diff --git a/packages/tldraw/src/test/commands/zoomOut.test.ts b/packages/tldraw/src/test/commands/zoomOut.test.ts index aec509e5f..810aefcaf 100644 --- a/packages/tldraw/src/test/commands/zoomOut.test.ts +++ b/packages/tldraw/src/test/commands/zoomOut.test.ts @@ -25,7 +25,7 @@ it('zooms by increments', () => { it('does not zoom out when camera is frozen', () => { editor.setCamera({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) editor.zoomOut() expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) }) diff --git a/packages/tldraw/src/test/commands/zoomToBounds.test.ts b/packages/tldraw/src/test/commands/zoomToBounds.test.ts index 42b6f1555..6ddc39c36 100644 --- a/packages/tldraw/src/test/commands/zoomToBounds.test.ts +++ b/packages/tldraw/src/test/commands/zoomToBounds.test.ts @@ -44,7 +44,7 @@ it('does not zoom past min', () => { it('does not zoom to bounds when camera is frozen', () => { editor.setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 }) expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 }) - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) editor.zoomToBounds(new Box(200, 300, 300, 300)) expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 }) }) diff --git a/packages/tldraw/src/test/commands/zoomToFit.test.ts b/packages/tldraw/src/test/commands/zoomToFit.test.ts index b5fecd252..eb86b7bab 100644 --- a/packages/tldraw/src/test/commands/zoomToFit.test.ts +++ b/packages/tldraw/src/test/commands/zoomToFit.test.ts @@ -14,7 +14,7 @@ it('converts correctly', () => { it('does not zoom to bounds when camera is frozen', () => { const cameraBefore = { ...editor.getCamera() } - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) editor.zoomToFit() expect(editor.getCamera()).toMatchObject(cameraBefore) }) diff --git a/packages/tldraw/src/test/commands/zoomToSelection.test.ts b/packages/tldraw/src/test/commands/zoomToSelection.test.ts index b49215ecd..98e0ed291 100644 --- a/packages/tldraw/src/test/commands/zoomToSelection.test.ts +++ b/packages/tldraw/src/test/commands/zoomToSelection.test.ts @@ -35,7 +35,7 @@ it('does not zoom past min', () => { it('does not zoom to selection when camera is frozen', () => { const cameraBefore = { ...editor.getCamera() } - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) editor.setSelectedShapes([ids.box1, ids.box2]) editor.zoomToSelection() expect(editor.getCamera()).toMatchObject(cameraBefore) diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index cd8265557..c0b8710c1 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -1023,8 +1023,6 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> { // (undocumented) brush: BoxModel | null; // (undocumented) - canMoveCamera: boolean; - // (undocumented) chatMessage: string; // (undocumented) currentPageId: TLPageId; diff --git a/packages/tlschema/api/api.json b/packages/tlschema/api/api.json index 5b74874eb..fd9d0605d 100644 --- a/packages/tlschema/api/api.json +++ b/packages/tlschema/api/api.json @@ -6688,33 +6688,6 @@ "endIndex": 3 } }, - { - "kind": "PropertySignature", - "canonicalReference": "@tldraw/tlschema!TLInstance#canMoveCamera:member", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "canMoveCamera: " - }, - { - "kind": "Content", - "text": "boolean" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": false, - "releaseTag": "Public", - "name": "canMoveCamera", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - } - }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/tlschema!TLInstance#chatMessage:member", diff --git a/packages/tlschema/src/migrations.test.ts b/packages/tlschema/src/migrations.test.ts index 377920f2d..852c02f23 100644 --- a/packages/tlschema/src/migrations.test.ts +++ b/packages/tlschema/src/migrations.test.ts @@ -2030,6 +2030,18 @@ describe('Fractional indexing for line points', () => { }) }) +describe('removes can move camera', () => { + const { up, down } = instanceMigrations.migrators[instanceVersions.RemoveCanMoveCamera] + + test('up works as expected', () => { + expect(up({ canMoveCamera: true })).toStrictEqual({}) + }) + + test('down works as expected', () => { + expect(down({})).toStrictEqual({ canMoveCamera: true }) + }) +}) + /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */ for (const migrator of allMigrators) { diff --git a/packages/tlschema/src/records/TLInstance.ts b/packages/tlschema/src/records/TLInstance.ts index 1a70188be..2ca9566e4 100644 --- a/packages/tlschema/src/records/TLInstance.ts +++ b/packages/tlschema/src/records/TLInstance.ts @@ -38,7 +38,6 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> { isChatting: boolean isPenMode: boolean isGridMode: boolean - canMoveCamera: boolean isFocused: boolean devicePixelRatio: number /** @@ -101,7 +100,6 @@ export function createInstanceRecordType(stylesById: Map { @@ -538,6 +536,14 @@ export const instanceMigrations = defineMigrations({ } }, }, + [instanceVersions.RemoveCanMoveCamera]: { + up: ({ canMoveCamera: _, ...instance }) => { + return instance + }, + down: (instance: TLInstance) => { + return { ...instance, canMoveCamera: true } + }, + }, }, }) From 89b557ea64547cee6ac074b284309bef7aee0570 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 14:06:15 +0000 Subject: [PATCH 38/82] fix tests --- apps/dotcom/src/hooks/useUrlState.ts | 9 ++++- packages/editor/api-report.md | 16 ++++---- packages/editor/api/api.json | 31 ++++++++++++++ packages/editor/src/index.ts | 4 +- packages/editor/src/lib/constants.ts | 11 ++--- packages/editor/src/lib/editor/Editor.ts | 19 ++++++--- .../lib/shapes/shared/defaultStyleDefs.tsx | 6 +-- packages/tldraw/src/test/commands/pan.test.ts | 12 ++++++ .../tldraw/src/test/commands/zoomIn.test.ts | 40 ++++++++++--------- .../tldraw/src/test/commands/zoomOut.test.ts | 17 ++++---- 10 files changed, 108 insertions(+), 57 deletions(-) diff --git a/apps/dotcom/src/hooks/useUrlState.ts b/apps/dotcom/src/hooks/useUrlState.ts index 451408566..f8ed3c8a0 100644 --- a/apps/dotcom/src/hooks/useUrlState.ts +++ b/apps/dotcom/src/hooks/useUrlState.ts @@ -1,5 +1,5 @@ import { default as React, useEffect } from 'react' -import { Editor, MAX_ZOOM, MIN_ZOOM, TLPageId, debounce, react, useEditor } from 'tldraw' +import { Editor, TLPageId, debounce, react, useEditor } from 'tldraw' const PARAMS = { // deprecated @@ -68,8 +68,13 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) { const viewport = viewportFromString(newViewportRaw) const { x, y, w, h } = viewport const { w: sw, h: sh } = editor.getViewportScreenBounds() + const naturalZoom = editor.getNaturalZoom() + const { zoomMin, zoomMax } = editor.getCameraOptions() - const zoom = Math.min(Math.max(Math.min(sw / w, sh / h), MIN_ZOOM), MAX_ZOOM) + const zoom = Math.min( + Math.max(Math.min(sw / w, sh / h), zoomMin * naturalZoom), + zoomMax * naturalZoom + ) editor.setCamera({ x: -x + (sw - w * zoom) / 2 / zoom, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 3ba922453..57a6dcf45 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -702,6 +702,8 @@ export class Editor extends EventEmitter { getInitialMetaForShape(_shape: TLShape): JsonObject; getInstanceState(): TLInstance; getIsMenuOpen(): boolean; + // (undocumented) + getNaturalZoom(): number; getOnlySelectedShape(): null | TLShape; getOpenMenus(): string[]; getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape; @@ -1061,6 +1063,11 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap // @public (undocumented) export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string; +// @internal (undocumented) +export const getDefaultCameraOptions: (cameraOptions: Partial> & { + type: TLCameraOptions['type']; +}) => TLCameraOptions; + // @public (undocumented) export function getFreshUserPreferences(): TLUserPreferences; @@ -1366,12 +1373,6 @@ export const MAX_PAGES = 40; // @internal (undocumented) export const MAX_SHAPES_PER_PAGE = 2000; -// @internal (undocumented) -export const MAX_ZOOM = 8; - -// @internal (undocumented) -export const MIN_ZOOM = 0.1; - // @public export function moveCameraWhenCloseToEdge(editor: Editor): void; @@ -2991,9 +2992,6 @@ export class WeakMapCache { export { whyAmIRunning } -// @internal (undocumented) -export const ZOOMS: number[]; - export * from "@tldraw/store"; export * from "@tldraw/tlschema"; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 04b394b45..7b2f2fc81 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -11335,6 +11335,37 @@ "isAbstract": false, "name": "getIsMenuOpen" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getNaturalZoom:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getNaturalZoom(): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getNaturalZoom" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getOnlySelectedShape:member(1)", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 2a5f13232..e1259d8a0 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -122,11 +122,9 @@ export { HIT_TEST_MARGIN, MAX_PAGES, MAX_SHAPES_PER_PAGE, - MAX_ZOOM, - MIN_ZOOM, MULTI_CLICK_DURATION, SVG_PADDING, - ZOOMS, + getDefaultCameraOptions, } from './lib/constants' export { Editor, diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index aa3f8df89..c1c180c11 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -11,13 +11,6 @@ export const ANIMATION_SHORT_MS = 80 /** @internal */ export const ANIMATION_MEDIUM_MS = 320 -/** @internal */ -export const ZOOMS = [0.1, 0.25, 0.5, 1, 2, 4, 8] -/** @internal */ -export const MIN_ZOOM = 0.1 -/** @internal */ -export const MAX_ZOOM = 8 - const DEFAULT_COMMON_CAMERA_OPTIONS = { zoomMax: 8, zoomMin: 0.1, @@ -82,7 +75,9 @@ export const SVG_PADDING = 32 /** @internal */ export const HASH_PATTERN_ZOOM_NAMES: Record = {} -for (let zoom = 1; zoom <= Math.ceil(MAX_ZOOM); zoom++) { +export const HASH_PATTERN_COUNT = 6 + +for (let zoom = 1; zoom <= HASH_PATTERN_COUNT; zoom++) { HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark` HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light` } diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 16be9d403..32fed347d 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -81,8 +81,6 @@ import { INTERNAL_POINTER_IDS, MAX_PAGES, MAX_SHAPES_PER_PAGE, - MAX_ZOOM, - MIN_ZOOM, SVG_PADDING, getDefaultCameraOptions, } from '../constants' @@ -2078,7 +2076,7 @@ export class Editor extends EventEmitter { return this._cameraOptions } - private getNaturalZoom() { + getNaturalZoom() { const cameraOptions = this.getCameraOptions() if (cameraOptions.type === 'infinite') return 1 const { padding } = cameraOptions @@ -2600,13 +2598,16 @@ export class Editor extends EventEmitter { const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28) + const naturalZoom = this.getNaturalZoom() + const { zoomMin, zoomMax } = this.getCameraOptions() + let zoom = clamp( Math.min( (viewportScreenBounds.width - inset) / bounds.width, (viewportScreenBounds.height - inset) / bounds.height ), - MIN_ZOOM, - MAX_ZOOM + zoomMin * naturalZoom, + zoomMax * naturalZoom ) if (opts?.targetZoom !== undefined) { @@ -3147,7 +3148,13 @@ export class Editor extends EventEmitter { ? Math.min(width / desiredWidth, height / desiredHeight) : height / desiredHeight - const targetZoom = clamp(this.getCamera().z * ratio, MIN_ZOOM, MAX_ZOOM) + const naturalZoom = this.getNaturalZoom() + const { zoomMin, zoomMax } = this.getCameraOptions() + const targetZoom = clamp( + this.getCamera().z * ratio, + zoomMin * naturalZoom, + zoomMax * naturalZoom + ) const targetWidth = this.getViewportScreenBounds().w / targetZoom const targetHeight = this.getViewportScreenBounds().h / targetZoom diff --git a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx index 3f8e2f831..47b40fb15 100644 --- a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx +++ b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx @@ -4,7 +4,6 @@ import { DefaultFontStyle, FileHelpers, HASH_PATTERN_ZOOM_NAMES, - MAX_ZOOM, SvgExportDef, TLDefaultColorTheme, TLDefaultFillStyle, @@ -13,6 +12,7 @@ import { debugFlags, useEditor, } from '@tldraw/editor' +import { HASH_PATTERN_COUNT } from '@tldraw/editor/src/lib/constants' import { useEffect, useMemo, useRef, useState } from 'react' /** @public */ @@ -155,7 +155,7 @@ type PatternDef = { zoom: number; url: string; darkMode: boolean } const getDefaultPatterns = () => { const defaultPatterns: PatternDef[] = [] - for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) { + for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { const whitePixelBlob = canvasBlob([1, 1], (ctx) => { ctx.fillStyle = DefaultColorThemePalette.lightMode.black.semi ctx.fillRect(0, 0, 1, 1) @@ -188,7 +188,7 @@ function usePattern() { useEffect(() => { const promises: Promise<{ zoom: number; url: string; darkMode: boolean }>[] = [] - for (let i = 1; i <= Math.ceil(MAX_ZOOM); i++) { + for (let i = 1; i <= HASH_PATTERN_COUNT; i++) { promises.push( generateImage(dpr, i, false).then((blob) => ({ zoom: i, diff --git a/packages/tldraw/src/test/commands/pan.test.ts b/packages/tldraw/src/test/commands/pan.test.ts index f3bad0be1..8a52e5581 100644 --- a/packages/tldraw/src/test/commands/pan.test.ts +++ b/packages/tldraw/src/test/commands/pan.test.ts @@ -14,6 +14,18 @@ describe('When panning', () => { editor.expectCameraToBe(200, 200, 1) }) + it('Updates the camera with panSpeed at 2', () => { + editor.setCameraOptions({ ...editor.getCameraOptions(), panSpeed: 2 }) + editor.pan({ x: 200, y: 200 }) + editor.expectCameraToBe(400, 400, 1) + }) + + it('Updates the camera with panSpeed', () => { + editor.setCameraOptions({ ...editor.getCameraOptions(), panSpeed: 0.5 }) + editor.pan({ x: 200, y: 200 }) + editor.expectCameraToBe(100, 100, 1) + }) + it('Updates the pageBounds', () => { const screenBounds = editor.getViewportScreenBounds() const beforeScreenBounds = new Box( diff --git a/packages/tldraw/src/test/commands/zoomIn.test.ts b/packages/tldraw/src/test/commands/zoomIn.test.ts index 5864d24b2..2337cdc12 100644 --- a/packages/tldraw/src/test/commands/zoomIn.test.ts +++ b/packages/tldraw/src/test/commands/zoomIn.test.ts @@ -1,4 +1,4 @@ -import { ZOOMS } from '@tldraw/editor' +import { getDefaultCameraOptions } from '@tldraw/editor' import { TestEditor } from '../TestEditor' let editor: TestEditor @@ -8,21 +8,19 @@ beforeEach(() => { }) it('zooms by increments', () => { - // Starts at 1 - expect(editor.getZoomLevel()).toBe(1) - expect(editor.getZoomLevel()).toBe(ZOOMS[3]) - // zooms in - expect(editor.getZoomLevel()).toBe(ZOOMS[3]) - editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[4]) - editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[5]) - editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[6]) + const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) - // does not zoom in past max + // Starts at 1 + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[6]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[4]) + editor.zoomIn() + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[5]) + editor.zoomIn() + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[6]) + // does not zoom out past min + editor.zoomIn() + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[6]) }) it('preserves the screen center', () => { @@ -48,12 +46,18 @@ it('preserves the screen center when offset', () => { }) 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 }) + const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) + + editor.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 }) editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[4]) - editor.setCamera({ x: 0, y: 0, z: (ZOOMS[2] + ZOOMS[3]) / 2 - 0.1 }) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[4]) + editor.setCamera({ + x: 0, + y: 0, + z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 - 0.1, + }) editor.zoomIn() - expect(editor.getZoomLevel()).toBe(ZOOMS[3]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) }) it('does not zoom when camera is frozen', () => { diff --git a/packages/tldraw/src/test/commands/zoomOut.test.ts b/packages/tldraw/src/test/commands/zoomOut.test.ts index 810aefcaf..75372add9 100644 --- a/packages/tldraw/src/test/commands/zoomOut.test.ts +++ b/packages/tldraw/src/test/commands/zoomOut.test.ts @@ -1,4 +1,4 @@ -import { ZOOMS } from '@tldraw/editor' +import { getDefaultCameraOptions } from '@tldraw/editor' import { TestEditor } from '../TestEditor' let editor: TestEditor @@ -7,19 +7,20 @@ beforeEach(() => { editor = new TestEditor() }) -it('zooms by increments', () => { +it('zooms out and in by increments', () => { + const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) + // Starts at 1 - expect(editor.getZoomLevel()).toBe(1) - expect(editor.getZoomLevel()).toBe(ZOOMS[3]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) editor.zoomOut() - expect(editor.getZoomLevel()).toBe(ZOOMS[2]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[2]) editor.zoomOut() - expect(editor.getZoomLevel()).toBe(ZOOMS[1]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[1]) editor.zoomOut() - expect(editor.getZoomLevel()).toBe(ZOOMS[0]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0]) // does not zoom out past min editor.zoomOut() - expect(editor.getZoomLevel()).toBe(ZOOMS[0]) + expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[0]) }) it('does not zoom out when camera is frozen', () => { From be30bf08eef00a1941249b70e641ddc2f2bd0343 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 27 Mar 2024 14:27:36 +0000 Subject: [PATCH 39/82] Update Dragging.ts --- .../src/lib/tools/HandTool/childStates/Dragging.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index aaa12fd18..4d52d86f9 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -16,10 +16,6 @@ export class Dragging extends StateNode { this.update() } - override onPointerMove: TLEventHandlers['onPointerMove'] = () => { - this.update() - } - override onPointerUp: TLEventHandlers['onPointerUp'] = () => { this.complete() } @@ -32,11 +28,14 @@ export class Dragging extends StateNode { this.complete() } + override onTick = () => { + this.update() + } + private update() { const { editor } = this const { currentScreenPoint, originScreenPoint } = editor.inputs - const delta = Vec.Sub(currentScreenPoint, originScreenPoint) - this.editor.pan(delta) + this.editor.setCamera(Vec.Sub(currentScreenPoint, originScreenPoint).add(this.camera)) } private complete() { From a998f7572eb1e6d31a63ec513178b00f3efdd8ec Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Fri, 29 Mar 2024 10:04:12 +0000 Subject: [PATCH 40/82] update can move camera locks --- apps/docs/content/docs/editor.mdx | 4 ++-- .../BeforeCreateUpdateShapeExample.tsx | 2 +- packages/editor/api-report.md | 2 +- packages/editor/api/api.json | 2 +- packages/editor/src/lib/editor/Editor.ts | 7 ++++++- packages/editor/src/lib/editor/types/misc-types.ts | 2 +- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/docs/content/docs/editor.mdx b/apps/docs/content/docs/editor.mdx index 2a445d257..a9bb16685 100644 --- a/apps/docs/content/docs/editor.mdx +++ b/apps/docs/content/docs/editor.mdx @@ -313,10 +313,10 @@ editor.setCamera(0, 0, 1) ### Freeze the camera -You can prevent the user from changing the camera using the [Editor#updateInstanceState](?) method. +You can prevent the user from changing the camera using the [Editor#setCameraOptions](?) method. ```ts -editor.setCameraOptions({ ...editor.getCameraOptions(), canMoveCamera: false }) +editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) ``` ### Turn on dark mode diff --git a/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx b/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx index 7f4b15370..db8c89547 100644 --- a/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx +++ b/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx @@ -44,7 +44,7 @@ export default function BeforeCreateUpdateShapeExample() { editor.zoomToBounds(new Box(-500, -500, 1000, 1000)) // lock the camera on that area - editor.updateInstanceState({ canMoveCamera: false }) + editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) }} components={{ // to make it a little clearer what's going on in this example, we'll draw a diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 5b502b7a4..a0f54a20e 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -1997,7 +1997,7 @@ export type TLCameraOptions = { zoomMax: number; isLocked: boolean; } & ({ - type: 'contain' | 'cover'; + type: 'contain' | 'cover' | 'limit'; bounds: BoxModel; padding: number[]; origin: number[]; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 8cc75a9c8..ca8f9eefb 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36802,7 +36802,7 @@ }, { "kind": "Content", - "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n type: 'contain' | 'cover';\n bounds: " + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n type: 'contain' | 'cover' | 'limit';\n bounds: " }, { "kind": "Reference", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d17a111f5..478f4ad65 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2163,7 +2163,12 @@ export class Editor extends EventEmitter { const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - const fitZoom = cameraOptions.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + const fitZoom = + cameraOptions.type === 'limit' + ? 1 + : cameraOptions.type === 'contain' + ? Math.min(zx, zy) + : Math.max(zx, zy) const maxZ = zoomMax * fitZoom const minZ = zoomMin * fitZoom diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 487e4c0d4..9602682f7 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -37,7 +37,7 @@ export type TLCameraOptions = { } | { /** The type of behavior. */ - type: 'contain' | 'cover' + type: 'contain' | 'cover' | 'limit' /** The bounds of the content (in page space) */ bounds: BoxModel /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ From f695d30bf2143e079dfb357b5e0f3a41116f100a Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Fri, 29 Mar 2024 17:13:26 +0000 Subject: [PATCH 41/82] fix bugs --- .../image-annotator/ImageAnnotationEditor.tsx | 267 ++++++------------ packages/editor/api-report.md | 7 +- packages/editor/api/api.json | 22 +- packages/editor/src/lib/editor/Editor.ts | 63 +++-- .../tools/HandTool/childStates/Dragging.ts | 7 +- 5 files changed, 156 insertions(+), 210 deletions(-) diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index 827a545a3..e68ed10b8 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -1,22 +1,16 @@ import { useCallback, useEffect, useState } from 'react' import { AssetRecordType, - Box, Editor, SVGContainer, TLImageShape, TLShapeId, Tldraw, - clamp, createShapeId, exportToBlob, - getIndexBelow, - react, track, - useBreakpoint, useEditor, } from 'tldraw' -import { PORTRAIT_BREAKPOINT } from 'tldraw/src/lib/ui/constants' import { AnnotatorImage } from './types' // TODO: @@ -31,9 +25,19 @@ export function ImageAnnotationEditor({ onDone: (result: Blob) => void }) { const [imageShapeId, setImageShapeId] = useState(null) + const [editor, setEditor] = useState(null as Editor | null) + function onMount(editor: Editor) { + setEditor(editor) + } + + useEffect(() => { + if (!editor) return + + // Turn off debug mode editor.updateInstanceState({ isDebugMode: false }) + // Create the asset and image shape const assetId = AssetRecordType.createId() editor.createAssets([ { @@ -51,10 +55,9 @@ export function ImageAnnotationEditor({ }, }, ]) - - const imageId = createShapeId() + const shapeId = createShapeId() editor.createShape({ - id: imageId, + id: shapeId, type: 'image', x: 0, y: 0, @@ -66,13 +69,85 @@ export function ImageAnnotationEditor({ }, }) - editor.history.clear() - setImageShapeId(imageId) + // Make sure the shape is at the bottom of the page + function makeSureShapeIsAtBottom() { + if (!editor) return - // zoom aaaaallll the way out. our camera constraints will make sure we end up nicely - // centered on the image - editor.setCamera({ x: 0, y: 0, z: 0.0001 }) - } + const shape = editor.getShape(shapeId) + if (!shape) return + + const pageId = editor.getCurrentPageId() + + // The shape should always be the child of the current page + if (shape.parentId !== pageId) { + editor.moveShapesToPage([shape], pageId) + } + + // The shape should always be at the bottom of the page's children + const siblings = editor.getSortedChildIdsForParent(pageId) + const currentBottomShape = editor.getShape(siblings[0])! + if (currentBottomShape.id !== shapeId) { + editor.sendToBack([shape]) + } + } + + makeSureShapeIsAtBottom() + + const removeOnCreate = editor.sideEffects.registerAfterCreateHandler( + 'shape', + makeSureShapeIsAtBottom + ) + + const removeOnChange = editor.sideEffects.registerAfterChangeHandler( + 'shape', + makeSureShapeIsAtBottom + ) + + // The shape should always be locked + const cleanupKeepShapeLocked = editor.sideEffects.registerBeforeChangeHandler( + 'shape', + (prev, next) => { + if (next.id !== shapeId) return next + if (next.isLocked) return next + return { ...prev, isLocked: true } + } + ) + + // Reset the history + editor.history.clear() + setImageShapeId(shapeId) + + return () => { + removeOnChange() + removeOnCreate() + cleanupKeepShapeLocked() + } + }, [image, editor]) + + useEffect(() => { + if (!editor) return + if (!imageShapeId) return + + // We want to set the default zoom only on first mount + let isInitial = true + editor.setCameraOptions( + { + type: 'contain', + bounds: { w: image.width, h: image.height, x: 0, y: 0 }, + padding: [0, 0], + origin: [0.5, 0.5], + zoomMax: 8, + zoomMin: 1, // prevent zoom from going below zero + zoomSteps: [1, 2, 4, 8], + zoomSpeed: 1, + panSpeed: 1, + isLocked: false, + }, + { initial: isInitial } + ) + + isInitial = false + }, [editor, imageShapeId, image]) return ( }, [imageShapeId, onDone]), }} - > - {imageShapeId && } - {imageShapeId && } - {imageShapeId && } - + /> ) } @@ -173,164 +244,8 @@ function DoneButton({ ) } -/** - * We want to keep our locked image at the bottom of the current page - people shouldn't be able to - * place other shapes beneath it. This component adds side effects for when shapes are created or - * updated to make sure that this shape is always kept at the bottom. - */ -function KeepShapeAtBottomOfCurrentPage({ shapeId }: { shapeId: TLShapeId }) { - const editor = useEditor() - - useEffect(() => { - function makeSureShapeIsAtBottom() { - let shape = editor.getShape(shapeId) - if (!shape) return - const pageId = editor.getCurrentPageId() - - if (shape.parentId !== pageId) { - editor.moveShapesToPage([shape], pageId) - shape = editor.getShape(shapeId)! - } - - const siblings = editor.getSortedChildIdsForParent(pageId) - const currentBottomShape = editor.getShape(siblings[0])! - if (currentBottomShape.id === shapeId) return - - editor.updateShape({ - id: shape.id, - type: shape.type, - isLocked: shape.isLocked, - index: getIndexBelow(currentBottomShape.index), - }) - } - - makeSureShapeIsAtBottom() - - const removeOnCreate = editor.sideEffects.registerAfterCreateHandler( - 'shape', - makeSureShapeIsAtBottom - ) - const removeOnChange = editor.sideEffects.registerAfterChangeHandler( - 'shape', - makeSureShapeIsAtBottom - ) - - return () => { - removeOnCreate() - removeOnChange() - } - }, [editor, shapeId]) - - return null -} - -function KeepShapeLocked({ shapeId }: { shapeId: TLShapeId }) { - const editor = useEditor() - - useEffect(() => { - const shape = editor.getShape(shapeId) - if (!shape) return - - editor.updateShape({ - id: shape.id, - type: shape.type, - isLocked: true, - }) - - const removeOnChange = editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next) => { - if (next.id !== shapeId) return next - if (next.isLocked) return next - return { ...prev, isLocked: true } - }) - - return () => { - removeOnChange() - } - }, [editor, shapeId]) - - return null -} - /** * We don't want the user to be able to scroll away from the image, or zoom it all the way out. This * component hooks into camera updates to keep the camera constrained - try uploading a very long, * thin image and seeing how the camera behaves. */ -function ConstrainCamera({ shapeId }: { shapeId: TLShapeId }) { - const editor = useEditor() - const breakpoint = useBreakpoint() - const isMobile = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM - - useEffect(() => { - const marginTop = 44 - const marginSide = isMobile ? 16 : 164 - const marginBottom = 60 - - function constrainCamera(camera: { x: number; y: number; z: number }): { - x: number - y: number - z: number - } { - const viewportBounds = editor.getViewportScreenBounds() - const targetBounds = editor.getShapePageBounds(shapeId)! - - const usableViewport = new Box( - marginSide, - marginTop, - viewportBounds.w - marginSide * 2, - viewportBounds.h - marginTop - marginBottom - ) - - const minZoom = Math.min( - usableViewport.w / targetBounds.w, - usableViewport.h / targetBounds.h, - 1 - ) - const zoom = Math.max(minZoom, camera.z) - - const centerX = targetBounds.x - targetBounds.w / 2 + usableViewport.midX / zoom - const centerY = targetBounds.y - targetBounds.h / 2 + usableViewport.midY / zoom - - const availableXMovement = Math.max(0, targetBounds.w - usableViewport.w / zoom) - const availableYMovement = Math.max(0, targetBounds.h - usableViewport.h / zoom) - - return { - x: clamp(camera.x, centerX - availableXMovement / 2, centerX + availableXMovement / 2), - y: clamp(camera.y, centerY - availableYMovement / 2, centerY + availableYMovement / 2), - z: zoom, - } - } - - const removeOnChange = editor.sideEffects.registerBeforeChangeHandler( - 'camera', - (_prev, next) => { - const constrained = constrainCamera(next) - if (constrained.x === next.x && constrained.y === next.y && constrained.z === next.z) - return next - return { ...next, ...constrained } - } - ) - - const removeReaction = react('update camera when viewport/shape changes', () => { - const original = editor.getCamera() - const constrained = constrainCamera(original) - if ( - original.x === constrained.x && - original.y === constrained.y && - original.z === constrained.z - ) { - return - } - - // this needs to be in a microtask for some reason, but idk why - queueMicrotask(() => editor.setCamera(constrained)) - }) - - return () => { - removeOnChange() - removeReaction() - } - }, [editor, isMobile, shapeId]) - - return null -} diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index a0f54a20e..ce8fbafa8 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -884,9 +884,14 @@ export class Editor extends EventEmitter { setCamera(point: VecLike, opts?: TLAnimationOptions & { immediate?: boolean; force?: boolean; + initial?: boolean; }): this; // (undocumented) - setCameraOptions(options: TLCameraOptions): this; + setCameraOptions(options: TLCameraOptions, opts?: { + immediate?: boolean; + force?: boolean; + initial?: boolean; + }): this; setCroppingShape(shape: null | TLShape | TLShapeId): this; setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this; setCurrentTool(id: string, info?: {}): this; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index ca8f9eefb..dcbd00f73 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -17075,7 +17075,7 @@ }, { "kind": "Content", - "text": " & {\n immediate?: boolean;\n force?: boolean;\n }" + "text": " & {\n immediate?: boolean;\n force?: boolean;\n initial?: boolean;\n }" }, { "kind": "Content", @@ -17134,6 +17134,14 @@ "text": "TLCameraOptions", "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Content", + "text": "{\n immediate?: boolean;\n force?: boolean;\n initial?: boolean;\n }" + }, { "kind": "Content", "text": "): " @@ -17149,8 +17157,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, @@ -17163,6 +17171,14 @@ "endIndex": 2 }, "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true } ], "isOptional": false, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 478f4ad65..2454e74c3 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2090,9 +2090,12 @@ export class Editor extends EventEmitter { } /** @public */ - setCameraOptions(options: TLCameraOptions) { + setCameraOptions( + options: TLCameraOptions, + opts?: { immediate?: boolean; force?: boolean; initial?: boolean } + ) { this._cameraOptions = options - this.setCamera(this.getCamera()) + this.setCamera(this.getCamera(), opts) return this } @@ -2201,44 +2204,46 @@ export class Editor extends EventEmitter { const freeH = (vsb.h - py * 2) / z - bounds.h x = clamp(x, minX + freeW * (z < zx || opts?.initial ? origin[1] : 1), minX) y = clamp(y, minY + freeH * (z < zy || opts?.initial ? origin[0] : 1), minY) - - // Update the point } } - point.set(x, y, z) - if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) { + if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) { return this } this.batch(() => { - this.store.put([{ ...currentCamera, ...point }]) // include id and meta here + this.store.put([{ ...currentCamera, x, y, z }]) // include id and meta here // 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 { currentScreenPoint, currentPagePoint } = this.inputs const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! - const event: TLPointerEventInfo = { - type: 'pointer', - target: 'canvas', - name: 'pointer_move', - // weird but true: we need to put the screen point back into client space - point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y), - pagePoint: this.inputs.currentPagePoint, - coalescedInfo: [], - pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE, - ctrlKey: this.inputs.ctrlKey, - altKey: this.inputs.altKey, - shiftKey: this.inputs.shiftKey, - button: 0, - isPen: this.getInstanceState().isPenMode ?? false, - } + // compare the next page point (derived from the curent camera) to the current page point + if ( + currentScreenPoint.x / z - x !== currentPagePoint.x || + currentScreenPoint.y / z - y !== currentPagePoint.y + ) { + // If it's changed, dispatch a pointer event + const event: TLPointerEventInfo = { + type: 'pointer', + target: 'canvas', + name: 'pointer_move', + // 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, + shiftKey: this.inputs.shiftKey, + button: 0, + isPen: this.getInstanceState().isPenMode ?? false, + } - if (opts?.immediate) { - this._flushEventForTick(event) - } else { - this.dispatch(event) + if (opts?.immediate) { + this._flushEventForTick(event) + } else { + this.dispatch(event) + } } this._tickCameraState() @@ -2264,7 +2269,7 @@ export class Editor extends EventEmitter { */ setCamera( point: VecLike, - opts?: TLAnimationOptions & { immediate?: boolean; force?: boolean } + opts?: TLAnimationOptions & { immediate?: boolean; force?: boolean; initial?: boolean } ): this { // Stop any camera animations this.stopCameraAnimation() @@ -2648,7 +2653,7 @@ export class Editor extends EventEmitter { if (isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() this.setCamera( - { x: (cx + offset.x * panSpeed) / cz, y: (cy + offset.y * panSpeed) / cz, z: cz }, + { x: cx + (offset.x * panSpeed) / cz, y: cy + (offset.y * panSpeed) / cz, z: cz }, animation ) this._flushEventsForTick(0) diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 4d52d86f9..73d6377b2 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -4,6 +4,7 @@ export class Dragging extends StateNode { static override id = 'dragging' camera = new Vec() + prev = new Vec() override onEnter = () => { const { editor } = this @@ -35,7 +36,11 @@ export class Dragging extends StateNode { private update() { const { editor } = this const { currentScreenPoint, originScreenPoint } = editor.inputs - this.editor.setCamera(Vec.Sub(currentScreenPoint, originScreenPoint).add(this.camera)) + const next = Vec.Sub(currentScreenPoint, originScreenPoint).add(this.camera) + if (next.equals(this.prev)) return + this.prev.setTo(next) + console.log('setting to', next.toJson()) + this.editor.setCamera({ x: next.x, y: next.y }) // leave out z } private complete() { From 22ef9e54a82648be2d367d841bff82da16e424d1 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Fri, 29 Mar 2024 17:17:52 +0000 Subject: [PATCH 42/82] ok --- .../src/examples/image-annotator/ImageAnnotationEditor.tsx | 2 +- packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index e68ed10b8..7faa5449d 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -134,7 +134,7 @@ export function ImageAnnotationEditor({ { type: 'contain', bounds: { w: image.width, h: image.height, x: 0, y: 0 }, - padding: [0, 0], + padding: [64, 32], origin: [0.5, 0.5], zoomMax: 8, zoomMin: 1, // prevent zoom from going below zero diff --git a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts index 73d6377b2..02ede4f1c 100644 --- a/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts +++ b/packages/tldraw/src/lib/tools/HandTool/childStates/Dragging.ts @@ -39,7 +39,6 @@ export class Dragging extends StateNode { const next = Vec.Sub(currentScreenPoint, originScreenPoint).add(this.camera) if (next.equals(this.prev)) return this.prev.setTo(next) - console.log('setting to', next.toJson()) this.editor.setCamera({ x: next.x, y: next.y }) // leave out z } From daaf33ddfff2faea57bf7d7661a193c0f279c3b1 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 15 Apr 2024 15:36:30 +0100 Subject: [PATCH 43/82] ok --- .../camera-options/CameraOptionsExample.tsx | 36 ++++---- .../image-annotator/ImageAnnotationEditor.tsx | 10 ++- packages/editor/api-report.md | 24 +++-- packages/editor/api/api.json | 88 ++++++------------- packages/editor/src/lib/TldrawEditor.tsx | 5 +- packages/editor/src/lib/constants.ts | 29 ++---- packages/editor/src/lib/editor/Editor.ts | 49 +++++------ .../editor/src/lib/editor/types/misc-types.ts | 29 +++--- .../tldraw/src/test/commands/zoomIn.test.ts | 4 +- .../tldraw/src/test/commands/zoomOut.test.ts | 2 +- 10 files changed, 112 insertions(+), 164 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index b8be85de1..db70d435c 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -2,19 +2,21 @@ import { TLCameraOptions, Tldraw } from 'tldraw' import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { - type: 'contain', - bounds: { - x: 0, - y: 0, - w: 1200, - h: 800, + constraints: { + type: 'contain', + bounds: { + x: 0, + y: 0, + w: 1200, + h: 800, + }, + padding: { x: 10, y: 200 }, + origin: { x: 0.5, y: 0.5 }, }, - origin: [0.5, 0.5], - padding: [200, 50], panSpeed: 1, - zoomSteps: [0.5, 0.75, 1, 1.5, 2], - zoomMax: 2, - zoomMin: 0.5, + zoomSteps: [0.1, 0.5, 0.75, 1, 1.5, 2, 8], + zoomMax: 8, + zoomMin: 0.1, zoomSpeed: 1, isLocked: false, } @@ -29,10 +31,12 @@ export default function CameraOptionsExample() { // These components are just included for debugging / visualization! OnTheCanvas: () => { // This component shows the bounds (in page space) - if (CAMERA_OPTIONS.type === 'infinite') return null + if (!CAMERA_OPTIONS.constraints) return null const { - bounds: { x, y, w, h }, + constraints: { + bounds: { x, y, w, h }, + }, } = CAMERA_OPTIONS return ( @@ -53,10 +57,12 @@ export default function CameraOptionsExample() { }, InFrontOfTheCanvas: () => { // This component shows the padding (in screen space) - if (CAMERA_OPTIONS.type === 'infinite') return null + if (!CAMERA_OPTIONS.constraints) return null const { - padding: [py, px], + constraints: { + padding: { x: px, y: py }, + }, } = CAMERA_OPTIONS if (!px && !py) return null diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index 7faa5449d..adb065170 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -132,10 +132,12 @@ export function ImageAnnotationEditor({ let isInitial = true editor.setCameraOptions( { - type: 'contain', - bounds: { w: image.width, h: image.height, x: 0, y: 0 }, - padding: [64, 32], - origin: [0.5, 0.5], + constraints: { + type: 'contain', + bounds: { w: image.width, h: image.height, x: 0, y: 0 }, + padding: { x: 32, y: 64 }, + origin: { x: 0.5, y: 0.5 }, + }, zoomMax: 8, zoomMin: 1, // prevent zoom from going below zero zoomSteps: [1, 2, 4, 8], diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index ce8fbafa8..bde808925 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -1087,9 +1087,7 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string; // @internal (undocumented) -export const getDefaultCameraOptions: (cameraOptions: Partial> & { - type: TLCameraOptions['type']; -}) => TLCameraOptions; +export const getDefaultCameraOptions: (cameraOptions?: Partial) => TLCameraOptions; // @public (undocumented) export function getFreshUserPreferences(): TLUserPreferences; @@ -2001,14 +1999,13 @@ export type TLCameraOptions = { zoomMin: number; zoomMax: number; isLocked: boolean; -} & ({ - type: 'contain' | 'cover' | 'limit'; - bounds: BoxModel; - padding: number[]; - origin: number[]; -} | { - type: 'infinite'; -}); + constraints?: { + type: 'contain' | 'cover' | 'limit'; + bounds: BoxModel; + padding: VecLike; + origin: VecLike; + }; +}; // @public (undocumented) export type TLCancelEvent = (info: TLCancelEventInfo) => void; @@ -2105,8 +2102,7 @@ export const TldrawEditor: React_2.NamedExoticComponent; // @public export interface TldrawEditorBaseProps { autoFocus?: boolean; - // (undocumented) - cameraOptions?: Partial> & Pick; + cameraOptions?: Partial; children?: ReactNode; className?: string; components?: TLEditorComponents; @@ -2137,7 +2133,7 @@ export type TLEditorComponents = Partial<{ // @public (undocumented) export interface TLEditorOptions { - cameraOptions?: Partial> & Pick; + cameraOptions?: Partial; getContainer: () => HTMLElement; inferDarkMode?: boolean; initialState?: string; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index dcbd00f73..1b1223e58 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36818,7 +36818,7 @@ }, { "kind": "Content", - "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n} & ({\n type: 'contain' | 'cover' | 'limit';\n bounds: " + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n constraints?: {\n type: 'contain' | 'cover' | 'limit';\n bounds: " }, { "kind": "Reference", @@ -36827,7 +36827,25 @@ }, { "kind": "Content", - "text": ";\n padding: number[];\n origin: number[];\n} | {\n type: 'infinite';\n})" + "text": ";\n padding: " + }, + { + "kind": "Reference", + "text": "VecLike", + "canonicalReference": "@tldraw/editor!VecLike:type" + }, + { + "kind": "Content", + "text": ";\n origin: " + }, + { + "kind": "Reference", + "text": "VecLike", + "canonicalReference": "@tldraw/editor!VecLike:type" + }, + { + "kind": "Content", + "text": ";\n };\n}" }, { "kind": "Content", @@ -36839,7 +36857,7 @@ "name": "TLCameraOptions", "typeTokenRange": { "startIndex": 1, - "endIndex": 4 + "endIndex": 8 } }, { @@ -37526,7 +37544,7 @@ { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TldrawEditorBaseProps#cameraOptions:member", - "docComment": "", + "docComment": "/**\n * Camera options for the editor.\n */\n", "excerptTokens": [ { "kind": "Content", @@ -37541,15 +37559,6 @@ "kind": "Content", "text": "<" }, - { - "kind": "Reference", - "text": "Exclude", - "canonicalReference": "!Exclude:type" - }, - { - "kind": "Content", - "text": "<" - }, { "kind": "Reference", "text": "TLCameraOptions", @@ -37557,25 +37566,7 @@ }, { "kind": "Content", - "text": ", 'type'>> & " - }, - { - "kind": "Reference", - "text": "Pick", - "canonicalReference": "!Pick:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!TLCameraOptions:type" - }, - { - "kind": "Content", - "text": ", 'type'>" + "text": ">" }, { "kind": "Content", @@ -37588,7 +37579,7 @@ "name": "cameraOptions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 11 + "endIndex": 5 } }, { @@ -38044,15 +38035,6 @@ "kind": "Content", "text": "<" }, - { - "kind": "Reference", - "text": "Exclude", - "canonicalReference": "!Exclude:type" - }, - { - "kind": "Content", - "text": "<" - }, { "kind": "Reference", "text": "TLCameraOptions", @@ -38060,25 +38042,7 @@ }, { "kind": "Content", - "text": ", 'type'>> & " - }, - { - "kind": "Reference", - "text": "Pick", - "canonicalReference": "!Pick:type" - }, - { - "kind": "Content", - "text": "<" - }, - { - "kind": "Reference", - "text": "TLCameraOptions", - "canonicalReference": "@tldraw/editor!TLCameraOptions:type" - }, - { - "kind": "Content", - "text": ", 'type'>" + "text": ">" }, { "kind": "Content", @@ -38091,7 +38055,7 @@ "name": "cameraOptions", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 11 + "endIndex": 5 } }, { diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index a758959ef..43a26be34 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -115,7 +115,10 @@ export interface TldrawEditorBaseProps { */ inferDarkMode?: boolean - cameraOptions?: Partial> & Pick + /** + * Camera options for the editor. + */ + cameraOptions?: Partial } /** diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index ebfb206c8..1337ed841 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -20,32 +20,13 @@ const DEFAULT_COMMON_CAMERA_OPTIONS = { isLocked: false, } -const DEFAULT_FIT_CONTAIN_CAMERA_OPTIONS = { - bounds: { x: 0, y: 0, w: 1200, h: 800 }, - padding: [0, 0], - origin: [0.5, 0.5], -} - /** @internal */ export const getDefaultCameraOptions = ( - cameraOptions: Partial> & { type: TLCameraOptions['type'] } -): TLCameraOptions => { - switch (cameraOptions.type) { - case 'infinite': { - return { - ...DEFAULT_COMMON_CAMERA_OPTIONS, - ...cameraOptions, - } - } - default: { - return { - ...DEFAULT_COMMON_CAMERA_OPTIONS, - ...DEFAULT_FIT_CONTAIN_CAMERA_OPTIONS, - ...cameraOptions, - } - } - } -} + cameraOptions: Partial = {} +): TLCameraOptions => ({ + ...DEFAULT_COMMON_CAMERA_OPTIONS, + ...cameraOptions, +}) /** @internal */ export const FOLLOW_CHASE_PROPORTION = 0.5 diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 2454e74c3..d56e4dab5 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -185,7 +185,7 @@ export interface TLEditorOptions { /** * Options for the editor's camera. */ - cameraOptions?: Partial> & Pick + cameraOptions?: Partial } /** @public */ @@ -206,9 +206,7 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions = cameraOptions?.type - ? getDefaultCameraOptions(cameraOptions) - : getDefaultCameraOptions({ type: 'infinite' }) + this._cameraOptions = getDefaultCameraOptions(cameraOptions) this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2078,15 +2076,15 @@ export class Editor extends EventEmitter { getNaturalZoom() { const cameraOptions = this.getCameraOptions() - if (cameraOptions.type === 'infinite') return 1 - const { padding } = cameraOptions + if (!cameraOptions.constraints) return 1 + const { padding } = cameraOptions.constraints const vsb = this.getViewportScreenBounds() - const py = Math.min(padding[0], vsb.w / 2) - const px = Math.min(padding[1], vsb.h / 2) - const bounds = Box.From(cameraOptions.bounds) + const py = Math.min(padding.y, vsb.w / 2) + const px = Math.min(padding.x, vsb.h / 2) + const bounds = Box.From(cameraOptions.constraints.bounds) const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - return cameraOptions.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + return cameraOptions.constraints.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) } /** @public */ @@ -2141,22 +2139,18 @@ export class Editor extends EventEmitter { const cameraOptions = this.getCameraOptions() // If bounds are provided, then we'll keep those bounds on screen - if (cameraOptions.type === 'infinite') { - // constrain the zoom - const { zoomMax, zoomMin } = cameraOptions - z = clamp(point.z, zoomMin, zoomMax) - } else { - const { zoomMax, zoomMin, padding, origin } = cameraOptions + if (cameraOptions.constraints) { + const { zoomMax, zoomMin, constraints: constriants } = cameraOptions const vsb = this.getViewportScreenBounds() // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) // Clamp padding to half the viewport size on either dimension - const py = Math.min(padding[0], vsb.w / 2) - const px = Math.min(padding[1], vsb.h / 2) + const py = Math.min(constriants.padding.y, vsb.w / 2) + const px = Math.min(constriants.padding.x, vsb.h / 2) // Expand the bounds by the padding - const bounds = Box.From(cameraOptions.bounds) + const bounds = Box.From(cameraOptions.constraints.bounds) // For each axis, the "natural zoom" is the zoom at // which the expanded bounds (with padding) would fit @@ -2166,10 +2160,11 @@ export class Editor extends EventEmitter { const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h + const fitZoom = - cameraOptions.type === 'limit' + cameraOptions.constraints.type === 'limit' ? 1 - : cameraOptions.type === 'contain' + : cameraOptions.constraints.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) const maxZ = zoomMax * fitZoom @@ -2202,8 +2197,12 @@ export class Editor extends EventEmitter { const minY = py / z - bounds.y const freeW = (vsb.w - px * 2) / z - bounds.w const freeH = (vsb.h - py * 2) / z - bounds.h - x = clamp(x, minX + freeW * (z < zx || opts?.initial ? origin[1] : 1), minX) - y = clamp(y, minY + freeH * (z < zy || opts?.initial ? origin[0] : 1), minY) + x = clamp(x, minX + freeW * (z < zx || opts?.initial ? constriants.origin.x : 1), minX) + y = clamp(y, minY + freeH * (z < zy || opts?.initial ? constriants.origin.y : 1), minY) + } else { + // constrain the zoom + const { zoomMax, zoomMin } = cameraOptions + z = clamp(point.z, zoomMin, zoomMax) } } @@ -2386,7 +2385,7 @@ export class Editor extends EventEmitter { * @public */ resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - const { isLocked, type } = this.getCameraOptions() + const { isLocked, constraints: constriants } = this.getCameraOptions() if (isLocked) return this const currentCamera = this.getCamera() @@ -2395,7 +2394,7 @@ export class Editor extends EventEmitter { let z = 1 - if (type !== 'infinite') { + if (constriants) { // For non-infinite fit, we'll set the camera to the natural zoom level... // unless it's already there, in which case we'll set zoom to 100% const naturalZoom = this.getNaturalZoom() diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 9602682f7..cf32733a0 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -1,5 +1,6 @@ import { BoxModel } from '@tldraw/tlschema' import { Box } from '../../primitives/Box' +import { VecLike } from '../../primitives/Vec' /** @public */ export type RequiredKeys = Partial> & Pick @@ -30,19 +31,15 @@ export type TLCameraOptions = { zoomMax: number /** Whether the camera is locked */ isLocked: boolean -} & ( - | { - /** The type of behavior. */ - type: 'infinite' - } - | { - /** The type of behavior. */ - type: 'contain' | 'cover' | 'limit' - /** The bounds of the content (in page space) */ - bounds: BoxModel - /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ - padding: number[] - /** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/ - origin: number[] - } -) + /** The camera constraints */ + constraints?: { + /** The type of constraint behavior. */ + type: 'contain' | 'cover' | 'limit' + /** The bounds of the content (in page space) */ + bounds: BoxModel + /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ + padding: VecLike + /** The origin for placement when the bounds are smaller than the viewport. Provide a number for x and y, or [x, y].*/ + origin: VecLike + } +} diff --git a/packages/tldraw/src/test/commands/zoomIn.test.ts b/packages/tldraw/src/test/commands/zoomIn.test.ts index 2337cdc12..c3ca247cd 100644 --- a/packages/tldraw/src/test/commands/zoomIn.test.ts +++ b/packages/tldraw/src/test/commands/zoomIn.test.ts @@ -8,7 +8,7 @@ beforeEach(() => { }) it('zooms by increments', () => { - const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) + const cameraOptions = getDefaultCameraOptions() // Starts at 1 expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) @@ -46,7 +46,7 @@ it('preserves the screen center when offset', () => { }) it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => { - const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) + const cameraOptions = getDefaultCameraOptions() editor.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 }) editor.zoomIn() diff --git a/packages/tldraw/src/test/commands/zoomOut.test.ts b/packages/tldraw/src/test/commands/zoomOut.test.ts index 75372add9..1a5c1453f 100644 --- a/packages/tldraw/src/test/commands/zoomOut.test.ts +++ b/packages/tldraw/src/test/commands/zoomOut.test.ts @@ -8,7 +8,7 @@ beforeEach(() => { }) it('zooms out and in by increments', () => { - const cameraOptions = getDefaultCameraOptions({ type: 'infinite' }) + const cameraOptions = getDefaultCameraOptions() // Starts at 1 expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) From a97371379e8ee18bb33830836adcaca56fd46bf3 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 15 Apr 2024 15:58:27 +0100 Subject: [PATCH 44/82] update fit --- .../camera-options/CameraOptionsExample.tsx | 15 +++-- .../image-annotator/ImageAnnotationEditor.tsx | 2 +- packages/editor/api-report.md | 6 +- packages/editor/api/api.json | 64 +++++++++---------- packages/editor/src/lib/editor/Editor.ts | 64 +++++++++++++++---- .../editor/src/lib/editor/types/misc-types.ts | 2 +- 6 files changed, 98 insertions(+), 55 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index db70d435c..e8f7bfb0b 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -3,7 +3,7 @@ import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { constraints: { - type: 'contain', + fit: 'max', bounds: { x: 0, y: 0, @@ -48,8 +48,15 @@ export default function CameraOptionsExample() { left: x, width: w, height: h, - background: 'white', - border: '1px dashed black', + // grey and white stripes + border: '1px dashed var(--color-text)', + backgroundImage: ` + linear-gradient(45deg, #AAAAAA22 25%, transparent 25%), + linear-gradient(-45deg, #AAAAAA22 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #AAAAAA22 75%), + linear-gradient(-45deg, transparent 75%, #AAAAAA22 75%)`, + backgroundSize: '200px 200px', + backgroundPosition: '0 0, 0 100px, 100px -100px, -100px 0px', }} /> @@ -76,7 +83,7 @@ export default function CameraOptionsExample() { left: px, width: `calc(100% - ${px * 2}px)`, height: `calc(100% - ${py * 2}px)`, - border: '1px dotted black', + border: '1px dotted var(--color-text)', }} /> diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index adb065170..26f943c0b 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -133,7 +133,7 @@ export function ImageAnnotationEditor({ editor.setCameraOptions( { constraints: { - type: 'contain', + fit: 'max', bounds: { w: image.width, h: image.height, x: 0, y: 0 }, padding: { x: 32, y: 64 }, origin: { x: 0.5, y: 0.5 }, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index bde808925..eec5cbb90 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -668,6 +668,8 @@ export class Editor extends EventEmitter { getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[]; getCamera(): TLCamera; // (undocumented) + getCameraFitZoom(): number; + // (undocumented) getCameraOptions(): TLCameraOptions; getCameraState(): "idle" | "moving"; getCanRedo(): boolean; @@ -703,8 +705,6 @@ export class Editor extends EventEmitter { getInitialMetaForShape(_shape: TLShape): JsonObject; getInstanceState(): TLInstance; getIsMenuOpen(): boolean; - // (undocumented) - getNaturalZoom(): number; getOnlySelectedShape(): null | TLShape; getOpenMenus(): string[]; getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape; @@ -2000,7 +2000,7 @@ export type TLCameraOptions = { zoomMax: number; isLocked: boolean; constraints?: { - type: 'contain' | 'cover' | 'limit'; + fit: 'max' | 'min' | 'none' | 'x' | 'y'; bounds: BoxModel; padding: VecLike; origin: VecLike; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 1b1223e58..c6642bc61 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -10053,6 +10053,37 @@ "isAbstract": false, "name": "getCamera" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#getCameraFitZoom:member(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "getCameraFitZoom(): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [], + "isOptional": false, + "isAbstract": false, + "name": "getCameraFitZoom" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)", @@ -11335,37 +11366,6 @@ "isAbstract": false, "name": "getIsMenuOpen" }, - { - "kind": "Method", - "canonicalReference": "@tldraw/editor!Editor#getNaturalZoom:member(1)", - "docComment": "", - "excerptTokens": [ - { - "kind": "Content", - "text": "getNaturalZoom(): " - }, - { - "kind": "Content", - "text": "number" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [], - "isOptional": false, - "isAbstract": false, - "name": "getNaturalZoom" - }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getOnlySelectedShape:member(1)", @@ -36818,7 +36818,7 @@ }, { "kind": "Content", - "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n constraints?: {\n type: 'contain' | 'cover' | 'limit';\n bounds: " + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n constraints?: {\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n bounds: " }, { "kind": "Reference", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d56e4dab5..d91e77bc5 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -46,6 +46,7 @@ import { assert, compact, dedupe, + exhaustiveSwitchError, getIndexAbove, getIndexBetween, getIndices, @@ -2074,9 +2075,11 @@ export class Editor extends EventEmitter { return this._cameraOptions } - getNaturalZoom() { + getCameraFitZoom() { const cameraOptions = this.getCameraOptions() - if (!cameraOptions.constraints) return 1 + if (!cameraOptions.constraints || cameraOptions.constraints.fit === 'none') { + return 1 + } const { padding } = cameraOptions.constraints const vsb = this.getViewportScreenBounds() const py = Math.min(padding.y, vsb.w / 2) @@ -2084,7 +2087,24 @@ export class Editor extends EventEmitter { const bounds = Box.From(cameraOptions.constraints.bounds) const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - return cameraOptions.constraints.type === 'contain' ? Math.min(zx, zy) : Math.max(zx, zy) + + switch (cameraOptions.constraints.fit) { + case 'min': { + return Math.max(zx, zy) + } + case 'max': { + return Math.min(zx, zy) + } + case 'x': { + return zx + } + case 'y': { + return zy + } + default: { + throw exhaustiveSwitchError(cameraOptions.constraints.fit) + } + } } /** @public */ @@ -2161,12 +2181,27 @@ export class Editor extends EventEmitter { const zx = (vsb.w - px * 2) / bounds.w const zy = (vsb.h - py * 2) / bounds.h - const fitZoom = - cameraOptions.constraints.type === 'limit' - ? 1 - : cameraOptions.constraints.type === 'contain' - ? Math.min(zx, zy) - : Math.max(zx, zy) + let fitZoom = 1 + + switch (cameraOptions.constraints.fit) { + case 'min': { + fitZoom = Math.max(zx, zy) + break + } + case 'max': { + fitZoom = Math.min(zx, zy) + break + } + case 'x': { + fitZoom = zx + break + } + case 'y': { + fitZoom = zy + break + } + } + const maxZ = zoomMax * fitZoom const minZ = zoomMin * fitZoom @@ -2397,7 +2432,8 @@ export class Editor extends EventEmitter { if (constriants) { // For non-infinite fit, we'll set the camera to the natural zoom level... // unless it's already there, in which case we'll set zoom to 100% - const naturalZoom = this.getNaturalZoom() + const naturalZoom = this.getCameraFitZoom() + console.log(naturalZoom) if (cz !== naturalZoom) { z = naturalZoom } @@ -2432,7 +2468,7 @@ export class Editor extends EventEmitter { const { zoomMax, zoomSteps } = this.getCameraOptions() if (zoomSteps === null || zoomSteps.length <= 1) return this - const naturalZoom = this.getNaturalZoom() + const naturalZoom = this.getCameraFitZoom() let zoom = zoomMax * naturalZoom @@ -2473,7 +2509,7 @@ export class Editor extends EventEmitter { const { zoomMin, zoomSteps } = this.getCameraOptions() if (zoomSteps === null || zoomSteps.length <= 1) return this - const naturalZoom = this.getNaturalZoom() + const naturalZoom = this.getCameraFitZoom() const { x: cx, y: cy, z: cz } = this.getCamera() @@ -2607,7 +2643,7 @@ export class Editor extends EventEmitter { const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28) - const naturalZoom = this.getNaturalZoom() + const naturalZoom = this.getCameraFitZoom() const { zoomMin, zoomMax } = this.getCameraOptions() let zoom = clamp( @@ -3157,7 +3193,7 @@ export class Editor extends EventEmitter { ? Math.min(width / desiredWidth, height / desiredHeight) : height / desiredHeight - const naturalZoom = this.getNaturalZoom() + const naturalZoom = this.getCameraFitZoom() const { zoomMin, zoomMax } = this.getCameraOptions() const targetZoom = clamp( this.getCamera().z * ratio, diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index cf32733a0..2a5e12962 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -34,7 +34,7 @@ export type TLCameraOptions = { /** The camera constraints */ constraints?: { /** The type of constraint behavior. */ - type: 'contain' | 'cover' | 'limit' + fit: 'min' | 'max' | 'x' | 'y' | 'none' /** The bounds of the content (in page space) */ bounds: BoxModel /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ From 4bc371a326379aace8676a789cfe1bde707d83bb Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 15 Apr 2024 19:02:51 +0100 Subject: [PATCH 45/82] add more options --- .../camera-options/CameraOptionsExample.tsx | 348 ++++++++++++++---- .../image-annotator/ImageAnnotationEditor.tsx | 2 + packages/editor/api-report.md | 2 + packages/editor/api/api.json | 2 +- packages/editor/src/lib/editor/Editor.ts | 144 +++++--- .../editor/src/lib/editor/types/misc-types.ts | 4 + 6 files changed, 386 insertions(+), 116 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index e8f7bfb0b..f612b8c73 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -1,4 +1,5 @@ -import { TLCameraOptions, Tldraw } from 'tldraw' +import { useEffect } from 'react' +import { TLCameraOptions, Tldraw, clamp, track, useEditor, useLocalStorageState } from 'tldraw' import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { @@ -10,7 +11,9 @@ const CAMERA_OPTIONS: TLCameraOptions = { w: 1200, h: 800, }, - padding: { x: 10, y: 200 }, + fitX: 'outside', + fitY: 'lock', + padding: { x: 100, y: 100 }, origin: { x: 0.5, y: 0.5 }, }, panSpeed: 1, @@ -25,72 +28,281 @@ export default function CameraOptionsExample() { return (
{ - // This component shows the bounds (in page space) - if (!CAMERA_OPTIONS.constraints) return null - - const { - constraints: { - bounds: { x, y, w, h }, - }, - } = CAMERA_OPTIONS - - return ( - <> -
- - ) - }, - InFrontOfTheCanvas: () => { - // This component shows the padding (in screen space) - if (!CAMERA_OPTIONS.constraints) return null - - const { - constraints: { - padding: { x: px, y: py }, - }, - } = CAMERA_OPTIONS - - if (!px && !py) return null - - return ( - <> -
- - ) - }, - }} - /> + // persistenceKey="camera-options" + components={components} + > + +
) } + +const PaddingDisplay = track(() => { + const editor = useEditor() + const cameraOptions = editor.getCameraOptions() + + if (!cameraOptions.constraints) return null + + const { + constraints: { + padding: { x: px, y: py }, + }, + } = cameraOptions + + return ( +
+ ) +}) + +const BoundsDisplay = track(() => { + const editor = useEditor() + const cameraOptions = editor.getCameraOptions() + + if (!cameraOptions.constraints) return null + + const { + constraints: { + bounds: { x, y, w, h }, + }, + } = cameraOptions + + return ( + <> +
+ + ) +}) + +const components = { + // These components are just included for debugging / visualization! + OnTheCanvas: BoundsDisplay, + InFrontOfTheCanvas: PaddingDisplay, +} + +const CameraOptionsControlPanel = track(() => { + const editor = useEditor() + + const [cameraOptions, setCameraOptions] = useLocalStorageState('camera ex', CAMERA_OPTIONS) + + useEffect(() => { + if (!editor) return + editor.setCameraOptions(cameraOptions, { immediate: true }) + }, [editor, cameraOptions]) + + const { constraints } = cameraOptions + + const updateOptions = ( + options: Partial< + Omit & { + constraints: Partial + } + > + ) => { + const cameraOptions = editor.getCameraOptions() + setCameraOptions({ + ...cameraOptions, + ...options, + constraints: options.constraints + ? { + ...cameraOptions.constraints!, + ...options.constraints, + } + : undefined, + }) + } + + return ( +
+ {constraints ? ( + <> +
+ + + + { + const val = clamp(Number(e.target.value), 0, 1) + updateOptions({ + constraints: { + origin: { + ...constraints.origin, + x: val, + }, + }, + }) + }} + /> + + { + const val = clamp(Number(e.target.value), 0, 1) + updateOptions({ + constraints: { + ...constraints, + origin: { + ...constraints.origin, + y: val, + }, + }, + }) + }} + /> + + { + const val = clamp(Number(e.target.value), 0) + updateOptions({ + constraints: { + ...constraints, + padding: { + ...constraints.padding, + x: val, + }, + }, + }) + }} + /> + + { + const val = clamp(Number(e.target.value), 0) + updateOptions({ + constraints: { + padding: { + ...constraints.padding, + y: val, + }, + }, + }) + }} + /> + + + + +
+ + ) : null} + +
+ ) +}) diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index 26f943c0b..90641f712 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -137,6 +137,8 @@ export function ImageAnnotationEditor({ bounds: { w: image.width, h: image.height, x: 0, y: 0 }, padding: { x: 32, y: 64 }, origin: { x: 0.5, y: 0.5 }, + fitX: 'inside', + fitY: 'inside', }, zoomMax: 8, zoomMin: 1, // prevent zoom from going below zero diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index eec5cbb90..24989bf47 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2001,6 +2001,8 @@ export type TLCameraOptions = { isLocked: boolean; constraints?: { fit: 'max' | 'min' | 'none' | 'x' | 'y'; + fitX: 'contain' | 'inside' | 'lock' | 'outside'; + fitY: 'contain' | 'inside' | 'lock' | 'outside'; bounds: BoxModel; padding: VecLike; origin: VecLike; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index c6642bc61..722708232 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36818,7 +36818,7 @@ }, { "kind": "Content", - "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n constraints?: {\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n bounds: " + "text": "{\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n zoomMin: number;\n zoomMax: number;\n isLocked: boolean;\n constraints?: {\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n fitX: 'contain' | 'inside' | 'lock' | 'outside';\n fitY: 'contain' | 'inside' | 'lock' | 'outside';\n bounds: " }, { "kind": "Reference", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index d91e77bc5..1295a8e99 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -207,7 +207,7 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions = getDefaultCameraOptions(cameraOptions) + this._cameraOptions.set(getDefaultCameraOptions(cameraOptions)) this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2068,11 +2068,11 @@ export class Editor extends EventEmitter { /* --------------------- Camera --------------------- */ - private _cameraOptions: TLCameraOptions + private _cameraOptions = atom('camera options', getDefaultCameraOptions({})) /** @public */ getCameraOptions() { - return this._cameraOptions + return this._cameraOptions.get() } getCameraFitZoom() { @@ -2112,7 +2112,7 @@ export class Editor extends EventEmitter { options: TLCameraOptions, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } ) { - this._cameraOptions = options + this._cameraOptions.set(options) this.setCamera(this.getCamera(), opts) return this } @@ -2160,14 +2160,14 @@ export class Editor extends EventEmitter { // If bounds are provided, then we'll keep those bounds on screen if (cameraOptions.constraints) { - const { zoomMax, zoomMin, constraints: constriants } = cameraOptions + const { zoomMax, zoomMin, constraints } = cameraOptions const vsb = this.getViewportScreenBounds() // Get padding (it's either a number or an array of 2 numbers for t/b, l/r) // Clamp padding to half the viewport size on either dimension - const py = Math.min(constriants.padding.y, vsb.w / 2) - const px = Math.min(constriants.padding.x, vsb.h / 2) + const py = Math.min(constraints.padding.y, vsb.w / 2) + const px = Math.min(constraints.padding.x, vsb.h / 2) // Expand the bounds by the padding const bounds = Box.From(cameraOptions.constraints.bounds) @@ -2213,31 +2213,86 @@ export class Editor extends EventEmitter { // We're trying to zoom out past the minimum zoom level, // or in past the maximum zoom level, so stop the camera // but keep the current center - const cxA = -currentCamera.x + vsb.w / currentCamera.z / 2 - const cyA = -currentCamera.y + vsb.h / currentCamera.z / 2 + const { x: cx, y: cy, z: cz } = currentCamera + const cxA = -cx + vsb.w / cz / 2 + const cyA = -cy + vsb.h / cz / 2 z = clamp(z, minZ, maxZ) - const cxB = -currentCamera.x + vsb.w / z / 2 - const cyB = -currentCamera.y + vsb.h / z / 2 - x = currentCamera.x + cxB - cxA - y = currentCamera.y + cyB - cyA + const cxB = -cx + vsb.w / z / 2 + const cyB = -cy + vsb.h / z / 2 + x = cx + cxB - cxA + y = cy + cyB - cyA } - // Math salad time. (ง •̀_•́)ง - // For each axis... if we're doing the initial camera position, or - // if we're below the natural zoom for the axis: clamp it with bounds - // plus padding in page space; or else use the origin for that axis - // to decide where to put the content. - const minX = px / z - bounds.x - const minY = py / z - bounds.y const freeW = (vsb.w - px * 2) / z - bounds.w + + if (opts?.initial) { + x = minX + freeW * constraints.origin.x + } else { + switch (constraints.fitX) { + case 'contain': { + x = clamp(x, minX + freeW * (z < zx ? constraints.origin.x : 1), minX) + break + } + case 'inside': { + x = + z < zx + ? clamp(x, px / z - bounds.x, (vsb.w - px) / z - bounds.w) + : clamp(x, minX + freeW, minX) + break + } + case 'outside': { + x = clamp(x, px / z - bounds.w, (vsb.w - px) / z) + break + } + case 'lock': { + x = minX + freeW * constraints.origin.x + break + } + } + } + + const minY = py / z - bounds.y const freeH = (vsb.h - py * 2) / z - bounds.h - x = clamp(x, minX + freeW * (z < zx || opts?.initial ? constriants.origin.x : 1), minX) - y = clamp(y, minY + freeH * (z < zy || opts?.initial ? constriants.origin.y : 1), minY) + if (opts?.initial) { + y = minY + freeH * constraints.origin.y + } else { + switch (constraints.fitY) { + case 'contain': { + y = clamp(y, minY + freeH * (z < zy ? constraints.origin.y : 1), minY) + break + } + case 'inside': { + y = + z < zy + ? clamp(y, py / z - bounds.y, (vsb.h - py) / z - bounds.h) + : clamp(y, minY + freeH, minY) + break + } + case 'outside': { + y = clamp(y, py / z - bounds.h, (vsb.h - py) / z) + break + } + case 'lock': { + y = minY + freeH * constraints.origin.y + break + } + } + } } else { - // constrain the zoom + // constrain the zoom, preserving the center const { zoomMax, zoomMin } = cameraOptions - z = clamp(point.z, zoomMin, zoomMax) + if (z > zoomMax || z < zoomMin) { + const vsb = this.getViewportScreenBounds() + const { x: cx, y: cy, z: cz } = currentCamera + const cxA = -cx + vsb.w / cz / 2 + const cyA = -cy + vsb.h / cz / 2 + z = clamp(point.z, zoomMin, zoomMax) + const cxB = -cx + vsb.w / z / 2 + const cyB = -cy + vsb.h / z / 2 + x = cx + cxB - cxA + y = cy + cyB - cyA + } } } @@ -2420,7 +2475,7 @@ export class Editor extends EventEmitter { * @public */ resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { - const { isLocked, constraints: constriants } = this.getCameraOptions() + const { isLocked, constraints: constraints } = this.getCameraOptions() if (isLocked) return this const currentCamera = this.getCamera() @@ -2429,13 +2484,12 @@ export class Editor extends EventEmitter { let z = 1 - if (constriants) { + if (constraints) { // For non-infinite fit, we'll set the camera to the natural zoom level... // unless it's already there, in which case we'll set zoom to 100% - const naturalZoom = this.getCameraFitZoom() - console.log(naturalZoom) - if (cz !== naturalZoom) { - z = naturalZoom + const fitZoom = this.getCameraFitZoom() + if (cz !== fitZoom) { + z = fitZoom } } @@ -2468,13 +2522,13 @@ export class Editor extends EventEmitter { const { zoomMax, zoomSteps } = this.getCameraOptions() if (zoomSteps === null || zoomSteps.length <= 1) return this - const naturalZoom = this.getCameraFitZoom() + const fitZoom = this.getCameraFitZoom() - let zoom = zoomMax * naturalZoom + let zoom = zoomMax * fitZoom for (let i = 1; i < zoomSteps.length; i++) { - const z1 = zoomSteps[i - 1] * naturalZoom - const z2 = zoomSteps[i] * naturalZoom + const z1 = zoomSteps[i - 1] * fitZoom + const z2 = zoomSteps[i] * fitZoom if (z2 - cz <= (z2 - z1) / 2) continue zoom = z2 break @@ -2509,15 +2563,15 @@ export class Editor extends EventEmitter { const { zoomMin, zoomSteps } = this.getCameraOptions() if (zoomSteps === null || zoomSteps.length <= 1) return this - const naturalZoom = this.getCameraFitZoom() + const fitZoom = this.getCameraFitZoom() const { x: cx, y: cy, z: cz } = this.getCamera() - let zoom = zoomMin * naturalZoom + let zoom = zoomMin * fitZoom for (let i = zoomSteps.length - 1; i > 0; i--) { - const z1 = zoomSteps[i - 1] * naturalZoom - const z2 = zoomSteps[i] * naturalZoom + const z1 = zoomSteps[i - 1] * fitZoom + const z2 = zoomSteps[i] * fitZoom if (z2 - cz >= (z2 - z1) / 2) continue zoom = z1 break @@ -2643,7 +2697,7 @@ export class Editor extends EventEmitter { const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28) - const naturalZoom = this.getCameraFitZoom() + const fitZoom = this.getCameraFitZoom() const { zoomMin, zoomMax } = this.getCameraOptions() let zoom = clamp( @@ -2651,8 +2705,8 @@ export class Editor extends EventEmitter { (viewportScreenBounds.width - inset) / bounds.width, (viewportScreenBounds.height - inset) / bounds.height ), - zoomMin * naturalZoom, - zoomMax * naturalZoom + zoomMin * fitZoom, + zoomMax * fitZoom ) if (opts?.targetZoom !== undefined) { @@ -3193,13 +3247,9 @@ export class Editor extends EventEmitter { ? Math.min(width / desiredWidth, height / desiredHeight) : height / desiredHeight - const naturalZoom = this.getCameraFitZoom() + const fitZoom = this.getCameraFitZoom() const { zoomMin, zoomMax } = this.getCameraOptions() - const targetZoom = clamp( - this.getCamera().z * ratio, - zoomMin * naturalZoom, - zoomMax * naturalZoom - ) + const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * fitZoom, zoomMax * fitZoom) const targetWidth = this.getViewportScreenBounds().w / targetZoom const targetHeight = this.getViewportScreenBounds().h / targetZoom diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 2a5e12962..e900d1afe 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -35,6 +35,10 @@ export type TLCameraOptions = { constraints?: { /** The type of constraint behavior. */ fit: 'min' | 'max' | 'x' | 'y' | 'none' + /** The behavior for the constraints on the x axis. */ + fitX: 'contain' | 'inside' | 'outside' | 'lock' + /** The behavior for the constraints on the y axis. */ + fitY: 'contain' | 'inside' | 'outside' | 'lock' /** The bounds of the content (in page space) */ bounds: BoxModel /** The padding around the bounds (in screen space). Provide a number for x and y, or [x, y]. */ From b8aa58252fdeeff249f54e4bb71f9718668c35cc Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 15 Apr 2024 19:25:57 +0100 Subject: [PATCH 46/82] Update useUrlState.ts --- apps/dotcom/src/hooks/useUrlState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dotcom/src/hooks/useUrlState.ts b/apps/dotcom/src/hooks/useUrlState.ts index f8ed3c8a0..20fb3353a 100644 --- a/apps/dotcom/src/hooks/useUrlState.ts +++ b/apps/dotcom/src/hooks/useUrlState.ts @@ -68,7 +68,7 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) { const viewport = viewportFromString(newViewportRaw) const { x, y, w, h } = viewport const { w: sw, h: sh } = editor.getViewportScreenBounds() - const naturalZoom = editor.getNaturalZoom() + const naturalZoom = editor.getCameraFitZoom() const { zoomMin, zoomMax } = editor.getCameraOptions() const zoom = Math.min( From f2efe7e63eb6615fc40be79b28d5a1fc4e450d40 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 15 Apr 2024 19:36:20 +0100 Subject: [PATCH 47/82] update defaults --- .../src/examples/camera-options/CameraOptionsExample.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index f612b8c73..aa2c55a29 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -11,13 +11,13 @@ const CAMERA_OPTIONS: TLCameraOptions = { w: 1200, h: 800, }, - fitX: 'outside', - fitY: 'lock', + fitX: 'contain', + fitY: 'contain', padding: { x: 100, y: 100 }, origin: { x: 0.5, y: 0.5 }, }, panSpeed: 1, - zoomSteps: [0.1, 0.5, 0.75, 1, 1.5, 2, 8], + zoomSteps: [0.1, 0.5, 0.75, 1, 1.5, 2, 4, 8], zoomMax: 8, zoomMin: 0.1, zoomSpeed: 1, From 55da75a498a5edf4f920c2f31c9ac7a8ba20af00 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 11:12:25 +0100 Subject: [PATCH 48/82] ok --- packages/editor/src/index.ts | 1 - packages/editor/src/lib/editor/Editor.ts | 16 ---------------- packages/editor/src/lib/hooks/useCanvasEvents.ts | 9 --------- packages/editor/src/lib/hooks/useHandleEvents.ts | 2 -- .../editor/src/lib/hooks/useSelectionEvents.ts | 2 -- .../lib/ui/components/Minimap/DefaultMinimap.tsx | 2 -- 6 files changed, 32 deletions(-) diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index b36ffc388..14a87f266 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -222,7 +222,6 @@ export { type TLPointerEventInfo, type TLPointerEventName, type TLPointerEventTarget, - type TLPointerMoveEventInfo, type TLTickEvent, type TLWheelEvent, type TLWheelEventInfo, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 22daf308e..14a51e43b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -126,7 +126,6 @@ import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo, - TLPointerMoveEventInfo, TLWheelEventInfo, } from './types/event-types' import { TLExternalAssetContent, TLExternalContent } from './types/external-content' @@ -8638,21 +8637,6 @@ export class Editor extends EventEmitter { /** @internal */ capturedPointerId: number | null = null - private _shouldCoalesce = (info: TLEventInfo) => { - if (!this._isCoalesableEvent(info)) return false - return ( - this._pendingEventsForNextTick.length === 1 && - this._pendingEventsForNextTick[0].name === info.name - ) - } - - private _isCoalesableEvent = (info: TLEventInfo): info is TLPointerMoveEventInfo => { - if ((info as any).coalescedInfo) { - return true - } - return false - } - /** * Dispatch an event to the editor. * diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 12faa68c4..26b6b05da 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -1,5 +1,4 @@ import React, { useMemo } from 'react' -import { Vec } from '../primitives/Vec' import { preventDefault, releasePointerCapture, @@ -58,18 +57,10 @@ export function useCanvasEvents() { lastX = e.clientX lastY = e.clientY - const { screenBounds } = editor.getInstanceState() - const { x: cx, y: cy, z: cz } = editor.getCamera() - const sx = lastX - screenBounds.x - const sy = lastY - screenBounds.y - const sz = e.pressure - editor.dispatch({ type: 'pointer', target: 'canvas', name: 'pointer_move', - pagePoint: new Vec(sx / cz - cx, sy / cz - cy, sz ?? 0.5), - coalescedInfo: [], ...getPointerInfo(e), }) } diff --git a/packages/editor/src/lib/hooks/useHandleEvents.ts b/packages/editor/src/lib/hooks/useHandleEvents.ts index 94f651b24..3fb42597e 100644 --- a/packages/editor/src/lib/hooks/useHandleEvents.ts +++ b/packages/editor/src/lib/hooks/useHandleEvents.ts @@ -54,8 +54,6 @@ export function useHandleEvents(id: TLShapeId, handleId: string) { target: 'handle', handle, shape, - pagePoint: editor.inputs.currentPagePoint.clone(), - coalescedInfo: [], name: 'pointer_move', ...getPointerInfo(e), }) diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index ec6c457f0..993330b2e 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -71,8 +71,6 @@ export function useSelectionEvents(handle: TLSelectionHandle) { type: 'pointer', target: 'selection', handle, - pagePoint: editor.inputs.currentPagePoint.clone(), - coalescedInfo: [], ...getPointerInfo(e), }) } diff --git a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx index 89965f9e5..a84eba262 100644 --- a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx +++ b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx @@ -126,8 +126,6 @@ export function DefaultMinimap() { ...getPointerInfo(e), point: screenPoint, isPen: editor.getInstanceState().isPenMode, - pagePoint: editor.inputs.currentPagePoint.clone(), - coalescedInfo: [], } editor.dispatch(info) From 2961b2e077ba6ab57c72dbe77ffb5d6fb393a619 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 14:27:42 +0100 Subject: [PATCH 49/82] docs and stuff --- .../components/PeopleMenu/PeopleMenuItem.tsx | 2 +- apps/dotcom/src/hooks/useUrlState.ts | 22 +- .../AfterCreateUpdateShapeExample.tsx | 2 +- .../AfterDeleteShapeExample.tsx | 2 +- .../BeforeDeleteShapeExample.tsx | 2 +- .../editable-shape/EditableShapeUtil.tsx | 2 +- packages/editor/api-report.md | 94 ++- packages/editor/api/api.json | 523 ++++++------ packages/editor/src/index.ts | 3 +- packages/editor/src/lib/constants.ts | 10 - packages/editor/src/lib/editor/Editor.ts | 742 ++++++++++-------- .../editor/src/lib/utils/edgeScrolling.ts | 7 +- .../src/lib/shapes/shared/ShapeFill.tsx | 4 +- .../lib/shapes/shared/defaultStyleDefs.tsx | 12 +- .../tldraw/src/lib/tools/HandTool/HandTool.ts | 14 +- .../src/lib/tools/SelectTool/selectHelpers.ts | 4 +- .../tools/ZoomTool/childStates/Pointing.ts | 4 +- .../ZoomTool/childStates/ZoomBrushing.ts | 6 +- .../ui/components/Minimap/DefaultMinimap.tsx | 4 +- .../components/ZoomMenu/DefaultZoomMenu.tsx | 4 +- .../tldraw/src/lib/ui/context/actions.tsx | 16 +- .../src/test/commands/animationSpeed.test.ts | 6 +- .../src/test/commands/centerOnPoint.test.ts | 2 +- 23 files changed, 822 insertions(+), 665 deletions(-) diff --git a/apps/dotcom/src/components/PeopleMenu/PeopleMenuItem.tsx b/apps/dotcom/src/components/PeopleMenu/PeopleMenuItem.tsx index 202a78657..c2242eb75 100644 --- a/apps/dotcom/src/components/PeopleMenu/PeopleMenuItem.tsx +++ b/apps/dotcom/src/components/PeopleMenu/PeopleMenuItem.tsx @@ -38,7 +38,7 @@ export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId editor.animateToUser(userId)} + onClick={() => editor.zoomToUser(userId)} onDoubleClick={handleFollowClick} > diff --git a/apps/dotcom/src/hooks/useUrlState.ts b/apps/dotcom/src/hooks/useUrlState.ts index 20fb3353a..8a9289464 100644 --- a/apps/dotcom/src/hooks/useUrlState.ts +++ b/apps/dotcom/src/hooks/useUrlState.ts @@ -1,5 +1,5 @@ import { default as React, useEffect } from 'react' -import { Editor, TLPageId, debounce, react, useEditor } from 'tldraw' +import { Editor, TLPageId, clamp, debounce, react, useEditor } from 'tldraw' const PARAMS = { // deprecated @@ -68,19 +68,19 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) { const viewport = viewportFromString(newViewportRaw) const { x, y, w, h } = viewport const { w: sw, h: sh } = editor.getViewportScreenBounds() - const naturalZoom = editor.getCameraFitZoom() + const fitZoom = editor.getCameraFitZoom() const { zoomMin, zoomMax } = editor.getCameraOptions() - const zoom = Math.min( - Math.max(Math.min(sw / w, sh / h), zoomMin * naturalZoom), - zoomMax * naturalZoom - ) + const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * fitZoom, zoomMax * fitZoom) - editor.setCamera({ - x: -x + (sw - w * zoom) / 2 / zoom, - y: -y + (sh - h * zoom) / 2 / zoom, - z: zoom, - }) + editor.setCamera( + { + x: -x + (sw - w * zoom) / 2 / zoom, + y: -y + (sh - h * zoom) / 2 / zoom, + z: zoom, + }, + { immediate: true } + ) } catch (err) { console.error(err) } diff --git a/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx b/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx index f5414a755..17cf70693 100644 --- a/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx +++ b/apps/examples/src/examples/after-create-update-shape/AfterCreateUpdateShapeExample.tsx @@ -68,5 +68,5 @@ function createDemoShapes(editor: Editor) { }, })) ) - .zoomToContent({ duration: 0 }) + .zoomToContent({ animation: { duration: 0 } }) } diff --git a/apps/examples/src/examples/after-delete-shape/AfterDeleteShapeExample.tsx b/apps/examples/src/examples/after-delete-shape/AfterDeleteShapeExample.tsx index 267c7b116..3c59bb0f9 100644 --- a/apps/examples/src/examples/after-delete-shape/AfterDeleteShapeExample.tsx +++ b/apps/examples/src/examples/after-delete-shape/AfterDeleteShapeExample.tsx @@ -58,5 +58,5 @@ function createDemoShapes(editor: Editor) { })), ]) - editor.zoomToContent({ duration: 0 }) + editor.zoomToContent({ animation: { duration: 0 } }) } diff --git a/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx b/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx index a4ab4db37..4362e5a2f 100644 --- a/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx +++ b/apps/examples/src/examples/before-delete-shape/BeforeDeleteShapeExample.tsx @@ -44,5 +44,5 @@ function createDemoShapes(editor: Editor) { }, }, ]) - .zoomToContent({ duration: 0 }) + .zoomToContent({ animation: { duration: 0 } }) } diff --git a/apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx b/apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx index 8e1f85652..a0e13d7f1 100644 --- a/apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx +++ b/apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx @@ -90,7 +90,7 @@ export class EditableShapeUtil extends BaseBoxShapeUtil { override onEditEnd: TLOnEditEndHandler = (shape) => { this.editor.animateShape( { ...shape, rotation: shape.rotation + Math.PI * 2 }, - { duration: 250 } + { animation: { duration: 250 } } ) } } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 574cbf3fa..6171c45ce 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -588,13 +588,24 @@ export class Editor extends EventEmitter { constructor({ store, user, shapeUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: TLEditorOptions); addOpenMenu(id: string): this; alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this; - animateShape(partial: null | TLShapePartial | undefined, animationOptions?: TLAnimationOptions): this; - animateShapes(partials: (null | TLShapePartial | undefined)[], animationOptions?: Partial<{ - duration: number; - easing: (t: number) => number; + animateShape(partial: null | TLShapePartial | undefined, opts?: Partial<{ + animation: Partial<{ + duration: number; + easing: (t: number) => number; + }>; + force: boolean; + immediate: boolean; + initial: boolean; + }>): this; + animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: Partial<{ + animation: Partial<{ + duration: number; + easing: (t: number) => number; + }>; + force: boolean; + immediate: boolean; + initial: boolean; }>): this; - animateToShape(shapeId: TLShapeId, opts?: TLAnimationOptions): this; - animateToUser(userId: string): this; // @internal (undocumented) annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: { extras?: Record; @@ -611,7 +622,7 @@ export class Editor extends EventEmitter { cancelDoubleClick(): void; // @internal (undocumented) capturedPointerId: null | number; - centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this; + centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this; clearOpenMenus(): this; // @internal protected _clickManager: ClickManager; @@ -681,9 +692,7 @@ export class Editor extends EventEmitter { getAssetForExternalContent(info: TLExternalAssetContent): Promise; getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[]; getCamera(): TLCamera; - // (undocumented) getCameraFitZoom(): number; - // (undocumented) getCameraOptions(): TLCameraOptions; getCameraState(): "idle" | "moving"; getCanRedo(): boolean; @@ -853,18 +862,10 @@ export class Editor extends EventEmitter { moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this; nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this; packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this; - pageToScreen(point: VecLike): { - x: number; - y: number; - z: number; - }; - pageToViewport(point: VecLike): { - x: number; - y: number; - z: number; - }; - pan(offset: VecLike, animation?: TLAnimationOptions): this; - panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this; + pageToScreen(point: VecLike): Vec; + pageToViewport(point: VecLike): Vec; + pan(offset: VecLike, opts?: TLCameraMoveOptions): this; + panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this; popFocusedGroupId(): this; putContentOntoCurrentPage(content: TLContent, options?: { point?: VecLike; @@ -883,27 +884,18 @@ export class Editor extends EventEmitter { renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this; renderingBoundsMargin: number; reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this; - resetZoom(point?: Vec, animation?: TLAnimationOptions): this; + resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this; resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this; readonly root: RootState; rotateShapesBy(shapes: TLShape[] | TLShapeId[], delta: number): this; - screenToPage(point: VecLike): { - x: number; - y: number; - z: number; - }; + screenToPage(point: VecLike): Vec; readonly scribbles: ScribbleManager; select(...shapes: TLShape[] | TLShapeId[]): this; selectAll(): this; selectNone(): this; sendBackward(shapes: TLShape[] | TLShapeId[]): this; sendToBack(shapes: TLShape[] | TLShapeId[]): this; - setCamera(point: VecLike, opts?: TLAnimationOptions & { - force?: boolean; - immediate?: boolean; - initial?: boolean; - }): this; - // (undocumented) + setCamera(point: VecLike, opts?: TLCameraMoveOptions): this; setCameraOptions(options: TLCameraOptions, opts?: { force?: boolean; immediate?: boolean; @@ -962,15 +954,17 @@ export class Editor extends EventEmitter { updateViewportScreenBounds(screenBounds: Box, center?: boolean): this; readonly user: UserPreferencesManager; visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this; - zoomIn(point?: Vec, animation?: TLAnimationOptions): this; - zoomOut(point?: Vec, animation?: TLAnimationOptions): this; - zoomToBounds(bounds: Box, opts?: { + zoomIn(point?: Vec, opts?: TLCameraMoveOptions): this; + zoomOut(point?: Vec, opts?: TLCameraMoveOptions): this; + zoomToBounds(bounds: BoxLike, opts?: { inset?: number; targetZoom?: number; - } & TLAnimationOptions): this; - zoomToContent(opts?: TLAnimationOptions): this; - zoomToFit(animation?: TLAnimationOptions): this; - zoomToSelection(animation?: TLAnimationOptions): this; + } & TLCameraMoveOptions): this; + zoomToContent(opts?: TLCameraMoveOptions): this; + zoomToFit(opts?: TLCameraMoveOptions): this; + zoomToSelection(opts?: TLCameraMoveOptions): this; + zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this; + zoomToUser(userId: string, opts?: TLCameraMoveOptions): this; } // @internal (undocumented) @@ -1222,9 +1216,6 @@ export function hardReset({ shouldReload }?: { // @public (undocumented) export function hardResetEditor(): void; -// @internal (undocumented) -export const HASH_PATTERN_ZOOM_NAMES: Record; - // @public (undocumented) export const HIT_TEST_MARGIN = 8; @@ -1925,12 +1916,6 @@ export type TLAfterCreateHandler = (record: R, source: 'remo // @public (undocumented) export type TLAfterDeleteHandler = (record: R, source: 'remote' | 'user') => void; -// @public (undocumented) -export type TLAnimationOptions = Partial<{ - duration: number; - easing: (t: number) => number; -}>; - // @public (undocumented) export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor; @@ -2013,6 +1998,17 @@ export type TLBrushProps = { opacity?: number; }; +// @public (undocumented) +export type TLCameraMoveOptions = Partial<{ + animation: Partial<{ + duration: number; + easing: (t: number) => number; + }>; + force: boolean; + immediate: boolean; + initial: boolean; +}>; + // @public (undocumented) export type TLCameraOptions = { zoomMax: number; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index fc835d7b1..2c15f9c55 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -7581,7 +7581,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#animateShape:member(1)", - "docComment": "/**\n * Animate a shape.\n *\n * @param partial - The shape partial to update.\n *\n * @param options - The animation's options.\n *\n * @example\n * ```ts\n * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 })\n * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { duration: 100, ease: t => t*t })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Animate a shape.\n *\n * @param partial - The shape partial to update.\n *\n * @param options - The animation's options.\n *\n * @example\n * ```ts\n * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 })\n * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { animation: { duration: 100, ease: t => t*t } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -7602,12 +7602,25 @@ }, { "kind": "Content", - "text": ", animationOptions?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<{\n animation: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>" }, { "kind": "Content", @@ -7624,8 +7637,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 8 + "startIndex": 10, + "endIndex": 11 }, "releaseTag": "Public", "isProtected": false, @@ -7640,10 +7653,10 @@ "isOptional": false }, { - "parameterName": "animationOptions", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 5, - "endIndex": 6 + "endIndex": 9 }, "isOptional": true } @@ -7655,7 +7668,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#animateShapes:member(1)", - "docComment": "/**\n * Animate shapes.\n *\n * @param partials - The shape partials to update.\n *\n * @param options - The animation's options.\n *\n * @example\n * ```ts\n * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }])\n * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { duration: 100, ease: t => t*t })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Animate shapes.\n *\n * @param partials - The shape partials to update.\n *\n * @param options - The animation's options.\n *\n * @example\n * ```ts\n * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }])\n * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { animation: { duration: 100, ease: t => t*t } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -7676,7 +7689,7 @@ }, { "kind": "Content", - "text": ", animationOptions?: " + "text": ", opts?: " }, { "kind": "Reference", @@ -7685,7 +7698,16 @@ }, { "kind": "Content", - "text": "<{\n duration: number;\n easing: (t: number) => number;\n }>" + "text": "<{\n animation: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n }>" }, { "kind": "Content", @@ -7702,8 +7724,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 8, - "endIndex": 9 + "startIndex": 10, + "endIndex": 11 }, "releaseTag": "Public", "isProtected": false, @@ -7718,10 +7740,10 @@ "isOptional": false }, { - "parameterName": "animationOptions", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 5, - "endIndex": 7 + "endIndex": 9 }, "isOptional": true } @@ -7730,120 +7752,6 @@ "isAbstract": false, "name": "animateShapes" }, - { - "kind": "Method", - "canonicalReference": "@tldraw/editor!Editor#animateToShape:member(1)", - "docComment": "/**\n * Animate the camera to a shape.\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "animateToShape(shapeId: " - }, - { - "kind": "Reference", - "text": "TLShapeId", - "canonicalReference": "@tldraw/tlschema!TLShapeId:type" - }, - { - "kind": "Content", - "text": ", opts?: " - }, - { - "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "this" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "shapeId", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - }, - { - "parameterName": "opts", - "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "isOptional": true - } - ], - "isOptional": false, - "isAbstract": false, - "name": "animateToShape" - }, - { - "kind": "Method", - "canonicalReference": "@tldraw/editor!Editor#animateToUser:member(1)", - "docComment": "/**\n * Animate the camera to a user's cursor position. This also briefly show the user's cursor if it's not currently visible.\n *\n * @param userId - The id of the user to aniamte to.\n *\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "animateToUser(userId: " - }, - { - "kind": "Content", - "text": "string" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Content", - "text": "this" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isStatic": false, - "returnTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 - }, - "releaseTag": "Public", - "isProtected": false, - "overloadIndex": 1, - "parameters": [ - { - "parameterName": "userId", - "parameterTypeTokenRange": { - "startIndex": 1, - "endIndex": 2 - }, - "isOptional": false - } - ], - "isOptional": false, - "isAbstract": false, - "name": "animateToUser" - }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#bail:member(1)", @@ -8160,7 +8068,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#centerOnPoint:member(1)", - "docComment": "/**\n * Center the camera on a point (in the current page space).\n *\n * @param point - The point in the current page space to center on.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.centerOnPoint({ x: 100, y: 100 })\n * editor.centerOnPoint({ x: 100, y: 100 }, { duration: 200 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Center the camera on a point (in the current page space).\n *\n * @param point - The point in the current page space to center on.\n *\n * @param animation - The camera move options.\n *\n * @example\n * ```ts\n * editor.centerOnPoint({ x: 100, y: 100 })\n * editor.centerOnPoint({ x: 100, y: 100 }, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -8173,12 +8081,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -8211,7 +8119,7 @@ "isOptional": false }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -10035,7 +9943,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCameraFitZoom:member(1)", - "docComment": "", + "docComment": "/**\n * Get the zoom level that would fit the camera to the current constraints.\n *\n * @example\n * ```ts\n * editor.getCameraFitZoom()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10066,7 +9974,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCameraOptions:member(1)", - "docComment": "/**\n * @public\n */\n", + "docComment": "/**\n * Get the current camera options.\n *\n * @example\n * ```ts\n * editor.getCameraOptions()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10098,7 +10006,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCameraState:member(1)", - "docComment": "/**\n * Whether the camera is moving or idle.\n *\n * @public\n */\n", + "docComment": "/**\n * Whether the camera is moving or idle.\n *\n * @example\n * ```ts\n * editor.getCameraState()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10374,7 +10282,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCurrentPage:member(1)", - "docComment": "/**\n * The current page.\n *\n * @public\n */\n", + "docComment": "/**\n * The current page.\n *\n * @example\n * ```ts\n * editor.getCurrentPage()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10442,7 +10350,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCurrentPageId:member(1)", - "docComment": "/**\n * The current page id.\n *\n * @public\n */\n", + "docComment": "/**\n * The current page id.\n *\n * @example\n * ```ts\n * editor.getCurrentPageId()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -10510,7 +10418,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getCurrentPageShapeIds:member(1)", - "docComment": "/**\n * An array of all of the shapes on the current page.\n *\n * @public\n */\n", + "docComment": "/**\n * An array of all of the shapes on the current page.\n *\n * @example\n * ```ts\n * editor.getCurrentPageIds()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -11643,7 +11551,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getPages:member(1)", - "docComment": "/**\n * Info about the project's current pages.\n *\n * @public\n */\n", + "docComment": "/**\n * Info about the project's current pages.\n *\n * @example\n * ```ts\n * editor.getPages()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -11970,7 +11878,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getRenderingBounds:member(1)", - "docComment": "/**\n * The current rendering bounds in the current page space, used for checking which shapes are \"on screen\".\n *\n * @public\n */\n", + "docComment": "/**\n * The current rendering bounds in the current page space, used for checking which shapes are \"on screen\".\n *\n * @example\n * ```ts\n * editor.getRenderingBounds()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -12002,7 +11910,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#getRenderingShapes:member(1)", - "docComment": "/**\n * Get the shapes that should be displayed in the current viewport.\n *\n * @public\n */\n", + "docComment": "/**\n * Get the shapes that should be displayed in the current viewport.\n *\n * @example\n * ```ts\n * editor.getRenderingShapes()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -15774,8 +15682,9 @@ "text": "): " }, { - "kind": "Content", - "text": "{\n x: number;\n y: number;\n z: number;\n }" + "kind": "Reference", + "text": "Vec", + "canonicalReference": "@tldraw/editor!Vec:class" }, { "kind": "Content", @@ -15823,8 +15732,9 @@ "text": "): " }, { - "kind": "Content", - "text": "{\n x: number;\n y: number;\n z: number;\n }" + "kind": "Reference", + "text": "Vec", + "canonicalReference": "@tldraw/editor!Vec:class" }, { "kind": "Content", @@ -15856,7 +15766,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#pan:member(1)", - "docComment": "/**\n * Pan the camera.\n *\n * @param offset - The offset in the current page space.\n *\n * @param animation - The animation options.\n *\n * @example\n * ```ts\n * editor.pan({ x: 100, y: 100 })\n * editor.pan({ x: 100, y: 100 }, { duration: 1000 })\n * ```\n *\n */\n", + "docComment": "/**\n * Pan the camera.\n *\n * @param offset - The offset in the current page space.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.pan({ x: 100, y: 100 })\n * editor.pan({ x: 100, y: 100 }, { animation: { duration: 1000 } })\n * ```\n *\n */\n", "excerptTokens": [ { "kind": "Content", @@ -15869,12 +15779,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -15907,7 +15817,7 @@ "isOptional": false }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -15922,7 +15832,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#panZoomIntoView:member(1)", - "docComment": "/**\n * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.\n *\n * @param ids - The ids of the shapes to pan and zoom into view.\n *\n * @param animation - The options for an animation.\n *\n * @public\n */\n", + "docComment": "/**\n * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible.\n *\n * @param ids - The ids of the shapes to pan and zoom into view.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.panZoomIntoView([myShape.id])\n * editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -15939,12 +15849,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -15977,7 +15887,7 @@ "isOptional": false }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 4, "endIndex": 5 @@ -16633,7 +16543,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#resetZoom:member(1)", - "docComment": "/**\n * Set the zoom back to 100%.\n *\n * @param point - The screen point to zoom out on. Defaults to the viewport screen center.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.resetZoom()\n * editor.resetZoom(editor.getViewportScreenCenter(), { duration: 200 })\n * editor.resetZoom(editor.getViewportScreenCenter(), { duration: 200 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Set the zoom back to 100%.\n *\n * @param point - The screen point to zoom out on. Defaults to the viewport screen center.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.resetZoom()\n * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } })\n * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -16646,12 +16556,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -16684,7 +16594,7 @@ "isOptional": true }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -16916,8 +16826,9 @@ "text": "): " }, { - "kind": "Content", - "text": "{\n x: number;\n y: number;\n z: number;\n }" + "kind": "Reference", + "text": "Vec", + "canonicalReference": "@tldraw/editor!Vec:class" }, { "kind": "Content", @@ -17228,7 +17139,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#setCamera:member(1)", - "docComment": "/**\n * Set the current camera.\n *\n * @param point - The new camera position.\n *\n * @param animation - Options for an animation.\n *\n * @example\n * ```ts\n * editor.setCamera({ x: 0, y: 0})\n * editor.setCamera({ x: 0, y: 0, z: 1.5})\n * editor.setCamera({ x: 0, y: 0, z: 1.5}, { duration: 1000, easing: (t) => t * t })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Set the current camera.\n *\n * @param point - The new camera position.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.setCamera({ x: 0, y: 0})\n * editor.setCamera({ x: 0, y: 0, z: 1.5})\n * editor.setCamera({ x: 0, y: 0, z: 1.5}, { animation: { duration: 1000, easing: (t) => t * t } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -17245,12 +17156,8 @@ }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" - }, - { - "kind": "Content", - "text": " & {\n force?: boolean;\n immediate?: boolean;\n initial?: boolean;\n }" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -17267,8 +17174,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 6, - "endIndex": 7 + "startIndex": 5, + "endIndex": 6 }, "releaseTag": "Public", "isProtected": false, @@ -17286,7 +17193,7 @@ "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, - "endIndex": 5 + "endIndex": 4 }, "isOptional": true } @@ -17298,7 +17205,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#setCameraOptions:member(1)", - "docComment": "/**\n * @public\n */\n", + "docComment": "/**\n * Set the camera options.\n *\n * @param options - The camera options to set.\n *\n * @param opts - The options for the change.\n *\n * @example\n * ```ts\n * editor.setCameraOptions(myCameraOptions)\n * editor.setCameraOptions(myCameraOptions, { immediate: true, force: true, initial: false })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -18425,7 +18332,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#slideCamera:member(1)", - "docComment": "/**\n * Slide the camera in a certain direction.\n *\n * @param opts - Options for the slide\n *\n * @public\n */\n", + "docComment": "/**\n * Slide the camera in a certain direction.\n *\n * @param opts - Options for the slide\n *\n * @example\n * ```ts\n * editor.slideCamera({ speed: 1, direction: { x: 1, y: 0 }, friction: 0.1 })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -18607,7 +18514,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#startFollowingUser:member(1)", - "docComment": "/**\n * Start viewport-following a user.\n *\n * @param userId - The id of the user to follow.\n *\n * @public\n */\n", + "docComment": "/**\n * Start viewport-following a user.\n *\n * @param userId - The id of the user to follow.\n *\n * @example\n * ```ts\n * editor.startFollowingUser(myUserId)\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -18655,7 +18562,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#stopCameraAnimation:member(1)", - "docComment": "/**\n * Stop the current camera animation, if any.\n *\n * @public\n */\n", + "docComment": "/**\n * Stop the current camera animation, if any.\n *\n * @example\n * ```ts\n * editor.stopCameraAnimation()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -18686,7 +18593,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#stopFollowingUser:member(1)", - "docComment": "/**\n * Stop viewport-following a user.\n *\n * @public\n */\n", + "docComment": "/**\n * Stop viewport-following a user.\n *\n * @example\n * ```ts\n * editor.stopFollowingUser()\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -19854,7 +19761,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomIn:member(1)", - "docComment": "/**\n * Zoom the camera in.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomIn()\n * editor.zoomIn(editor.getViewportScreenCenter(), { duration: 120 })\n * editor.zoomIn(editor.inputs.currentScreenPoint, { duration: 120 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Zoom the camera in.\n *\n * @param point - The screen point to zoom in on. Defaults to the screen center\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomIn()\n * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } })\n * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -19867,12 +19774,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -19905,7 +19812,7 @@ "isOptional": true }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -19920,7 +19827,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomOut:member(1)", - "docComment": "/**\n * Zoom the camera out.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomOut()\n * editor.zoomOut(editor.getViewportScreenCenter(), { duration: 120 })\n * editor.zoomOut(editor.inputs.currentScreenPoint, { duration: 120 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Zoom the camera out.\n *\n * @param point - The point to zoom out on. Defaults to the viewport screen center.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomOut()\n * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } })\n * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -19933,12 +19840,12 @@ }, { "kind": "Content", - "text": ", animation?: " + "text": ", opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -19971,7 +19878,7 @@ "isOptional": true }, { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 3, "endIndex": 4 @@ -19986,7 +19893,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomToBounds:member(1)", - "docComment": "/**\n * Zoom the camera to fit a bounding box (in the current page space).\n *\n * @param bounds - The bounding box.\n *\n * @param options - The options for an animation, target zoom, or custom inset amount.\n *\n * @example\n * ```ts\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds, { duration: 100 })\n * editor.zoomToBounds(myBounds, { inset: 0, targetZoom: 1 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Zoom the camera to fit a bounding box (in the current page space).\n *\n * @param bounds - The bounding box.\n *\n * @param opts - The camera move options, target zoom, or custom inset amount.\n *\n * @example\n * ```ts\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds, { animation: { duration: 200 } })\n * editor.zoomToBounds(myBounds, { animation: { duration: 200 }, inset: 0, targetZoom: 1 })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -19994,8 +19901,8 @@ }, { "kind": "Reference", - "text": "Box", - "canonicalReference": "@tldraw/editor!Box:class" + "text": "BoxLike", + "canonicalReference": "@tldraw/editor!BoxLike:type" }, { "kind": "Content", @@ -20007,8 +19914,8 @@ }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -20056,7 +19963,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomToContent:member(1)", - "docComment": "/**\n * Move the camera to the nearest content.\n *\n * @param opts - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomToContent()\n * editor.zoomToContent({ duration: 200 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Move the camera to the nearest content.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToContent()\n * editor.zoomToContent({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -20064,8 +19971,8 @@ }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -20105,16 +20012,16 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomToFit:member(1)", - "docComment": "/**\n * Zoom the camera to fit the current page's content in the viewport.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomToFit()\n * editor.zoomToFit({ duration: 200 })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Zoom the camera to fit the current page's content in the viewport.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToFit()\n * editor.zoomToFit({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "zoomToFit(animation?: " + "text": "zoomToFit(opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -20139,7 +20046,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -20154,16 +20061,16 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#zoomToSelection:member(1)", - "docComment": "/**\n * Zoom the camera to fit the current selection in the viewport.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomToSelection()\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Zoom the camera to fit the current selection in the viewport.\n *\n * @param animation - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToSelection()\n * editor.zoomToSelection({ animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "zoomToSelection(animation?: " + "text": "zoomToSelection(opts?: " }, { "kind": "Reference", - "text": "TLAnimationOptions", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type" + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" }, { "kind": "Content", @@ -20188,7 +20095,7 @@ "overloadIndex": 1, "parameters": [ { - "parameterName": "animation", + "parameterName": "opts", "parameterTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -20199,6 +20106,137 @@ "isOptional": false, "isAbstract": false, "name": "zoomToSelection" + }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#zoomToShape:member(1)", + "docComment": "/**\n * Animate the camera to a shape.\n *\n * @param shapeId - The id of the shape to animate to.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToShape(myShape.id)\n * editor.zoomToShape(myShape.id, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "zoomToShape(shapeId: " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Reference", + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "this" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "shapeId", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "zoomToShape" + }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#zoomToUser:member(1)", + "docComment": "/**\n * Animate the camera to a user's cursor position. This also briefly show the user's cursor if it's not currently visible.\n *\n * @param userId - The id of the user to aniamte to.\n *\n * @param opts - The camera move options.\n *\n * @example\n * ```ts\n * editor.zoomToUser(myUserId)\n * editor.zoomToUser(myUserId, { animation: { duration: 200 } })\n * ```\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "zoomToUser(userId: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Reference", + "text": "TLCameraMoveOptions", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "this" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "userId", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": true + } + ], + "isOptional": false, + "isAbstract": false, + "name": "zoomToUser" } ], "extendsTokenRange": { @@ -36312,37 +36350,6 @@ "endIndex": 4 } }, - { - "kind": "TypeAlias", - "canonicalReference": "@tldraw/editor!TLAnimationOptions:type", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type TLAnimationOptions = " - }, - { - "kind": "Reference", - "text": "Partial", - "canonicalReference": "!Partial:type" - }, - { - "kind": "Content", - "text": "<{\n duration: number;\n easing: (t: number) => number;\n}>" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "packages/editor/src/lib/editor/Editor.ts", - "releaseTag": "Public", - "name": "TLAnimationOptions", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } - }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/editor!TLAnyShapeUtilConstructor:type", @@ -37069,6 +37076,46 @@ "endIndex": 4 } }, + { + "kind": "TypeAlias", + "canonicalReference": "@tldraw/editor!TLCameraMoveOptions:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type TLCameraMoveOptions = " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<{\n animation: " + }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<{\n duration: number;\n easing: (t: number) => number;\n }>;\n force: boolean;\n immediate: boolean;\n initial: boolean;\n}>" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/editor/src/lib/editor/Editor.ts", + "releaseTag": "Public", + "name": "TLCameraMoveOptions", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, { "kind": "TypeAlias", "canonicalReference": "@tldraw/editor!TLCameraOptions:type", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 14a87f266..fb001ee65 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -114,7 +114,6 @@ export { DOUBLE_CLICK_DURATION, DRAG_DISTANCE, GRID_STEPS, - HASH_PATTERN_ZOOM_NAMES, HIT_TEST_MARGIN, MAX_PAGES, MAX_SHAPES_PER_PAGE, @@ -125,7 +124,7 @@ export { } from './lib/constants' export { Editor, - type TLAnimationOptions, + type TLCameraMoveOptions, type TLEditorOptions, type TLResizeShapeOptions, } from './lib/editor/Editor' diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 62330b1fc..a49d0bfe9 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -53,16 +53,6 @@ export const DRAG_DISTANCE = 16 // 4 squared /** @internal */ export const SVG_PADDING = 32 -/** @internal */ -export const HASH_PATTERN_ZOOM_NAMES: Record = {} - -export const HASH_PATTERN_COUNT = 6 - -for (let zoom = 1; zoom <= HASH_PATTERN_COUNT; zoom++) { - HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark` - HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light` -} - /** @internal */ export const DEFAULT_ANIMATION_OPTIONS = { duration: 0, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 14a51e43b..cd1833dfe 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -58,7 +58,6 @@ import { sortByIndex, structuredClone, } from '@tldraw/utils' -import { Number } from 'core-js' import { EventEmitter } from 'eventemitter3' import { flushSync } from 'react-dom' import { createRoot } from 'react-dom/client' @@ -84,7 +83,7 @@ import { MAX_SHAPES_PER_PAGE, getDefaultCameraOptions, } from '../constants' -import { Box } from '../primitives/Box' +import { Box, BoxLike } from '../primitives/Box' import { Mat, MatLike, MatModel } from '../primitives/Mat' import { Vec, VecLike } from '../primitives/Vec' import { EASINGS } from '../primitives/easings' @@ -134,9 +133,14 @@ import { OptionalKeys, RequiredKeys, TLCameraOptions, TLSvgOptions } from './typ import { TLResizeHandle } from './types/selection-types' /** @public */ -export type TLAnimationOptions = Partial<{ - duration: number - easing: (t: number) => number +export type TLCameraMoveOptions = Partial<{ + animation: Partial<{ + duration: number + easing: (t: number) => number + }> + immediate: boolean + force: boolean + initial: boolean }> /** @public */ @@ -2036,13 +2040,39 @@ export class Editor extends EventEmitter { /* --------------------- Camera --------------------- */ - private _cameraOptions = atom('camera options', getDefaultCameraOptions({})) - - /** @public */ - getCameraOptions() { - return this._cameraOptions.get() + /** @internal */ + @computed + private getCameraId() { + return CameraRecordType.createId(this.getCurrentPageId()) } + /** + * The current camera. + * + * @public + */ + @computed getCamera() { + return this.store.get(this.getCameraId())! + } + + /** + * The current camera zoom level. + * + * @public + */ + @computed getZoomLevel() { + return this.getCamera().z + } + + /** + * Get the zoom level that would fit the camera to the current constraints. + * + * @example + * ```ts + * editor.getCameraFitZoom() + * ``` + * + * @public */ getCameraFitZoom() { const cameraOptions = this.getCameraOptions() if (!cameraOptions.constraints || cameraOptions.constraints.fit === 'none') { @@ -2075,7 +2105,34 @@ export class Editor extends EventEmitter { } } - /** @public */ + private _cameraOptions = atom('camera options', getDefaultCameraOptions({})) + + /** + * Get the current camera options. + * + * @example + * ```ts + * editor.getCameraOptions() + * ``` + * + * @public */ + getCameraOptions() { + return this._cameraOptions.get() + } + + /** + * Set the camera options. + * + * @example + * ```ts + * editor.setCameraOptions(myCameraOptions) + * editor.setCameraOptions(myCameraOptions, { immediate: true, force: true, initial: false }) + * ``` + * + * @param options - The camera options to set. + * @param opts - The options for the change. + * + * @public */ setCameraOptions( options: TLCameraOptions, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } @@ -2085,30 +2142,6 @@ export class Editor extends EventEmitter { return this } - /** @internal */ - @computed - private getCameraId() { - return CameraRecordType.createId(this.getCurrentPageId()) - } - - /** - * The current camera. - * - * @public - */ - @computed getCamera() { - return this.store.get(this.getCameraId())! - } - - /** - * The current camera zoom level. - * - * @public - */ - @computed getZoomLevel() { - return this.getCamera().z - } - /** @internal */ private _setCamera( point: Vec, @@ -2191,60 +2224,72 @@ export class Editor extends EventEmitter { y = cy + cyB - cyA } + // x axis + const minX = px / z - bounds.x const freeW = (vsb.w - px * 2) / z - bounds.w + const originX = minX + freeW * constraints.origin.x if (opts?.initial) { - x = minX + freeW * constraints.origin.x + // Center according to the origin + x = originX } else { switch (constraints.fitX) { + case 'lock': { + // Center according to the origin + x = originX + break + } case 'contain': { - x = clamp(x, minX + freeW * (z < zx ? constraints.origin.x : 1), minX) + // When below fit zoom, center the camera + if (z < zx) x = originX + // When above fit zoom, keep the bounds within padding distance of the viewport edge + else x = clamp(x, minX + freeW, minX) break } case 'inside': { - x = - z < zx - ? clamp(x, px / z - bounds.x, (vsb.w - px) / z - bounds.w) - : clamp(x, minX + freeW, minX) + // When below fit zoom, constrain the camera so that the bounds stay completely within the viewport + if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w) + // When above fit zoom, keep the bounds within padding distance of the viewport edge + else x = clamp(x, minX + freeW, minX) break } case 'outside': { + // Constrain the camera so that the bounds never leaves the viewport x = clamp(x, px / z - bounds.w, (vsb.w - px) / z) break } - case 'lock': { - x = minX + freeW * constraints.origin.x - break - } } } + // y axis + const minY = py / z - bounds.y const freeH = (vsb.h - py * 2) / z - bounds.h + const originY = minY + freeH * constraints.origin.y + if (opts?.initial) { - y = minY + freeH * constraints.origin.y + y = originY } else { switch (constraints.fitY) { + case 'lock': { + y = originY + break + } case 'contain': { - y = clamp(y, minY + freeH * (z < zy ? constraints.origin.y : 1), minY) + if (z < zy) y = originY + else y = clamp(y, minY + freeH, minY) break } case 'inside': { - y = - z < zy - ? clamp(y, py / z - bounds.y, (vsb.h - py) / z - bounds.h) - : clamp(y, minY + freeH, minY) + if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h) + else y = clamp(y, minY + freeH, minY) break } case 'outside': { y = clamp(y, py / z - bounds.h, (vsb.h - py) / z) break } - case 'lock': { - y = minY + freeH * constraints.origin.y - break - } } } } else { @@ -2317,18 +2362,15 @@ export class Editor extends EventEmitter { * ```ts * editor.setCamera({ x: 0, y: 0}) * editor.setCamera({ x: 0, y: 0, z: 1.5}) - * editor.setCamera({ x: 0, y: 0, z: 1.5}, { duration: 1000, easing: (t) => t * t }) + * editor.setCamera({ x: 0, y: 0, z: 1.5}, { animation: { duration: 1000, easing: (t) => t * t } }) * ``` * * @param point - The new camera position. - * @param animation - Options for an animation. + * @param opts - The camera move options. * * @public */ - setCamera( - point: VecLike, - opts?: TLAnimationOptions & { immediate?: boolean; force?: boolean; initial?: boolean } - ): this { + setCamera(point: VecLike, opts?: TLCameraMoveOptions): this { // Stop any camera animations this.stopCameraAnimation() @@ -2343,10 +2385,9 @@ export class Editor extends EventEmitter { if (!Number.isFinite(_point.y)) _point.y = 0 if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel() - if (opts && (opts.duration || opts.easing)) { + if (opts?.animation) { const { width, height } = this.getViewportScreenBounds() - // todo: animate this - return this._animateToViewport( + this._animateToViewport( new Box(-point.x, -point.y, width / _point.z, height / _point.z), opts ) @@ -2363,23 +2404,18 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.centerOnPoint({ x: 100, y: 100 }) - * editor.centerOnPoint({ x: 100, y: 100 }, { duration: 200 }) + * editor.centerOnPoint({ x: 100, y: 100 }, { animation: { duration: 200 } }) * ``` * * @param point - The point in the current page space to center on. - * @param animation - The options for an animation. + * @param animation - The camera move options. * * @public */ - centerOnPoint(point: VecLike, animation?: TLAnimationOptions): this { + centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this - const { width: pw, height: ph } = this.getViewportPageBounds() - - this.setCamera( - { x: -(point.x - pw / 2), y: -(point.y - ph / 2), z: this.getCamera().z }, - animation - ) + this.setCamera(new Vec(-(point.x - pw / 2), -(point.y - ph / 2), this.getCamera().z), opts) return this } @@ -2389,20 +2425,17 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomToContent() - * editor.zoomToContent({ duration: 200 }) + * editor.zoomToContent({ animation: { duration: 200 } }) * ``` * - * @param opts - The options for an animation. + * @param opts - The camera move options. * * @public */ - zoomToContent(opts: TLAnimationOptions = { duration: 220 }): this { + zoomToContent(opts: TLCameraMoveOptions = { animation: { duration: 220 } }): this { const bounds = this.getSelectionPageBounds() ?? this.getCurrentPageBounds() - - if (bounds) { - this.zoomToBounds(bounds, { targetZoom: Math.min(1, this.getZoomLevel()), ...opts }) - } - + if (!bounds) return this + this.zoomToBounds(bounds, { targetZoom: Math.min(1, this.getZoomLevel()), ...opts }) return this } @@ -2412,19 +2445,18 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomToFit() - * editor.zoomToFit({ duration: 200 }) + * editor.zoomToFit({ animation: { duration: 200 } }) * ``` * - * @param animation - The options for an animation. + * @param opts - The camera move options. * * @public */ - zoomToFit(animation?: TLAnimationOptions): this { + zoomToFit(opts?: TLCameraMoveOptions): this { const ids = [...this.getCurrentPageShapeIds()] if (ids.length <= 0) return this - const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id)))) - this.zoomToBounds(pageBounds, animation) + this.zoomToBounds(pageBounds, opts) return this } @@ -2434,16 +2466,16 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.resetZoom() - * editor.resetZoom(editor.getViewportScreenCenter(), { duration: 200 }) - * editor.resetZoom(editor.getViewportScreenCenter(), { duration: 200 }) + * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) + * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) * ``` * * @param point - The screen point to zoom out on. Defaults to the viewport screen center. - * @param animation - The options for an animation. + * @param opts - The camera move options. * * @public */ - resetZoom(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { + resetZoom(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { const { isLocked, constraints: constraints } = this.getCameraOptions() if (isLocked) return this @@ -2463,8 +2495,8 @@ export class Editor extends EventEmitter { } this.setCamera( - { x: cx + (x / z - x) - (x / cz - x), y: cy + (y / z - y) - (y / cz - y), z }, - animation + new Vec(cx + (x / z - x) - (x / cz - x), cy + (y / z - y) - (y / cz - y), z), + opts ) return this } @@ -2475,40 +2507,41 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomIn() - * editor.zoomIn(editor.getViewportScreenCenter(), { duration: 120 }) - * editor.zoomIn(editor.inputs.currentScreenPoint, { duration: 120 }) + * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) + * editor.zoomIn(editor.inputs.currentScreenPoint, { animation: { duration: 200 } }) * ``` * - * @param animation - The options for an animation. + * @param point - The screen point to zoom in on. Defaults to the screen center + * @param opts - The camera move options. * * @public */ - zoomIn(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { + zoomIn(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() const { zoomMax, zoomSteps } = this.getCameraOptions() - if (zoomSteps === null || zoomSteps.length <= 1) return this - - const fitZoom = this.getCameraFitZoom() - - let zoom = zoomMax * fitZoom - - for (let i = 1; i < zoomSteps.length; i++) { - const z1 = zoomSteps[i - 1] * fitZoom - const z2 = zoomSteps[i] * fitZoom - if (z2 - cz <= (z2 - z1) / 2) continue - zoom = z2 - break + if (zoomSteps !== null && zoomSteps.length > 1) { + const fitZoom = this.getCameraFitZoom() + let zoom = zoomMax * fitZoom + for (let i = 1; i < zoomSteps.length; i++) { + const z1 = zoomSteps[i - 1] * fitZoom + const z2 = zoomSteps[i] * fitZoom + if (z2 - cz <= (z2 - z1) / 2) continue + zoom = z2 + break + } + this.setCamera( + new Vec( + cx + (point.x / zoom - point.x) - (point.x / cz - point.x), + cy + (point.y / zoom - point.y) - (point.y / cz - point.y), + zoom + ), + opts + ) } - const { x, y } = point - this.setCamera( - { x: cx + (x / zoom - x) - (x / cz - x), y: cy + (y / zoom - y) - (y / cz - y), z: zoom }, - animation - ) - return this } @@ -2518,45 +2551,40 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomOut() - * editor.zoomOut(editor.getViewportScreenCenter(), { duration: 120 }) - * editor.zoomOut(editor.inputs.currentScreenPoint, { duration: 120 }) + * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } }) + * editor.zoomOut(editor.inputs.currentScreenPoint, { animation: { duration: 120 } }) * ``` * - * @param animation - The options for an animation. + * @param point - The point to zoom out on. Defaults to the viewport screen center. + * @param opts - The camera move options. * * @public */ - zoomOut(point = this.getViewportScreenCenter(), animation?: TLAnimationOptions): this { + zoomOut(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this const { zoomMin, zoomSteps } = this.getCameraOptions() - if (zoomSteps === null || zoomSteps.length <= 1) return this - - const fitZoom = this.getCameraFitZoom() - - const { x: cx, y: cy, z: cz } = this.getCamera() - - let zoom = zoomMin * fitZoom - - for (let i = zoomSteps.length - 1; i > 0; i--) { - const z1 = zoomSteps[i - 1] * fitZoom - const z2 = zoomSteps[i] * fitZoom - if (z2 - cz >= (z2 - z1) / 2) continue - zoom = z1 - break + if (zoomSteps !== null && zoomSteps.length > 1) { + const fitZoom = this.getCameraFitZoom() + const { x: cx, y: cy, z: cz } = this.getCamera() + let zoom = zoomMin * fitZoom + for (let i = zoomSteps.length - 1; i > 0; i--) { + const z1 = zoomSteps[i - 1] * fitZoom + const z2 = zoomSteps[i] * fitZoom + if (z2 - cz >= (z2 - z1) / 2) continue + zoom = z1 + break + } + this.setCamera( + { + x: cx + (point.x / zoom - point.x) - (point.x / cz - point.x), + y: cy + (point.y / zoom - point.y) - (point.y / cz - point.y), + z: zoom, + }, + opts + ) } - const { x, y } = point - - this.setCamera( - { - x: cx + (x / zoom - x) - (x / cz - x), - y: cy + (y / zoom - y) - (y / cz - y), - z: zoom, - }, - animation - ) - return this } @@ -2566,35 +2594,40 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomToSelection() + * editor.zoomToSelection({ animation: { duration: 200 } }) * ``` * - * @param animation - The options for an animation. + * @param animation - The camera move options. * * @public */ - zoomToSelection(animation?: TLAnimationOptions): this { + zoomToSelection(opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this - const selectionPageBounds = this.getSelectionPageBounds() - if (!selectionPageBounds) return this - - this.zoomToBounds(selectionPageBounds, { - targetZoom: Math.max(1, this.getZoomLevel()), - ...animation, - }) - + if (selectionPageBounds) { + this.zoomToBounds(selectionPageBounds, { + targetZoom: Math.max(1, this.getZoomLevel()), + ...opts, + }) + } return this } /** * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if possible. * + * @example + * ```ts + * editor.panZoomIntoView([myShape.id]) + * editor.panZoomIntoView([myShape.id], { animation: { duration: 200 } }) + * ``` + * * @param ids - The ids of the shapes to pan and zoom into view. - * @param animation - The options for an animation. + * @param opts - The camera move options. * * @public */ - panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this { + panZoomIntoView(ids: TLShapeId[], opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this if (ids.length <= 0) return this @@ -2603,7 +2636,7 @@ export class Editor extends EventEmitter { const viewportPageBounds = this.getViewportPageBounds() if (viewportPageBounds.h < selectionBounds.h || viewportPageBounds.w < selectionBounds.w) { - this.zoomToBounds(selectionBounds, { targetZoom: this.getCamera().z, ...animation }) + this.zoomToBounds(selectionBounds, { targetZoom: this.getCamera().z, ...opts }) return this } else { @@ -2633,8 +2666,8 @@ export class Editor extends EventEmitter { // inside x-bounds } - const camera = this.getCamera() - this.setCamera({ x: camera.x + offsetX, y: camera.y + offsetY, z: camera.z }, animation) + const { x: cx, y: cy, z: cz } = this.getCamera() + this.setCamera(new Vec(cx + offsetX, cy + offsetY, cz), opts) } return this @@ -2646,19 +2679,18 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.zoomToBounds(myBounds) - * editor.zoomToBounds(myBounds) - * editor.zoomToBounds(myBounds, { duration: 100 }) - * editor.zoomToBounds(myBounds, { inset: 0, targetZoom: 1 }) + * editor.zoomToBounds(myBounds, { animation: { duration: 200 } }) + * editor.zoomToBounds(myBounds, { animation: { duration: 200 }, inset: 0, targetZoom: 1 }) * ``` * * @param bounds - The bounding box. - * @param options - The options for an animation, target zoom, or custom inset amount. + * @param opts - The camera move options, target zoom, or custom inset amount. * * @public */ zoomToBounds( - bounds: Box, - opts?: { targetZoom?: number; inset?: number } & TLAnimationOptions + bounds: BoxLike, + opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions ): this { if (this.getCameraOptions().isLocked) return this @@ -2671,8 +2703,8 @@ export class Editor extends EventEmitter { let zoom = clamp( Math.min( - (viewportScreenBounds.width - inset) / bounds.width, - (viewportScreenBounds.height - inset) / bounds.height + (viewportScreenBounds.width - inset) / bounds.w, + (viewportScreenBounds.height - inset) / bounds.h ), zoomMin * fitZoom, zoomMax * fitZoom @@ -2683,11 +2715,11 @@ export class Editor extends EventEmitter { } this.setCamera( - { - x: -bounds.minX + (viewportScreenBounds.width - bounds.width * zoom) / 2 / zoom, - y: -bounds.minY + (viewportScreenBounds.height - bounds.height * zoom) / 2 / zoom, - z: zoom, - }, + new Vec( + -bounds.x + (viewportScreenBounds.width - bounds.w * zoom) / 2 / zoom, + -bounds.y + (viewportScreenBounds.height - bounds.h * zoom) / 2 / zoom, + zoom + ), opts ) @@ -2700,19 +2732,19 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.pan({ x: 100, y: 100 }) - * editor.pan({ x: 100, y: 100 }, { duration: 1000 }) + * editor.pan({ x: 100, y: 100 }, { animation: { duration: 1000 } }) * ``` * * @param offset - The offset in the current page space. - * @param animation - The animation options. + * @param opts - The camera move options. */ - pan(offset: VecLike, animation?: TLAnimationOptions): this { + pan(offset: VecLike, opts?: TLCameraMoveOptions): this { const { isLocked, panSpeed } = this.getCameraOptions() if (isLocked) return this const { x: cx, y: cy, z: cz } = this.getCamera() this.setCamera( - { x: cx + (offset.x * panSpeed) / cz, y: cy + (offset.y * panSpeed) / cz, z: cz }, - animation + new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz), + opts ) this._flushEventsForTick(0) return this @@ -2721,6 +2753,11 @@ export class Editor extends EventEmitter { /** * Stop the current camera animation, if any. * + * @example + * ```ts + * editor.stopCameraAnimation() + * ``` + * * @public */ stopCameraAnimation(): this { @@ -2738,16 +2775,16 @@ export class Editor extends EventEmitter { } /** @internal */ - private _animateViewport(ms: number) { + private _animateViewport(ms: number): void { if (!this._viewportAnimation) return - const cancel = () => { - this.removeListener('tick', this._animateViewport) - this.removeListener('stop-camera-animation', cancel) + const cancelAnimation = () => { + this.off('tick', this._animateViewport) + this.off('stop-camera-animation', cancelAnimation) this._viewportAnimation = null } - this.once('stop-camera-animation', cancel) + this.once('stop-camera-animation', cancelAnimation) this._viewportAnimation.elapsed += ms @@ -2755,7 +2792,6 @@ export class Editor extends EventEmitter { if (elapsed > duration) { this._setCamera(new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width)) - cancel() return } @@ -2770,8 +2806,12 @@ export class Editor extends EventEmitter { } /** @internal */ - private _animateToViewport(targetViewportPage: Box, opts = {} as TLAnimationOptions) { - const { duration = 0, easing = EASINGS.easeInOutCubic } = opts + private _animateToViewport( + targetViewportPage: Box, + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions + ) { + if (!opts.animation) return + const { duration = 0, easing = EASINGS.easeInOutCubic } = opts.animation const animationSpeed = this.user.getAnimationSpeed() const viewportPageBounds = this.getViewportPageBounds() @@ -2804,7 +2844,7 @@ export class Editor extends EventEmitter { } // On each tick, animate the viewport - this.addListener('tick', this._animateViewport) + this.on('tick', this._animateViewport) return this } @@ -2812,6 +2852,11 @@ export class Editor extends EventEmitter { /** * Slide the camera in a certain direction. * + * @example + * ```ts + * editor.slideCamera({ speed: 1, direction: { x: 1, y: 0 }, friction: 0.1 }) + * ``` + * * @param opts - Options for the slide * @public */ @@ -2825,18 +2870,17 @@ export class Editor extends EventEmitter { ): this { if (this.getCameraOptions().isLocked) return this - this.stopCameraAnimation() - const animationSpeed = this.user.getAnimationSpeed() - if (animationSpeed === 0) return this + this.stopCameraAnimation() + const { speed, friction, direction, speedThreshold = 0.01 } = opts let currentSpeed = Math.min(speed, 1) const cancel = () => { - this.removeListener('tick', moveCamera) - this.removeListener('stop-camera-animation', cancel) + this.off('tick', moveCamera) + this.off('stop-camera-animation', cancel) } this.once('stop-camera-animation', cancel) @@ -2854,19 +2898,25 @@ export class Editor extends EventEmitter { } } - this.addListener('tick', moveCamera) + this.on('tick', moveCamera) return this } /** - * Animate the camera to a user's cursor position. - * This also briefly show the user's cursor if it's not currently visible. + * Animate the camera to a user's cursor position. This also briefly show the user's cursor if it's not currently visible. + * + * @example + * ```ts + * editor.zoomToUser(myUserId) + * editor.zoomToUser(myUserId, { animation: { duration: 200 } }) + * ``` * * @param userId - The id of the user to aniamte to. + * @param opts - The camera move options. * @public */ - animateToUser(userId: string): this { + zoomToUser(userId: string, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this { const presences = this.store.query.records('instance_presence', () => ({ userId: { eq: userId }, })) @@ -2892,9 +2942,11 @@ export class Editor extends EventEmitter { } // Only animate the camera if the user is on the same page as us - const options = isOnSamePage ? { duration: 500 } : undefined + if (opts && opts.animation && !isOnSamePage) { + opts.animation = undefined + } - this.centerOnPoint(presence.cursor, options) + this.centerOnPoint(presence.cursor, opts) // Highlight the user's cursor const { highlightedUserIds } = this.getInstanceState() @@ -2916,9 +2968,18 @@ export class Editor extends EventEmitter { /** * Animate the camera to a shape. * + * @example + * ```ts + * editor.zoomToShape(myShape.id) + * editor.zoomToShape(myShape.id, { animation: { duration: 200 } }) + * ``` + * + * @param shapeId - The id of the shape to animate to. + * @param opts - The camera move options. + * * @public */ - animateToShape(shapeId: TLShapeId, opts: TLAnimationOptions = DEFAULT_ANIMATION_OPTIONS): this { + zoomToShape(shapeId: TLShapeId, opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this const activeArea = this.getViewportScreenBounds().clone().expandBy(-32) @@ -2946,14 +3007,14 @@ export class Editor extends EventEmitter { targetViewportPage.x -= (targetViewportPage.width - shapePageBounds.width) / 2 } - return this._animateToViewport(targetViewportPage, opts) + this._animateToViewport(targetViewportPage, opts) + return this } // Viewport /** @internal */ private _willSetInitialBounds = true - private _wasInset = false /** * Update the viewport. The viewport will measure the size and screen position of its container @@ -3082,11 +3143,11 @@ export class Editor extends EventEmitter { screenToPage(point: VecLike) { const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! const { x: cx, y: cy, z: cz = 1 } = this.getCamera() - return { - x: (point.x - screenBounds.x) / cz - cx, - y: (point.y - screenBounds.y) / cz - cy, - z: point.z ?? 0.5, - } + return new Vec( + (point.x - screenBounds.x) / cz - cx, + (point.y - screenBounds.y) / cz - cy, + point.z ?? 0.5 + ) } /** @@ -3104,12 +3165,11 @@ export class Editor extends EventEmitter { pageToScreen(point: VecLike) { const screenBounds = this.getViewportScreenBounds() const { x: cx, y: cy, z: cz = 1 } = this.getCamera() - - return { - x: (point.x + cx) * cz + screenBounds.x, - y: (point.y + cy) * cz + screenBounds.y, - z: point.z ?? 0.5, - } + return new Vec( + (point.x + cx) * cz + screenBounds.x, + (point.y + cy) * cz + screenBounds.y, + point.z ?? 0.5 + ) } /** @@ -3126,12 +3186,7 @@ export class Editor extends EventEmitter { */ pageToViewport(point: VecLike) { const { x: cx, y: cy, z: cz = 1 } = this.getCamera() - - return { - x: (point.x + cx) * cz, - y: (point.y + cy) * cz, - z: point.z ?? 0.5, - } + return new Vec((point.x + cx) * cz, (point.y + cy) * cz, point.z ?? 0.5) } // Following @@ -3139,6 +3194,11 @@ export class Editor extends EventEmitter { /** * Start viewport-following a user. * + * @example + * ```ts + * editor.startFollowingUser(myUserId) + * ``` + * * @param userId - The id of the user to follow. * * @public @@ -3161,106 +3221,106 @@ export class Editor extends EventEmitter { transact(() => { this.stopFollowingUser() - this.updateInstanceState({ followingUserId: userId }, { ephemeral: true }) }) const cancel = () => { - this.removeListener('frame', moveTowardsUser) - this.removeListener('stop-following', cancel) + this.off('frame', moveTowardsUser) + this.off('stop-following', cancel) } let isCaughtUp = false - const moveTowardsUser = () => { - // Stop following if we can't find the user - const leaderPresence = [...leaderPresences.get()] - .sort((a, b) => { - return a.lastActivityTimestamp - b.lastActivityTimestamp - }) - .pop() - if (!leaderPresence) { - this.stopFollowingUser() - return - } + const moveTowardsUser = () => + transact(() => { + // Stop following if we can't find the user + const leaderPresence = [...leaderPresences.get()] + .sort((a, b) => { + return a.lastActivityTimestamp - b.lastActivityTimestamp + }) + .pop() + if (!leaderPresence) { + this.stopFollowingUser() + return + } - // Change page if leader is on a different page - const isOnSamePage = leaderPresence.currentPageId === this.getCurrentPageId() - const chaseProportion = isOnSamePage ? FOLLOW_CHASE_PROPORTION : 1 - if (!isOnSamePage) { - this.stopFollowingUser() - this.setCurrentPage(leaderPresence.currentPageId) - this.startFollowingUser(userId) - return - } + // Change page if leader is on a different page + const isOnSamePage = leaderPresence.currentPageId === this.getCurrentPageId() + const chaseProportion = isOnSamePage ? FOLLOW_CHASE_PROPORTION : 1 + if (!isOnSamePage) { + this.stopFollowingUser() + this.setCurrentPage(leaderPresence.currentPageId) + this.startFollowingUser(userId) + return + } - // Get the bounds of the follower (me) and the leader (them) - const { center, width, height } = this.getViewportPageBounds() - const leaderScreen = Box.From(leaderPresence.screenBounds) - const leaderWidth = leaderScreen.width / leaderPresence.camera.z - const leaderHeight = leaderScreen.height / leaderPresence.camera.z - const leaderCenter = new Vec( - leaderWidth / 2 - leaderPresence.camera.x, - leaderHeight / 2 - leaderPresence.camera.y - ) - - // At this point, let's check if we're following someone who's following us. - // If so, we can't try to contain their entire viewport - // because that would become a feedback loop where we zoom, they zoom, etc. - const isFollowingFollower = leaderPresence.followingUserId === thisUserId - - // Figure out how much to zoom - const desiredWidth = width + (leaderWidth - width) * chaseProportion - const desiredHeight = height + (leaderHeight - height) * chaseProportion - const ratio = !isFollowingFollower - ? Math.min(width / desiredWidth, height / desiredHeight) - : height / desiredHeight - - const fitZoom = this.getCameraFitZoom() - const { zoomMin, zoomMax } = this.getCameraOptions() - const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * fitZoom, zoomMax * fitZoom) - const targetWidth = this.getViewportScreenBounds().w / targetZoom - const targetHeight = this.getViewportScreenBounds().h / targetZoom - - // Figure out where to move the camera - const displacement = leaderCenter.sub(center) - const targetCenter = Vec.Add(center, Vec.Mul(displacement, chaseProportion)) - - // Now let's assess whether we've caught up to the leader or not - const distance = Vec.Sub(targetCenter, center).len() - const zoomChange = Math.abs(targetZoom - this.getCamera().z) - - // If we're chasing the leader... - // Stop chasing if we're close enough - if (distance < FOLLOW_CHASE_PAN_SNAP && zoomChange < FOLLOW_CHASE_ZOOM_SNAP) { - isCaughtUp = true - return - } - - // If we're already caught up with the leader... - // Only start moving again if we're far enough away - if ( - isCaughtUp && - distance < FOLLOW_CHASE_PAN_UNSNAP && - zoomChange < FOLLOW_CHASE_ZOOM_UNSNAP - ) { - return - } - - // Update the camera! - isCaughtUp = false - this.stopCameraAnimation() - this._setCamera( - new Vec( - -(targetCenter.x - targetWidth / 2), - -(targetCenter.y - targetHeight / 2), - targetZoom + // Get the bounds of the follower (me) and the leader (them) + const { center, width, height } = this.getViewportPageBounds() + const leaderScreen = Box.From(leaderPresence.screenBounds) + const leaderWidth = leaderScreen.width / leaderPresence.camera.z + const leaderHeight = leaderScreen.height / leaderPresence.camera.z + const leaderCenter = new Vec( + leaderWidth / 2 - leaderPresence.camera.x, + leaderHeight / 2 - leaderPresence.camera.y ) - ) - } + + // At this point, let's check if we're following someone who's following us. + // If so, we can't try to contain their entire viewport + // because that would become a feedback loop where we zoom, they zoom, etc. + const isFollowingFollower = leaderPresence.followingUserId === thisUserId + + // Figure out how much to zoom + const desiredWidth = width + (leaderWidth - width) * chaseProportion + const desiredHeight = height + (leaderHeight - height) * chaseProportion + const ratio = !isFollowingFollower + ? Math.min(width / desiredWidth, height / desiredHeight) + : height / desiredHeight + + const fitZoom = this.getCameraFitZoom() + const { zoomMin, zoomMax } = this.getCameraOptions() + const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * fitZoom, zoomMax * fitZoom) + const targetWidth = this.getViewportScreenBounds().w / targetZoom + const targetHeight = this.getViewportScreenBounds().h / targetZoom + + // Figure out where to move the camera + const displacement = leaderCenter.sub(center) + const targetCenter = Vec.Add(center, Vec.Mul(displacement, chaseProportion)) + + // Now let's assess whether we've caught up to the leader or not + const distance = Vec.Sub(targetCenter, center).len() + const zoomChange = Math.abs(targetZoom - this.getCamera().z) + + // If we're chasing the leader... + // Stop chasing if we're close enough + if (distance < FOLLOW_CHASE_PAN_SNAP && zoomChange < FOLLOW_CHASE_ZOOM_SNAP) { + isCaughtUp = true + return + } + + // If we're already caught up with the leader... + // Only start moving again if we're far enough away + if ( + isCaughtUp && + distance < FOLLOW_CHASE_PAN_UNSNAP && + zoomChange < FOLLOW_CHASE_ZOOM_UNSNAP + ) { + return + } + + // Update the camera! + isCaughtUp = false + this.stopCameraAnimation() + this._setCamera( + new Vec( + -(targetCenter.x - targetWidth / 2), + -(targetCenter.y - targetHeight / 2), + targetZoom + ) + ) + }) this.once('stop-following', cancel) - this.addListener('frame', moveTowardsUser) + this.on('frame', moveTowardsUser) return this } @@ -3268,11 +3328,18 @@ export class Editor extends EventEmitter { /** * Stop viewport-following a user. * + * @example + * ```ts + * editor.stopFollowingUser() + * ``` + * * @public */ stopFollowingUser(): this { - this.updateInstanceState({ followingUserId: null }, { ephemeral: true }) - this.emit('stop-following') + transact(() => { + this.updateInstanceState({ followingUserId: null }, { ephemeral: true }) + this.emit('stop-following') + }) return this } @@ -3283,6 +3350,11 @@ export class Editor extends EventEmitter { /** * Whether the camera is moving or idle. * + * @example + * ```ts + * editor.getCameraState() + * ``` + * * @public */ getCameraState() { @@ -3415,6 +3487,11 @@ export class Editor extends EventEmitter { /** * Get the shapes that should be displayed in the current viewport. * + * @example + * ```ts + * editor.getRenderingShapes() + * ``` + * * @public */ @computed getRenderingShapes() { @@ -3436,6 +3513,11 @@ export class Editor extends EventEmitter { /** * The current rendering bounds in the current page space, used for checking which shapes are "on screen". * + * @example + * ```ts + * editor.getRenderingBounds() + * ``` + * * @public */ getRenderingBounds() { @@ -3454,7 +3536,6 @@ export class Editor extends EventEmitter { * editor.updateRenderingBounds() * ``` * - * * @internal */ updateRenderingBounds(): this { @@ -3482,6 +3563,11 @@ export class Editor extends EventEmitter { /** * Info about the project's current pages. * + * @example + * ```ts + * editor.getPages() + * ``` + * * @public */ @computed getPages(): TLPage[] { @@ -3491,6 +3577,11 @@ export class Editor extends EventEmitter { /** * The current page. * + * @example + * ```ts + * editor.getCurrentPage() + * ``` + * * @public */ getCurrentPage(): TLPage { @@ -3500,6 +3591,11 @@ export class Editor extends EventEmitter { /** * The current page id. * + * @example + * ```ts + * editor.getCurrentPageId() + * ``` + * * @public */ @computed getCurrentPageId(): TLPageId { @@ -3529,6 +3625,11 @@ export class Editor extends EventEmitter { /** * An array of all of the shapes on the current page. * + * @example + * ```ts + * editor.getCurrentPageIds() + * ``` + * * @public */ getCurrentPageShapeIds() { @@ -5619,7 +5720,7 @@ export class Editor extends EventEmitter { const viewportPageBounds = this.getViewportPageBounds() if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) { this.centerOnPoint(selectionPageBounds.center, { - duration: ANIMATION_MEDIUM_MS, + animation: { duration: ANIMATION_MEDIUM_MS }, }) } } @@ -6961,7 +7062,7 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }) - * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { duration: 100, ease: t => t*t }) + * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { animation: { duration: 100, ease: t => t*t } }) * ``` * * @param partial - The shape partial to update. @@ -6971,9 +7072,9 @@ export class Editor extends EventEmitter { */ animateShape( partial: TLShapePartial | null | undefined, - animationOptions?: TLAnimationOptions + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions ): this { - return this.animateShapes([partial], animationOptions) + return this.animateShapes([partial], opts) } /** @@ -6982,7 +7083,7 @@ export class Editor extends EventEmitter { * @example * ```ts * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }]) - * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { duration: 100, ease: t => t*t }) + * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { animation: { duration: 100, ease: t => t*t } }) * ``` * * @param partials - The shape partials to update. @@ -6992,9 +7093,10 @@ export class Editor extends EventEmitter { */ animateShapes( partials: (TLShapePartial | null | undefined)[], - animationOptions = {} as TLAnimationOptions + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions ): this { - const { duration = 500, easing = EASINGS.linear } = animationOptions + if (!opts.animation) return this + const { duration = 500, easing = EASINGS.linear } = opts.animation const animationId = uniqueId() @@ -7047,7 +7149,7 @@ export class Editor extends EventEmitter { // update shapes also removes the shape from animating shapes } - this.removeListener('tick', handleTick) + this.off('tick', handleTick) return } @@ -7078,7 +7180,7 @@ export class Editor extends EventEmitter { this._updateShapes(updates, { squashing: true }) } - this.addListener('tick', handleTick) + this.on('tick', handleTick) return this } @@ -8510,7 +8612,6 @@ export class Editor extends EventEmitter { */ cancel(): this { this.dispatch({ type: 'misc', name: 'cancel' }) - this._tickManager.tick() return this } @@ -8526,7 +8627,6 @@ export class Editor extends EventEmitter { */ interrupt(): this { this.dispatch({ type: 'misc', name: 'interrupt' }) - this._tickManager.tick() return this } @@ -8777,12 +8877,12 @@ export class Editor extends EventEmitter { const { x: cx, y: cy, z: cz } = this.getCamera() - const { panSpeed, zoomSpeed } = this.getCameraOptions() - this.stopCameraAnimation() if (this.getInstanceState().followingUserId) { this.stopFollowingUser() } + + const { panSpeed, zoomSpeed } = this.getCameraOptions() this._setCamera( new Vec( cx + (dx * panSpeed) / cz - x / cz + x / (z * zoomSpeed), @@ -8943,6 +9043,7 @@ export class Editor extends EventEmitter { } if (this.inputs.isPanning && this.inputs.isPointing) { + clearTimeout(this._longPressTimeout) // Handle panning const { currentScreenPoint, previousScreenPoint } = this.inputs this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint)) @@ -8989,6 +9090,7 @@ export class Editor extends EventEmitter { if (info.button === 1) { if (!this.inputs.keys.has(' ')) { inputs.isPanning = false + this.slideCamera({ speed: Math.min(2, this.inputs.pointerVelocity.len()), direction: this.inputs.pointerVelocity, @@ -9098,6 +9200,8 @@ export class Editor extends EventEmitter { break } case 'pointer_up': { + clearTimeout(this._longPressTimeout) + const otherEvent = this._clickManager.transformPointerUpEvent(info) if (info.name !== otherEvent.name) { this.root.handleEvent(info) diff --git a/packages/editor/src/lib/utils/edgeScrolling.ts b/packages/editor/src/lib/utils/edgeScrolling.ts index e58699675..4c23eb0c1 100644 --- a/packages/editor/src/lib/utils/edgeScrolling.ts +++ b/packages/editor/src/lib/utils/edgeScrolling.ts @@ -1,5 +1,6 @@ import { COARSE_POINTER_WIDTH, EDGE_SCROLL_DISTANCE, EDGE_SCROLL_SPEED } from '../constants' import { Editor } from '../editor/Editor' +import { Vec } from '../primitives/Vec' /** * Helper function to get the scroll proximity factor for a given position. @@ -64,9 +65,5 @@ export function moveCameraWhenCloseToEdge(editor: Editor) { const camera = editor.getCamera() - editor.setCamera({ - x: camera.x + scrollDeltaX, - y: camera.y + scrollDeltaY, - z: camera.z, - }) + editor.setCamera(new Vec(camera.x + scrollDeltaX, camera.y + scrollDeltaY, camera.z)) } diff --git a/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx b/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx index abf831b0e..e2096d14d 100644 --- a/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx +++ b/packages/tldraw/src/lib/shapes/shared/ShapeFill.tsx @@ -1,5 +1,4 @@ import { - HASH_PATTERN_ZOOM_NAMES, TLDefaultColorStyle, TLDefaultColorTheme, TLDefaultFillStyle, @@ -10,6 +9,7 @@ import { useValue, } from '@tldraw/editor' import React from 'react' +import { HASH_PATTERN_ZOOM_NAMES } from './defaultStyleDefs' export interface ShapeFillProps { d: string @@ -39,7 +39,7 @@ export const ShapeFill = React.memo(function ShapeFill({ theme, d, color, fill } } }) -const PatternFill = function PatternFill({ d, color, theme }: ShapeFillProps) { +export function PatternFill({ d, color, theme }: ShapeFillProps) { const editor = useEditor() const svgExport = useSvgExportContext() const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor]) diff --git a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx index 64708285c..d2407bb32 100644 --- a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx +++ b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx @@ -3,7 +3,6 @@ import { DefaultFontFamilies, DefaultFontStyle, FileHelpers, - HASH_PATTERN_ZOOM_NAMES, SvgExportDef, TLDefaultFillStyle, TLDefaultFontStyle, @@ -11,10 +10,19 @@ import { debugFlags, useEditor, } from '@tldraw/editor' -import { HASH_PATTERN_COUNT } from '@tldraw/editor/src/lib/constants' import { useEffect, useMemo, useRef, useState } from 'react' import { useDefaultColorTheme } from './ShapeFill' +/** @internal */ +export const HASH_PATTERN_ZOOM_NAMES: Record = {} + +const HASH_PATTERN_COUNT = 6 + +for (let zoom = 1; zoom <= HASH_PATTERN_COUNT; zoom++) { + HASH_PATTERN_ZOOM_NAMES[zoom + '_dark'] = `hash_pattern_zoom_${zoom}_dark` + HASH_PATTERN_ZOOM_NAMES[zoom + '_light'] = `hash_pattern_zoom_${zoom}_light` +} + /** @public */ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef { return { diff --git a/packages/tldraw/src/lib/tools/HandTool/HandTool.ts b/packages/tldraw/src/lib/tools/HandTool/HandTool.ts index b1153a196..a33e034bd 100644 --- a/packages/tldraw/src/lib/tools/HandTool/HandTool.ts +++ b/packages/tldraw/src/lib/tools/HandTool/HandTool.ts @@ -12,14 +12,18 @@ export class HandTool extends StateNode { override onDoubleClick: TLClickEvent = (info) => { if (info.phase === 'settle') { const { currentScreenPoint } = this.editor.inputs - this.editor.zoomIn(currentScreenPoint, { duration: 220, easing: EASINGS.easeOutQuint }) + this.editor.zoomIn(currentScreenPoint, { + animation: { duration: 220, easing: EASINGS.easeOutQuint }, + }) } } override onTripleClick: TLClickEvent = (info) => { if (info.phase === 'settle') { const { currentScreenPoint } = this.editor.inputs - this.editor.zoomOut(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint }) + this.editor.zoomOut(currentScreenPoint, { + animation: { duration: 320, easing: EASINGS.easeOutQuint }, + }) } } @@ -31,9 +35,11 @@ export class HandTool extends StateNode { } = this.editor if (zoomLevel === 1) { - this.editor.zoomToFit({ duration: 400, easing: EASINGS.easeOutQuint }) + this.editor.zoomToFit({ animation: { duration: 400, easing: EASINGS.easeOutQuint } }) } else { - this.editor.resetZoom(currentScreenPoint, { duration: 320, easing: EASINGS.easeOutQuint }) + this.editor.resetZoom(currentScreenPoint, { + animation: { duration: 320, easing: EASINGS.easeOutQuint }, + }) } } } diff --git a/packages/tldraw/src/lib/tools/SelectTool/selectHelpers.ts b/packages/tldraw/src/lib/tools/SelectTool/selectHelpers.ts index d242ed302..14aed1ac6 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/selectHelpers.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/selectHelpers.ts @@ -148,7 +148,9 @@ export function zoomToShapeIfOffscreen(editor: Editor) { y: (eb.center.y - viewportPageBounds.center.y) * 2, }) editor.zoomToBounds(nextBounds, { - duration: ANIMATION_MEDIUM_MS, + animation: { + duration: ANIMATION_MEDIUM_MS, + }, inset: 0, }) } diff --git a/packages/tldraw/src/lib/tools/ZoomTool/childStates/Pointing.ts b/packages/tldraw/src/lib/tools/ZoomTool/childStates/Pointing.ts index 14c90cfaa..42c227d2d 100644 --- a/packages/tldraw/src/lib/tools/ZoomTool/childStates/Pointing.ts +++ b/packages/tldraw/src/lib/tools/ZoomTool/childStates/Pointing.ts @@ -26,9 +26,9 @@ export class Pointing extends StateNode { private complete() { const { currentScreenPoint } = this.editor.inputs if (this.editor.inputs.altKey) { - this.editor.zoomOut(currentScreenPoint, { duration: 220 }) + this.editor.zoomOut(currentScreenPoint, { animation: { duration: 220 } }) } else { - this.editor.zoomIn(currentScreenPoint, { duration: 220 }) + this.editor.zoomIn(currentScreenPoint, { animation: { duration: 220 } }) } this.parent.transition('idle', this.info) } diff --git a/packages/tldraw/src/lib/tools/ZoomTool/childStates/ZoomBrushing.ts b/packages/tldraw/src/lib/tools/ZoomTool/childStates/ZoomBrushing.ts index 2a29c6ca2..ab6373eb3 100644 --- a/packages/tldraw/src/lib/tools/ZoomTool/childStates/ZoomBrushing.ts +++ b/packages/tldraw/src/lib/tools/ZoomTool/childStates/ZoomBrushing.ts @@ -48,13 +48,13 @@ export class ZoomBrushing extends StateNode { if (zoomBrush.width < threshold && zoomBrush.height < threshold) { const point = this.editor.inputs.currentScreenPoint if (this.editor.inputs.altKey) { - this.editor.zoomOut(point, { duration: 220 }) + this.editor.zoomOut(point, { animation: { duration: 220 } }) } else { - this.editor.zoomIn(point, { duration: 220 }) + this.editor.zoomIn(point, { animation: { duration: 220 } }) } } else { const targetZoom = this.editor.inputs.altKey ? this.editor.getZoomLevel() / 2 : undefined - this.editor.zoomToBounds(zoomBrush, { targetZoom, duration: 220 }) + this.editor.zoomToBounds(zoomBrush, { targetZoom, animation: { duration: 220 } }) } this.parent.transition('idle', this.info) diff --git a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx index a84eba262..4d525ba0c 100644 --- a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx +++ b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx @@ -54,7 +54,7 @@ export function DefaultMinimap() { minimap.originPagePoint.setTo(clampedPoint) minimap.originPageCenter.setTo(editor.getViewportPageBounds().center) - editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS }) + editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } }) }, [editor, minimap] ) @@ -85,7 +85,7 @@ export function DefaultMinimap() { const pagePoint = Vec.Add(point, delta) minimap.originPagePoint.setTo(pagePoint) minimap.originPageCenter.setTo(point) - editor.centerOnPoint(point, { duration: ANIMATION_MEDIUM_MS }) + editor.centerOnPoint(point, { animation: { duration: ANIMATION_MEDIUM_MS } }) } function release(e: PointerEvent) { diff --git a/packages/tldraw/src/lib/ui/components/ZoomMenu/DefaultZoomMenu.tsx b/packages/tldraw/src/lib/ui/components/ZoomMenu/DefaultZoomMenu.tsx index 42e45f2b7..165b04737 100644 --- a/packages/tldraw/src/lib/ui/components/ZoomMenu/DefaultZoomMenu.tsx +++ b/packages/tldraw/src/lib/ui/components/ZoomMenu/DefaultZoomMenu.tsx @@ -55,7 +55,9 @@ const ZoomTriggerButton = forwardRef( const msg = useTranslation() const handleDoubleClick = useCallback(() => { - editor.resetZoom(editor.getViewportScreenCenter(), { duration: ANIMATION_MEDIUM_MS }) + editor.resetZoom(editor.getViewportScreenCenter(), { + animation: { duration: ANIMATION_MEDIUM_MS }, + }) }, [editor]) return ( diff --git a/packages/tldraw/src/lib/ui/context/actions.tsx b/packages/tldraw/src/lib/ui/context/actions.tsx index 549616a5e..95370fc4e 100644 --- a/packages/tldraw/src/lib/ui/context/actions.tsx +++ b/packages/tldraw/src/lib/ui/context/actions.tsx @@ -1036,7 +1036,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { readonlyOk: true, onSelect(source) { trackEvent('zoom-in', { source }) - editor.zoomIn(editor.getViewportScreenCenter(), { duration: ANIMATION_MEDIUM_MS }) + editor.zoomIn(editor.getViewportScreenCenter(), { + animation: { duration: ANIMATION_MEDIUM_MS }, + }) }, }, { @@ -1046,7 +1048,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { readonlyOk: true, onSelect(source) { trackEvent('zoom-out', { source }) - editor.zoomOut(editor.getViewportScreenCenter(), { duration: ANIMATION_MEDIUM_MS }) + editor.zoomOut(editor.getViewportScreenCenter(), { + animation: { duration: ANIMATION_MEDIUM_MS }, + }) }, }, { @@ -1057,7 +1061,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { readonlyOk: true, onSelect(source) { trackEvent('reset-zoom', { source }) - editor.resetZoom(editor.getViewportScreenCenter(), { duration: ANIMATION_MEDIUM_MS }) + editor.resetZoom(editor.getViewportScreenCenter(), { + animation: { duration: ANIMATION_MEDIUM_MS }, + }) }, }, { @@ -1067,7 +1073,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { readonlyOk: true, onSelect(source) { trackEvent('zoom-to-fit', { source }) - editor.zoomToFit({ duration: ANIMATION_MEDIUM_MS }) + editor.zoomToFit({ animation: { duration: ANIMATION_MEDIUM_MS } }) }, }, { @@ -1080,7 +1086,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { if (mustGoBackToSelectToolFirst()) return trackEvent('zoom-to-selection', { source }) - editor.zoomToSelection({ duration: ANIMATION_MEDIUM_MS }) + editor.zoomToSelection({ animation: { duration: ANIMATION_MEDIUM_MS } }) }, }, { diff --git a/packages/tldraw/src/test/commands/animationSpeed.test.ts b/packages/tldraw/src/test/commands/animationSpeed.test.ts index f2a5b0e6d..0021f91c7 100644 --- a/packages/tldraw/src/test/commands/animationSpeed.test.ts +++ b/packages/tldraw/src/test/commands/animationSpeed.test.ts @@ -11,7 +11,7 @@ jest.useFakeTimers() it('zooms in gradually when duration is present and animtion speed is default', () => { expect(editor.getZoomLevel()).toBe(1) editor.user.updateUserPreferences({ animationSpeed: 1 }) // default - editor.zoomIn(undefined, { duration: 100 }) + editor.zoomIn(undefined, { animation: { duration: 100 } }) editor.emit('tick', 25) // <-- quarter way expect(editor.getZoomLevel()).not.toBe(2) editor.emit('tick', 25) // 50 <-- half way @@ -23,14 +23,14 @@ it('zooms in gradually when duration is present and animtion speed is default', it('zooms in gradually when duration is present and animtion speed is off', () => { expect(editor.getZoomLevel()).toBe(1) editor.user.updateUserPreferences({ animationSpeed: 0 }) // none - editor.zoomIn(undefined, { duration: 100 }) + editor.zoomIn(undefined, { animation: { duration: 100 } }) expect(editor.getZoomLevel()).toBe(2) // <-- Should skip! }) it('zooms in gradually when duration is present and animtion speed is double', () => { expect(editor.getZoomLevel()).toBe(1) editor.user.updateUserPreferences({ animationSpeed: 2 }) // default - editor.zoomIn(undefined, { duration: 100 }) + editor.zoomIn(undefined, { animation: { duration: 100 } }) editor.emit('tick', 25) // <-- half way expect(editor.getZoomLevel()).not.toBe(2) editor.emit('tick', 25) // 50 <-- should finish diff --git a/packages/tldraw/src/test/commands/centerOnPoint.test.ts b/packages/tldraw/src/test/commands/centerOnPoint.test.ts index b3720e28f..54ea862fb 100644 --- a/packages/tldraw/src/test/commands/centerOnPoint.test.ts +++ b/packages/tldraw/src/test/commands/centerOnPoint.test.ts @@ -12,7 +12,7 @@ it('centers on the point', () => { }) it('centers on the point with animation', () => { - editor.centerOnPoint({ x: 400, y: 400 }, { duration: 200 }) + editor.centerOnPoint({ x: 400, y: 400 }, { animation: { duration: 200 } }) expect(editor.getViewportPageCenter()).not.toMatchObject({ x: 400, y: 400 }) jest.advanceTimersByTime(100) expect(editor.getViewportPageCenter()).not.toMatchObject({ x: 400, y: 400 }) From 854fad9404d5c5f65b76dcc7ac341bf5abc556d5 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 15:01:33 +0100 Subject: [PATCH 50/82] Update editor.mdx --- apps/docs/content/docs/editor.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/editor.mdx b/apps/docs/content/docs/editor.mdx index 690470358..309902c80 100644 --- a/apps/docs/content/docs/editor.mdx +++ b/apps/docs/content/docs/editor.mdx @@ -301,7 +301,7 @@ editor.setCamera(0, 0, 1) ### Freeze the camera -You can prevent the user from changing the camera using the [Editor#setCameraOptions](?) method. +You can prevent the user from changing the camera using the Editor#setCameraOptions method. ```ts editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) From d316f27234aa77273d946848202a246aea469dee Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 15:02:20 +0100 Subject: [PATCH 51/82] Update Editor.ts --- packages/editor/api-report.md | 2 +- packages/editor/api/api.json | 23 ++++++++++++++++++----- packages/editor/src/lib/editor/Editor.ts | 4 ++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 6171c45ce..447863387 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -896,7 +896,7 @@ export class Editor extends EventEmitter { sendBackward(shapes: TLShape[] | TLShapeId[]): this; sendToBack(shapes: TLShape[] | TLShapeId[]): this; setCamera(point: VecLike, opts?: TLCameraMoveOptions): this; - setCameraOptions(options: TLCameraOptions, opts?: { + setCameraOptions(options: Partial, opts?: { force?: boolean; immediate?: boolean; initial?: boolean; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 2c15f9c55..277040a69 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -17211,11 +17211,24 @@ "kind": "Content", "text": "setCameraOptions(options: " }, + { + "kind": "Reference", + "text": "Partial", + "canonicalReference": "!Partial:type" + }, + { + "kind": "Content", + "text": "<" + }, { "kind": "Reference", "text": "TLCameraOptions", "canonicalReference": "@tldraw/editor!TLCameraOptions:type" }, + { + "kind": "Content", + "text": ">" + }, { "kind": "Content", "text": ", opts?: " @@ -17239,8 +17252,8 @@ ], "isStatic": false, "returnTypeTokenRange": { - "startIndex": 5, - "endIndex": 6 + "startIndex": 8, + "endIndex": 9 }, "releaseTag": "Public", "isProtected": false, @@ -17250,15 +17263,15 @@ "parameterName": "options", "parameterTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 5 }, "isOptional": false }, { "parameterName": "opts", "parameterTypeTokenRange": { - "startIndex": 3, - "endIndex": 4 + "startIndex": 6, + "endIndex": 7 }, "isOptional": true } diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index cd1833dfe..e6c85f710 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2134,10 +2134,10 @@ export class Editor extends EventEmitter { * * @public */ setCameraOptions( - options: TLCameraOptions, + options: Partial, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } ) { - this._cameraOptions.set(options) + this._cameraOptions.set({ ...this._cameraOptions.__unsafe__getWithoutCapture(), ...options }) this.setCamera(this.getCamera(), opts) return this } From 5705b19dd3ae41005de1a1444937131fdda44dda Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 15:04:22 +0100 Subject: [PATCH 52/82] use a partial --- apps/docs/content/docs/editor.mdx | 2 +- .../BeforeCreateUpdateShapeExample.tsx | 2 +- packages/tldraw/src/test/commands/pan.test.ts | 4 ++-- packages/tldraw/src/test/commands/zoomIn.test.ts | 2 +- packages/tldraw/src/test/commands/zoomOut.test.ts | 2 +- packages/tldraw/src/test/commands/zoomToBounds.test.ts | 2 +- packages/tldraw/src/test/commands/zoomToFit.test.ts | 2 +- packages/tldraw/src/test/commands/zoomToSelection.test.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/docs/content/docs/editor.mdx b/apps/docs/content/docs/editor.mdx index 309902c80..583896996 100644 --- a/apps/docs/content/docs/editor.mdx +++ b/apps/docs/content/docs/editor.mdx @@ -304,7 +304,7 @@ editor.setCamera(0, 0, 1) You can prevent the user from changing the camera using the Editor#setCameraOptions method. ```ts -editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) +editor.setCameraOptions({ isLocked: true }) ``` ### Turn on dark mode diff --git a/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx b/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx index db8c89547..11c8b019e 100644 --- a/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx +++ b/apps/examples/src/examples/before-create-update-shape/BeforeCreateUpdateShapeExample.tsx @@ -44,7 +44,7 @@ export default function BeforeCreateUpdateShapeExample() { editor.zoomToBounds(new Box(-500, -500, 1000, 1000)) // lock the camera on that area - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) }} components={{ // to make it a little clearer what's going on in this example, we'll draw a diff --git a/packages/tldraw/src/test/commands/pan.test.ts b/packages/tldraw/src/test/commands/pan.test.ts index 8a52e5581..247aab4a3 100644 --- a/packages/tldraw/src/test/commands/pan.test.ts +++ b/packages/tldraw/src/test/commands/pan.test.ts @@ -15,13 +15,13 @@ describe('When panning', () => { }) it('Updates the camera with panSpeed at 2', () => { - editor.setCameraOptions({ ...editor.getCameraOptions(), panSpeed: 2 }) + editor.setCameraOptions({ panSpeed: 2 }) editor.pan({ x: 200, y: 200 }) editor.expectCameraToBe(400, 400, 1) }) it('Updates the camera with panSpeed', () => { - editor.setCameraOptions({ ...editor.getCameraOptions(), panSpeed: 0.5 }) + editor.setCameraOptions({ panSpeed: 0.5 }) editor.pan({ x: 200, y: 200 }) editor.expectCameraToBe(100, 100, 1) }) diff --git a/packages/tldraw/src/test/commands/zoomIn.test.ts b/packages/tldraw/src/test/commands/zoomIn.test.ts index c3ca247cd..41dae0ab0 100644 --- a/packages/tldraw/src/test/commands/zoomIn.test.ts +++ b/packages/tldraw/src/test/commands/zoomIn.test.ts @@ -63,7 +63,7 @@ it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => { it('does not zoom when camera is frozen', () => { editor.setCamera({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) editor.zoomIn() expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) }) diff --git a/packages/tldraw/src/test/commands/zoomOut.test.ts b/packages/tldraw/src/test/commands/zoomOut.test.ts index 1a5c1453f..09bce0367 100644 --- a/packages/tldraw/src/test/commands/zoomOut.test.ts +++ b/packages/tldraw/src/test/commands/zoomOut.test.ts @@ -26,7 +26,7 @@ it('zooms out and in by increments', () => { it('does not zoom out when camera is frozen', () => { editor.setCamera({ x: 0, y: 0, z: 1 }) expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) editor.zoomOut() expect(editor.getCamera()).toMatchObject({ x: 0, y: 0, z: 1 }) }) diff --git a/packages/tldraw/src/test/commands/zoomToBounds.test.ts b/packages/tldraw/src/test/commands/zoomToBounds.test.ts index 6ddc39c36..ff6967d07 100644 --- a/packages/tldraw/src/test/commands/zoomToBounds.test.ts +++ b/packages/tldraw/src/test/commands/zoomToBounds.test.ts @@ -44,7 +44,7 @@ it('does not zoom past min', () => { it('does not zoom to bounds when camera is frozen', () => { editor.setScreenBounds({ x: 0, y: 0, w: 1000, h: 1000 }) expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 }) - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) editor.zoomToBounds(new Box(200, 300, 300, 300)) expect(editor.getViewportPageCenter().toJson()).toCloselyMatchObject({ x: 500, y: 500 }) }) diff --git a/packages/tldraw/src/test/commands/zoomToFit.test.ts b/packages/tldraw/src/test/commands/zoomToFit.test.ts index eb86b7bab..254982883 100644 --- a/packages/tldraw/src/test/commands/zoomToFit.test.ts +++ b/packages/tldraw/src/test/commands/zoomToFit.test.ts @@ -14,7 +14,7 @@ it('converts correctly', () => { it('does not zoom to bounds when camera is frozen', () => { const cameraBefore = { ...editor.getCamera() } - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) editor.zoomToFit() expect(editor.getCamera()).toMatchObject(cameraBefore) }) diff --git a/packages/tldraw/src/test/commands/zoomToSelection.test.ts b/packages/tldraw/src/test/commands/zoomToSelection.test.ts index 98e0ed291..847690b98 100644 --- a/packages/tldraw/src/test/commands/zoomToSelection.test.ts +++ b/packages/tldraw/src/test/commands/zoomToSelection.test.ts @@ -35,7 +35,7 @@ it('does not zoom past min', () => { it('does not zoom to selection when camera is frozen', () => { const cameraBefore = { ...editor.getCamera() } - editor.setCameraOptions({ ...editor.getCameraOptions(), isLocked: true }) + editor.setCameraOptions({ isLocked: true }) editor.setSelectedShapes([ids.box1, ids.box2]) editor.zoomToSelection() expect(editor.getCamera()).toMatchObject(cameraBefore) From cad8ebb167fda0d29a7046d65b57f253b4034b2b Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 16 Apr 2024 15:04:58 +0100 Subject: [PATCH 53/82] Update editor.mdx --- apps/docs/content/docs/editor.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/editor.mdx b/apps/docs/content/docs/editor.mdx index 583896996..68ec57dc5 100644 --- a/apps/docs/content/docs/editor.mdx +++ b/apps/docs/content/docs/editor.mdx @@ -301,7 +301,7 @@ editor.setCamera(0, 0, 1) ### Freeze the camera -You can prevent the user from changing the camera using the Editor#setCameraOptions method. +You can prevent the user from changing the camera using the `Editor.setCameraOptions` method. ```ts editor.setCameraOptions({ isLocked: true }) From 056b71c7479a7621b91158842434a5abdc19b45a Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Apr 2024 09:48:11 +0100 Subject: [PATCH 54/82] fix options --- .../camera-options/CameraOptionsExample.tsx | 169 ++++++++++++++---- packages/editor/src/lib/editor/Editor.ts | 20 +-- 2 files changed, 144 insertions(+), 45 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index aa2c55a29..9ea1bbaa5 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -1,5 +1,14 @@ import { useEffect } from 'react' -import { TLCameraOptions, Tldraw, clamp, track, useEditor, useLocalStorageState } from 'tldraw' +import { + BoxModel, + TLCameraOptions, + Tldraw, + Vec, + clamp, + track, + useEditor, + useLocalStorageState, +} from 'tldraw' import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { @@ -24,6 +33,13 @@ const CAMERA_OPTIONS: TLCameraOptions = { isLocked: false, } +const BOUNDS_SIZES: Record = { + a4: { x: 0, y: 0, w: 1050, h: 1485 }, + landscape: { x: 0, y: 0, w: 1600, h: 900 }, + portrait: { x: 0, y: 0, w: 900, h: 1600 }, + square: { x: 0, y: 0, w: 900, h: 900 }, +} + export default function CameraOptionsExample() { return (
@@ -75,6 +91,10 @@ const BoundsDisplay = track(() => { }, } = cameraOptions + const d = Vec.ToAngle({ x: w, y: h }) * (180 / Math.PI) + const colB = '#00000002' + const colA = '#0000001F' + return ( <>
{ // grey and white stripes border: '1px dashed var(--color-text)', backgroundImage: ` - linear-gradient(45deg, #AAAAAA44 25%, transparent 25%), - linear-gradient(-45deg, #AAAAAA44 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #AAAAAA44 75%), - linear-gradient(-45deg, transparent 75%, #AAAAAA44 75%)`, + + `, backgroundSize: '200px 200px', backgroundPosition: '0 0, 0 100px, 100px -100px, -100px 0px', }} - /> + > +
+
) }) @@ -124,16 +157,18 @@ const CameraOptionsControlPanel = track(() => { } > ) => { + const { constraints } = options const cameraOptions = editor.getCameraOptions() setCameraOptions({ ...cameraOptions, ...options, - constraints: options.constraints - ? { - ...cameraOptions.constraints!, - ...options.constraints, - } - : undefined, + constraints: + constraints === undefined + ? cameraOptions.constraints + : { + ...(cameraOptions.constraints! ?? CAMERA_OPTIONS.constraints), + ...constraints, + }, }) } @@ -149,19 +184,74 @@ const CameraOptionsControlPanel = track(() => { zIndex: 1000000, }} > - {constraints ? ( - <> -
+
+ + { + const val = clamp(Number(e.target.value), 0, 2) + updateOptions({ zoomSpeed: val }) + }} + /> + + { + const val = clamp(Number(e.target.value), 0, 2) + updateOptions({ panSpeed: val }) + }} + /> + + + {constraints ? ( + <> -
- - ) : null} - + + ) : null} +
+
+ + +
) }) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index ffca87024..6cf731b25 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -2143,16 +2143,16 @@ export class Editor extends EventEmitter { /** @internal */ private _setCamera( - point: Vec, + point: VecLike, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } ): this { const currentCamera = this.getCamera() - let { x, y, z } = point + let { x, y, z = currentCamera.z } = point // If force is true, then we'll set the camera to the point regardless of // the camera options, so that we can handle gestures that permit elasticity - // or decay. + // or decay, or animations that occur while the camera is locked. if (!opts?.force) { // Apply any adjustments based on the camera options @@ -2299,7 +2299,7 @@ export class Editor extends EventEmitter { const { x: cx, y: cy, z: cz } = currentCamera const cxA = -cx + vsb.w / cz / 2 const cyA = -cy + vsb.h / cz / 2 - z = clamp(point.z, zoomMin, zoomMax) + z = clamp(z, zoomMin, zoomMax) const cxB = -cx + vsb.w / z / 2 const cyB = -cy + vsb.h / z / 2 x = cx + cxB - cxA @@ -2313,7 +2313,7 @@ export class Editor extends EventEmitter { } this.batch(() => { - const camera = { ...currentCamera, ...point } + const camera = { ...currentCamera, x, y, z } this.store.put([camera]) // include id and meta here // Dispatch a new pointer move because the pointer's page will have changed @@ -2370,6 +2370,9 @@ export class Editor extends EventEmitter { * @public */ setCamera(point: VecLike, opts?: TLCameraMoveOptions): this { + const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture() + if (isLocked && !opts?.force) return this + // Stop any camera animations this.stopCameraAnimation() @@ -2378,7 +2381,7 @@ export class Editor extends EventEmitter { this.stopFollowingUser() } - const _point = Vec.From(point) + const _point = Vec.Cast(point) if (!Number.isFinite(_point.x)) _point.x = 0 if (!Number.isFinite(_point.y)) _point.y = 0 @@ -8917,10 +8920,7 @@ export class Editor extends EventEmitter { // Update the camera here, which will dispatch a pointer move... // this will also update the pointer position, etc - const { x: cx, y: cy, z: cz } = this.getCamera() - this._setCamera(new Vec(cx + info.delta.x / cz, cy + info.delta.y / cz, cz), { - immediate: true, - }) + this.pan(info.delta, { immediate: true }) if ( !inputs.isDragging && From 7e08ff018176b263605f209ce67d1c2cfac225a9 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Apr 2024 10:12:57 +0100 Subject: [PATCH 55/82] remove zoom min and zoom max --- .../camera-options/CameraOptionsExample.tsx | 49 ++++++++++++++----- .../image-annotator/ImageAnnotationEditor.tsx | 2 - packages/editor/api-report.md | 2 - packages/editor/api/api.json | 2 +- packages/editor/src/lib/editor/Editor.ts | 30 ++++++++---- .../editor/src/lib/editor/types/misc-types.ts | 4 -- 6 files changed, 57 insertions(+), 32 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index 9ea1bbaa5..204e643b3 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -12,6 +12,10 @@ import { import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { + isLocked: false, + panSpeed: 1, + zoomSpeed: 1, + zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], constraints: { fit: 'max', bounds: { @@ -25,12 +29,6 @@ const CAMERA_OPTIONS: TLCameraOptions = { padding: { x: 100, y: 100 }, origin: { x: 0.5, y: 0.5 }, }, - panSpeed: 1, - zoomSteps: [0.1, 0.5, 0.75, 1, 1.5, 2, 4, 8], - zoomMax: 8, - zoomMin: 0.1, - zoomSpeed: 1, - isLocked: false, } const BOUNDS_SIZES: Record = { @@ -195,6 +193,32 @@ const CameraOptionsControlPanel = track(() => { justifyContent: 'center', }} > + + + + { + const val = clamp(Number(e.target.value), 0, 2) + updateOptions({ panSpeed: val }) + }} + /> { updateOptions({ zoomSpeed: val }) }} /> - + { - const val = clamp(Number(e.target.value), 0, 2) - updateOptions({ panSpeed: val }) + const val = e.target.value.split(', ').map((v) => Number(v)) + updateOptions({ zoomSteps: val }) }} /> diff --git a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx index 90641f712..0749a85a3 100644 --- a/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx +++ b/apps/examples/src/examples/image-annotator/ImageAnnotationEditor.tsx @@ -140,8 +140,6 @@ export function ImageAnnotationEditor({ fitX: 'inside', fitY: 'inside', }, - zoomMax: 8, - zoomMin: 1, // prevent zoom from going below zero zoomSteps: [1, 2, 4, 8], zoomSpeed: 1, panSpeed: 1, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 547cd8fe6..c2ba1e98c 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2007,8 +2007,6 @@ export type TLCameraMoveOptions = Partial<{ // @public (undocumented) export type TLCameraOptions = { - zoomMax: number; - zoomMin: number; constraints?: { fitX: 'contain' | 'inside' | 'lock' | 'outside'; fitY: 'contain' | 'inside' | 'lock' | 'outside'; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 31b8f4602..fce4d94fb 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36966,7 +36966,7 @@ }, { "kind": "Content", - "text": "{\n zoomMax: number;\n zoomMin: number;\n constraints?: {\n fitX: 'contain' | 'inside' | 'lock' | 'outside';\n fitY: 'contain' | 'inside' | 'lock' | 'outside';\n bounds: " + "text": "{\n constraints?: {\n fitX: 'contain' | 'inside' | 'lock' | 'outside';\n fitY: 'contain' | 'inside' | 'lock' | 'outside';\n bounds: " }, { "kind": "Reference", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 6cf731b25..5c2369d88 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -53,6 +53,7 @@ import { getIndicesBetween, getOwnProperty, hasOwnProperty, + last, objectMapValues, sortById, sortByIndex, @@ -2136,7 +2137,9 @@ export class Editor extends EventEmitter { options: Partial, opts?: { immediate?: boolean; force?: boolean; initial?: boolean } ) { - this._cameraOptions.set({ ...this._cameraOptions.__unsafe__getWithoutCapture(), ...options }) + const next = { ...this._cameraOptions.__unsafe__getWithoutCapture(), ...options } + if (next.zoomSteps?.length < 1) next.zoomSteps = [1] + this._cameraOptions.set(next) this.setCamera(this.getCamera(), opts) return this } @@ -2158,9 +2161,12 @@ export class Editor extends EventEmitter { const cameraOptions = this.getCameraOptions() + const zoomMin = cameraOptions.zoomSteps[0] + const zoomMax = last(cameraOptions.zoomSteps)! + // If bounds are provided, then we'll keep those bounds on screen if (cameraOptions.constraints) { - const { zoomMax, zoomMin, constraints } = cameraOptions + const { constraints } = cameraOptions const vsb = this.getViewportScreenBounds() @@ -2293,7 +2299,7 @@ export class Editor extends EventEmitter { } } else { // constrain the zoom, preserving the center - const { zoomMax, zoomMin } = cameraOptions + if (z > zoomMax || z < zoomMin) { const vsb = this.getViewportScreenBounds() const { x: cx, y: cy, z: cz } = currentCamera @@ -2523,10 +2529,10 @@ export class Editor extends EventEmitter { const { x: cx, y: cy, z: cz } = this.getCamera() - const { zoomMax, zoomSteps } = this.getCameraOptions() + const { zoomSteps } = this.getCameraOptions() if (zoomSteps !== null && zoomSteps.length > 1) { const fitZoom = this.getCameraFitZoom() - let zoom = zoomMax * fitZoom + let zoom = last(zoomSteps)! * fitZoom for (let i = 1; i < zoomSteps.length; i++) { const z1 = zoomSteps[i - 1] * fitZoom const z2 = zoomSteps[i] * fitZoom @@ -2565,12 +2571,12 @@ export class Editor extends EventEmitter { zoomOut(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { if (this.getCameraOptions().isLocked) return this - const { zoomMin, zoomSteps } = this.getCameraOptions() + const { zoomSteps } = this.getCameraOptions() if (zoomSteps !== null && zoomSteps.length > 1) { const fitZoom = this.getCameraFitZoom() const { x: cx, y: cy, z: cz } = this.getCamera() - let zoom = zoomMin * fitZoom - for (let i = zoomSteps.length - 1; i > 0; i--) { + let zoom = zoomSteps[0] * fitZoom + for (let i = zoomSteps.length - 2; i > 0; i--) { const z1 = zoomSteps[i - 1] * fitZoom const z2 = zoomSteps[i] * fitZoom if (z2 - cz >= (z2 - z1) / 2) continue @@ -2701,7 +2707,9 @@ export class Editor extends EventEmitter { const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28) const fitZoom = this.getCameraFitZoom() - const { zoomMin, zoomMax } = this.getCameraOptions() + const { zoomSteps } = this.getCameraOptions() + const zoomMin = zoomSteps[0] + const zoomMax = last(zoomSteps)! let zoom = clamp( Math.min( @@ -3279,7 +3287,9 @@ export class Editor extends EventEmitter { : height / desiredHeight const fitZoom = this.getCameraFitZoom() - const { zoomMin, zoomMax } = this.getCameraOptions() + const { zoomSteps } = this.getCameraOptions() + const zoomMin = zoomSteps[0] + const zoomMax = last(zoomSteps)! const targetZoom = clamp(this.getCamera().z * ratio, zoomMin * fitZoom, zoomMax * fitZoom) const targetWidth = this.getViewportScreenBounds().w / targetZoom const targetHeight = this.getViewportScreenBounds().h / targetZoom diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index e900d1afe..8f30fe300 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -25,10 +25,6 @@ export type TLCameraOptions = { zoomSpeed: number /** The steps that a user can zoom between with zoom in / zoom out (zoom factors) */ zoomSteps: number[] - /** A minimum zoom factor (e.g. .5x of the zoom at which the shape is fully on screen) */ - zoomMin: number - /** A maximum zoom factor (e.g. 2x of the zoom at which the shape is fully on screen) */ - zoomMax: number /** Whether the camera is locked */ isLocked: boolean /** The camera constraints */ From 39300135e96c941e748e616ab1b47763033b8d01 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Apr 2024 11:04:13 +0100 Subject: [PATCH 56/82] Update useUrlState.ts --- apps/dotcom/src/hooks/useUrlState.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/dotcom/src/hooks/useUrlState.ts b/apps/dotcom/src/hooks/useUrlState.ts index 8a9289464..363b811d9 100644 --- a/apps/dotcom/src/hooks/useUrlState.ts +++ b/apps/dotcom/src/hooks/useUrlState.ts @@ -69,7 +69,9 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) { const { x, y, w, h } = viewport const { w: sw, h: sh } = editor.getViewportScreenBounds() const fitZoom = editor.getCameraFitZoom() - const { zoomMin, zoomMax } = editor.getCameraOptions() + const { zoomSteps } = editor.getCameraOptions() + const zoomMin = zoomSteps[0] + const zoomMax = zoomSteps[zoomSteps.length - 1] const zoom = clamp(Math.min(sw / w, sh / h), zoomMin * fitZoom, zoomMax * fitZoom) From 27c5c1a520cddd68cfcd7ea4136eae2dd34afe6f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 17 Apr 2024 11:45:53 +0100 Subject: [PATCH 57/82] Update useSlides.tsx --- apps/examples/src/examples/slides/useSlides.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/examples/src/examples/slides/useSlides.tsx b/apps/examples/src/examples/slides/useSlides.tsx index 9f135c2ea..4f3d79562 100644 --- a/apps/examples/src/examples/slides/useSlides.tsx +++ b/apps/examples/src/examples/slides/useSlides.tsx @@ -8,7 +8,10 @@ export function moveToSlide(editor: Editor, slide: SlideShape) { if (!bounds) return $currentSlide.set(slide) editor.selectNone() - editor.zoomToBounds(bounds, { duration: 500, easing: EASINGS.easeInOutCubic, inset: 0 }) + editor.zoomToBounds(bounds, { + inset: 0, + animation: { duration: 500, easing: EASINGS.easeInOutCubic }, + }) } export function useSlides() { From a9d1c921c7c1c0b79b8549beddccbf2238415429 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 11:37:54 +0100 Subject: [PATCH 58/82] cleanup some pointer stuff --- .../camera-options/CameraOptionsExample.tsx | 1 + packages/editor/api-report.md | 7 +- packages/editor/api/api.json | 2 +- packages/editor/src/index.ts | 2 +- packages/editor/src/lib/constants.ts | 15 +- packages/editor/src/lib/editor/Editor.ts | 316 +++++++++--------- .../src/lib/editor/managers/ClickManager.ts | 216 ++++++------ .../editor/src/lib/editor/types/misc-types.ts | 1 + packages/tldraw/src/test/TestEditor.ts | 4 +- .../tldraw/src/test/commands/zoomIn.test.ts | 6 +- .../tldraw/src/test/commands/zoomOut.test.ts | 4 +- 11 files changed, 275 insertions(+), 299 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index 204e643b3..f03470e58 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -12,6 +12,7 @@ import { import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { + wheelBehavior: 'pan', isLocked: false, panSpeed: 1, zoomSpeed: 1, diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index c2ba1e98c..2b8c14ac6 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -459,6 +459,9 @@ export const DEFAULT_ANIMATION_OPTIONS: { easing: (t: number) => number; }; +// @internal (undocumented) +export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions; + // @public (undocumented) export function DefaultBackground(): JSX_2.Element; @@ -1093,9 +1096,6 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap // @public (undocumented) export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string; -// @internal (undocumented) -export const getDefaultCameraOptions: (cameraOptions?: Partial) => TLCameraOptions; - // @public (undocumented) export function getFreshUserPreferences(): TLUserPreferences; @@ -2019,6 +2019,7 @@ export type TLCameraOptions = { zoomSpeed: number; zoomSteps: number[]; isLocked: boolean; + wheelBehavior: 'none' | 'pan' | 'zoom'; }; // @public (undocumented) diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index d7b6cb4c5..d5958ed22 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -36993,7 +36993,7 @@ }, { "kind": "Content", - "text": ";\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n}" + "text": ";\n fit: 'max' | 'min' | 'none' | 'x' | 'y';\n };\n panSpeed: number;\n zoomSpeed: number;\n zoomSteps: number[];\n isLocked: boolean;\n wheelBehavior: 'none' | 'pan' | 'zoom';\n}" }, { "kind": "Content", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index fb001ee65..61322272d 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -111,6 +111,7 @@ export { ANIMATION_SHORT_MS, CAMERA_SLIDE_FRICTION, DEFAULT_ANIMATION_OPTIONS, + DEFAULT_CAMERA_OPTIONS, DOUBLE_CLICK_DURATION, DRAG_DISTANCE, GRID_STEPS, @@ -120,7 +121,6 @@ export { MULTI_CLICK_DURATION, SIDES, SVG_PADDING, - getDefaultCameraOptions, } from './lib/constants' export { Editor, diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index a49d0bfe9..a552b3c9e 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -11,24 +11,15 @@ export const ANIMATION_SHORT_MS = 80 /** @internal */ export const ANIMATION_MEDIUM_MS = 320 -const DEFAULT_COMMON_CAMERA_OPTIONS = { - zoomMax: 8, - zoomMin: 0.1, +/** @internal */ +export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], zoomSpeed: 1, panSpeed: 1, isLocked: false, + wheelBehavior: 'pan', } -/** @internal */ -export const getDefaultCameraOptions = ( - cameraOptions: Partial = {} -): TLCameraOptions => ({ - ...DEFAULT_COMMON_CAMERA_OPTIONS, - ...cameraOptions, -}) - -/** @internal */ export const FOLLOW_CHASE_PROPORTION = 0.5 /** @internal */ export const FOLLOW_CHASE_PAN_SNAP = 0.1 diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 7523a6c8a..b0ed88e76 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -71,6 +71,7 @@ import { COARSE_DRAG_DISTANCE, COLLABORATOR_IDLE_TIMEOUT, DEFAULT_ANIMATION_OPTIONS, + DEFAULT_CAMERA_OPTIONS, DRAG_DISTANCE, FOLLOW_CHASE_PAN_SNAP, FOLLOW_CHASE_PAN_UNSNAP, @@ -82,7 +83,6 @@ import { LONG_PRESS_DURATION, MAX_PAGES, MAX_SHAPES_PER_PAGE, - getDefaultCameraOptions, } from '../constants' import { Box, BoxLike } from '../primitives/Box' import { Mat, MatLike, MatModel } from '../primitives/Mat' @@ -211,7 +211,7 @@ export class Editor extends EventEmitter { this.snaps = new SnapManager(this) - this._cameraOptions.set(getDefaultCameraOptions(cameraOptions)) + this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions }) this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false) @@ -2105,7 +2105,7 @@ export class Editor extends EventEmitter { } } - private _cameraOptions = atom('camera options', getDefaultCameraOptions({})) + private _cameraOptions = atom('camera options', DEFAULT_CAMERA_OPTIONS) /** * Get the current camera options. @@ -8551,6 +8551,8 @@ export class Editor extends EventEmitter { // Reset velocity on pointer down, or when a pinch starts or ends if (info.name === 'pointer_down' || this.inputs.isPinching) { pointerVelocity.set(0, 0) + this.inputs.originScreenPoint.setTo(currentScreenPoint) + this.inputs.originPagePoint.setTo(currentPagePoint) } // todo: We only have to do this if there are multiple users in the document @@ -8805,15 +8807,20 @@ export class Editor extends EventEmitter { this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150) } - const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs + const { originPagePoint, currentPagePoint } = inputs if (!inputs.isPointing) { inputs.isDragging = false } + const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + const pageState = this.store.get(this._getCurrentPageStateId())! + const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture()! + const camera = this.store.unsafeGetWithoutCapture(this.getCameraId())! + switch (type) { case 'pinch': { - if (this.getCameraOptions().isLocked) return + if (cameraOptions.isLocked) return clearTimeout(this._longPressTimeout) this._updateInputsFromEvent(info) @@ -8824,7 +8831,7 @@ export class Editor extends EventEmitter { if (!inputs.isEditing) { this._pinchStart = this.getCamera().z if (!this._selectedShapeIdsAtPointerDown.length) { - this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() + this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds] } this._didPinch = true @@ -8844,17 +8851,21 @@ export class Editor extends EventEmitter { delta: { x: dx, y: dy }, } = info - const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! - const { x, y } = Vec.SubXY(info.point, screenBounds.x, screenBounds.y) + // The center of the pinch in screen space + const { x, y } = Vec.SubXY( + info.point, + instanceState.screenBounds.x, + instanceState.screenBounds.y + ) - const { x: cx, y: cy, z: cz } = this.getCamera() + const { x: cx, y: cy, z: cz } = camera this.stopCameraAnimation() - if (this.getInstanceState().followingUserId) { + if (instanceState.followingUserId) { this.stopFollowingUser() } - const { panSpeed, zoomSpeed } = this.getCameraOptions() + const { panSpeed, zoomSpeed } = cameraOptions this._setCamera( new Vec( cx + (dx * panSpeed) / cz - x / cz + x / (z * zoomSpeed), @@ -8869,18 +8880,24 @@ export class Editor extends EventEmitter { case 'pinch_end': { if (!inputs.isPinching) return this + // Stop pinching inputs.isPinching = false - const { _selectedShapeIdsAtPointerDown } = this - this.setSelectedShapes(this._selectedShapeIdsAtPointerDown, { squashing: true }) + + // Stash and clear the + const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this this._selectedShapeIdsAtPointerDown = [] if (this._didPinch) { this._didPinch = false - this.once('tick', () => { - if (!this._didPinch) { - this.setSelectedShapes(_selectedShapeIdsAtPointerDown, { squashing: true }) - } - }) + if (shapesToReselect.length > 0) { + this.once('tick', () => { + if (!this._didPinch) { + // Unless we've started pinching again... + // Reselect the shapes that were selected when the pinch started + this.setSelectedShapes(shapesToReselect, { squashing: true }) + } + }) + } } return // Stop here! @@ -8888,164 +8905,164 @@ export class Editor extends EventEmitter { } } case 'wheel': { - if (this.getCameraOptions().isLocked) return + if (cameraOptions.isLocked) return this._updateInputsFromEvent(info) if (this.getIsMenuOpen()) { // noop } else { + const { panSpeed, zoomSpeed, wheelBehavior } = cameraOptions + + if (wheelBehavior === 'none') return + + // Stop any camera animation this.stopCameraAnimation() - if (this.getInstanceState().followingUserId) { + // Stop following any following user + if (instanceState.followingUserId) { this.stopFollowingUser() } - if (inputs.ctrlKey) { - // todo: Start or update the zoom end interval - // If the alt or ctrl keys are pressed, - // zoom or pan the camera and then return. + const { x: cx, y: cy, z: cz } = camera + const { x: dx, y: dy, z: dz = 0 } = info.delta - // Subtract the top left offset from the user's point + // If the camera behavior is "zoom" and the ctrl key is presssed, then pan; + // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom + const behavior = + wheelBehavior === 'zoom' ? (inputs.ctrlKey ? 'pan' : 'zoom') : wheelBehavior - const { x, y } = this.inputs.currentScreenPoint - - const { x: cx, y: cy, z: cz } = this.getCamera() - - const { zoomSpeed } = this.getCameraOptions() - const zoom = cz + (info.delta.z ?? 0) * zoomSpeed * cz - - this._setCamera( - new Vec(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom), - { immediate: true } - ) - - // We want to return here because none of the states in our - // statechart should respond to this event (a camera zoom) - return - } - - // Update the camera here, which will dispatch a pointer move... - // this will also update the pointer position, etc - this.pan(info.delta, { immediate: true }) - - if ( - !inputs.isDragging && - inputs.isPointing && - Vec.Dist2(originPagePoint, currentPagePoint) > - (this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / - this.getZoomLevel() - ) { - clearTimeout(this._longPressTimeout) - inputs.isDragging = true + switch (behavior) { + case 'zoom': { + // Zoom in on current screen point using the wheel delta + const { x, y } = this.inputs.currentScreenPoint + const zoom = cz + (dz ?? 0) * zoomSpeed * cz + this._setCamera( + new Vec( + cx + (x / zoom - x) - (x / cz - x), + cy + (y / zoom - y) - (y / cz - y), + zoom + ), + { immediate: true } + ) + return + } + case 'pan': { + // Pan the camera based on the wheel delta + this._setCamera(new Vec(cx + (dx * panSpeed) / cz, cy + (dy * panSpeed) / cz, cz), { + immediate: true, + }) + return + } } } break } case 'pointer': { - // If we're pinching, return + // Ignore pointer events while we're pinching if (inputs.isPinching) return this._updateInputsFromEvent(info) - const { isPen } = info + const { isPenMode } = instanceState switch (info.name) { case 'pointer_down': { + // If we're in pen mode and the input is not a pen type, then stop here + if (isPenMode && !isPen) return + + // Close any open menus this.clearOpenMenus() + // Start a long press timeout this._longPressTimeout = setTimeout(() => { this.dispatch({ ...info, name: 'long_press' }) }, LONG_PRESS_DURATION) + // Save the selected ids at pointer down this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() // Firefox bug fix... // If it's a left-mouse-click, we store the pointer id for later user - if (info.button === 0) { - this.capturedPointerId = info.pointerId - } + if (info.button === 0) this.capturedPointerId = info.pointerId // Add the button from the buttons set inputs.buttons.add(info.button) + // Start pointing and stop dragging inputs.isPointing = true inputs.isDragging = false - if (this.getInstanceState().isPenMode) { - if (!isPen) { - return - } - } else { - if (isPen) { - this.updateInstanceState({ isPenMode: true }) - } - } + // If pen mode is off but we're not already in pen mode, turn that on + if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true }) + // On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser if (info.button === 5) { - // Eraser button activates eraser this._restoreToolId = this.getCurrentToolId() this.complete() this.setCurrentTool('eraser') } else if (info.button === 1) { - // Middle mouse pan activates panning + // Middle mouse pan activates panning unless we're already panning (with spacebar) if (!this.inputs.isPanning) { this._prevCursor = this.getInstanceState().cursor.type } - this.inputs.isPanning = true + clearTimeout(this._longPressTimeout) } + // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click + // Also stop here, we don't want the state chart to receive the event if (this.inputs.isPanning) { this.stopCameraAnimation() this.setCursor({ type: 'grabbing', rotation: 0 }) return this } - originScreenPoint.setTo(currentScreenPoint) - originPagePoint.setTo(currentPagePoint) break } case 'pointer_move': { // If the user is in pen mode, but the pointer is not a pen, stop here. - if (!isPen && this.getInstanceState().isPenMode) { - return - } + if (!isPen && isPenMode) return + // If we've started panning, then clear any long press timeout if (this.inputs.isPanning && this.inputs.isPointing) { - clearTimeout(this._longPressTimeout) - // Handle panning + // Handle spacebar / middle mouse button panning const { currentScreenPoint, previousScreenPoint } = this.inputs - this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint)) + const { x: cx, y: cy, z: cz } = camera + const { panSpeed } = cameraOptions + const offset = Vec.Sub(currentScreenPoint, previousScreenPoint) + this.setCamera( + new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz), + { immediate: true } + ) return } if ( - !inputs.isDragging && inputs.isPointing && + !inputs.isDragging && Vec.Dist2(originPagePoint, currentPagePoint) > - (this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / - this.getZoomLevel() + (instanceState.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) / camera.z ) { - clearTimeout(this._longPressTimeout) + // Start dragging inputs.isDragging = true + clearTimeout(this._longPressTimeout) } break } case 'pointer_up': { + // Stop dragging / pointing + inputs.isDragging = false + inputs.isPointing = false + clearTimeout(this._longPressTimeout) + // Remove the button from the buttons set inputs.buttons.delete(info.button) - inputs.isPointing = false - inputs.isDragging = false + // Suppressing pointerup here as doesn't seem to do what we what here. + if (this.getIsMenuOpen()) return - if (this.getIsMenuOpen()) { - // Suppressing pointerup here as doesn't seem to do what we what here. - return - } - - if (!isPen && this.getInstanceState().isPenMode) { - return - } + // If we're in pen mode and we're not using a pen, stop here + if (instanceState.isPenMode && !isPen) return // Firefox bug fix... // If it's the same pointer that we stored earlier... @@ -9056,50 +9073,40 @@ export class Editor extends EventEmitter { } if (inputs.isPanning) { - if (info.button === 1) { - if (!this.inputs.keys.has(' ')) { - inputs.isPanning = false + const slideDirection = this.inputs.pointerVelocity + const slideSpeed = Math.min(2, slideDirection.len()) - this.slideCamera({ - speed: Math.min(2, this.inputs.pointerVelocity.len()), - direction: this.inputs.pointerVelocity, - friction: CAMERA_SLIDE_FRICTION, - }) - this.setCursor({ type: this._prevCursor, rotation: 0 }) - } else { - this.slideCamera({ - speed: Math.min(2, this.inputs.pointerVelocity.len()), - direction: this.inputs.pointerVelocity, - friction: CAMERA_SLIDE_FRICTION, - }) - this.setCursor({ - type: 'grab', - rotation: 0, - }) + switch (info.button) { + case 0: { + this.setCursor({ type: 'grab', rotation: 0 }) + break } - } else if (info.button === 0) { + case 1: { + if (this.inputs.keys.has(' ')) { + this.setCursor({ type: 'grab', rotation: 0 }) + } else { + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } + } + } + + if (slideSpeed > 0) { this.slideCamera({ - speed: Math.min(2, this.inputs.pointerVelocity.len()), - direction: this.inputs.pointerVelocity, + speed: slideSpeed, + direction: slideDirection, friction: CAMERA_SLIDE_FRICTION, }) - this.setCursor({ - type: 'grab', - rotation: 0, - }) } } else { if (info.button === 5) { - // Eraser button activates eraser + // If we were erasing with a stylus button, restore the tool we were using before we started erasing this.complete() this.setCurrentTool(this._restoreToolId) } } - break } } - break } case 'keyboard': { @@ -9114,12 +9121,13 @@ export class Editor extends EventEmitter { inputs.keys.add(info.code) // If the space key is pressed (but meta / control isn't!) activate panning - if (!info.ctrlKey && info.code === 'Space') { + if (info.code === 'Space' && !info.ctrlKey) { if (!this.inputs.isPanning) { - this._prevCursor = this.getInstanceState().cursor.type + this._prevCursor = instanceState.cursor.type } this.inputs.isPanning = true + clearTimeout(this._longPressTimeout) this.setCursor({ type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 }) } @@ -9129,9 +9137,15 @@ export class Editor extends EventEmitter { // Remove the key from the keys set inputs.keys.delete(info.code) - if (info.code === 'Space' && !this.inputs.buttons.has(1)) { - this.inputs.isPanning = false - this.setCursor({ type: this._prevCursor, rotation: 0 }) + // If we've lifted the space key, + if (info.code === 'Space') { + if (this.inputs.buttons.has(1)) { + // If we're still middle dragging, continue panning + } else { + // otherwise, stop panning + this.inputs.isPanning = false + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } } break @@ -9153,39 +9167,19 @@ export class Editor extends EventEmitter { info.name = 'right_click' } - // If a pointer event, send the event to the click manager. - if (info.isPen === this.getInstanceState().isPenMode) { - switch (info.name) { - case 'pointer_down': { - const otherEvent = this._clickManager.transformPointerDownEvent(info) - if (info.name !== otherEvent.name) { - this.root.handleEvent(info) - this.emit('event', info) - this.root.handleEvent(otherEvent) - this.emit('event', otherEvent) - return - } - - break - } - case 'pointer_up': { - clearTimeout(this._longPressTimeout) - - const otherEvent = this._clickManager.transformPointerUpEvent(info) - if (info.name !== otherEvent.name) { - this.root.handleEvent(info) - this.emit('event', info) - this.root.handleEvent(otherEvent) - this.emit('event', otherEvent) - return - } - - break - } - case 'pointer_move': { - this._clickManager.handleMove() - break - } + // If a left click pointer event, send the event to the click manager. + const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + if (info.isPen === isPenMode) { + // The click manager may return a new event, i.e. a double click event + // depending on the event coming in and its own state. If the event has + // changed then hand both events to the statechart + const clickInfo = this._clickManager.handlePointerEvent(info) + if (info.name !== clickInfo.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(clickInfo) + this.emit('event', clickInfo) + return } } } diff --git a/packages/editor/src/lib/editor/managers/ClickManager.ts b/packages/editor/src/lib/editor/managers/ClickManager.ts index ef5e53b10..ba5a1d25b 100644 --- a/packages/editor/src/lib/editor/managers/ClickManager.ts +++ b/packages/editor/src/lib/editor/managers/ClickManager.ts @@ -95,116 +95,121 @@ export class ClickManager { lastPointerInfo = {} as TLPointerEventInfo - /** - * Start the double click timeout. - * - * @param info - The event info. - */ - transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => { - if (!this._clickState) return info + handlePointerEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => { + switch (info.name) { + case 'pointer_down': { + if (!this._clickState) return info - this._clickScreenPoint = Vec.From(info.point) + this._clickScreenPoint = Vec.From(info.point) - if ( - this._previousScreenPoint && - this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE - ) { - this._clickState = 'idle' - } + if ( + this._previousScreenPoint && + this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE + ) { + this._clickState = 'idle' + } - this._previousScreenPoint = this._clickScreenPoint + this._previousScreenPoint = this._clickScreenPoint - this.lastPointerInfo = info + this.lastPointerInfo = info - switch (this._clickState) { - case 'idle': { - this._clickState = 'pendingDouble' - this._clickTimeout = this._getClickTimeout(this._clickState) - return info // returns the pointer event - } - case 'pendingDouble': { - this._clickState = 'pendingTriple' - this._clickTimeout = this._getClickTimeout(this._clickState) - return { - ...info, - type: 'click', - name: 'double_click', - phase: 'down', + switch (this._clickState) { + case 'idle': { + this._clickState = 'pendingDouble' + this._clickTimeout = this._getClickTimeout(this._clickState) + return info // returns the pointer event + } + case 'pendingDouble': { + this._clickState = 'pendingTriple' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'double_click', + phase: 'down', + } + } + case 'pendingTriple': { + this._clickState = 'pendingQuadruple' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'triple_click', + phase: 'down', + } + } + case 'pendingQuadruple': { + this._clickState = 'pendingOverflow' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'quadruple_click', + phase: 'down', + } + } + case 'pendingOverflow': { + this._clickState = 'overflow' + this._clickTimeout = this._getClickTimeout(this._clickState) + return info + } + default: { + // overflow + this._clickTimeout = this._getClickTimeout(this._clickState) + return info + } } } - case 'pendingTriple': { - this._clickState = 'pendingQuadruple' - this._clickTimeout = this._getClickTimeout(this._clickState) - return { - ...info, - type: 'click', - name: 'triple_click', - phase: 'down', + case 'pointer_up': { + if (!this._clickState) return info + + this._clickScreenPoint = Vec.From(info.point) + + switch (this._clickState) { + case 'pendingTriple': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'double_click', + phase: 'up', + } + } + case 'pendingQuadruple': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'triple_click', + phase: 'up', + } + } + case 'pendingOverflow': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'quadruple_click', + phase: 'up', + } + } + default: { + // idle, pendingDouble, overflow + return info + } } } - case 'pendingQuadruple': { - this._clickState = 'pendingOverflow' - this._clickTimeout = this._getClickTimeout(this._clickState) - return { - ...info, - type: 'click', - name: 'quadruple_click', - phase: 'down', + case 'pointer_move': { + if ( + this._clickState !== 'idle' && + this._clickScreenPoint && + Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) > + (this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) + ) { + this.cancelDoubleClickTimeout() } - } - case 'pendingOverflow': { - this._clickState = 'overflow' - this._clickTimeout = this._getClickTimeout(this._clickState) - return info - } - default: { - // overflow - this._clickTimeout = this._getClickTimeout(this._clickState) - return info - } - } - } - - /** - * Emit click_up events on pointer up. - * - * @param info - The event info. - */ - transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => { - if (!this._clickState) return info - - this._clickScreenPoint = Vec.From(info.point) - - switch (this._clickState) { - case 'pendingTriple': { - return { - ...this.lastPointerInfo, - type: 'click', - name: 'double_click', - phase: 'up', - } - } - case 'pendingQuadruple': { - return { - ...this.lastPointerInfo, - type: 'click', - name: 'triple_click', - phase: 'up', - } - } - case 'pendingOverflow': { - return { - ...this.lastPointerInfo, - type: 'click', - name: 'quadruple_click', - phase: 'up', - } - } - default: { - // idle, pendingDouble, overflow return info } } + return info } /** @@ -216,21 +221,4 @@ export class ClickManager { this._clickTimeout = clearTimeout(this._clickTimeout) this._clickState = 'idle' } - - /** - * Handle a move event, possibly cancelling the click timeout. - * - * @internal - */ - handleMove = () => { - // Cancel a double click event if the user has started dragging. - if ( - this._clickState !== 'idle' && - this._clickScreenPoint && - Vec.Dist2(this._clickScreenPoint, this.editor.inputs.currentScreenPoint) > - (this.editor.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) - ) { - this.cancelDoubleClickTimeout() - } - } } diff --git a/packages/editor/src/lib/editor/types/misc-types.ts b/packages/editor/src/lib/editor/types/misc-types.ts index 8f30fe300..bc609e872 100644 --- a/packages/editor/src/lib/editor/types/misc-types.ts +++ b/packages/editor/src/lib/editor/types/misc-types.ts @@ -19,6 +19,7 @@ export type TLSvgOptions = { /** @public */ export type TLCameraOptions = { + wheelBehavior: 'zoom' | 'pan' | 'none' /** The speed of a scroll wheel / trackpad pan */ panSpeed: number /** The speed of a scroll wheel / trackpad zoom */ diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index 9bb3b527d..715d7ca26 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -200,12 +200,12 @@ export class TestEditor extends Editor { * _transformPointerDownSpy.mockRestore()) */ _transformPointerDownSpy = jest - .spyOn(this._clickManager, 'transformPointerDownEvent') + .spyOn(this._clickManager, 'handlePointerEvent') .mockImplementation((info) => { return info }) _transformPointerUpSpy = jest - .spyOn(this._clickManager, 'transformPointerDownEvent') + .spyOn(this._clickManager, 'handlePointerEvent') .mockImplementation((info) => { return info }) diff --git a/packages/tldraw/src/test/commands/zoomIn.test.ts b/packages/tldraw/src/test/commands/zoomIn.test.ts index 41dae0ab0..21ed190bc 100644 --- a/packages/tldraw/src/test/commands/zoomIn.test.ts +++ b/packages/tldraw/src/test/commands/zoomIn.test.ts @@ -1,4 +1,4 @@ -import { getDefaultCameraOptions } from '@tldraw/editor' +import { DEFAULT_CAMERA_OPTIONS } from '@tldraw/editor' import { TestEditor } from '../TestEditor' let editor: TestEditor @@ -8,7 +8,7 @@ beforeEach(() => { }) it('zooms by increments', () => { - const cameraOptions = getDefaultCameraOptions() + const cameraOptions = DEFAULT_CAMERA_OPTIONS // Starts at 1 expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) @@ -46,7 +46,7 @@ it('preserves the screen center when offset', () => { }) it('zooms to from B to D when B >= (C - A)/2, else zooms from B to C', () => { - const cameraOptions = getDefaultCameraOptions() + const cameraOptions = DEFAULT_CAMERA_OPTIONS editor.setCamera({ x: 0, y: 0, z: (cameraOptions.zoomSteps[2] + cameraOptions.zoomSteps[3]) / 2 }) editor.zoomIn() diff --git a/packages/tldraw/src/test/commands/zoomOut.test.ts b/packages/tldraw/src/test/commands/zoomOut.test.ts index 09bce0367..b9deefcac 100644 --- a/packages/tldraw/src/test/commands/zoomOut.test.ts +++ b/packages/tldraw/src/test/commands/zoomOut.test.ts @@ -1,4 +1,4 @@ -import { getDefaultCameraOptions } from '@tldraw/editor' +import { DEFAULT_CAMERA_OPTIONS } from '@tldraw/editor' import { TestEditor } from '../TestEditor' let editor: TestEditor @@ -8,7 +8,7 @@ beforeEach(() => { }) it('zooms out and in by increments', () => { - const cameraOptions = getDefaultCameraOptions() + const cameraOptions = DEFAULT_CAMERA_OPTIONS // Starts at 1 expect(editor.getZoomLevel()).toBe(cameraOptions.zoomSteps[3]) From 20cbb6d4bfdcfb49df1cf92494f8f17435ebb463 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 12:41:25 +0100 Subject: [PATCH 59/82] ok --- packages/editor/src/lib/editor/Editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index b0ed88e76..f232ecc8f 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8929,7 +8929,7 @@ export class Editor extends EventEmitter { // If the camera behavior is "zoom" and the ctrl key is presssed, then pan; // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom const behavior = - wheelBehavior === 'zoom' ? (inputs.ctrlKey ? 'pan' : 'zoom') : wheelBehavior + wheelBehavior === 'pan' ? (inputs.ctrlKey ? 'zoom' : 'pan') : wheelBehavior switch (behavior) { case 'zoom': { From ced1eadd8cc9318c2be9e55b9b5bd597bc3d4b1a Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:05:39 +0100 Subject: [PATCH 60/82] ok --- .../src/examples/camera-options/CameraOptionsExample.tsx | 2 +- packages/editor/src/lib/constants.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index f03470e58..0bcb96c38 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -12,8 +12,8 @@ import { import 'tldraw/tldraw.css' const CAMERA_OPTIONS: TLCameraOptions = { - wheelBehavior: 'pan', isLocked: false, + wheelBehavior: 'pan', panSpeed: 1, zoomSpeed: 1, zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index a552b3c9e..13bf3626d 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -13,11 +13,11 @@ export const ANIMATION_MEDIUM_MS = 320 /** @internal */ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = { - zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], - zoomSpeed: 1, - panSpeed: 1, isLocked: false, wheelBehavior: 'pan', + panSpeed: 1, + zoomSpeed: 1, + zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8], } export const FOLLOW_CHASE_PROPORTION = 0.5 From 8aca34fd9afe2fa1ad3982378c0c7e815bac291e Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:08:21 +0100 Subject: [PATCH 61/82] ok --- .../src/examples/camera-options/CameraOptionsExample.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index 0bcb96c38..a499fd3d5 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -235,10 +235,12 @@ const CameraOptionsControlPanel = track(() => { { const val = e.target.value.split(', ').map((v) => Number(v)) - updateOptions({ zoomSteps: val }) + if (val.every((v) => typeof v === 'number' && Number.isFinite(v))) { + updateOptions({ zoomSteps: val }) + } }} /> From 6d03c73443507b2f15c6cd6c0cd074e12e070301 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:21:44 +0100 Subject: [PATCH 62/82] Update Editor.ts --- packages/editor/src/lib/editor/Editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index f232ecc8f..28e2a68b1 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -8883,7 +8883,7 @@ export class Editor extends EventEmitter { // Stop pinching inputs.isPinching = false - // Stash and clear the + // Stash and clear the shapes that were selected when the pinch started const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this this._selectedShapeIdsAtPointerDown = [] From e209256084cbeec28f59d600c7dd39b81218b105 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:23:28 +0100 Subject: [PATCH 63/82] ok --- packages/editor/src/lib/constants.ts | 5 +++++ packages/editor/src/lib/editor/Editor.ts | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 13bf3626d..7b68334dd 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -107,3 +107,8 @@ export const LONG_PRESS_DURATION = 500 /** @internal */ export const TEXT_SHADOW_LOD = 0.35 + +export const LEFT_MOUSE_BUTTON = 0 +export const RIGHT_MOUSE_BUTTON = 2 +export const MIDDLE_MOUSE_BUTTON = 1 +export const STYLUS_ERASER_BUTTON = 5 diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 28e2a68b1..9cbf9f71d 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -80,9 +80,13 @@ import { FOLLOW_CHASE_ZOOM_UNSNAP, HIT_TEST_MARGIN, INTERNAL_POINTER_IDS, + LEFT_MOUSE_BUTTON, LONG_PRESS_DURATION, MAX_PAGES, MAX_SHAPES_PER_PAGE, + MIDDLE_MOUSE_BUTTON, + RIGHT_MOUSE_BUTTON, + STYLUS_ERASER_BUTTON, } from '../constants' import { Box, BoxLike } from '../primitives/Box' import { Mat, MatLike, MatModel } from '../primitives/Mat' @@ -8983,7 +8987,7 @@ export class Editor extends EventEmitter { // Firefox bug fix... // If it's a left-mouse-click, we store the pointer id for later user - if (info.button === 0) this.capturedPointerId = info.pointerId + if (info.button === LEFT_MOUSE_BUTTON) this.capturedPointerId = info.pointerId // Add the button from the buttons set inputs.buttons.add(info.button) @@ -8996,11 +9000,11 @@ export class Editor extends EventEmitter { if (!isPenMode && isPen) this.updateInstanceState({ isPenMode: true }) // On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser - if (info.button === 5) { + if (info.button === STYLUS_ERASER_BUTTON) { this._restoreToolId = this.getCurrentToolId() this.complete() this.setCurrentTool('eraser') - } else if (info.button === 1) { + } else if (info.button === MIDDLE_MOUSE_BUTTON) { // Middle mouse pan activates panning unless we're already panning (with spacebar) if (!this.inputs.isPanning) { this._prevCursor = this.getInstanceState().cursor.type @@ -9077,11 +9081,11 @@ export class Editor extends EventEmitter { const slideSpeed = Math.min(2, slideDirection.len()) switch (info.button) { - case 0: { + case LEFT_MOUSE_BUTTON: { this.setCursor({ type: 'grab', rotation: 0 }) break } - case 1: { + case MIDDLE_MOUSE_BUTTON: { if (this.inputs.keys.has(' ')) { this.setCursor({ type: 'grab', rotation: 0 }) } else { @@ -9098,7 +9102,7 @@ export class Editor extends EventEmitter { }) } } else { - if (info.button === 5) { + if (info.button === STYLUS_ERASER_BUTTON) { // If we were erasing with a stylus button, restore the tool we were using before we started erasing this.complete() this.setCurrentTool(this._restoreToolId) @@ -9161,9 +9165,9 @@ export class Editor extends EventEmitter { // Correct the info name for right / middle clicks if (info.type === 'pointer') { - if (info.button === 1) { + if (info.button === LEFT_MOUSE_BUTTON) { info.name = 'middle_click' - } else if (info.button === 2) { + } else if (info.button === RIGHT_MOUSE_BUTTON) { info.name = 'right_click' } From 009ecc2b5a64218419169f19a0f1cc0df03f04a8 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:33:42 +0100 Subject: [PATCH 64/82] ok --- packages/editor/src/lib/hooks/useCanvasEvents.ts | 3 ++- packages/editor/src/lib/hooks/useSelectionEvents.ts | 3 ++- packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index 26b6b05da..c3468e7c6 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -1,4 +1,5 @@ import React, { useMemo } from 'react' +import { RIGHT_MOUSE_BUTTON } from '../constants' import { preventDefault, releasePointerCapture, @@ -19,7 +20,7 @@ export function useCanvasEvents() { function onPointerDown(e: React.PointerEvent) { if ((e as any).isKilled) return - if (e.button === 2) { + if (e.button === RIGHT_MOUSE_BUTTON) { editor.dispatch({ type: 'pointer', target: 'canvas', diff --git a/packages/editor/src/lib/hooks/useSelectionEvents.ts b/packages/editor/src/lib/hooks/useSelectionEvents.ts index 993330b2e..30b604e22 100644 --- a/packages/editor/src/lib/hooks/useSelectionEvents.ts +++ b/packages/editor/src/lib/hooks/useSelectionEvents.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react' +import { RIGHT_MOUSE_BUTTON } from '../constants' import { TLSelectionHandle } from '../editor/types/selection-types' import { loopToHtmlElement, @@ -18,7 +19,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) { const onPointerDown: React.PointerEventHandler = (e) => { if ((e as any).isKilled) return - if (e.button === 2) { + if (e.button === RIGHT_MOUSE_BUTTON) { editor.dispatch({ type: 'pointer', target: 'selection', diff --git a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts index f57dbd81f..f1227c4cc 100644 --- a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts +++ b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts @@ -641,6 +641,7 @@ export function useNativeClipboardEvents() { let disablingMiddleClickPaste = false const pointerUpHandler = (e: PointerEvent) => { if (e.button === 1) { + // middle mouse button disablingMiddleClickPaste = true requestAnimationFrame(() => { disablingMiddleClickPaste = false From 338968758114c338125385281f04d9428469cf8b Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 14:39:35 +0100 Subject: [PATCH 65/82] Update actions.tsx --- packages/tldraw/src/lib/ui/context/actions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tldraw/src/lib/ui/context/actions.tsx b/packages/tldraw/src/lib/ui/context/actions.tsx index 95370fc4e..7df9b8412 100644 --- a/packages/tldraw/src/lib/ui/context/actions.tsx +++ b/packages/tldraw/src/lib/ui/context/actions.tsx @@ -20,6 +20,7 @@ import { useEditor, } from '@tldraw/editor' import * as React from 'react' +import { ADJACENT_NOTE_MARGIN } from '../../shapes/note/noteHelpers' import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers' import { getEmbedInfo } from '../../utils/embeds/embeds' import { fitFrameToContent, removeFrame } from '../../utils/frames/frames' @@ -818,7 +819,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { trackEvent('pack-shapes', { source }) editor.mark('pack') const selectedShapeIds = editor.getSelectedShapeIds() - editor.packShapes(selectedShapeIds, 16) + editor.packShapes(selectedShapeIds, ADJACENT_NOTE_MARGIN) kickoutOccludedShapes(editor, selectedShapeIds) }, }, From d1b270b4a3fcbb6789bf2b513ecef7981e7d212f Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 15:28:48 +0100 Subject: [PATCH 66/82] fix --- apps/docs/content/docs/camera.mdx | 1 + packages/editor/src/lib/editor/Editor.ts | 5 ++-- .../src/lib/editor/managers/ClickManager.ts | 23 ++++++++----------- 3 files changed, 13 insertions(+), 16 deletions(-) create mode 100644 apps/docs/content/docs/camera.mdx diff --git a/apps/docs/content/docs/camera.mdx b/apps/docs/content/docs/camera.mdx new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/docs/content/docs/camera.mdx @@ -0,0 +1 @@ + diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 9cbf9f71d..9fbe9f2b4 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -9143,7 +9143,7 @@ export class Editor extends EventEmitter { // If we've lifted the space key, if (info.code === 'Space') { - if (this.inputs.buttons.has(1)) { + if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) { // If we're still middle dragging, continue panning } else { // otherwise, stop panning @@ -9151,7 +9151,6 @@ export class Editor extends EventEmitter { this.setCursor({ type: this._prevCursor, rotation: 0 }) } } - break } case 'key_repeat': { @@ -9165,7 +9164,7 @@ export class Editor extends EventEmitter { // Correct the info name for right / middle clicks if (info.type === 'pointer') { - if (info.button === LEFT_MOUSE_BUTTON) { + if (info.button === MIDDLE_MOUSE_BUTTON) { info.name = 'middle_click' } else if (info.button === RIGHT_MOUSE_BUTTON) { info.name = 'right_click' diff --git a/packages/editor/src/lib/editor/managers/ClickManager.ts b/packages/editor/src/lib/editor/managers/ClickManager.ts index ba5a1d25b..4c7a4253b 100644 --- a/packages/editor/src/lib/editor/managers/ClickManager.ts +++ b/packages/editor/src/lib/editor/managers/ClickManager.ts @@ -99,12 +99,11 @@ export class ClickManager { switch (info.name) { case 'pointer_down': { if (!this._clickState) return info - this._clickScreenPoint = Vec.From(info.point) if ( this._previousScreenPoint && - this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE + Vec.Dist2(this._previousScreenPoint, this._clickScreenPoint) > MAX_CLICK_DISTANCE ** 2 ) { this._clickState = 'idle' } @@ -114,11 +113,6 @@ export class ClickManager { this.lastPointerInfo = info switch (this._clickState) { - case 'idle': { - this._clickState = 'pendingDouble' - this._clickTimeout = this._getClickTimeout(this._clickState) - return info // returns the pointer event - } case 'pendingDouble': { this._clickState = 'pendingTriple' this._clickTimeout = this._getClickTimeout(this._clickState) @@ -149,21 +143,23 @@ export class ClickManager { phase: 'down', } } + case 'idle': { + this._clickState = 'pendingDouble' + break + } case 'pendingOverflow': { this._clickState = 'overflow' - this._clickTimeout = this._getClickTimeout(this._clickState) - return info + break } default: { // overflow - this._clickTimeout = this._getClickTimeout(this._clickState) - return info } } + this._clickTimeout = this._getClickTimeout(this._clickState) + return info } case 'pointer_up': { if (!this._clickState) return info - this._clickScreenPoint = Vec.From(info.point) switch (this._clickState) { @@ -193,9 +189,10 @@ export class ClickManager { } default: { // idle, pendingDouble, overflow - return info } } + + return info } case 'pointer_move': { if ( From 4a5271ac3d45ac3ed8aaa6dab8738da1b23111a0 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 15:35:37 +0100 Subject: [PATCH 67/82] Update camera.mdx --- apps/docs/content/docs/camera.mdx | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/docs/content/docs/camera.mdx b/apps/docs/content/docs/camera.mdx index 8b1378917..a6eb09e19 100644 --- a/apps/docs/content/docs/camera.mdx +++ b/apps/docs/content/docs/camera.mdx @@ -1 +1,34 @@ +--- +title: Camera +status: published +author: steveruizok +date: 4/18/2023 +--- +This article covers the space and coordinate systems used in the tldraw editor. + +# Viewport + +The viewport is the rectangular area of the editor. + +The camera determines which part of the current page is displayed inside of the viewport. + +The [Editor#viewportScreenBounds](?) is a [Box](?) that describes the size and position of the component's canvas in actual screen pixels. + +The [Editor.viewportPageBounds](?) is a [Box](?) that describes the size and position of the part of the current page that is displayed in the viewport. + +## Screen vs. page coordinates + +In tldraw, coordinates are either be in page or screen space. + +A "screen point" refers to the point's distance from the top left corner of the component. + +A "page point" refers to the point's distance from the "zero point" of the canvas. + +When the camera is at `{x: 0, y: 0, z: 0}`, the screen point and page point will be identical. As the camera moves, however, the viewport will display a different part of the page; and so a screen point will correspond to a different page point. + +You can convert between coordinate spaces using [Editor#screenToPage](?) and [Editor#pageToScreen](?). + +## Camera options + +You can use the editor's camera options to configure the behavior of the editor's camera. From cefe694798728709d1341040d7ae7ce0753b20b7 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 16:28:27 +0100 Subject: [PATCH 68/82] Update camera.mdx --- apps/docs/content/docs/camera.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/docs/camera.mdx b/apps/docs/content/docs/camera.mdx index a6eb09e19..281988250 100644 --- a/apps/docs/content/docs/camera.mdx +++ b/apps/docs/content/docs/camera.mdx @@ -1,6 +1,6 @@ --- title: Camera -status: published +status: draft author: steveruizok date: 4/18/2023 --- From caf9ec4ee1838254c968b749d3018eb069d79045 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Thu, 18 Apr 2024 17:18:39 +0100 Subject: [PATCH 69/82] rename initial to reset --- .../src/examples/camera-options/CameraOptionsExample.tsx | 2 +- packages/editor/api-report.md | 6 +++--- packages/editor/api/api.json | 6 +++--- packages/editor/src/lib/editor/Editor.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx index a499fd3d5..9ff5a2d21 100644 --- a/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx +++ b/apps/examples/src/examples/camera-options/CameraOptionsExample.tsx @@ -415,7 +415,7 @@ const CameraOptionsControlPanel = track(() => {