Faster selection / erasing (#3454)
This PR makes a small improvement to the way we measure distances. (Often we measure distances multiple times per frame per shape on the screen). In many cases, we compare a minimum distance. This makes those checks faster by avoiding a square root. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features ### Release Notes - Improve performance of minimum distance checks.
This commit is contained in:
parent
152b915704
commit
3ceebc82f8
39 changed files with 441 additions and 539 deletions
|
@ -3,7 +3,6 @@ import {
|
||||||
DefaultContextMenuContent,
|
DefaultContextMenuContent,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawHandles,
|
TldrawHandles,
|
||||||
TldrawHoveredShapeIndicator,
|
|
||||||
TldrawScribble,
|
TldrawScribble,
|
||||||
TldrawSelectionBackground,
|
TldrawSelectionBackground,
|
||||||
TldrawSelectionForeground,
|
TldrawSelectionForeground,
|
||||||
|
@ -23,7 +22,6 @@ const defaultComponents = {
|
||||||
SelectionForeground: TldrawSelectionForeground,
|
SelectionForeground: TldrawSelectionForeground,
|
||||||
SelectionBackground: TldrawSelectionBackground,
|
SelectionBackground: TldrawSelectionBackground,
|
||||||
Handles: TldrawHandles,
|
Handles: TldrawHandles,
|
||||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//[2]
|
//[2]
|
||||||
|
|
|
@ -118,7 +118,7 @@ export class Arc2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
length: number;
|
length: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -332,7 +332,7 @@ export class Circle2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
nearestPoint(point: Vec): Vec;
|
nearestPoint(point: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -414,7 +414,7 @@ export class CubicSpline2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
get length(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -471,9 +471,6 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const DefaultHandles: ({ children }: TLHandlesProps) => JSX_2.Element;
|
export const DefaultHandles: ({ children }: TLHandlesProps) => JSX_2.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export function DefaultHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps): JSX_2.Element | null;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function DefaultScribble({ scribble, zoom, color, opacity, className }: TLScribbleProps): JSX_2.Element | null;
|
export function DefaultScribble({ scribble, zoom, color, opacity, className }: TLScribbleProps): JSX_2.Element | null;
|
||||||
|
|
||||||
|
@ -552,7 +549,7 @@ export class Edge2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
get length(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -968,7 +965,7 @@ export class Ellipse2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
h: number;
|
h: number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
nearestPoint(A: Vec): Vec;
|
nearestPoint(A: Vec): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1458,7 +1455,7 @@ export class Polyline2d extends Geometry2d {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getVertices(): Vec[];
|
getVertices(): Vec[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get length(): number;
|
get length(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -2302,11 +2299,6 @@ export type TLHistoryMark = {
|
||||||
onRedo: boolean;
|
onRedo: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLHoveredShapeIndicatorProps = {
|
|
||||||
shapeId: TLShapeId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
|
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
|
||||||
|
|
||||||
|
@ -2539,6 +2531,7 @@ export type TLShapeIndicatorProps = {
|
||||||
color?: string | undefined;
|
color?: string | undefined;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -2726,7 +2719,6 @@ export function useEditorComponents(): Partial<{
|
||||||
Spinner: ComponentType | null;
|
Spinner: ComponentType | null;
|
||||||
SelectionForeground: ComponentType<TLSelectionForegroundProps> | null;
|
SelectionForeground: ComponentType<TLSelectionForegroundProps> | null;
|
||||||
SelectionBackground: ComponentType<TLSelectionBackgroundProps> | null;
|
SelectionBackground: ComponentType<TLSelectionBackgroundProps> | null;
|
||||||
HoveredShapeIndicator: ComponentType<TLHoveredShapeIndicatorProps> | null;
|
|
||||||
OnTheCanvas: ComponentType | null;
|
OnTheCanvas: ComponentType | null;
|
||||||
InFrontOfTheCanvas: ComponentType | null;
|
InFrontOfTheCanvas: ComponentType | null;
|
||||||
LoadingScreen: ComponentType | null;
|
LoadingScreen: ComponentType | null;
|
||||||
|
@ -2850,6 +2842,8 @@ export class Vec {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number;
|
static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
static DistMin(A: VecLike, B: VecLike, n: number): boolean;
|
||||||
|
// (undocumented)
|
||||||
static Div(A: VecLike, t: number): Vec;
|
static Div(A: VecLike, t: number): Vec;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
div(t: number): this;
|
div(t: number): this;
|
||||||
|
|
|
@ -605,14 +605,6 @@
|
||||||
"text": "Vec",
|
"text": "Vec",
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ", _zoom: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "number"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "): "
|
"text": "): "
|
||||||
|
@ -628,8 +620,8 @@
|
||||||
],
|
],
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 7,
|
"startIndex": 5,
|
||||||
"endIndex": 8
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -650,14 +642,6 @@
|
||||||
"endIndex": 4
|
"endIndex": 4
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameterName": "_zoom",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 5,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -4438,7 +4422,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ", _zoom: "
|
"text": ", distance?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -4483,12 +4467,12 @@
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameterName": "_zoom",
|
"parameterName": "distance",
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 5,
|
"startIndex": 5,
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -5746,14 +5730,6 @@
|
||||||
"text": "Vec",
|
"text": "Vec",
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ", zoom: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "number"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "): "
|
"text": "): "
|
||||||
|
@ -5769,8 +5745,8 @@
|
||||||
],
|
],
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 7,
|
"startIndex": 5,
|
||||||
"endIndex": 8
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -5791,14 +5767,6 @@
|
||||||
"endIndex": 4
|
"endIndex": 4
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameterName": "zoom",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 5,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -6449,61 +6417,6 @@
|
||||||
],
|
],
|
||||||
"name": "DefaultHandles"
|
"name": "DefaultHandles"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Function",
|
|
||||||
"canonicalReference": "@tldraw/editor!DefaultHoveredShapeIndicator:function(1)",
|
|
||||||
"docComment": "/**\n * @public\n */\n",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "export declare function DefaultHoveredShapeIndicator({ shapeId }: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "TLHoveredShapeIndicatorProps",
|
|
||||||
"canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "): "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "import(\"react/jsx-runtime\")."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "JSX.Element",
|
|
||||||
"canonicalReference": "@types/react!JSX.Element:interface"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": " | null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultHoveredShapeIndicator.tsx",
|
|
||||||
"returnTypeTokenRange": {
|
|
||||||
"startIndex": 3,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"overloadIndex": 1,
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"parameterName": "{ shapeId }",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 2
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "DefaultHoveredShapeIndicator"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Function",
|
"kind": "Function",
|
||||||
"canonicalReference": "@tldraw/editor!DefaultScribble:function(1)",
|
"canonicalReference": "@tldraw/editor!DefaultScribble:function(1)",
|
||||||
|
@ -7134,7 +7047,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ", _zoom: "
|
"text": ", distance?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -7179,12 +7092,12 @@
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameterName": "_zoom",
|
"parameterName": "distance",
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 5,
|
"startIndex": 5,
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -20311,14 +20224,6 @@
|
||||||
"text": "Vec",
|
"text": "Vec",
|
||||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ", zoom: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "number"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "): "
|
"text": "): "
|
||||||
|
@ -20334,8 +20239,8 @@
|
||||||
],
|
],
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 7,
|
"startIndex": 5,
|
||||||
"endIndex": 8
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -20356,14 +20261,6 @@
|
||||||
"endIndex": 4
|
"endIndex": 4
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameterName": "zoom",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 5,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -28466,7 +28363,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ", zoom: "
|
"text": ", distance?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -28511,12 +28408,12 @@
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameterName": "zoom",
|
"parameterName": "distance",
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 5,
|
"startIndex": 5,
|
||||||
"endIndex": 6
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -39669,41 +39566,6 @@
|
||||||
"endIndex": 2
|
"endIndex": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "TypeAlias",
|
|
||||||
"canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type",
|
|
||||||
"docComment": "/**\n * @public\n */\n",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "export type TLHoveredShapeIndicatorProps = "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "{\n shapeId: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "TLShapeId",
|
|
||||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";\n}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultHoveredShapeIndicator.tsx",
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"name": "TLHoveredShapeIndicatorProps",
|
|
||||||
"typeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "TypeAlias",
|
"kind": "TypeAlias",
|
||||||
"canonicalReference": "@tldraw/editor!TLInterruptEvent:type",
|
"canonicalReference": "@tldraw/editor!TLInterruptEvent:type",
|
||||||
|
@ -41940,7 +41802,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n color?: string | undefined;\n opacity?: number;\n className?: string;\n}"
|
"text": ";\n color?: string | undefined;\n opacity?: number;\n className?: string;\n hidden?: boolean;\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -43835,24 +43697,6 @@
|
||||||
"text": "TLSelectionBackgroundProps",
|
"text": "TLSelectionBackgroundProps",
|
||||||
"canonicalReference": "@tldraw/editor!TLSelectionBackgroundProps:type"
|
"canonicalReference": "@tldraw/editor!TLSelectionBackgroundProps:type"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "> | null;\n HoveredShapeIndicator: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "ComponentType",
|
|
||||||
"canonicalReference": "@types/react!React.ComponentType:type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "TLHoveredShapeIndicatorProps",
|
|
||||||
"canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "> | null;\n OnTheCanvas: "
|
"text": "> | null;\n OnTheCanvas: "
|
||||||
|
@ -43906,7 +43750,7 @@
|
||||||
"fileUrlPath": "packages/editor/src/lib/hooks/useEditorComponents.tsx",
|
"fileUrlPath": "packages/editor/src/lib/hooks/useEditorComponents.tsx",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 90
|
"endIndex": 86
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -46051,6 +45895,88 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "DistanceToLineThroughPoint"
|
"name": "DistanceToLineThroughPoint"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/editor!Vec.DistMin:member(1)",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "static DistMin(A: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "VecLike",
|
||||||
|
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", B: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "VecLike",
|
||||||
|
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", n: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isStatic": true,
|
||||||
|
"returnTypeTokenRange": {
|
||||||
|
"startIndex": 7,
|
||||||
|
"endIndex": 8
|
||||||
|
},
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"isProtected": false,
|
||||||
|
"overloadIndex": 1,
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"parameterName": "A",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "B",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "n",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 5,
|
||||||
|
"endIndex": 6
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOptional": false,
|
||||||
|
"isAbstract": false,
|
||||||
|
"name": "DistMin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Method",
|
"kind": "Method",
|
||||||
"canonicalReference": "@tldraw/editor!Vec#div:member(1)",
|
"canonicalReference": "@tldraw/editor!Vec#div:member(1)",
|
||||||
|
|
|
@ -465,7 +465,7 @@ input,
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-width: calc(1.5px * var(--tl-scale));
|
stroke-width: calc(1.5px * var(--tl-scale));
|
||||||
contain: size;
|
contain: size layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------ SelectionBox ------------------ */
|
/* ------------------ SelectionBox ------------------ */
|
||||||
|
|
|
@ -62,10 +62,6 @@ export {
|
||||||
DefaultHandles,
|
DefaultHandles,
|
||||||
type TLHandlesProps,
|
type TLHandlesProps,
|
||||||
} from './lib/components/default-components/DefaultHandles'
|
} from './lib/components/default-components/DefaultHandles'
|
||||||
export {
|
|
||||||
DefaultHoveredShapeIndicator,
|
|
||||||
type TLHoveredShapeIndicatorProps,
|
|
||||||
} from './lib/components/default-components/DefaultHoveredShapeIndicator'
|
|
||||||
export {
|
export {
|
||||||
DefaultScribble,
|
DefaultScribble,
|
||||||
type TLScribbleProps,
|
type TLScribbleProps,
|
||||||
|
|
|
@ -151,8 +151,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
||||||
<BrushWrapper />
|
<BrushWrapper />
|
||||||
<ScribbleWrapper />
|
<ScribbleWrapper />
|
||||||
<ZoomBrushWrapper />
|
<ZoomBrushWrapper />
|
||||||
<SelectedIdIndicators />
|
<ShapeIndicators />
|
||||||
<HoveredShapeIndicator />
|
|
||||||
<HintedShapeIndicator />
|
<HintedShapeIndicator />
|
||||||
<SnapIndicatorWrapper />
|
<SnapIndicatorWrapper />
|
||||||
<SelectionForegroundWrapper />
|
<SelectionForegroundWrapper />
|
||||||
|
@ -431,16 +430,17 @@ function ShapesToDisplay() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectedIdIndicators() {
|
function ShapeIndicators() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const selectedShapeIds = useValue('selectedShapeIds', () => editor.getSelectedShapeIds(), [
|
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
|
||||||
editor,
|
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
|
||||||
])
|
const idsToDisplay = useValue(
|
||||||
const shouldDisplay = useValue(
|
|
||||||
'should display selected ids',
|
'should display selected ids',
|
||||||
() => {
|
() => {
|
||||||
// todo: move to tldraw selected ids wrapper
|
// todo: move to tldraw selected ids wrappe
|
||||||
return (
|
const prev = rPreviousSelectedShapeIds.current
|
||||||
|
const next = new Set<TLShapeId>()
|
||||||
|
if (
|
||||||
editor.isInAny(
|
editor.isInAny(
|
||||||
'select.idle',
|
'select.idle',
|
||||||
'select.brushing',
|
'select.brushing',
|
||||||
|
@ -449,52 +449,51 @@ function SelectedIdIndicators() {
|
||||||
'select.pointing_shape',
|
'select.pointing_shape',
|
||||||
'select.pointing_selection',
|
'select.pointing_selection',
|
||||||
'select.pointing_handle'
|
'select.pointing_handle'
|
||||||
) && !editor.getInstanceState().isChangingStyle
|
) &&
|
||||||
)
|
!editor.getInstanceState().isChangingStyle
|
||||||
|
) {
|
||||||
|
const selected = editor.getSelectedShapeIds()
|
||||||
|
for (const id of selected) {
|
||||||
|
next.add(id)
|
||||||
|
}
|
||||||
|
if (editor.isInAny('select.idle', 'select.editing_shape')) {
|
||||||
|
const instanceState = editor.getInstanceState()
|
||||||
|
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
|
||||||
|
const hovered = editor.getHoveredShapeId()
|
||||||
|
if (hovered) next.add(hovered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.size !== next.size) {
|
||||||
|
rPreviousSelectedShapeIds.current = next
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of next) {
|
||||||
|
if (!prev.has(id)) {
|
||||||
|
rPreviousSelectedShapeIds.current = next
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev
|
||||||
},
|
},
|
||||||
[editor]
|
[editor]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { ShapeIndicator } = useEditorComponents()
|
const { ShapeIndicator } = useEditorComponents()
|
||||||
|
|
||||||
if (!ShapeIndicator) return null
|
if (!ShapeIndicator) return null
|
||||||
if (!shouldDisplay) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedShapeIds.map((id) => (
|
{renderingShapes.map(({ id }) => (
|
||||||
<ShapeIndicator
|
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
|
||||||
key={id + '_indicator'}
|
|
||||||
className="tl-user-indicator__selected"
|
|
||||||
shapeId={id}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const HoveredShapeIndicator = function HoveredShapeIndicator() {
|
|
||||||
const editor = useEditor()
|
|
||||||
const { HoveredShapeIndicator } = useEditorComponents()
|
|
||||||
const isCoarsePointer = useValue(
|
|
||||||
'coarse pointer',
|
|
||||||
() => editor.getInstanceState().isCoarsePointer,
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
const isHoveringCanvas = useValue(
|
|
||||||
'hovering canvas',
|
|
||||||
() => editor.getInstanceState().isHoveringCanvas,
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
const hoveredShapeId = useValue('hovered id', () => editor.getCurrentPageState().hoveredShapeId, [
|
|
||||||
editor,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (isCoarsePointer || !isHoveringCanvas || !hoveredShapeId || !HoveredShapeIndicator) return null
|
|
||||||
|
|
||||||
return <HoveredShapeIndicator shapeId={hoveredShapeId} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function HintedShapeIndicator() {
|
function HintedShapeIndicator() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const { ShapeIndicator } = useEditorComponents()
|
const { ShapeIndicator } = useEditorComponents()
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { TLShapeId } from '@tldraw/tlschema'
|
|
||||||
import { useEditorComponents } from '../../hooks/useEditorComponents'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLHoveredShapeIndicatorProps = {
|
|
||||||
shapeId: TLShapeId
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function DefaultHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps) {
|
|
||||||
const { ShapeIndicator } = useEditorComponents()
|
|
||||||
if (!ShapeIndicator) return null
|
|
||||||
return <ShapeIndicator className="tl-user-indicator__hovered" shapeId={shapeId} />
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useStateTracking, useValue } from '@tldraw/state'
|
import { useQuickReactor, useStateTracking, useValue } from '@tldraw/state'
|
||||||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { memo } from 'react'
|
import { memo, useLayoutEffect, useRef } from 'react'
|
||||||
import type { Editor } from '../../editor/Editor'
|
import type { Editor } from '../../editor/Editor'
|
||||||
import { ShapeUtil } from '../../editor/shapes/ShapeUtil'
|
import { ShapeUtil } from '../../editor/shapes/ShapeUtil'
|
||||||
import { useEditor } from '../../hooks/useEditor'
|
import { useEditor } from '../../hooks/useEditor'
|
||||||
|
@ -38,6 +38,7 @@ export type TLShapeIndicatorProps = {
|
||||||
color?: string | undefined
|
color?: string | undefined
|
||||||
opacity?: number
|
opacity?: number
|
||||||
className?: string
|
className?: string
|
||||||
|
hidden?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -45,28 +46,34 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
|
||||||
shapeId,
|
shapeId,
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
|
hidden,
|
||||||
opacity,
|
opacity,
|
||||||
}: TLShapeIndicatorProps) {
|
}: TLShapeIndicatorProps) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const transform = useValue(
|
const rIndicator = useRef<SVGSVGElement>(null)
|
||||||
|
|
||||||
|
useQuickReactor(
|
||||||
'indicator transform',
|
'indicator transform',
|
||||||
() => {
|
() => {
|
||||||
|
const elm = rIndicator.current
|
||||||
|
if (!elm) return
|
||||||
const pageTransform = editor.getShapePageTransform(shapeId)
|
const pageTransform = editor.getShapePageTransform(shapeId)
|
||||||
if (!pageTransform) return ''
|
if (!pageTransform) return
|
||||||
return pageTransform.toCssString()
|
elm.style.setProperty('transform', pageTransform.toCssString())
|
||||||
},
|
},
|
||||||
[editor, shapeId]
|
[editor, shapeId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const elm = rIndicator.current
|
||||||
|
if (!elm) return
|
||||||
|
elm.style.setProperty('display', hidden ? 'none' : 'block')
|
||||||
|
}, [hidden])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className={classNames('tl-overlays__item', className)}>
|
<svg ref={rIndicator} className={classNames('tl-overlays__item', className)}>
|
||||||
<g
|
<g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
|
||||||
className="tl-shape-indicator"
|
|
||||||
transform={transform}
|
|
||||||
stroke={color ?? 'var(--color-selected)'}
|
|
||||||
opacity={opacity}
|
|
||||||
>
|
|
||||||
<InnerIndicator editor={editor} id={shapeId} />
|
<InnerIndicator editor={editor} id={shapeId} />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -4490,7 +4490,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
||||||
isPointInShape(
|
isPointInShape(
|
||||||
shape: TLShape | TLShapeId,
|
shape: TLShape | TLShapeId,
|
||||||
point: VecLike,
|
point: VecLike,
|
||||||
|
@ -8377,7 +8376,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
private _pendingEventsForNextTick: TLEventInfo[] = []
|
private _pendingEventsForNextTick: TLEventInfo[] = []
|
||||||
|
|
||||||
private _flushEventsForTick = (elapsed: number) => {
|
private _flushEventsForTick(elapsed: number) {
|
||||||
this.batch(() => {
|
this.batch(() => {
|
||||||
if (this._pendingEventsForNextTick.length > 0) {
|
if (this._pendingEventsForNextTick.length > 0) {
|
||||||
const events = [...this._pendingEventsForNextTick]
|
const events = [...this._pendingEventsForNextTick]
|
||||||
|
|
|
@ -133,10 +133,8 @@ export class HandleSnaps {
|
||||||
let minDistanceForSnapPoint = snapThreshold
|
let minDistanceForSnapPoint = snapThreshold
|
||||||
let nearestSnapPoint: Vec | null = null
|
let nearestSnapPoint: Vec | null = null
|
||||||
for (const snapPoint of this.iterateSnapPointsInPageSpace(currentShapeId, handle)) {
|
for (const snapPoint of this.iterateSnapPointsInPageSpace(currentShapeId, handle)) {
|
||||||
const distance = Vec.Dist(handleInPageSpace, snapPoint)
|
if (Vec.DistMin(handleInPageSpace, snapPoint, minDistanceForSnapPoint)) {
|
||||||
|
minDistanceForSnapPoint = Vec.Dist(handleInPageSpace, snapPoint)
|
||||||
if (distance < minDistanceForSnapPoint) {
|
|
||||||
minDistanceForSnapPoint = distance
|
|
||||||
nearestSnapPoint = snapPoint
|
nearestSnapPoint = snapPoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,10 +152,9 @@ export class HandleSnaps {
|
||||||
|
|
||||||
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace)
|
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace)
|
||||||
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
||||||
const distance = Vec.Dist(handleInPageSpace, nearestInPageSpace)
|
|
||||||
|
|
||||||
if (distance < minDistanceForOutline) {
|
if (Vec.DistMin(handleInPageSpace, nearestInPageSpace, minDistanceForOutline)) {
|
||||||
minDistanceForOutline = distance
|
minDistanceForOutline = Vec.Dist(handleInPageSpace, nearestInPageSpace)
|
||||||
nearestPointOnOutline = nearestInPageSpace
|
nearestPointOnOutline = nearestInPageSpace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,8 +263,7 @@ export function getCurvedArrowInfo(
|
||||||
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
||||||
}
|
}
|
||||||
|
|
||||||
const distAB = Vec.Dist(tA, tB)
|
if (Vec.DistMin(tA, tB, minLength)) {
|
||||||
if (distAB < minLength) {
|
|
||||||
if (offsetA !== 0 && offsetB !== 0) {
|
if (offsetA !== 0 && offsetB !== 0) {
|
||||||
offsetA *= -1.5
|
offsetA *= -1.5
|
||||||
offsetB *= -1.5
|
offsetB *= -1.5
|
||||||
|
|
|
@ -152,9 +152,8 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
|
||||||
|
|
||||||
const tA = a.clone().add(u.clone().mul(offsetA * (didFlip ? -1 : 1)))
|
const tA = a.clone().add(u.clone().mul(offsetA * (didFlip ? -1 : 1)))
|
||||||
const tB = b.clone().sub(u.clone().mul(offsetB * (didFlip ? -1 : 1)))
|
const tB = b.clone().sub(u.clone().mul(offsetB * (didFlip ? -1 : 1)))
|
||||||
const distAB = Vec.Dist(tA, tB)
|
|
||||||
|
|
||||||
if (distAB < minLength) {
|
if (Vec.DistMin(tA, tB, minLength)) {
|
||||||
if (offsetA !== 0 && offsetB !== 0) {
|
if (offsetA !== 0 && offsetB !== 0) {
|
||||||
// both bound + offset
|
// both bound + offset
|
||||||
offsetA *= -1.5
|
offsetA *= -1.5
|
||||||
|
@ -241,7 +240,7 @@ function updateArrowheadPointWithBoundShape(
|
||||||
|
|
||||||
if (intersection !== null) {
|
if (intersection !== null) {
|
||||||
targetInt =
|
targetInt =
|
||||||
intersection.sort((p1, p2) => Vec.Dist(p1, targetFrom) - Vec.Dist(p2, targetFrom))[0] ??
|
intersection.sort((p1, p2) => Vec.Dist2(p1, targetFrom) - Vec.Dist2(p2, targetFrom))[0] ??
|
||||||
(isClosed ? undefined : targetTo)
|
(isClosed ? undefined : targetTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,10 +146,14 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
||||||
|
|
||||||
handleEvent = (info: Exclude<TLEventInfo, TLPinchEventInfo>) => {
|
handleEvent = (info: Exclude<TLEventInfo, TLPinchEventInfo>) => {
|
||||||
const cbName = EVENT_NAME_MAP[info.name]
|
const cbName = EVENT_NAME_MAP[info.name]
|
||||||
const x = this.getCurrent()
|
const currentActiveChild = this._current.__unsafe__getWithoutCapture()
|
||||||
this[cbName]?.(info as any)
|
this[cbName]?.(info as any)
|
||||||
if (this.getCurrent() === x && this.getIsActive()) {
|
if (
|
||||||
x?.handleEvent(info)
|
this._isActive.__unsafe__getWithoutCapture() &&
|
||||||
|
currentActiveChild &&
|
||||||
|
currentActiveChild === this._current.__unsafe__getWithoutCapture()
|
||||||
|
) {
|
||||||
|
currentActiveChild.handleEvent(info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,6 @@ import {
|
||||||
import { DefaultGrid, TLGridProps } from '../components/default-components/DefaultGrid'
|
import { DefaultGrid, TLGridProps } from '../components/default-components/DefaultGrid'
|
||||||
import { DefaultHandle, TLHandleProps } from '../components/default-components/DefaultHandle'
|
import { DefaultHandle, TLHandleProps } from '../components/default-components/DefaultHandle'
|
||||||
import { DefaultHandles, TLHandlesProps } from '../components/default-components/DefaultHandles'
|
import { DefaultHandles, TLHandlesProps } from '../components/default-components/DefaultHandles'
|
||||||
import {
|
|
||||||
DefaultHoveredShapeIndicator,
|
|
||||||
TLHoveredShapeIndicatorProps,
|
|
||||||
} from '../components/default-components/DefaultHoveredShapeIndicator'
|
|
||||||
import { DefaultScribble, TLScribbleProps } from '../components/default-components/DefaultScribble'
|
import { DefaultScribble, TLScribbleProps } from '../components/default-components/DefaultScribble'
|
||||||
import {
|
import {
|
||||||
DefaultSelectionBackground,
|
DefaultSelectionBackground,
|
||||||
|
@ -71,7 +67,6 @@ export interface BaseEditorComponents {
|
||||||
Spinner: ComponentType
|
Spinner: ComponentType
|
||||||
SelectionForeground: ComponentType<TLSelectionForegroundProps>
|
SelectionForeground: ComponentType<TLSelectionForegroundProps>
|
||||||
SelectionBackground: ComponentType<TLSelectionBackgroundProps>
|
SelectionBackground: ComponentType<TLSelectionBackgroundProps>
|
||||||
HoveredShapeIndicator: ComponentType<TLHoveredShapeIndicatorProps>
|
|
||||||
OnTheCanvas: ComponentType
|
OnTheCanvas: ComponentType
|
||||||
InFrontOfTheCanvas: ComponentType
|
InFrontOfTheCanvas: ComponentType
|
||||||
LoadingScreen: ComponentType
|
LoadingScreen: ComponentType
|
||||||
|
@ -129,7 +124,6 @@ export function EditorComponentsProvider({
|
||||||
Spinner: DefaultSpinner,
|
Spinner: DefaultSpinner,
|
||||||
SelectionBackground: DefaultSelectionBackground,
|
SelectionBackground: DefaultSelectionBackground,
|
||||||
SelectionForeground: DefaultSelectionForeground,
|
SelectionForeground: DefaultSelectionForeground,
|
||||||
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
|
|
||||||
ShapeIndicator: DefaultShapeIndicator,
|
ShapeIndicator: DefaultShapeIndicator,
|
||||||
OnTheCanvas: null,
|
OnTheCanvas: null,
|
||||||
InFrontOfTheCanvas: null,
|
InFrontOfTheCanvas: null,
|
||||||
|
|
|
@ -317,6 +317,11 @@ export class Vec {
|
||||||
return Math.hypot(A.y - B.y, A.x - B.x)
|
return Math.hypot(A.y - B.y, A.x - B.x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get whether a distance between two points is less than a number. This is faster to calulate than using `Vec.Dist(a, b) < n`.
|
||||||
|
static DistMin(A: VecLike, B: VecLike, n: number): boolean {
|
||||||
|
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y) < n ** 2
|
||||||
|
}
|
||||||
|
|
||||||
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
|
// Get the squared distance between two points. This is faster to calculate (no square root) so useful for "minimum distance" checks where the actual measurement does not matter.
|
||||||
static Dist2(A: VecLike, B: VecLike): number {
|
static Dist2(A: VecLike, B: VecLike): number {
|
||||||
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)
|
return (A.x - B.x) * (A.x - B.x) + (A.y - B.y) * (A.y - B.y)
|
||||||
|
@ -416,11 +421,12 @@ export class Vec {
|
||||||
}
|
}
|
||||||
|
|
||||||
static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec {
|
static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec {
|
||||||
|
if (Vec.Equals(A, P)) return Vec.From(P)
|
||||||
|
if (Vec.Equals(B, P)) return Vec.From(P)
|
||||||
|
|
||||||
const u = Vec.Tan(B, A)
|
const u = Vec.Tan(B, A)
|
||||||
const C = Vec.Add(A, Vec.Mul(u, Vec.Sub(P, A).pry(u)))
|
const C = Vec.Add(A, Vec.Mul(u, Vec.Sub(P, A).pry(u)))
|
||||||
|
|
||||||
// todo: fix error P is B or A, which leads to a NaN value
|
|
||||||
|
|
||||||
if (clamp) {
|
if (clamp) {
|
||||||
if (C.x < Math.min(A.x, B.x)) return Vec.Cast(A.x < B.x ? A : B)
|
if (C.x < Math.min(A.x, B.x)) return Vec.Cast(A.x < B.x ? A : B)
|
||||||
if (C.x > Math.max(A.x, B.x)) return Vec.Cast(A.x > B.x ? A : B)
|
if (C.x > Math.max(A.x, B.x)) return Vec.Cast(A.x > B.x ? A : B)
|
||||||
|
|
|
@ -66,7 +66,7 @@ export class Arc2d extends Geometry2d {
|
||||||
return nearest
|
return nearest
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||||
const { _center, radius, measure, angleStart, angleEnd } = this
|
const { _center, radius, measure, angleStart, angleEnd } = this
|
||||||
const intersection = intersectLineSegmentCircle(A, B, _center, radius)
|
const intersection = intersectLineSegmentCircle(A, B, _center, radius)
|
||||||
if (intersection === null) return false
|
if (intersection === null) return false
|
||||||
|
|
|
@ -49,8 +49,8 @@ export class Circle2d extends Geometry2d {
|
||||||
return _center.clone().add(point.clone().sub(_center).uni().mul(radius))
|
return _center.clone().add(point.clone().sub(_center).uni().mul(radius))
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||||
const { _center, radius } = this
|
const { _center, radius } = this
|
||||||
return intersectLineSegmentCircle(A, B, _center, radius) !== null
|
return intersectLineSegmentCircle(A, B, _center, radius + distance) !== null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ export class CubicSpline2d extends Geometry2d {
|
||||||
return nearest
|
return nearest
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||||
return this.segments.some((segment) => segment.hitTestLineSegment(A, B, zoom))
|
return this.segments.some((segment) => segment.hitTestLineSegment(A, B))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,9 @@ export class Edge2d extends Geometry2d {
|
||||||
return new Vec(cx, cy)
|
return new Vec(cx, cy)
|
||||||
}
|
}
|
||||||
|
|
||||||
override hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
override hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||||
return linesIntersect(A, B, this.start, this.end)
|
return (
|
||||||
|
linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,8 +90,8 @@ export class Ellipse2d extends Geometry2d {
|
||||||
return nearest
|
return nearest
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||||
return this.edges.some((edge) => edge.hitTestLineSegment(A, B, zoom))
|
return this.edges.some((edge) => edge.hitTestLineSegment(A, B))
|
||||||
}
|
}
|
||||||
|
|
||||||
getBounds() {
|
getBounds() {
|
||||||
|
|
|
@ -30,24 +30,46 @@ export abstract class Geometry2d {
|
||||||
|
|
||||||
abstract nearestPoint(point: Vec): Vec
|
abstract nearestPoint(point: Vec): Vec
|
||||||
|
|
||||||
|
// hitTestPoint(point: Vec, margin = 0, hitInside = false) {
|
||||||
|
// // We've removed the broad phase here; that should be done outside of the call
|
||||||
|
// return this.distanceToPoint(point, hitInside) <= margin
|
||||||
|
// }
|
||||||
|
|
||||||
hitTestPoint(point: Vec, margin = 0, hitInside = false) {
|
hitTestPoint(point: Vec, margin = 0, hitInside = false) {
|
||||||
// We've removed the broad phase here; that should be done outside of the call
|
// First check whether the point is inside
|
||||||
return this.distanceToPoint(point, hitInside) <= margin
|
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Then check whether the distance is within the margin
|
||||||
|
return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin
|
||||||
}
|
}
|
||||||
|
|
||||||
distanceToPoint(point: Vec, hitInside = false) {
|
distanceToPoint(point: Vec, hitInside = false) {
|
||||||
const dist = point.dist(this.nearestPoint(point))
|
return (
|
||||||
|
point.dist(this.nearestPoint(point)) *
|
||||||
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
|
(this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
|
||||||
return -dist
|
? -1
|
||||||
}
|
: 1)
|
||||||
return dist
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
distanceToLineSegment(A: Vec, B: Vec) {
|
distanceToLineSegment(A: Vec, B: Vec) {
|
||||||
const point = this.nearestPointOnLineSegment(A, B)
|
if (A.equals(B)) return this.distanceToPoint(A)
|
||||||
const dist = Vec.DistanceToLineSegment(A, B, point) // repeated, bleh
|
const { vertices } = this
|
||||||
return this.isClosed && this.isFilled && pointInPolygon(point, this.vertices) ? -dist : dist
|
let nearest: Vec | undefined
|
||||||
|
let dist = Infinity
|
||||||
|
let d: number, p: Vec, q: Vec
|
||||||
|
for (let i = 0; i < vertices.length; i++) {
|
||||||
|
p = vertices[i]
|
||||||
|
q = Vec.NearestPointOnLineSegment(A, B, p, true)
|
||||||
|
d = Vec.Dist2(p, q)
|
||||||
|
if (d < dist) {
|
||||||
|
dist = d
|
||||||
|
nearest = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nearest) throw Error('nearest point not found')
|
||||||
|
return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||||
|
@ -58,14 +80,14 @@ export abstract class Geometry2d {
|
||||||
const { vertices } = this
|
const { vertices } = this
|
||||||
let nearest: Vec | undefined
|
let nearest: Vec | undefined
|
||||||
let dist = Infinity
|
let dist = Infinity
|
||||||
let d: number
|
let d: number, p: Vec, q: Vec
|
||||||
let p: Vec
|
|
||||||
for (let i = 0; i < vertices.length; i++) {
|
for (let i = 0; i < vertices.length; i++) {
|
||||||
p = vertices[i]
|
p = vertices[i]
|
||||||
d = Vec.DistanceToLineSegment(A, B, p)
|
q = Vec.NearestPointOnLineSegment(A, B, p, true)
|
||||||
|
d = Vec.Dist2(p, q)
|
||||||
if (d < dist) {
|
if (d < dist) {
|
||||||
dist = d
|
dist = d
|
||||||
nearest = p
|
nearest = q
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!nearest) throw Error('nearest point not found')
|
if (!nearest) throw Error('nearest point not found')
|
||||||
|
|
|
@ -65,7 +65,13 @@ export class Polyline2d extends Geometry2d {
|
||||||
return nearest
|
return nearest
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||||
return this.segments.some((edge) => edge.hitTestLineSegment(A, B, zoom))
|
const { segments } = this
|
||||||
|
for (let i = 0, n = segments.length; i < n; i++) {
|
||||||
|
if (segments[i].hitTestLineSegment(A, B, distance)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { HALF_PI, PI } from '../utils'
|
||||||
import { Ellipse2d } from './Ellipse2d'
|
import { Ellipse2d } from './Ellipse2d'
|
||||||
import { Geometry2dOptions } from './Geometry2d'
|
import { Geometry2dOptions } from './Geometry2d'
|
||||||
|
|
||||||
|
const STADIUM_VERTICES_LENGTH = 18
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export class Stadium2d extends Ellipse2d {
|
export class Stadium2d extends Ellipse2d {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -12,28 +14,31 @@ export class Stadium2d extends Ellipse2d {
|
||||||
}
|
}
|
||||||
|
|
||||||
getVertices() {
|
getVertices() {
|
||||||
// Perimeter of the ellipse
|
|
||||||
const w = Math.max(1, this.w)
|
const w = Math.max(1, this.w)
|
||||||
const h = Math.max(1, this.h)
|
const h = Math.max(1, this.h)
|
||||||
const cx = w / 2
|
const cx = w / 2
|
||||||
const cy = h / 2
|
const cy = h / 2
|
||||||
|
const points: Vec[] = Array(STADIUM_VERTICES_LENGTH)
|
||||||
const len = 10
|
let t1: number, t2: number
|
||||||
const points: Vec[] = Array(len * 2 - 2)
|
|
||||||
|
|
||||||
if (h > w) {
|
if (h > w) {
|
||||||
for (let i = 0; i < len - 1; i++) {
|
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
||||||
const t1 = -PI + (PI * i) / (len - 2)
|
t1 = -PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||||
const t2 = (PI * i) / (len - 2)
|
t2 = (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||||
points[i] = new Vec(cx + cx * Math.cos(t1), cx + cx * Math.sin(t1))
|
points[i] = new Vec(cx + cx * Math.cos(t1), cx + cx * Math.sin(t1))
|
||||||
points[i + (len - 1)] = new Vec(cx + cx * Math.cos(t2), h - cx + cx * Math.sin(t2))
|
points[i + (STADIUM_VERTICES_LENGTH - 1)] = new Vec(
|
||||||
|
cx + cx * Math.cos(t2),
|
||||||
|
h - cx + cx * Math.sin(t2)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < len - 1; i++) {
|
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
||||||
const t1 = -HALF_PI + (PI * i) / (len - 2)
|
t1 = -HALF_PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||||
const t2 = HALF_PI + (PI * -i) / (len - 2)
|
t2 = HALF_PI + (PI * -i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||||
points[i] = new Vec(w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1))
|
points[i] = new Vec(w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1))
|
||||||
points[i + (len - 1)] = new Vec(cy - cy * Math.cos(t2), h - cy + cy * Math.sin(t2))
|
points[i + (STADIUM_VERTICES_LENGTH - 1)] = new Vec(
|
||||||
|
cy - cy * Math.cos(t2),
|
||||||
|
h - cy + cy * Math.sin(t2)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,6 @@ import { TLGeoShape } from '@tldraw/editor';
|
||||||
import { TLHandle } from '@tldraw/editor';
|
import { TLHandle } from '@tldraw/editor';
|
||||||
import { TLHandlesProps } from '@tldraw/editor';
|
import { TLHandlesProps } from '@tldraw/editor';
|
||||||
import { TLHighlightShape } from '@tldraw/editor';
|
import { TLHighlightShape } from '@tldraw/editor';
|
||||||
import { TLHoveredShapeIndicatorProps } from '@tldraw/editor';
|
|
||||||
import { TLImageShape } from '@tldraw/editor';
|
import { TLImageShape } from '@tldraw/editor';
|
||||||
import { TLInterruptEvent } from '@tldraw/editor';
|
import { TLInterruptEvent } from '@tldraw/editor';
|
||||||
import { TLKeyboardEvent } from '@tldraw/editor';
|
import { TLKeyboardEvent } from '@tldraw/editor';
|
||||||
|
@ -1452,9 +1451,6 @@ export interface TldrawFile {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | null;
|
export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | null;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export function TldrawHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps): JSX_2.Element | null;
|
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export const TldrawImage: NamedExoticComponent< {
|
export const TldrawImage: NamedExoticComponent< {
|
||||||
snapshot: StoreSnapshot<TLRecord>;
|
snapshot: StoreSnapshot<TLRecord>;
|
||||||
|
|
|
@ -16579,61 +16579,6 @@
|
||||||
],
|
],
|
||||||
"name": "TldrawHandles"
|
"name": "TldrawHandles"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Function",
|
|
||||||
"canonicalReference": "tldraw!TldrawHoveredShapeIndicator:function(1)",
|
|
||||||
"docComment": "/**\n * @public\n */\n",
|
|
||||||
"excerptTokens": [
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "export declare function TldrawHoveredShapeIndicator({ shapeId }: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "TLHoveredShapeIndicatorProps",
|
|
||||||
"canonicalReference": "@tldraw/editor!TLHoveredShapeIndicatorProps:type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "): "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "import(\"react/jsx-runtime\")."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "JSX.Element",
|
|
||||||
"canonicalReference": "@types/react!JSX.Element:interface"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": " | null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ";"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fileUrlPath": "packages/tldraw/src/lib/canvas/TldrawHoveredShapeIndicator.tsx",
|
|
||||||
"returnTypeTokenRange": {
|
|
||||||
"startIndex": 3,
|
|
||||||
"endIndex": 6
|
|
||||||
},
|
|
||||||
"releaseTag": "Public",
|
|
||||||
"overloadIndex": 1,
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"parameterName": "{ shapeId }",
|
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 2
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "TldrawHoveredShapeIndicator"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Variable",
|
"kind": "Variable",
|
||||||
"canonicalReference": "tldraw!TldrawImage:var",
|
"canonicalReference": "tldraw!TldrawImage:var",
|
||||||
|
|
|
@ -9,7 +9,6 @@ export * from '@tldraw/editor'
|
||||||
export { Tldraw, type TldrawProps } from './lib/Tldraw'
|
export { Tldraw, type TldrawProps } from './lib/Tldraw'
|
||||||
export { TldrawImage, type TldrawImageProps } from './lib/TldrawImage'
|
export { TldrawImage, type TldrawImageProps } from './lib/TldrawImage'
|
||||||
export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
||||||
export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator'
|
|
||||||
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
||||||
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
||||||
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { useLayoutEffect, useMemo } from 'react'
|
import { useLayoutEffect, useMemo } from 'react'
|
||||||
import { TldrawHandles } from './canvas/TldrawHandles'
|
import { TldrawHandles } from './canvas/TldrawHandles'
|
||||||
import { TldrawHoveredShapeIndicator } from './canvas/TldrawHoveredShapeIndicator'
|
|
||||||
import { TldrawScribble } from './canvas/TldrawScribble'
|
import { TldrawScribble } from './canvas/TldrawScribble'
|
||||||
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
||||||
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
||||||
|
@ -90,7 +89,6 @@ export function Tldraw(props: TldrawProps) {
|
||||||
SelectionForeground: TldrawSelectionForeground,
|
SelectionForeground: TldrawSelectionForeground,
|
||||||
SelectionBackground: TldrawSelectionBackground,
|
SelectionBackground: TldrawSelectionBackground,
|
||||||
Handles: TldrawHandles,
|
Handles: TldrawHandles,
|
||||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
|
||||||
..._components,
|
..._components,
|
||||||
}),
|
}),
|
||||||
[_components]
|
[_components]
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {
|
|
||||||
TLHoveredShapeIndicatorProps,
|
|
||||||
useEditor,
|
|
||||||
useEditorComponents,
|
|
||||||
useValue,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function TldrawHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps) {
|
|
||||||
const editor = useEditor()
|
|
||||||
const { ShapeIndicator } = useEditorComponents()
|
|
||||||
const showHoveredShapeIndicator = useValue(
|
|
||||||
'show hovered',
|
|
||||||
() => {
|
|
||||||
// When the editor is editing a shape and hovering that shape,
|
|
||||||
// don't show its indicator; but DO show other hover indicators
|
|
||||||
if (editor.isIn('select.editing_shape')) {
|
|
||||||
return editor.getHoveredShapeId() !== editor.getEditingShapeId()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherise, only show the hovered indicator when the editor
|
|
||||||
// is in the idle state
|
|
||||||
return editor.isInAny('select.idle')
|
|
||||||
},
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
if (!ShapeIndicator) return null
|
|
||||||
if (!showHoveredShapeIndicator) return null
|
|
||||||
return <ShapeIndicator className="tl-user-indicator__hovered" shapeId={shapeId} />
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
|
@ -160,7 +160,7 @@ export class Drawing extends StateNode {
|
||||||
return (
|
return (
|
||||||
firstPoint !== lastPoint &&
|
firstPoint !== lastPoint &&
|
||||||
this.currentLineLength > strokeWidth * 4 &&
|
this.currentLineLength > strokeWidth * 4 &&
|
||||||
Vec.Dist(firstPoint, lastPoint) < strokeWidth * 2
|
Vec.DistMin(firstPoint, lastPoint, strokeWidth * 2)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +224,9 @@ export class Drawing extends StateNode {
|
||||||
this.pagePointWhereNextSegmentChanged = null
|
this.pagePointWhereNextSegmentChanged = null
|
||||||
const segments = [...shape.props.segments, newSegment]
|
const segments = [...shape.props.segments, newSegment]
|
||||||
|
|
||||||
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||||
this.currentLineLength = this.getLineLength(segments)
|
this.currentLineLength = this.getLineLength(segments)
|
||||||
|
}
|
||||||
|
|
||||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
|
@ -411,7 +413,10 @@ export class Drawing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSegments = [...newSegments, newFreeSegment]
|
const finalSegments = [...newSegments, newFreeSegment]
|
||||||
|
|
||||||
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||||
this.currentLineLength = this.getLineLength(finalSegments)
|
this.currentLineLength = this.getLineLength(finalSegments)
|
||||||
|
}
|
||||||
|
|
||||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||||
id,
|
id,
|
||||||
|
@ -486,11 +491,10 @@ export class Drawing extends StateNode {
|
||||||
lastPoint,
|
lastPoint,
|
||||||
newPoint
|
newPoint
|
||||||
)
|
)
|
||||||
const distance = Vec.Dist(nearestPointOnSegment, newPoint)
|
|
||||||
|
|
||||||
if (distance < minDistance) {
|
if (Vec.DistMin(nearestPointOnSegment, newPoint, minDistance)) {
|
||||||
nearestPoint = nearestPointOnSegment.toFixed().toJson()
|
nearestPoint = nearestPointOnSegment.toFixed().toJson()
|
||||||
minDistance = distance
|
minDistance = Vec.Dist(nearestPointOnSegment, newPoint)
|
||||||
snapSegment = segment
|
snapSegment = segment
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -598,7 +602,9 @@ export class Drawing extends StateNode {
|
||||||
points: newPoints,
|
points: newPoints,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||||
this.currentLineLength = this.getLineLength(newSegments)
|
this.currentLineLength = this.getLineLength(newSegments)
|
||||||
|
}
|
||||||
|
|
||||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||||
id,
|
id,
|
||||||
|
@ -659,7 +665,7 @@ export class Drawing extends StateNode {
|
||||||
for (let i = 0; i < segment.points.length - 1; i++) {
|
for (let i = 0; i < segment.points.length - 1; i++) {
|
||||||
const A = segment.points[i]
|
const A = segment.points[i]
|
||||||
const B = segment.points[i + 1]
|
const B = segment.points[i + 1]
|
||||||
length += Vec.Sub(B, A).len2()
|
length += Vec.Dist2(B, A)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -311,8 +311,8 @@ export function inkyCloudSvgPath(
|
||||||
}
|
}
|
||||||
const arcs = getCloudArcs(width, height, seed, size)
|
const arcs = getCloudArcs(width, height, seed, size)
|
||||||
const avgArcLength =
|
const avgArcLength =
|
||||||
arcs.reduce((sum, arc) => sum + Vec.Dist(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
arcs.reduce((sum, arc) => sum + Vec.Dist2(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
||||||
const shouldMutatePoints = avgArcLength > mutMultiplier * 15
|
const shouldMutatePoints = avgArcLength > (mutMultiplier * 15) ** 2
|
||||||
|
|
||||||
const mutPoint = shouldMutatePoints ? (p: Vec) => new Vec(mut(p.x), mut(p.y)) : (p: Vec) => p
|
const mutPoint = shouldMutatePoints ? (p: Vec) => new Vec(mut(p.x), mut(p.y)) : (p: Vec) => p
|
||||||
let pathA = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
let pathA = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
||||||
|
|
|
@ -53,8 +53,8 @@ export class Pointing extends StateNode {
|
||||||
const points = structuredClone(this.shape.props.points)
|
const points = structuredClone(this.shape.props.points)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Vec.Dist(endHandle, prevEndHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES ||
|
Vec.DistMin(endHandle, prevEndHandle, MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES) ||
|
||||||
Vec.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES
|
Vec.DistMin(nextPoint, endHandle, MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES)
|
||||||
) {
|
) {
|
||||||
// Don't add a new point if the distance between the last two points is too small
|
// Don't add a new point if the distance between the last two points is too small
|
||||||
points[endHandle.id] = {
|
points[endHandle.id] = {
|
||||||
|
|
|
@ -68,7 +68,7 @@ export function getStrokePoints(
|
||||||
// Strip points that are too close to the first point.
|
// Strip points that are too close to the first point.
|
||||||
let pt = pts[1]
|
let pt = pts[1]
|
||||||
while (pt) {
|
while (pt) {
|
||||||
if (Vec.Dist(pt, pts[0]) > size / 3) break
|
if (Vec.Dist2(pt, pts[0]) > (size / 3) ** 2) break
|
||||||
pts[0].z = Math.max(pts[0].z, pt.z) // Use maximum pressure
|
pts[0].z = Math.max(pts[0].z, pt.z) // Use maximum pressure
|
||||||
pts.splice(1, 1)
|
pts.splice(1, 1)
|
||||||
pt = pts[1]
|
pt = pts[1]
|
||||||
|
@ -78,7 +78,7 @@ export function getStrokePoints(
|
||||||
const last = pts.pop()!
|
const last = pts.pop()!
|
||||||
pt = pts[pts.length - 1]
|
pt = pts[pts.length - 1]
|
||||||
while (pt) {
|
while (pt) {
|
||||||
if (Vec.Dist(pt, last) > size / 3) break
|
if (Vec.Dist2(pt, last) > (size / 3) ** 2) break
|
||||||
pts.pop()
|
pts.pop()
|
||||||
pt = pts[pts.length - 1]
|
pt = pts[pts.length - 1]
|
||||||
pointsRemovedFromNearEnd++
|
pointsRemovedFromNearEnd++
|
||||||
|
@ -88,7 +88,7 @@ export function getStrokePoints(
|
||||||
const isComplete =
|
const isComplete =
|
||||||
options.last ||
|
options.last ||
|
||||||
!options.simulatePressure ||
|
!options.simulatePressure ||
|
||||||
(pts.length > 1 && Vec.Dist(pts[pts.length - 1], pts[pts.length - 2]) < size) ||
|
(pts.length > 1 && Vec.Dist2(pts[pts.length - 1], pts[pts.length - 2]) < size ** 2) ||
|
||||||
pointsRemovedFromNearEnd > 0
|
pointsRemovedFromNearEnd > 0
|
||||||
|
|
||||||
// Add extra points between the two, to help avoid "dash" lines
|
// Add extra points between the two, to help avoid "dash" lines
|
||||||
|
|
|
@ -79,35 +79,49 @@ export class Erasing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const erasingShapeIds = this.editor.getErasingShapeIds()
|
const { editor, excludedShapeIds } = this
|
||||||
const zoomLevel = this.editor.getZoomLevel()
|
const erasingShapeIds = editor.getErasingShapeIds()
|
||||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
const zoomLevel = editor.getZoomLevel()
|
||||||
|
const currentPageShapes = editor.getCurrentPageShapes()
|
||||||
const {
|
const {
|
||||||
inputs: { currentPagePoint, previousPagePoint },
|
inputs: { currentPagePoint, previousPagePoint },
|
||||||
} = this.editor
|
} = editor
|
||||||
|
|
||||||
const { excludedShapeIds } = this
|
|
||||||
|
|
||||||
this.pushPointToScribble()
|
this.pushPointToScribble()
|
||||||
|
|
||||||
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
||||||
|
const minDist = HIT_TEST_MARGIN / zoomLevel
|
||||||
|
|
||||||
for (const shape of currentPageShapes) {
|
for (const shape of currentPageShapes) {
|
||||||
if (this.editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
if (editor.isShapeOfType<TLGroupShape>(shape, 'group')) continue
|
||||||
|
|
||||||
// Avoid testing masked shapes, unless the pointer is inside the mask
|
// Avoid testing masked shapes, unless the pointer is inside the mask
|
||||||
const pageMask = this.editor.getShapeMask(shape.id)
|
const pageMask = editor.getShapeMask(shape.id)
|
||||||
if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) {
|
if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit test the shape using a line segment
|
// Hit test the shape using a line segment
|
||||||
const geometry = this.editor.getShapeGeometry(shape)
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
const A = this.editor.getPointInShapeSpace(shape, previousPagePoint)
|
const pageTransform = editor.getShapePageTransform(shape)
|
||||||
const B = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
if (!geometry || !pageTransform) continue
|
||||||
|
const pt = pageTransform.clone().invert()
|
||||||
|
const A = pt.applyToPoint(previousPagePoint)
|
||||||
|
const B = pt.applyToPoint(currentPagePoint)
|
||||||
|
|
||||||
if (geometry.hitTestLineSegment(A, B, HIT_TEST_MARGIN / zoomLevel)) {
|
// If the line segment is entirely above / below / left / right of the shape's bounding box, skip the hit test
|
||||||
erasing.add(this.editor.getOutermostSelectableShape(shape).id)
|
const { bounds } = geometry
|
||||||
|
if (
|
||||||
|
bounds.minX - minDist > Math.max(A.x, B.x) ||
|
||||||
|
bounds.minY - minDist > Math.max(A.y, B.y) ||
|
||||||
|
bounds.maxX + minDist < Math.min(A.x, B.x) ||
|
||||||
|
bounds.maxY + minDist < Math.min(A.y, B.y)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geometry.hitTestLineSegment(A, B, minDist)) {
|
||||||
|
erasing.add(editor.getOutermostSelectableShape(shape).id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,14 +132,16 @@ export class Erasing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete() {
|
complete() {
|
||||||
this.editor.deleteShapes(this.editor.getCurrentPageState().erasingShapeIds)
|
const { editor } = this
|
||||||
this.editor.setErasingShapes([])
|
editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds)
|
||||||
|
editor.setErasingShapes([])
|
||||||
this.parent.transition('idle')
|
this.parent.transition('idle')
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.editor.setErasingShapes([])
|
const { editor } = this
|
||||||
this.editor.bailToMark(this.markId)
|
editor.setErasingShapes([])
|
||||||
|
editor.bailToMark(this.markId)
|
||||||
this.parent.transition('idle', this.info)
|
this.parent.transition('idle', this.info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
HIT_TEST_MARGIN,
|
|
||||||
Mat,
|
Mat,
|
||||||
StateNode,
|
StateNode,
|
||||||
TLCancelEvent,
|
TLCancelEvent,
|
||||||
|
@ -24,7 +23,6 @@ export class Brushing extends StateNode {
|
||||||
|
|
||||||
info = {} as TLPointerEventInfo & { target: 'canvas' }
|
info = {} as TLPointerEventInfo & { target: 'canvas' }
|
||||||
|
|
||||||
brush = new Box()
|
|
||||||
initialSelectedShapeIds: TLShapeId[] = []
|
initialSelectedShapeIds: TLShapeId[] = []
|
||||||
excludedShapeIds = new Set<TLShapeId>()
|
excludedShapeIds = new Set<TLShapeId>()
|
||||||
isWrapMode = false
|
isWrapMode = false
|
||||||
|
@ -103,18 +101,22 @@ export class Brushing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private hitTestShapes() {
|
private hitTestShapes() {
|
||||||
const zoomLevel = this.editor.getZoomLevel()
|
const { editor, excludedShapeIds, isWrapMode } = this
|
||||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
|
||||||
const currentPageId = this.editor.getCurrentPageId()
|
|
||||||
const {
|
const {
|
||||||
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey },
|
||||||
} = this.editor
|
} = editor
|
||||||
|
|
||||||
|
// We'll be collecting shape ids of selected shapes; if we're holding shift key, we start from our initial shapes
|
||||||
|
const results = new Set(shiftKey ? this.initialSelectedShapeIds : [])
|
||||||
|
|
||||||
|
// In wrap mode, we need to completely enclose a shape to select it
|
||||||
|
const isWrapping = isWrapMode ? !ctrlKey : ctrlKey
|
||||||
|
|
||||||
// Set the brush to contain the current and origin points
|
// Set the brush to contain the current and origin points
|
||||||
this.brush.setTo(Box.FromPoints([originPagePoint, currentPagePoint]))
|
const brush = Box.FromPoints([originPagePoint, currentPagePoint])
|
||||||
|
|
||||||
// We'll be collecting shape ids
|
// We'll be testing the corners of the brush against the shapes
|
||||||
const results = new Set(shiftKey ? this.initialSelectedShapeIds : [])
|
const { corners } = brush
|
||||||
|
|
||||||
let A: Vec,
|
let A: Vec,
|
||||||
B: Vec,
|
B: Vec,
|
||||||
|
@ -123,64 +125,59 @@ export class Brushing extends StateNode {
|
||||||
pageTransform: Mat | undefined,
|
pageTransform: Mat | undefined,
|
||||||
localCorners: Vec[]
|
localCorners: Vec[]
|
||||||
|
|
||||||
// We'll be testing the corners of the brush against the shapes
|
const currentPageShapes = editor.getCurrentPageShapes()
|
||||||
const { corners } = this.brush
|
const currentPageId = editor.getCurrentPageId()
|
||||||
|
|
||||||
const { excludedShapeIds, isWrapMode } = this
|
|
||||||
|
|
||||||
const isWrapping = isWrapMode ? !ctrlKey : ctrlKey
|
|
||||||
|
|
||||||
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
||||||
shape = currentPageShapes[i]
|
shape = currentPageShapes[i]
|
||||||
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
if (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes
|
||||||
if (results.has(shape.id)) continue testAllShapes
|
|
||||||
|
|
||||||
pageBounds = this.editor.getShapePageBounds(shape)
|
pageBounds = editor.getShapePageBounds(shape)
|
||||||
if (!pageBounds) continue testAllShapes
|
if (!pageBounds) continue testAllShapes
|
||||||
|
|
||||||
// If the brush fully wraps a shape, it's almost certainly a hit
|
// If the brush fully wraps a shape, it's almost certainly a hit
|
||||||
if (this.brush.contains(pageBounds)) {
|
if (brush.contains(pageBounds)) {
|
||||||
this.handleHit(shape, currentPagePoint, currentPageId, results, corners)
|
this.handleHit(shape, currentPagePoint, currentPageId, results, corners)
|
||||||
continue testAllShapes
|
continue testAllShapes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should we even test for a single segment intersections? Only if
|
// If we're in wrap mode and the brush did not fully encloses the shape, it's a miss
|
||||||
// we're not holding the ctrl key for alternate selection mode
|
// We also skip frames unless we've completely selected the frame.
|
||||||
// (only wraps count!), or if the shape is a frame.
|
if (isWrapping || editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
||||||
if (isWrapping || this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
|
||||||
continue testAllShapes
|
continue testAllShapes
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the brush collides the page bounds, then do hit tests against
|
// If the brush collides the page bounds, then do hit tests against
|
||||||
// each of the brush's four sides.
|
// each of the brush's four sides.
|
||||||
if (this.brush.collides(pageBounds)) {
|
if (brush.collides(pageBounds)) {
|
||||||
// Shapes expect to hit test line segments in their own coordinate system,
|
// Shapes expect to hit test line segments in their own coordinate system,
|
||||||
// so we first need to get the brush corners in the shape's local space.
|
// so we first need to get the brush corners in the shape's local space.
|
||||||
const geometry = this.editor.getShapeGeometry(shape)
|
pageTransform = editor.getShapePageTransform(shape)
|
||||||
|
if (!pageTransform) continue testAllShapes
|
||||||
pageTransform = this.editor.getShapePageTransform(shape)
|
|
||||||
|
|
||||||
if (!pageTransform) {
|
|
||||||
continue testAllShapes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether any of the the brush edges intersect the shape
|
|
||||||
localCorners = pageTransform.clone().invert().applyToPoints(corners)
|
localCorners = pageTransform.clone().invert().applyToPoints(corners)
|
||||||
|
// See if any of the edges intersect the shape's geometry
|
||||||
hitTestBrushEdges: for (let i = 0; i < localCorners.length; i++) {
|
const geometry = editor.getShapeGeometry(shape)
|
||||||
|
hitTestBrushEdges: for (let i = 0; i < 4; i++) {
|
||||||
A = localCorners[i]
|
A = localCorners[i]
|
||||||
B = localCorners[(i + 1) % localCorners.length]
|
B = localCorners[(i + 1) % 4]
|
||||||
|
if (geometry.hitTestLineSegment(A, B, 0)) {
|
||||||
if (geometry.hitTestLineSegment(A, B, HIT_TEST_MARGIN / zoomLevel)) {
|
|
||||||
this.handleHit(shape, currentPagePoint, currentPageId, results, corners)
|
this.handleHit(shape, currentPagePoint, currentPageId, results, corners)
|
||||||
break hitTestBrushEdges
|
break hitTestBrushEdges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
editor.getInstanceState().isCoarsePointer
|
||||||
|
|
||||||
this.editor.updateInstanceState({ brush: { ...this.brush.toJson() } })
|
const currentBrush = editor.getInstanceState().brush
|
||||||
this.editor.setSelectedShapes(Array.from(results), { squashing: true })
|
if (!currentBrush || !brush.equals(currentBrush)) {
|
||||||
|
editor.updateInstanceState({ brush: { ...brush.toJson() } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = editor.getSelectedShapeIds()
|
||||||
|
if (current.length !== results.size || current.some((id) => !results.has(id))) {
|
||||||
|
editor.setSelectedShapes(Array.from(results), { squashing: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onInterrupt: TLInterruptEvent = () => {
|
override onInterrupt: TLInterruptEvent = () => {
|
||||||
|
@ -203,7 +200,6 @@ export class Brushing extends StateNode {
|
||||||
// page mask; and if so, check to see if the brush intersects it
|
// page mask; and if so, check to see if the brush intersects it
|
||||||
const selectedShape = this.editor.getOutermostSelectableShape(shape)
|
const selectedShape = this.editor.getOutermostSelectableShape(shape)
|
||||||
const pageMask = this.editor.getShapeMask(selectedShape.id)
|
const pageMask = this.editor.getShapeMask(selectedShape.id)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
pageMask &&
|
pageMask &&
|
||||||
!polygonsIntersect(pageMask, corners) &&
|
!polygonsIntersect(pageMask, corners) &&
|
||||||
|
@ -211,7 +207,6 @@ export class Brushing extends StateNode {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results.add(selectedShape.id)
|
results.add(selectedShape.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,8 @@ export class PointingArrowLabel extends StateNode {
|
||||||
let nextLabelPosition
|
let nextLabelPosition
|
||||||
if (info.isStraight) {
|
if (info.isStraight) {
|
||||||
// straight arrows
|
// straight arrows
|
||||||
const lineLength = Vec.Dist(info.start.point, info.end.point)
|
const lineLength = Vec.Dist2(info.start.point, info.end.point)
|
||||||
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
|
const segmentLength = Vec.Dist2(info.end.point, nearestPoint)
|
||||||
nextLabelPosition = 1 - segmentLength / lineLength
|
nextLabelPosition = 1 - segmentLength / lineLength
|
||||||
} else {
|
} else {
|
||||||
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
HIT_TEST_MARGIN,
|
|
||||||
StateNode,
|
StateNode,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
TLFrameShape,
|
TLFrameShape,
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
TLShape,
|
TLShape,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
Vec,
|
Vec,
|
||||||
intersectLineSegmentPolyline,
|
intersectLineSegmentPolygon,
|
||||||
pointInPolygon,
|
pointInPolygon,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
|
@ -83,7 +82,8 @@ export class ScribbleBrushing extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateScribbleSelection(addPoint: boolean) {
|
private updateScribbleSelection(addPoint: boolean) {
|
||||||
const zoomLevel = this.editor.getZoomLevel()
|
const { editor } = this
|
||||||
|
// const zoomLevel = this.editor.getZoomLevel()
|
||||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
const currentPageShapes = this.editor.getCurrentPageShapes()
|
||||||
const {
|
const {
|
||||||
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
||||||
|
@ -98,36 +98,53 @@ export class ScribbleBrushing extends StateNode {
|
||||||
const shapes = currentPageShapes
|
const shapes = currentPageShapes
|
||||||
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
|
let shape: TLShape, geometry: Geometry2d, A: Vec, B: Vec
|
||||||
|
|
||||||
|
const minDist = 0 // HIT_TEST_MARGIN / zoomLevel
|
||||||
|
|
||||||
for (let i = 0, n = shapes.length; i < n; i++) {
|
for (let i = 0, n = shapes.length; i < n; i++) {
|
||||||
shape = shapes[i]
|
shape = shapes[i]
|
||||||
geometry = this.editor.getShapeGeometry(shape)
|
|
||||||
|
|
||||||
// If the shape is a group or is already selected or locked, don't select it
|
// If the shape is a group or is already selected or locked, don't select it
|
||||||
if (
|
if (
|
||||||
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||||
newlySelectedShapeIds.has(shape.id) ||
|
newlySelectedShapeIds.has(shape.id) ||
|
||||||
this.editor.isShapeOrAncestorLocked(shape)
|
editor.isShapeOrAncestorLocked(shape)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
geometry = editor.getShapeGeometry(shape)
|
||||||
|
|
||||||
// If the scribble started inside of the frame, don't select it
|
// If the scribble started inside of the frame, don't select it
|
||||||
if (this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
if (
|
||||||
const point = this.editor.getPointInShapeSpace(shape, originPagePoint)
|
editor.isShapeOfType<TLFrameShape>(shape, 'frame') &&
|
||||||
if (geometry.bounds.containsPoint(point)) {
|
geometry.bounds.containsPoint(editor.getPointInShapeSpace(shape, originPagePoint))
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hit test the shape using a line segment
|
||||||
|
const pageTransform = editor.getShapePageTransform(shape)
|
||||||
|
if (!geometry || !pageTransform) continue
|
||||||
|
const pt = pageTransform.clone().invert()
|
||||||
|
A = pt.applyToPoint(previousPagePoint)
|
||||||
|
B = pt.applyToPoint(currentPagePoint)
|
||||||
|
|
||||||
|
// If the line segment is entirely above / below / left / right of the shape's bounding box, skip the hit test
|
||||||
|
const { bounds } = geometry
|
||||||
|
if (
|
||||||
|
bounds.minX - minDist > Math.max(A.x, B.x) ||
|
||||||
|
bounds.minY - minDist > Math.max(A.y, B.y) ||
|
||||||
|
bounds.maxX + minDist < Math.min(A.x, B.x) ||
|
||||||
|
bounds.maxY + minDist < Math.min(A.y, B.y)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
A = this.editor.getPointInShapeSpace(shape, previousPagePoint)
|
if (geometry.hitTestLineSegment(A, B, minDist)) {
|
||||||
B = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
|
||||||
if (geometry.hitTestLineSegment(A, B, HIT_TEST_MARGIN / zoomLevel)) {
|
|
||||||
const outermostShape = this.editor.getOutermostSelectableShape(shape)
|
const outermostShape = this.editor.getOutermostSelectableShape(shape)
|
||||||
|
|
||||||
const pageMask = this.editor.getShapeMask(outermostShape.id)
|
const pageMask = this.editor.getShapeMask(outermostShape.id)
|
||||||
|
|
||||||
if (pageMask) {
|
if (pageMask) {
|
||||||
const intersection = intersectLineSegmentPolyline(
|
const intersection = intersectLineSegmentPolygon(
|
||||||
previousPagePoint,
|
previousPagePoint,
|
||||||
currentPagePoint,
|
currentPagePoint,
|
||||||
pageMask
|
pageMask
|
||||||
|
@ -142,16 +159,13 @@ export class ScribbleBrushing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.setSelectedShapes(
|
const current = editor.getSelectedShapeIds()
|
||||||
[
|
const next = new Set<TLShapeId>(
|
||||||
...new Set<TLShapeId>(
|
shiftKey ? [...newlySelectedShapeIds, ...initialSelectedShapeIds] : [...newlySelectedShapeIds]
|
||||||
shiftKey
|
|
||||||
? [...newlySelectedShapeIds, ...initialSelectedShapeIds]
|
|
||||||
: [...newlySelectedShapeIds]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
{ squashing: true }
|
|
||||||
)
|
)
|
||||||
|
if (current.length !== next.size || current.some((id) => !next.has(id))) {
|
||||||
|
this.editor.setSelectedShapes(Array.from(next), { squashing: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private complete() {
|
private complete() {
|
||||||
|
|
|
@ -213,15 +213,14 @@ describe('<TldrawEditor />', () => {
|
||||||
|
|
||||||
// Is the shape's component rendering?
|
// Is the shape's component rendering?
|
||||||
expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
|
expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
|
||||||
|
// though indicator should be display none
|
||||||
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(0)
|
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
|
||||||
|
|
||||||
// Select the shape
|
// Select the shape
|
||||||
await act(async () => editor.select(id))
|
await act(async () => editor.select(id))
|
||||||
|
|
||||||
expect(editor.getSelectedShapeIds().length).toBe(1)
|
expect(editor.getSelectedShapeIds().length).toBe(1)
|
||||||
|
// though indicator it should be visible
|
||||||
// Is the shape's component rendering?
|
|
||||||
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
|
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
|
||||||
|
|
||||||
// Select the eraser tool...
|
// Select the eraser tool...
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
const now = () => {
|
const now = () => {
|
||||||
const hrTime = process.hrtime()
|
return Number(process.hrtime.bigint()) / 1e6
|
||||||
return hrTime[0] * 1000 + hrTime[1] / 1000000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PerformanceMeasurer {
|
export class PerformanceMeasurer {
|
||||||
|
@ -17,6 +16,7 @@ export class PerformanceMeasurer {
|
||||||
cold = 0
|
cold = 0
|
||||||
fastest = Infinity
|
fastest = Infinity
|
||||||
slowest = -Infinity
|
slowest = -Infinity
|
||||||
|
didRun = false
|
||||||
|
|
||||||
totalStart = 0
|
totalStart = 0
|
||||||
totalEnd = 0
|
totalEnd = 0
|
||||||
|
@ -60,6 +60,12 @@ export class PerformanceMeasurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
|
if (this.didRun) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
this.didRun = true
|
||||||
|
|
||||||
const { fns, beforeFns, afterFns, warmupIterations, iterations } = this
|
const { fns, beforeFns, afterFns, warmupIterations, iterations } = this
|
||||||
|
|
||||||
// Run the cold run
|
// Run the cold run
|
||||||
|
@ -134,20 +140,21 @@ export class PerformanceMeasurer {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Table(...ps: PerformanceMeasurer[]) {
|
static Table(...ps: PerformanceMeasurer[]) {
|
||||||
|
ps.forEach((p) => p.run())
|
||||||
const table: Record<string, Record<string, number | string>> = {}
|
const table: Record<string, Record<string, number | string>> = {}
|
||||||
const fastest = ps.map((p) => p.average).reduce((a, b) => Math.min(a, b))
|
// const fastest = ps.map((p) => p.average).reduce((a, b) => Math.min(a, b))
|
||||||
const totalFastest = ps.map((p) => p.totalTime).reduce((a, b) => Math.min(a, b))
|
// const totalFastest = ps.map((p) => p.totalTime).reduce((a, b) => Math.min(a, b))
|
||||||
|
|
||||||
ps.forEach(
|
ps.forEach(
|
||||||
(p) =>
|
(p) =>
|
||||||
(table[p.name] = {
|
(table[p.name] = {
|
||||||
['Runs']: p.warmupIterations + p.iterations,
|
|
||||||
['Cold']: Number(p.cold.toFixed(2)),
|
['Cold']: Number(p.cold.toFixed(2)),
|
||||||
['Slowest']: Number(p.slowest.toFixed(2)),
|
['Slowest']: Number(p.slowest.toFixed(2)),
|
||||||
['Fastest']: Number(p.fastest.toFixed(2)),
|
['Fastest']: Number(p.fastest.toFixed(2)),
|
||||||
['Average']: Number(p.average.toFixed(2)),
|
['Average']: Number(p.average.toFixed(2)),
|
||||||
['Slower (Avg)']: Number((p.average / fastest).toFixed(2)),
|
// ['Slower (Avg)']: Number((p.average / fastest).toFixed(2)),
|
||||||
['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)),
|
// ['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)),
|
||||||
|
['Total']: Number(p.totalTime.toFixed(2)),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLShapePartial, createShapeId } from '@tldraw/editor'
|
import { TLShapePartial, Vec, createShapeId } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../TestEditor'
|
import { TestEditor } from '../TestEditor'
|
||||||
import { PerformanceMeasurer } from './PerformanceMeasurer'
|
import { PerformanceMeasurer } from './PerformanceMeasurer'
|
||||||
|
|
||||||
|
@ -125,9 +125,6 @@ describe.skip('Example perf tests', () => {
|
||||||
editor.updateShapes(shapesToUpdate)
|
editor.updateShapes(shapesToUpdate)
|
||||||
})
|
})
|
||||||
|
|
||||||
withUpdateShape.run()
|
|
||||||
withUpdateShapes.run()
|
|
||||||
|
|
||||||
PerformanceMeasurer.Table(withUpdateShape, withUpdateShapes)
|
PerformanceMeasurer.Table(withUpdateShape, withUpdateShapes)
|
||||||
}, 10000)
|
}, 10000)
|
||||||
|
|
||||||
|
@ -157,7 +154,6 @@ describe.skip('Example perf tests', () => {
|
||||||
const shape = editor.getCurrentPageShapes()[0]
|
const shape = editor.getCurrentPageShapes()[0]
|
||||||
editor.updateShape({ ...shape, x: shape.x + 1 })
|
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||||
})
|
})
|
||||||
.run()
|
|
||||||
|
|
||||||
const renderingShapes2 = new PerformanceMeasurer('Measure rendering bounds with 200 shapes', {
|
const renderingShapes2 = new PerformanceMeasurer('Measure rendering bounds with 200 shapes', {
|
||||||
warmupIterations: 10,
|
warmupIterations: 10,
|
||||||
|
@ -184,8 +180,32 @@ describe.skip('Example perf tests', () => {
|
||||||
const shape = editor.getCurrentPageShapes()[0]
|
const shape = editor.getCurrentPageShapes()[0]
|
||||||
editor.updateShape({ ...shape, x: shape.x + 1 })
|
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||||
})
|
})
|
||||||
.run()
|
|
||||||
|
|
||||||
PerformanceMeasurer.Table(renderingShapes, renderingShapes2)
|
PerformanceMeasurer.Table(renderingShapes, renderingShapes2)
|
||||||
}, 10000)
|
}, 10000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.skip('measures dist', () => {
|
||||||
|
const ITEMS = 100000
|
||||||
|
const MIN_DIST = 0.712311
|
||||||
|
const vecs = Array.from(Array(ITEMS)).map(
|
||||||
|
() => new Vec((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2)
|
||||||
|
)
|
||||||
|
const withDistA = new PerformanceMeasurer('old', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 100,
|
||||||
|
}).add(() => {
|
||||||
|
for (let i = 0; i < ITEMS - 1; i++) {
|
||||||
|
Vec.Dist(vecs[i], vecs[i + 1]) < MIN_DIST
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const withDistB = new PerformanceMeasurer('new', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 100,
|
||||||
|
}).add(() => {
|
||||||
|
for (let i = 0; i < ITEMS - 1; i++) {
|
||||||
|
Vec.DistMin(vecs[i], vecs[i + 1], MIN_DIST)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
PerformanceMeasurer.Table(withDistA, withDistB)
|
||||||
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue