[fix] zero width / height bounds (#1840)

This PR fixes zero width or height on Geometry2d bounds. It adds the
`zeroFix` helper to the `Box2d` class.

### Change Type

- [x] `patch` — Bug fix

### Test Plan

1. Create a straight line
2. Create a straight arrow that binds to the straight line

- [x] Unit Tests

### Release Notes

- Fix bug with straight lines / arrows
This commit is contained in:
Steve Ruiz 2023-09-08 15:45:30 +01:00 committed by GitHub
parent 9d8a0b0a8d
commit f21eaeb4d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 64 additions and 37 deletions

View file

@ -268,6 +268,10 @@ export class Box2d {
x: number;
// (undocumented)
y: number;
// (undocumented)
static ZeroFix(other: Box2d | Box2dModel): Box2d;
// (undocumented)
zeroFix(): this;
}
// @internal (undocumented)

View file

@ -79,8 +79,8 @@ export const Shape = track(function Shape({
if (!shape) return null
const bounds = editor.getShapeGeometry(shape).bounds
setProperty('width', Math.max(1, Math.ceil(bounds.width)) + 'px')
setProperty('height', Math.max(1, Math.ceil(bounds.height)) + 'px')
setProperty('width', Math.max(1, bounds.width) + 'px')
setProperty('height', Math.max(1, bounds.height) + 'px')
},
[editor]
)

View file

@ -16,30 +16,18 @@ export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
const rSvg = useRef<SVGSVGElement>(null)
useTransform(rSvg, brush.x, brush.y)
const w = toDomPrecision(Math.max(1, brush.w))
const h = toDomPrecision(Math.max(1, brush.h))
return (
<svg className="tl-overlays__item" ref={rSvg}>
{color ? (
<g className="tl-brush" opacity={opacity}>
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill={color}
opacity={0.75}
/>
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill="none"
stroke={color}
opacity={0.1}
/>
<rect width={w} height={h} fill={color} opacity={0.75} />
<rect width={w} height={h} fill="none" stroke={color} opacity={0.1} />
</g>
) : (
<rect
className="tl-brush tl-brush__default"
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
/>
<rect className="tl-brush tl-brush__default" width={w} height={h} />
)}
</svg>
)

View file

@ -32,10 +32,7 @@ export const DefaultSelectionForeground: TLSelectionForegroundComponent = ({
y: -expandOutlineBy,
})
bounds = bounds.clone().expandBy(expandOutlineBy)
const width = Math.max(1, bounds.width)
const height = Math.max(1, bounds.height)
bounds = bounds.clone().expandBy(expandOutlineBy).zeroFix()
return (
<svg
@ -45,8 +42,8 @@ export const DefaultSelectionForeground: TLSelectionForegroundComponent = ({
>
<rect
className={classNames('tl-selection__fg__outline')}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
width={toDomPrecision(bounds.width)}
height={toDomPrecision(bounds.height)}
/>
</svg>
)

View file

@ -542,6 +542,16 @@ export class Box2d {
static Equals(a: Box2d | Box2dModel, b: Box2d | Box2dModel) {
return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
}
zeroFix() {
this.w = Math.max(1, this.w)
this.h = Math.max(1, this.h)
return this
}
static ZeroFix(other: Box2d | Box2dModel) {
return new Box2d(other.x, other.y, Math.max(1, other.w), Math.max(1, other.h))
}
}
/** @public */

View file

@ -1,4 +1,5 @@
import {
Box2d,
RotateCorner,
TLEmbedShape,
TLSelectionForegroundComponent,
@ -22,7 +23,7 @@ const IS_FIREFOX =
navigator.userAgent.toLowerCase().indexOf('firefox') > -1
export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
function TldrawSelectionForeground({ bounds, rotation }) {
function TldrawSelectionForeground({ bounds, rotation }: { bounds: Box2d; rotation: number }) {
const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null)
@ -54,13 +55,13 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
})
if (!bounds) return null
bounds = bounds.clone().expandBy(expandOutlineBy)
bounds = bounds.clone().expandBy(expandOutlineBy).zeroFix()
const zoom = editor.zoomLevel
const isChangingStyle = editor.instanceState.isChangingStyle
const width = Math.max(1, bounds.width)
const height = Math.max(1, bounds.height)
const width = bounds.width
const height = bounds.height
const size = 8 / zoom
const isTinyX = width < size * 2
@ -239,7 +240,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
corner="bottom_right_rotate"
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>{' '}
/>
<MobileRotateHandle
data-testid="selection.rotate.mobile"
cx={isSmallX ? -targetSize * 1.5 : width / 2}
@ -257,7 +258,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
pointerEvents="all"
x={0}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
width={toDomPrecision(width)}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...topEvents}
@ -271,7 +272,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
height={toDomPrecision(height)}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...rightEvents}
@ -285,7 +286,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
pointerEvents="all"
x={0}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
width={toDomPrecision(width)}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...bottomEvents}
@ -299,7 +300,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
height={toDomPrecision(height)}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...leftEvents}

View file

@ -499,3 +499,29 @@ describe('reparenting issue', () => {
expect(editor.getShape(arrow2Id)!.index).toBe('a1G')
})
})
describe('line bug', () => {
it('works as expected when binding to a straight line', () => {
editor.selectAll().deleteShapes(editor.selectedShapeIds)
expect(editor.currentPageShapes.length).toBe(0)
editor
.setCurrentTool('line')
.keyDown('Shift')
.pointerMove(0, 0)
.pointerDown()
.pointerMove(0, 100)
.pointerUp()
.keyUp('Shift')
.setCurrentTool('arrow')
.keyDown('Shift')
.pointerMove(50, 50)
.pointerDown()
.pointerMove(0, 50)
.pointerUp()
.keyUp('Shift')
expect(editor.currentPageShapes.length).toBe(2)
})
})

View file

@ -1,5 +1,6 @@
import {
Arc2d,
Box2d,
DefaultFontFamilies,
Edge2d,
Group2d,
@ -260,7 +261,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// we've got a target! the handle is being dragged over a shape, bind to it
const targetGeometry = this.editor.getShapeGeometry(target)
const targetBounds = targetGeometry.bounds
const targetBounds = Box2d.ZeroFix(targetGeometry.bounds)
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
let precise = isPrecise
@ -492,7 +493,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
) && !this.editor.instanceState.isReadonly
const info = this.editor.getArrowInfo(shape)
const bounds = this.editor.getShapeGeometry(shape).bounds
const bounds = Box2d.ZeroFix(this.editor.getShapeGeometry(shape).bounds)
// eslint-disable-next-line react-hooks/rules-of-hooks
const changeIndex = React.useMemo<number>(() => {