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:
parent
2c8b431c1f
commit
cd8c92779d
5 changed files with 39 additions and 13 deletions
|
@ -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;
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -369,6 +369,11 @@ export abstract class TLShapeUtil<T extends TLUnknownShape> {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
expandSelectionOutlinePx(shape: T): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue