[fix] overlay rendering issues (#1389)
This PR fixes several issues with the way that SVG overlays were rendered. - fixes editing embed shape on firefox (weird SVG pointer events bug) - fixes layering of overlays - collaborator cursors are offset ### Change Type - [x] `patch` — change to unshipped changes ### Test Plan 1. Try editing an embed shape on Firefox 2. Confirm that cursor hints are no longer spinning 3. Confirm that cursors are displayed correctly over other shapes
This commit is contained in:
parent
267fea8d5a
commit
0cc95c271d
14 changed files with 467 additions and 339 deletions
|
@ -3,6 +3,11 @@ import '@tldraw/tldraw/editor.css'
|
||||||
import '@tldraw/tldraw/ui.css'
|
import '@tldraw/tldraw/ui.css'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
const SHOW_MOVING_CURSOR = false
|
||||||
|
const CURSOR_SPEED = 0.1
|
||||||
|
const CIRCLE_RADIUS = 100
|
||||||
|
const UPDATE_FPS = 60
|
||||||
|
|
||||||
export default function UserPresenceExample() {
|
export default function UserPresenceExample() {
|
||||||
const rTimeout = useRef<any>(-1)
|
const rTimeout = useRef<any>(-1)
|
||||||
return (
|
return (
|
||||||
|
@ -17,6 +22,7 @@ export default function UserPresenceExample() {
|
||||||
// we're having to create these ourselves.
|
// we're having to create these ourselves.
|
||||||
|
|
||||||
const userId = TLUser.createCustomId('user-1')
|
const userId = TLUser.createCustomId('user-1')
|
||||||
|
|
||||||
const user = TLUser.create({
|
const user = TLUser.create({
|
||||||
id: userId,
|
id: userId,
|
||||||
name: 'User 1',
|
name: 'User 1',
|
||||||
|
@ -48,22 +54,34 @@ export default function UserPresenceExample() {
|
||||||
clearTimeout(rTimeout.current)
|
clearTimeout(rTimeout.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
rTimeout.current = setInterval(() => {
|
if (SHOW_MOVING_CURSOR) {
|
||||||
const SPEED = 0.1
|
rTimeout.current = setInterval(() => {
|
||||||
const R = 400
|
const k = 1000 / CURSOR_SPEED
|
||||||
const k = 1000 / SPEED
|
const now = Date.now()
|
||||||
const t = (Date.now() % k) / k
|
const t = (now % k) / k
|
||||||
// rotate in a circle
|
// rotate in a circle
|
||||||
const x = Math.cos(t * Math.PI * 2) * R
|
app.store.put([
|
||||||
const y = Math.sin(t * Math.PI * 2) * R
|
{
|
||||||
|
...userPresence,
|
||||||
|
cursor: {
|
||||||
|
x: Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||||
|
y: Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||||
|
},
|
||||||
|
lastActivityTimestamp: now,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, 1000 / UPDATE_FPS)
|
||||||
|
} else {
|
||||||
app.store.put([
|
app.store.put([
|
||||||
{
|
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
||||||
...userPresence,
|
|
||||||
cursor: { x, y },
|
|
||||||
lastActivityTimestamp: Date.now(),
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
}, 100)
|
|
||||||
|
rTimeout.current = setInterval(() => {
|
||||||
|
app.store.put([
|
||||||
|
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
||||||
|
])
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -262,6 +262,15 @@ input,
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-overlays__item {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-svg-context {
|
.tl-svg-context {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
|
@ -271,15 +280,6 @@ input,
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-svg-origin-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: none;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------- Background ------------------- */
|
/* ------------------- Background ------------------- */
|
||||||
|
|
||||||
.tl-background {
|
.tl-background {
|
||||||
|
@ -399,6 +399,68 @@ input,
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------- Overlay Stack --------------- */
|
||||||
|
|
||||||
|
/* back of the stack, behind user's stuff */
|
||||||
|
.tl-collaborator__scribble {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-collaborator__brush {
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-collaborator__shape-indicator {
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-handles {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-scribble {
|
||||||
|
z-index: 52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-brush {
|
||||||
|
z-index: 51;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-indicator__selected {
|
||||||
|
z-index: 51;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-indicator__hovered {
|
||||||
|
z-index: 52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-snapline {
|
||||||
|
z-index: 53;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-selection__fg {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-user-indicator__hint {
|
||||||
|
z-index: 55;
|
||||||
|
stroke-width: calc(2.5px * var(--tl-scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* behind collaborator cursor */
|
||||||
|
.tl-collaborator__cursor-hint {
|
||||||
|
z-index: 56;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-collaborator__cursor {
|
||||||
|
z-index: 57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-cursor {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------- Indicator ------------------- */
|
/* -------------------- Indicator ------------------- */
|
||||||
|
|
||||||
.tl-shape-indicator {
|
.tl-shape-indicator {
|
||||||
|
@ -407,13 +469,9 @@ input,
|
||||||
stroke-width: calc(1.5px * var(--tl-scale));
|
stroke-width: calc(1.5px * var(--tl-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-shape-indicator__hinting {
|
|
||||||
stroke-width: calc(2.5px * var(--tl-scale));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------ SelectionBox ------------------ */
|
/* ------------------ SelectionBox ------------------ */
|
||||||
|
|
||||||
.tlui-selection__bg {
|
.tl-selection__bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
@ -422,11 +480,7 @@ input,
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-selection__fg {
|
.tl-selection__fg__outline {
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-selection__fg__outline {
|
|
||||||
fill: none;
|
fill: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
stroke: var(--color-selection-stroke);
|
stroke: var(--color-selection-stroke);
|
||||||
|
|
|
@ -119,15 +119,15 @@ export const Canvas = track(function Canvas({
|
||||||
<ShapesToDisplay />
|
<ShapesToDisplay />
|
||||||
</div>
|
</div>
|
||||||
<div className="tl-overlays">
|
<div className="tl-overlays">
|
||||||
<ScribbleWrapper />
|
<HandlesWrapper />
|
||||||
<BrushWrapper />
|
<BrushWrapper />
|
||||||
|
<ScribbleWrapper />
|
||||||
<ZoomBrushWrapper />
|
<ZoomBrushWrapper />
|
||||||
<SelectedIdIndicators />
|
<SelectedIdIndicators />
|
||||||
<HoveredShapeIndicator />
|
<HoveredShapeIndicator />
|
||||||
<SelectionFg />
|
|
||||||
<HintedShapeIndicator />
|
<HintedShapeIndicator />
|
||||||
<SnapLinesWrapper />
|
<SnapLinesWrapper />
|
||||||
<HandlesWrapper />
|
<SelectionFg />
|
||||||
{debugFlags.newLiveCollaborators.value ? (
|
{debugFlags.newLiveCollaborators.value ? (
|
||||||
<LiveCollaboratorsNext />
|
<LiveCollaboratorsNext />
|
||||||
) : (
|
) : (
|
||||||
|
@ -162,7 +162,7 @@ const ScribbleWrapper = track(function ScribbleWrapper() {
|
||||||
|
|
||||||
if (!(Scribble && scribble)) return null
|
if (!(Scribble && scribble)) return null
|
||||||
|
|
||||||
return <Scribble scribble={scribble} zoom={zoom} />
|
return <Scribble className="tl-user-scribble" scribble={scribble} zoom={zoom} />
|
||||||
})
|
})
|
||||||
|
|
||||||
const BrushWrapper = track(function BrushWrapper() {
|
const BrushWrapper = track(function BrushWrapper() {
|
||||||
|
@ -172,7 +172,7 @@ const BrushWrapper = track(function BrushWrapper() {
|
||||||
|
|
||||||
if (!(Brush && brush && app.isIn('select.brushing'))) return null
|
if (!(Brush && brush && app.isIn('select.brushing'))) return null
|
||||||
|
|
||||||
return <Brush brush={brush} />
|
return <Brush className="tl-user-brush" brush={brush} />
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ZoomBrushWrapper = track(function Zoom() {
|
export const ZoomBrushWrapper = track(function Zoom() {
|
||||||
|
@ -182,7 +182,7 @@ export const ZoomBrushWrapper = track(function Zoom() {
|
||||||
|
|
||||||
if (!(ZoomBrush && zoomBrush && app.isIn('zoom'))) return null
|
if (!(ZoomBrush && zoomBrush && app.isIn('zoom'))) return null
|
||||||
|
|
||||||
return <ZoomBrush brush={zoomBrush} />
|
return <ZoomBrush className="tl-user-brush" brush={zoomBrush} />
|
||||||
})
|
})
|
||||||
|
|
||||||
export const SnapLinesWrapper = track(function SnapLines() {
|
export const SnapLinesWrapper = track(function SnapLines() {
|
||||||
|
@ -198,7 +198,7 @@ export const SnapLinesWrapper = track(function SnapLines() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{lines.map((line) => (
|
{lines.map((line) => (
|
||||||
<SnapLine line={line} key={line.id} zoom={zoomLevel} />
|
<SnapLine key={line.id} className="tl-user-snapline" line={line} zoom={zoomLevel} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -248,7 +248,7 @@ const HandlesWrapper = track(function HandlesWrapper() {
|
||||||
handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1))
|
handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container">
|
<svg className="tl-user-handles tl-overlays__item">
|
||||||
<g transform={Matrix2d.toCssString(transform)}>
|
<g transform={Matrix2d.toCssString(transform)}>
|
||||||
{handlesToDisplay.map((handle) => {
|
{handlesToDisplay.map((handle) => {
|
||||||
return <HandleWrapper key={handle.id} shapeId={onlySelectedShape.id} handle={handle} />
|
return <HandleWrapper key={handle.id} shapeId={onlySelectedShape.id} handle={handle} />
|
||||||
|
@ -317,7 +317,7 @@ const SelectedIdIndicators = track(function SelectedIdIndicators() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{app.selectedIds.map((id) => (
|
{app.selectedIds.map((id) => (
|
||||||
<ShapeIndicator key={id + '_indicator'} id={id} />
|
<ShapeIndicator key={id + '_indicator'} className="tl-user-indicator__selected" id={id} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -334,7 +334,7 @@ const HoveredShapeIndicator = function HoveredShapeIndicator() {
|
||||||
|
|
||||||
if (!displayingHoveredId) return null
|
if (!displayingHoveredId) return null
|
||||||
|
|
||||||
return <ShapeIndicator id={displayingHoveredId} />
|
return <ShapeIndicator className="tl-user-indicator__hovered" id={displayingHoveredId} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const HintedShapeIndicator = track(function HintedShapeIndicator() {
|
const HintedShapeIndicator = track(function HintedShapeIndicator() {
|
||||||
|
@ -347,7 +347,7 @@ const HintedShapeIndicator = track(function HintedShapeIndicator() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ids.map((id) => (
|
{ids.map((id) => (
|
||||||
<ShapeIndicator id={id} key={id + '_hinting'} isHinting />
|
<ShapeIndicator className="tl-user-indicator__hint" id={id} key={id + '_hinting'} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -373,7 +373,7 @@ function Cursor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CollaboratorHint() {
|
function CollaboratorHint() {
|
||||||
return <path id="cursor_hint" fill="currentColor" d="M -2,-5 2,0 -2,5 Z" opacity=".8" />
|
return <path id="cursor_hint" fill="currentColor" d="M -2,-5 2,0 -2,5 Z" />
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArrowheadDot() {
|
function ArrowheadDot() {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
|
||||||
const offset = cropStrokeWidth / 2
|
const offset = cropStrokeWidth / 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container">
|
<svg className="tl-overlays__item">
|
||||||
{/* Top left */}
|
{/* Top left */}
|
||||||
<polyline
|
<polyline
|
||||||
className="tl-corner-crop-handle"
|
className="tl-corner-crop-handle"
|
||||||
|
|
|
@ -4,16 +4,21 @@ import { useRef } from 'react'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../hooks/useTransform'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLBrushComponent = (props: { brush: Box2dModel; color?: string }) => any | null
|
export type TLBrushComponent = (props: {
|
||||||
|
brush: Box2dModel
|
||||||
|
color?: string
|
||||||
|
opacity?: number
|
||||||
|
className?: string
|
||||||
|
}) => any | null
|
||||||
|
|
||||||
export const DefaultBrush: TLBrushComponent = ({ brush, color }) => {
|
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
||||||
const rSvg = useRef<SVGSVGElement>(null)
|
const rSvg = useRef<SVGSVGElement>(null)
|
||||||
useTransform(rSvg, brush.x, brush.y)
|
useTransform(rSvg, brush.x, brush.y)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container" ref={rSvg}>
|
<svg className="tl-overlays__item" ref={rSvg}>
|
||||||
{color ? (
|
{color ? (
|
||||||
<g className="tl-brush">
|
<g className="tl-brush" opacity={opacity}>
|
||||||
<rect
|
<rect
|
||||||
width={toDomPrecision(Math.max(1, brush.w))}
|
width={toDomPrecision(Math.max(1, brush.w))}
|
||||||
height={toDomPrecision(Math.max(1, brush.h))}
|
height={toDomPrecision(Math.max(1, brush.h))}
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import { Box2d, clamp, radiansToDegrees, Vec2d } from '@tldraw/primitives'
|
import { Box2d, clamp, Vec2d } from '@tldraw/primitives'
|
||||||
import { Vec2dModel } from '@tldraw/tlschema'
|
import { Vec2dModel } from '@tldraw/tlschema'
|
||||||
|
import classNames from 'classnames'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../hooks/useTransform'
|
||||||
|
|
||||||
export type TLCollaboratorHintComponent = (props: {
|
export type TLCollaboratorHintComponent = (props: {
|
||||||
|
className?: string
|
||||||
point: Vec2dModel
|
point: Vec2dModel
|
||||||
viewport: Box2d
|
viewport: Box2d
|
||||||
zoom: number
|
zoom: number
|
||||||
|
opacity?: number
|
||||||
color: string
|
color: string
|
||||||
}) => JSX.Element | null
|
}) => JSX.Element | null
|
||||||
|
|
||||||
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
||||||
|
className,
|
||||||
zoom,
|
zoom,
|
||||||
point,
|
point,
|
||||||
color,
|
color,
|
||||||
viewport,
|
viewport,
|
||||||
|
opacity = 1,
|
||||||
}) => {
|
}) => {
|
||||||
const rSvg = useRef<SVGSVGElement>(null)
|
const rSvg = useRef<SVGSVGElement>(null)
|
||||||
|
|
||||||
|
@ -23,12 +28,13 @@ export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
||||||
clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom),
|
clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom),
|
||||||
clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom),
|
clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom),
|
||||||
1 / zoom,
|
1 / zoom,
|
||||||
radiansToDegrees(Vec2d.Angle(viewport.center, point))
|
Vec2d.Angle(viewport.center, point)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg ref={rSvg} className="tl-svg-origin-container">
|
<svg ref={rSvg} className={classNames('tl-overlays__item', className)}>
|
||||||
<use href="#cursor_hint" color={color} />
|
<use href="#cursor_hint" color={color} strokeWidth={3} stroke="var(--color-background)" />
|
||||||
|
<use href="#cursor_hint" color={color} opacity={opacity} />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import { Vec2dModel } from '@tldraw/tlschema'
|
import { Vec2dModel } from '@tldraw/tlschema'
|
||||||
import { memo } from 'react'
|
import classNames from 'classnames'
|
||||||
|
import { memo, useRef } from 'react'
|
||||||
|
import { useTransform } from '../hooks/useTransform'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLCursorComponent = (props: {
|
export type TLCursorComponent = (props: {
|
||||||
|
className?: string
|
||||||
point: Vec2dModel | null
|
point: Vec2dModel | null
|
||||||
zoom: number
|
zoom: number
|
||||||
color?: string
|
color?: string
|
||||||
name: string | null
|
name: string | null
|
||||||
}) => any | null
|
}) => any | null
|
||||||
|
|
||||||
const _Cursor: TLCursorComponent = ({ zoom, point, color, name }) => {
|
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name }) => {
|
||||||
|
const rDiv = useRef<HTMLDivElement>(null)
|
||||||
|
useTransform(rDiv, point?.x, point?.y, 1 / zoom)
|
||||||
|
|
||||||
if (!point) return null
|
if (!point) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={rDiv} className={classNames('tl-overlays__item', className)}>
|
||||||
className="tl-cursor"
|
<svg className="tl-cursor">
|
||||||
style={{ transform: `translate(${point.x}px, ${point.y}px) scale(${1 / zoom})` }}
|
|
||||||
>
|
|
||||||
<svg>
|
|
||||||
<use href="#cursor" color={color} />
|
<use href="#cursor" color={color} />
|
||||||
</svg>
|
</svg>
|
||||||
{name !== null && name !== '' && (
|
{name !== null && name !== '' && (
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
export type TLHandleComponent = (props: { shapeId: TLShapeId; handle: TLHandle }) => any | null
|
export type TLHandleComponent = (props: {
|
||||||
|
shapeId: TLShapeId
|
||||||
|
handle: TLHandle
|
||||||
|
className?: string
|
||||||
|
}) => any | null
|
||||||
|
|
||||||
export const DefaultHandle: TLHandleComponent = ({ handle }) => {
|
export const DefaultHandle: TLHandleComponent = ({ handle, className }) => {
|
||||||
return (
|
return (
|
||||||
<g className={classNames('tl-handle', { 'tl-handle__hint': handle.type !== 'vertex' })}>
|
<g
|
||||||
|
className={classNames(
|
||||||
|
'tl-handle',
|
||||||
|
{ 'tl-handle__hint': handle.type !== 'vertex' },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<circle className="tl-handle__bg" />
|
<circle className="tl-handle__bg" />
|
||||||
<circle className="tl-handle__fg" />
|
<circle className="tl-handle__fg" />
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { EASINGS, getStroke } from '@tldraw/primitives'
|
import { EASINGS, getStroke } from '@tldraw/primitives'
|
||||||
import { TLScribble } from '@tldraw/tlschema'
|
import { TLScribble } from '@tldraw/tlschema'
|
||||||
|
import classNames from 'classnames'
|
||||||
import { getSvgPathFromStroke } from '../utils/svg'
|
import { getSvgPathFromStroke } from '../utils/svg'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -8,9 +9,16 @@ export type TLScribbleComponent = (props: {
|
||||||
zoom: number
|
zoom: number
|
||||||
color?: string
|
color?: string
|
||||||
opacity?: number
|
opacity?: number
|
||||||
|
className?: string
|
||||||
}) => any
|
}) => any
|
||||||
|
|
||||||
export const DefaultScribble: TLScribbleComponent = ({ scribble, zoom, color, opacity }) => {
|
export const DefaultScribble: TLScribbleComponent = ({
|
||||||
|
scribble,
|
||||||
|
zoom,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
const d = getSvgPathFromStroke(
|
const d = getSvgPathFromStroke(
|
||||||
getStroke(scribble.points, {
|
getStroke(scribble.points, {
|
||||||
size: scribble.size / zoom,
|
size: scribble.size / zoom,
|
||||||
|
@ -21,7 +29,7 @@ export const DefaultScribble: TLScribbleComponent = ({ scribble, zoom, color, op
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container">
|
<svg className={className ? classNames('tl-overlays__item', className) : className}>
|
||||||
<path
|
<path
|
||||||
className="tl-scribble"
|
className="tl-scribble"
|
||||||
d={d}
|
d={d}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { rangeIntersection } from '@tldraw/primitives'
|
import { rangeIntersection } from '@tldraw/primitives'
|
||||||
|
import classNames from 'classnames'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { type GapsSnapLine, type PointsSnapLine, type SnapLine } from '../app/managers/SnapManager'
|
import { type GapsSnapLine, type PointsSnapLine, type SnapLine } from '../app/managers/SnapManager'
|
||||||
|
|
||||||
|
@ -148,11 +149,15 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLSnapLineComponent = (props: { line: SnapLine; zoom: number }) => any
|
export type TLSnapLineComponent = (props: {
|
||||||
|
className?: string
|
||||||
|
line: SnapLine
|
||||||
|
zoom: number
|
||||||
|
}) => any
|
||||||
|
|
||||||
export const DefaultSnapLine: TLSnapLineComponent = ({ line, zoom }) => {
|
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container">
|
<svg className={classNames('tl-overlays__item', className)}>
|
||||||
{line.type === 'points' ? (
|
{line.type === 'points' ? (
|
||||||
<PointsSnapLine {...line} zoom={zoom} />
|
<PointsSnapLine {...line} zoom={zoom} />
|
||||||
) : line.type === 'gaps' ? (
|
) : line.type === 'gaps' ? (
|
||||||
|
|
|
@ -111,10 +111,17 @@ const Collaborator = track(function Collaborator({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{brush && CollaboratorBrush ? (
|
{brush && CollaboratorBrush ? (
|
||||||
<CollaboratorBrush key={userId + '_brush'} brush={brush} color={color} />
|
<CollaboratorBrush
|
||||||
|
className="tl-collaborator__brush"
|
||||||
|
key={userId + '_brush'}
|
||||||
|
brush={brush}
|
||||||
|
color={color}
|
||||||
|
opacity={0.1}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isCursorInViewport && CollaboratorCursor ? (
|
{isCursorInViewport && CollaboratorCursor ? (
|
||||||
<CollaboratorCursor
|
<CollaboratorCursor
|
||||||
|
className="tl-collaborator__cursor"
|
||||||
key={userId + '_cursor'}
|
key={userId + '_cursor'}
|
||||||
point={cursor}
|
point={cursor}
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -123,6 +130,7 @@ const Collaborator = track(function Collaborator({
|
||||||
/>
|
/>
|
||||||
) : CollaboratorHint ? (
|
) : CollaboratorHint ? (
|
||||||
<CollaboratorHint
|
<CollaboratorHint
|
||||||
|
className="tl-collaborator__cursor-hint"
|
||||||
key={userId + '_cursor_hint'}
|
key={userId + '_cursor_hint'}
|
||||||
point={cursor}
|
point={cursor}
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -132,6 +140,7 @@ const Collaborator = track(function Collaborator({
|
||||||
) : null}
|
) : null}
|
||||||
{scribble && CollaboratorScribble ? (
|
{scribble && CollaboratorScribble ? (
|
||||||
<CollaboratorScribble
|
<CollaboratorScribble
|
||||||
|
className="tl-collaborator__scribble"
|
||||||
key={userId + '_scribble'}
|
key={userId + '_scribble'}
|
||||||
scribble={scribble}
|
scribble={scribble}
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -141,7 +150,13 @@ const Collaborator = track(function Collaborator({
|
||||||
) : null}
|
) : null}
|
||||||
{CollaboratorShapeIndicator &&
|
{CollaboratorShapeIndicator &&
|
||||||
selectedIds.map((shapeId) => (
|
selectedIds.map((shapeId) => (
|
||||||
<CollaboratorShapeIndicator key={userId + '_' + shapeId} id={shapeId} color={color} />
|
<CollaboratorShapeIndicator
|
||||||
|
className="tl-collaborator__shape-indicator"
|
||||||
|
key={userId + '_' + shapeId}
|
||||||
|
id={shapeId}
|
||||||
|
color={color}
|
||||||
|
opacity={0.5}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -120,7 +120,7 @@ export const SelectionBg = track(function SelectionBg() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="tlui-selection__bg"
|
className="tl-selection__bg"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
transform,
|
transform,
|
||||||
|
|
|
@ -8,6 +8,11 @@ import { useSelectionEvents } from '../hooks/useSelectionEvents'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../hooks/useTransform'
|
||||||
import { CropHandles } from './CropHandles'
|
import { CropHandles } from './CropHandles'
|
||||||
|
|
||||||
|
const IS_FIREFOX =
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
navigator.userAgent &&
|
||||||
|
navigator.userAgent.toLowerCase().indexOf('firefox') > -1
|
||||||
|
|
||||||
export const SelectionFg = track(function SelectionFg() {
|
export const SelectionFg = track(function SelectionFg() {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const rSvg = useRef<SVGSVGElement>(null)
|
const rSvg = useRef<SVGSVGElement>(null)
|
||||||
|
@ -68,12 +73,11 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
(onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) &&
|
(onlyShape ? !app.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) &&
|
||||||
!isChangingStyles
|
!isChangingStyles
|
||||||
|
|
||||||
const shouldDisplayBox =
|
let shouldDisplayBox =
|
||||||
(showSelectionBounds &&
|
(showSelectionBounds &&
|
||||||
app.isInAny(
|
app.isInAny(
|
||||||
'select.idle',
|
'select.idle',
|
||||||
'select.brushing',
|
'select.brushing',
|
||||||
'select.editing_shape',
|
|
||||||
'select.scribble_brushing',
|
'select.scribble_brushing',
|
||||||
'select.pointing_canvas',
|
'select.pointing_canvas',
|
||||||
'select.pointing_selection',
|
'select.pointing_selection',
|
||||||
|
@ -81,10 +85,17 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
'select.crop.idle',
|
'select.crop.idle',
|
||||||
'select.crop.pointing_crop',
|
'select.crop.pointing_crop',
|
||||||
'select.pointing_resize_handle',
|
'select.pointing_resize_handle',
|
||||||
'select.pointing_crop_handle'
|
'select.pointing_crop_handle',
|
||||||
|
'select.editing_shape'
|
||||||
)) ||
|
)) ||
|
||||||
(showSelectionBounds && app.isIn('select.resizing') && onlyShape && shapes[0].type === 'text')
|
(showSelectionBounds && app.isIn('select.resizing') && onlyShape && shapes[0].type === 'text')
|
||||||
|
|
||||||
|
if (IS_FIREFOX && shouldDisplayBox) {
|
||||||
|
if (app.onlySelectedShape?.type === 'embed') {
|
||||||
|
shouldDisplayBox = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const showCropHandles =
|
const showCropHandles =
|
||||||
app.isInAny('select.pointing_crop_handle', 'select.crop.idle', 'select.crop.pointing_crop') &&
|
app.isInAny('select.pointing_crop_handle', 'select.crop.idle', 'select.crop.pointing_crop') &&
|
||||||
!isChangingStyles &&
|
!isChangingStyles &&
|
||||||
|
@ -162,255 +173,255 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
textHandleHeight * zoom >= 4
|
textHandleHeight * zoom >= 4
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container" ref={rSvg}>
|
<svg ref={rSvg} className="tl-overlays__item tl-selection__fg" data-wd="selection-foreground">
|
||||||
<g data-wd="selection-foreground" className="tlui-selection__fg">
|
{shouldDisplayBox && (
|
||||||
<rect
|
<rect
|
||||||
className={classNames('tlui-selection__fg__outline', { 'tl-hidden': !shouldDisplayBox })}
|
className={classNames('tl-selection__fg__outline')}
|
||||||
width={toDomPrecision(width)}
|
width={toDomPrecision(width)}
|
||||||
height={toDomPrecision(height)}
|
height={toDomPrecision(height)}
|
||||||
/>
|
/>
|
||||||
<RotateCornerHandle
|
)}
|
||||||
data-wd="selection.rotate.top-left"
|
<RotateCornerHandle
|
||||||
cx={0}
|
data-wd="selection.rotate.top-left"
|
||||||
cy={0}
|
cx={0}
|
||||||
targetSize={targetSize}
|
cy={0}
|
||||||
corner="top_left_rotate"
|
targetSize={targetSize}
|
||||||
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
|
corner="top_left_rotate"
|
||||||
isHidden={hideRotateCornerHandles}
|
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
|
||||||
/>
|
isHidden={hideRotateCornerHandles}
|
||||||
<RotateCornerHandle
|
/>
|
||||||
data-wd="selection.rotate.top-right"
|
<RotateCornerHandle
|
||||||
cx={width + targetSize * 3}
|
data-wd="selection.rotate.top-right"
|
||||||
cy={0}
|
cx={width + targetSize * 3}
|
||||||
targetSize={targetSize}
|
cy={0}
|
||||||
corner="top_right_rotate"
|
targetSize={targetSize}
|
||||||
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
|
corner="top_right_rotate"
|
||||||
isHidden={hideRotateCornerHandles}
|
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
|
||||||
/>
|
isHidden={hideRotateCornerHandles}
|
||||||
<RotateCornerHandle
|
/>
|
||||||
data-wd="selection.rotate.bottom-left"
|
<RotateCornerHandle
|
||||||
cx={0}
|
data-wd="selection.rotate.bottom-left"
|
||||||
cy={height + targetSize * 3}
|
cx={0}
|
||||||
targetSize={targetSize}
|
cy={height + targetSize * 3}
|
||||||
corner="bottom_left_rotate"
|
targetSize={targetSize}
|
||||||
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
|
corner="bottom_left_rotate"
|
||||||
isHidden={hideRotateCornerHandles}
|
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
|
||||||
/>
|
isHidden={hideRotateCornerHandles}
|
||||||
<RotateCornerHandle
|
/>
|
||||||
data-wd="selection.rotate.bottom-right"
|
<RotateCornerHandle
|
||||||
cx={width + targetSize * 3}
|
data-wd="selection.rotate.bottom-right"
|
||||||
cy={height + targetSize * 3}
|
cx={width + targetSize * 3}
|
||||||
targetSize={targetSize}
|
cy={height + targetSize * 3}
|
||||||
corner="bottom_right_rotate"
|
targetSize={targetSize}
|
||||||
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
|
corner="bottom_right_rotate"
|
||||||
isHidden={hideRotateCornerHandles}
|
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
|
||||||
/>{' '}
|
isHidden={hideRotateCornerHandles}
|
||||||
<MobileRotateHandle
|
/>{' '}
|
||||||
data-wd="selection.rotate.mobile"
|
<MobileRotateHandle
|
||||||
cx={isSmallX ? -targetSize * 1.5 : width / 2}
|
data-wd="selection.rotate.mobile"
|
||||||
cy={isSmallX ? height / 2 : -targetSize * 1.5}
|
cx={isSmallX ? -targetSize * 1.5 : width / 2}
|
||||||
size={size}
|
cy={isSmallX ? height / 2 : -targetSize * 1.5}
|
||||||
isHidden={hideMobileRotateHandle}
|
size={size}
|
||||||
/>
|
isHidden={hideMobileRotateHandle}
|
||||||
{/* Targets */}
|
/>
|
||||||
<rect
|
{/* Targets */}
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideEdgeTargets,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideEdgeTargets,
|
||||||
data-wd="selection.resize.top"
|
})}
|
||||||
aria-label="top target"
|
data-wd="selection.resize.top"
|
||||||
pointerEvents="all"
|
aria-label="top target"
|
||||||
x={0}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
|
x={0}
|
||||||
width={toDomPrecision(Math.max(1, width))}
|
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
|
||||||
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
|
width={toDomPrecision(Math.max(1, width))}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
|
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
|
||||||
{...topEvents}
|
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
|
||||||
/>
|
{...topEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideEdgeTargets,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideEdgeTargets,
|
||||||
data-wd="selection.resize.right"
|
})}
|
||||||
aria-label="right target"
|
data-wd="selection.resize.right"
|
||||||
pointerEvents="all"
|
aria-label="right target"
|
||||||
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
|
pointerEvents="all"
|
||||||
y={0}
|
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
|
||||||
height={toDomPrecision(Math.max(1, height))}
|
y={0}
|
||||||
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
|
height={toDomPrecision(Math.max(1, height))}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
|
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
|
||||||
{...rightEvents}
|
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
|
||||||
/>
|
{...rightEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideEdgeTargets,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideEdgeTargets,
|
||||||
data-wd="selection.resize.bottom"
|
})}
|
||||||
aria-label="bottom target"
|
data-wd="selection.resize.bottom"
|
||||||
pointerEvents="all"
|
aria-label="bottom target"
|
||||||
x={0}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
|
x={0}
|
||||||
width={toDomPrecision(Math.max(1, width))}
|
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
|
||||||
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
|
width={toDomPrecision(Math.max(1, width))}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
|
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
|
||||||
{...bottomEvents}
|
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
|
||||||
/>
|
{...bottomEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideEdgeTargets,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideEdgeTargets,
|
||||||
data-wd="selection.resize.left"
|
})}
|
||||||
aria-label="left target"
|
data-wd="selection.resize.left"
|
||||||
pointerEvents="all"
|
aria-label="left target"
|
||||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
|
pointerEvents="all"
|
||||||
y={0}
|
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
|
||||||
height={toDomPrecision(Math.max(1, height))}
|
y={0}
|
||||||
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
|
height={toDomPrecision(Math.max(1, height))}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
|
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
|
||||||
{...leftEvents}
|
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
|
||||||
/>
|
{...leftEvents}
|
||||||
{/* Corner Targets */}
|
/>
|
||||||
<rect
|
{/* Corner Targets */}
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideTopLeftCorner,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideTopLeftCorner,
|
||||||
data-wd="selection.target.top-left"
|
})}
|
||||||
aria-label="top-left target"
|
data-wd="selection.target.top-left"
|
||||||
pointerEvents="all"
|
aria-label="top-left target"
|
||||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
|
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
|
||||||
width={toDomPrecision(targetSizeX * 3)}
|
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
|
||||||
height={toDomPrecision(targetSizeY * 3)}
|
width={toDomPrecision(targetSizeX * 3)}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
|
height={toDomPrecision(targetSizeY * 3)}
|
||||||
{...topLeftEvents}
|
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
|
||||||
/>
|
{...topLeftEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideTopRightCorner,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideTopRightCorner,
|
||||||
data-wd="selection.target.top-right"
|
})}
|
||||||
aria-label="top-right target"
|
data-wd="selection.target.top-right"
|
||||||
pointerEvents="all"
|
aria-label="top-right target"
|
||||||
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
|
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
|
||||||
width={toDomPrecision(targetSizeX * 3)}
|
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
|
||||||
height={toDomPrecision(targetSizeY * 3)}
|
width={toDomPrecision(targetSizeX * 3)}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
|
height={toDomPrecision(targetSizeY * 3)}
|
||||||
{...topRightEvents}
|
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
|
||||||
/>
|
{...topRightEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideBottomRightCorner,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideBottomRightCorner,
|
||||||
data-wd="selection.target.bottom-right"
|
})}
|
||||||
aria-label="bottom-right target"
|
data-wd="selection.target.bottom-right"
|
||||||
pointerEvents="all"
|
aria-label="bottom-right target"
|
||||||
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
|
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
|
||||||
width={toDomPrecision(targetSizeX * 3)}
|
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
|
||||||
height={toDomPrecision(targetSizeY * 3)}
|
width={toDomPrecision(targetSizeX * 3)}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
|
height={toDomPrecision(targetSizeY * 3)}
|
||||||
{...bottomRightEvents}
|
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
|
||||||
/>
|
{...bottomRightEvents}
|
||||||
<rect
|
/>
|
||||||
className={classNames('tl-transparent', {
|
<rect
|
||||||
'tl-hidden': hideBottomLeftCorner,
|
className={classNames('tl-transparent', {
|
||||||
})}
|
'tl-hidden': hideBottomLeftCorner,
|
||||||
data-wd="selection.target.bottom-left"
|
})}
|
||||||
aria-label="bottom-left target"
|
data-wd="selection.target.bottom-left"
|
||||||
pointerEvents="all"
|
aria-label="bottom-left target"
|
||||||
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
|
pointerEvents="all"
|
||||||
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
|
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
|
||||||
width={toDomPrecision(targetSizeX * 3)}
|
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
|
||||||
height={toDomPrecision(targetSizeY * 3)}
|
width={toDomPrecision(targetSizeX * 3)}
|
||||||
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
|
height={toDomPrecision(targetSizeY * 3)}
|
||||||
{...bottomLeftEvents}
|
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
|
||||||
/>
|
{...bottomLeftEvents}
|
||||||
{/* Resize Handles */}
|
/>
|
||||||
{showResizeHandles && (
|
{/* Resize Handles */}
|
||||||
<>
|
{showResizeHandles && (
|
||||||
<rect
|
<>
|
||||||
data-wd="selection.resize.top-left"
|
<rect
|
||||||
className={classNames('tl-corner-handle', {
|
data-wd="selection.resize.top-left"
|
||||||
'tl-hidden': hideTopLeftCorner,
|
className={classNames('tl-corner-handle', {
|
||||||
})}
|
'tl-hidden': hideTopLeftCorner,
|
||||||
aria-label="top_left handle"
|
})}
|
||||||
x={toDomPrecision(0 - size / 2)}
|
aria-label="top_left handle"
|
||||||
y={toDomPrecision(0 - size / 2)}
|
x={toDomPrecision(0 - size / 2)}
|
||||||
width={toDomPrecision(size)}
|
y={toDomPrecision(0 - size / 2)}
|
||||||
height={toDomPrecision(size)}
|
width={toDomPrecision(size)}
|
||||||
/>
|
height={toDomPrecision(size)}
|
||||||
<rect
|
|
||||||
data-wd="selection.resize.top-right"
|
|
||||||
className={classNames('tl-corner-handle', {
|
|
||||||
'tl-hidden': hideTopRightCorner,
|
|
||||||
})}
|
|
||||||
aria-label="top_right handle"
|
|
||||||
x={toDomPrecision(width - size / 2)}
|
|
||||||
y={toDomPrecision(0 - size / 2)}
|
|
||||||
width={toDomPrecision(size)}
|
|
||||||
height={toDomPrecision(size)}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
data-wd="selection.resize.bottom-right"
|
|
||||||
className={classNames('tl-corner-handle', {
|
|
||||||
'tl-hidden': hideBottomRightCorner,
|
|
||||||
})}
|
|
||||||
aria-label="bottom_right handle"
|
|
||||||
x={toDomPrecision(width - size / 2)}
|
|
||||||
y={toDomPrecision(height - size / 2)}
|
|
||||||
width={toDomPrecision(size)}
|
|
||||||
height={toDomPrecision(size)}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
data-wd="selection.resize.bottom-left"
|
|
||||||
className={classNames('tl-corner-handle', {
|
|
||||||
'tl-hidden': hideBottomLeftCorner,
|
|
||||||
})}
|
|
||||||
aria-label="bottom_left handle"
|
|
||||||
x={toDomPrecision(0 - size / 2)}
|
|
||||||
y={toDomPrecision(height - size / 2)}
|
|
||||||
width={toDomPrecision(size)}
|
|
||||||
height={toDomPrecision(size)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{showTextResizeHandles && (
|
|
||||||
<>
|
|
||||||
<rect
|
|
||||||
data-wd="selection.text-resize.left.handle"
|
|
||||||
className="tl-text-handle"
|
|
||||||
aria-label="bottom_left handle"
|
|
||||||
x={toDomPrecision(0 - size / 4)}
|
|
||||||
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
|
|
||||||
rx={size / 4}
|
|
||||||
width={toDomPrecision(size / 2)}
|
|
||||||
height={toDomPrecision(textHandleHeight)}
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
data-wd="selection.text-resize.right.handle"
|
|
||||||
className="tl-text-handle"
|
|
||||||
aria-label="bottom_left handle"
|
|
||||||
rx={size / 4}
|
|
||||||
x={toDomPrecision(width - size / 4)}
|
|
||||||
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
|
|
||||||
width={toDomPrecision(size / 2)}
|
|
||||||
height={toDomPrecision(textHandleHeight)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* Crop Handles */}
|
|
||||||
{showCropHandles && (
|
|
||||||
<CropHandles
|
|
||||||
{...{
|
|
||||||
size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
hideAlternateHandles: hideAlternateCropHandles,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<rect
|
||||||
</g>
|
data-wd="selection.resize.top-right"
|
||||||
|
className={classNames('tl-corner-handle', {
|
||||||
|
'tl-hidden': hideTopRightCorner,
|
||||||
|
})}
|
||||||
|
aria-label="top_right handle"
|
||||||
|
x={toDomPrecision(width - size / 2)}
|
||||||
|
y={toDomPrecision(0 - size / 2)}
|
||||||
|
width={toDomPrecision(size)}
|
||||||
|
height={toDomPrecision(size)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
data-wd="selection.resize.bottom-right"
|
||||||
|
className={classNames('tl-corner-handle', {
|
||||||
|
'tl-hidden': hideBottomRightCorner,
|
||||||
|
})}
|
||||||
|
aria-label="bottom_right handle"
|
||||||
|
x={toDomPrecision(width - size / 2)}
|
||||||
|
y={toDomPrecision(height - size / 2)}
|
||||||
|
width={toDomPrecision(size)}
|
||||||
|
height={toDomPrecision(size)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
data-wd="selection.resize.bottom-left"
|
||||||
|
className={classNames('tl-corner-handle', {
|
||||||
|
'tl-hidden': hideBottomLeftCorner,
|
||||||
|
})}
|
||||||
|
aria-label="bottom_left handle"
|
||||||
|
x={toDomPrecision(0 - size / 2)}
|
||||||
|
y={toDomPrecision(height - size / 2)}
|
||||||
|
width={toDomPrecision(size)}
|
||||||
|
height={toDomPrecision(size)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showTextResizeHandles && (
|
||||||
|
<>
|
||||||
|
<rect
|
||||||
|
data-wd="selection.text-resize.left.handle"
|
||||||
|
className="tl-text-handle"
|
||||||
|
aria-label="bottom_left handle"
|
||||||
|
x={toDomPrecision(0 - size / 4)}
|
||||||
|
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
|
||||||
|
rx={size / 4}
|
||||||
|
width={toDomPrecision(size / 2)}
|
||||||
|
height={toDomPrecision(textHandleHeight)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
data-wd="selection.text-resize.right.handle"
|
||||||
|
className="tl-text-handle"
|
||||||
|
aria-label="bottom_left handle"
|
||||||
|
rx={size / 4}
|
||||||
|
x={toDomPrecision(width - size / 4)}
|
||||||
|
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
|
||||||
|
width={toDomPrecision(size / 2)}
|
||||||
|
height={toDomPrecision(textHandleHeight)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Crop Handles */}
|
||||||
|
{showCropHandles && (
|
||||||
|
<CropHandles
|
||||||
|
{...{
|
||||||
|
size,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
hideAlternateHandles: hideAlternateCropHandles,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Matrix2d } from '@tldraw/primitives'
|
|
||||||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
@ -51,15 +50,14 @@ export const InnerIndicator = ({ app, id }: { app: App; id: TLShapeId }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShapeIndicator = React.memo(function ShapeIndicator({
|
export type TLShapeIndicatorComponent = (props: {
|
||||||
id,
|
|
||||||
isHinting,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
id: TLShapeId
|
id: TLShapeId
|
||||||
isHinting?: boolean
|
color?: string | undefined
|
||||||
color?: string
|
opacity?: number
|
||||||
}) {
|
className?: string
|
||||||
|
}) => JSX.Element | null
|
||||||
|
|
||||||
|
const _ShapeIndicator: TLShapeIndicatorComponent = ({ id, className, color, opacity }) => {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
|
||||||
const transform = useValue(
|
const transform = useValue(
|
||||||
|
@ -67,28 +65,23 @@ export const ShapeIndicator = React.memo(function ShapeIndicator({
|
||||||
() => {
|
() => {
|
||||||
const pageTransform = app.getPageTransformById(id)
|
const pageTransform = app.getPageTransformById(id)
|
||||||
if (!pageTransform) return ''
|
if (!pageTransform) return ''
|
||||||
return Matrix2d.toCssString(pageTransform)
|
return pageTransform.toCssString()
|
||||||
},
|
},
|
||||||
[app, id]
|
[app, id]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="tl-svg-origin-container">
|
<svg className={classNames('tl-overlays__item', className)}>
|
||||||
<g
|
<g
|
||||||
className={classNames('tl-shape-indicator', {
|
className="tl-shape-indicator"
|
||||||
'tl-shape-indicator__hinting': isHinting,
|
|
||||||
})}
|
|
||||||
transform={transform}
|
transform={transform}
|
||||||
stroke={color ?? 'var(--color-selected)'}
|
stroke={color ?? 'var(--color-selected)'}
|
||||||
|
opacity={opacity}
|
||||||
>
|
>
|
||||||
<InnerIndicator app={app} id={id} />
|
<InnerIndicator app={app} id={id} />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
export type TLShapeIndicatorComponent = (props: {
|
export const ShapeIndicator = React.memo(_ShapeIndicator)
|
||||||
id: TLShapeId
|
|
||||||
isHinting?: boolean | undefined
|
|
||||||
color?: string | undefined
|
|
||||||
}) => JSX.Element | null
|
|
||||||
|
|
Loading…
Reference in a new issue