arrows: separate out handle behavior from labels (#2621)

This is a followup on the arrows work.
- allow labels to go to the ends if no arrowhead is present
- avoid using / overloading TLHandle and use a new PointingLabel state
to specifically address label movement
- removes the feature flag to launch this feature!

### Change Type

- [x] `patch` — Bug fix

### Release Notes

- Arrow labels: provide more polish on label placement

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Mime Čuvalo 2024-01-31 11:17:03 +00:00 committed by GitHub
parent f87702bda4
commit 34a95b2ec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 572 additions and 445 deletions

View file

@ -969,9 +969,7 @@ export const EVENT_NAME_MAP: Record<Exclude<TLEventName, TLPinchEventName>, keyo
export function extractSessionStateFromLegacySnapshot(store: Record<string, UnknownRecord>): null | TLSessionStateSnapshot; export function extractSessionStateFromLegacySnapshot(store: Record<string, UnknownRecord>): null | TLSessionStateSnapshot;
// @internal (undocumented) // @internal (undocumented)
export const featureFlags: { export const featureFlags: Record<string, DebugFlag<boolean>>;
canMoveArrowLabel: DebugFlag<boolean>;
};
// @public (undocumented) // @public (undocumented)
export type GapsSnapLine = { export type GapsSnapLine = {
@ -1041,6 +1039,9 @@ export abstract class Geometry2d {
_vertices: undefined | Vec[]; _vertices: undefined | Vec[];
} }
// @public
export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number;
// @public (undocumented) // @public (undocumented)
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): { export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): {
start: Vec; start: Vec;
@ -1092,6 +1093,9 @@ export function getPointerInfo(e: PointerEvent | React.PointerEvent): {
isPen: boolean; isPen: boolean;
}; };
// @public
export function getPointInArcT(mAB: number, A: number, B: number, P: number): number;
// @public // @public
export function getPointOnCircle(center: VecLike, r: number, a: number): Vec; export function getPointOnCircle(center: VecLike, r: number, a: number): Vec;
@ -1661,8 +1665,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
onDropShapesOver?: TLOnDragHandler<Shape>; onDropShapesOver?: TLOnDragHandler<Shape>;
onEditEnd?: TLOnEditEndHandler<Shape>; onEditEnd?: TLOnEditEndHandler<Shape>;
onHandleDrag?: TLOnHandleDragHandler<Shape>; onHandleDrag?: TLOnHandleDragHandler<Shape>;
onHandleDragEnd?: TLOnHandleDragStartHandler<Shape>;
onHandleDragStart?: TLOnHandleDragStartHandler<Shape>;
onResize?: TLOnResizeHandler<Shape>; onResize?: TLOnResizeHandler<Shape>;
onResizeEnd?: TLOnResizeEndHandler<Shape>; onResizeEnd?: TLOnResizeEndHandler<Shape>;
onResizeStart?: TLOnResizeStartHandler<Shape>; onResizeStart?: TLOnResizeStartHandler<Shape>;
@ -2382,9 +2384,6 @@ export type TLOnHandleDragHandler<T extends TLShape> = (shape: T, info: {
initial?: T | undefined; initial?: T | undefined;
}) => TLShapePartial<T> | void; }) => TLShapePartial<T> | void;
// @public (undocumented)
export type TLOnHandleDragStartHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void;
// @public // @public
export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void; export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void;

View file

