Expand selection outline for single-selected draw shape (#1379)

Expand the selection outline on draw shapes according to pen thickness
when only one shape is selected.

![Kapture 2023-05-15 at 16 20
01](https://github.com/tldraw/tldraw/assets/1489520/373f0ec1-f43d-46c9-9729-0c84aaf2564b)

Right now the outline of many of our shapes don't take stroke thickness
into account. This is a pretty hard thing to get right, so in the short
term here's a fix for one of the most common places this is an issue:
selecting a single horizontal/vertical draw shape. This fix isn't
perfect: resizing gets slightly janky when you completely flip the shape
- see how the handle leaves the cursor behind in the gif when that
happens. We can revisit with a more comprehensive solution later.

This is pulled out from the highlighter work! The highlighter shape will
use the shape APIs added here.

### Change Type

- [x] `patch` — Bug Fix

### Test Plan

1. Create a draw shape
2. Select it
3. Selection bounds should include the stroke width
4. Add another shape to the selection
5. Selection bounds should no longer include the stroke width

### Release Notes

- Improve selection outlines around horizontal or vertical draw shapes
This commit is contained in:
alex 2023-05-16 09:35:45 +01:00 committed by GitHub
parent 2c8b431c1f
commit cd8c92779d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 39 additions and 13 deletions

View file

@ -1864,6 +1864,8 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
// (undocumented) // (undocumented)
defaultProps(): TLDrawShape['props']; defaultProps(): TLDrawShape['props'];
// (undocumented) // (undocumented)
expandSelectionOutlinePx(shape: TLDrawShape): number;
// (undocumented)
getBounds(shape: TLDrawShape): Box2d; getBounds(shape: TLDrawShape): Box2d;
// (undocumented) // (undocumented)
getCenter(shape: TLDrawShape): Vec2d; getCenter(shape: TLDrawShape): Vec2d;
@ -2513,6 +2515,8 @@ export abstract class TLShapeUtil<T extends TLUnknownShape> {
canUnmount: TLShapeUtilFlag<T>; canUnmount: TLShapeUtilFlag<T>;
center(shape: T): Vec2dModel; center(shape: T): Vec2dModel;
abstract defaultProps(): T['props']; abstract defaultProps(): T['props'];
// @internal (undocumented)
expandSelectionOutlinePx(shape: T): number;
protected abstract getBounds(shape: T): Box2d; protected abstract getBounds(shape: T): Box2d;
abstract getCenter(shape: T): Vec2dModel; abstract getCenter(shape: T): Vec2dModel;
getEditingBounds: (shape: T) => Box2d; getEditingBounds: (shape: T) => Box2d;

View file

@ -303,6 +303,11 @@ export class TLDrawUtil extends TLShapeUtil<TLDrawShape> {
}, },
} }
} }
expandSelectionOutlinePx(shape: TLDrawShape): number {
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
return (this.app.getStrokeWidth(shape.props.size) * multiplier) / 2
}
} }
/** @public */ /** @public */

View file

@ -369,6 +369,11 @@ export abstract class TLShapeUtil<T extends TLUnknownShape> {
return false return false
} }
/** @internal */
expandSelectionOutlinePx(shape: T): number {
return 0
}
// Events // Events
/** /**

View file

@ -25,14 +25,24 @@ export const SelectionFg = track(function SelectionFg() {
const isDefaultCursor = !app.isMenuOpen && app.cursor.type === 'default' const isDefaultCursor = !app.isMenuOpen && app.cursor.type === 'default'
const isCoarsePointer = app.isCoarsePointer const isCoarsePointer = app.isCoarsePointer
const bounds = app.selectionBounds let bounds = app.selectionBounds
const shapes = app.selectedShapes
const onlyShape = shapes.length === 1 ? shapes[0] : null
useTransform(rSvg, bounds?.x, bounds?.y, 1, app.selectionRotation) // if all shapes have an expandBy for the selection outline, we can expand by the l
const expandOutlineBy = onlyShape
? app.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape)
: 0
useTransform(rSvg, bounds?.x, bounds?.y, 1, app.selectionRotation, {
x: -expandOutlineBy,
y: -expandOutlineBy,
})
if (!bounds) return null if (!bounds) return null
bounds = bounds.clone().expandBy(expandOutlineBy)
const zoom = app.zoomLevel const zoom = app.zoomLevel
const shapes = app.selectedShapes
const rotation = app.selectionRotation const rotation = app.selectionRotation
const isChangingStyles = app.isChangingStyle const isChangingStyles = app.isChangingStyle
@ -54,8 +64,6 @@ export const SelectionFg = track(function SelectionFg() {
const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75) const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75) const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const onlyShape = shapes.length === 1 ? shapes[0] : null
const showSelectionBounds = const showSelectionBounds =
(onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) && (onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) &&
!isChangingStyles !isChangingStyles

View file

@ -1,3 +1,4 @@
import { VecLike } from '@tldraw/primitives'
import { useLayoutEffect } from 'react' import { useLayoutEffect } from 'react'
export function useTransform( export function useTransform(
@ -5,21 +6,24 @@ export function useTransform(
x?: number, x?: number,
y?: number, y?: number,
scale?: number, scale?: number,
rotate?: number rotate?: number,
additionalOffset?: VecLike
) { ) {
useLayoutEffect(() => { useLayoutEffect(() => {
const elm = ref.current const elm = ref.current
if (!elm) return if (!elm) return
if (x === undefined) return if (x === undefined) return
let trans = `translate(${x}px, ${y}px)`
if (scale !== undefined) { if (scale !== undefined) {
trans += ` scale(${scale})`
}
if (rotate !== undefined) { if (rotate !== undefined) {
elm.style.transform = `translate(${x}px, ${y}px) scale(${scale}) rotate(${rotate}rad)` trans += ` rotate(${rotate}rad)`
} else {
elm.style.transform = `translate(${x}px, ${y}px) scale(${scale})`
} }
} else { if (additionalOffset) {
elm.style.transform = `translate(${x}px, ${y}px)` trans += ` translate(${additionalOffset.x}px, ${additionalOffset.y}px)`
} }
elm.style.transform = trans
}) })
} }