[Fix] Missing bend handles on curved arrows (#2661)
Fixes #2660. <img width="629" alt="image" src="https://github.com/tldraw/tldraw/assets/23072548/a661b76c-4877-42b1-aca7-5e5fcc5bc44b"> ### Change Type - [x] `patch` — Bug fix ### Test Plan 1. Create a handle with a lot of bend but with a start and end handle that are close together. The bend handle should still be visible. ### Release Notes - Fixed a bug where the bend handle on arrows with a large curve could sometimes be hidden.
This commit is contained in:
parent
3f453569f6
commit
ef1f331e1f
3 changed files with 52 additions and 43 deletions
|
@ -3,6 +3,7 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../constants'
|
||||||
import { useCanvasEvents } from '../hooks/useCanvasEvents'
|
import { useCanvasEvents } from '../hooks/useCanvasEvents'
|
||||||
import { useCoarsePointer } from '../hooks/useCoarsePointer'
|
import { useCoarsePointer } from '../hooks/useCoarsePointer'
|
||||||
import { useDocumentEvents } from '../hooks/useDocumentEvents'
|
import { useDocumentEvents } from '../hooks/useDocumentEvents'
|
||||||
|
@ -13,6 +14,7 @@ import { useGestureEvents } from '../hooks/useGestureEvents'
|
||||||
import { useHandleEvents } from '../hooks/useHandleEvents'
|
import { useHandleEvents } from '../hooks/useHandleEvents'
|
||||||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||||
import { Mat } from '../primitives/Mat'
|
import { Mat } from '../primitives/Mat'
|
||||||
|
import { Vec } from '../primitives/Vec'
|
||||||
import { toDomPrecision } from '../primitives/utils'
|
import { toDomPrecision } from '../primitives/utils'
|
||||||
import { debugFlags } from '../utils/debug-flags'
|
import { debugFlags } from '../utils/debug-flags'
|
||||||
import { GeometryDebuggingView } from './GeometryDebuggingView'
|
import { GeometryDebuggingView } from './GeometryDebuggingView'
|
||||||
|
@ -204,77 +206,77 @@ function SnapLinesWrapper() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_HANDLE_DISTANCE = 48
|
|
||||||
|
|
||||||
function HandlesWrapper() {
|
function HandlesWrapper() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const { Handles } = useEditorComponents()
|
const { Handles } = useEditorComponents()
|
||||||
|
|
||||||
const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
|
const zoomLevel = useValue('zoomLevel', () => editor.getZoomLevel(), [editor])
|
||||||
|
|
||||||
const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [
|
const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [
|
||||||
editor,
|
editor,
|
||||||
])
|
])
|
||||||
const onlySelectedShape = useValue('onlySelectedShape', () => editor.getOnlySelectedShape(), [
|
|
||||||
|
const isReadonly = useValue('isChangingStyle', () => editor.getInstanceState().isReadonly, [
|
||||||
editor,
|
editor,
|
||||||
])
|
])
|
||||||
|
|
||||||
const isChangingStyle = useValue(
|
const isChangingStyle = useValue(
|
||||||
'isChangingStyle',
|
'isChangingStyle',
|
||||||
() => editor.getInstanceState().isChangingStyle,
|
() => editor.getInstanceState().isChangingStyle,
|
||||||
[editor]
|
[editor]
|
||||||
)
|
)
|
||||||
const isReadonly = useValue('isChangingStyle', () => editor.getInstanceState().isReadonly, [
|
|
||||||
|
const onlySelectedShape = useValue('onlySelectedShape', () => editor.getOnlySelectedShape(), [
|
||||||
editor,
|
editor,
|
||||||
])
|
])
|
||||||
const handles = useValue(
|
|
||||||
'handles',
|
|
||||||
() => {
|
|
||||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
||||||
if (onlySelectedShape) {
|
|
||||||
return editor.getShapeHandles(onlySelectedShape)
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
const transform = useValue(
|
const transform = useValue(
|
||||||
'transform',
|
'transform',
|
||||||
() => {
|
() => {
|
||||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
if (!onlySelectedShape) return null
|
||||||
if (onlySelectedShape) {
|
|
||||||
return editor.getShapePageTransform(onlySelectedShape)
|
return editor.getShapePageTransform(onlySelectedShape)
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
},
|
},
|
||||||
[editor]
|
[editor, onlySelectedShape]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly) return null
|
const handles = useValue(
|
||||||
|
'handles',
|
||||||
|
() => {
|
||||||
|
if (!onlySelectedShape) return null
|
||||||
|
|
||||||
|
const handles = editor.getShapeHandles(onlySelectedShape)
|
||||||
if (!handles) return null
|
if (!handles) return null
|
||||||
if (!transform) return null
|
|
||||||
|
|
||||||
// Don't display a temporary handle if the distance between it and its neighbors is too small.
|
// ( •-)-----(-⦾-)----(-• ) ok
|
||||||
const handlesToDisplay: TLHandle[] = []
|
// ( •-)(-⦾-)----(-• ) ok
|
||||||
|
// ( •()⦾-)----(-• ) not ok, hide the virtual handle
|
||||||
|
const minDistBetweenVirtualHandlesAndRegularHandles =
|
||||||
|
((isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoomLevel) * 2
|
||||||
|
|
||||||
for (let i = 0, handle = handles[i]; i < handles.length; i++, handle = handles[i]) {
|
return handles
|
||||||
if (handle.type !== 'vertex') {
|
.sort((a) => (a.type === 'vertex' ? 1 : -1))
|
||||||
|
.filter((handle, i) => {
|
||||||
|
if (handle.type !== 'virtual') return true
|
||||||
const prev = handles[i - 1]
|
const prev = handles[i - 1]
|
||||||
const next = handles[i + 1]
|
const next = handles[i + 1]
|
||||||
if (prev && next) {
|
return (
|
||||||
if (Math.hypot(prev.y - next.y, prev.x - next.x) < MIN_HANDLE_DISTANCE / zoomLevel) {
|
(!prev || Vec.Dist(handle, prev) >= minDistBetweenVirtualHandlesAndRegularHandles) &&
|
||||||
continue
|
(!next || Vec.Dist(handle, next) >= minDistBetweenVirtualHandlesAndRegularHandles)
|
||||||
}
|
)
|
||||||
}
|
})
|
||||||
}
|
},
|
||||||
|
[editor, onlySelectedShape, zoomLevel, isCoarse]
|
||||||
|
)
|
||||||
|
|
||||||
handlesToDisplay.push(handle)
|
if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly || !handles || !transform) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Handles>
|
<Handles>
|
||||||
<g transform={Mat.toCssString(transform)}>
|
<g transform={Mat.toCssString(transform)}>
|
||||||
{handlesToDisplay.map((handle) => {
|
{handles.map((handle) => {
|
||||||
return (
|
return (
|
||||||
<HandleWrapper
|
<HandleWrapper
|
||||||
key={handle.id}
|
key={handle.id}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { ComponentType } from 'react'
|
import { ComponentType } from 'react'
|
||||||
|
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS } from '../../constants'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLHandleComponent = ComponentType<{
|
export type TLHandleComponent = ComponentType<{
|
||||||
|
@ -13,9 +14,6 @@ export type TLHandleComponent = ComponentType<{
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className, zoom }) => {
|
export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className, zoom }) => {
|
||||||
const bgRadius = (isCoarse ? 20 : 12) / zoom
|
|
||||||
const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom
|
|
||||||
|
|
||||||
if (handle.type === 'text-adjust') {
|
if (handle.type === 'text-adjust') {
|
||||||
return (
|
return (
|
||||||
<g className={classNames('tl-handle', className)}>
|
<g className={classNames('tl-handle', className)}>
|
||||||
|
@ -24,6 +22,9 @@ export const DefaultHandle: TLHandleComponent = ({ handle, isCoarse, className,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bgRadius = (isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoom
|
||||||
|
const fgRadius = (handle.type === 'create' && isCoarse ? 3 : 4) / zoom
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -101,3 +101,9 @@ export const EDGE_SCROLL_DISTANCE = 8
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const COARSE_POINTER_WIDTH = 12
|
export const COARSE_POINTER_WIDTH = 12
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const COARSE_HANDLE_RADIUS = 20
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const HANDLE_RADIUS = 12
|
||||||
|
|
Loading…
Reference in a new issue