@ -21183,6 +21183,99 @@
], ],
"implementsTokenRanges": [] "implementsTokenRanges": []
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getArcMeasure:function(1)",
"docComment": "/**\n * Get the measure of an arc.\n *\n * @param A - The angle from center to arc's start point (A) on the circle\n *\n * @param B - The angle from center to arc's end point (B) on the circle\n *\n * @param sweepFlag - 1 if the arc is clockwise, 0 if counter-clockwise\n *\n * @param largeArcFlag - 1 if the arc is greater than 180 degrees, 0 if less than 180 degrees\n *\n * @returns The measure of the arc, negative if counter-clockwise\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getArcMeasure(A: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", B: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", sweepFlag: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", largeArcFlag: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/primitives/utils.ts",
"returnTypeTokenRange": {
"startIndex": 9,
"endIndex": 10
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "A",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "B",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
},
{
"parameterName": "sweepFlag",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "largeArcFlag",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
],
"name": "getArcMeasure"
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "@tldraw/editor!getArrowTerminalsInArrowSpace:function(1)", "canonicalReference": "@tldraw/editor!getArrowTerminalsInArrowSpace:function(1)",
@ -21898,6 +21991,99 @@
], ],
"name": "getPointerInfo" "name": "getPointerInfo"
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!getPointInArcT:function(1)",
"docComment": "/**\n * Returns the t value of the point on the arc.\n *\n * @param mAB - The measure of the arc from A to B, negative if counter-clockwise\n *\n * @param A - The angle from center to arc's start point (A) on the circle\n *\n * @param B - The angle from center to arc's end point (B) on the circle\n *\n * @param P - The angle on the circle (P) to find the t value for\n *\n * @returns The t value of the point on the arc, with 0 being the start and 1 being the end\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getPointInArcT(mAB: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", A: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", B: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ", P: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/primitives/utils.ts",
"returnTypeTokenRange": {
"startIndex": 9,
"endIndex": 10
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "mAB",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "A",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
},
{
"parameterName": "B",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
},
"isOptional": false
},
{
"parameterName": "P",
"parameterTypeTokenRange": {
"startIndex": 7,
"endIndex": 8
},
"isOptional": false
}
],
"name": "getPointInArcT"
},
{ {
"kind": "Function", "kind": "Function",
"canonicalReference": "@tldraw/editor!getPointOnCircle:function(1)", "canonicalReference": "@tldraw/editor!getPointOnCircle:function(1)",
@ -31338,76 +31524,6 @@
"isProtected": false, "isProtected": false,
"isAbstract": false "isAbstract": false
}, },
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#onHandleDragEnd:member",
"docComment": "/**\n * A callback called when a shape starts being dragged.\n *\n * @param shape - The shape.\n *\n * @returns A change to apply to the shape, or void.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "onHandleDragEnd?: "
},
{
"kind": "Reference",
"text": "TLOnHandleDragStartHandler",
"canonicalReference": "@tldraw/editor!TLOnHandleDragStartHandler:type"
},
{
"kind": "Content",
"text": "<Shape>"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onHandleDragEnd",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#onHandleDragStart:member",
"docComment": "/**\n * A callback called when a shape starts being dragged.\n *\n * @param shape - The shape.\n *\n * @returns A change to apply to the shape, or void.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "onHandleDragStart?: "
},
{
"kind": "Reference",
"text": "TLOnHandleDragStartHandler",
"canonicalReference": "@tldraw/editor!TLOnHandleDragStartHandler:type"
},
{
"kind": "Content",
"text": "<Shape>"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onHandleDragStart",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{ {
"kind": "Property", "kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#onResize:member", "canonicalReference": "@tldraw/editor!ShapeUtil#onResize:member",
@ -39551,63 +39667,6 @@
"endIndex": 8 "endIndex": 8
} }
}, },
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLOnHandleDragStartHandler:type",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type TLOnHandleDragStartHandler<T extends "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "> = "
},
{
"kind": "Content",
"text": "(shape: T) => "
},
{
"kind": "Reference",
"text": "TLShapePartial",
"canonicalReference": "@tldraw/tlschema!TLShapePartial:type"
},
{
"kind": "Content",
"text": "<T> | void"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/editor/shapes/ShapeUtil.ts",
"releaseTag": "Public",
"name": "TLOnHandleDragStartHandler",
"typeParameters": [
{
"typeParameterName": "T",
"constraintTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"defaultTypeTokenRange": {
"startIndex": 0,
"endIndex": 0
}
}
],
"typeTokenRange": {
"startIndex": 3,
"endIndex": 6
}
},
{ {
"kind": "TypeAlias", "kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLOnMountHandler:type", "canonicalReference": "@tldraw/editor!TLOnMountHandler:type",

View file

@ -525,16 +525,6 @@ input,
pointer-events: none; pointer-events: none;
} }
.tl-handle__text-adjust {
cursor: var(--tl-cursor-grab);
fill: transparent;
}
.tl-handle__text-adjust:hover {
stroke-width: calc(2px * var(--tl-scale));
stroke: var(--color-selection-stroke);
}
.tl-handle__create { .tl-handle__create {
opacity: 0; opacity: 0;
} }
@ -1153,6 +1143,10 @@ input,
align-items: center; align-items: center;
} }
.tl-arrow-label__inner p {
cursor: var(--tl-cursor-grab);
}
.tl-arrow-label p, .tl-arrow-label p,
.tl-arrow-label textarea { .tl-arrow-label textarea {
margin: 0px; margin: 0px;

View file

@ -175,7 +175,6 @@ export {
type TLOnDragHandler, type TLOnDragHandler,
type TLOnEditEndHandler, type TLOnEditEndHandler,
type TLOnHandleDragHandler, type TLOnHandleDragHandler,
type TLOnHandleDragStartHandler,
type TLOnResizeEndHandler, type TLOnResizeEndHandler,
type TLOnResizeHandler, type TLOnResizeHandler,
type TLOnResizeStartHandler, type TLOnResizeStartHandler,
@ -319,6 +318,8 @@ export {
clockwiseAngleDist, clockwiseAngleDist,
counterClockwiseAngleDist, counterClockwiseAngleDist,
degreesToRadians, degreesToRadians,
getArcMeasure,
getPointInArcT,
getPointOnCircle, getPointOnCircle,
getPolygonVertices, getPolygonVertices,
isSafeFloat, isSafeFloat,

View file

@ -14,14 +14,6 @@ export type TLHandleComponent = ComponentType<{
/** @public */ /** @public */
export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className, zoom }) => { export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className, zoom }) => {
if (handle.type === 'text-adjust') {
return (
<g className={classNames('tl-handle', 'tl-handle__text-adjust', className)}>
<rect rx={4} ry={4} width={handle.w} height={handle.h} />
</g>
)
}
const bgRadius = (isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoom const bgRadius = (isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoom
const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom

View file

@ -411,15 +411,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/ */
onTranslateEnd?: TLOnTranslateEndHandler<Shape> onTranslateEnd?: TLOnTranslateEndHandler<Shape>
/**
* A callback called when a shape starts being dragged.
*
* @param shape - The shape.
* @returns A change to apply to the shape, or void.
* @public
*/
onHandleDragStart?: TLOnHandleDragStartHandler<Shape>
/** /**
* A callback called when a shape's handle changes. * A callback called when a shape's handle changes.
* *
@ -430,15 +421,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/ */
onHandleDrag?: TLOnHandleDragHandler<Shape> onHandleDrag?: TLOnHandleDragHandler<Shape>
/**
* A callback called when a shape starts being dragged.
*
* @param shape - The shape.
* @returns A change to apply to the shape, or void.
* @public
*/
onHandleDragEnd?: TLOnHandleDragStartHandler<Shape>
/** /**
* A callback called when a shape starts being rotated. * A callback called when a shape starts being rotated.
* *
@ -604,9 +586,6 @@ export type TLOnBindingChangeHandler<T extends TLShape> = (shape: T) => TLShapeP
/** @public */ /** @public */
export type TLOnChildrenChangeHandler<T extends TLShape> = (shape: T) => TLShapePartial[] | void export type TLOnChildrenChangeHandler<T extends TLShape> = (shape: T) => TLShapePartial[] | void
/** @public */
export type TLOnHandleDragStartHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
/** @public */ /** @public */
export type TLOnHandleDragHandler<T extends TLShape> = ( export type TLOnHandleDragHandler<T extends TLShape> = (
shape: T, shape: T,
@ -617,9 +596,6 @@ export type TLOnHandleDragHandler<T extends TLShape> = (
} }
) => TLShapePartial<T> | void ) => TLShapePartial<T> | void
/** @public */
export type TLOnHandleDragEndHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
/** @public */ /** @public */
export type TLOnClickHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void export type TLOnClickHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
/** @public */ /** @public */

View file

@ -1,6 +1,6 @@
import { Vec } from '../Vec' import { Vec } from '../Vec'
import { intersectLineSegmentCircle } from '../intersect' import { intersectLineSegmentCircle } from '../intersect'
import { PI, PI2, getPointOnCircle, shortAngleDist } from '../utils' import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
import { Geometry2d, Geometry2dOptions } from './Geometry2d' import { Geometry2d, Geometry2dOptions } from './Geometry2d'
import { getVerticesCountForLength } from './geometry-constants' import { getVerticesCountForLength } from './geometry-constants'
@ -89,49 +89,3 @@ export class Arc2d extends Geometry2d {
return vertices return vertices
} }
} }
/**
* Returns the t value of the point on the arc.
*
* @param mAB - The measure of the arc from A to B, negative if counter-clockwise
* @param A - The angle from center to arc's start point (A) on the circle
* @param B - The angle from center to arc's end point (B) on the circle
* @param P - The angle on the circle (P) to find the t value for
*
* @returns The t value of the point on the arc, with 0 being the start and 1 being the end
*
* @public
*/
function getPointInArcT(mAB: number, A: number, B: number, P: number): number {
let mAP: number
if (Math.abs(mAB) > PI) {
mAP = shortAngleDist(A, P)
const mPB = shortAngleDist(P, B)
if (Math.abs(mAP) < Math.abs(mPB)) {
return mAP / mAB
} else {
return (mAB - mPB) / mAB
}
} else {
mAP = shortAngleDist(A, P)
return mAP / mAB
}
}
/**
* Get the measure of an arc.
*
* @param A The angle from center to arc's start point (A) on the circle
* @param B The angle from center to arc's end point (B) on the circle
* @param sweepFlag 1 if the arc is clockwise, 0 if counter-clockwise
* @param largeArcFlag 1 if the arc is greater than 180 degrees, 0 if less than 180 degrees
*
* @returns The measure of the arc, negative if counter-clockwise
*
* @public
*/
function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number) {
const m = ((2 * ((B - A) % PI2)) % PI2) - ((B - A) % PI2)
if (!largeArcFlag) return m
return (PI2 - Math.abs(m)) * (sweepFlag ? 1 : -1)
}

View file

@ -0,0 +1,59 @@
import { getPointInArcT } from './utils'
describe('getPointInArcT', () => {
it('should return 0 for the start of the arc', () => {
const mAB = Math.PI / 2 // 90 degrees
const A = 0 // Start angle
const B = Math.PI / 2 // End angle
const P = 0 // Point angle, same as start
expect(getPointInArcT(mAB, A, B, P)).toBe(0)
})
it('should return 1 for the end of the arc', () => {
const mAB = Math.PI / 2 // 90 degrees
const A = 0 // Start angle
const B = Math.PI / 2 // End angle
const P = Math.PI / 2 // Point angle, same as end
expect(getPointInArcT(mAB, A, B, P)).toBe(1)
})
it('should return 0.5 for the midpoint of the arc', () => {
const mAB = Math.PI // 180 degrees
const A = 0 // Start angle
const B = Math.PI // End angle
const P = Math.PI / 2 // Point angle, midpoint
expect(getPointInArcT(mAB, A, B, P)).toBe(0.5)
})
it('should handle negative arcs correctly', () => {
const mAB = -Math.PI / 2 // -90 degrees, counter-clockwise
const A = Math.PI / 2 // Start angle
const B = 0 // End angle
const P = Math.PI / 4 // Point angle, quarter way
expect(getPointInArcT(mAB, A, B, P)).toBe(0.5)
})
it('should return correct t value for arcs larger than PI', () => {
const mAB = Math.PI * 1.5 // 270 degrees
const A = 0 // Start angle
const B = -Math.PI / 2 // End angle, going counter-clockwise
const P = -Math.PI / 4 // Point angle, halfway
expect(getPointInArcT(mAB, A, B, P)).toBe(7 / 6)
})
it('should handle edge case where measurement to center is negative but measure to points near the end are positive', () => {
const mAB = -2.8 // Arc measure
const A = 0 // Start angle
const B = 2.2 // End angle
const P = 1.1 // Point angle, should be near the end
expect(getPointInArcT(mAB, A, B, P)).toBe(0)
})
it('should handle edge case where measurement to center is negative but measure to points near the end are positive with other endpoint', () => {
const mAB = 0 // Arc measure
const A = 0 // Start angle
const B = 2.2 // End angle
const P = 1.1 // Point angle, should be near the end
expect(getPointInArcT(mAB, A, B, P)).toBe(1)
})
})

View file

@ -372,3 +372,58 @@ export function angleDistance(fromAngle: number, toAngle: number, direction: num
: counterClockwiseAngleDist(fromAngle, toAngle) : counterClockwiseAngleDist(fromAngle, toAngle)
return dist return dist
} }
/**
* Returns the t value of the point on the arc.
*
* @param mAB - The measure of the arc from A to B, negative if counter-clockwise
* @param A - The angle from center to arc's start point (A) on the circle
* @param B - The angle from center to arc's end point (B) on the circle
* @param P - The angle on the circle (P) to find the t value for
*
* @returns The t value of the point on the arc, with 0 being the start and 1 being the end
*
* @public
*/
export function getPointInArcT(mAB: number, A: number, B: number, P: number): number {
let mAP: number
if (Math.abs(mAB) > PI) {
mAP = shortAngleDist(A, P)
const mPB = shortAngleDist(P, B)
if (Math.abs(mAP) < Math.abs(mPB)) {
return mAP / mAB
} else {
return (mAB - mPB) / mAB
}
} else {
mAP = shortAngleDist(A, P)
const t = mAP / mAB
// If the arc is something like -2.8 to 2.2, then we'll get a weird bug
// where the measurement to the center is negative but measure to points
// near the end are positive
if (Math.sign(mAP) !== Math.sign(mAB)) {
return Math.abs(t) > 0.5 ? 1 : 0
}
return t
}
}
/**
* Get the measure of an arc.
*
* @param A - The angle from center to arc's start point (A) on the circle
* @param B - The angle from center to arc's end point (B) on the circle
* @param sweepFlag - 1 if the arc is clockwise, 0 if counter-clockwise
* @param largeArcFlag - 1 if the arc is greater than 180 degrees, 0 if less than 180 degrees
*
* @returns The measure of the arc, negative if counter-clockwise
*
* @public
*/
export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number) {
const m = ((2 * ((B - A) % PI2)) % PI2) - ((B - A) % PI2)
if (!largeArcFlag) return m
return (PI2 - Math.abs(m)) * (sweepFlag ? 1 : -1)
}

View file

@ -7,9 +7,9 @@ import { Atom, atom, react } from '@tldraw/state'
// development. Use `createFeatureFlag` to create a boolean flag which will be // development. Use `createFeatureFlag` to create a boolean flag which will be
// `true` by default in development and staging, and `false` in production. // `true` by default in development and staging, and `false` in production.
/** @internal */ /** @internal */
export const featureFlags = { export const featureFlags: Record<string, DebugFlag<boolean>> = {
canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'), // canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'),
} satisfies Record<string, DebugFlag<boolean>> }
/** @internal */ /** @internal */
export const debugFlags = { export const debugFlags = {
@ -111,16 +111,16 @@ function createDebugValue<T>(
}) })
} }
function createFeatureFlag( // function createFeatureFlag(
name: string, // name: string,
defaults: Defaults<boolean> = { all: true, production: false } // defaults: Defaults<boolean> = { all: true, production: false }
) { // ) {
return createDebugValueBase({ // return createDebugValueBase({
name, // name,
defaults, // defaults,
shouldStoreForSession: true, // shouldStoreForSession: true,
}) // })
} // }
function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> { function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
const defaultValue = getDefaultValue(def) const defaultValue = getDefaultValue(def)

View file

@ -80,7 +80,6 @@ import { TLOnBeforeUpdateHandler } from '@tldraw/editor';
import { TLOnDoubleClickHandler } from '@tldraw/editor'; import { TLOnDoubleClickHandler } from '@tldraw/editor';
import { TLOnEditEndHandler } from '@tldraw/editor'; import { TLOnEditEndHandler } from '@tldraw/editor';
import { TLOnHandleDragHandler } from '@tldraw/editor'; import { TLOnHandleDragHandler } from '@tldraw/editor';
import { TLOnHandleDragStartHandler } from '@tldraw/editor';
import { TLOnResizeEndHandler } from '@tldraw/editor'; import { TLOnResizeEndHandler } from '@tldraw/editor';
import { TLOnResizeHandler } from '@tldraw/editor'; import { TLOnResizeHandler } from '@tldraw/editor';
import { TLOnTranslateHandler } from '@tldraw/editor'; import { TLOnTranslateHandler } from '@tldraw/editor';
@ -164,8 +163,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented) // (undocumented)
onHandleDrag: TLOnHandleDragHandler<TLArrowShape>; onHandleDrag: TLOnHandleDragHandler<TLArrowShape>;
// (undocumented) // (undocumented)
onHandleDragStart: TLOnHandleDragStartHandler<TLArrowShape>;
// (undocumented)
onResize: TLOnResizeHandler<TLArrowShape>; onResize: TLOnResizeHandler<TLArrowShape>;
// (undocumented) // (undocumented)
onTranslate?: TLOnTranslateHandler<TLArrowShape>; onTranslate?: TLOnTranslateHandler<TLArrowShape>;
@ -1082,7 +1079,7 @@ function Root({ id, children, modal, debugOpen, }: {
// @public (undocumented) // @public (undocumented)
export class SelectTool extends StateNode { export class SelectTool extends StateNode {
// (undocumented) // (undocumented)
static children: () => (typeof Brushing | typeof Crop | typeof Cropping | typeof DraggingHandle | typeof EditingShape | typeof Idle_11 | typeof PointingCanvas | typeof PointingCropHandle | typeof PointingHandle | typeof PointingResizeHandle | typeof PointingRotateHandle | typeof PointingSelection | typeof PointingShape | typeof Resizing | typeof Rotating | typeof ScribbleBrushing | typeof Translating)[]; static children: () => (typeof Brushing | typeof Crop | typeof Cropping | typeof DraggingHandle | typeof EditingShape | typeof Idle_11 | typeof PointingArrowLabel | typeof PointingCanvas | typeof PointingCropHandle | typeof PointingHandle | typeof PointingResizeHandle | typeof PointingRotateHandle | typeof PointingSelection | typeof PointingShape | typeof Resizing | typeof Rotating | typeof ScribbleBrushing | typeof Translating)[];
// (undocumented) // (undocumented)
static id: string; static id: string;
// (undocumented) // (undocumented)

View file

@ -1127,50 +1127,6 @@
"isProtected": false, "isProtected": false,
"isAbstract": false "isAbstract": false
}, },
{
"kind": "Property",
"canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onHandleDragStart:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "onHandleDragStart: "
},
{
"kind": "Reference",
"text": "TLOnHandleDragStartHandler",
"canonicalReference": "@tldraw/editor!TLOnHandleDragStartHandler:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLArrowShape",
"canonicalReference": "@tldraw/tlschema!TLArrowShape:type"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "onHandleDragStart",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{ {
"kind": "Property", "kind": "Property",
"canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onResize:member", "canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onResize:member",
@ -12681,6 +12637,15 @@
"kind": "Content", "kind": "Content",
"text": " | typeof " "text": " | typeof "
}, },
{
"kind": "Reference",
"text": "PointingArrowLabel",
"canonicalReference": "@tldraw/tldraw!~PointingArrowLabel:class"
},
{
"kind": "Content",
"text": " | typeof "
},
{ {
"kind": "Reference", "kind": "Reference",
"text": "PointingCanvas", "text": "PointingCanvas",
@ -12791,7 +12756,7 @@
"name": "children", "name": "children",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 36 "endIndex": 38
}, },
"isStatic": true, "isStatic": true,
"isProtected": false, "isProtected": false,

View file

@ -18,7 +18,6 @@ import {
TLHandle, TLHandle,
TLOnEditEndHandler, TLOnEditEndHandler,
TLOnHandleDragHandler, TLOnHandleDragHandler,
TLOnHandleDragStartHandler,
TLOnResizeHandler, TLOnResizeHandler,
TLOnTranslateHandler, TLOnTranslateHandler,
TLOnTranslateStartHandler, TLOnTranslateStartHandler,
@ -28,10 +27,7 @@ import {
Vec, Vec,
arrowShapeMigrations, arrowShapeMigrations,
arrowShapeProps, arrowShapeProps,
clockwiseAngleDist,
counterClockwiseAngleDist,
deepCopy, deepCopy,
featureFlags,
getArrowTerminalsInArrowSpace, getArrowTerminalsInArrowSpace,
getDefaultColorTheme, getDefaultColorTheme,
mapObjectMapValues, mapObjectMapValues,
@ -64,7 +60,6 @@ let globalRenderIndex = 0
enum ARROW_HANDLES { enum ARROW_HANDLES {
START = 'start', START = 'start',
MIDDLE = 'middle', MIDDLE = 'middle',
LABEL = 'middle-text',
END = 'end', END = 'end',
} }
@ -150,11 +145,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
override getHandles(shape: TLArrowShape): TLHandle[] { override getHandles(shape: TLArrowShape): TLHandle[] {
const info = this.editor.getArrowInfo(shape)! const info = this.editor.getArrowInfo(shape)!
const hasText = shape.props.text.trim()
const labelGeometry = hasText
? (this.editor.getShapeGeometry<Group2d>(shape).children[1] as Rectangle2d)
: null
return [ return [
{ {
id: ARROW_HANDLES.START, id: ARROW_HANDLES.START,
@ -172,17 +162,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
y: info.middle.y, y: info.middle.y,
canBind: false, canBind: false,
}, },
featureFlags.canMoveArrowLabel.get() &&
labelGeometry && {
id: ARROW_HANDLES.LABEL,
type: 'text-adjust',
index: 'a4',
x: labelGeometry.x,
y: labelGeometry.y,
w: labelGeometry.w,
h: labelGeometry.h,
canBind: false,
},
{ {
id: ARROW_HANDLES.END, id: ARROW_HANDLES.END,
type: 'vertex', type: 'vertex',
@ -194,19 +173,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
].filter(Boolean) as TLHandle[] ].filter(Boolean) as TLHandle[]
} }
private _labelDragOffset = new Vec(0, 0)
override onHandleDragStart: TLOnHandleDragStartHandler<TLArrowShape> = (shape) => {
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
const labelGeometry = geometry.children[1] as Rectangle2d
if (labelGeometry) {
const pointInShapeSpace = this.editor.getPointInShapeSpace(
shape,
this.editor.inputs.currentPagePoint
)
this._labelDragOffset = Vec.Sub(labelGeometry.center, pointInShapeSpace)
}
}
override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => { override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => {
const handleId = handle.id as ARROW_HANDLES const handleId = handle.id as ARROW_HANDLES
@ -227,42 +193,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
return { id: shape.id, type: shape.type, props: { bend } } return { id: shape.id, type: shape.type, props: { bend } }
} }
// This is for moving the text label to a different position on the arrow.
if (handleId === ARROW_HANDLES.LABEL) {
const next = deepCopy(shape) as TLArrowShape
const info = this.editor.getArrowInfo(shape)!
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
const lineGeometry = geometry.children[0] as Geometry2d
const pointInShapeSpace = this.editor.getPointInShapeSpace(
shape,
this.editor.inputs.currentPagePoint
)
const nearestPoint = lineGeometry.nearestPoint(
Vec.Add(pointInShapeSpace, this._labelDragOffset)
)
let nextLabelPosition
if (info.isStraight) {
const lineLength = Vec.Dist(info.start.point, info.end.point)
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
nextLabelPosition = 1 - segmentLength / lineLength
} else {
const isClockwise = shape.props.bend < 0
const distFn = isClockwise ? clockwiseAngleDist : counterClockwiseAngleDist
const angleCenterNearestPoint = Vec.Angle(info.handleArc.center, nearestPoint)
const angleCenterStart = Vec.Angle(info.handleArc.center, info.start.point)
const angleCenterEnd = Vec.Angle(info.handleArc.center, info.end.point)
const arcLength = distFn(angleCenterStart, angleCenterEnd)
const segmentLength = distFn(angleCenterNearestPoint, angleCenterEnd)
nextLabelPosition = 1 - segmentLength / arcLength
}
next.props.labelPosition = nextLabelPosition
return next
}
// Start or end, pointing the arrow... // Start or end, pointing the arrow...
const next = deepCopy(shape) as TLArrowShape const next = deepCopy(shape) as TLArrowShape

View file

@ -268,14 +268,28 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
const debugGeom: Geometry2d[] = [] const debugGeom: Geometry2d[] = []
const info = editor.getArrowInfo(shape)! const info = editor.getArrowInfo(shape)!
const hasStartArrowhead = info.start.arrowhead !== 'none'
const hasEndArrowhead = info.end.arrowhead !== 'none'
if (info.isStraight) { if (info.isStraight) {
const range = getStraightArrowLabelRange(editor, shape, info) const range = getStraightArrowLabelRange(editor, shape, info)
const clampedPosition = clamp(shape.props.labelPosition, range.start, range.end) let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
labelCenter = Vec.Lrp(info.start.point, info.end.point, clampedPosition) labelCenter = Vec.Lrp(info.start.point, info.end.point, clampedPosition)
} else { } else {
const range = getCurvedArrowLabelRange(editor, shape, info) const range = getCurvedArrowLabelRange(editor, shape, info)
if (range.dbg) debugGeom.push(...range.dbg) if (range.dbg) debugGeom.push(...range.dbg)
const clampedPosition = clamp(shape.props.labelPosition, range.start, range.end) let clampedPosition = clamp(
shape.props.labelPosition,
hasStartArrowhead ? range.start : 0,
hasEndArrowhead ? range.end : 1
)
// This makes the position snap in the middle.
clampedPosition = clampedPosition >= 0.48 && clampedPosition <= 0.52 ? 0.5 : clampedPosition
const labelAngle = interpolateArcAngles( const labelAngle = interpolateArcAngles(
Vec.Angle(info.bodyArc.center, info.start.point), Vec.Angle(info.bodyArc.center, info.start.point),
Vec.Angle(info.bodyArc.center, info.end.point), Vec.Angle(info.bodyArc.center, info.end.point),

View file

@ -5,6 +5,7 @@ import { Cropping } from './childStates/Cropping'
import { DraggingHandle } from './childStates/DraggingHandle' import { DraggingHandle } from './childStates/DraggingHandle'
import { EditingShape } from './childStates/EditingShape' import { EditingShape } from './childStates/EditingShape'
import { Idle } from './childStates/Idle' import { Idle } from './childStates/Idle'
import { PointingArrowLabel } from './childStates/PointingArrowLabel'
import { PointingCanvas } from './childStates/PointingCanvas' import { PointingCanvas } from './childStates/PointingCanvas'
import { PointingCropHandle } from './childStates/PointingCropHandle' import { PointingCropHandle } from './childStates/PointingCropHandle'
import { PointingHandle } from './childStates/PointingHandle' import { PointingHandle } from './childStates/PointingHandle'
@ -39,6 +40,7 @@ export class SelectTool extends StateNode {
Resizing, Resizing,
Rotating, Rotating,
PointingRotateHandle, PointingRotateHandle,
PointingArrowLabel,
PointingHandle, PointingHandle,
DraggingHandle, DraggingHandle,
] ]

View file

@ -11,7 +11,6 @@ import {
TLPointerEventInfo, TLPointerEventInfo,
TLShapeId, TLShapeId,
TLShapePartial, TLShapePartial,
TLUnknownShape,
Vec, Vec,
deepCopy, deepCopy,
snapAngle, snapAngle,
@ -110,15 +109,6 @@ export class DraggingHandle extends StateNode {
} }
// --> // -->
const util = this.editor.getShapeUtil(shape)
const changes = util.onHandleDragStart?.(shape)
const next: TLShapePartial<any> = { ...shape, ...changes }
if (changes) {
this.editor.updateShapes([next], { squashing: true })
}
this.update() this.update()
this.editor.select(this.shapeId) this.editor.select(this.shapeId)
@ -180,18 +170,6 @@ export class DraggingHandle extends StateNode {
this.editor.setHintingShapes([]) this.editor.setHintingShapes([])
this.editor.snaps.clear() this.editor.snaps.clear()
const { editor, shapeId } = this
const shape = editor.getShape(shapeId) as TLArrowShape | (TLUnknownShape & TLArrowShape)
if (shape) {
const util = this.editor.getShapeUtil(shape)
const changes = util.onHandleDragEnd?.(shape)
const next: TLShapePartial<any> = { ...shape, ...changes }
if (changes) {
this.editor.updateShapes([next], { squashing: true })
}
}
this.editor.updateInstanceState( this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } }, { cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true } { ephemeral: true }

View file

@ -1,7 +1,9 @@
import { import {
Editor, Editor,
Group2d,
HIT_TEST_MARGIN, HIT_TEST_MARGIN,
StateNode, StateNode,
TLArrowShape,
TLClickEventInfo, TLClickEventInfo,
TLEventHandlers, TLEventHandlers,
TLGroupShape, TLGroupShape,
@ -90,7 +92,25 @@ export class Idle extends StateNode {
break break
} }
case 'shape': { case 'shape': {
if (this.editor.isShapeOrAncestorLocked(info.shape)) { const { shape } = info
const pointInShapeSpace = this.editor.getPointInShapeSpace(
shape,
this.editor.inputs.currentPagePoint
)
// todo: Extract into general hit test for arrows
if (this.editor.isShapeOfType<TLArrowShape>(shape, 'arrow')) {
// How should we handle multiple labels? Do shapes ever have multiple labels?
const labelGeometry = this.editor.getShapeGeometry<Group2d>(shape).children[1]
// Knowing what we know about arrows... if the shape has no text in its label,
// then the label geometry should not be there.
if (labelGeometry && pointInPolygon(pointInShapeSpace, labelGeometry.vertices)) {
// We're moving the label on a shape.
this.parent.transition('pointing_arrow_label', info)
break
}
}
if (this.editor.isShapeOrAncestorLocked(shape)) {
this.parent.transition('pointing_canvas', info) this.parent.transition('pointing_canvas', info)
break break
} }

View file

@ -0,0 +1,143 @@
import {
Arc2d,
Geometry2d,
Group2d,
StateNode,
TLArrowShape,
TLEventHandlers,
TLPointerEventInfo,
TLShapeId,
Vec,
getPointInArcT,
} from '@tldraw/editor'
export class PointingArrowLabel extends StateNode {
static override id = 'pointing_arrow_label'
shapeId = '' as TLShapeId
markId = ''
private info = {} as TLPointerEventInfo & {
shape: TLArrowShape
onInteractionEnd?: string
isCreating: boolean
}
private updateCursor() {
this.editor.updateInstanceState({
cursor: {
type: 'grabbing',
rotation: 0,
},
})
}
override onEnter = (
info: TLPointerEventInfo & {
shape: TLArrowShape
onInteractionEnd?: string
isCreating: boolean
}
) => {
const { shape } = info
this.parent.setCurrentToolIdMask(info.onInteractionEnd)
this.info = info
this.shapeId = shape.id
this.updateCursor()
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
const labelGeometry = geometry.children[1]
if (!labelGeometry) {
throw Error(`Expected to find an arrow label geometry for shape: ${shape.id}`)
}
const { currentPagePoint } = this.editor.inputs
const pointInShapeSpace = this.editor.getPointInShapeSpace(shape, currentPagePoint)
this._labelDragOffset = Vec.Sub(labelGeometry.center, pointInShapeSpace)
this.markId = 'label-drag start'
this.editor.mark(this.markId)
this.editor.setSelectedShapes([this.shapeId])
}
override onExit = () => {
this.parent.setCurrentToolIdMask(undefined)
this.editor.updateInstanceState(
{ cursor: { type: 'default', rotation: 0 } },
{ ephemeral: true }
)
}
private _labelDragOffset = new Vec(0, 0)
override onPointerMove = () => {
const { isDragging } = this.editor.inputs
if (!isDragging) return
const shape = this.editor.getShape<TLArrowShape>(this.shapeId)
if (!shape) return
const info = this.editor.getArrowInfo(shape)!
const groupGeometry = this.editor.getShapeGeometry<Group2d>(shape)
const bodyGeometry = groupGeometry.children[0] as Geometry2d
const pointInShapeSpace = this.editor.getPointInShapeSpace(
shape,
this.editor.inputs.currentPagePoint
)
const nearestPoint = bodyGeometry.nearestPoint(
Vec.Add(pointInShapeSpace, this._labelDragOffset)
)
let nextLabelPosition
if (info.isStraight) {
// straight arrows
const lineLength = Vec.Dist(info.start.point, info.end.point)
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
nextLabelPosition = 1 - segmentLength / lineLength
} else {
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
nextLabelPosition = getPointInArcT(measure, angleStart, angleEnd, _center.angle(nearestPoint))
}
this.editor.updateShape<TLArrowShape>(
{ id: shape.id, type: shape.type, props: { labelPosition: nextLabelPosition } },
{ squashing: true }
)
}
override onPointerUp = () => {
this.complete()
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.cancel()
}
override onComplete: TLEventHandlers['onComplete'] = () => {
this.cancel()
}
override onInterrupt = () => {
this.cancel()
}
private complete() {
if (this.info.onInteractionEnd) {
this.editor.setCurrentTool(this.info.onInteractionEnd, {})
} else {
this.parent.transition('idle')
}
}
private cancel() {
this.editor.bailToMark(this.markId)
if (this.info.onInteractionEnd) {
this.editor.setCurrentTool(this.info.onInteractionEnd, {})
} else {
this.parent.transition('idle')
}
}
}

View file

@ -7,6 +7,7 @@ const ids = {
box1: createShapeId('box1'), box1: createShapeId('box1'),
line1: createShapeId('line1'), line1: createShapeId('line1'),
embed1: createShapeId('embed1'), embed1: createShapeId('embed1'),
arrow1: createShapeId('arrow1'),
} }
jest.useFakeTimers() jest.useFakeTimers()
@ -166,6 +167,56 @@ describe('DraggingHandle', () => {
}) })
}) })
describe('PointingLabel', () => {
it('Enters from pointing_arrow_label and exits to idle', () => {
editor.createShapes([
{ id: ids.arrow1, type: 'arrow', x: 100, y: 100, props: { text: 'Test Label' } },
])
const shape = editor.getShape(ids.arrow1)
editor.pointerDown(150, 150, {
target: 'shape',
shape,
})
editor.pointerMove(100, 100)
editor.expectToBeIn('select.pointing_arrow_label')
editor.pointerUp()
editor.expectToBeIn('select.idle')
})
it('Bails on escape', () => {
editor.createShapes([
{ id: ids.arrow1, type: 'arrow', x: 100, y: 100, props: { text: 'Test Label' } },
])
const shape = editor.getShape(ids.arrow1)
editor.pointerDown(150, 150, {
target: 'shape',
shape,
})
editor.pointerMove(100, 100)
editor.expectToBeIn('select.pointing_arrow_label')
editor.cancel()
editor.expectToBeIn('select.idle')
})
it('Doesnt go into pointing_arrow_label mode if not selecting the arrow shape', () => {
editor.createShapes([
{ id: ids.arrow1, type: 'arrow', x: 100, y: 100, props: { text: 'Test Label' } },
])
const shape = editor.getShape(ids.arrow1)
editor.pointerDown(0, 150, {
target: 'shape',
shape,
})
editor.pointerMove(100, 100)
editor.expectToBeIn('select.translating')
editor.pointerUp()
editor.expectToBeIn('select.idle')
})
})
describe('When double clicking a shape', () => { describe('When double clicking a shape', () => {
it('begins editing a geo shapes label', () => { it('begins editing a geo shapes label', () => {
editor editor

View file

@ -960,16 +960,12 @@ export interface TLHandle {
canBind?: boolean; canBind?: boolean;
// (undocumented) // (undocumented)
canSnap?: boolean; canSnap?: boolean;
// (undocumented)
h?: number;
id: string; id: string;
// (undocumented) // (undocumented)
index: string; index: string;
// (undocumented) // (undocumented)
type: TLHandleType; type: TLHandleType;
// (undocumented) // (undocumented)
w?: number;
// (undocumented)
x: number; x: number;
// (undocumented) // (undocumented)
y: number; y: number;

View file

@ -6027,33 +6027,6 @@
"endIndex": 2 "endIndex": 2
} }
}, },
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/tlschema!TLHandle#h:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "h?: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "h",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{ {
"kind": "PropertySignature", "kind": "PropertySignature",
"canonicalReference": "@tldraw/tlschema!TLHandle#id:member", "canonicalReference": "@tldraw/tlschema!TLHandle#id:member",
@ -6136,33 +6109,6 @@
"endIndex": 2 "endIndex": 2
} }
}, },
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/tlschema!TLHandle#w:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "w?: "
},
{
"kind": "Content",
"text": "number"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "w",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
},
{ {
"kind": "PropertySignature", "kind": "PropertySignature",
"canonicalReference": "@tldraw/tlschema!TLHandle#x:member", "canonicalReference": "@tldraw/tlschema!TLHandle#x:member",

View file

@ -5,7 +5,7 @@ import { SetValue } from '../util-types'
* The handle types used by tldraw's default shapes. * The handle types used by tldraw's default shapes.
* *
* @public */ * @public */
export const TL_HANDLE_TYPES = new Set(['vertex', 'virtual', 'create', 'text-adjust'] as const) export const TL_HANDLE_TYPES = new Set(['vertex', 'virtual', 'create'] as const)
/** /**
* A type for the handle types used by tldraw's default shapes. * A type for the handle types used by tldraw's default shapes.
@ -27,8 +27,6 @@ export interface TLHandle {
index: string index: string
x: number x: number
y: number y: number
w?: number
h?: number
} }
/** @internal */ /** @internal */
@ -40,6 +38,4 @@ export const handleValidator: T.Validator<TLHandle> = T.object({
index: T.string, index: T.string,
x: T.number, x: T.number,
y: T.number, y: T.number,
w: T.optional(T.number),
h: T.optional(T.number),
}) })