Don't hover locked shapes (#3575)

This PR:
- updates `getHoveredId` to `getHoveredShapeId`
- adds an option to ignore locked shapes to `Editor.getShapeAtPoint`.

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `improvement` — Improving existing features

### Test Plan

1. Put two shapes on top of eachother
2. Lock the top shape
3. Hover the shape
4. The bottom shape should be hovered
5. Right click
6. The top shape should be selected 

- [x] Unit tests

### Release Notes

- Fixed a bug with locked shapes being hoverable.
This commit is contained in:
Steve Ruiz 2024-04-27 18:30:24 +01:00 committed by GitHub
parent 8c0e3c7f93
commit 0d0d38361d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 13 deletions

View file

@ -755,6 +755,7 @@ export class Editor extends EventEmitter<TLEventMap> {
hitFrameInside?: boolean | undefined;
hitInside?: boolean | undefined;
hitLabels?: boolean | undefined;
hitLocked?: boolean | undefined;
margin?: number | undefined;
renderingOnly?: boolean | undefined;
}): TLShape | undefined;

View file

@ -4082,6 +4082,7 @@ export class Editor extends EventEmitter<TLEventMap> {
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<TLEventMap> {
const {
filter,
margin = 0,
hitLocked = false,
hitLabels = false,
hitInside = false,
hitFrameInside = false,
@ -4110,12 +4112,13 @@ export class Editor extends EventEmitter<TLEventMap> {
? 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)

View file

@ -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)
}
}
}

View file

@ -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) => {

View file

@ -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,
})

View file

@ -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)

View file

@ -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,

View file

@ -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])
})