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.
pull/3628/merge
Steve Ruiz 2024-04-27 18:30:24 +01:00 zatwierdzone przez GitHub
rodzic 8c0e3c7f93
commit 0d0d38361d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 67 dodań i 13 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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