[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:
parent
9d8a0b0a8d
commit
f21eaeb4d8
8 changed files with 64 additions and 37 deletions
|
@ -268,6 +268,10 @@ export class Box2d {
|
|||
x: number;
|
||||
// (undocumented)
|
||||
y: number;
|
||||
// (undocumented)
|
||||
static ZeroFix(other: Box2d | Box2dModel): Box2d;
|
||||
// (undocumented)
|
||||
zeroFix(): this;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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>(() => {
|
||||
|
|
Loading…
Reference in a new issue