[fix] geo shape text label placement (#1927)
This PR fixes the text label placement for geo shapes. (It also fixes the way an ellipse renders when set to dash or dotted). There's still the slightest offset of the text label's outline when you begin editing. Maybe we should keep the indicator instead? ### Change Type - [x] `patch` — Bug fix ### Test Plan Create a hexagon shape hit enter to type indicator is offset, text label is no longer offset --------- Co-authored-by: David Sheldrick <d.j.sheldrick@gmail.com>
This commit is contained in:
parent
398bd352ae
commit
9e4dbd1901
11 changed files with 141 additions and 68 deletions
|
@ -328,6 +328,9 @@ export function clamp(n: number, min: number, max: number): number;
|
|||
// @public
|
||||
export function clampRadians(r: number): number;
|
||||
|
||||
// @public
|
||||
export function clockwiseAngleDist(a0: number, a1: number): number;
|
||||
|
||||
export { computed }
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -1029,22 +1029,17 @@ input,
|
|||
/* ---------------- Geo shape ---------------- */
|
||||
|
||||
.tl-text-label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
text-shadow: var(--tl-text-outline);
|
||||
line-height: inherit;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tl-text-label[data-isediting='true'] {
|
||||
outline: calc(var(--tl-scale) * 1.5px) solid var(--color-selected);
|
||||
}
|
||||
|
||||
.tl-text-label[data-isediting='true'] .tl-text-content {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -1168,7 +1163,6 @@ input,
|
|||
|
||||
.tl-arrow-label[data-isediting='true'] > .tl-arrow-label__inner {
|
||||
background-color: var(--color-background);
|
||||
border: calc(var(--tl-scale) * 1.5px) solid var(--color-selected);
|
||||
}
|
||||
|
||||
.tl-arrow-label__inner {
|
||||
|
@ -1355,7 +1349,6 @@ input,
|
|||
|
||||
.tl-frame-label__editing {
|
||||
color: transparent;
|
||||
outline: 1.5px solid var(--color-selection-stroke);
|
||||
white-space: pre;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
|
|
|
@ -297,6 +297,7 @@ export {
|
|||
canonicalizeRotation,
|
||||
clamp,
|
||||
clampRadians,
|
||||
clockwiseAngleDist,
|
||||
degreesToRadians,
|
||||
getArcLength,
|
||||
getPointOnCircle,
|
||||
|
|
|
@ -322,6 +322,7 @@ function SelectedIdIndicators() {
|
|||
'select.idle',
|
||||
'select.brushing',
|
||||
'select.scribble_brushing',
|
||||
'select.editing_shape',
|
||||
'select.pointing_shape',
|
||||
'select.pointing_selection',
|
||||
'select.pointing_handle'
|
||||
|
|
|
@ -6,6 +6,7 @@ import { nearestMultiple } from '../hooks/useDPRMultiple'
|
|||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
import { Matrix2d } from '../primitives/Matrix2d'
|
||||
import { toDomPrecision } from '../primitives/utils'
|
||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||
|
||||
/*
|
||||
|
@ -170,9 +171,11 @@ const CulledShape = React.memo(
|
|||
<div
|
||||
className="tl-shape__culled"
|
||||
style={{
|
||||
transform: `translate(${bounds.minX}px, ${bounds.minY}px)`,
|
||||
width: Math.max(1, bounds.width),
|
||||
height: Math.max(1, bounds.height),
|
||||
transform: `translate(${toDomPrecision(bounds.minX)}px, ${toDomPrecision(
|
||||
bounds.minY
|
||||
)}px)`,
|
||||
width: Math.max(1, toDomPrecision(bounds.width)),
|
||||
height: Math.max(1, toDomPrecision(bounds.height)),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -109,6 +109,22 @@ export function canonicalizeRotation(a: number) {
|
|||
return a
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the clockwise angle distance between two angles.
|
||||
*
|
||||
* @param a0 - The first angle.
|
||||
* @param a1 - The second angle.
|
||||
* @public
|
||||
*/
|
||||
export function clockwiseAngleDist(a0: number, a1: number): number {
|
||||
a0 = canonicalizeRotation(a0)
|
||||
a1 = canonicalizeRotation(a1)
|
||||
if (a0 > a1) {
|
||||
a1 += PI2
|
||||
}
|
||||
return a1 - a0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short angle distance between two angles.
|
||||
*
|
||||
|
@ -270,12 +286,27 @@ export function getPointOnCircle(cx: number, cy: number, r: number, a: number) {
|
|||
export function getPolygonVertices(width: number, height: number, sides: number) {
|
||||
const cx = width / 2
|
||||
const cy = height / 2
|
||||
const pointsOnPerimeter = []
|
||||
const pointsOnPerimeter: Vec2d[] = []
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const step = PI2 / sides
|
||||
const t = -TAU + i * step
|
||||
pointsOnPerimeter.push(new Vec2d(cx + cx * Math.cos(t), cy + cy * Math.sin(t)))
|
||||
const x = cx + cx * Math.cos(t)
|
||||
const y = cy + cy * Math.sin(t)
|
||||
if (x < minX) minX = x
|
||||
if (y < minY) minY = y
|
||||
pointsOnPerimeter.push(new Vec2d(x, y))
|
||||
}
|
||||
|
||||
if (minX !== 0 || minY !== 0) {
|
||||
for (let i = 0; i < pointsOnPerimeter.length; i++) {
|
||||
const pt = pointsOnPerimeter[i]
|
||||
pt.x -= minX
|
||||
pt.y -= minY
|
||||
}
|
||||
}
|
||||
|
||||
return pointsOnPerimeter
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
getSolidStraightArrowPath,
|
||||
getStraightArrowHandlePath,
|
||||
toDomPrecision,
|
||||
useIsEditing,
|
||||
} from '@tldraw/editor'
|
||||
import React from 'react'
|
||||
import { ShapeFill, getShapeFillSvg, useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
|
@ -692,6 +693,22 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
|
||||
const maskId = (shape.id + '_clip').replace(':', '_')
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const isEditing = useIsEditing(shape.id)
|
||||
|
||||
if (isEditing && labelGeometry) {
|
||||
return (
|
||||
<rect
|
||||
x={toDomPrecision(labelGeometry.x)}
|
||||
y={toDomPrecision(labelGeometry.y)}
|
||||
width={labelGeometry.w}
|
||||
height={labelGeometry.h}
|
||||
rx={3.5}
|
||||
ry={3.5}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<g>
|
||||
{includeMask && (
|
||||
|
|
|
@ -532,6 +532,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
text={text}
|
||||
labelColor={labelColor}
|
||||
wrap
|
||||
bounds={props.geo === 'cloud' ? this.getGeometry(shape).bounds : undefined}
|
||||
/>
|
||||
{shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.zoomLevel} />
|
||||
|
@ -813,10 +814,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
|
||||
override onResize: TLOnResizeHandler<TLGeoShape> = (
|
||||
shape,
|
||||
{ initialBounds, handle, newPoint, scaleX, scaleY }
|
||||
{ handle, newPoint, scaleX, scaleY, initialShape }
|
||||
) => {
|
||||
let w = initialBounds.width * scaleX
|
||||
let h = initialBounds.height * scaleY
|
||||
// use the w/h from props here instead of the initialBounds here,
|
||||
// since cloud shapes calculated bounds can differ from the props w/h.
|
||||
let w = initialShape.props.w * scaleX
|
||||
let h = (initialShape.props.h + initialShape.props.growY) * scaleY
|
||||
let overShrinkX = 0
|
||||
let overShrinkY = 0
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ import {
|
|||
TLDefaultSizeStyle,
|
||||
Vec2d,
|
||||
Vec2dModel,
|
||||
clockwiseAngleDist,
|
||||
getPointOnCircle,
|
||||
rng,
|
||||
shortAngleDist,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
function getPillCircumference(width: number, height: number) {
|
||||
|
@ -200,8 +201,8 @@ export function getCloudArcs(
|
|||
const radius = Vec2d.Dist(center, leftWigglePoint)
|
||||
|
||||
arcs.push({
|
||||
leftPoint: leftWigglePoint,
|
||||
rightPoint: rightWigglePoint,
|
||||
leftPoint: leftWigglePoint.clone(),
|
||||
rightPoint: rightWigglePoint.clone(),
|
||||
arcPoint,
|
||||
center,
|
||||
radius,
|
||||
|
@ -265,11 +266,13 @@ export function cloudSvgPath(
|
|||
size: TLDefaultSizeStyle
|
||||
) {
|
||||
const arcs = getCloudArcs(width, height, seed, size)
|
||||
let path = `M${arcs[0].leftPoint.x},${arcs[0].leftPoint.y}`
|
||||
let path = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
||||
|
||||
// now draw arcs for each circle, starting where it intersects the previous circle, and ending where it intersects the next circle
|
||||
for (const { rightPoint, radius } of arcs) {
|
||||
path += ` A${radius},${radius} 0 0,1 ${rightPoint.x},${rightPoint.y}`
|
||||
path += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 0,1 ${toDomPrecision(
|
||||
rightPoint.x
|
||||
)},${toDomPrecision(rightPoint.y)}`
|
||||
}
|
||||
|
||||
path += ' Z'
|
||||
|
@ -289,18 +292,22 @@ export function inkyCloudSvgPath(
|
|||
}
|
||||
const mutPoint = (p: Vec2d) => new Vec2d(mut(p.x), mut(p.y))
|
||||
const arcs = getCloudArcs(width, height, seed, size)
|
||||
let pathA = `M${arcs[0].leftPoint.x},${arcs[0].leftPoint.y}`
|
||||
let pathA = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
|
||||
let leftMutPoint = mutPoint(arcs[0].leftPoint)
|
||||
let pathB = `M${leftMutPoint.x},${leftMutPoint.y}`
|
||||
let pathB = `M${toDomPrecision(leftMutPoint.x)},${toDomPrecision(leftMutPoint.y)}`
|
||||
|
||||
for (const { rightPoint, radius, arcPoint } of arcs) {
|
||||
pathA += ` A${radius},${radius} 0 0,1 ${rightPoint.x},${rightPoint.y}`
|
||||
pathA += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 0,1 ${toDomPrecision(
|
||||
rightPoint.x
|
||||
)},${toDomPrecision(rightPoint.y)}`
|
||||
const rightMutPoint = mutPoint(rightPoint)
|
||||
const mutArcPoint = mutPoint(arcPoint)
|
||||
const mutCenter = getCenterOfCircleGivenThreePoints(leftMutPoint, rightMutPoint, mutArcPoint)
|
||||
const mutRadius = Math.abs(Vec2d.Dist(mutCenter, leftMutPoint))
|
||||
|
||||
pathB += ` A${mutRadius},${mutRadius} 0 0,1 ${rightMutPoint.x},${rightMutPoint.y}`
|
||||
pathB += ` A${toDomPrecision(mutRadius)},${toDomPrecision(mutRadius)} 0 0,1 ${toDomPrecision(
|
||||
rightMutPoint.x
|
||||
)},${toDomPrecision(rightMutPoint.y)}`
|
||||
leftMutPoint = rightMutPoint
|
||||
}
|
||||
|
||||
|
@ -319,7 +326,7 @@ export function pointsOnArc(
|
|||
const startAngle = Vec2d.Angle(center, startPoint)
|
||||
const endAngle = Vec2d.Angle(center, endPoint)
|
||||
|
||||
const l = shortAngleDist(startAngle, endAngle)
|
||||
const l = clockwiseAngleDist(startAngle, endAngle)
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const t = i / (numPoints - 1)
|
||||
|
|
|
@ -28,8 +28,8 @@ export const DashStyleEllipse = React.memo(function DashStyleEllipse({
|
|||
const theme = useDefaultColorTheme()
|
||||
const cx = w / 2
|
||||
const cy = h / 2
|
||||
const rx = Math.max(0, cx - sw / 2)
|
||||
const ry = Math.max(0, cy - sw / 2)
|
||||
const rx = Math.max(0, cx)
|
||||
const ry = Math.max(0, cy)
|
||||
|
||||
const perimeter = perimeterOfEllipse(rx, ry)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
Box2d,
|
||||
TLDefaultColorStyle,
|
||||
TLDefaultFillStyle,
|
||||
TLDefaultFontStyle,
|
||||
|
@ -27,6 +28,7 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
align,
|
||||
verticalAlign,
|
||||
wrap,
|
||||
bounds,
|
||||
}: {
|
||||
id: T['id']
|
||||
type: T['type']
|
||||
|
@ -38,6 +40,7 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
wrap?: boolean
|
||||
text: string
|
||||
labelColor: TLDefaultColorStyle
|
||||
bounds?: Box2d
|
||||
}) {
|
||||
const {
|
||||
rInput,
|
||||
|
@ -71,48 +74,59 @@ export const TextLabel = React.memo(function TextLabel<
|
|||
style={{
|
||||
justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
|
||||
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
||||
...(bounds
|
||||
? {
|
||||
top: bounds.minY,
|
||||
left: bounds.minX,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
position: 'absolute',
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="tl-text-label__inner"
|
||||
style={{
|
||||
fontSize: LABEL_FONT_SIZES[size],
|
||||
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
|
||||
minHeight: isEmpty ? LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 32 : 0,
|
||||
minWidth: isEmpty ? 33 : 0,
|
||||
color: theme[labelColor].solid,
|
||||
}}
|
||||
>
|
||||
<div className="tl-text tl-text-content" dir="ltr">
|
||||
{finalText}
|
||||
{isEmpty && !isInteractive ? null : (
|
||||
<div
|
||||
className="tl-text-label__inner"
|
||||
style={{
|
||||
fontSize: LABEL_FONT_SIZES[size],
|
||||
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
|
||||
minHeight: TEXT_PROPS.lineHeight + 32,
|
||||
minWidth: 0,
|
||||
color: theme[labelColor].solid,
|
||||
}}
|
||||
>
|
||||
<div className="tl-text tl-text-content" dir="ltr">
|
||||
{finalText}
|
||||
</div>
|
||||
{isInteractive && (
|
||||
<textarea
|
||||
ref={rInput}
|
||||
className="tl-text tl-text-input"
|
||||
name="text"
|
||||
tabIndex={-1}
|
||||
autoComplete="false"
|
||||
autoCapitalize="false"
|
||||
autoCorrect="false"
|
||||
autoSave="false"
|
||||
autoFocus={isEditing}
|
||||
placeholder=""
|
||||
spellCheck="true"
|
||||
wrap="off"
|
||||
dir="auto"
|
||||
datatype="wysiwyg"
|
||||
defaultValue={text}
|
||||
onFocus={handleFocus}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
onContextMenu={stopEventPropagation}
|
||||
onPointerDown={handleInputPointerDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isInteractive && (
|
||||
<textarea
|
||||
ref={rInput}
|
||||
className="tl-text tl-text-input"
|
||||
name="text"
|
||||
tabIndex={-1}
|
||||
autoComplete="false"
|
||||
autoCapitalize="false"
|
||||
autoCorrect="false"
|
||||
autoSave="false"
|
||||
autoFocus={isEditing}
|
||||
placeholder=""
|
||||
spellCheck="true"
|
||||
wrap="off"
|
||||
dir="auto"
|
||||
datatype="wysiwyg"
|
||||
defaultValue={text}
|
||||
onFocus={handleFocus}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
onContextMenu={stopEventPropagation}
|
||||
onPointerDown={handleInputPointerDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue