arrows: add ability to change label placement (#2557)
This adds the ability to drag the label on an arrow to a different location within the line segment/arc. https://github.com/tldraw/tldraw/assets/469604/dbd2ee35-bebc-48d6-b8ee-fcf12ce91fa5 - A lot of the complexity lay in ensuring a fixed distance from the ends of the arrowheads. - I added a new type of handle `text-adjust` that makes the text box the very handle itself. - I added a `ARROW_HANDLES` enum - we should use more enums! - The bulk of the changes are in ArrowShapeUtil — check that out in particular obviously :) Along the way, I tried to improve a couple spots as I touched them: - added some more documentation to Vec.ts because some of the functions in there were obscure/new to me. (at least the naming, hah) - added `getPointOnCircle` which was being done in a couple places independently and refactored those places. ### Questions - the `getPointOnCircle` API changed. Is this considered breaking and/or should I leave the signature the same? Wasn't sure if it was a big deal or not. - I made `labelPosition` in the schema always but I guess it could have been optional? Lemme know if there's a preference. - Any feedback on tests? Happy to expand those if necessary. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. For arrow in [straightArrow, curvedArrow] test the following: a. Label in the middle b. Label at both ends of the arrow c. Test arrows in different directions d. Rotating the endpoints and seeing that the label stays at the end of the arrow at a fixed width. e. Test different stroke widths. f. Test with different arrowheads. 2. Also, test arcs that are more circle like than arc-like. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds ability to change label position on arrows. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
parent
d1bc456162
commit
07cda7ef9f
33 changed files with 1363 additions and 239 deletions
|
@ -10,7 +10,7 @@ import {
|
|||
TLDefaultSizeStyle,
|
||||
TLHandle,
|
||||
TLOnBeforeUpdateHandler,
|
||||
TLOnHandleChangeHandler,
|
||||
TLOnHandleDragHandler,
|
||||
TLOnResizeHandler,
|
||||
Vec,
|
||||
deepCopy,
|
||||
|
@ -153,10 +153,7 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
return next
|
||||
}
|
||||
|
||||
override onHandleChange: TLOnHandleChangeHandler<SpeechBubbleShape> = (
|
||||
_,
|
||||
{ handle, initial }
|
||||
) => {
|
||||
override onHandleDrag: TLOnHandleDragHandler<SpeechBubbleShape> = (_, { handle, initial }) => {
|
||||
const newHandle = deepCopy(handle)
|
||||
newHandle.x = newHandle.x / initial!.props.w
|
||||
newHandle.y = newHandle.y / initial!.props.h
|
||||
|
|
|
@ -75,6 +75,9 @@ import { useValue } from '@tldraw/state';
|
|||
import { VecModel } from '@tldraw/tlschema';
|
||||
import { whyAmIRunning } from '@tldraw/state';
|
||||
|
||||
// @public
|
||||
export function angleDistance(fromAngle: number, toAngle: number, direction: number): number;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const ANIMATION_MEDIUM_MS = 320;
|
||||
|
||||
|
@ -199,6 +202,8 @@ export class Box {
|
|||
// (undocumented)
|
||||
static From(box: BoxModel): Box;
|
||||
// (undocumented)
|
||||
static FromCenter(center: VecLike, size: VecLike): Box;
|
||||
// (undocumented)
|
||||
static FromPoints(points: VecLike[]): Box;
|
||||
// (undocumented)
|
||||
getHandlePoint(handle: SelectionCorner | SelectionEdge): Vec;
|
||||
|
@ -339,6 +344,9 @@ export { computed }
|
|||
// @public (undocumented)
|
||||
export const coreShapes: readonly [typeof GroupShapeUtil];
|
||||
|
||||
// @public
|
||||
export function counterClockwiseAngleDist(a0: number, a1: number): number;
|
||||
|
||||
// @public
|
||||
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
||||
|
||||
|
@ -960,7 +968,9 @@ export const EVENT_NAME_MAP: Record<Exclude<TLEventName, TLPinchEventName>, keyo
|
|||
export function extractSessionStateFromLegacySnapshot(store: Record<string, UnknownRecord>): null | TLSessionStateSnapshot;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const featureFlags: Record<string, DebugFlag<boolean>>;
|
||||
export const featureFlags: {
|
||||
canMoveArrowLabel: DebugFlag<boolean>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type GapsSnapLine = {
|
||||
|
@ -987,6 +997,8 @@ export abstract class Geometry2d {
|
|||
// (undocumented)
|
||||
get center(): Vec;
|
||||
// (undocumented)
|
||||
debugColor?: string;
|
||||
// (undocumented)
|
||||
distanceToLineSegment(A: Vec, B: Vec): number;
|
||||
// (undocumented)
|
||||
distanceToPoint(point: Vec, hitInside?: boolean): number;
|
||||
|
@ -1001,6 +1013,8 @@ export abstract class Geometry2d {
|
|||
// (undocumented)
|
||||
hitTestPoint(point: Vec, margin?: number, hitInside?: boolean): boolean;
|
||||
// (undocumented)
|
||||
ignore?: boolean;
|
||||
// (undocumented)
|
||||
isClosed: boolean;
|
||||
// (undocumented)
|
||||
isFilled: boolean;
|
||||
|
@ -1078,7 +1092,7 @@ export function getPointerInfo(e: PointerEvent | React.PointerEvent): {
|
|||
};
|
||||
|
||||
// @public
|
||||
export function getPointOnCircle(cx: number, cy: number, r: number, a: number): Vec;
|
||||
export function getPointOnCircle(center: VecLike, r: number, a: number): Vec;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getPolygonVertices(width: number, height: number, sides: number): Vec[];
|
||||
|
@ -1119,6 +1133,8 @@ export class Group2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
hitTestPoint(point: Vec, margin: number, hitInside: boolean): boolean;
|
||||
// (undocumented)
|
||||
ignoredChildren: Geometry2d[];
|
||||
// (undocumented)
|
||||
nearestPoint(point: Vec): Vec;
|
||||
// (undocumented)
|
||||
toSimpleSvgPath(): string;
|
||||
|
@ -1643,7 +1659,9 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
}>;
|
||||
onDropShapesOver?: TLOnDragHandler<Shape>;
|
||||
onEditEnd?: TLOnEditEndHandler<Shape>;
|
||||
onHandleChange?: TLOnHandleChangeHandler<Shape>;
|
||||
onHandleDrag?: TLOnHandleDragHandler<Shape>;
|
||||
onHandleDragEnd?: TLOnHandleDragStartHandler<Shape>;
|
||||
onHandleDragStart?: TLOnHandleDragStartHandler<Shape>;
|
||||
onResize?: TLOnResizeHandler<Shape>;
|
||||
onResizeEnd?: TLOnResizeEndHandler<Shape>;
|
||||
onResizeStart?: TLOnResizeStartHandler<Shape>;
|
||||
|
@ -2357,12 +2375,15 @@ export type TLOnDragHandler<T extends TLShape, R = void> = (shape: T, shapes: TL
|
|||
export type TLOnEditEndHandler<T extends TLShape> = (shape: T) => void;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLOnHandleChangeHandler<T extends TLShape> = (shape: T, info: {
|
||||
export type TLOnHandleDragHandler<T extends TLShape> = (shape: T, info: {
|
||||
handle: TLHandle;
|
||||
isPrecise: boolean;
|
||||
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;
|
||||
|
||||
|
@ -2800,7 +2821,6 @@ export class Vec {
|
|||
static Clockwise(A: VecLike, B: VecLike, C: VecLike): boolean;
|
||||
// (undocumented)
|
||||
clone(): Vec;
|
||||
// (undocumented)
|
||||
static Cpr(A: VecLike, B: VecLike): number;
|
||||
// (undocumented)
|
||||
cpr(V: VecLike): number;
|
||||
|
@ -2828,7 +2848,6 @@ export class Vec {
|
|||
static DivV(A: VecLike, B: VecLike): Vec;
|
||||
// (undocumented)
|
||||
divV(V: VecLike): this;
|
||||
// (undocumented)
|
||||
static Dpr(A: VecLike, B: VecLike): number;
|
||||
// (undocumented)
|
||||
dpr(V: VecLike): number;
|
||||
|
@ -2854,7 +2873,6 @@ export class Vec {
|
|||
static Len2(A: VecLike): number;
|
||||
// (undocumented)
|
||||
len2(): number;
|
||||
// (undocumented)
|
||||
static Lrp(A: VecLike, B: VecLike, t: number): Vec;
|
||||
// (undocumented)
|
||||
lrp(B: VecLike, t: number): Vec;
|
||||
|
@ -2885,14 +2903,12 @@ export class Vec {
|
|||
static Nudge(A: VecLike, B: VecLike, distance: number): Vec;
|
||||
// (undocumented)
|
||||
nudge(B: VecLike, distance: number): this;
|
||||
// (undocumented)
|
||||
static Per(A: VecLike): Vec;
|
||||
// (undocumented)
|
||||
per(): this;
|
||||
static PointsBetween(A: VecModel, B: VecModel, steps?: number): Vec[];
|
||||
// (undocumented)
|
||||
get pressure(): number;
|
||||
// (undocumented)
|
||||
static Pry(A: VecLike, B: VecLike): number;
|
||||
// (undocumented)
|
||||
pry(V: VecLike): number;
|
||||
|
@ -2962,7 +2978,6 @@ export class Vec {
|
|||
static ToString(A: VecLike): string;
|
||||
// (undocumented)
|
||||
toString(): string;
|
||||
// (undocumented)
|
||||
static Uni(A: VecLike): Vec;
|
||||
// (undocumented)
|
||||
uni(): Vec;
|
||||
|
|
|
@ -172,6 +172,83 @@
|
|||
"name": "",
|
||||
"preserveMemberOrder": false,
|
||||
"members": [
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!angleDistance:function(1)",
|
||||
"docComment": "/**\n * Get the angle of a point on an arc.\n *\n * @param fromAngle - The angle from center to arc's start point (A) on the circle\n *\n * @param toAngle - The angle from center to arc's end point (B) on the circle\n *\n * @param direction - The direction of the arc (1 = counter-clockwise, -1 = clockwise)\n *\n * @returns The distance in radians between the two angles according to the direction\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export declare function angleDistance(fromAngle: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", toAngle: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", direction: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/utils.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "fromAngle",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "toAngle",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "direction",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"name": "angleDistance"
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!approximately:function(1)",
|
||||
|
@ -2246,6 +2323,73 @@
|
|||
"isAbstract": false,
|
||||
"name": "From"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Box.FromCenter:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "static FromCenter(center: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", size: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Box",
|
||||
"canonicalReference": "@tldraw/editor!Box:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": true,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "center",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "size",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "FromCenter"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Box.FromPoints:member(1)",
|
||||
|
@ -4598,6 +4742,67 @@
|
|||
"endIndex": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!counterClockwiseAngleDist:function(1)",
|
||||
"docComment": "/**\n * Get the counter-clockwise angle distance between two angles.\n *\n * @param a0 - The first angle.\n *\n * @param a1 - The second angle.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export declare function counterClockwiseAngleDist(a0: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", a1: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/utils.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "a0",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "a1",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"name": "counterClockwiseAngleDist"
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!createSessionStateSnapshotSignal:function(1)",
|
||||
|
@ -20119,6 +20324,36 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d#debugColor:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "debugColor?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "string"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "debugColor",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d#distanceToLineSegment:member(1)",
|
||||
|
@ -20512,6 +20747,36 @@
|
|||
"isAbstract": false,
|
||||
"name": "hitTestPoint"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d#ignore:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "ignore?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "ignore",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d#isClosed:member",
|
||||
|
@ -21636,23 +21901,16 @@
|
|||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!getPointOnCircle:function(1)",
|
||||
"docComment": "/**\n * Get a point on the perimeter of a circle.\n *\n * @param cx - The center x of the circle.\n *\n * @param cy - The center y of the circle.\n *\n * @param r - The radius of the circle.\n *\n * @param a - The normalized point on the circle.\n *\n * @public\n */\n",
|
||||
"docComment": "/**\n * Get a point on the perimeter of a circle.\n *\n * @param center - The center of the circle.\n *\n * @param r - The radius of the circle.\n *\n * @param a - The angle in radians.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export declare function getPointOnCircle(cx: "
|
||||
"text": "export declare function getPointOnCircle(center: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", cy: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -21686,14 +21944,14 @@
|
|||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/utils.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 9,
|
||||
"endIndex": 10
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "cx",
|
||||
"parameterName": "center",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
|
@ -21701,7 +21959,7 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "cy",
|
||||
"parameterName": "r",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
|
@ -21709,20 +21967,12 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "r",
|
||||
"parameterName": "a",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "a",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"name": "getPointOnCircle"
|
||||
|
@ -22337,6 +22587,41 @@
|
|||
"isAbstract": false,
|
||||
"name": "hitTestPoint"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!Group2d#ignoredChildren:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "ignoredChildren: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Geometry2d",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "ignoredChildren",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Group2d#nearestPoint:member(1)",
|
||||
|
@ -31020,17 +31305,17 @@
|
|||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!ShapeUtil#onHandleChange:member",
|
||||
"canonicalReference": "@tldraw/editor!ShapeUtil#onHandleDrag:member",
|
||||
"docComment": "/**\n * A callback called when a shape's handle changes.\n *\n * @param shape - The current shape.\n *\n * @param info - An object containing the handle and whether the handle is 'precise' or not.\n *\n * @returns A change to apply to the shape, or void.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onHandleChange?: "
|
||||
"text": "onHandleDrag?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLOnHandleChangeHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleChangeHandler:type"
|
||||
"text": "TLOnHandleDragHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleDragHandler:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -31044,7 +31329,77 @@
|
|||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "onHandleChange",
|
||||
"name": "onHandleDrag",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isStatic": false,
|
||||
"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
|
||||
|
@ -39132,12 +39487,12 @@
|
|||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleChangeHandler:type",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleDragHandler:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type TLOnHandleChangeHandler<T extends "
|
||||
"text": "export type TLOnHandleDragHandler<T extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -39177,7 +39532,7 @@
|
|||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/editor/shapes/ShapeUtil.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "TLOnHandleChangeHandler",
|
||||
"name": "TLOnHandleDragHandler",
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "T",
|
||||
|
@ -39196,6 +39551,63 @@
|
|||
"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",
|
||||
|
@ -43901,7 +44313,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Cpr:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Cross product of two vectors which is used to calculate the area of a parallelogram.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -44791,7 +45203,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Dpr:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Dot product of two vectors which is used to calculate the angle between them.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -45507,7 +45919,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Lrp:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Linearly interpolate between two points.\n *\n * @param A - The first point.\n *\n * @param B - The second point.\n *\n * @param t - The interpolation value between 0 and 1.\n *\n * @returns The interpolated point.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -46496,7 +46908,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Per:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Get the perpendicular vector to A.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -46712,7 +47124,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Pry:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Get the projection of A onto B.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -48557,7 +48969,7 @@
|
|||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Vec.Uni:member(1)",
|
||||
"docComment": "",
|
||||
"docComment": "/**\n * Get the unit vector of A.\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -174,7 +174,8 @@ export {
|
|||
type TLOnDoubleClickHandler,
|
||||
type TLOnDragHandler,
|
||||
type TLOnEditEndHandler,
|
||||
type TLOnHandleChangeHandler,
|
||||
type TLOnHandleDragHandler,
|
||||
type TLOnHandleDragStartHandler,
|
||||
type TLOnResizeEndHandler,
|
||||
type TLOnResizeHandler,
|
||||
type TLOnResizeStartHandler,
|
||||
|
@ -308,6 +309,7 @@ export {
|
|||
PI,
|
||||
PI2,
|
||||
SIN,
|
||||
angleDistance,
|
||||
approximately,
|
||||
areAnglesCompatible,
|
||||
average,
|
||||
|
@ -315,6 +317,7 @@ export {
|
|||
clamp,
|
||||
clampRadians,
|
||||
clockwiseAngleDist,
|
||||
counterClockwiseAngleDist,
|
||||
degreesToRadians,
|
||||
getPointOnCircle,
|
||||
getPolygonVertices,
|
||||
|
|
|
@ -2,6 +2,8 @@ import { track } from '@tldraw/state'
|
|||
import { modulate } from '@tldraw/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { Geometry2d } from '../primitives/geometry/Geometry2d'
|
||||
import { Group2d } from '../primitives/geometry/Group2d'
|
||||
|
||||
function useTick(isEnabled = true) {
|
||||
const [_, setTick] = useState(0)
|
||||
|
@ -69,15 +71,7 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
|
|||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{showStroke && (
|
||||
<path
|
||||
stroke="red"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="1"
|
||||
d={geometry.toSimpleSvgPath()}
|
||||
/>
|
||||
)}
|
||||
{showStroke && <GeometryStroke geometry={geometry} />}
|
||||
{showVertices &&
|
||||
vertices.map((v, i) => (
|
||||
<circle
|
||||
|
@ -107,3 +101,25 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
|
|||
</svg>
|
||||
)
|
||||
})
|
||||
|
||||
function GeometryStroke({ geometry }: { geometry: Geometry2d }) {
|
||||
if (geometry instanceof Group2d) {
|
||||
return (
|
||||
<>
|
||||
{[...geometry.children, ...geometry.ignoredChildren].map((child, i) => (
|
||||
<GeometryStroke geometry={child} key={i} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<path
|
||||
stroke={geometry.debugColor ?? 'red'}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="1"
|
||||
d={geometry.toSimpleSvgPath()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,14 @@ export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className,
|
|||
const bgRadius = (isCoarse ? 20 : 12) / zoom
|
||||
const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom
|
||||
|
||||
if (handle.type === 'text-adjust') {
|
||||
return (
|
||||
<g className={classNames('tl-handle', className)}>
|
||||
<rect fill="transparent" width={handle.w} height={handle.h} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<g
|
||||
className={classNames(
|
||||
|
|
|
@ -411,6 +411,34 @@ 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.
|
||||
*
|
||||
* @param shape - The current shape.
|
||||
* @param info - An object containing the handle and whether the handle is 'precise' or not.
|
||||
* @returns A change to apply to the shape, or void.
|
||||
* @public
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -440,16 +468,6 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*/
|
||||
onRotateEnd?: TLOnRotateEndHandler<Shape>
|
||||
|
||||
/**
|
||||
* A callback called when a shape's handle changes.
|
||||
*
|
||||
* @param shape - The current shape.
|
||||
* @param info - An object containing the handle and whether the handle is 'precise' or not.
|
||||
* @returns A change to apply to the shape, or void.
|
||||
* @public
|
||||
*/
|
||||
onHandleChange?: TLOnHandleChangeHandler<Shape>
|
||||
|
||||
/**
|
||||
* Not currently used.
|
||||
*
|
||||
|
@ -587,7 +605,10 @@ export type TLOnBindingChangeHandler<T extends TLShape> = (shape: T) => TLShapeP
|
|||
export type TLOnChildrenChangeHandler<T extends TLShape> = (shape: T) => TLShapePartial[] | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnHandleChangeHandler<T extends TLShape> = (
|
||||
export type TLOnHandleDragStartHandler<T extends TLShape> = (shape: T) => TLShapePartial<T> | void
|
||||
|
||||
/** @public */
|
||||
export type TLOnHandleDragHandler<T extends TLShape> = (
|
||||
shape: T,
|
||||
info: {
|
||||
handle: TLHandle
|
||||
|
@ -596,6 +617,9 @@ export type TLOnHandleChangeHandler<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 */
|
||||
|
|
|
@ -119,6 +119,8 @@ export const MIN_ARROW_LENGTH = 10
|
|||
/** @internal */
|
||||
export const BOUND_ARROW_OFFSET = 10
|
||||
/** @internal */
|
||||
export const LABEL_TO_ARROW_PADDING = 20
|
||||
/** @internal */
|
||||
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -354,6 +354,10 @@ export class Box {
|
|||
return new Box(box.x, box.y, box.w, box.h)
|
||||
}
|
||||
|
||||
static FromCenter(center: VecLike, size: VecLike) {
|
||||
return new Box(center.x - size.x / 2, center.y - size.y / 2, size.x, size.y)
|
||||
}
|
||||
|
||||
static FromPoints(points: VecLike[]) {
|
||||
if (points.length === 0) return new Box()
|
||||
let minX = Infinity
|
||||
|
|
|
@ -302,6 +302,9 @@ export class Vec {
|
|||
return new Vec(-A.x, -A.y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the perpendicular vector to A.
|
||||
*/
|
||||
static Per(A: VecLike): Vec {
|
||||
return new Vec(A.y, -A.x)
|
||||
}
|
||||
|
@ -318,6 +321,9 @@ export class Vec {
|
|||
return Math.hypot(A.y - B.y, A.x - B.x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dot product of two vectors which is used to calculate the angle between them.
|
||||
*/
|
||||
static Dpr(A: VecLike, B: VecLike): number {
|
||||
return A.x * B.x + A.y * B.y
|
||||
}
|
||||
|
@ -330,6 +336,9 @@ export class Vec {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross product of two vectors which is used to calculate the area of a parallelogram.
|
||||
*/
|
||||
static Cpr(A: VecLike, B: VecLike) {
|
||||
return A.x * B.y - B.x * A.y
|
||||
}
|
||||
|
@ -342,10 +351,16 @@ export class Vec {
|
|||
return Math.hypot(A.x, A.y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the projection of A onto B.
|
||||
*/
|
||||
static Pry(A: VecLike, B: VecLike): number {
|
||||
return Vec.Dpr(A, B) / Vec.Len(B)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit vector of A.
|
||||
*/
|
||||
static Uni(A: VecLike) {
|
||||
return Vec.Div(A, Vec.Len(A))
|
||||
}
|
||||
|
@ -441,6 +456,13 @@ export class Vec {
|
|||
return Math.atan2(B.y - A.y, B.x - A.x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolate between two points.
|
||||
* @param A - The first point.
|
||||
* @param B - The second point.
|
||||
* @param t - The interpolation value between 0 and 1.
|
||||
* @returns The interpolated point.
|
||||
*/
|
||||
static Lrp(A: VecLike, B: VecLike, t: number): Vec {
|
||||
return Vec.Sub(B, A).mul(t).add(A)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Vec } from '../Vec'
|
||||
import { intersectLineSegmentCircle } from '../intersect'
|
||||
import { PI, PI2, shortAngleDist } from '../utils'
|
||||
import { PI, PI2, getPointOnCircle, shortAngleDist } from '../utils'
|
||||
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
||||
import { getVerticesCountForLength } from './geometry-constants'
|
||||
|
||||
|
@ -83,7 +83,7 @@ export class Arc2d extends Geometry2d {
|
|||
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
|
||||
const t = (i / n) * measure
|
||||
const angle = angleStart + t
|
||||
vertices.push(_center.clone().add(new Vec(Math.cos(angle), Math.sin(angle)).mul(radius)))
|
||||
vertices.push(getPointOnCircle(_center, radius, angle))
|
||||
}
|
||||
|
||||
return vertices
|
||||
|
@ -93,10 +93,10 @@ export class Arc2d extends Geometry2d {
|
|||
/**
|
||||
* 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 point on the circle (P) to find the t value for
|
||||
* @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
|
||||
*
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Box } from '../Box'
|
||||
import { Vec } from '../Vec'
|
||||
import { intersectLineSegmentCircle } from '../intersect'
|
||||
import { PI2 } from '../utils'
|
||||
import { PI2, getPointOnCircle } from '../utils'
|
||||
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
||||
import { getVerticesCountForLength } from './geometry-constants'
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class Circle2d extends Geometry2d {
|
|||
const vertices: Vec[] = []
|
||||
for (let i = 0, n = getVerticesCountForLength(perimeter); i < n; i++) {
|
||||
const angle = (i / n) * PI2
|
||||
vertices.push(_center.clone().add(Vec.FromAngle(angle).mul(radius)))
|
||||
vertices.push(getPointOnCircle(_center, radius, angle))
|
||||
}
|
||||
return vertices
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ export interface Geometry2dOptions {
|
|||
isClosed: boolean
|
||||
isLabel?: boolean
|
||||
isSnappable?: boolean
|
||||
debugColor?: string
|
||||
ignore?: boolean
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -15,12 +17,16 @@ export abstract class Geometry2d {
|
|||
isClosed = true
|
||||
isLabel = false
|
||||
isSnappable = true
|
||||
debugColor?: string
|
||||
ignore?: boolean
|
||||
|
||||
constructor(opts: Geometry2dOptions) {
|
||||
this.isFilled = opts.isFilled
|
||||
this.isClosed = opts.isClosed
|
||||
this.isSnappable = opts.isSnappable ?? false
|
||||
this.isLabel = opts.isLabel ?? false
|
||||
this.debugColor = opts.debugColor
|
||||
this.ignore = opts.ignore
|
||||
}
|
||||
|
||||
abstract getVertices(): Vec[]
|
||||
|
|
|
@ -4,7 +4,8 @@ import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
|||
|
||||
/** @public */
|
||||
export class Group2d extends Geometry2d {
|
||||
children: Geometry2d[]
|
||||
children: Geometry2d[] = []
|
||||
ignoredChildren: Geometry2d[] = []
|
||||
|
||||
constructor(
|
||||
config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {
|
||||
|
@ -12,11 +13,16 @@ export class Group2d extends Geometry2d {
|
|||
}
|
||||
) {
|
||||
super({ ...config, isClosed: true, isFilled: false })
|
||||
const { children } = config
|
||||
|
||||
if (children.length === 0) throw Error('Group2d must have at least one child')
|
||||
for (const child of config.children) {
|
||||
if (child.ignore) {
|
||||
this.ignoredChildren.push(child)
|
||||
} else {
|
||||
this.children.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
this.children = children
|
||||
if (this.children.length === 0) throw Error('Group2d must have at least one child')
|
||||
}
|
||||
|
||||
override getVertices(): Vec[] {
|
||||
|
|
|
@ -205,15 +205,15 @@ export function radiansToDegrees(r: number): number {
|
|||
/**
|
||||
* Get a point on the perimeter of a circle.
|
||||
*
|
||||
* @param cx - The center x of the circle.
|
||||
* @param cy - The center y of the circle.
|
||||
* @param center - The center of the circle.
|
||||
* @param r - The radius of the circle.
|
||||
* @param a - The normalized point on the circle.
|
||||
* @param a - The angle in radians.
|
||||
* @public
|
||||
*/
|
||||
export function getPointOnCircle(cx: number, cy: number, r: number, a: number) {
|
||||
return new Vec(cx + r * Math.cos(a), cy + r * Math.sin(a))
|
||||
export function getPointOnCircle(center: VecLike, r: number, a: number) {
|
||||
return new Vec(center.x, center.y).add(Vec.FromAngle(a, r))
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function getPolygonVertices(width: number, height: number, sides: number) {
|
||||
const cx = width / 2
|
||||
|
@ -356,3 +356,19 @@ export function toFixed(v: number) {
|
|||
export const isSafeFloat = (n: number) => {
|
||||
return Math.abs(n) < Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the angle of a point on an arc.
|
||||
* @param fromAngle - The angle from center to arc's start point (A) on the circle
|
||||
* @param toAngle - The angle from center to arc's end point (B) on the circle
|
||||
* @param direction - The direction of the arc (1 = counter-clockwise, -1 = clockwise)
|
||||
* @returns The distance in radians between the two angles according to the direction
|
||||
* @public
|
||||
*/
|
||||
export function angleDistance(fromAngle: number, toAngle: number, direction: number) {
|
||||
const dist =
|
||||
direction < 0
|
||||
? clockwiseAngleDist(fromAngle, toAngle)
|
||||
: counterClockwiseAngleDist(fromAngle, toAngle)
|
||||
return dist
|
||||
}
|
||||
|
|
|
@ -7,10 +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: Record<string, DebugFlag<boolean>> = {
|
||||
// todo: remove this. it's not used, but we only have one feature flag and i
|
||||
// wanted an example :(
|
||||
}
|
||||
export const featureFlags = {
|
||||
canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'),
|
||||
} satisfies Record<string, DebugFlag<boolean>>
|
||||
|
||||
/** @internal */
|
||||
export const debugFlags = {
|
||||
|
@ -109,16 +108,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)
|
||||
|
|
|
@ -79,7 +79,8 @@ import { TLOnBeforeCreateHandler } from '@tldraw/editor';
|
|||
import { TLOnBeforeUpdateHandler } from '@tldraw/editor';
|
||||
import { TLOnDoubleClickHandler } from '@tldraw/editor';
|
||||
import { TLOnEditEndHandler } from '@tldraw/editor';
|
||||
import { TLOnHandleChangeHandler } 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';
|
||||
|
@ -161,7 +162,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// (undocumented)
|
||||
onEditEnd: TLOnEditEndHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onHandleChange: TLOnHandleChangeHandler<TLArrowShape>;
|
||||
onHandleDrag: TLOnHandleDragHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onHandleDragStart: TLOnHandleDragStartHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLArrowShape>;
|
||||
// (undocumented)
|
||||
|
@ -208,6 +211,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}, never>;
|
||||
bend: Validator<number>;
|
||||
text: Validator<string>;
|
||||
labelPosition: Validator<number>;
|
||||
};
|
||||
// (undocumented)
|
||||
toSvg(shape: TLArrowShape, ctx: SvgExportContext): SVGGElement;
|
||||
|
@ -908,7 +912,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
// (undocumented)
|
||||
static migrations: Migrations;
|
||||
// (undocumented)
|
||||
onHandleChange: TLOnHandleChangeHandler<TLLineShape>;
|
||||
onHandleDrag: TLOnHandleDragHandler<TLLineShape>;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<TLLineShape>;
|
||||
// (undocumented)
|
||||
|
|
|
@ -1085,17 +1085,17 @@
|
|||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onHandleChange:member",
|
||||
"canonicalReference": "@tldraw/tldraw!ArrowShapeUtil#onHandleDrag:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onHandleChange: "
|
||||
"text": "onHandleDrag: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLOnHandleChangeHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleChangeHandler:type"
|
||||
"text": "TLOnHandleDragHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleDragHandler:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -1118,7 +1118,51 @@
|
|||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onHandleChange",
|
||||
"name": "onHandleDrag",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
},
|
||||
"isStatic": false,
|
||||
"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
|
||||
|
@ -1450,7 +1494,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string>;\n }"
|
||||
"text": "<string>;\n labelPosition: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Validator",
|
||||
"canonicalReference": "@tldraw/validate!Validator:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<number>;\n }"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -1463,7 +1516,7 @@
|
|||
"name": "props",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 42
|
||||
"endIndex": 44
|
||||
},
|
||||
"isStatic": true,
|
||||
"isProtected": false,
|
||||
|
@ -10759,17 +10812,17 @@
|
|||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/tldraw!LineShapeUtil#onHandleChange:member",
|
||||
"canonicalReference": "@tldraw/tldraw!LineShapeUtil#onHandleDrag:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onHandleChange: "
|
||||
"text": "onHandleDrag: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLOnHandleChangeHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleChangeHandler:type"
|
||||
"text": "TLOnHandleDragHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnHandleDragHandler:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -10792,7 +10845,7 @@
|
|||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onHandleChange",
|
||||
"name": "onHandleDrag",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
|
|
|
@ -314,7 +314,7 @@ describe('Other cases when arrow are moved', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When a shape it rotated', () => {
|
||||
describe('When a shape is rotated', () => {
|
||||
it('binds correctly', () => {
|
||||
editor.setCurrentTool('arrow').pointerDown(0, 0).pointerMove(375, 375)
|
||||
|
||||
|
@ -352,6 +352,36 @@ describe('When a shape it rotated', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Arrow labels', () => {
|
||||
beforeEach(() => {
|
||||
// Create an arrow with a label
|
||||
editor.setCurrentTool('arrow').pointerDown(10, 10).pointerMove(100, 100).pointerUp()
|
||||
const arrowId = editor.getOnlySelectedShape()!.id
|
||||
editor.updateShapes([{ id: arrowId, type: 'arrow', props: { text: 'Test Label' } }])
|
||||
})
|
||||
|
||||
it('should create an arrow with a label', () => {
|
||||
const arrowId = editor.getOnlySelectedShape()!.id
|
||||
const arrow = editor.getShape(arrowId)
|
||||
expect(arrow).toMatchObject({
|
||||
props: {
|
||||
text: 'Test Label',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update the label of an arrow', () => {
|
||||
const arrowId = editor.getOnlySelectedShape()!.id
|
||||
editor.updateShapes([{ id: arrowId, type: 'arrow', props: { text: 'New Label' } }])
|
||||
const arrow = editor.getShape(arrowId)
|
||||
expect(arrow).toMatchObject({
|
||||
props: {
|
||||
text: 'New Label',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resizing', () => {
|
||||
it('resizes', () => {
|
||||
editor
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Box,
|
||||
DefaultFontFamilies,
|
||||
Edge2d,
|
||||
Geometry2d,
|
||||
Group2d,
|
||||
Rectangle2d,
|
||||
SVGContainer,
|
||||
|
@ -16,7 +17,8 @@ import {
|
|||
TLDefaultFillStyle,
|
||||
TLHandle,
|
||||
TLOnEditEndHandler,
|
||||
TLOnHandleChangeHandler,
|
||||
TLOnHandleDragHandler,
|
||||
TLOnHandleDragStartHandler,
|
||||
TLOnResizeHandler,
|
||||
TLOnTranslateHandler,
|
||||
TLOnTranslateStartHandler,
|
||||
|
@ -26,7 +28,10 @@ import {
|
|||
Vec,
|
||||
arrowShapeMigrations,
|
||||
arrowShapeProps,
|
||||
clockwiseAngleDist,
|
||||
counterClockwiseAngleDist,
|
||||
deepCopy,
|
||||
featureFlags,
|
||||
getArrowTerminalsInArrowSpace,
|
||||
getDefaultColorTheme,
|
||||
mapObjectMapValues,
|
||||
|
@ -37,18 +42,14 @@ import {
|
|||
import React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import {
|
||||
ARROW_LABEL_FONT_SIZES,
|
||||
FONT_FAMILIES,
|
||||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
import { ARROW_LABEL_FONT_SIZES, STROKE_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import {
|
||||
getFillDefForCanvas,
|
||||
getFillDefForExport,
|
||||
getFontDefForExport,
|
||||
} from '../shared/defaultStyleDefs'
|
||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||
import { getArrowLabelPosition } from './arrowLabel'
|
||||
import { getArrowheadPathForType } from './arrowheads'
|
||||
import {
|
||||
getCurvedArrowHandlePath,
|
||||
|
@ -60,7 +61,12 @@ import { ArrowTextLabel } from './components/ArrowTextLabel'
|
|||
|
||||
let globalRenderIndex = 0
|
||||
|
||||
export const ARROW_END_OFFSET = 0.1
|
||||
enum ARROW_HANDLES {
|
||||
START = 'start',
|
||||
MIDDLE = 'middle',
|
||||
LABEL = 'middle-text',
|
||||
END = 'end',
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||
|
@ -89,6 +95,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
arrowheadStart: 'none',
|
||||
arrowheadEnd: 'arrow',
|
||||
text: '',
|
||||
labelPosition: 0.5,
|
||||
font: 'draw',
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +103,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
getGeometry(shape: TLArrowShape) {
|
||||
const info = this.editor.getArrowInfo(shape)!
|
||||
|
||||
const debugGeom: Geometry2d[] = []
|
||||
|
||||
const bodyGeom = info.isStraight
|
||||
? new Edge2d({
|
||||
start: Vec.From(info.start.point),
|
||||
|
@ -110,76 +119,45 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
largeArcFlag: info.bodyArc.largeArcFlag,
|
||||
})
|
||||
|
||||
let labelGeom: Rectangle2d | undefined
|
||||
|
||||
let labelGeom
|
||||
if (shape.props.text.trim()) {
|
||||
const bodyBounds = bodyGeom.bounds
|
||||
|
||||
const { w, h } = this.editor.textMeasure.measureText(shape.props.text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: null,
|
||||
})
|
||||
|
||||
let width = w
|
||||
let height = h
|
||||
|
||||
if (bodyBounds.width > bodyBounds.height) {
|
||||
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w))
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
|
||||
shape.props.text,
|
||||
{
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: width,
|
||||
}
|
||||
)
|
||||
|
||||
width = squishedWidth
|
||||
height = squishedHeight
|
||||
}
|
||||
|
||||
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
|
||||
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
|
||||
shape.props.text,
|
||||
{
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: width,
|
||||
}
|
||||
)
|
||||
|
||||
width = squishedWidth
|
||||
height = squishedHeight
|
||||
}
|
||||
|
||||
const labelPosition = getArrowLabelPosition(this.editor, shape)
|
||||
debugGeom.push(...labelPosition.debugGeom)
|
||||
labelGeom = new Rectangle2d({
|
||||
x: info.middle.x - width / 2 - 4.25,
|
||||
y: info.middle.y - height / 2 - 4.25,
|
||||
width: width + 8.5,
|
||||
height: height + 8.5,
|
||||
x: labelPosition.box.x,
|
||||
y: labelPosition.box.y,
|
||||
width: labelPosition.box.w,
|
||||
height: labelPosition.box.h,
|
||||
isFilled: true,
|
||||
isLabel: true,
|
||||
})
|
||||
}
|
||||
|
||||
return new Group2d({
|
||||
children: labelGeom ? [bodyGeom, labelGeom] : [bodyGeom],
|
||||
children: [...(labelGeom ? [bodyGeom, labelGeom] : [bodyGeom]), ...debugGeom],
|
||||
isSnappable: false,
|
||||
})
|
||||
}
|
||||
|
||||
private getLength(shape: TLArrowShape): number {
|
||||
const info = this.editor.getArrowInfo(shape)!
|
||||
|
||||
return info.isStraight
|
||||
? Vec.Dist(info.start.handle, info.end.handle)
|
||||
: Math.abs(info.handleArc.length)
|
||||
}
|
||||
|
||||
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: 'start',
|
||||
id: ARROW_HANDLES.START,
|
||||
type: 'vertex',
|
||||
index: 'a0',
|
||||
x: info.start.handle.x,
|
||||
|
@ -187,31 +165,52 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
canBind: true,
|
||||
},
|
||||
{
|
||||
id: 'middle',
|
||||
id: ARROW_HANDLES.MIDDLE,
|
||||
type: 'virtual',
|
||||
index: 'a2',
|
||||
x: info.middle.x,
|
||||
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: 'end',
|
||||
id: ARROW_HANDLES.END,
|
||||
type: 'vertex',
|
||||
index: 'a3',
|
||||
x: info.end.handle.x,
|
||||
y: info.end.handle.y,
|
||||
canBind: true,
|
||||
},
|
||||
]
|
||||
].filter(Boolean) as TLHandle[]
|
||||
}
|
||||
|
||||
override onHandleChange: TLOnHandleChangeHandler<TLArrowShape> = (
|
||||
shape,
|
||||
{ handle, isPrecise }
|
||||
) => {
|
||||
const handleId = handle.id as 'start' | 'middle' | 'end'
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (handleId === 'middle') {
|
||||
override onHandleDrag: TLOnHandleDragHandler<TLArrowShape> = (shape, { handle, isPrecise }) => {
|
||||
const handleId = handle.id as ARROW_HANDLES
|
||||
|
||||
if (handleId === ARROW_HANDLES.MIDDLE) {
|
||||
// Bending the arrow...
|
||||
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
|
||||
|
||||
|
@ -228,13 +227,46 @@ 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
|
||||
|
||||
const pageTransform = this.editor.getShapePageTransform(next.id)!
|
||||
const pointInPageSpace = pageTransform.applyToPoint(handle)
|
||||
|
||||
if (this.editor.inputs.ctrlKey) {
|
||||
// todo: maybe double check that this isn't equal to the other handle too?
|
||||
// Skip binding
|
||||
|
@ -271,6 +303,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
const targetGeometry = this.editor.getShapeGeometry(target)
|
||||
const targetBounds = Box.ZeroFix(targetGeometry.bounds)
|
||||
const pageTransform = this.editor.getShapePageTransform(next.id)!
|
||||
const pointInPageSpace = pageTransform.applyToPoint(handle)
|
||||
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
|
||||
|
||||
let precise = isPrecise
|
||||
|
@ -293,7 +327,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
// Double check that we're not going to be doing an imprecise snap on
|
||||
// the same shape twice, as this would result in a zero length line
|
||||
const otherHandle = next.props[handleId === 'start' ? 'end' : 'start']
|
||||
const otherHandle =
|
||||
next.props[handleId === ARROW_HANDLES.START ? ARROW_HANDLES.END : ARROW_HANDLES.START]
|
||||
if (
|
||||
otherHandle.type === 'binding' &&
|
||||
target.id === otherHandle.boundShapeId &&
|
||||
|
@ -381,7 +416,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}),
|
||||
})
|
||||
|
||||
for (const handleName of ['start', 'end'] as const) {
|
||||
for (const handleName of [ARROW_HANDLES.START, ARROW_HANDLES.END] as const) {
|
||||
const terminal = shape.props[handleName]
|
||||
if (terminal.type !== 'binding') continue
|
||||
result = {
|
||||
|
@ -539,7 +574,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
handle: TLHandle
|
||||
): TLShapePartial<TLArrowShape> | void => {
|
||||
switch (handle.id) {
|
||||
case 'start': {
|
||||
case ARROW_HANDLES.START: {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
|
@ -549,7 +584,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
},
|
||||
}
|
||||
}
|
||||
case 'end': {
|
||||
case ARROW_HANDLES.END: {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
|
@ -599,17 +634,11 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
if (onlySelectedShape === shape && shouldDisplayHandles) {
|
||||
const sw = 2
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
info.isStraight
|
||||
? Vec.Dist(info.start.handle, info.end.handle)
|
||||
: Math.abs(info.handleArc.length),
|
||||
sw,
|
||||
{
|
||||
end: 'skip',
|
||||
start: 'skip',
|
||||
lengthRatio: 2.5,
|
||||
}
|
||||
)
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(this.getLength(shape), sw, {
|
||||
end: 'skip',
|
||||
start: 'skip',
|
||||
lengthRatio: 2.5,
|
||||
})
|
||||
|
||||
handlePath =
|
||||
shape.props.start.type === 'binding' || shape.props.end.type === 'binding' ? (
|
||||
|
@ -650,9 +679,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
}
|
||||
)
|
||||
|
||||
const labelGeometry = shape.props.text.trim()
|
||||
? (this.editor.getShapeGeometry<Group2d>(shape).children[1] as Rectangle2d)
|
||||
: null
|
||||
const labelPosition = getArrowLabelPosition(this.editor, shape)
|
||||
|
||||
const maskStartArrowhead = !(
|
||||
info.start.arrowhead === 'none' || info.start.arrowhead === 'arrow'
|
||||
|
@ -676,12 +703,12 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
height={toDomPrecision(bounds.height + 200)}
|
||||
fill="white"
|
||||
/>
|
||||
{labelGeometry && (
|
||||
{shape.props.text.trim() && (
|
||||
<rect
|
||||
x={labelGeometry.x}
|
||||
y={labelGeometry.y}
|
||||
width={labelGeometry.w}
|
||||
height={labelGeometry.h}
|
||||
x={labelPosition.box.x}
|
||||
y={labelPosition.box.y}
|
||||
width={labelPosition.box.w}
|
||||
height={labelPosition.box.h}
|
||||
fill="black"
|
||||
rx={4}
|
||||
ry={4}
|
||||
|
@ -742,8 +769,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
text={shape.props.text}
|
||||
font={shape.props.font}
|
||||
size={shape.props.size}
|
||||
position={info.middle}
|
||||
width={labelGeometry?.w ?? 0}
|
||||
position={labelPosition.box.center}
|
||||
width={labelPosition.box.w}
|
||||
labelColor={theme[shape.props.labelColor].solid}
|
||||
/>
|
||||
</>
|
||||
|
|
337
packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts
Normal file
337
packages/tldraw/src/lib/shapes/arrow/arrowLabel.ts
Normal file
|
@ -0,0 +1,337 @@
|
|||
import {
|
||||
Arc2d,
|
||||
Box,
|
||||
Circle2d,
|
||||
Edge2d,
|
||||
Editor,
|
||||
Geometry2d,
|
||||
Polygon2d,
|
||||
TLArrowInfo,
|
||||
TLArrowShape,
|
||||
Vec,
|
||||
VecLike,
|
||||
angleDistance,
|
||||
clamp,
|
||||
getPointOnCircle,
|
||||
intersectCirclePolygon,
|
||||
intersectLineSegmentPolygon,
|
||||
} from '@tldraw/editor'
|
||||
import {
|
||||
ARROW_LABEL_FONT_SIZES,
|
||||
ARROW_LABEL_PADDING,
|
||||
FONT_FAMILIES,
|
||||
LABEL_TO_ARROW_PADDING,
|
||||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
|
||||
const labelSizeCache = new WeakMap<TLArrowShape, Vec>()
|
||||
|
||||
export function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||
const cachedSize = labelSizeCache.get(shape)
|
||||
if (cachedSize) return cachedSize
|
||||
|
||||
const info = editor.getArrowInfo(shape)!
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
const bodyGeom = info.isStraight
|
||||
? new Edge2d({
|
||||
start: Vec.From(info.start.point),
|
||||
end: Vec.From(info.end.point),
|
||||
})
|
||||
: new Arc2d({
|
||||
center: Vec.Cast(info.handleArc.center),
|
||||
radius: info.handleArc.radius,
|
||||
start: Vec.Cast(info.start.point),
|
||||
end: Vec.Cast(info.end.point),
|
||||
sweepFlag: info.bodyArc.sweepFlag,
|
||||
largeArcFlag: info.bodyArc.largeArcFlag,
|
||||
})
|
||||
|
||||
if (shape.props.text.trim()) {
|
||||
const bodyBounds = bodyGeom.bounds
|
||||
|
||||
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: null,
|
||||
})
|
||||
|
||||
width = w
|
||||
height = h
|
||||
|
||||
if (bodyBounds.width > bodyBounds.height) {
|
||||
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w))
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
|
||||
shape.props.text,
|
||||
{
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: width,
|
||||
}
|
||||
)
|
||||
|
||||
width = squishedWidth
|
||||
height = squishedHeight
|
||||
}
|
||||
|
||||
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
|
||||
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
|
||||
|
||||
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
|
||||
shape.props.text,
|
||||
{
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
||||
maxWidth: width,
|
||||
}
|
||||
)
|
||||
|
||||
width = squishedWidth
|
||||
height = squishedHeight
|
||||
}
|
||||
}
|
||||
|
||||
const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2)
|
||||
labelSizeCache.set(shape, size)
|
||||
return size
|
||||
}
|
||||
|
||||
function getLabelToArrowPadding(editor: Editor, shape: TLArrowShape) {
|
||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||
const labelToArrowPadding =
|
||||
LABEL_TO_ARROW_PADDING +
|
||||
(strokeWidth - STROKE_SIZES.s) * 2 +
|
||||
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)
|
||||
|
||||
return labelToArrowPadding
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the range of possible label positions for a straight arrow. The full possible range is 0
|
||||
* to 1, but as the label itself takes up space the usable range is smaller.
|
||||
*/
|
||||
function getStraightArrowLabelRange(
|
||||
editor: Editor,
|
||||
shape: TLArrowShape,
|
||||
info: Extract<TLArrowInfo, { isStraight: true }>
|
||||
): { start: number; end: number } {
|
||||
const labelSize = getArrowLabelSize(editor, shape)
|
||||
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
|
||||
|
||||
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
||||
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
|
||||
const endOffset = Vec.Nudge(info.end.point, info.start.point, labelToArrowPadding)
|
||||
|
||||
// assuming we just stick the label in the middle of the shape, where does the arrow intersect the label?
|
||||
const intersectionPoints = intersectLineSegmentPolygon(
|
||||
startOffset,
|
||||
endOffset,
|
||||
Box.FromCenter(info.middle, labelSize).corners
|
||||
)
|
||||
if (!intersectionPoints || intersectionPoints.length !== 2) {
|
||||
return { start: 0.5, end: 0.5 }
|
||||
}
|
||||
|
||||
// there should be two intersection points - one near the start, and one near the end
|
||||
let [startIntersect, endIntersect] = intersectionPoints
|
||||
if (Vec.Dist2(startIntersect, startOffset) > Vec.Dist2(endIntersect, startOffset)) {
|
||||
;[endIntersect, startIntersect] = intersectionPoints
|
||||
}
|
||||
|
||||
// take our nudged start and end points and scooch them in even further to give us the possible
|
||||
// range for the position of the _center_ of the label
|
||||
const startConstrained = startOffset.add(Vec.Sub(info.middle, startIntersect))
|
||||
const endConstrained = endOffset.add(Vec.Sub(info.middle, endIntersect))
|
||||
|
||||
// now we can work out the range of possible label positions
|
||||
const start = Vec.Dist(info.start.point, startConstrained) / info.length
|
||||
const end = Vec.Dist(info.start.point, endConstrained) / info.length
|
||||
return { start, end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the range of possible label positions for a curved arrow. The full possible range is 0
|
||||
* to 1, but as the label itself takes up space the usable range is smaller.
|
||||
*/
|
||||
function getCurvedArrowLabelRange(
|
||||
editor: Editor,
|
||||
shape: TLArrowShape,
|
||||
info: Extract<TLArrowInfo, { isStraight: false }>
|
||||
): { start: number; end: number; dbg?: Geometry2d[] } {
|
||||
const labelSize = getArrowLabelSize(editor, shape)
|
||||
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
|
||||
const direction = Math.sign(shape.props.bend)
|
||||
|
||||
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
||||
const labelToArrowPaddingRad = (labelToArrowPadding / info.handleArc.radius) * direction
|
||||
const startOffsetAngle = Vec.Angle(info.bodyArc.center, info.start.point) - labelToArrowPaddingRad
|
||||
const endOffsetAngle = Vec.Angle(info.bodyArc.center, info.end.point) + labelToArrowPaddingRad
|
||||
const startOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, startOffsetAngle)
|
||||
const endOffset = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, endOffsetAngle)
|
||||
|
||||
const dbg: Geometry2d[] = []
|
||||
|
||||
// unlike the straight arrow, we can't just stick the label in the middle of the shape when
|
||||
// we're working out the range. this is because as the label moves along the curve, the place
|
||||
// where the arrow intersects with label changes. instead, we have to stick the label center on
|
||||
// the `startOffset` (the start-most place where it can go), then find where it intersects with
|
||||
// the arc. because of the symmetry of the label rectangle, we can move the label to that new
|
||||
// center and take that as the start-most possible point.
|
||||
const startIntersections = intersectArcPolygon(
|
||||
info.bodyArc.center,
|
||||
info.bodyArc.radius,
|
||||
startOffsetAngle,
|
||||
endOffsetAngle,
|
||||
direction,
|
||||
Box.FromCenter(startOffset, labelSize).corners
|
||||
)
|
||||
|
||||
dbg.push(
|
||||
new Polygon2d({
|
||||
points: Box.FromCenter(startOffset, labelSize).corners,
|
||||
debugColor: 'lime',
|
||||
isFilled: false,
|
||||
ignore: true,
|
||||
})
|
||||
)
|
||||
|
||||
const endIntersections = intersectArcPolygon(
|
||||
info.bodyArc.center,
|
||||
info.bodyArc.radius,
|
||||
startOffsetAngle,
|
||||
endOffsetAngle,
|
||||
direction,
|
||||
Box.FromCenter(endOffset, labelSize).corners
|
||||
)
|
||||
|
||||
dbg.push(
|
||||
new Polygon2d({
|
||||
points: Box.FromCenter(endOffset, labelSize).corners,
|
||||
debugColor: 'lime',
|
||||
isFilled: false,
|
||||
ignore: true,
|
||||
})
|
||||
)
|
||||
for (const pt of [
|
||||
...(startIntersections ?? []),
|
||||
...(endIntersections ?? []),
|
||||
startOffset,
|
||||
endOffset,
|
||||
]) {
|
||||
dbg.push(
|
||||
new Circle2d({
|
||||
x: pt.x - 3,
|
||||
y: pt.y - 3,
|
||||
radius: 3,
|
||||
isFilled: false,
|
||||
debugColor: 'magenta',
|
||||
ignore: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// if we have one or more intersections (we shouldn't have more than two) then the one we need
|
||||
// is the one furthest from the arrow terminal
|
||||
const startConstrained =
|
||||
(startIntersections && furthest(info.start.point, startIntersections)) ?? info.middle
|
||||
const endConstrained =
|
||||
(endIntersections && furthest(info.end.point, endIntersections)) ?? info.middle
|
||||
|
||||
const startAngle = Vec.Angle(info.bodyArc.center, info.start.point)
|
||||
const endAngle = Vec.Angle(info.bodyArc.center, info.end.point)
|
||||
const constrainedStartAngle = Vec.Angle(info.bodyArc.center, startConstrained)
|
||||
const constrainedEndAngle = Vec.Angle(info.bodyArc.center, endConstrained)
|
||||
|
||||
// if the arc is small enough that there's no room for the label to move, we constrain it to the middle.
|
||||
if (
|
||||
angleDistance(startAngle, constrainedStartAngle, direction) >
|
||||
angleDistance(startAngle, constrainedEndAngle, direction)
|
||||
) {
|
||||
return { start: 0.5, end: 0.5, dbg }
|
||||
}
|
||||
|
||||
// now we can work out the range of possible label positions
|
||||
const fullDistance = angleDistance(startAngle, endAngle, direction)
|
||||
const start = angleDistance(startAngle, constrainedStartAngle, direction) / fullDistance
|
||||
const end = angleDistance(startAngle, constrainedEndAngle, direction) / fullDistance
|
||||
return { start, end, dbg }
|
||||
}
|
||||
|
||||
export function getArrowLabelPosition(editor: Editor, shape: TLArrowShape) {
|
||||
let labelCenter
|
||||
const debugGeom: Geometry2d[] = []
|
||||
const info = editor.getArrowInfo(shape)!
|
||||
|
||||
if (info.isStraight) {
|
||||
const range = getStraightArrowLabelRange(editor, shape, info)
|
||||
const clampedPosition = clamp(shape.props.labelPosition, range.start, range.end)
|
||||
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)
|
||||
const labelAngle = interpolateArcAngles(
|
||||
Vec.Angle(info.bodyArc.center, info.start.point),
|
||||
Vec.Angle(info.bodyArc.center, info.end.point),
|
||||
Math.sign(shape.props.bend),
|
||||
clampedPosition
|
||||
)
|
||||
labelCenter = getPointOnCircle(info.bodyArc.center, info.bodyArc.radius, labelAngle)
|
||||
}
|
||||
|
||||
const labelSize = getArrowLabelSize(editor, shape)
|
||||
|
||||
return { box: Box.FromCenter(labelCenter, labelSize), debugGeom }
|
||||
}
|
||||
|
||||
function intersectArcPolygon(
|
||||
center: VecLike,
|
||||
radius: number,
|
||||
angleStart: number,
|
||||
angleEnd: number,
|
||||
direction: number,
|
||||
polygon: VecLike[]
|
||||
) {
|
||||
const intersections = intersectCirclePolygon(center, radius, polygon)
|
||||
|
||||
// filter the circle intersections to just the ones from the arc
|
||||
const fullArcDistance = angleDistance(angleStart, angleEnd, direction)
|
||||
return intersections?.filter((pt) => {
|
||||
const pDistance = angleDistance(angleStart, Vec.Angle(center, pt), direction)
|
||||
return pDistance >= 0 && pDistance <= fullArcDistance
|
||||
})
|
||||
}
|
||||
|
||||
function furthest(from: VecLike, candidates: VecLike[]): VecLike | null {
|
||||
let furthest: VecLike | null = null
|
||||
let furthestDist = -Infinity
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const dist = Vec.Dist2(from, candidate)
|
||||
if (dist > furthestDist) {
|
||||
furthest = candidate
|
||||
furthestDist = dist
|
||||
}
|
||||
}
|
||||
|
||||
return furthest
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param angleStart - The angle of the start of the arc
|
||||
* @param angleEnd - The angle of the end of the arc
|
||||
* @param direction - The direction of the arc (1 = counter-clockwise, -1 = clockwise)
|
||||
* @param t - A number between 0 and 1 representing the position along the arc
|
||||
* @returns
|
||||
*/
|
||||
function interpolateArcAngles(angleStart: number, angleEnd: number, direction: number, t: number) {
|
||||
const dist = angleDistance(angleStart, angleEnd, direction)
|
||||
return angleStart + dist * t * direction * -1
|
||||
}
|
|
@ -104,7 +104,7 @@ export class Pointing extends StateNode {
|
|||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||
const initial = this.shape
|
||||
const startHandle = handles.find((h) => h.id === 'start')!
|
||||
const change = util.onHandleChange?.(shape, {
|
||||
const change = util.onHandleDrag?.(shape, {
|
||||
handle: { ...startHandle, x: 0, y: 0 },
|
||||
isPrecise: true,
|
||||
initial: initial,
|
||||
|
@ -141,7 +141,7 @@ export class Pointing extends StateNode {
|
|||
const initial = this.shape
|
||||
const point = this.editor.getPointInShapeSpace(shape, this.editor.inputs.currentPagePoint)
|
||||
const endHandle = handles.find((h) => h.id === 'end')!
|
||||
const change = util.onHandleChange?.(shapeWithOutEndOffset, {
|
||||
const change = util.onHandleDrag?.(shapeWithOutEndOffset, {
|
||||
handle: { ...endHandle, x: point.x, y: point.y },
|
||||
isPrecise: false, // sure about that?
|
||||
initial: initial,
|
||||
|
@ -161,7 +161,7 @@ export class Pointing extends StateNode {
|
|||
const util = this.editor.getShapeUtil<TLArrowShape>('arrow')
|
||||
const initial = this.shape
|
||||
const startHandle = handles.find((h) => h.id === 'start')!
|
||||
const change = util.onHandleChange?.(shapeWithOutEndOffset, {
|
||||
const change = util.onHandleDrag?.(shapeWithOutEndOffset, {
|
||||
handle: { ...startHandle, x: 0, y: 0 },
|
||||
isPrecise: this.didTimeout, // sure about that?
|
||||
initial: initial,
|
||||
|
|
|
@ -92,12 +92,7 @@ export function getPillPoints(width: number, height: number, numPoints: number)
|
|||
points.push(Vec.Add(section.start, Vec.Mul(section.delta, sectionOffset)))
|
||||
} else {
|
||||
points.push(
|
||||
getPointOnCircle(
|
||||
section.center.x,
|
||||
section.center.y,
|
||||
radius,
|
||||
section.startAngle + sectionOffset / radius
|
||||
)
|
||||
getPointOnCircle(section.center, radius, section.startAngle + sectionOffset / radius)
|
||||
)
|
||||
}
|
||||
sectionOffset += spacing
|
||||
|
@ -377,7 +372,7 @@ export function pointsOnArc(
|
|||
for (let i = 0; i < numPoints; i++) {
|
||||
const t = i / (numPoints - 1)
|
||||
const angle = startAngle + l * t
|
||||
const point = getPointOnCircle(center.x, center.y, radius, angle)
|
||||
const point = getPointOnCircle(center, radius, angle)
|
||||
results.push(point)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
ShapeUtil,
|
||||
TLHandle,
|
||||
TLLineShape,
|
||||
TLOnHandleChangeHandler,
|
||||
TLOnHandleDragHandler,
|
||||
TLOnResizeHandler,
|
||||
Vec,
|
||||
WeakMapCache,
|
||||
|
@ -129,7 +129,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
}
|
||||
}
|
||||
|
||||
override onHandleChange: TLOnHandleChangeHandler<TLLineShape> = (shape, { handle }) => {
|
||||
override onHandleDrag: TLOnHandleDragHandler<TLLineShape> = (shape, { handle }) => {
|
||||
const next = deepCopy(shape)
|
||||
|
||||
switch (handle.id) {
|
||||
|
|
|
@ -54,4 +54,8 @@ export const MIN_ARROW_LENGTH = 48
|
|||
/** @internal */
|
||||
export const BOUND_ARROW_OFFSET = 10
|
||||
/** @internal */
|
||||
export const LABEL_TO_ARROW_PADDING = 20
|
||||
/** @internal */
|
||||
export const ARROW_LABEL_PADDING = 4.25
|
||||
/** @internal */
|
||||
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
TLPointerEventInfo,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
TLUnknownShape,
|
||||
Vec,
|
||||
deepCopy,
|
||||
snapAngle,
|
||||
|
@ -109,6 +110,15 @@ 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)
|
||||
|
@ -169,6 +179,19 @@ export class DraggingHandle extends StateNode {
|
|||
this.parent.setCurrentToolIdMask(undefined)
|
||||
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 }
|
||||
|
@ -266,7 +289,7 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
const changes = util.onHandleChange?.(shape, {
|
||||
const changes = util.onHandleDrag?.(shape, {
|
||||
handle: {
|
||||
...initialHandle,
|
||||
x: point.x,
|
||||
|
|
|
@ -553,7 +553,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
})
|
||||
|
||||
const handles = editor.getShapeHandles(v2ShapeFresh)!
|
||||
const change = util.onHandleChange!(v2ShapeFresh, {
|
||||
const change = util.onHandleDrag!(v2ShapeFresh, {
|
||||
handle: {
|
||||
...handles.find((h) => h.id === handleId)!,
|
||||
x: point.x,
|
||||
|
|
|
@ -67,6 +67,7 @@ export const arrowShapeProps: {
|
|||
}, never>;
|
||||
bend: T.Validator<number>;
|
||||
text: T.Validator<string>;
|
||||
labelPosition: T.Validator<number>;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
@ -959,12 +960,16 @@ 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;
|
||||
|
|
|
@ -427,7 +427,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string>;\n}"
|
||||
"text": "<string>;\n labelPosition: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "T.Validator",
|
||||
"canonicalReference": "@tldraw/validate!Validator:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<number>;\n}"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/shapes/TLArrowShape.ts",
|
||||
|
@ -436,7 +445,7 @@
|
|||
"name": "arrowShapeProps",
|
||||
"variableTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 42
|
||||
"endIndex": 44
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -6018,6 +6027,33 @@
|
|||
"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",
|
||||
|
@ -6100,6 +6136,33 @@
|
|||
"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",
|
||||
|
|
|
@ -1771,6 +1771,32 @@ describe('add isPrecise to arrow handles', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('add AddLabelPosition to arrow handles', () => {
|
||||
const { up, down } = arrowShapeMigrations.migrators[ArrowMigrationVersions.AddLabelPosition]
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(
|
||||
up({
|
||||
props: {},
|
||||
})
|
||||
).toEqual({
|
||||
props: { labelPosition: 0.5 },
|
||||
})
|
||||
})
|
||||
|
||||
test('down works as expected', () => {
|
||||
expect(
|
||||
down({
|
||||
props: {
|
||||
labelPosition: 0.5,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
props: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const invalidUrl = 'invalid-url'
|
||||
const validUrl = ''
|
||||
|
||||
|
|
|
@ -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'] as const)
|
||||
export const TL_HANDLE_TYPES = new Set(['vertex', 'virtual', 'create', 'text-adjust'] as const)
|
||||
|
||||
/**
|
||||
* A type for the handle types used by tldraw's default shapes.
|
||||
|
@ -27,6 +27,8 @@ export interface TLHandle {
|
|||
index: string
|
||||
x: number
|
||||
y: number
|
||||
w?: number
|
||||
h?: number
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -38,4 +40,6 @@ 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),
|
||||
})
|
||||
|
|
|
@ -69,6 +69,7 @@ export const arrowShapeProps = {
|
|||
end: ArrowShapeTerminal,
|
||||
bend: T.number,
|
||||
text: T.string,
|
||||
labelPosition: T.number,
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -80,11 +81,12 @@ export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
|
|||
export const ArrowMigrationVersions = {
|
||||
AddLabelColor: 1,
|
||||
AddIsPrecise: 2,
|
||||
AddLabelPosition: 3,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const arrowShapeMigrations = defineMigrations({
|
||||
currentVersion: ArrowMigrationVersions.AddIsPrecise,
|
||||
currentVersion: ArrowMigrationVersions.AddLabelPosition,
|
||||
migrators: {
|
||||
[ArrowMigrationVersions.AddLabelColor]: {
|
||||
up: (record) => {
|
||||
|
@ -104,6 +106,7 @@ export const arrowShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
[ArrowMigrationVersions.AddIsPrecise]: {
|
||||
up: (record) => {
|
||||
const { start, end } = record.props
|
||||
|
@ -156,5 +159,24 @@ export const arrowShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
[ArrowMigrationVersions.AddLabelPosition]: {
|
||||
up: (record) => {
|
||||
return {
|
||||
...record,
|
||||
props: {
|
||||
...record.props,
|
||||
labelPosition: 0.5,
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record) => {
|
||||
const { labelPosition: _, ...props } = record.props
|
||||
return {
|
||||
...record,
|
||||
props,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -56,6 +56,7 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
|
|||
arrowheadEnd: 'arrow',
|
||||
text: '',
|
||||
font: 'draw',
|
||||
labelPosition: 0.5,
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue