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:
parent
f87702bda4
commit
34a95b2ec8
22 changed files with 572 additions and 445 deletions
|
@ -969,9 +969,7 @@ export const EVENT_NAME_MAP: Record<Exclude<TLEventName, TLPinchEventName>, keyo
|
|||
export function extractSessionStateFromLegacySnapshot(store: Record<string, UnknownRecord>): null | TLSessionStateSnapshot;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const featureFlags: {
|
||||
canMoveArrowLabel: DebugFlag<boolean>;
|
||||
};
|
||||
export const featureFlags: Record<string, DebugFlag<boolean>>;
|
||||
|
||||
// @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<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
onDropShapesOver?: TLOnDragHandler<Shape>;
|
||||
onEditEnd?: TLOnEditEndHandler<Shape>;
|
||||
onHandleDrag?: TLOnHandleDragHandler<Shape>;
|
||||
onHandleDragEnd?: TLOnHandleDragStartHandler<Shape>;
|
||||
onHandleDragStart?: TLOnHandleDragStartHandler<Shape>;
|
||||
onResize?: TLOnResizeHandler<Shape>;
|
||||
onResizeEnd?: TLOnResizeEndHandler<Shape>;
|
||||
onResizeStart?: TLOnResizeStartHandler<Shape>;
|
||||
|
@ -2382,9 +2384,6 @@ export type TLOnHandleDragHandler<T extends TLShape> = (shape: T, info: {
|
|||
initial?: T | undefined;
|
||||
}) => TLShapePartial<T> | void;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLOnHandleDragStartHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void;
|
||||
|
||||
// @public
|
||||
export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void;
|
||||
|
||||
|
|
|
@ -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": "<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",
|
||||
"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<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",
|
||||
"canonicalReference": "@tldraw/editor!TLOnMountHandler:type",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -14,14 +14,6 @@ export type TLHandleComponent = ComponentType<{
|
|||
|
||||
/** @public */
|
||||
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 fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom
|
||||
|
||||
|
|
|
@ -411,15 +411,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -430,15 +421,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -604,9 +586,6 @@ export type TLOnBindingChangeHandler<T extends TLShape> = (shape: T) => TLShapeP
|
|||
/** @public */
|
||||
export type TLOnChildrenChangeHandler<T extends TLShape> = (shape: T) => TLShapePartial[] | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnHandleDragStartHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnHandleDragHandler<T extends TLShape> = (
|
||||
shape: T,
|
||||
|
@ -617,9 +596,6 @@ export type TLOnHandleDragHandler<T extends TLShape> = (
|
|||
}
|
||||
) => TLShapePartial<T> | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnHandleDragEndHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnClickHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
|
||||
/** @public */
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
59
packages/editor/src/lib/primitives/utils.test.ts
Normal file
59
packages/editor/src/lib/primitives/utils.test.ts
Normal 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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<string, DebugFlag<boolean>>
|
||||
export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
||||
// canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'),
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const debugFlags = {
|
||||
|
@ -111,16 +111,16 @@ function createDebugValue<T>(
|
|||
})
|
||||
}
|
||||
|
||||
function createFeatureFlag(
|
||||
name: string,
|
||||
defaults: Defaults<boolean> = { all: true, production: false }
|
||||
) {
|
||||
return createDebugValueBase({
|
||||
name,
|
||||
defaults,
|
||||
shouldStoreForSession: true,
|
||||
})
|
||||
}
|
||||
// function createFeatureFlag(
|
||||
// name: string,
|
||||
// defaults: Defaults<boolean> = { all: true, production: false }
|
||||
// ) {
|
||||
// return createDebugValueBase({
|
||||
// name,
|
||||
// defaults,
|
||||
// shouldStoreForSession: true,
|
||||
// })
|
||||
// }
|
||||
|
||||
function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
|
||||
const defaultValue = getDefaultValue(def)
|
||||
|
|
|
@ -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<TLArrowShape> {
|
|||
// (undocumented)
|
||||
onHandleDrag: TLOnHandleDragHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onHandleDragStart: TLOnHandleDragStartHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onTranslate?: TLOnTranslateHandler<TLArrowShape>;
|
||||
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<TLArrowShape> {
|
|||
override getHandles(shape: TLArrowShape): TLHandle[] {
|
||||
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 [
|
||||
{
|
||||
id: ARROW_HANDLES.START,
|
||||
|
@ -172,17 +162,6 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
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<TLArrowShape> {
|
|||
].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 }) => {
|
||||
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 } }
|
||||
}
|
||||
|
||||
// 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...
|
||||
|
||||
const next = deepCopy(shape) as TLArrowShape
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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<any> = { ...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<any> = { ...shape, ...changes }
|
||||
if (changes) {
|
||||
this.editor.updateShapes([next], { squashing: true })
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
|
|
|
@ -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<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)
|
||||
break
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<TLHandle> = T.object({
|
|||
index: T.string,
|
||||
x: T.number,
|
||||
y: T.number,
|
||||
w: T.optional(T.number),
|
||||
h: T.optional(T.number),
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue