From 34a95b2ec8811fc50eaf74a9a4139909e9b834b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mime=20=C4=8Cuvalo?= Date: Wed, 31 Jan 2024 11:17:03 +0000 Subject: [PATCH] arrows: separate out handle behavior from labels (#2621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/editor/api-report.md | 15 +- packages/editor/api/api.json | 313 +++++++++++------- packages/editor/editor.css | 14 +- packages/editor/src/index.ts | 3 +- .../default-components/DefaultHandle.tsx | 8 - .../editor/src/lib/editor/shapes/ShapeUtil.ts | 24 -- .../src/lib/primitives/geometry/Arc2d.ts | 48 +-- .../editor/src/lib/primitives/utils.test.ts | 59 ++++ packages/editor/src/lib/primitives/utils.ts | 55 +++ packages/editor/src/lib/utils/debug-flags.ts | 26 +- packages/tldraw/api-report.md | 5 +- packages/tldraw/api/api.json | 55 +-- .../src/lib/shapes/arrow/ArrowShapeUtil.tsx | 70 ---- .../tldraw/src/lib/shapes/arrow/arrowLabel.ts | 18 +- .../src/lib/tools/SelectTool/SelectTool.ts | 2 + .../SelectTool/childStates/DraggingHandle.ts | 22 -- .../lib/tools/SelectTool/childStates/Idle.ts | 22 +- .../childStates/PointingArrowLabel.ts | 143 ++++++++ packages/tldraw/src/test/SelectTool.test.ts | 51 +++ packages/tlschema/api-report.md | 4 - packages/tlschema/api/api.json | 54 --- packages/tlschema/src/misc/TLHandle.ts | 6 +- 22 files changed, 572 insertions(+), 445 deletions(-) create mode 100644 packages/editor/src/lib/primitives/utils.test.ts create mode 100644 packages/tldraw/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index faa1ec593..b519170f5 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -969,9 +969,7 @@ export const EVENT_NAME_MAP: Record, keyo export function extractSessionStateFromLegacySnapshot(store: Record): null | TLSessionStateSnapshot; // @internal (undocumented) -export const featureFlags: { - canMoveArrowLabel: DebugFlag; -}; +export const featureFlags: Record>; // @public (undocumented) export type GapsSnapLine = { @@ -1041,6 +1039,9 @@ export abstract class Geometry2d { _vertices: undefined | Vec[]; } +// @public +export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number; + // @public (undocumented) export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): { start: Vec; @@ -1092,6 +1093,9 @@ export function getPointerInfo(e: PointerEvent | React.PointerEvent): { isPen: boolean; }; +// @public +export function getPointInArcT(mAB: number, A: number, B: number, P: number): number; + // @public export function getPointOnCircle(center: VecLike, r: number, a: number): Vec; @@ -1661,8 +1665,6 @@ export abstract class ShapeUtil { onDropShapesOver?: TLOnDragHandler; onEditEnd?: TLOnEditEndHandler; onHandleDrag?: TLOnHandleDragHandler; - onHandleDragEnd?: TLOnHandleDragStartHandler; - onHandleDragStart?: TLOnHandleDragStartHandler; onResize?: TLOnResizeHandler; onResizeEnd?: TLOnResizeEndHandler; onResizeStart?: TLOnResizeStartHandler; @@ -2382,9 +2384,6 @@ export type TLOnHandleDragHandler = (shape: T, info: { initial?: T | undefined; }) => TLShapePartial | void; -// @public (undocumented) -export type TLOnHandleDragStartHandler = (shape: T) => TLShapePartial | void; - // @public export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 9435b47a7..0f64ca540 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -21183,6 +21183,99 @@ ], "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", "canonicalReference": "@tldraw/editor!getArrowTerminalsInArrowSpace:function(1)", @@ -21898,6 +21991,99 @@ ], "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", "canonicalReference": "@tldraw/editor!getPointOnCircle:function(1)", @@ -31338,76 +31524,6 @@ "isProtected": 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": "" - }, - { - "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": "" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": false, - "isOptional": true, - "releaseTag": "Public", - "name": "onHandleDragStart", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - }, - "isStatic": false, - "isProtected": false, - "isAbstract": false - }, { "kind": "Property", "canonicalReference": "@tldraw/editor!ShapeUtil#onResize:member", @@ -39551,63 +39667,6 @@ "endIndex": 8 } }, - { - "kind": "TypeAlias", - "canonicalReference": "@tldraw/editor!TLOnHandleDragStartHandler:type", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type TLOnHandleDragStartHandler = " - }, - { - "kind": "Content", - "text": "(shape: T) => " - }, - { - "kind": "Reference", - "text": "TLShapePartial", - "canonicalReference": "@tldraw/tlschema!TLShapePartial:type" - }, - { - "kind": "Content", - "text": " | 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", "canonicalReference": "@tldraw/editor!TLOnMountHandler:type", diff --git a/packages/editor/editor.css b/packages/editor/editor.css index 44ebdcbcd..eb011ecb7 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -525,16 +525,6 @@ input, 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 { opacity: 0; } @@ -1153,6 +1143,10 @@ input, align-items: center; } +.tl-arrow-label__inner p { + cursor: var(--tl-cursor-grab); +} + .tl-arrow-label p, .tl-arrow-label textarea { margin: 0px; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 8d48b3155..0e85d8e63 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -175,7 +175,6 @@ export { type TLOnDragHandler, type TLOnEditEndHandler, type TLOnHandleDragHandler, - type TLOnHandleDragStartHandler, type TLOnResizeEndHandler, type TLOnResizeHandler, type TLOnResizeStartHandler, @@ -319,6 +318,8 @@ export { clockwiseAngleDist, counterClockwiseAngleDist, degreesToRadians, + getArcMeasure, + getPointInArcT, getPointOnCircle, getPolygonVertices, isSafeFloat, diff --git a/packages/editor/src/lib/components/default-components/DefaultHandle.tsx b/packages/editor/src/lib/components/default-components/DefaultHandle.tsx index 5e110b97f..d24500dd2 100644 --- a/packages/editor/src/lib/components/default-components/DefaultHandle.tsx +++ b/packages/editor/src/lib/components/default-components/DefaultHandle.tsx @@ -14,14 +14,6 @@ export type TLHandleComponent = ComponentType<{ /** @public */ export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className, zoom }) => { - if (handle.type === 'text-adjust') { - return ( - - - - ) - } - const bgRadius = (isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoom const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom diff --git a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts index 064c4679b..894ae80b1 100644 --- a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts +++ b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts @@ -411,15 +411,6 @@ export abstract class ShapeUtil { */ onTranslateEnd?: TLOnTranslateEndHandler - /** - * 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 - /** * A callback called when a shape's handle changes. * @@ -430,15 +421,6 @@ export abstract class ShapeUtil { */ onHandleDrag?: TLOnHandleDragHandler - /** - * 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 - /** * A callback called when a shape starts being rotated. * @@ -604,9 +586,6 @@ export type TLOnBindingChangeHandler = (shape: T) => TLShapeP /** @public */ export type TLOnChildrenChangeHandler = (shape: T) => TLShapePartial[] | void -/** @public */ -export type TLOnHandleDragStartHandler = (shape: T) => TLShapePartial | void - /** @public */ export type TLOnHandleDragHandler = ( shape: T, @@ -617,9 +596,6 @@ export type TLOnHandleDragHandler = ( } ) => TLShapePartial | void -/** @public */ -export type TLOnHandleDragEndHandler = (shape: T) => TLShapePartial | void - /** @public */ export type TLOnClickHandler = (shape: T) => TLShapePartial | void /** @public */ diff --git a/packages/editor/src/lib/primitives/geometry/Arc2d.ts b/packages/editor/src/lib/primitives/geometry/Arc2d.ts index d74aef2be..5feee17ef 100644 --- a/packages/editor/src/lib/primitives/geometry/Arc2d.ts +++ b/packages/editor/src/lib/primitives/geometry/Arc2d.ts @@ -1,6 +1,6 @@ import { Vec } from '../Vec' import { intersectLineSegmentCircle } from '../intersect' -import { PI, PI2, getPointOnCircle, shortAngleDist } from '../utils' +import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils' import { Geometry2d, Geometry2dOptions } from './Geometry2d' import { getVerticesCountForLength } from './geometry-constants' @@ -89,49 +89,3 @@ export class Arc2d extends Geometry2d { 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) -} diff --git a/packages/editor/src/lib/primitives/utils.test.ts b/packages/editor/src/lib/primitives/utils.test.ts new file mode 100644 index 000000000..7a7262b94 --- /dev/null +++ b/packages/editor/src/lib/primitives/utils.test.ts @@ -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) + }) +}) diff --git a/packages/editor/src/lib/primitives/utils.ts b/packages/editor/src/lib/primitives/utils.ts index d02eb105c..a4f163719 100644 --- a/packages/editor/src/lib/primitives/utils.ts +++ b/packages/editor/src/lib/primitives/utils.ts @@ -372,3 +372,58 @@ export function angleDistance(fromAngle: number, toAngle: number, direction: num : counterClockwiseAngleDist(fromAngle, toAngle) 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) +} diff --git a/packages/editor/src/lib/utils/debug-flags.ts b/packages/editor/src/lib/utils/debug-flags.ts index 7a1e55f1f..589f5ee23 100644 --- a/packages/editor/src/lib/utils/debug-flags.ts +++ b/packages/editor/src/lib/utils/debug-flags.ts @@ -7,9 +7,9 @@ import { Atom, atom, react } from '@tldraw/state' // development. Use `createFeatureFlag` to create a boolean flag which will be // `true` by default in development and staging, and `false` in production. /** @internal */ -export const featureFlags = { - canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'), -} satisfies Record> +export const featureFlags: Record> = { + // canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'), +} /** @internal */ export const debugFlags = { @@ -111,16 +111,16 @@ function createDebugValue( }) } -function createFeatureFlag( - name: string, - defaults: Defaults = { all: true, production: false } -) { - return createDebugValueBase({ - name, - defaults, - shouldStoreForSession: true, - }) -} +// function createFeatureFlag( +// name: string, +// defaults: Defaults = { all: true, production: false } +// ) { +// return createDebugValueBase({ +// name, +// defaults, +// shouldStoreForSession: true, +// }) +// } function createDebugValueBase(def: DebugFlagDef): DebugFlag { const defaultValue = getDefaultValue(def) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 8f504a08a..8f6d6266d 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -80,7 +80,6 @@ import { TLOnBeforeUpdateHandler } from '@tldraw/editor'; import { TLOnDoubleClickHandler } from '@tldraw/editor'; import { TLOnEditEndHandler } from '@tldraw/editor'; import { TLOnHandleDragHandler } from '@tldraw/editor'; -import { TLOnHandleDragStartHandler } from '@tldraw/editor'; import { TLOnResizeEndHandler } from '@tldraw/editor'; import { TLOnResizeHandler } from '@tldraw/editor'; import { TLOnTranslateHandler } from '@tldraw/editor'; @@ -164,8 +163,6 @@ export class ArrowShapeUtil extends ShapeUtil { // (undocumented) onHandleDrag: TLOnHandleDragHandler; // (undocumented) - onHandleDragStart: TLOnHandleDragStartHandler; - // (undocumented) onResize: TLOnResizeHandler; // (undocumented) onTranslate?: TLOnTranslateHandler; @@ -1082,7 +1079,7 @@ function Root({ id, children, modal, debugOpen, }: { // @public (undocumented) export class SelectTool extends StateNode { // (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) static id: string; // (undocumented) diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 4f99dbe77..b4b645901 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -1127,50 +1127,6 @@ "isProtected": 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", "canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onResize:member", @@ -12681,6 +12637,15 @@ "kind": "Content", "text": " | typeof " }, + { + "kind": "Reference", + "text": "PointingArrowLabel", + "canonicalReference": "@tldraw/tldraw!~PointingArrowLabel:class" + }, + { + "kind": "Content", + "text": " | typeof " + }, { "kind": "Reference", "text": "PointingCanvas", @@ -12791,7 +12756,7 @@ "name": "children", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 36 + "endIndex": 38 }, "isStatic": true, "isProtected": false, diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx index 05231db7b..fc7d7013d 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx @@ -18,7 +18,6 @@ import { TLHandle, TLOnEditEndHandler, TLOnHandleDragHandler, - TLOnHandleDragStartHandler, TLOnResizeHandler, TLOnTranslateHandler, TLOnTranslateStartHandler, @@ -28,10 +27,7 @@ import { Vec, arrowShapeMigrations, arrowShapeProps, - clockwiseAngleDist, - counterClockwiseAngleDist, deepCopy, - featureFlags, getArrowTerminalsInArrowSpace, getDefaultColorTheme, mapObjectMapValues, @@ -64,7 +60,6 @@ let globalRenderIndex = 0 enum ARROW_HANDLES { START = 'start', MIDDLE = 'middle', - LABEL = 'middle-text', END = 'end', } @@ -150,11 +145,6 @@ export class ArrowShapeUtil extends ShapeUtil { override getHandles(shape: TLArrowShape): TLHandle[] { const info = this.editor.getArrowInfo(shape)! - const hasText = shape.props.text.trim() - const labelGeometry = hasText - ? (this.editor.getShapeGeometry(shape).children[1] as Rectangle2d) - : null - return [ { id: ARROW_HANDLES.START, @@ -172,17 +162,6 @@ export class ArrowShapeUtil extends ShapeUtil { y: info.middle.y, 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, type: 'vertex', @@ -194,19 +173,6 @@ export class ArrowShapeUtil extends ShapeUtil { ].filter(Boolean) as TLHandle[] } - private _labelDragOffset = new Vec(0, 0) - override onHandleDragStart: TLOnHandleDragStartHandler = (shape) => { - const geometry = this.editor.getShapeGeometry(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 = (shape, { handle, isPrecise }) => { const handleId = handle.id as ARROW_HANDLES @@ -227,42 +193,6 @@ export class ArrowShapeUtil extends ShapeUtil { 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(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... const next = deepCopy(shape) as TLArrowShape diff --git a/packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts b/packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts index 32f5b9d18..bd496a1d9 100644 --- a/packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts +++ b/packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts @@ -268,14 +268,28 @@ export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) { const debugGeom: Geometry2d[] = [] const info = editor.getArrowInfo(shape)! + const hasStartArrowhead = info.start.arrowhead !== 'none' + const hasEndArrowhead = info.end.arrowhead !== 'none' if (info.isStraight) { 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) } else { const range = getCurvedArrowLabelRange(editor, shape, info) 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( Vec.Angle(info.bodyArc.center, info.start.point), Vec.Angle(info.bodyArc.center, info.end.point), diff --git a/packages/tldraw/src/lib/tools/SelectTool/SelectTool.ts b/packages/tldraw/src/lib/tools/SelectTool/SelectTool.ts index 82a03cc51..6c5b288bd 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/SelectTool.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/SelectTool.ts @@ -5,6 +5,7 @@ import { Cropping } from './childStates/Cropping' import { DraggingHandle } from './childStates/DraggingHandle' import { EditingShape } from './childStates/EditingShape' import { Idle } from './childStates/Idle' +import { PointingArrowLabel } from './childStates/PointingArrowLabel' import { PointingCanvas } from './childStates/PointingCanvas' import { PointingCropHandle } from './childStates/PointingCropHandle' import { PointingHandle } from './childStates/PointingHandle' @@ -39,6 +40,7 @@ export class SelectTool extends StateNode { Resizing, Rotating, PointingRotateHandle, + PointingArrowLabel, PointingHandle, DraggingHandle, ] diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.ts index 338c5b71c..ed9b48bdb 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.ts @@ -11,7 +11,6 @@ import { TLPointerEventInfo, TLShapeId, TLShapePartial, - TLUnknownShape, Vec, deepCopy, snapAngle, @@ -110,15 +109,6 @@ export class DraggingHandle extends StateNode { } // --> - const util = this.editor.getShapeUtil(shape) - const changes = util.onHandleDragStart?.(shape) - - const next: TLShapePartial = { ...shape, ...changes } - - if (changes) { - this.editor.updateShapes([next], { squashing: true }) - } - this.update() this.editor.select(this.shapeId) @@ -180,18 +170,6 @@ export class DraggingHandle extends StateNode { this.editor.setHintingShapes([]) 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 = { ...shape, ...changes } - if (changes) { - this.editor.updateShapes([next], { squashing: true }) - } - } - this.editor.updateInstanceState( { cursor: { type: 'default', rotation: 0 } }, { ephemeral: true } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts index 45ce0509f..48ab99699 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Idle.ts @@ -1,7 +1,9 @@ import { Editor, + Group2d, HIT_TEST_MARGIN, StateNode, + TLArrowShape, TLClickEventInfo, TLEventHandlers, TLGroupShape, @@ -90,7 +92,25 @@ export class Idle extends StateNode { break } 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(shape, 'arrow')) { + // How should we handle multiple labels? Do shapes ever have multiple labels? + const labelGeometry = this.editor.getShapeGeometry(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) break } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts new file mode 100644 index 000000000..f99dfa5d0 --- /dev/null +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts @@ -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(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(this.shapeId) + if (!shape) return + + const info = this.editor.getArrowInfo(shape)! + + const groupGeometry = this.editor.getShapeGeometry(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( + { 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') + } + } +} diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index 8f66224fa..1b2f918e3 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -7,6 +7,7 @@ const ids = { box1: createShapeId('box1'), line1: createShapeId('line1'), embed1: createShapeId('embed1'), + arrow1: createShapeId('arrow1'), } 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', () => { it('begins editing a geo shapes label', () => { editor diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 312076174..78e4948bf 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -960,16 +960,12 @@ export interface TLHandle { canBind?: boolean; // (undocumented) canSnap?: boolean; - // (undocumented) - h?: number; id: string; // (undocumented) index: string; // (undocumented) type: TLHandleType; // (undocumented) - w?: number; - // (undocumented) x: number; // (undocumented) y: number; diff --git a/packages/tlschema/api/api.json b/packages/tlschema/api/api.json index 290aa233f..c0908dbe8 100644 --- a/packages/tlschema/api/api.json +++ b/packages/tlschema/api/api.json @@ -6027,33 +6027,6 @@ "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", "canonicalReference": "@tldraw/tlschema!TLHandle#id:member", @@ -6136,33 +6109,6 @@ "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", "canonicalReference": "@tldraw/tlschema!TLHandle#x:member", diff --git a/packages/tlschema/src/misc/TLHandle.ts b/packages/tlschema/src/misc/TLHandle.ts index 02f2dc374..2fcb04939 100644 --- a/packages/tlschema/src/misc/TLHandle.ts +++ b/packages/tlschema/src/misc/TLHandle.ts @@ -5,7 +5,7 @@ import { SetValue } from '../util-types' * The handle types used by tldraw's default shapes. * * @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. @@ -27,8 +27,6 @@ export interface TLHandle { index: string x: number y: number - w?: number - h?: number } /** @internal */ @@ -40,6 +38,4 @@ export const handleValidator: T.Validator = T.object({ index: T.string, x: T.number, y: T.number, - w: T.optional(T.number), - h: T.optional(T.number), })