[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:
Steve Ruiz 2024-01-27 12:33:27 +00:00 committed by GitHub
parent 3f453569f6
commit ef1f331e1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 43 deletions

View file

@ -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(
if (!handles) return null 'handles',
if (!transform) return null () => {
if (!onlySelectedShape) return null
// Don't display a temporary handle if the distance between it and its neighbors is too small. const handles = editor.getShapeHandles(onlySelectedShape)
const handlesToDisplay: TLHandle[] = [] if (!handles) return null
for (let i = 0, handle = handles[i]; i < handles.length; i++, handle = handles[i]) { // ( •-)-----(-⦾-)----(-• ) ok
if (handle.type !== 'vertex') { // ( •-)(-⦾-)----(-• ) ok
const prev = handles[i - 1] // ( •()⦾-)----(-• ) not ok, hide the virtual handle
const next = handles[i + 1] const minDistBetweenVirtualHandlesAndRegularHandles =
if (prev && next) { ((isCoarse ? COARSE_HANDLE_RADIUS : HANDLE_RADIUS) / zoomLevel) * 2
if (Math.hypot(prev.y - next.y, prev.x - next.x) < MIN_HANDLE_DISTANCE / zoomLevel) {
continue
}
}
}
handlesToDisplay.push(handle) return handles
.sort((a) => (a.type === 'vertex' ? 1 : -1))
.filter((handle, i) => {
if (handle.type !== 'virtual') return true
const prev = handles[i - 1]
const next = handles[i + 1]
return (
(!prev || Vec.Dist(handle, prev) >= minDistBetweenVirtualHandlesAndRegularHandles) &&
(!next || Vec.Dist(handle, next) >= minDistBetweenVirtualHandlesAndRegularHandles)
)
})
},
[editor, onlySelectedShape, zoomLevel, isCoarse]
)
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}

View file

@ -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(

View file

@ -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