[Snapping 4/5] Add handle-point snapping (#2841)
Currently, when dragging line handles they'll snap to the outlines of other shapes, but not to their vertices. This can make it hard to snap precisely to certain key places, like the handles of other lines, or the corners of `geo` shapes. This diff adds a new snap type for handles - snapping to points: ![Kapture 2024-02-14 at 16 30 41](https://github.com/tldraw/tldraw/assets/1489520/046109d3-2961-463f-bf71-9350ea1204bc) This adds to the new snapping API so the snapping points can very easily be customised on a shape-by-shape basis. Closes TLD-2198 This PR is part of a series - please don't merge it until the things before it have landed! 1. #2827 2. #2831 3. #2793 4. #2841 (you are here) 5. #2845 ### Change Type - [x] `minor` — New feature ### Test Plan 1. create a line shape 2. drag its handles whilst holding command 3. it should snap to the outlines of other shapes, vertices of other line shapes, and the bounding box corners/center of most 'boxy' shapes (geo, embed, etc) - [x] Unit Tests ### Release Notes - Line handles
This commit is contained in:
parent
77865d9f5e
commit
89881397b5
9 changed files with 359 additions and 46 deletions
|
@ -156,6 +156,8 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
// (undocumented)
|
||||
getGeometry(shape: Shape): Geometry2d;
|
||||
// (undocumented)
|
||||
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<any>;
|
||||
}
|
||||
|
||||
|
@ -1141,6 +1143,7 @@ export const HALF_PI: number;
|
|||
// @public
|
||||
export interface HandleSnapGeometry {
|
||||
outline?: Geometry2d | null;
|
||||
points?: VecModel[];
|
||||
}
|
||||
|
||||
// @public
|
||||
|
|
|
@ -1275,6 +1275,55 @@
|
|||
"isAbstract": false,
|
||||
"name": "getGeometry"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!BaseBoxShapeUtil#getHandleSnapGeometry:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getHandleSnapGeometry(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "Shape"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "HandleSnapGeometry",
|
||||
"canonicalReference": "@tldraw/editor!HandleSnapGeometry:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getHandleSnapGeometry"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!BaseBoxShapeUtil#onResize:member",
|
||||
|
@ -22959,7 +23008,7 @@
|
|||
{
|
||||
"kind": "Interface",
|
||||
"canonicalReference": "@tldraw/editor!HandleSnapGeometry:interface",
|
||||
"docComment": "/**\n * When dragging a handle, users can snap the handle to key geometry on other nearby shapes. Customize how handles snap to a shape by returning this from {@link ShapeUtil.getHandleSnapGeometry}.\n *\n * @public\n */\n",
|
||||
"docComment": "/**\n * When dragging a handle, users can snap the handle to key geometry on other nearby shapes. Customize how handles snap to a shape by returning this from {@link ShapeUtil.getHandleSnapGeometry}.\n *\n * Any co-ordinates here should be in the shape's local space.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -23002,6 +23051,38 @@
|
|||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/editor!HandleSnapGeometry#points:member",
|
||||
"docComment": "/**\n * Key points on the shape that the handle will snap to. For example, the corners of a rectangle, or the centroid of a triangle. By default, no points are used.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "points?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecModel",
|
||||
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "points",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"extendsTokenRanges": []
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { computed } from '@tldraw/state'
|
||||
import { TLShape } from '@tldraw/tlschema'
|
||||
import { TLShape, VecModel } from '@tldraw/tlschema'
|
||||
import { assertExists } from '@tldraw/utils'
|
||||
import { Vec } from '../../../primitives/Vec'
|
||||
import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
|
||||
|
@ -12,6 +12,8 @@ import { SnapData, SnapManager } from './SnapManager'
|
|||
* Customize how handles snap to a shape by returning this from
|
||||
* {@link ShapeUtil.getHandleSnapGeometry}.
|
||||
*
|
||||
* Any co-ordinates here should be in the shape's local space.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface HandleSnapGeometry {
|
||||
|
@ -21,6 +23,11 @@ export interface HandleSnapGeometry {
|
|||
* Set this to `null` to disable handle snapping to this shape's outline.
|
||||
*/
|
||||
outline?: Geometry2d | null
|
||||
/**
|
||||
* Key points on the shape that the handle will snap to. For example, the corners of a
|
||||
* rectangle, or the centroid of a triangle. By default, no points are used.
|
||||
*/
|
||||
points?: VecModel[]
|
||||
}
|
||||
|
||||
export class HandleSnaps {
|
||||
|
@ -39,61 +46,104 @@ export class HandleSnaps {
|
|||
snapGeometry.outline === undefined
|
||||
? editor.getShapeGeometry(shape)
|
||||
: snapGeometry.outline,
|
||||
|
||||
points: snapGeometry.points,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
snapHandle({
|
||||
private getHandleSnapPosition({
|
||||
handlePoint,
|
||||
additionalSegments,
|
||||
}: {
|
||||
handlePoint: Vec
|
||||
additionalSegments: Vec[][]
|
||||
}): SnapData | null {
|
||||
}): Vec | null {
|
||||
const snapThreshold = this.manager.getSnapThreshold()
|
||||
|
||||
// Find the nearest point that is within the snap threshold
|
||||
let minDistance = snapThreshold
|
||||
let nearestPoint: Vec | null = null
|
||||
// We snap to two different parts of the shape's handle snap geometry:
|
||||
// 1. The `points`. These are handles or other key points that we want to snap to with a
|
||||
// higher priority than the normal outline snapping.
|
||||
// 2. The `outline`. This describes the outline of the shape, and we just snap to the
|
||||
// nearest point on that outline.
|
||||
|
||||
// Start with the points:
|
||||
let minDistanceForSnapPoint = snapThreshold
|
||||
let nearestSnapPoint: Vec | null = null
|
||||
for (const shapeId of this.manager.getSnappableShapes()) {
|
||||
const handleSnapOutline = this.getSnapGeometryCache().get(shapeId)?.outline
|
||||
if (!handleSnapOutline) continue
|
||||
const snapPoints = this.getSnapGeometryCache().get(shapeId)?.points
|
||||
if (!snapPoints) continue
|
||||
|
||||
const shapePageTransform = assertExists(this.editor.getShapePageTransform(shapeId))
|
||||
const pointInShapeSpace = this.editor.getPointInShapeSpace(shapeId, handlePoint)
|
||||
const nearestShapePointInShapeSpace = handleSnapOutline.nearestPoint(pointInShapeSpace)
|
||||
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
||||
const distance = Vec.Dist(handlePoint, nearestInPageSpace)
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
nearestPoint = nearestInPageSpace
|
||||
for (const snapPointInShapeSpace of snapPoints) {
|
||||
const snapPointInPageSpace = shapePageTransform.applyToPoint(snapPointInShapeSpace)
|
||||
const distance = Vec.Dist(handlePoint, snapPointInPageSpace)
|
||||
|
||||
if (distance < minDistanceForSnapPoint) {
|
||||
minDistanceForSnapPoint = distance
|
||||
nearestSnapPoint = snapPointInPageSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle additional segments:
|
||||
// if we found a snap point, return it - we don't need to check the outlines because points
|
||||
// have a higher priority
|
||||
if (nearestSnapPoint) return nearestSnapPoint
|
||||
|
||||
let minDistanceForOutline = snapThreshold
|
||||
let nearestPointOnOutline: Vec | null = null
|
||||
|
||||
for (const shapeId of this.manager.getSnappableShapes()) {
|
||||
const snapOutline = this.getSnapGeometryCache().get(shapeId)?.outline
|
||||
if (!snapOutline) continue
|
||||
|
||||
const shapePageTransform = assertExists(this.editor.getShapePageTransform(shapeId))
|
||||
const pointInShapeSpace = this.editor.getPointInShapeSpace(shapeId, handlePoint)
|
||||
|
||||
const nearestShapePointInShapeSpace = snapOutline.nearestPoint(pointInShapeSpace)
|
||||
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
||||
const distance = Vec.Dist(handlePoint, nearestInPageSpace)
|
||||
|
||||
if (distance < minDistanceForOutline) {
|
||||
minDistanceForOutline = distance
|
||||
nearestPointOnOutline = nearestInPageSpace
|
||||
}
|
||||
}
|
||||
|
||||
// We also allow passing "additionSegments" for self-snapping.
|
||||
// TODO(alex): replace this with a proper self-snapping solution
|
||||
for (const segment of additionalSegments) {
|
||||
const nearestOnSegment = Vec.NearestPointOnLineSegment(segment[0], segment[1], handlePoint)
|
||||
const distance = Vec.Dist(handlePoint, nearestOnSegment)
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
nearestPoint = nearestOnSegment
|
||||
if (distance < minDistanceForOutline) {
|
||||
minDistanceForOutline = distance
|
||||
nearestPointOnOutline = nearestOnSegment
|
||||
}
|
||||
}
|
||||
|
||||
// if we found a point on the outline, return it
|
||||
if (nearestPointOnOutline) return nearestPointOnOutline
|
||||
|
||||
// if not, there's no nearby snap point
|
||||
return null
|
||||
}
|
||||
|
||||
snapHandle(opts: { handlePoint: Vec; additionalSegments: Vec[][] }): SnapData | null {
|
||||
const snapPosition = this.getHandleSnapPosition(opts)
|
||||
|
||||
// If we found a point, display snap lines, and return the nudge
|
||||
if (nearestPoint) {
|
||||
if (snapPosition) {
|
||||
this.manager.setIndicators([
|
||||
{
|
||||
id: uniqueId(),
|
||||
type: 'points',
|
||||
points: [nearestPoint],
|
||||
points: [snapPosition],
|
||||
},
|
||||
])
|
||||
|
||||
return { nudge: Vec.Sub(nearestPoint, handlePoint) }
|
||||
return { nudge: Vec.Sub(snapPosition, opts.handlePoint) }
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { TLBaseShape } from '@tldraw/tlschema'
|
||||
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
||||
import { Rectangle2d } from '../../primitives/geometry/Rectangle2d'
|
||||
import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
|
||||
import { ShapeUtil, TLOnResizeHandler } from './ShapeUtil'
|
||||
import { resizeBox } from './shared/resizeBox'
|
||||
|
||||
|
@ -20,4 +21,10 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
|||
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||
return resizeBox(shape, info)
|
||||
}
|
||||
|
||||
override getHandleSnapGeometry(shape: Shape): HandleSnapGeometry {
|
||||
return {
|
||||
points: this.getGeometry(shape).bounds.cornersAndCenter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -910,6 +910,10 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
// (undocumented)
|
||||
getHandles(shape: TLLineShape): TLHandle[];
|
||||
// (undocumented)
|
||||
getHandleSnapGeometry(shape: TLLineShape): {
|
||||
points: VecModel[];
|
||||
};
|
||||
// (undocumented)
|
||||
getOutlineSegments(shape: TLLineShape): Vec[][];
|
||||
// (undocumented)
|
||||
hideResizeHandles: () => boolean;
|
||||
|
|
|
@ -10964,6 +10964,64 @@
|
|||
"isAbstract": false,
|
||||
"name": "getHandles"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/tldraw!LineShapeUtil#getHandleSnapGeometry:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getHandleSnapGeometry(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLLineShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLLineShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n points: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecModel",
|
||||
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[];\n }"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getHandleSnapGeometry"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/tldraw!LineShapeUtil#getOutlineSegments:member(1)",
|
||||
|
|
|
@ -214,6 +214,28 @@ describe('Snapping', () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('snaps to vertices on other line shapes', () => {
|
||||
editor.createShapesFromJsx([
|
||||
<TL.line
|
||||
x={150}
|
||||
y={150}
|
||||
handles={{ ['a1' as IndexKey]: { x: 200, y: 0 }, ['a2' as IndexKey]: { x: 300, y: 0 } }}
|
||||
/>,
|
||||
])
|
||||
|
||||
editor.select(id)
|
||||
|
||||
editor
|
||||
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
|
||||
.pointerMove(205, 1, undefined, { ctrlKey: true })
|
||||
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: { handles: { a1: { x: 200, y: 0 } } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Misc', () => {
|
||||
|
|
|
@ -380,6 +380,12 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override getHandleSnapGeometry(shape: TLLineShape) {
|
||||
return {
|
||||
points: Object.values(shape.props.handles),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -162,12 +162,17 @@ describe('custom shape bounds snapping - translate', () => {
|
|||
describe('custom handle snapping', () => {
|
||||
type TestShape = TLBaseShape<
|
||||
'test',
|
||||
{ w: number; h: number; handleGeomVertices: VecModel[] | 'default' | null }
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
handleOutline: VecModel[] | 'default' | null
|
||||
handlePoints: 'default' | VecModel[]
|
||||
}
|
||||
>
|
||||
class TestShapeUtil extends BaseBoxShapeUtil<TestShape> {
|
||||
static override type = 'test'
|
||||
override getDefaultProps(): TestShape['props'] {
|
||||
return { w: 100, h: 100, handleGeomVertices: 'default' }
|
||||
return { w: 100, h: 100, handleOutline: 'default', handlePoints: 'default' }
|
||||
}
|
||||
override component() {
|
||||
throw new Error('Method not implemented.')
|
||||
|
@ -176,14 +181,15 @@ describe('custom handle snapping', () => {
|
|||
throw new Error('Method not implemented.')
|
||||
}
|
||||
override getHandleSnapGeometry(shape: TestShape) {
|
||||
const vertices = shape.props.handleGeomVertices
|
||||
const { handleOutline, handlePoints } = shape.props
|
||||
return {
|
||||
outline:
|
||||
vertices === 'default'
|
||||
handleOutline === 'default'
|
||||
? undefined
|
||||
: vertices === null
|
||||
: handleOutline === null
|
||||
? null
|
||||
: new Polyline2d({ points: vertices.map(Vec.From) }),
|
||||
: new Polyline2d({ points: handleOutline.map(Vec.From) }),
|
||||
points: handlePoints === 'default' ? undefined : handlePoints,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,13 +226,12 @@ describe('custom handle snapping', () => {
|
|||
return { x: handle.x, y: handle.y }
|
||||
}
|
||||
|
||||
describe('with default handleGeomVertices', () => {
|
||||
describe('with default handleSnapGeometry.outline', () => {
|
||||
test('snaps handles to the box of the shape', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(215, 205, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition().x).toBe(215)
|
||||
expect(handlePosition().y).toBe(200)
|
||||
expect(handlePosition()).toMatchObject({ x: 215, y: 200 })
|
||||
})
|
||||
|
||||
test("doesn't particularly snap to vertices", () => {
|
||||
|
@ -234,25 +239,23 @@ describe('custom handle snapping', () => {
|
|||
editor.pointerMove(204, 205, undefined, { ctrlKey: true })
|
||||
// only snapped to the nearest edge, not the vertex
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition().x).toBe(200)
|
||||
expect(handlePosition().y).toBe(205)
|
||||
expect(handlePosition()).toMatchObject({ x: 200, y: 205 })
|
||||
})
|
||||
|
||||
test("doesn't snap to the center", () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(251, 251, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
expect(handlePosition().x).toBe(251)
|
||||
expect(handlePosition().y).toBe(251)
|
||||
expect(handlePosition()).toMatchObject({ x: 251, y: 251 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with empty handleGeomVertices', () => {
|
||||
describe('with empty handleSnapGeometry.outline', () => {
|
||||
beforeEach(() => {
|
||||
editor.updateShape<TestShape>({
|
||||
id: ids.test,
|
||||
type: 'test',
|
||||
props: { handleGeomVertices: null },
|
||||
props: { handleOutline: null },
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -260,19 +263,18 @@ describe('custom handle snapping', () => {
|
|||
startDraggingHandle()
|
||||
editor.pointerMove(215, 205, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
expect(handlePosition().x).toBe(215)
|
||||
expect(handlePosition().y).toBe(205)
|
||||
expect(handlePosition()).toMatchObject({ x: 215, y: 205 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with custom handleGeomVertices', () => {
|
||||
describe('with custom handleSnapGeometry.outline', () => {
|
||||
beforeEach(() => {
|
||||
editor.updateShape<TestShape>({
|
||||
id: ids.test,
|
||||
type: 'test',
|
||||
props: {
|
||||
// a diagonal line from the top left to the bottom right
|
||||
handleGeomVertices: [
|
||||
handleOutline: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
],
|
||||
|
@ -284,16 +286,96 @@ describe('custom handle snapping', () => {
|
|||
startDraggingHandle()
|
||||
editor.pointerMove(235, 205, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
expect(handlePosition().x).toBe(235)
|
||||
expect(handlePosition().y).toBe(205)
|
||||
expect(handlePosition()).toMatchObject({ x: 235, y: 205 })
|
||||
})
|
||||
|
||||
test('snaps to the custom geometry', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(210, 214, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition().x).toBe(212)
|
||||
expect(handlePosition().y).toBe(212)
|
||||
expect(handlePosition()).toMatchObject({ x: 212, y: 212 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with default handleSnapGeometry.points', () => {
|
||||
test('doesnt snap to the center', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(251, 251, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
expect(handlePosition()).toMatchObject({ x: 251, y: 251 })
|
||||
})
|
||||
|
||||
test('doesnt snap to corners', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(203, 202, undefined, { ctrlKey: true })
|
||||
// snaps to edge, not corner:
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 203, y: 200 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with custom handleSnapGeometry.points', () => {
|
||||
beforeEach(() => {
|
||||
editor.updateShape<TestShape>({
|
||||
id: ids.test,
|
||||
type: 'test',
|
||||
props: {
|
||||
handlePoints: [
|
||||
{ x: 30, y: 30 },
|
||||
{ x: 70, y: 50 },
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('snaps to the custom points', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(235, 235, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 230, y: 230 })
|
||||
|
||||
editor.snaps.clearIndicators()
|
||||
editor.pointerMove(265, 255, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 270, y: 250 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with custom handleSnapGeometry.points along the outline', () => {
|
||||
beforeEach(() => {
|
||||
editor.updateShape<TestShape>({
|
||||
id: ids.test,
|
||||
type: 'test',
|
||||
props: {
|
||||
handlePoints: editor
|
||||
.getShapeGeometry(ids.test)
|
||||
.bounds.cornersAndCenter.map(({ x, y }) => ({ x, y })),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('snaps to points over outline', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(203, 202, undefined, { ctrlKey: true })
|
||||
// snaps to corner, not edge:
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 200, y: 200 })
|
||||
})
|
||||
|
||||
test('can still snap to non-outline points', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(255, 255, undefined, { ctrlKey: true })
|
||||
// snaps to the center:
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 250, y: 250 })
|
||||
})
|
||||
|
||||
test('can still snap to non-point outlines', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(235, 205, undefined, { ctrlKey: true })
|
||||
// snaps to the edge:
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(handlePosition()).toMatchObject({ x: 235, y: 200 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue