[improvement] Select unfilled shapes by clicking on their stroke (#438)
* removes touch events from middle of shapes * Improve ellipse * selectable stroke when not selected, fill when selected * Update BrushSession.spec.ts * Fix test
This commit is contained in:
parent
c67c0871ff
commit
52ae47371d
8 changed files with 230 additions and 185 deletions
1
apps/www/next-env.d.ts
vendored
1
apps/www/next-env.d.ts
vendored
|
@ -1,5 +1,4 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
|
|
@ -39,17 +39,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M
|
|||
|
||||
hitTestBounds = (shape: T, bounds: TLBounds) => {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
|
||||
if (!shape.rotation) {
|
||||
return (
|
||||
Utils.boundsContain(bounds, shapeBounds) ||
|
||||
Utils.boundsContain(shapeBounds, bounds) ||
|
||||
Utils.boundsCollide(shapeBounds, bounds)
|
||||
)
|
||||
}
|
||||
|
||||
const corners = Utils.getRotatedCorners(shapeBounds, shape.rotation)
|
||||
|
||||
return (
|
||||
corners.every((point) => Utils.pointInBounds(point, bounds)) ||
|
||||
intersectPolylineBounds(corners, bounds).length > 0
|
||||
|
|
|
@ -218,6 +218,26 @@ const tlcss = css`
|
|||
contain: layout style size;
|
||||
}
|
||||
|
||||
.tl-stroke-hitarea {
|
||||
cursor: pointer;
|
||||
fill: none;
|
||||
stroke: transparent;
|
||||
stroke-width: calc(24px * var(--tl-scale));
|
||||
pointer-events: stroke;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.tl-fill-hitarea {
|
||||
cursor: pointer;
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
stroke-width: calc(24px * var(--tl-scale));
|
||||
pointer-events: all;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.tl-counter-scaled {
|
||||
transform: scale(var(--tl-scale));
|
||||
}
|
||||
|
@ -354,6 +374,7 @@ const tlcss = css`
|
|||
|
||||
.tl-handle {
|
||||
pointer-events: all;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tl-handle:hover .tl-handle-bg {
|
||||
|
@ -365,6 +386,7 @@ const tlcss = css`
|
|||
}
|
||||
|
||||
.tl-handle:active .tl-handle-bg {
|
||||
cursor: grabbing;
|
||||
fill: var(--tl-selectFill);
|
||||
}
|
||||
|
||||
|
@ -389,6 +411,7 @@ const tlcss = css`
|
|||
stroke-width: calc(3px * var(--tl-scale));
|
||||
fill: var(--tl-selectFill);
|
||||
stroke: var(--tl-selected);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-centered-g {
|
||||
|
|
|
@ -6,9 +6,9 @@ describe('Brush session', () => {
|
|||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.movePointer([-10, -10])
|
||||
.movePointer([-48, -48])
|
||||
.startSession(SessionType.Brush)
|
||||
.movePointer([10, 10])
|
||||
.movePointer([48, 48])
|
||||
.completeSession()
|
||||
expect(app.status).toBe(TDStatus.Idle)
|
||||
expect(app.selectedIds.length).toBe(1)
|
||||
|
|
|
@ -144,16 +144,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
shaftPath =
|
||||
arrowDist > 2 ? (
|
||||
<>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||
strokeDasharray="none"
|
||||
strokeDashoffset="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
pointerEvents="stroke"
|
||||
/>
|
||||
<path className="tl-stroke-hitarea" d={path} />
|
||||
<path
|
||||
d={path}
|
||||
fill={styles.stroke}
|
||||
|
@ -207,17 +198,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
// Curved arrow path
|
||||
shaftPath = (
|
||||
<>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke="none"
|
||||
strokeWidth={Math.max(8, strokeWidth)}
|
||||
strokeDasharray="none"
|
||||
strokeDashoffset="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
pointerEvents="stroke"
|
||||
/>
|
||||
<path className="tl-stroke-hitarea" d={path} />
|
||||
<path
|
||||
d={path}
|
||||
fill={isDraw ? styles.stroke : 'none'}
|
||||
|
@ -227,7 +208,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
pointerEvents="stroke"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
@ -238,30 +219,38 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
|
|||
<g pointerEvents="none" opacity={isGhost ? GHOSTED_OPACITY : 1}>
|
||||
{shaftPath}
|
||||
{startArrowHead && (
|
||||
<>
|
||||
<path
|
||||
className="tl-stroke-hitarea"
|
||||
d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
|
||||
/>
|
||||
<path
|
||||
d={`M ${startArrowHead.left} L ${start.point} ${startArrowHead.right}`}
|
||||
fill="none"
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
pointerEvents="stroke"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{endArrowHead && (
|
||||
<>
|
||||
<path
|
||||
className="tl-stroke-hitarea"
|
||||
d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
|
||||
/>
|
||||
<path
|
||||
d={`M ${endArrowHead.left} L ${end.point} ${endArrowHead.right}`}
|
||||
fill="none"
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeDashoffset="none"
|
||||
strokeDasharray="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
pointerEvents="stroke"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
</SVGContainer>
|
||||
|
|
|
@ -51,7 +51,8 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
)
|
||||
}
|
||||
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(({ shape, meta, isGhost, events }, ref) => {
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, meta, isSelected, isGhost, events }, ref) => {
|
||||
const { points, style, isComplete } = shape
|
||||
|
||||
const polygonPathTDSnapshot = React.useMemo(() => {
|
||||
|
@ -97,6 +98,10 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
return (
|
||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
||||
<g opacity={isGhost ? GHOSTED_OPACITY : 1}>
|
||||
<path
|
||||
className={shouldFill && isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
d={pathTDSnapshot}
|
||||
/>
|
||||
{shouldFill && (
|
||||
<path
|
||||
d={polygonPathTDSnapshot}
|
||||
|
@ -104,7 +109,7 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
fill={fill}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
pointerEvents="fill"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
<path
|
||||
|
@ -114,7 +119,7 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
strokeWidth={strokeWidth / 2}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
</SVGContainer>
|
||||
|
@ -142,6 +147,10 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
return (
|
||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
||||
<g opacity={isGhost ? GHOSTED_OPACITY : 1}>
|
||||
<path
|
||||
className={shouldFill && isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
d={pathTDSnapshot}
|
||||
/>
|
||||
<path
|
||||
d={pathTDSnapshot}
|
||||
fill={shouldFill ? fill : 'none'}
|
||||
|
@ -149,7 +158,7 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
strokeWidth={Math.min(4, strokeWidth * 2)}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
pointerEvents={shouldFill ? 'all' : 'stroke'}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<path
|
||||
d={pathTDSnapshot}
|
||||
|
@ -160,12 +169,13 @@ export class DrawUtil extends TDShapeUtil<T, E> {
|
|||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
pointerEvents="stroke"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</g>
|
||||
</SVGContainer>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Indicator = TDShapeUtil.Indicator<T>(({ shape }) => {
|
||||
const { points } = shape
|
||||
|
|
|
@ -39,7 +39,7 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
|
|||
}
|
||||
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, isGhost, isBinding, meta, events }, ref) => {
|
||||
({ shape, isGhost, isSelected, isBinding, meta, events }, ref) => {
|
||||
const {
|
||||
radius: [radiusX, radiusY],
|
||||
style,
|
||||
|
@ -68,18 +68,25 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
|
|||
ry={ry + 2}
|
||||
/>
|
||||
)}
|
||||
<ellipse
|
||||
className={isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
cx={radiusX}
|
||||
cy={radiusY}
|
||||
rx={radiusX}
|
||||
ry={radiusY}
|
||||
/>
|
||||
<path
|
||||
d={getEllipseIndicatorPathTDSnapshot(shape, this.getCenter(shape))}
|
||||
stroke="none"
|
||||
fill={style.isFilled ? styles.fill : 'none'}
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<path
|
||||
d={path}
|
||||
fill={styles.stroke}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={styles.strokeWidth}
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
opacity={isGhost ? GHOSTED_OPACITY : 1}
|
||||
|
@ -111,16 +118,23 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
|
|||
/>
|
||||
)}
|
||||
<ellipse
|
||||
className={isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
cx={radiusX}
|
||||
cy={radiusY}
|
||||
rx={rx}
|
||||
ry={ry}
|
||||
rx={radiusX}
|
||||
ry={radiusY}
|
||||
/>
|
||||
<ellipse
|
||||
cx={radiusX}
|
||||
cy={radiusY}
|
||||
rx={radiusX}
|
||||
ry={radiusY}
|
||||
fill={styles.fill}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
@ -130,7 +144,16 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
|
|||
)
|
||||
|
||||
Indicator = TDShapeUtil.Indicator<T, M>(({ shape }) => {
|
||||
return <path d={getEllipseIndicatorPathTDSnapshot(shape, this.getCenter(shape))} />
|
||||
const {
|
||||
radius: [radiusX, radiusY],
|
||||
style: { dash },
|
||||
} = shape
|
||||
|
||||
return dash === DashStyle.Draw ? (
|
||||
<path d={getEllipseIndicatorPathTDSnapshot(shape, this.getCenter(shape))} />
|
||||
) : (
|
||||
<ellipse cx={radiusX} cy={radiusY} rx={radiusX} ry={radiusY} />
|
||||
)
|
||||
})
|
||||
|
||||
hitTestPoint = (shape: T, point: number[]): boolean => {
|
||||
|
|
|
@ -41,7 +41,7 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
}
|
||||
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, isBinding, isGhost, meta, events }, ref) => {
|
||||
({ shape, isBinding, isSelected, isGhost, meta, events }, ref) => {
|
||||
const { id, size, style } = shape
|
||||
|
||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||
|
@ -50,6 +50,7 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
const pathTDSnapshot = getRectanglePath(shape)
|
||||
const indicatorPath = getRectangleIndicatorPathTDSnapshot(shape)
|
||||
|
||||
return (
|
||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
||||
|
@ -63,18 +64,20 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
/>
|
||||
)}
|
||||
<path
|
||||
d={getRectangleIndicatorPathTDSnapshot(shape)}
|
||||
className={isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
d={indicatorPath}
|
||||
/>
|
||||
<path
|
||||
d={indicatorPath}
|
||||
fill={style.isFilled ? styles.fill : 'none'}
|
||||
radius={strokeWidth}
|
||||
stroke="none"
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<path
|
||||
d={pathTDSnapshot}
|
||||
fill={styles.stroke}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={styles.strokeWidth}
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
opacity={isGhost ? GHOSTED_OPACITY : 1}
|
||||
/>
|
||||
</SVGContainer>
|
||||
|
@ -107,9 +110,6 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
y1={start[1]}
|
||||
x2={end[0]}
|
||||
y2={end[1]}
|
||||
stroke={styles.stroke}
|
||||
strokeWidth={sw}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
/>
|
||||
|
@ -118,6 +118,7 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
return (
|
||||
<SVGContainer ref={ref} id={shape.id + '_svg'} {...events}>
|
||||
<g opacity={isGhost ? GHOSTED_OPACITY : 1}>
|
||||
{isBinding && (
|
||||
<rect
|
||||
className="tl-binding-indicator"
|
||||
|
@ -127,17 +128,27 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
|
|||
height={h + 64}
|
||||
/>
|
||||
)}
|
||||
<rect
|
||||
className={isSelected ? 'tl-fill-hitarea' : 'tl-stroke-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
fill={styles.fill}
|
||||
strokeWidth={sw}
|
||||
stroke="none"
|
||||
pointerEvents="all"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<g pointerEvents="stroke">{paths}</g>
|
||||
)}
|
||||
<g pointerEvents="none" stroke={styles.stroke} strokeWidth={sw} strokeLinecap="round">
|
||||
{paths}
|
||||
</g>
|
||||
</g>
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue