diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 99c6779e9..d94e710a5 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -755,6 +755,7 @@ export class Editor extends EventEmitter { hitFrameInside?: boolean | undefined; hitInside?: boolean | undefined; hitLabels?: boolean | undefined; + hitLocked?: boolean | undefined; margin?: number | undefined; renderingOnly?: boolean | undefined; }): TLShape | undefined; diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 88e44208f..ba238136d 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -4082,6 +4082,7 @@ export class Editor extends EventEmitter { renderingOnly?: boolean margin?: number hitInside?: boolean + hitLocked?: boolean // TODO: we probably need to rename this, we don't quite _always_ // respect this esp. in the part below that does "Check labels first" hitLabels?: boolean @@ -4094,6 +4095,7 @@ export class Editor extends EventEmitter { const { filter, margin = 0, + hitLocked = false, hitLabels = false, hitInside = false, hitFrameInside = false, @@ -4110,12 +4112,13 @@ export class Editor extends EventEmitter { ? this.getCurrentPageRenderingShapesSorted() : this.getCurrentPageShapesSorted() ).filter((shape) => { - if (this.isShapeOfType(shape, 'group')) return false + if ((shape.isLocked && !hitLocked) || this.isShapeOfType(shape, 'group')) return false const pageMask = this.getShapeMask(shape) if (pageMask && !pointInPolygon(point, pageMask)) return false if (filter) return filter(shape) return true }) + for (let i = shapesToCheck.length - 1; i >= 0; i--) { const shape = shapesToCheck[i] const geometry = this.getShapeGeometry(shape) diff --git a/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts b/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts index b4d716944..c66ebc3a7 100644 --- a/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts +++ b/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts @@ -1,5 +1,5 @@ import { StateNode, TLEventHandlers } from '@tldraw/editor' -import { updateHoveredId } from '../../../tools/selection-logic/updateHoveredId' +import { updateHoveredShapeId } from '../../../tools/selection-logic/updateHoveredShapeId' export class Idle extends StateNode { static override id = 'idle' @@ -8,7 +8,7 @@ export class Idle extends StateNode { switch (info.target) { case 'shape': case 'canvas': { - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) } } } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts index a1d799c6a..1693cfebc 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts @@ -1,7 +1,7 @@ import { StateNode, TLEventHandlers, TLFrameShape, TLShape, TLTextShape } from '@tldraw/editor' import { getTextLabels } from '../../../utils/shapes/shapes' import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShapeOnCanvasPointerDown' -import { updateHoveredId } from '../../selection-logic/updateHoveredId' +import { updateHoveredShapeId } from '../../selection-logic/updateHoveredShapeId' export class EditingShape extends StateNode { static override id = 'editing_shape' @@ -12,7 +12,7 @@ export class EditingShape extends StateNode { const editingShape = this.editor.getEditingShape() if (!editingShape) throw Error('Entered editing state without an editing shape') this.hitShapeForPointerUp = null - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) this.editor.select(editingShape) } @@ -44,7 +44,7 @@ export class EditingShape extends StateNode { switch (info.target) { case 'shape': case 'canvas': { - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) return } } @@ -145,7 +145,7 @@ export class EditingShape extends StateNode { this.editor.select(hitShape.id) this.editor.setEditingShape(hitShape.id) - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) } override onComplete: TLEventHandlers['onComplete'] = (info) => { diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts index 3c9952e03..489557156 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts @@ -19,7 +19,7 @@ import { import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShapeOnCanvasPointerDown' import { getShouldEnterCropMode } from '../../selection-logic/getShouldEnterCropModeOnPointerDown' import { selectOnCanvasPointerUp } from '../../selection-logic/selectOnCanvasPointerUp' -import { updateHoveredId } from '../../selection-logic/updateHoveredId' +import { updateHoveredShapeId } from '../../selection-logic/updateHoveredShapeId' import { kickoutOccludedShapes, startEditingShapeWithLabel } from '../selectHelpers' const SKIPPED_KEYS_FOR_AUTO_EDITING = [ @@ -38,12 +38,12 @@ export class Idle extends StateNode { override onEnter = () => { this.parent.setCurrentToolIdMask(undefined) - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) this.editor.setCursor({ type: 'default', rotation: 0 }) } override onPointerMove: TLEventHandlers['onPointerMove'] = () => { - updateHoveredId(this.editor) + updateHoveredShapeId(this.editor) } override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => { @@ -356,6 +356,7 @@ export class Idle extends StateNode { margin: HIT_TEST_MARGIN / this.editor.getZoomLevel(), hitInside: false, hitLabels: true, + hitLocked: true, hitFrameInside: false, renderingOnly: true, }) diff --git a/packages/tldraw/src/lib/tools/selection-logic/updateHoveredId.ts b/packages/tldraw/src/lib/tools/selection-logic/updateHoveredShapeId.ts similarity index 83% rename from packages/tldraw/src/lib/tools/selection-logic/updateHoveredId.ts rename to packages/tldraw/src/lib/tools/selection-logic/updateHoveredShapeId.ts index f03d333c3..665ae6ff2 100644 --- a/packages/tldraw/src/lib/tools/selection-logic/updateHoveredId.ts +++ b/packages/tldraw/src/lib/tools/selection-logic/updateHoveredShapeId.ts @@ -1,6 +1,6 @@ import { Editor, HIT_TEST_MARGIN, TLShape, throttle } from '@tldraw/editor' -function _updateHoveredId(editor: Editor) { +function _updateHoveredShapeId(editor: Editor) { // todo: consider replacing `get hoveredShapeId` with this; it would mean keeping hoveredShapeId in memory rather than in the store and possibly re-computing it more often than necessary const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint, { hitInside: false, @@ -31,5 +31,6 @@ function _updateHoveredId(editor: Editor) { return editor.setHoveredShape(shapeToHover.id) } -export const updateHoveredId = - process.env.NODE_ENV === 'test' ? _updateHoveredId : throttle(_updateHoveredId, 32) +/** @internal */ +export const updateHoveredShapeId = + process.env.NODE_ENV === 'test' ? _updateHoveredShapeId : throttle(_updateHoveredShapeId, 32) diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index 9bb3b527d..220833cb7 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -396,6 +396,25 @@ export class TestEditor extends Editor { return this } + rightClick = ( + x = this.inputs.currentScreenPoint.x, + y = this.inputs.currentScreenPoint.y, + options?: PointerEventInit, + modifiers?: EventModifiers + ) => { + this.dispatch({ + ...this.getPointerEventInfo(x, y, options, modifiers), + name: 'pointer_down', + button: 2, + }).forceTick() + this.dispatch({ + ...this.getPointerEventInfo(x, y, options, modifiers), + name: 'pointer_up', + button: 2, + }).forceTick() + return this + } + doubleClick = ( x = this.inputs.currentScreenPoint.x, y = this.inputs.currentScreenPoint.y, diff --git a/packages/tldraw/src/test/selection-omnibus.test.ts b/packages/tldraw/src/test/selection-omnibus.test.ts index e02acabf0..e4790b547 100644 --- a/packages/tldraw/src/test/selection-omnibus.test.ts +++ b/packages/tldraw/src/test/selection-omnibus.test.ts @@ -1922,3 +1922,32 @@ describe('When a shape is locked', () => { expect(editor.getSelectedShapeIds()).toEqual([ids.box2, ids.box3]) }) }) + +it('Ignores locked shapes when hovering', () => { + editor.createShape({ x: 100, y: 100, type: 'geo', props: { fill: 'solid' } }) + const a = editor.getLastCreatedShape() + editor.createShape({ x: 100, y: 100, type: 'geo', props: { fill: 'solid' } }) + const b = editor.getLastCreatedShape() + expect(a).not.toBe(b) + + // lock b + editor.toggleLock([b]) + + // Hover both shapes + editor.pointerMove(100, 100) + + // Even though b is in front of A, A should be the hovered shape + expect(editor.getHoveredShapeId()).toBe(a.id) + // right click should select the hovered shape + editor.rightClick() + expect(editor.getSelectedShapeIds()).toEqual([a.id]) + + // Delete A + editor.cancel() + editor.deleteShape(a) + // now that A is gone, we should have no hovered shape + expect(editor.getHoveredShapeId()).toBe(null) + // Now that A is gone, right click should be b + editor.rightClick() + expect(editor.getSelectedShapeIds()).toEqual([b.id]) +})