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,
|
||||
TldrawEditor,
|
||||
TldrawHandles,
|
||||
TldrawHoveredShapeIndicator,
|
||||
TldrawScribble,
|
||||
TldrawSelectionBackground,
|
||||
TldrawSelectionForeground,
|
||||
|
@ -23,7 +22,6 @@ const defaultComponents = {
|
|||
SelectionForeground: TldrawSelectionForeground,
|
||||
SelectionBackground: TldrawSelectionBackground,
|
||||
Handles: TldrawHandles,
|
||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
||||
}
|
||||
|
||||
//[2]
|
||||
|
|
|
@ -118,7 +118,7 @@ export class Arc2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||
// (undocumented)
|
||||
length: number;
|
||||
// (undocumented)
|
||||
|
@ -332,7 +332,7 @@ export class Circle2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||
// (undocumented)
|
||||
nearestPoint(point: Vec): Vec;
|
||||
// (undocumented)
|
||||
|
@ -414,7 +414,7 @@ export class CubicSpline2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||
// (undocumented)
|
||||
get length(): number;
|
||||
// (undocumented)
|
||||
|
@ -471,9 +471,6 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
|
|||
// @public (undocumented)
|
||||
export const DefaultHandles: ({ children }: TLHandlesProps) => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export function DefaultHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps): JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export function DefaultScribble({ scribble, zoom, color, opacity, className }: TLScribbleProps): JSX_2.Element | null;
|
||||
|
||||
|
@ -552,7 +549,7 @@ export class Edge2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||
// (undocumented)
|
||||
get length(): number;
|
||||
// (undocumented)
|
||||
|
@ -968,7 +965,7 @@ export class Ellipse2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
h: number;
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean;
|
||||
// (undocumented)
|
||||
nearestPoint(A: Vec): Vec;
|
||||
// (undocumented)
|
||||
|
@ -1458,7 +1455,7 @@ export class Polyline2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||
hitTestLineSegment(A: Vec, B: Vec, distance?: number): boolean;
|
||||
// (undocumented)
|
||||
get length(): number;
|
||||
// (undocumented)
|
||||
|
@ -2302,11 +2299,6 @@ export type TLHistoryMark = {
|
|||
onRedo: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLHoveredShapeIndicatorProps = {
|
||||
shapeId: TLShapeId;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
|
||||
|
||||
|
@ -2539,6 +2531,7 @@ export type TLShapeIndicatorProps = {
|
|||
color?: string | undefined;
|
||||
opacity?: number;
|
||||
className?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -2726,7 +2719,6 @@ export function useEditorComponents(): Partial<{
|
|||
Spinner: ComponentType | null;
|
||||
SelectionForeground: ComponentType<TLSelectionForegroundProps> | null;
|
||||
SelectionBackground: ComponentType<TLSelectionBackgroundProps> | null;
|
||||
HoveredShapeIndicator: ComponentType<TLHoveredShapeIndicatorProps> | null;
|
||||
OnTheCanvas: ComponentType | null;
|
||||
InFrontOfTheCanvas: ComponentType | null;
|
||||
LoadingScreen: ComponentType | null;
|
||||
|
@ -2850,6 +2842,8 @@ export class Vec {
|
|||
// (undocumented)
|
||||
static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number;
|
||||
// (undocumented)
|
||||
static DistMin(A: VecLike, B: VecLike, n: number): boolean;
|
||||
// (undocumented)
|
||||
static Div(A: VecLike, t: number): Vec;
|
||||
// (undocumented)
|
||||
div(t: number): this;
|
||||
|
|
|
@ -605,14 +605,6 @@
|
|||
"text": "Vec",
|
||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", _zoom: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
|
@ -628,8 +620,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -650,14 +642,6 @@
|
|||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "_zoom",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -4438,7 +4422,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", _zoom: "
|
||||
"text": ", distance?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -4483,12 +4467,12 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "_zoom",
|
||||
"parameterName": "distance",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -5746,14 +5730,6 @@
|
|||
"text": "Vec",
|
||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", zoom: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
|
@ -5769,8 +5745,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -5791,14 +5767,6 @@
|
|||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "zoom",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -6449,61 +6417,6 @@
|
|||
],
|
||||
"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",
|
||||
"canonicalReference": "@tldraw/editor!DefaultScribble:function(1)",
|
||||
|
@ -7134,7 +7047,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", _zoom: "
|
||||
"text": ", distance?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -7179,12 +7092,12 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "_zoom",
|
||||
"parameterName": "distance",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -20311,14 +20224,6 @@
|
|||
"text": "Vec",
|
||||
"canonicalReference": "@tldraw/editor!Vec:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", zoom: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
|
@ -20334,8 +20239,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -20356,14 +20261,6 @@
|
|||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "zoom",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -28466,7 +28363,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", zoom: "
|
||||
"text": ", distance?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -28511,12 +28408,12 @@
|
|||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "zoom",
|
||||
"parameterName": "distance",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
"isOptional": true
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
|
@ -39669,41 +39566,6 @@
|
|||
"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",
|
||||
"canonicalReference": "@tldraw/editor!TLInterruptEvent:type",
|
||||
|
@ -41940,7 +41802,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
|
@ -43835,24 +43697,6 @@
|
|||
"text": "TLSelectionBackgroundProps",
|
||||
"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",
|
||||
"text": "> | null;\n OnTheCanvas: "
|
||||
|
@ -43906,7 +43750,7 @@
|
|||
"fileUrlPath": "packages/editor/src/lib/hooks/useEditorComponents.tsx",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 90
|
||||
"endIndex": 86
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
@ -46051,6 +45895,88 @@
|
|||
"isAbstract": false,
|
||||
"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",
|
||||
"canonicalReference": "@tldraw/editor!Vec#div:member(1)",
|
||||
|
|
|
@ -465,7 +465,7 @@ input,
|
|||
transform-origin: top left;
|
||||
fill: none;
|
||||
stroke-width: calc(1.5px * var(--tl-scale));
|
||||
contain: size;
|
||||
contain: size layout;
|
||||
}
|
||||
|
||||
/* ------------------ SelectionBox ------------------ */
|
||||
|
|
|
@ -62,10 +62,6 @@ export {
|
|||
DefaultHandles,
|
||||
type TLHandlesProps,
|
||||
} from './lib/components/default-components/DefaultHandles'
|
||||
export {
|
||||
DefaultHoveredShapeIndicator,
|
||||
type TLHoveredShapeIndicatorProps,
|
||||
} from './lib/components/default-components/DefaultHoveredShapeIndicator'
|
||||
export {
|
||||
DefaultScribble,
|
||||
type TLScribbleProps,
|
||||
|
|
|
@ -151,8 +151,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<BrushWrapper />
|
||||
<ScribbleWrapper />
|
||||
<ZoomBrushWrapper />
|
||||
<SelectedIdIndicators />
|
||||
<HoveredShapeIndicator />
|
||||
<ShapeIndicators />
|
||||
<HintedShapeIndicator />
|
||||
<SnapIndicatorWrapper />
|
||||
<SelectionForegroundWrapper />
|
||||
|
@ -431,16 +430,17 @@ function ShapesToDisplay() {
|
|||
)
|
||||
}
|
||||
|
||||
function SelectedIdIndicators() {
|
||||
function ShapeIndicators() {
|
||||
const editor = useEditor()
|
||||
const selectedShapeIds = useValue('selectedShapeIds', () => editor.getSelectedShapeIds(), [
|
||||
editor,
|
||||
])
|
||||
const shouldDisplay = useValue(
|
||||
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
|
||||
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
|
||||
const idsToDisplay = useValue(
|
||||
'should display selected ids',
|
||||
() => {
|
||||
// todo: move to tldraw selected ids wrapper
|
||||
return (
|
||||
// todo: move to tldraw selected ids wrappe
|
||||
const prev = rPreviousSelectedShapeIds.current
|
||||
const next = new Set<TLShapeId>()
|
||||
if (
|
||||
editor.isInAny(
|
||||
'select.idle',
|
||||
'select.brushing',
|
||||
|
@ -449,52 +449,51 @@ function SelectedIdIndicators() {
|
|||
'select.pointing_shape',
|
||||
'select.pointing_selection',
|
||||
'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]
|
||||
)
|
||||
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
|
||||
if (!ShapeIndicator) return null
|
||||
if (!shouldDisplay) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedShapeIds.map((id) => (
|
||||
<ShapeIndicator
|
||||
key={id + '_indicator'}
|
||||
className="tl-user-indicator__selected"
|
||||
shapeId={id}
|
||||
/>
|
||||
{renderingShapes.map(({ id }) => (
|
||||
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(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() {
|
||||
const editor = useEditor()
|
||||
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 classNames from 'classnames'
|
||||
import { memo } from 'react'
|
||||
import { memo, useLayoutEffect, useRef } from 'react'
|
||||
import type { Editor } from '../../editor/Editor'
|
||||
import { ShapeUtil } from '../../editor/shapes/ShapeUtil'
|
||||
import { useEditor } from '../../hooks/useEditor'
|
||||
|
@ -38,6 +38,7 @@ export type TLShapeIndicatorProps = {
|
|||
color?: string | undefined
|
||||
opacity?: number
|
||||
className?: string
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -45,28 +46,34 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
|
|||
shapeId,
|
||||
className,
|
||||
color,
|
||||
hidden,
|
||||
opacity,
|
||||
}: TLShapeIndicatorProps) {
|
||||
const editor = useEditor()
|
||||
|
||||
const transform = useValue(
|
||||
const rIndicator = useRef<SVGSVGElement>(null)
|
||||
|
||||
useQuickReactor(
|
||||
'indicator transform',
|
||||
() => {
|
||||
const elm = rIndicator.current
|
||||
if (!elm) return
|
||||
const pageTransform = editor.getShapePageTransform(shapeId)
|
||||
if (!pageTransform) return ''
|
||||
return pageTransform.toCssString()
|
||||
if (!pageTransform) return
|
||||
elm.style.setProperty('transform', pageTransform.toCssString())
|
||||
},
|
||||
[editor, shapeId]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const elm = rIndicator.current
|
||||
if (!elm) return
|
||||
elm.style.setProperty('display', hidden ? 'none' : 'block')
|
||||
}, [hidden])
|
||||
|
||||
return (
|
||||
<svg className={classNames('tl-overlays__item', className)}>
|
||||
<g
|
||||
className="tl-shape-indicator"
|
||||
transform={transform}
|
||||
stroke={color ?? 'var(--color-selected)'}
|
||||
opacity={opacity}
|
||||
>
|
||||
<svg ref={rIndicator} className={classNames('tl-overlays__item', className)}>
|
||||
<g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
|
||||
<InnerIndicator editor={editor} id={shapeId} />
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
@ -4490,7 +4490,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
isPointInShape(
|
||||
shape: TLShape | TLShapeId,
|
||||
point: VecLike,
|
||||
|
@ -8377,7 +8376,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
private _pendingEventsForNextTick: TLEventInfo[] = []
|
||||
|
||||
private _flushEventsForTick = (elapsed: number) => {
|
||||
private _flushEventsForTick(elapsed: number) {
|
||||
this.batch(() => {
|
||||
if (this._pendingEventsForNextTick.length > 0) {
|
||||
const events = [...this._pendingEventsForNextTick]
|
||||
|
|
|
@ -133,10 +133,8 @@ export class HandleSnaps {
|
|||
let minDistanceForSnapPoint = snapThreshold
|
||||
let nearestSnapPoint: Vec | null = null
|
||||
for (const snapPoint of this.iterateSnapPointsInPageSpace(currentShapeId, handle)) {
|
||||
const distance = Vec.Dist(handleInPageSpace, snapPoint)
|
||||
|
||||
if (distance < minDistanceForSnapPoint) {
|
||||
minDistanceForSnapPoint = distance
|
||||
if (Vec.DistMin(handleInPageSpace, snapPoint, minDistanceForSnapPoint)) {
|
||||
minDistanceForSnapPoint = Vec.Dist(handleInPageSpace, snapPoint)
|
||||
nearestSnapPoint = snapPoint
|
||||
}
|
||||
}
|
||||
|
@ -154,10 +152,9 @@ export class HandleSnaps {
|
|||
|
||||
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace)
|
||||
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
||||
const distance = Vec.Dist(handleInPageSpace, nearestInPageSpace)
|
||||
|
||||
if (distance < minDistanceForOutline) {
|
||||
minDistanceForOutline = distance
|
||||
if (Vec.DistMin(handleInPageSpace, nearestInPageSpace, minDistanceForOutline)) {
|
||||
minDistanceForOutline = Vec.Dist(handleInPageSpace, nearestInPageSpace)
|
||||
nearestPointOnOutline = nearestInPageSpace
|
||||
}
|
||||
}
|
||||
|
|
|
@ -263,8 +263,7 @@ export function getCurvedArrowInfo(
|
|||
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
||||
}
|
||||
|
||||
const distAB = Vec.Dist(tA, tB)
|
||||
if (distAB < minLength) {
|
||||
if (Vec.DistMin(tA, tB, minLength)) {
|
||||
if (offsetA !== 0 && offsetB !== 0) {
|
||||
offsetA *= -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 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) {
|
||||
// both bound + offset
|
||||
offsetA *= -1.5
|
||||
|
@ -241,7 +240,7 @@ function updateArrowheadPointWithBoundShape(
|
|||
|
||||
if (intersection !== null) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -146,10 +146,14 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
|
||||
handleEvent = (info: Exclude<TLEventInfo, TLPinchEventInfo>) => {
|
||||
const cbName = EVENT_NAME_MAP[info.name]
|
||||
const x = this.getCurrent()
|
||||
const currentActiveChild = this._current.__unsafe__getWithoutCapture()
|
||||
this[cbName]?.(info as any)
|
||||
if (this.getCurrent() === x && this.getIsActive()) {
|
||||
x?.handleEvent(info)
|
||||
if (
|
||||
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 { DefaultHandle, TLHandleProps } from '../components/default-components/DefaultHandle'
|
||||
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 {
|
||||
DefaultSelectionBackground,
|
||||
|
@ -71,7 +67,6 @@ export interface BaseEditorComponents {
|
|||
Spinner: ComponentType
|
||||
SelectionForeground: ComponentType<TLSelectionForegroundProps>
|
||||
SelectionBackground: ComponentType<TLSelectionBackgroundProps>
|
||||
HoveredShapeIndicator: ComponentType<TLHoveredShapeIndicatorProps>
|
||||
OnTheCanvas: ComponentType
|
||||
InFrontOfTheCanvas: ComponentType
|
||||
LoadingScreen: ComponentType
|
||||
|
@ -129,7 +124,6 @@ export function EditorComponentsProvider({
|
|||
Spinner: DefaultSpinner,
|
||||
SelectionBackground: DefaultSelectionBackground,
|
||||
SelectionForeground: DefaultSelectionForeground,
|
||||
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
|
||||
ShapeIndicator: DefaultShapeIndicator,
|
||||
OnTheCanvas: null,
|
||||
InFrontOfTheCanvas: null,
|
||||
|
|
|
@ -317,6 +317,11 @@ export class Vec {
|
|||
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.
|
||||
static Dist2(A: VecLike, B: VecLike): number {
|
||||
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 {
|
||||
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 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 (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)
|
||||
|
|
|
@ -66,7 +66,7 @@ export class Arc2d extends Geometry2d {
|
|||
return nearest
|
||||
}
|
||||
|
||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||
const { _center, radius, measure, angleStart, angleEnd } = this
|
||||
const intersection = intersectLineSegmentCircle(A, B, _center, radius)
|
||||
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))
|
||||
}
|
||||
|
||||
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
||||
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||
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
|
||||
}
|
||||
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
||||
return this.segments.some((segment) => segment.hitTestLineSegment(A, B, zoom))
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||
return this.segments.some((segment) => segment.hitTestLineSegment(A, B))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,9 @@ export class Edge2d extends Geometry2d {
|
|||
return new Vec(cx, cy)
|
||||
}
|
||||
|
||||
override hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
|
||||
return linesIntersect(A, B, this.start, this.end)
|
||||
override hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||
return (
|
||||
linesIntersect(A, B, this.start, this.end) || this.distanceToLineSegment(A, B) <= distance
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,8 +90,8 @@ export class Ellipse2d extends Geometry2d {
|
|||
return nearest
|
||||
}
|
||||
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
||||
return this.edges.some((edge) => edge.hitTestLineSegment(A, B, zoom))
|
||||
hitTestLineSegment(A: Vec, B: Vec): boolean {
|
||||
return this.edges.some((edge) => edge.hitTestLineSegment(A, B))
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
|
|
|
@ -30,24 +30,46 @@ export abstract class Geometry2d {
|
|||
|
||||
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) {
|
||||
// We've removed the broad phase here; that should be done outside of the call
|
||||
return this.distanceToPoint(point, hitInside) <= margin
|
||||
// First check whether the point is inside
|
||||
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) {
|
||||
const dist = point.dist(this.nearestPoint(point))
|
||||
|
||||
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
|
||||
return -dist
|
||||
}
|
||||
return dist
|
||||
return (
|
||||
point.dist(this.nearestPoint(point)) *
|
||||
(this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)
|
||||
? -1
|
||||
: 1)
|
||||
)
|
||||
}
|
||||
|
||||
distanceToLineSegment(A: Vec, B: Vec) {
|
||||
const point = this.nearestPointOnLineSegment(A, B)
|
||||
const dist = Vec.DistanceToLineSegment(A, B, point) // repeated, bleh
|
||||
return this.isClosed && this.isFilled && pointInPolygon(point, this.vertices) ? -dist : dist
|
||||
if (A.equals(B)) return this.distanceToPoint(A)
|
||||
const { vertices } = this
|
||||
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 {
|
||||
|
@ -58,14 +80,14 @@ export abstract class Geometry2d {
|
|||
const { vertices } = this
|
||||
let nearest: Vec | undefined
|
||||
let dist = Infinity
|
||||
let d: number
|
||||
let p: Vec
|
||||
let d: number, p: Vec, q: Vec
|
||||
for (let i = 0; i < vertices.length; 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) {
|
||||
dist = d
|
||||
nearest = p
|
||||
nearest = q
|
||||
}
|
||||
}
|
||||
if (!nearest) throw Error('nearest point not found')
|
||||
|
|
|
@ -65,7 +65,13 @@ export class Polyline2d extends Geometry2d {
|
|||
return nearest
|
||||
}
|
||||
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
|
||||
return this.segments.some((edge) => edge.hitTestLineSegment(A, B, zoom))
|
||||
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
|
||||
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 { Geometry2dOptions } from './Geometry2d'
|
||||
|
||||
const STADIUM_VERTICES_LENGTH = 18
|
||||
|
||||
/** @public */
|
||||
export class Stadium2d extends Ellipse2d {
|
||||
constructor(
|
||||
|
@ -12,28 +14,31 @@ export class Stadium2d extends Ellipse2d {
|
|||
}
|
||||
|
||||
getVertices() {
|
||||
// Perimeter of the ellipse
|
||||
const w = Math.max(1, this.w)
|
||||
const h = Math.max(1, this.h)
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
|
||||
const len = 10
|
||||
const points: Vec[] = Array(len * 2 - 2)
|
||||
|
||||
const points: Vec[] = Array(STADIUM_VERTICES_LENGTH)
|
||||
let t1: number, t2: number
|
||||
if (h > w) {
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
const t1 = -PI + (PI * i) / (len - 2)
|
||||
const t2 = (PI * i) / (len - 2)
|
||||
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
||||
t1 = -PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||
t2 = (PI * i) / (STADIUM_VERTICES_LENGTH - 2)
|
||||
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 {
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
const t1 = -HALF_PI + (PI * i) / (len - 2)
|
||||
const t2 = HALF_PI + (PI * -i) / (len - 2)
|
||||
for (let i = 0; i < STADIUM_VERTICES_LENGTH - 1; i++) {
|
||||
t1 = -HALF_PI + (PI * i) / (STADIUM_VERTICES_LENGTH - 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 + (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 { TLHandlesProps } from '@tldraw/editor';
|
||||
import { TLHighlightShape } from '@tldraw/editor';
|
||||
import { TLHoveredShapeIndicatorProps } from '@tldraw/editor';
|
||||
import { TLImageShape } from '@tldraw/editor';
|
||||
import { TLInterruptEvent } from '@tldraw/editor';
|
||||
import { TLKeyboardEvent } from '@tldraw/editor';
|
||||
|
@ -1452,9 +1451,6 @@ export interface TldrawFile {
|
|||
// @public (undocumented)
|
||||
export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export function TldrawHoveredShapeIndicator({ shapeId }: TLHoveredShapeIndicatorProps): JSX_2.Element | null;
|
||||
|
||||
// @public
|
||||
export const TldrawImage: NamedExoticComponent< {
|
||||
snapshot: StoreSnapshot<TLRecord>;
|
||||
|
|
|
@ -16579,61 +16579,6 @@
|
|||
],
|
||||
"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",
|
||||
"canonicalReference": "tldraw!TldrawImage:var",
|
||||
|
|
|
@ -9,7 +9,6 @@ export * from '@tldraw/editor'
|
|||
export { Tldraw, type TldrawProps } from './lib/Tldraw'
|
||||
export { TldrawImage, type TldrawImageProps } from './lib/TldrawImage'
|
||||
export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
||||
export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator'
|
||||
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
||||
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
||||
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
import { useLayoutEffect, useMemo } from 'react'
|
||||
import { TldrawHandles } from './canvas/TldrawHandles'
|
||||
import { TldrawHoveredShapeIndicator } from './canvas/TldrawHoveredShapeIndicator'
|
||||
import { TldrawScribble } from './canvas/TldrawScribble'
|
||||
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
||||
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
||||
|
@ -90,7 +89,6 @@ export function Tldraw(props: TldrawProps) {
|
|||
SelectionForeground: TldrawSelectionForeground,
|
||||
SelectionBackground: TldrawSelectionBackground,
|
||||
Handles: TldrawHandles,
|
||||
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
|
||||
..._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 (
|
||||
firstPoint !== lastPoint &&
|
||||
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
|
||||
const segments = [...shape.props.segments, newSegment]
|
||||
|
||||
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||
this.currentLineLength = this.getLineLength(segments)
|
||||
}
|
||||
|
||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||
id: shape.id,
|
||||
|
@ -411,7 +413,10 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
const finalSegments = [...newSegments, newFreeSegment]
|
||||
|
||||
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||
this.currentLineLength = this.getLineLength(finalSegments)
|
||||
}
|
||||
|
||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||
id,
|
||||
|
@ -486,11 +491,10 @@ export class Drawing extends StateNode {
|
|||
lastPoint,
|
||||
newPoint
|
||||
)
|
||||
const distance = Vec.Dist(nearestPointOnSegment, newPoint)
|
||||
|
||||
if (distance < minDistance) {
|
||||
if (Vec.DistMin(nearestPointOnSegment, newPoint, minDistance)) {
|
||||
nearestPoint = nearestPointOnSegment.toFixed().toJson()
|
||||
minDistance = distance
|
||||
minDistance = Vec.Dist(nearestPointOnSegment, newPoint)
|
||||
snapSegment = segment
|
||||
break
|
||||
}
|
||||
|
@ -598,7 +602,9 @@ export class Drawing extends StateNode {
|
|||
points: newPoints,
|
||||
}
|
||||
|
||||
if (this.currentLineLength < STROKE_SIZES[shape.props.size] * 4) {
|
||||
this.currentLineLength = this.getLineLength(newSegments)
|
||||
}
|
||||
|
||||
const shapePartial: TLShapePartial<DrawableShape> = {
|
||||
id,
|
||||
|
@ -659,7 +665,7 @@ export class Drawing extends StateNode {
|
|||
for (let i = 0; i < segment.points.length - 1; i++) {
|
||||
const A = segment.points[i]
|
||||
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 avgArcLength =
|
||||
arcs.reduce((sum, arc) => sum + Vec.Dist(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
||||
const shouldMutatePoints = avgArcLength > mutMultiplier * 15
|
||||
arcs.reduce((sum, arc) => sum + Vec.Dist2(arc.leftPoint, arc.rightPoint), 0) / arcs.length
|
||||
const shouldMutatePoints = avgArcLength > (mutMultiplier * 15) ** 2
|
||||
|
||||
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)}`
|
||||
|
|
|
@ -53,8 +53,8 @@ export class Pointing extends StateNode {
|
|||
const points = structuredClone(this.shape.props.points)
|
||||
|
||||
if (
|
||||
Vec.Dist(endHandle, prevEndHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES ||
|
||||
Vec.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES
|
||||
Vec.DistMin(endHandle, prevEndHandle, 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
|
||||
points[endHandle.id] = {
|
||||
|
|
|
@ -68,7 +68,7 @@ export function getStrokePoints(
|
|||
// Strip points that are too close to the first point.
|
||||
let pt = pts[1]
|
||||
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.splice(1, 1)
|
||||
pt = pts[1]
|
||||
|
@ -78,7 +78,7 @@ export function getStrokePoints(
|
|||
const last = pts.pop()!
|
||||
pt = pts[pts.length - 1]
|
||||
while (pt) {
|
||||
if (Vec.Dist(pt, last) > size / 3) break
|
||||
if (Vec.Dist2(pt, last) > (size / 3) ** 2) break
|
||||
pts.pop()
|
||||
pt = pts[pts.length - 1]
|
||||
pointsRemovedFromNearEnd++
|
||||
|
@ -88,7 +88,7 @@ export function getStrokePoints(
|
|||
const isComplete =
|
||||
options.last ||
|
||||
!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
|
||||
|
||||
// Add extra points between the two, to help avoid "dash" lines
|
||||
|
|
|
@ -79,35 +79,49 @@ export class Erasing extends StateNode {
|
|||
}
|
||||
|
||||
update() {
|
||||
const erasingShapeIds = this.editor.getErasingShapeIds()
|
||||
const zoomLevel = this.editor.getZoomLevel()
|
||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
||||
const { editor, excludedShapeIds } = this
|
||||
const erasingShapeIds = editor.getErasingShapeIds()
|
||||
const zoomLevel = editor.getZoomLevel()
|
||||
const currentPageShapes = editor.getCurrentPageShapes()
|
||||
const {
|
||||
inputs: { currentPagePoint, previousPagePoint },
|
||||
} = this.editor
|
||||
|
||||
const { excludedShapeIds } = this
|
||||
} = editor
|
||||
|
||||
this.pushPointToScribble()
|
||||
|
||||
const erasing = new Set<TLShapeId>(erasingShapeIds)
|
||||
const minDist = HIT_TEST_MARGIN / zoomLevel
|
||||
|
||||
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
|
||||
const pageMask = this.editor.getShapeMask(shape.id)
|
||||
const pageMask = editor.getShapeMask(shape.id)
|
||||
if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Hit test the shape using a line segment
|
||||
const geometry = this.editor.getShapeGeometry(shape)
|
||||
const A = this.editor.getPointInShapeSpace(shape, previousPagePoint)
|
||||
const B = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
||||
const geometry = editor.getShapeGeometry(shape)
|
||||
const pageTransform = editor.getShapePageTransform(shape)
|
||||
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)) {
|
||||
erasing.add(this.editor.getOutermostSelectableShape(shape).id)
|
||||
// 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
|
||||
}
|
||||
|
||||
if (geometry.hitTestLineSegment(A, B, minDist)) {
|
||||
erasing.add(editor.getOutermostSelectableShape(shape).id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,14 +132,16 @@ export class Erasing extends StateNode {
|
|||
}
|
||||
|
||||
complete() {
|
||||
this.editor.deleteShapes(this.editor.getCurrentPageState().erasingShapeIds)
|
||||
this.editor.setErasingShapes([])
|
||||
const { editor } = this
|
||||
editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds)
|
||||
editor.setErasingShapes([])
|
||||
this.parent.transition('idle')
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.editor.setErasingShapes([])
|
||||
this.editor.bailToMark(this.markId)
|
||||
const { editor } = this
|
||||
editor.setErasingShapes([])
|
||||
editor.bailToMark(this.markId)
|
||||
this.parent.transition('idle', this.info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Box,
|
||||
HIT_TEST_MARGIN,
|
||||
Mat,
|
||||
StateNode,
|
||||
TLCancelEvent,
|
||||
|
@ -24,7 +23,6 @@ export class Brushing extends StateNode {
|
|||
|
||||
info = {} as TLPointerEventInfo & { target: 'canvas' }
|
||||
|
||||
brush = new Box()
|
||||
initialSelectedShapeIds: TLShapeId[] = []
|
||||
excludedShapeIds = new Set<TLShapeId>()
|
||||
isWrapMode = false
|
||||
|
@ -103,18 +101,22 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
|
||||
private hitTestShapes() {
|
||||
const zoomLevel = this.editor.getZoomLevel()
|
||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
||||
const currentPageId = this.editor.getCurrentPageId()
|
||||
const { editor, excludedShapeIds, isWrapMode } = this
|
||||
const {
|
||||
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
|
||||
this.brush.setTo(Box.FromPoints([originPagePoint, currentPagePoint]))
|
||||
const brush = Box.FromPoints([originPagePoint, currentPagePoint])
|
||||
|
||||
// We'll be collecting shape ids
|
||||
const results = new Set(shiftKey ? this.initialSelectedShapeIds : [])
|
||||
// We'll be testing the corners of the brush against the shapes
|
||||
const { corners } = brush
|
||||
|
||||
let A: Vec,
|
||||
B: Vec,
|
||||
|
@ -123,64 +125,59 @@ export class Brushing extends StateNode {
|
|||
pageTransform: Mat | undefined,
|
||||
localCorners: Vec[]
|
||||
|
||||
// We'll be testing the corners of the brush against the shapes
|
||||
const { corners } = this.brush
|
||||
|
||||
const { excludedShapeIds, isWrapMode } = this
|
||||
|
||||
const isWrapping = isWrapMode ? !ctrlKey : ctrlKey
|
||||
const currentPageShapes = editor.getCurrentPageShapes()
|
||||
const currentPageId = editor.getCurrentPageId()
|
||||
|
||||
testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) {
|
||||
shape = currentPageShapes[i]
|
||||
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
||||
if (results.has(shape.id)) continue testAllShapes
|
||||
if (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes
|
||||
|
||||
pageBounds = this.editor.getShapePageBounds(shape)
|
||||
pageBounds = editor.getShapePageBounds(shape)
|
||||
if (!pageBounds) continue testAllShapes
|
||||
|
||||
// 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)
|
||||
continue testAllShapes
|
||||
}
|
||||
|
||||
// Should we even test for a single segment intersections? Only if
|
||||
// we're not holding the ctrl key for alternate selection mode
|
||||
// (only wraps count!), or if the shape is a frame.
|
||||
if (isWrapping || this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
||||
// If we're in wrap mode and the brush did not fully encloses the shape, it's a miss
|
||||
// We also skip frames unless we've completely selected the frame.
|
||||
if (isWrapping || editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
||||
continue testAllShapes
|
||||
}
|
||||
|
||||
// If the brush collides the page bounds, then do hit tests against
|
||||
// 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,
|
||||
// so we first need to get the brush corners in the shape's local space.
|
||||
const geometry = this.editor.getShapeGeometry(shape)
|
||||
|
||||
pageTransform = this.editor.getShapePageTransform(shape)
|
||||
|
||||
if (!pageTransform) {
|
||||
continue testAllShapes
|
||||
}
|
||||
|
||||
// Check whether any of the the brush edges intersect the shape
|
||||
pageTransform = editor.getShapePageTransform(shape)
|
||||
if (!pageTransform) continue testAllShapes
|
||||
localCorners = pageTransform.clone().invert().applyToPoints(corners)
|
||||
|
||||
hitTestBrushEdges: for (let i = 0; i < localCorners.length; i++) {
|
||||
// See if any of the edges intersect the shape's geometry
|
||||
const geometry = editor.getShapeGeometry(shape)
|
||||
hitTestBrushEdges: for (let i = 0; i < 4; i++) {
|
||||
A = localCorners[i]
|
||||
B = localCorners[(i + 1) % localCorners.length]
|
||||
|
||||
if (geometry.hitTestLineSegment(A, B, HIT_TEST_MARGIN / zoomLevel)) {
|
||||
B = localCorners[(i + 1) % 4]
|
||||
if (geometry.hitTestLineSegment(A, B, 0)) {
|
||||
this.handleHit(shape, currentPagePoint, currentPageId, results, corners)
|
||||
break hitTestBrushEdges
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.getInstanceState().isCoarsePointer
|
||||
|
||||
this.editor.updateInstanceState({ brush: { ...this.brush.toJson() } })
|
||||
this.editor.setSelectedShapes(Array.from(results), { squashing: true })
|
||||
const currentBrush = editor.getInstanceState().brush
|
||||
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 = () => {
|
||||
|
@ -203,7 +200,6 @@ export class Brushing extends StateNode {
|
|||
// page mask; and if so, check to see if the brush intersects it
|
||||
const selectedShape = this.editor.getOutermostSelectableShape(shape)
|
||||
const pageMask = this.editor.getShapeMask(selectedShape.id)
|
||||
|
||||
if (
|
||||
pageMask &&
|
||||
!polygonsIntersect(pageMask, corners) &&
|
||||
|
@ -211,7 +207,6 @@ export class Brushing extends StateNode {
|
|||
) {
|
||||
return
|
||||
}
|
||||
|
||||
results.add(selectedShape.id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,8 +88,8 @@ export class PointingArrowLabel extends StateNode {
|
|||
let nextLabelPosition
|
||||
if (info.isStraight) {
|
||||
// straight arrows
|
||||
const lineLength = Vec.Dist(info.start.point, info.end.point)
|
||||
const segmentLength = Vec.Dist(info.end.point, nearestPoint)
|
||||
const lineLength = Vec.Dist2(info.start.point, info.end.point)
|
||||
const segmentLength = Vec.Dist2(info.end.point, nearestPoint)
|
||||
nextLabelPosition = 1 - segmentLength / lineLength
|
||||
} else {
|
||||
const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0] as Arc2d
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Geometry2d,
|
||||
HIT_TEST_MARGIN,
|
||||
StateNode,
|
||||
TLEventHandlers,
|
||||
TLFrameShape,
|
||||
|
@ -8,7 +7,7 @@ import {
|
|||
TLShape,
|
||||
TLShapeId,
|
||||
Vec,
|
||||
intersectLineSegmentPolyline,
|
||||
intersectLineSegmentPolygon,
|
||||
pointInPolygon,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
|
@ -83,7 +82,8 @@ export class ScribbleBrushing extends StateNode {
|
|||
}
|
||||
|
||||
private updateScribbleSelection(addPoint: boolean) {
|
||||
const zoomLevel = this.editor.getZoomLevel()
|
||||
const { editor } = this
|
||||
// const zoomLevel = this.editor.getZoomLevel()
|
||||
const currentPageShapes = this.editor.getCurrentPageShapes()
|
||||
const {
|
||||
inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint },
|
||||
|
@ -98,36 +98,53 @@ export class ScribbleBrushing extends StateNode {
|
|||
const shapes = currentPageShapes
|
||||
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++) {
|
||||
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 (
|
||||
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||
editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
|
||||
newlySelectedShapeIds.has(shape.id) ||
|
||||
this.editor.isShapeOrAncestorLocked(shape)
|
||||
editor.isShapeOrAncestorLocked(shape)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
geometry = editor.getShapeGeometry(shape)
|
||||
|
||||
// If the scribble started inside of the frame, don't select it
|
||||
if (this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
||||
const point = this.editor.getPointInShapeSpace(shape, originPagePoint)
|
||||
if (geometry.bounds.containsPoint(point)) {
|
||||
if (
|
||||
editor.isShapeOfType<TLFrameShape>(shape, 'frame') &&
|
||||
geometry.bounds.containsPoint(editor.getPointInShapeSpace(shape, originPagePoint))
|
||||
) {
|
||||
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)
|
||||
B = this.editor.getPointInShapeSpace(shape, currentPagePoint)
|
||||
if (geometry.hitTestLineSegment(A, B, HIT_TEST_MARGIN / zoomLevel)) {
|
||||
if (geometry.hitTestLineSegment(A, B, minDist)) {
|
||||
const outermostShape = this.editor.getOutermostSelectableShape(shape)
|
||||
|
||||
const pageMask = this.editor.getShapeMask(outermostShape.id)
|
||||
|
||||
if (pageMask) {
|
||||
const intersection = intersectLineSegmentPolyline(
|
||||
const intersection = intersectLineSegmentPolygon(
|
||||
previousPagePoint,
|
||||
currentPagePoint,
|
||||
pageMask
|
||||
|
@ -142,16 +159,13 @@ export class ScribbleBrushing extends StateNode {
|
|||
}
|
||||
}
|
||||
|
||||
this.editor.setSelectedShapes(
|
||||
[
|
||||
...new Set<TLShapeId>(
|
||||
shiftKey
|
||||
? [...newlySelectedShapeIds, ...initialSelectedShapeIds]
|
||||
: [...newlySelectedShapeIds]
|
||||
),
|
||||
],
|
||||
{ squashing: true }
|
||||
const current = editor.getSelectedShapeIds()
|
||||
const next = new Set<TLShapeId>(
|
||||
shiftKey ? [...newlySelectedShapeIds, ...initialSelectedShapeIds] : [...newlySelectedShapeIds]
|
||||
)
|
||||
if (current.length !== next.size || current.some((id) => !next.has(id))) {
|
||||
this.editor.setSelectedShapes(Array.from(next), { squashing: true })
|
||||
}
|
||||
}
|
||||
|
||||
private complete() {
|
||||
|
|
|
@ -213,15 +213,14 @@ describe('<TldrawEditor />', () => {
|
|||
|
||||
// Is the shape's component rendering?
|
||||
expect(document.querySelectorAll('.tl-shape')).toHaveLength(1)
|
||||
|
||||
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(0)
|
||||
// though indicator should be display none
|
||||
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
|
||||
|
||||
// Select the shape
|
||||
await act(async () => editor.select(id))
|
||||
|
||||
expect(editor.getSelectedShapeIds().length).toBe(1)
|
||||
|
||||
// Is the shape's component rendering?
|
||||
// though indicator it should be visible
|
||||
expect(document.querySelectorAll('.tl-shape-indicator')).toHaveLength(1)
|
||||
|
||||
// Select the eraser tool...
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const now = () => {
|
||||
const hrTime = process.hrtime()
|
||||
return hrTime[0] * 1000 + hrTime[1] / 1000000
|
||||
return Number(process.hrtime.bigint()) / 1e6
|
||||
}
|
||||
|
||||
export class PerformanceMeasurer {
|
||||
|
@ -17,6 +16,7 @@ export class PerformanceMeasurer {
|
|||
cold = 0
|
||||
fastest = Infinity
|
||||
slowest = -Infinity
|
||||
didRun = false
|
||||
|
||||
totalStart = 0
|
||||
totalEnd = 0
|
||||
|
@ -60,6 +60,12 @@ export class PerformanceMeasurer {
|
|||
}
|
||||
|
||||
run() {
|
||||
if (this.didRun) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.didRun = true
|
||||
|
||||
const { fns, beforeFns, afterFns, warmupIterations, iterations } = this
|
||||
|
||||
// Run the cold run
|
||||
|
@ -134,20 +140,21 @@ export class PerformanceMeasurer {
|
|||
}
|
||||
|
||||
static Table(...ps: PerformanceMeasurer[]) {
|
||||
ps.forEach((p) => p.run())
|
||||
const table: Record<string, Record<string, number | string>> = {}
|
||||
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 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))
|
||||
|
||||
ps.forEach(
|
||||
(p) =>
|
||||
(table[p.name] = {
|
||||
['Runs']: p.warmupIterations + p.iterations,
|
||||
['Cold']: Number(p.cold.toFixed(2)),
|
||||
['Slowest']: Number(p.slowest.toFixed(2)),
|
||||
['Fastest']: Number(p.fastest.toFixed(2)),
|
||||
['Average']: Number(p.average.toFixed(2)),
|
||||
['Slower (Avg)']: Number((p.average / fastest).toFixed(2)),
|
||||
['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)),
|
||||
// ['Slower (Avg)']: Number((p.average / fastest).toFixed(2)),
|
||||
// ['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)),
|
||||
['Total']: Number(p.totalTime.toFixed(2)),
|
||||
})
|
||||
)
|
||||
// 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 { PerformanceMeasurer } from './PerformanceMeasurer'
|
||||
|
||||
|
@ -125,9 +125,6 @@ describe.skip('Example perf tests', () => {
|
|||
editor.updateShapes(shapesToUpdate)
|
||||
})
|
||||
|
||||
withUpdateShape.run()
|
||||
withUpdateShapes.run()
|
||||
|
||||
PerformanceMeasurer.Table(withUpdateShape, withUpdateShapes)
|
||||
}, 10000)
|
||||
|
||||
|
@ -157,7 +154,6 @@ describe.skip('Example perf tests', () => {
|
|||
const shape = editor.getCurrentPageShapes()[0]
|
||||
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||
})
|
||||
.run()
|
||||
|
||||
const renderingShapes2 = new PerformanceMeasurer('Measure rendering bounds with 200 shapes', {
|
||||
warmupIterations: 10,
|
||||
|
@ -184,8 +180,32 @@ describe.skip('Example perf tests', () => {
|
|||
const shape = editor.getCurrentPageShapes()[0]
|
||||
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||
})
|
||||
.run()
|
||||
|
||||
PerformanceMeasurer.Table(renderingShapes, renderingShapes2)
|
||||
}, 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…
Reference in a new issue