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:
parent
8c0e3c7f93
commit
0d0d38361d
8 changed files with 67 additions and 13 deletions
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
|
@ -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])
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue