diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts index 33d78df5b..8604744bf 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts @@ -352,21 +352,42 @@ function getTranslatingSnapshot(editor: Editor) { let noteAdjacentPositions: NotePit[] | undefined let noteSnapshot: MovingShapeSnapshot | undefined - const underCursor = editor.getHoveredShape() - if (underCursor) { - if (editor.isShapeOfType(underCursor, 'note')) { - const snapshot = shapeSnapshots.find((s) => s.shape.id === underCursor.id) - if (snapshot) { - noteSnapshot = snapshot - noteAdjacentPositions = getAvailableNoteAdjacentPositions( - editor, - snapshot.pageRotation, - underCursor.props.growY ?? 0 - ) - } + const { originPagePoint } = editor.inputs + + if (shapeSnapshots.length === 1) { + noteSnapshot = shapeSnapshots[0] + } else { + const allHoveredNotes = shapeSnapshots.filter( + (s) => + editor.isShapeOfType(s.shape, 'note') && + editor.isPointInShape(s.shape, originPagePoint) + ) + + if (allHoveredNotes.length === 0) { + // noop + } else if (allHoveredNotes.length === 1) { + // just one, easy + noteSnapshot = allHoveredNotes[0] + } else { + // More than one under the cursor, so we need to find the highest shape in z-order + const allShapesSorted = editor.getCurrentPageShapesSorted() + noteSnapshot = allHoveredNotes + .map((s) => ({ + snapshot: s, + index: allShapesSorted.findIndex((shape) => shape.id === s.shape.id), + })) + .sort((a, b) => b.index - a.index)[0]?.snapshot // highest up first } } + if (noteSnapshot) { + noteAdjacentPositions = getAvailableNoteAdjacentPositions( + editor, + noteSnapshot.pageRotation, + (noteSnapshot.shape as TLNoteShape).props.growY ?? 0 + ) + } + return { averagePagePoint: Vec.Average(pagePoints), movingShapes, diff --git a/packages/tldraw/src/test/translating.test.ts b/packages/tldraw/src/test/translating.test.ts index d01762e0b..31b4c7057 100644 --- a/packages/tldraw/src/test/translating.test.ts +++ b/packages/tldraw/src/test/translating.test.ts @@ -2086,4 +2086,42 @@ describe('Note shape grid helper positions / pits', () => { editor.expectShapeToMatch({ ...shapeB, x: 216, y: -4 }) expect(editor.getSelectionPageBounds()).toMatchObject({ x: 216, y: -4, w: 400, h: 200 }) }) + + it('When multiple notes are under the cursor, uses the top-most one', () => { + editor.createShape({ type: 'note' }) + editor.createShape({ type: 'note', x: 500, y: 500 }) + editor.createShape({ type: 'note', x: 501, y: 501 }) + const [shapeB, shapeC] = editor.getLastCreatedShapes(2) + + const pit = { x: 320, y: 100 } // right of shapeA + + editor.select(shapeB, shapeC) + + expect(editor.getSelectionPageBounds()).toMatchObject({ x: 500, y: 500, w: 201, h: 201 }) + + // First we do it with C in front + editor.bringToFront([shapeC]) + editor + .pointerMove(600, 600) // center of b but overlapping C + .pointerDown() + .pointerMove(pit.x - 4, pit.y - 4) // not exactly in the pit... + + // B snaps the selection to the pit + editor.expectShapeToMatch({ id: shapeB.id, x: 219, y: -1 }) // not snapped + editor.expectShapeToMatch({ id: shapeC.id, x: 220, y: 0 }) // snapped + + editor.cancel() + + // Now let's do it with B in front + editor.bringToFront([shapeB]) + + editor + .pointerMove(600, 600) // center of b but overlapping C + .pointerDown() + .pointerMove(pit.x - 4, pit.y - 4) // not exactly in the pit... + + // B snaps the selection to the pit + editor.expectShapeToMatch({ id: shapeB.id, x: 220, y: 0 }) // snapped + editor.expectShapeToMatch({ id: shapeC.id, x: 221, y: 1 }) // not snapped + }) })