[Snapping 3/5] Custom snapping API (#2793)
This diff adds an API for customising our existing snap types. These are: 1. Bound snapping. When translating or resizing a shape, it'll snap to certain key points on the bounds of particular shapes. Previously, these were hard-coded to the corners and center of the bounding box of the shape. Now, a shape can bring its own (e.g. a triangle may add snapping for its 3 corners, and it's centroid rather than bounding box center. 2. Handle outline snapping. When dragging a handle, it'll snap to the outline of other shapes geometry. Now, shapes can return different geometry for this sort of snapping if they like. Each of these is customised through a method on `ShapeUtil`: `getBoundsSnapGeometry` and `getHandleSnapGeometry`. These return interfaces describing the different geometry that can be snapped to in both these cases. Currently, each returns an object with a single property, but there are more types of snapping coming in follow-up PRs. When reviewing this PR, start with the definitions of `BoundsSnapGeometry` in `BoundsSnaps.ts` and `HandleSnapGeometry` in `HandleSnaps.ts` This doesn't add point snapping - i'll add that in a follow-up! It'll be customisable with the `getHandleSnapGeometry` API. Fixes TLD-2197 This PR is part of a series - please don't merge it until the things before it have landed! 1. #2827 4. #2831 5. #2793 (you are here) 6. #2841 7. #2845 ### Change Type - [x] `minor` — New feature [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan - [x] Unit Tests ### Release Notes - Add `ShapeUtil.getSnapInfo` for customising shape snaps.
This commit is contained in:
parent
ac0259a6af
commit
77865d9f5e
15 changed files with 726 additions and 886 deletions
|
@ -159,6 +159,11 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
||||||
onResize: TLOnResizeHandler<any>;
|
onResize: TLOnResizeHandler<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface BoundsSnapGeometry {
|
||||||
|
points?: VecModel[];
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface BoundsSnapPoint {
|
export interface BoundsSnapPoint {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -198,6 +203,8 @@ export class Box {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get corners(): Vec[];
|
get corners(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
get cornersAndCenter(): Vec[];
|
||||||
|
// (undocumented)
|
||||||
static Equals(a: Box | BoxModel, b: Box | BoxModel): boolean;
|
static Equals(a: Box | BoxModel, b: Box | BoxModel): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
equals(other: Box | BoxModel): boolean;
|
equals(other: Box | BoxModel): boolean;
|
||||||
|
@ -264,8 +271,6 @@ export class Box {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get size(): Vec;
|
get size(): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get snapPoints(): Vec[];
|
|
||||||
// (undocumented)
|
|
||||||
snapToGrid(size: number): void;
|
snapToGrid(size: number): void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toFixed(): this;
|
toFixed(): this;
|
||||||
|
@ -982,12 +987,8 @@ export abstract class Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get area(): number;
|
get area(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
_area: number | undefined;
|
|
||||||
// (undocumented)
|
|
||||||
get bounds(): Box;
|
get bounds(): Box;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
_bounds: Box | undefined;
|
|
||||||
// (undocumented)
|
|
||||||
get center(): Vec;
|
get center(): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
debugColor?: string;
|
debugColor?: string;
|
||||||
|
@ -1020,15 +1021,9 @@ export abstract class Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
nearestPointOnLineSegment(A: Vec, B: Vec): Vec;
|
nearestPointOnLineSegment(A: Vec, B: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get snapPoints(): Vec[];
|
|
||||||
// (undocumented)
|
|
||||||
_snapPoints: undefined | Vec[];
|
|
||||||
// (undocumented)
|
|
||||||
toSimpleSvgPath(): string;
|
toSimpleSvgPath(): string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get vertices(): Vec[];
|
get vertices(): Vec[];
|
||||||
// (undocumented)
|
|
||||||
_vertices: undefined | Vec[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
|
@ -1143,6 +1138,11 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const HALF_PI: number;
|
export const HALF_PI: number;
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface HandleSnapGeometry {
|
||||||
|
outline?: Geometry2d | null;
|
||||||
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function hardReset({ shouldReload }?: {
|
export function hardReset({ shouldReload }?: {
|
||||||
shouldReload?: boolean | undefined;
|
shouldReload?: boolean | undefined;
|
||||||
|
@ -1600,10 +1600,12 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
expandSelectionOutlinePx(shape: Shape): number;
|
expandSelectionOutlinePx(shape: Shape): number;
|
||||||
|
getBoundsSnapGeometry(shape: Shape): BoundsSnapGeometry;
|
||||||
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[];
|
||||||
abstract getDefaultProps(): Shape['props'];
|
abstract getDefaultProps(): Shape['props'];
|
||||||
abstract getGeometry(shape: Shape): Geometry2d;
|
abstract getGeometry(shape: Shape): Geometry2d;
|
||||||
getHandles?(shape: Shape): TLHandle[];
|
getHandles?(shape: Shape): TLHandle[];
|
||||||
|
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry;
|
||||||
getOutlineSegments(shape: Shape): Vec[][];
|
getOutlineSegments(shape: Shape): Vec[][];
|
||||||
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
hideResizeHandles: TLShapeUtilFlag<Shape>;
|
||||||
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
hideRotateHandle: TLShapeUtilFlag<Shape>;
|
||||||
|
|
|
@ -1317,6 +1317,56 @@
|
||||||
},
|
},
|
||||||
"implementsTokenRanges": []
|
"implementsTokenRanges": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Interface",
|
||||||
|
"canonicalReference": "@tldraw/editor!BoundsSnapGeometry:interface",
|
||||||
|
"docComment": "/**\n * When moving or resizing shapes, the bounds of the shape can snap to key geometry on other nearby shapes. Customize how a shape snaps to others with {@link ShapeUtil.getBoundsSnapGeometry}.\n *\n * @public\n */\n",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "export interface BoundsSnapGeometry "
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileUrlPath": "packages/editor/src/lib/editor/managers/SnapManager/BoundsSnaps.ts",
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "BoundsSnapGeometry",
|
||||||
|
"preserveMemberOrder": false,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"kind": "PropertySignature",
|
||||||
|
"canonicalReference": "@tldraw/editor!BoundsSnapGeometry#points:member",
|
||||||
|
"docComment": "/**\n * Points that this shape will snap to. By default, this will be the corners and center of the shapes bounding box. To disable snapping to a specific point, use an empty array.\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": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Interface",
|
"kind": "Interface",
|
||||||
"canonicalReference": "@tldraw/editor!BoundsSnapPoint:interface",
|
"canonicalReference": "@tldraw/editor!BoundsSnapPoint:interface",
|
||||||
|
@ -2028,6 +2078,41 @@
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
"isAbstract": false
|
"isAbstract": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Property",
|
||||||
|
"canonicalReference": "@tldraw/editor!Box#cornersAndCenter:member",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "get cornersAndCenter(): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "Vec",
|
||||||
|
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "[]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": true,
|
||||||
|
"isOptional": false,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "cornersAndCenter",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 3
|
||||||
|
},
|
||||||
|
"isStatic": false,
|
||||||
|
"isProtected": false,
|
||||||
|
"isAbstract": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Box#equals:member(1)",
|
"canonicalReference": "@tldraw/editor!Box#equals:member(1)",
|
||||||
|
@ -3562,41 +3647,6 @@
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
"isAbstract": false
|
"isAbstract": false
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Box#snapPoints:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "get snapPoints(): "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "Vec",
|
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": true,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "snapPoints",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 3
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Box#snapToGrid:member(1)",
|
"canonicalReference": "@tldraw/editor!Box#snapToGrid:member(1)",
|
||||||
|
@ -20202,149 +20252,6 @@
|
||||||
"name": "Geometry2d",
|
"name": "Geometry2d",
|
||||||
"preserveMemberOrder": false,
|
"preserveMemberOrder": false,
|
||||||
"members": [
|
"members": [
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#_area:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "_area: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "number | undefined"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": false,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "_area",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 2
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#_bounds:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "_bounds: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "Box",
|
|
||||||
"canonicalReference": "@tldraw/editor!Box:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": " | undefined"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": false,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "_bounds",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 3
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#_snapPoints:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "_snapPoints: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "undefined | "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "Vec",
|
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": false,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "_snapPoints",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 4
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#_vertices:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "_vertices: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "undefined | "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "Vec",
|
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": false,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "_vertices",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 4
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Constructor",
|
"kind": "Constructor",
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d:constructor(1)",
|
"canonicalReference": "@tldraw/editor!Geometry2d:constructor(1)",
|
||||||
|
@ -21195,41 +21102,6 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "nearestPointOnLineSegment"
|
"name": "nearestPointOnLineSegment"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Property",
|
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#snapPoints:member",
|
|
||||||
"docComment": "",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "get snapPoints(): "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "Vec",
|
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "[]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isReadonly": true,
|
|
||||||
"isOptional": false,
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "snapPoints",
|
|
||||||
"propertyTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 3
|
|
||||||
},
|
|
||||||
"isStatic": false,
|
|
||||||
"isProtected": false,
|
|
||||||
"isAbstract": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Geometry2d#toSimpleSvgPath:member(1)",
|
"canonicalReference": "@tldraw/editor!Geometry2d#toSimpleSvgPath:member(1)",
|
||||||
|
@ -23084,6 +22956,56 @@
|
||||||
"endIndex": 2
|
"endIndex": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "export interface HandleSnapGeometry "
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileUrlPath": "packages/editor/src/lib/editor/managers/SnapManager/HandleSnaps.ts",
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "HandleSnapGeometry",
|
||||||
|
"preserveMemberOrder": false,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"kind": "PropertySignature",
|
||||||
|
"canonicalReference": "@tldraw/editor!HandleSnapGeometry#outline:member",
|
||||||
|
"docComment": "/**\n * A `Geometry2d` that describe the outline of the shape that the handle will snap to - fills are ignored. By default, this is the same geometry returned by {@link ShapeUtil.getGeometry}. Set this to `null` to disable handle snapping to this shape's outline.\n */\n",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "outline?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "Geometry2d",
|
||||||
|
"canonicalReference": "@tldraw/editor!Geometry2d:class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": " | null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": false,
|
||||||
|
"isOptional": true,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "outline",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extendsTokenRanges": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Function",
|
"kind": "Function",
|
||||||
"canonicalReference": "@tldraw/editor!hardReset:function(1)",
|
"canonicalReference": "@tldraw/editor!hardReset:function(1)",
|
||||||
|
@ -30256,6 +30178,55 @@
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
"isAbstract": false
|
"isAbstract": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/editor!ShapeUtil#getBoundsSnapGeometry:member(1)",
|
||||||
|
"docComment": "/**\n * Get the geometry to use when snapping to this this shape in translate/resize operations. See {@link BoundsSnapGeometry} for details.\n */\n",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "getBoundsSnapGeometry(shape: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "Shape"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "BoundsSnapGeometry",
|
||||||
|
"canonicalReference": "@tldraw/editor!BoundsSnapGeometry: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": "getBoundsSnapGeometry"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!ShapeUtil#getCanvasSvgDefs:member(1)",
|
"canonicalReference": "@tldraw/editor!ShapeUtil#getCanvasSvgDefs:member(1)",
|
||||||
|
@ -30425,6 +30396,55 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "getHandles"
|
"name": "getHandles"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/editor!ShapeUtil#getHandleSnapGeometry:member(1)",
|
||||||
|
"docComment": "/**\n * Get the geometry to use when snapping handles to this shape. See {@link HandleSnapGeometry} for details.\n */\n",
|
||||||
|
"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": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!ShapeUtil#getOutlineSegments:member(1)",
|
"canonicalReference": "@tldraw/editor!ShapeUtil#getOutlineSegments:member(1)",
|
||||||
|
|
|
@ -154,7 +154,11 @@ export type {
|
||||||
TLBeforeCreateHandler,
|
TLBeforeCreateHandler,
|
||||||
TLBeforeDeleteHandler,
|
TLBeforeDeleteHandler,
|
||||||
} from './lib/editor/managers/SideEffectManager'
|
} from './lib/editor/managers/SideEffectManager'
|
||||||
export { type BoundsSnapPoint } from './lib/editor/managers/SnapManager/BoundsSnaps'
|
export {
|
||||||
|
type BoundsSnapGeometry,
|
||||||
|
type BoundsSnapPoint,
|
||||||
|
} from './lib/editor/managers/SnapManager/BoundsSnaps'
|
||||||
|
export { type HandleSnapGeometry } from './lib/editor/managers/SnapManager/HandleSnaps'
|
||||||
export {
|
export {
|
||||||
SnapManager,
|
SnapManager,
|
||||||
type GapsSnapIndicator,
|
type GapsSnapIndicator,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { computed } from '@tldraw/state'
|
import { computed } from '@tldraw/state'
|
||||||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
import { TLShape, TLShapeId, VecModel } from '@tldraw/tlschema'
|
||||||
import { assertExists, dedupe } from '@tldraw/utils'
|
import { assertExists, dedupe } from '@tldraw/utils'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
@ -22,6 +22,20 @@ import {
|
||||||
SnapManager,
|
SnapManager,
|
||||||
} from './SnapManager'
|
} from './SnapManager'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When moving or resizing shapes, the bounds of the shape can snap to key geometry on other nearby
|
||||||
|
* shapes. Customize how a shape snaps to others with {@link ShapeUtil.getBoundsSnapGeometry}.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface BoundsSnapGeometry {
|
||||||
|
/**
|
||||||
|
* Points that this shape will snap to. By default, this will be the corners and center of the
|
||||||
|
* shapes bounding box. To disable snapping to a specific point, use an empty array.
|
||||||
|
*/
|
||||||
|
points?: VecModel[]
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface BoundsSnapPoint {
|
export interface BoundsSnapPoint {
|
||||||
id: string
|
id: string
|
||||||
|
@ -188,11 +202,15 @@ export class BoundsSnaps {
|
||||||
@computed private getSnapPointsCache() {
|
@computed private getSnapPointsCache() {
|
||||||
const { editor } = this
|
const { editor } = this
|
||||||
return editor.store.createComputedCache<BoundsSnapPoint[], TLShape>('snapPoints', (shape) => {
|
return editor.store.createComputedCache<BoundsSnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||||
const pageTransfrorm = editor.getShapePageTransform(shape.id)
|
const pageTransform = editor.getShapePageTransform(shape.id)
|
||||||
if (!pageTransfrorm) return undefined
|
if (!pageTransform) return undefined
|
||||||
const snapPoints = this.editor.getShapeGeometry(shape).snapPoints
|
const boundsSnapGeometry = editor.getShapeUtil(shape).getBoundsSnapGeometry(shape)
|
||||||
|
const snapPoints =
|
||||||
|
boundsSnapGeometry.points ?? editor.getShapeGeometry(shape).bounds.cornersAndCenter
|
||||||
|
|
||||||
|
if (!pageTransform || !snapPoints) return undefined
|
||||||
return snapPoints.map((point, i) => {
|
return snapPoints.map((point, i) => {
|
||||||
const { x, y } = Mat.applyToPoint(pageTransfrorm, point)
|
const { x, y } = Mat.applyToPoint(pageTransform, point)
|
||||||
return { x, y, id: `${shape.id}:${i}` }
|
return { x, y, id: `${shape.id}:${i}` }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -208,12 +226,12 @@ export class BoundsSnaps {
|
||||||
const snappableShapes = this.manager.getSnappableShapes()
|
const snappableShapes = this.manager.getSnappableShapes()
|
||||||
const result: BoundsSnapPoint[] = []
|
const result: BoundsSnapPoint[] = []
|
||||||
|
|
||||||
snappableShapes.forEach((shapeId) => {
|
for (const shapeId of snappableShapes) {
|
||||||
const snapPoints = snapPointsCache.get(shapeId)
|
const snapPoints = snapPointsCache.get(shapeId)
|
||||||
if (snapPoints) {
|
if (snapPoints) {
|
||||||
result.push(...snapPoints)
|
result.push(...snapPoints)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,45 @@
|
||||||
import { computed } from '@tldraw/state'
|
import { computed } from '@tldraw/state'
|
||||||
import { VecModel } from '@tldraw/tlschema'
|
import { TLShape } from '@tldraw/tlschema'
|
||||||
import { deepCopy } from '@tldraw/utils'
|
import { assertExists } from '@tldraw/utils'
|
||||||
import { Mat } from '../../../primitives/Mat'
|
|
||||||
import { Vec } from '../../../primitives/Vec'
|
import { Vec } from '../../../primitives/Vec'
|
||||||
|
import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
|
||||||
import { uniqueId } from '../../../utils/uniqueId'
|
import { uniqueId } from '../../../utils/uniqueId'
|
||||||
import { Editor } from '../../Editor'
|
import { Editor } from '../../Editor'
|
||||||
import { SnapData, SnapManager } from './SnapManager'
|
import { SnapData, SnapManager } from './SnapManager'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface HandleSnapGeometry {
|
||||||
|
/**
|
||||||
|
* A `Geometry2d` that describe the outline of the shape that the handle will snap to - fills
|
||||||
|
* are ignored. By default, this is the same geometry returned by {@link ShapeUtil.getGeometry}.
|
||||||
|
* Set this to `null` to disable handle snapping to this shape's outline.
|
||||||
|
*/
|
||||||
|
outline?: Geometry2d | null
|
||||||
|
}
|
||||||
|
|
||||||
export class HandleSnaps {
|
export class HandleSnaps {
|
||||||
readonly editor: Editor
|
readonly editor: Editor
|
||||||
constructor(readonly manager: SnapManager) {
|
constructor(readonly manager: SnapManager) {
|
||||||
this.editor = manager.editor
|
this.editor = manager.editor
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed private getOutlinesInPageSpace() {
|
@computed private getSnapGeometryCache() {
|
||||||
return Array.from(this.manager.getSnappableShapes(), (id) => {
|
const { editor } = this
|
||||||
const geometry = this.editor.getShapeGeometry(id)
|
return editor.store.createComputedCache('handle snap geometry', (shape: TLShape) => {
|
||||||
const outline = deepCopy(geometry.vertices)
|
const snapGeometry = editor.getShapeUtil(shape).getHandleSnapGeometry(shape)
|
||||||
if (geometry.isClosed) outline.push(outline[0])
|
|
||||||
const pageTransform = this.editor.getShapePageTransform(id)
|
return {
|
||||||
if (!pageTransform) throw Error('No page transform')
|
outline:
|
||||||
return Mat.applyToPoints(pageTransform, outline)
|
snapGeometry.outline === undefined
|
||||||
|
? editor.getShapeGeometry(shape)
|
||||||
|
: snapGeometry.outline,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,27 +51,36 @@ export class HandleSnaps {
|
||||||
additionalSegments: Vec[][]
|
additionalSegments: Vec[][]
|
||||||
}): SnapData | null {
|
}): SnapData | null {
|
||||||
const snapThreshold = this.manager.getSnapThreshold()
|
const snapThreshold = this.manager.getSnapThreshold()
|
||||||
const outlinesInPageSpace = this.getOutlinesInPageSpace()
|
|
||||||
|
|
||||||
// Find the nearest point that is within the snap threshold
|
// Find the nearest point that is within the snap threshold
|
||||||
let minDistance = snapThreshold
|
let minDistance = snapThreshold
|
||||||
let nearestPoint: Vec | null = null
|
let nearestPoint: Vec | null = null
|
||||||
let C: VecModel, D: VecModel, nearest: Vec, distance: number
|
|
||||||
const allSegments = [...outlinesInPageSpace, ...additionalSegments]
|
|
||||||
for (const outline of allSegments) {
|
|
||||||
for (let i = 0; i < outline.length - 1; i++) {
|
|
||||||
C = outline[i]
|
|
||||||
D = outline[i + 1]
|
|
||||||
|
|
||||||
nearest = Vec.NearestPointOnLineSegment(C, D, handlePoint)
|
for (const shapeId of this.manager.getSnappableShapes()) {
|
||||||
distance = Vec.Dist(handlePoint, nearest)
|
const handleSnapOutline = this.getSnapGeometryCache().get(shapeId)?.outline
|
||||||
|
if (!handleSnapOutline) 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 (isNaN(distance)) continue
|
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance
|
minDistance = distance
|
||||||
nearestPoint = nearest
|
nearestPoint = nearestInPageSpace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle additional segments:
|
||||||
|
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 we found a point, display snap lines, and return the nudge
|
// If we found a point, display snap lines, and return the nudge
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
|
import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
|
||||||
import { isShapeId, TLFrameShape, TLGroupShape, TLParentId, TLShapeId } from '@tldraw/tlschema'
|
import { TLFrameShape, TLGroupShape, TLParentId, TLShapeId, isShapeId } from '@tldraw/tlschema'
|
||||||
import { Vec, VecLike } from '../../../primitives/Vec'
|
import { Vec, VecLike } from '../../../primitives/Vec'
|
||||||
import type { Editor } from '../../Editor'
|
import type { Editor } from '../../Editor'
|
||||||
import { BoundsSnaps } from './BoundsSnaps'
|
import { BoundsSnaps } from './BoundsSnaps'
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { Box } from '../../primitives/Box'
|
||||||
import { Vec } from '../../primitives/Vec'
|
import { Vec } from '../../primitives/Vec'
|
||||||
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
||||||
import type { Editor } from '../Editor'
|
import type { Editor } from '../Editor'
|
||||||
|
import { BoundsSnapGeometry } from '../managers/SnapManager/BoundsSnaps'
|
||||||
|
import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
|
||||||
import { SvgExportContext } from '../types/SvgExportContext'
|
import { SvgExportContext } from '../types/SvgExportContext'
|
||||||
import { TLResizeHandle } from '../types/selection-types'
|
import { TLResizeHandle } from '../types/selection-types'
|
||||||
|
|
||||||
|
@ -276,6 +278,22 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the geometry to use when snapping to this this shape in translate/resize operations. See
|
||||||
|
* {@link BoundsSnapGeometry} for details.
|
||||||
|
*/
|
||||||
|
getBoundsSnapGeometry(shape: Shape): BoundsSnapGeometry {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the geometry to use when snapping handles to this shape. See {@link HandleSnapGeometry}
|
||||||
|
* for details.
|
||||||
|
*/
|
||||||
|
getHandleSnapGeometry(shape: Shape): HandleSnapGeometry {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -134,7 +134,7 @@ export class Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
get snapPoints() {
|
get cornersAndCenter() {
|
||||||
return [
|
return [
|
||||||
new Vec(this.minX, this.minY),
|
new Vec(this.minX, this.minY),
|
||||||
new Vec(this.maxX, this.minY),
|
new Vec(this.maxX, this.minY),
|
||||||
|
|
|
@ -79,7 +79,7 @@ export abstract class Geometry2d {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_vertices: Vec[] | undefined
|
private _vertices: Vec[] | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
get vertices(): Vec[] {
|
get vertices(): Vec[] {
|
||||||
|
@ -94,7 +94,7 @@ export abstract class Geometry2d {
|
||||||
return Box.FromPoints(this.vertices)
|
return Box.FromPoints(this.vertices)
|
||||||
}
|
}
|
||||||
|
|
||||||
_bounds: Box | undefined
|
private _bounds: Box | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
get bounds(): Box {
|
get bounds(): Box {
|
||||||
|
@ -104,22 +104,12 @@ export abstract class Geometry2d {
|
||||||
return this._bounds
|
return this._bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
_snapPoints: Vec[] | undefined
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
get snapPoints() {
|
|
||||||
if (!this._snapPoints) {
|
|
||||||
this._snapPoints = this.bounds.snapPoints
|
|
||||||
}
|
|
||||||
return this._snapPoints
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
get center() {
|
get center() {
|
||||||
return this.bounds.center
|
return this.bounds.center
|
||||||
}
|
}
|
||||||
|
|
||||||
_area: number | undefined
|
private _area: number | undefined
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
get area() {
|
get area() {
|
||||||
|
|
|
@ -40,10 +40,13 @@ beforeEach(() => {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getShape = () => editor.getShape<TLLineShape>(id)!
|
||||||
|
const getHandles = () => (editor.getShapeUtil('line') as LineShapeUtil).getHandles(getShape())
|
||||||
|
|
||||||
describe('Translating', () => {
|
describe('Translating', () => {
|
||||||
it('updates the line', () => {
|
it('updates the line', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
editor.pointerDown(25, 25, { target: 'shape', shape: editor.getShape<TLLineShape>(id) })
|
editor.pointerDown(25, 25, { target: 'shape', shape: getShape() })
|
||||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||||
editor.expectShapeToMatch({
|
editor.expectShapeToMatch({
|
||||||
id: id,
|
id: id,
|
||||||
|
@ -55,7 +58,7 @@ describe('Translating', () => {
|
||||||
it('updates the line when rotated', () => {
|
it('updates the line when rotated', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
const shape = editor.getShape<TLLineShape>(id)!
|
const shape = getShape()
|
||||||
editor.updateShape({ ...shape, rotation: Math.PI / 2 })
|
editor.updateShape({ ...shape, rotation: Math.PI / 2 })
|
||||||
|
|
||||||
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
|
editor.pointerDown(250, 250, { target: 'shape', shape: shape })
|
||||||
|
@ -73,10 +76,9 @@ describe('Mid-point handles', () => {
|
||||||
it('create new handle', () => {
|
it('create new handle', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
const shape = editor.getShape<TLLineShape>(id)!
|
|
||||||
editor.pointerDown(200, 200, {
|
editor.pointerDown(200, 200, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape,
|
shape: getShape(),
|
||||||
handle: {
|
handle: {
|
||||||
id: 'mid-0',
|
id: 'mid-0',
|
||||||
type: 'create',
|
type: 'create',
|
||||||
|
@ -92,7 +94,6 @@ describe('Mid-point handles', () => {
|
||||||
id: id,
|
id: id,
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
...shape.props.handles,
|
|
||||||
a1V: { x: 200, y: 200 },
|
a1V: { x: 200, y: 200 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,13 +105,11 @@ describe('Mid-point handles', () => {
|
||||||
|
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
const shape = editor.getShape<TLLineShape>(id)!
|
|
||||||
const util = editor.getShapeUtil('line') as LineShapeUtil
|
|
||||||
editor
|
editor
|
||||||
.pointerDown(200, 200, {
|
.pointerDown(200, 200, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape,
|
shape: getShape(),
|
||||||
handle: util.getHandles(shape).find((h) => h.id === 'mid-0')!,
|
handle: getHandles().find((h) => h.id === 'mid-0')!,
|
||||||
})
|
})
|
||||||
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
@ -119,7 +118,6 @@ describe('Mid-point handles', () => {
|
||||||
id: id,
|
id: id,
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
...shape.props.handles,
|
|
||||||
a1V: { x: 50, y: 80 },
|
a1V: { x: 50, y: 80 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -130,29 +128,26 @@ describe('Mid-point handles', () => {
|
||||||
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
|
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
const getShape = () => editor.getShape<TLLineShape>(id)!
|
|
||||||
const util = editor.getShapeUtil('line') as LineShapeUtil
|
|
||||||
|
|
||||||
// use a mid-point handle to create a new handle
|
// use a mid-point handle to create a new handle
|
||||||
editor
|
editor
|
||||||
.pointerDown(200, 200, {
|
.pointerDown(200, 200, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape: getShape(),
|
shape: getShape(),
|
||||||
handle: util.getHandles(getShape()).find((h) => h.id === 'mid-0')!,
|
handle: getHandles().find((h) => h.id === 'mid-0')!,
|
||||||
})
|
})
|
||||||
.pointerMove(230, 200)
|
.pointerMove(230, 200)
|
||||||
.pointerMove(200, 200)
|
.pointerMove(200, 200)
|
||||||
.pointerUp()
|
.pointerUp()
|
||||||
|
|
||||||
// 3 actual points, plus 2 mid-points:
|
// 3 actual points, plus 2 mid-points:
|
||||||
expect(util.getHandles(getShape())).toHaveLength(5)
|
expect(getHandles()).toHaveLength(5)
|
||||||
|
|
||||||
// now, try dragging the newly created handle. it should still snap:
|
// now, try dragging the newly created handle. it should still snap:
|
||||||
editor
|
editor
|
||||||
.pointerDown(200, 200, {
|
.pointerDown(200, 200, {
|
||||||
target: 'handle',
|
target: 'handle',
|
||||||
shape: getShape(),
|
shape: getShape(),
|
||||||
handle: util.getHandles(getShape()).find((h) => h.id === 'a1V')!,
|
handle: getHandles().find((h) => h.id === 'a1V')!,
|
||||||
})
|
})
|
||||||
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
@ -161,7 +156,6 @@ describe('Mid-point handles', () => {
|
||||||
id: id,
|
id: id,
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
...getShape().props.handles,
|
|
||||||
a1V: { x: 50, y: 80 },
|
a1V: { x: 50, y: 80 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -169,10 +163,63 @@ describe('Mid-point handles', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Snapping', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.updateShape({
|
||||||
|
id: id,
|
||||||
|
type: 'line',
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
a1: { x: 0, y: 0 },
|
||||||
|
a2: { x: 100, y: 0 },
|
||||||
|
a3: { x: 100, y: 100 },
|
||||||
|
a4: { x: 0, y: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('snaps endpoints to itself', () => {
|
||||||
|
editor.select(id)
|
||||||
|
|
||||||
|
editor
|
||||||
|
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
|
||||||
|
.pointerMove(50, 95, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
editor.expectShapeToMatch({
|
||||||
|
id: id,
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
a1: { x: 50, y: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't snap to the segment of the current handle", () => {
|
||||||
|
editor.select(id)
|
||||||
|
|
||||||
|
editor
|
||||||
|
.pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] })
|
||||||
|
.pointerMove(5, 2, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
editor.expectShapeToMatch({
|
||||||
|
id: id,
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
a1: { x: 5, y: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Misc', () => {
|
describe('Misc', () => {
|
||||||
it('preserves handle positions on spline type change', () => {
|
it('preserves handle positions on spline type change', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
const shape = editor.getShape<TLLineShape>(id)!
|
const shape = getShape()
|
||||||
const prevHandles = deepCopy(shape.props.handles)
|
const prevHandles = deepCopy(shape.props.handles)
|
||||||
|
|
||||||
editor.updateShapes([
|
editor.updateShapes([
|
||||||
|
@ -195,7 +242,6 @@ describe('Misc', () => {
|
||||||
|
|
||||||
it('resizes', () => {
|
it('resizes', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
editor.getShape<TLLineShape>(id)!
|
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.pointerDown(150, 0, { target: 'selection', handle: 'bottom' })
|
.pointerDown(150, 0, { target: 'selection', handle: 'bottom' })
|
||||||
|
@ -229,7 +275,7 @@ describe('Misc', () => {
|
||||||
editor.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
|
editor.createShapes([{ id: boxID, type: 'geo', x: 500, y: 150, props: { w: 100, h: 50 } }])
|
||||||
|
|
||||||
const box = editor.getShape<TLGeoShape>(boxID)!
|
const box = editor.getShape<TLGeoShape>(boxID)!
|
||||||
const line = editor.getShape<TLLineShape>(id)!
|
const line = getShape()
|
||||||
|
|
||||||
editor.select(boxID, id)
|
editor.select(boxID, id)
|
||||||
|
|
||||||
|
@ -247,9 +293,7 @@ describe('Misc', () => {
|
||||||
it('duplicates', () => {
|
it('duplicates', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
editor
|
editor.keyDown('Alt').pointerDown(25, 25, { target: 'shape', shape: getShape() })
|
||||||
.keyDown('Alt')
|
|
||||||
.pointerDown(25, 25, { target: 'shape', shape: editor.getShape<TLLineShape>(id) })
|
|
||||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||||
editor.pointerUp().keyUp('Alt')
|
editor.pointerUp().keyUp('Alt')
|
||||||
|
|
||||||
|
@ -259,9 +303,7 @@ describe('Misc', () => {
|
||||||
it('deletes', () => {
|
it('deletes', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
editor
|
editor.keyDown('Alt').pointerDown(25, 25, { target: 'shape', shape: getShape() })
|
||||||
.keyDown('Alt')
|
|
||||||
.pointerDown(25, 25, { target: 'shape', shape: editor.getShape<TLLineShape>(id) })
|
|
||||||
editor.pointerMove(50, 50) // Move shape by 25, 25
|
editor.pointerMove(50, 50) // Move shape by 25, 25
|
||||||
editor.pointerUp().keyUp('Alt')
|
editor.pointerUp().keyUp('Alt')
|
||||||
|
|
||||||
|
|
|
@ -329,7 +329,7 @@ function getTranslatingSnapshot(editor: Editor) {
|
||||||
} else {
|
} else {
|
||||||
const selectionPageBounds = editor.getSelectionPageBounds()
|
const selectionPageBounds = editor.getSelectionPageBounds()
|
||||||
if (selectionPageBounds) {
|
if (selectionPageBounds) {
|
||||||
initialSnapPoints = selectionPageBounds.snapPoints.map((p, i) => ({
|
initialSnapPoints = selectionPageBounds.cornersAndCenter.map((p, i) => ({
|
||||||
id: 'selection:' + i,
|
id: 'selection:' + i,
|
||||||
x: p.x,
|
x: p.x,
|
||||||
y: p.y,
|
y: p.y,
|
||||||
|
|
299
packages/tldraw/src/test/customSnapping.test.tsx
Normal file
299
packages/tldraw/src/test/customSnapping.test.tsx
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
IndexKey,
|
||||||
|
Polyline2d,
|
||||||
|
TLAnyShapeUtilConstructor,
|
||||||
|
TLBaseShape,
|
||||||
|
TLLineShape,
|
||||||
|
TLShapeId,
|
||||||
|
Vec,
|
||||||
|
VecModel,
|
||||||
|
} from '@tldraw/editor'
|
||||||
|
import { TestEditor } from './TestEditor'
|
||||||
|
import { TL } from './test-jsx'
|
||||||
|
|
||||||
|
describe('custom shape bounds snapping - translate', () => {
|
||||||
|
type TestShape = TLBaseShape<
|
||||||
|
'test',
|
||||||
|
{ w: number; h: number; boundsSnapPoints: VecModel[] | null }
|
||||||
|
>
|
||||||
|
class TestShapeUtil extends BaseBoxShapeUtil<TestShape> {
|
||||||
|
static override type = 'test'
|
||||||
|
override getDefaultProps() {
|
||||||
|
return { w: 100, h: 100, boundsSnapPoints: null }
|
||||||
|
}
|
||||||
|
override component() {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
override indicator() {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
override getBoundsSnapGeometry(shape: TestShape) {
|
||||||
|
return {
|
||||||
|
points: shape.props.boundsSnapPoints ?? undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shapeUtils = [TestShapeUtil] as TLAnyShapeUtilConstructor[]
|
||||||
|
|
||||||
|
let editor: TestEditor
|
||||||
|
let ids: Record<string, TLShapeId>
|
||||||
|
beforeEach(() => {
|
||||||
|
editor = new TestEditor({ shapeUtils })
|
||||||
|
ids = editor.createShapesFromJsx([
|
||||||
|
<TL.geo ref="box" x={0} y={0} w={100} h={100} />,
|
||||||
|
<TL.test ref="test" x={200} y={200} w={100} h={100} boundsSnapPoints={null} />,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with default boundSnapPoints', () => {
|
||||||
|
test('normal snapping works with default boundSnapPoints when moving test shape', () => {
|
||||||
|
// start translating the test shape
|
||||||
|
editor.setSelectedShapes([ids.test]).pointerDown(250, 250)
|
||||||
|
|
||||||
|
// move the left edge of the test shape to the right edge of the box shape - it should snap
|
||||||
|
editor.pointerMove(155, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(100)
|
||||||
|
|
||||||
|
// move the left edge of the test shape to the center of the box shape - it should snap
|
||||||
|
editor.pointerMove(105, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(2)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normal snapping works with default boundSnapPoints when snapping to test shape', () => {
|
||||||
|
// start translating the box shape
|
||||||
|
editor.setSelectedShapes([ids.box]).pointerDown(50, 50)
|
||||||
|
|
||||||
|
// move the left edge of the box shape to the right edge of the test shape - it should snap
|
||||||
|
editor.pointerMove(155, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(100)
|
||||||
|
|
||||||
|
// move the left edge of the box shape to the center of the test shape - it should snap
|
||||||
|
editor.pointerMove(205, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(2)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(150)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with only the center in boundSnapPoints', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.updateShape<TestShape>({
|
||||||
|
id: ids.test,
|
||||||
|
type: 'test',
|
||||||
|
props: { boundsSnapPoints: [{ x: 50, y: 50 }] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when moving the test shape', () => {
|
||||||
|
beforeEach(() => editor.select(ids.test).pointerDown(250, 250))
|
||||||
|
|
||||||
|
test('does not snap its edges to the box edges', () => {
|
||||||
|
editor.pointerMove(155, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(105)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('snaps its center to the box right edge', () => {
|
||||||
|
editor.pointerMove(105, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(50)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when moving the box shape', () => {
|
||||||
|
beforeEach(() => editor.select(ids.box).pointerDown(50, 50))
|
||||||
|
|
||||||
|
test('does not snap to the left edge of the test shape', () => {
|
||||||
|
editor.pointerMove(155, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(105)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('snaps its right edge to the center of the test shape', () => {
|
||||||
|
editor.pointerMove(205, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(150)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with empty boundSnapPoints', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.updateShape<TestShape>({
|
||||||
|
id: ids.test,
|
||||||
|
type: 'test',
|
||||||
|
props: { boundsSnapPoints: [] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('test shape does not snap to anything', () => {
|
||||||
|
editor.select(ids.test).pointerDown(250, 250)
|
||||||
|
|
||||||
|
// try to snap our left edge to the right edge of the box shape - it should not snap
|
||||||
|
editor.pointerMove(155, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(105)
|
||||||
|
|
||||||
|
// try to snap our left edge to the center of the box shape - it should not snap
|
||||||
|
editor.pointerMove(105, 250, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(55)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('box shape does not snap to test shape', () => {
|
||||||
|
editor.select(ids.box).pointerDown(50, 50)
|
||||||
|
|
||||||
|
// try to snap our left edge to the right edge of the test shape - it should not snap
|
||||||
|
editor.pointerMove(155, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(105)
|
||||||
|
|
||||||
|
// try to snap our right edge to the center of the test shape - it should not snap
|
||||||
|
editor.pointerMove(205, 50, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(editor.getOnlySelectedShape()?.x).toBe(155)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('custom handle snapping', () => {
|
||||||
|
type TestShape = TLBaseShape<
|
||||||
|
'test',
|
||||||
|
{ w: number; h: number; handleGeomVertices: VecModel[] | 'default' | null }
|
||||||
|
>
|
||||||
|
class TestShapeUtil extends BaseBoxShapeUtil<TestShape> {
|
||||||
|
static override type = 'test'
|
||||||
|
override getDefaultProps(): TestShape['props'] {
|
||||||
|
return { w: 100, h: 100, handleGeomVertices: 'default' }
|
||||||
|
}
|
||||||
|
override component() {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
override indicator() {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
override getHandleSnapGeometry(shape: TestShape) {
|
||||||
|
const vertices = shape.props.handleGeomVertices
|
||||||
|
return {
|
||||||
|
outline:
|
||||||
|
vertices === 'default'
|
||||||
|
? undefined
|
||||||
|
: vertices === null
|
||||||
|
? null
|
||||||
|
: new Polyline2d({ points: vertices.map(Vec.From) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shapeUtils = [TestShapeUtil] as TLAnyShapeUtilConstructor[]
|
||||||
|
|
||||||
|
let editor: TestEditor
|
||||||
|
let ids: Record<string, TLShapeId>
|
||||||
|
beforeEach(() => {
|
||||||
|
editor = new TestEditor({ shapeUtils })
|
||||||
|
ids = editor.createShapesFromJsx([
|
||||||
|
<TL.line
|
||||||
|
ref="line"
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
handles={{
|
||||||
|
['a1' as IndexKey]: { x: 0, y: 0 },
|
||||||
|
['a2' as IndexKey]: { x: 100, y: 100 },
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<TL.test ref="test" x={200} y={200} w={100} h={100} boundsSnapPoints={null} />,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
function startDraggingHandle() {
|
||||||
|
const shape = editor.select(ids.line).getOnlySelectedShape()! as TLLineShape
|
||||||
|
const handles = editor.getShapeHandles(shape)!
|
||||||
|
editor.pointerDown(100, 100, { target: 'handle', shape, handle: handles[handles.length - 1] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePosition() {
|
||||||
|
const shape = editor.select(ids.line).getOnlySelectedShape()! as TLLineShape
|
||||||
|
const handles = editor.getShapeHandles(shape)!
|
||||||
|
const handle = handles[handles.length - 1]
|
||||||
|
return { x: handle.x, y: handle.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('with default handleGeomVertices', () => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("doesn't particularly snap to vertices", () => {
|
||||||
|
startDraggingHandle()
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with empty handleGeomVertices', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.updateShape<TestShape>({
|
||||||
|
id: ids.test,
|
||||||
|
type: 'test',
|
||||||
|
props: { handleGeomVertices: null },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("doesn't snap to the shape at all", () => {
|
||||||
|
startDraggingHandle()
|
||||||
|
editor.pointerMove(215, 205, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(handlePosition().x).toBe(215)
|
||||||
|
expect(handlePosition().y).toBe(205)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with custom handleGeomVertices', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
editor.updateShape<TestShape>({
|
||||||
|
id: ids.test,
|
||||||
|
type: 'test',
|
||||||
|
props: {
|
||||||
|
// a diagonal line from the top left to the bottom right
|
||||||
|
handleGeomVertices: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 100, y: 100 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not snap to the normal edges of the shape', () => {
|
||||||
|
startDraggingHandle()
|
||||||
|
editor.pointerMove(235, 205, undefined, { ctrlKey: true })
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||||
|
expect(handlePosition().x).toBe(235)
|
||||||
|
expect(handlePosition().y).toBe(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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -46,7 +46,10 @@ export const TL = new Proxy(
|
||||||
return createElement(key as string)
|
return createElement(key as string)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
) as { [K in TLDefaultShape['type']]: (props: PropsForShape<K>) => null }
|
) as { [K in TLDefaultShape['type']]: (props: PropsForShape<K>) => null } & Record<
|
||||||
|
string,
|
||||||
|
(props: PropsForShape<string>) => null
|
||||||
|
>
|
||||||
|
|
||||||
export function shapesFromJsx(shapes: React.JSX.Element | Array<React.JSX.Element>) {
|
export function shapesFromJsx(shapes: React.JSX.Element | Array<React.JSX.Element>) {
|
||||||
const ids = {} as Record<string, TLShapeId>
|
const ids = {} as Record<string, TLShapeId>
|
||||||
|
|
|
@ -1,584 +0,0 @@
|
||||||
import { Geometry2d, Rectangle2d, ShapeUtil, SnapIndicator, createShapeId } from '@tldraw/editor'
|
|
||||||
import { TestEditor } from './TestEditor'
|
|
||||||
|
|
||||||
let editor: TestEditor
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
editor?.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ids = {
|
|
||||||
frame1: createShapeId('frame1'),
|
|
||||||
frame2: createShapeId('frame2'),
|
|
||||||
box1: createShapeId('box1'),
|
|
||||||
box2: createShapeId('box2'),
|
|
||||||
line1: createShapeId('line1'),
|
|
||||||
boxD: createShapeId('boxD'),
|
|
||||||
boxE: createShapeId('boxE'),
|
|
||||||
boxF: createShapeId('boxF'),
|
|
||||||
boxG: createShapeId('boxG'),
|
|
||||||
boxH: createShapeId('boxH'),
|
|
||||||
boxX: createShapeId('boxX'),
|
|
||||||
|
|
||||||
boxT: createShapeId('boxT'),
|
|
||||||
|
|
||||||
lineA: createShapeId('lineA'),
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNumSnapPoints = (snap: SnapIndicator): number => {
|
|
||||||
return snap.type === 'points' ? snap.points.length : (null as any as number)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSnapPoints = (snap: SnapIndicator) => {
|
|
||||||
return snap.type === 'points' ? snap.points : null
|
|
||||||
}
|
|
||||||
|
|
||||||
type __TopLeftSnapOnlyShape = any
|
|
||||||
|
|
||||||
class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
|
|
||||||
static override type = '__test_top_left_snap_only' as const
|
|
||||||
|
|
||||||
getDefaultProps(): __TopLeftSnapOnlyShape['props'] {
|
|
||||||
return { width: 10, height: 10 }
|
|
||||||
}
|
|
||||||
component() {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
indicator() {
|
|
||||||
throw new Error('Method not implemented.')
|
|
||||||
}
|
|
||||||
getGeometry(shape: __TopLeftSnapOnlyShape): Geometry2d {
|
|
||||||
return new Rectangle2d({
|
|
||||||
width: shape.props.width,
|
|
||||||
height: shape.props.height,
|
|
||||||
isFilled: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const __TopLeftSnapOnlyShape = __TopLeftSnapOnlyShapeUtil
|
|
||||||
|
|
||||||
describe.skip('custom snapping points', () => {
|
|
||||||
const shapeUtils = Object.freeze([__TopLeftSnapOnlyShape])
|
|
||||||
beforeEach(() => {
|
|
||||||
editor?.dispose()
|
|
||||||
editor = new TestEditor({
|
|
||||||
shapeUtils,
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ B │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
})
|
|
||||||
editor.createShapes([
|
|
||||||
{
|
|
||||||
type: '__test_top_left_snap_only',
|
|
||||||
id: ids.boxT,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
props: { width: 100, height: 100 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'geo',
|
|
||||||
id: ids.box1,
|
|
||||||
x: 200,
|
|
||||||
y: 200,
|
|
||||||
props: { w: 100, h: 100 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'geo',
|
|
||||||
id: ids.box2,
|
|
||||||
x: 400,
|
|
||||||
y: 400,
|
|
||||||
props: { w: 100, h: 100 },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows other shapes to snap to custom snap points', () => {
|
|
||||||
// should snap to 0 on y axis
|
|
||||||
// x────────────x───────x
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │ │ A │
|
|
||||||
// │ │ │ x │
|
|
||||||
// │ │ │ drag │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
editor.pointerDown(250, 250, ids.box1).pointerMove(250, 51, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should not snap to 100 on y axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(250, 151, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 101 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should not snap to 50 on y axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │ x───────x
|
|
||||||
// │ │ │ A │
|
|
||||||
// └───────┘ │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(250, 101, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 51 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should snap to 0 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ drag │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(51, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 0, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should not snap to 100 on x axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(151, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 101, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should not snap to 50 on x axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(101, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 51, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows shapes with custom points to snap to other shapes', () => {
|
|
||||||
// should snap to 200 on y axis
|
|
||||||
// x────────────x───────x
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │ │ A │
|
|
||||||
// │ │ │ x │
|
|
||||||
// │ drag │ │ │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
editor.pointerDown(50, 50, ids.boxT).pointerMove(50, 251, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 250 on y axis
|
|
||||||
// x─────────────────x
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// x───────┐ │ x │
|
|
||||||
// │ T │ │ │
|
|
||||||
// │ │ x───────x
|
|
||||||
// │ drag │
|
|
||||||
// └───────┘
|
|
||||||
editor.pointerMove(50, 301, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 250 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
|
|
||||||
// should snap to 300 on y axis
|
|
||||||
// x─────────────x───────x
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ drag │
|
|
||||||
// └───────┘
|
|
||||||
editor.pointerMove(50, 351, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 300 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 200 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(251, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 200, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 250 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// │ x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// x │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(301, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 250, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
|
|
||||||
// should snap to 300 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(351, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 300, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('becomes part of the selection bounding box if there is more than one shape in the selection', () => {
|
|
||||||
// ┌────────────────────────┐
|
|
||||||
// │ │
|
|
||||||
// │ x───────┐ │
|
|
||||||
// │ │ T │ │
|
|
||||||
// │ │ │ │
|
|
||||||
// │ │ │ │
|
|
||||||
// │ └───────┘ x │
|
|
||||||
// │ │ x───────x │
|
|
||||||
// │ │ │ A │ │
|
|
||||||
// │ │ │ x │ │
|
|
||||||
// │ │ │ │ │
|
|
||||||
// │ │ x───────x │
|
|
||||||
// │ │ │
|
|
||||||
// └───────────┼────────────┘
|
|
||||||
// │
|
|
||||||
// │ 450
|
|
||||||
// x───┼───x
|
|
||||||
// │ B │ │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
editor.select(ids.boxT, ids.box1)
|
|
||||||
editor.pointerDown(50, 50, ids.boxT).pointerMove(351, 50, { ctrlKey: true })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
expect(getSnapPoints(editor.snaps.getIndicators()![0])?.map(({ x }) => x)).toEqual([450, 450])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe.skip('custom snapping points', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
editor?.dispose()
|
|
||||||
editor = new TestEditor({
|
|
||||||
shapeUtils: [__TopLeftSnapOnlyShape],
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ B │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
})
|
|
||||||
editor.createShapes([
|
|
||||||
{
|
|
||||||
type: '__test_top_left_snap_only',
|
|
||||||
id: ids.boxT,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
props: { width: 100, height: 100 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'geo',
|
|
||||||
id: ids.box1,
|
|
||||||
x: 200,
|
|
||||||
y: 200,
|
|
||||||
props: { w: 100, h: 100 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'geo',
|
|
||||||
id: ids.box2,
|
|
||||||
x: 400,
|
|
||||||
y: 400,
|
|
||||||
props: { w: 100, h: 100 },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows other shapes to snap to custom snap points', () => {
|
|
||||||
// should snap to 0 on y axis
|
|
||||||
// x────────────x───────x
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │ │ A │
|
|
||||||
// │ │ │ x │
|
|
||||||
// │ │ │ drag │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
editor.pointerDown(250, 250, ids.box1).pointerMove(250, 51, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should not snap to 100 on y axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(250, 151, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 101 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should not snap to 50 on y axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │ x───────x
|
|
||||||
// │ │ │ A │
|
|
||||||
// └───────┘ │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(250, 101, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 200, y: 51 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should snap to 0 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ drag │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(51, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 0, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should not snap to 100 on x axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(151, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 101, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
|
|
||||||
// should not snap to 50 on x axis
|
|
||||||
// x───────┐
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ │
|
|
||||||
// └───────┘
|
|
||||||
//
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ drag │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(101, 250, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.box1)).toMatchObject({ x: 51, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows shapes with custom points to snap to other shapes', () => {
|
|
||||||
// should snap to 200 on y axis
|
|
||||||
// x────────────x───────x
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │ │ A │
|
|
||||||
// │ │ │ x │
|
|
||||||
// │ drag │ │ │
|
|
||||||
// └───────┘ x───────x
|
|
||||||
editor.pointerDown(50, 50, ids.boxT).pointerMove(50, 251, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 200 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 250 on y axis
|
|
||||||
// x─────────────────x
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// x───────┐ │ x │
|
|
||||||
// │ T │ │ │
|
|
||||||
// │ │ x───────x
|
|
||||||
// │ drag │
|
|
||||||
// └───────┘
|
|
||||||
editor.pointerMove(50, 301, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 250 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
|
|
||||||
// should snap to 300 on y axis
|
|
||||||
// x─────────────x───────x
|
|
||||||
// x───────x
|
|
||||||
// │ A │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────┐ x───────x
|
|
||||||
// │ T │
|
|
||||||
// │ │
|
|
||||||
// │ drag │
|
|
||||||
// └───────┘
|
|
||||||
editor.pointerMove(50, 351, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 0, y: 300 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 200 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(251, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 200, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
|
|
||||||
// should snap to 250 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// │ x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// x │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
editor.pointerMove(301, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 250, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
|
|
||||||
// should snap to 300 on x axis
|
|
||||||
// x x───────┐
|
|
||||||
// │ │ T │
|
|
||||||
// │ │ │
|
|
||||||
// │ │ drag │
|
|
||||||
// │ └───────┘
|
|
||||||
// │
|
|
||||||
// x x───────x
|
|
||||||
// │ │ A │
|
|
||||||
// │ │ x │
|
|
||||||
// │ │ │
|
|
||||||
// x x───────x
|
|
||||||
editor.pointerMove(351, 50, { ctrlKey: true })
|
|
||||||
expect(editor.getShape(ids.boxT)).toMatchObject({ x: 300, y: 0 })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('becomes part of the selection bounding box if there is more than one shape in the selection', () => {
|
|
||||||
// ┌────────────────────────┐
|
|
||||||
// │ │
|
|
||||||
// │ x───────┐ │
|
|
||||||
// │ │ T │ │
|
|
||||||
// │ │ │ │
|
|
||||||
// │ │ │ │
|
|
||||||
// │ └───────┘ x │
|
|
||||||
// │ │ x───────x │
|
|
||||||
// │ │ │ A │ │
|
|
||||||
// │ │ │ x │ │
|
|
||||||
// │ │ │ │ │
|
|
||||||
// │ │ x───────x │
|
|
||||||
// │ │ │
|
|
||||||
// └───────────┼────────────┘
|
|
||||||
// │
|
|
||||||
// │ 450
|
|
||||||
// x───┼───x
|
|
||||||
// │ B │ │
|
|
||||||
// │ x │
|
|
||||||
// │ │
|
|
||||||
// x───────x
|
|
||||||
editor.select(ids.boxT, ids.box1)
|
|
||||||
editor.pointerDown(50, 50, ids.boxT).pointerMove(351, 50, { ctrlKey: true })
|
|
||||||
expect(editor.snaps.getIndicators()?.length).toBe(1)
|
|
||||||
expect(getNumSnapPoints(editor.snaps.getIndicators()![0])).toBe(2)
|
|
||||||
expect(getSnapPoints(editor.snaps.getIndicators()![0])?.map(({ x }) => x)).toEqual([450, 450])
|
|
||||||
})
|
|
||||||
})
|
|
Loading…
Add table
Add a link
Reference in a new issue