diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 6b305fb5c..ad1170e83 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -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) diff --git a/packages/editor/editor.css b/packages/editor/editor.css index 7c7bc7a3a..842c9b5a8 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -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; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index f8e4c33e0..452815232 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -297,6 +297,7 @@ export { canonicalizeRotation, clamp, clampRadians, + clockwiseAngleDist, degreesToRadians, getArcLength, getPointOnCircle, diff --git a/packages/editor/src/lib/components/Canvas.tsx b/packages/editor/src/lib/components/Canvas.tsx index a4902072c..17219988d 100644 --- a/packages/editor/src/lib/components/Canvas.tsx +++ b/packages/editor/src/lib/components/Canvas.tsx @@ -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' diff --git a/packages/editor/src/lib/components/Shape.tsx b/packages/editor/src/lib/components/Shape.tsx index ef7f8b8c9..e9f948739 100644 --- a/packages/editor/src/lib/components/Shape.tsx +++ b/packages/editor/src/lib/components/Shape.tsx @@ -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(
) diff --git a/packages/editor/src/lib/primitives/utils.ts b/packages/editor/src/lib/primitives/utils.ts index 0f7e6f2cc..00f8fd1be 100644 --- a/packages/editor/src/lib/primitives/utils.ts +++ b/packages/editor/src/lib/primitives/utils.ts @@ -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 } diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx index 502376b58..cd5ce2559 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx @@ -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 { const maskId = (shape.id + '_clip').replace(':', '_') + // eslint-disable-next-line react-hooks/rules-of-hooks + const isEditing = useIsEditing(shape.id) + + if (isEditing && labelGeometry) { + return ( + + ) + } + return ( {includeMask && ( diff --git a/packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx b/packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx index 24566347b..49ae4da34 100644 --- a/packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx @@ -532,6 +532,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { text={text} labelColor={labelColor} wrap + bounds={props.geo === 'cloud' ? this.getGeometry(shape).bounds : undefined} /> {shape.props.url && ( @@ -813,10 +814,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { override onResize: TLOnResizeHandler = ( 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 diff --git a/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts b/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts index 09cd4e15d..c2db93980 100644 --- a/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts +++ b/packages/tldraw/src/lib/shapes/geo/cloudOutline.ts @@ -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) diff --git a/packages/tldraw/src/lib/shapes/geo/components/DashStyleEllipse.tsx b/packages/tldraw/src/lib/shapes/geo/components/DashStyleEllipse.tsx index d5f545e2c..ff3666cb7 100644 --- a/packages/tldraw/src/lib/shapes/geo/components/DashStyleEllipse.tsx +++ b/packages/tldraw/src/lib/shapes/geo/components/DashStyleEllipse.tsx @@ -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) diff --git a/packages/tldraw/src/lib/shapes/shared/TextLabel.tsx b/packages/tldraw/src/lib/shapes/shared/TextLabel.tsx index 033f2ca2d..ca76ed2ae 100644 --- a/packages/tldraw/src/lib/shapes/shared/TextLabel.tsx +++ b/packages/tldraw/src/lib/shapes/shared/TextLabel.tsx @@ -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', + } + : {}), }} > -
-
- {finalText} + {isEmpty && !isInteractive ? null : ( +
+
+ {finalText} +
+ {isInteractive && ( +