From e7987ca451ba9b4305af418ede084e6de8fb5876 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 16:24:03 +0100 Subject: [PATCH 01/14] moves to div renderer --- .../core/src/components/bounds/bounds-bg.tsx | 25 +- .../core/src/components/bounds/bounds.tsx | 142 +++++---- .../core/src/components/brush/BrushUpdater.ts | 5 +- packages/core/src/components/brush/brush.tsx | 6 +- .../core/src/components/canvas/canvas.tsx | 23 +- .../src/components/container/container.tsx | 23 ++ .../core/src/components/container/index.ts | 1 + .../core/src/components/handles/handle.tsx | 39 ++- .../src/components/handles/handles.test.tsx | 2 +- .../core/src/components/handles/handles.tsx | 15 +- packages/core/src/components/page/page.tsx | 11 +- .../core/src/components/renderer/renderer.tsx | 22 +- .../shape-indicator/shape-indicator.test.tsx | 4 +- .../shape-indicator/shape-indicator.tsx | 21 +- .../components/shape/editing-text-shape.tsx | 28 +- .../src/components/shape/rendered-shape.tsx | 39 ++- .../core/src/components/shape/shape-node.tsx | 4 +- .../core/src/components/shape/shape.test.tsx | 5 +- packages/core/src/components/shape/shape.tsx | 91 +++--- .../src/components/svg-container/index.ts | 1 + .../svg-container/svg-container.tsx | 13 + packages/core/src/hooks/index.ts | 1 + packages/core/src/hooks/useCameraCss.tsx | 26 +- packages/core/src/hooks/usePosition.ts | 20 ++ .../core/src/hooks/usePreventNavigation.tsx | 2 +- packages/core/src/hooks/useSelection.tsx | 4 +- packages/core/src/hooks/useShapeTree.tsx | 23 +- packages/core/src/hooks/useStyle.tsx | 52 +++- packages/core/src/hooks/useTLContext.tsx | 8 +- packages/core/src/test/box.tsx | 20 +- packages/core/src/test/mockUtils.tsx | 2 +- packages/core/src/types.ts | 63 +++- packages/tldraw/src/shape/shape-utils.tsx | 25 +- .../tldraw/src/shape/shapes/arrow/arrow.tsx | 281 +++++++++--------- .../tldraw/src/shape/shapes/draw/draw.tsx | 199 ++++++------- .../src/shape/shapes/ellipse/ellipse.tsx | 138 ++++----- .../tldraw/src/shape/shapes/group/group.tsx | 106 ++++--- .../src/shape/shapes/rectangle/rectangle.tsx | 185 ++++++------ .../tldraw/src/shape/shapes/text/text.tsx | 265 ++++++++--------- packages/tldraw/src/state/tldr.ts | 4 +- packages/tldraw/src/state/tlstate.ts | 2 +- packages/tldraw/src/types.ts | 10 +- 42 files changed, 1104 insertions(+), 852 deletions(-) create mode 100644 packages/core/src/components/container/container.tsx create mode 100644 packages/core/src/components/container/index.ts create mode 100644 packages/core/src/components/svg-container/index.ts create mode 100644 packages/core/src/components/svg-container/svg-container.tsx create mode 100644 packages/core/src/hooks/usePosition.ts diff --git a/packages/core/src/components/bounds/bounds-bg.tsx b/packages/core/src/components/bounds/bounds-bg.tsx index 39f195bb1..a994e799d 100644 --- a/packages/core/src/components/bounds/bounds-bg.tsx +++ b/packages/core/src/components/bounds/bounds-bg.tsx @@ -1,7 +1,9 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import type { TLBounds } from '+types' -import { Utils } from '+utils' -import { useBoundsEvents } from '+hooks' +import { useBoundsEvents, usePosition } from '+hooks' +import { Container } from '+components/container' +import { SVGContainer } from '+components/svg-container' interface BoundsBgProps { bounds: TLBounds @@ -11,20 +13,11 @@ interface BoundsBgProps { export function BoundsBg({ bounds, rotation }: BoundsBgProps): JSX.Element { const events = useBoundsEvents() - const { width, height } = bounds - - const center = Utils.getBoundsCenter(bounds) - return ( - + + + + + ) } diff --git a/packages/core/src/components/bounds/bounds.tsx b/packages/core/src/components/bounds/bounds.tsx index 528a320c2..6f56a85a4 100644 --- a/packages/core/src/components/bounds/bounds.tsx +++ b/packages/core/src/components/bounds/bounds.tsx @@ -1,10 +1,13 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import { TLBoundsEdge, TLBoundsCorner, TLBounds } from '+types' -import { Utils } from '+utils' import { CenterHandle } from './center-handle' import { RotateHandle } from './rotate-handle' import { CornerHandle } from './corner-handle' import { EdgeHandle } from './edge-handle' +import { usePosition } from '+hooks' +import { Container } from '+components/container' +import { SVGContainer } from '+components/svg-container' interface BoundsProps { zoom: number @@ -14,6 +17,27 @@ interface BoundsProps { viewportWidth: number } +// function setTransform(elm: SVGSVGElement, padding: number, bounds: TLBounds, rotation: number) { +// const center = Utils.getBoundsCenter(bounds) +// const transform = ` +// rotate(${rotation * (180 / Math.PI)},${center}) +// translate(${bounds.minX - padding},${bounds.minY - padding}) +// rotate(${(bounds.rotation || 0) * (180 / Math.PI)},0,0)` +// elm.setAttribute('transform', transform) +// elm.setAttribute('width', bounds.width + padding * 2 + 'px') +// elm.setAttribute('height', bounds.height + padding * 2 + 'px') +// } + +// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { +// const transform = ` +// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) +// rotate(${rotation + (bounds.rotation || 0)}rad) +// ` +// elm.style.setProperty('transform', transform) +// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) +// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) +// } + export function Bounds({ zoom, bounds, @@ -23,65 +47,65 @@ export function Bounds({ }: BoundsProps): JSX.Element { const targetSize = (viewportWidth < 768 ? 16 : 8) / zoom // Touch target size const size = 8 / zoom // Touch target size - const center = Utils.getBoundsCenter(bounds) return ( - - - {!isLocked && ( - <> - - - - - - - - - - - )} - + + + + {!isLocked && ( + <> + + + + + + + + + + + )} + + ) } diff --git a/packages/core/src/components/brush/BrushUpdater.ts b/packages/core/src/components/brush/BrushUpdater.ts index 24982ccc5..d760b3713 100644 --- a/packages/core/src/components/brush/BrushUpdater.ts +++ b/packages/core/src/components/brush/BrushUpdater.ts @@ -2,7 +2,7 @@ import * as React from 'react' import type { TLBounds } from '+types' export class BrushUpdater { - ref = React.createRef() + ref = React.createRef() isControlled = false @@ -18,8 +18,7 @@ export class BrushUpdater { if (!elm) return elm.setAttribute('opacity', '1') - elm.setAttribute('x', bounds.minX.toString()) - elm.setAttribute('y', bounds.minY.toString()) + elm.setAttribute('transform', `translate(${bounds.minX.toString()}, ${bounds.minY.toString()})`) elm.setAttribute('width', bounds.width.toString()) elm.setAttribute('height', bounds.height.toString()) } diff --git a/packages/core/src/components/brush/brush.tsx b/packages/core/src/components/brush/brush.tsx index a8ba11b34..ec3521926 100644 --- a/packages/core/src/components/brush/brush.tsx +++ b/packages/core/src/components/brush/brush.tsx @@ -4,5 +4,9 @@ import { BrushUpdater } from './BrushUpdater' export const brushUpdater = new BrushUpdater() export const Brush = React.memo((): JSX.Element | null => { - return + return ( + + + + ) }) diff --git a/packages/core/src/components/canvas/canvas.tsx b/packages/core/src/components/canvas/canvas.tsx index af75bbc0c..4a9d30dc0 100644 --- a/packages/core/src/components/canvas/canvas.tsx +++ b/packages/core/src/components/canvas/canvas.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react' import { usePreventNavigation, @@ -18,24 +19,24 @@ function resetError() { void null } -interface CanvasProps { +interface CanvasProps> { page: TLPage pageState: TLPageState hideBounds?: boolean hideHandles?: boolean hideIndicators?: boolean - meta?: Record + meta?: M } -export function Canvas({ +export function Canvas>({ page, pageState, meta, hideHandles = false, hideBounds = false, hideIndicators = false, -}: CanvasProps): JSX.Element { - const rCanvas = React.useRef(null) +}: CanvasProps): JSX.Element { + const rCanvas = React.useRef(null) const rContainer = React.useRef(null) useResizeObserver(rCanvas) @@ -48,14 +49,14 @@ export function Canvas({ const events = useCanvasEvents() - const rGroup = useCameraCss(rContainer, pageState) + const rLayer = useCameraCss(rContainer, pageState) return (
- +
- - + {/* */} +
({ meta={meta} /> - +
- +
) } diff --git a/packages/core/src/components/container/container.tsx b/packages/core/src/components/container/container.tsx new file mode 100644 index 000000000..c88bdb820 --- /dev/null +++ b/packages/core/src/components/container/container.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import type { TLBounds } from '+types' +import { usePosition } from '+hooks' + +interface ContainerProps { + bounds: TLBounds + rotation?: number + id?: string + className?: string + children: React.ReactNode +} + +export const Container = React.memo( + ({ id, bounds, rotation = 0, className, children }: ContainerProps) => { + const rBounds = usePosition(bounds, rotation) + + return ( +
+ {children} +
+ ) + } +) diff --git a/packages/core/src/components/container/index.ts b/packages/core/src/components/container/index.ts new file mode 100644 index 000000000..a4b85ca9f --- /dev/null +++ b/packages/core/src/components/container/index.ts @@ -0,0 +1 @@ +export * from './container' diff --git a/packages/core/src/components/handles/handle.tsx b/packages/core/src/components/handles/handle.tsx index ef7c598a4..8fa422bc2 100644 --- a/packages/core/src/components/handles/handle.tsx +++ b/packages/core/src/components/handles/handle.tsx @@ -1,24 +1,41 @@ import * as React from 'react' import { useHandleEvents } from '+hooks' +import { Container } from '+components/container' +import Utils from '+utils' +import { SVGContainer } from '+components/svg-container' interface HandleProps { id: string point: number[] - zoom: number } -export const Handle = React.memo(({ id, point, zoom }: HandleProps) => { +export const Handle = React.memo(({ id, point }: HandleProps) => { const events = useHandleEvents(id) + const bounds = React.useMemo( + () => + Utils.translateBounds( + { + minX: 0, + minY: 0, + maxX: 32, + maxY: 32, + width: 32, + height: 32, + }, + point + ), + [point] + ) + return ( - - - - + + + + + + + + ) }) diff --git a/packages/core/src/components/handles/handles.test.tsx b/packages/core/src/components/handles/handles.test.tsx index 5be665bf8..67a60bdb6 100644 --- a/packages/core/src/components/handles/handles.test.tsx +++ b/packages/core/src/components/handles/handles.test.tsx @@ -4,6 +4,6 @@ import { Handles } from './handles' describe('handles', () => { test('mounts component without crashing', () => { - renderWithContext() + renderWithContext() }) }) diff --git a/packages/core/src/components/handles/handles.tsx b/packages/core/src/components/handles/handles.tsx index 8d9cd1116..8a62b215d 100644 --- a/packages/core/src/components/handles/handles.tsx +++ b/packages/core/src/components/handles/handles.tsx @@ -1,35 +1,26 @@ import * as React from 'react' import { Vec } from '+utils' import type { TLShape } from '+types' -import { useTLContext } from '+hooks' import { Handle } from './handle' interface HandlesProps { shape: TLShape - zoom: number } -const toAngle = 180 / Math.PI - -export const Handles = React.memo(({ shape, zoom }: HandlesProps): JSX.Element | null => { - const { shapeUtils } = useTLContext() - - const center = shapeUtils[shape.type].getCenter(shape) - +export const Handles = React.memo(({ shape }: HandlesProps): JSX.Element | null => { if (shape.handles === undefined) { return null } return ( - + <> {Object.values(shape.handles).map((handle) => ( ))} - + ) }) diff --git a/packages/core/src/components/page/page.tsx b/packages/core/src/components/page/page.tsx index 934c6cc6c..0b9412fac 100644 --- a/packages/core/src/components/page/page.tsx +++ b/packages/core/src/components/page/page.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import * as React from 'react' import type { TLBinding, TLPage, TLPageState, TLShape } from '+types' import { useSelection, useShapeTree, useHandles, useRenderOnResize, useTLContext } from '+hooks' @@ -7,26 +8,26 @@ import { Handles } from '+components/handles' import { ShapeNode } from '+components/shape' import { ShapeIndicator } from '+components/shape-indicator' -interface PageProps { +interface PageProps> { page: TLPage pageState: TLPageState hideBounds: boolean hideHandles: boolean hideIndicators: boolean - meta?: Record + meta?: M } /** * The Page component renders the current page. */ -export function Page({ +export function Page>({ page, pageState, hideBounds, hideHandles, hideIndicators, meta, -}: PageProps): JSX.Element { +}: PageProps): JSX.Element { const { callbacks, shapeUtils, inputs } = useTLContext() const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange) @@ -69,7 +70,7 @@ export function Page({ variant="hovered" /> )} - {!hideHandles && shapeWithHandles && } + {!hideHandles && shapeWithHandles && } ) } diff --git a/packages/core/src/components/renderer/renderer.tsx b/packages/core/src/components/renderer/renderer.tsx index 06e583788..148ac52e3 100644 --- a/packages/core/src/components/renderer/renderer.tsx +++ b/packages/core/src/components/renderer/renderer.tsx @@ -13,12 +13,15 @@ import { Canvas } from '../canvas' import { Inputs } from '../../inputs' import { useTLTheme, TLContext, TLContextType } from '../../hooks' -export interface RendererProps> - extends Partial { +export interface RendererProps< + T extends TLShape, + E extends HTMLElement | SVGElement, + M extends Record +> extends Partial { /** * An object containing instances of your shape classes. */ - shapeUtils: TLShapeUtils + shapeUtils: TLShapeUtils /** * The current page, containing shapes and bindings. */ @@ -63,7 +66,11 @@ export interface RendererProps>({ +export function Renderer< + T extends TLShape, + E extends SVGElement | HTMLElement, + M extends Record +>({ shapeUtils, page, pageState, @@ -73,17 +80,18 @@ export function Renderer>({ hideIndicators = false, hideBounds = false, ...rest -}: RendererProps): JSX.Element { +}: RendererProps): JSX.Element { useTLTheme(theme) const rScreenBounds = React.useRef(null) + const rPageState = React.useRef(pageState) React.useEffect(() => { rPageState.current = pageState }, [pageState]) - const [context] = React.useState(() => ({ + const [context] = React.useState>(() => ({ callbacks: rest, shapeUtils, rScreenBounds, @@ -92,7 +100,7 @@ export function Renderer>({ })) return ( - + }> { test('mounts component without crashing', () => { - renderWithSvg() + renderWithSvg( + + ) }) }) diff --git a/packages/core/src/components/shape-indicator/shape-indicator.tsx b/packages/core/src/components/shape-indicator/shape-indicator.tsx index 9f5501de4..002cfa440 100644 --- a/packages/core/src/components/shape-indicator/shape-indicator.tsx +++ b/packages/core/src/components/shape-indicator/shape-indicator.tsx @@ -1,20 +1,25 @@ import * as React from 'react' import type { TLShape } from '+types' -import { useTLContext } from '+hooks' +import { usePosition, useTLContext } from '+hooks' export const ShapeIndicator = React.memo( ({ shape, variant }: { shape: TLShape; variant: 'selected' | 'hovered' }) => { const { shapeUtils } = useTLContext() const utils = shapeUtils[shape.type] - - const center = utils.getCenter(shape) - const rotation = (shape.rotation || 0) * (180 / Math.PI) - const transform = `rotate(${rotation}, ${center}) translate(${shape.point})` + const bounds = utils.getBounds(shape) + const rBounds = usePosition(bounds, shape.rotation) return ( - - {shapeUtils[shape.type].renderIndicator(shape)} - +
+ + {utils.renderIndicator(shape)} + +
) } ) diff --git a/packages/core/src/components/shape/editing-text-shape.tsx b/packages/core/src/components/shape/editing-text-shape.tsx index c8694f75b..7e5cb5072 100644 --- a/packages/core/src/components/shape/editing-text-shape.tsx +++ b/packages/core/src/components/shape/editing-text-shape.tsx @@ -2,13 +2,15 @@ import { useTLContext } from '+hooks' import * as React from 'react' import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types' -interface EditingShapeProps extends TLRenderInfo { +interface EditingShapeProps + extends TLRenderInfo { shape: T - utils: TLShapeUtil + utils: TLShapeUtil } -export function EditingTextShape({ +export function EditingTextShape({ shape, + events, utils, isEditing, isBinding, @@ -16,12 +18,12 @@ export function EditingTextShape({ isSelected, isCurrentParent, meta, -}: EditingShapeProps) { +}: EditingShapeProps) { const { callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp }, } = useTLContext() - const ref = React.useRef(null) + const ref = React.useRef(null) React.useEffect(() => { // Firefox fix? @@ -32,18 +34,22 @@ export function EditingTextShape({ }, 0) }, [shape.id]) - return utils.render(shape, { + return utils.render({ ref, + shape, isEditing, isHovered, isSelected, isCurrentParent, isBinding, - onTextChange, - onTextBlur, - onTextFocus, - onTextKeyDown, - onTextKeyUp, + events: { + ...events, + onTextChange, + onTextBlur, + onTextFocus, + onTextKeyDown, + onTextKeyUp, + }, meta, }) } diff --git a/packages/core/src/components/shape/rendered-shape.tsx b/packages/core/src/components/shape/rendered-shape.tsx index 5c88b6dda..7aeccdc78 100644 --- a/packages/core/src/components/shape/rendered-shape.tsx +++ b/packages/core/src/components/shape/rendered-shape.tsx @@ -1,13 +1,9 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types' -interface RenderedShapeProps extends TLRenderInfo { - shape: T - utils: TLShapeUtil -} - export const RenderedShape = React.memo( - function RenderedShape({ + >({ shape, utils, isEditing, @@ -15,16 +11,29 @@ export const RenderedShape = React.memo( isHovered, isSelected, isCurrentParent, + events, meta, - }: RenderedShapeProps) { - return utils.render(shape, { - isEditing, - isBinding, - isHovered, - isSelected, - isCurrentParent, - meta, - }) + }: TLRenderInfo & { + shape: T + utils: TLShapeUtil + }) => { + const ref = utils.getRef(shape) + + return ( + + ) }, (prev, next) => { // If these have changed, then definitely render diff --git a/packages/core/src/components/shape/shape-node.tsx b/packages/core/src/components/shape/shape-node.tsx index 1e721e63c..17250f4e5 100644 --- a/packages/core/src/components/shape/shape-node.tsx +++ b/packages/core/src/components/shape/shape-node.tsx @@ -3,7 +3,7 @@ import type { IShapeTreeNode, TLShape, TLShapeUtils } from '+types' import { Shape } from './shape' export const ShapeNode = React.memo( - >({ + ({ shape, utils, children, @@ -13,7 +13,7 @@ export const ShapeNode = React.memo( isSelected, isCurrentParent, meta, - }: { utils: TLShapeUtils } & IShapeTreeNode) => { + }: { utils: TLShapeUtils } & IShapeTreeNode) => { return ( <> { test('mounts component without crashing', () => { renderWithSvg( { ) }) }) + +// { shape: TLShape; ref: ForwardedRef; } & TLRenderInfo & RefAttributes +// { shape: BoxShape; ref: ForwardedRef; } & TLRenderInfo & RefAttributes' diff --git a/packages/core/src/components/shape/shape.tsx b/packages/core/src/components/shape/shape.tsx index 4a9ee17c3..001f3b801 100644 --- a/packages/core/src/components/shape/shape.tsx +++ b/packages/core/src/components/shape/shape.tsx @@ -1,10 +1,27 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { useShapeEvents } from '+hooks' -import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types' +import { usePosition, useShapeEvents } from '+hooks' +import type { IShapeTreeNode, TLBounds, TLShape, TLShapeUtil } from '+types' import { RenderedShape } from './rendered-shape' import { EditingTextShape } from './editing-text-shape' +import { Container } from '+components/container' +import { SVGContainer } from '+components/svg-container' -export const Shape = >({ +// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { +// const transform = ` +// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) +// rotate(${rotation + (bounds.rotation || 0)}rad) +// ` +// elm.style.setProperty('transform', transform) +// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) +// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) +// } + +export const Shape = < + T extends TLShape, + E extends SVGElement | HTMLElement, + M extends Record +>({ shape, utils, isEditing, @@ -13,42 +30,46 @@ export const Shape = >({ isSelected, isCurrentParent, meta, -}: { utils: TLShapeUtil } & IShapeTreeNode) => { +}: IShapeTreeNode & { + utils: TLShapeUtil +}) => { + const bounds = utils.getBounds(shape) const events = useShapeEvents(shape.id, isCurrentParent) - const center = utils.getCenter(shape) - const rotation = (shape.rotation || 0) * (180 / Math.PI) - const transform = `rotate(${rotation}, ${center}) translate(${shape.point})` return ( - - {isEditing && utils.isEditableText ? ( - - ) : ( - - )} - + + {isEditing && utils.isEditableText ? ( + + ) : ( + + )} + + ) } diff --git a/packages/core/src/components/svg-container/index.ts b/packages/core/src/components/svg-container/index.ts new file mode 100644 index 000000000..74b860e70 --- /dev/null +++ b/packages/core/src/components/svg-container/index.ts @@ -0,0 +1 @@ +export * from './svg-container' diff --git a/packages/core/src/components/svg-container/svg-container.tsx b/packages/core/src/components/svg-container/svg-container.tsx new file mode 100644 index 000000000..9d07203ef --- /dev/null +++ b/packages/core/src/components/svg-container/svg-container.tsx @@ -0,0 +1,13 @@ +import * as React from 'react' + +interface SvgContainerProps { + children: React.ReactNode +} + +export const SVGContainer = React.memo(({ children }: SvgContainerProps) => { + return ( + + {children} + + ) +}) diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index 52101db5c..fc340110d 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -14,3 +14,4 @@ export * from './useHandleEvents' export * from './useHandles' export * from './usePreventNavigation' export * from './useBoundsEvents' +export * from './usePosition' diff --git a/packages/core/src/hooks/useCameraCss.tsx b/packages/core/src/hooks/useCameraCss.tsx index 5e40fe552..ee7faa65b 100644 --- a/packages/core/src/hooks/useCameraCss.tsx +++ b/packages/core/src/hooks/useCameraCss.tsx @@ -3,21 +3,29 @@ import * as React from 'react' import type { TLPageState } from '+types' export function useCameraCss(ref: React.RefObject, pageState: TLPageState) { - const rGroup = React.useRef(null) + const rLayer = React.useRef(null) // Update the tl-zoom CSS variable when the zoom changes React.useEffect(() => { ref.current!.style.setProperty('--tl-zoom', pageState.camera.zoom.toString()) }, [pageState.camera.zoom]) - // Update the group's position when the camera moves or zooms React.useEffect(() => { - const { - zoom, - point: [x = 0, y = 0], - } = pageState.camera - rGroup.current?.setAttribute('transform', `scale(${zoom}) translate(${x} ${y})`) - }, [pageState.camera]) + ref.current!.style.setProperty('--tl-camera-x', pageState.camera.point[0] + 'px') + ref.current!.style.setProperty('--tl-camera-y', pageState.camera.point[1] + 'px') + }, [pageState.camera.point]) - return rGroup + // Update the group's position when the camera moves or zooms + // React.useEffect(() => { + // const { + // zoom, + // point: [x = 0, y = 0], + // } = pageState.camera + // rLayer.current?.style.setProperty( + // 'transform', + // `scale(${zoom},${zoom}) translate(${x}px,${y}px)` + // ) + // }, [pageState.camera]) + + return rLayer } diff --git a/packages/core/src/hooks/usePosition.ts b/packages/core/src/hooks/usePosition.ts new file mode 100644 index 000000000..bc1e17732 --- /dev/null +++ b/packages/core/src/hooks/usePosition.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import * as React from 'react' +import type { TLBounds } from '+types' + +export function usePosition(bounds: TLBounds, rotation = 0) { + const rBounds = React.useRef(null) + + React.useEffect(() => { + const elm = rBounds.current! + const transform = ` + translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) + rotate(${rotation + (bounds.rotation || 0)}rad) + ` + elm.style.setProperty('transform', transform) + elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) + elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) + }, [rBounds, bounds, rotation]) + + return rBounds +} diff --git a/packages/core/src/hooks/usePreventNavigation.tsx b/packages/core/src/hooks/usePreventNavigation.tsx index c22311aff..a28a7b63a 100644 --- a/packages/core/src/hooks/usePreventNavigation.tsx +++ b/packages/core/src/hooks/usePreventNavigation.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import * as React from 'react' -export function usePreventNavigation(rCanvas: React.RefObject): void { +export function usePreventNavigation(rCanvas: React.RefObject): void { React.useEffect(() => { const preventGestureNavigation = (event: TouchEvent) => { event.preventDefault() diff --git a/packages/core/src/hooks/useSelection.tsx b/packages/core/src/hooks/useSelection.tsx index 040ccd112..73d1c39a8 100644 --- a/packages/core/src/hooks/useSelection.tsx +++ b/packages/core/src/hooks/useSelection.tsx @@ -6,10 +6,10 @@ function canvasToScreen(point: number[], camera: TLPageState['camera']): number[ return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom] } -export function useSelection( +export function useSelection( page: TLPage, pageState: TLPageState, - shapeUtils: TLShapeUtils + shapeUtils: TLShapeUtils ) { const { rScreenBounds } = useTLContext() const { selectedIds } = pageState diff --git a/packages/core/src/hooks/useShapeTree.tsx b/packages/core/src/hooks/useShapeTree.tsx index 03b366b70..78d90f6e5 100644 --- a/packages/core/src/hooks/useShapeTree.tsx +++ b/packages/core/src/hooks/useShapeTree.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import type { @@ -13,8 +14,8 @@ import type { import { Utils, Vec } from '+utils' function addToShapeTree>( - shape: TLShape, - branch: IShapeTreeNode[], + shape: T, + branch: IShapeTreeNode[], shapes: TLPage['shapes'], pageState: { bindingTargetId?: string @@ -27,7 +28,7 @@ function addToShapeTree>( }, meta?: M ) { - const node: IShapeTreeNode = { + const node: IShapeTreeNode = { shape, isCurrentParent: pageState.currentParentId === shape.id, isEditing: pageState.editingId === shape.id, @@ -37,7 +38,7 @@ function addToShapeTree>( (shape.children ? shape.children.includes(pageState.hoveredId) : false) : false, isBinding: pageState.bindingTargetId === shape.id, - meta, + meta: meta as any, } branch.push(node) @@ -54,14 +55,18 @@ function addToShapeTree>( } } -function shapeIsInViewport(shape: TLShape, bounds: TLBounds, viewport: TLBounds) { +function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) { return Utils.boundsContain(viewport, bounds) || Utils.boundsCollide(viewport, bounds) } -export function useShapeTree>( +export function useShapeTree< + T extends TLShape, + E extends SVGElement | HTMLElement, + M extends Record +>( page: TLPage, pageState: TLPageState, - shapeUtils: TLShapeUtils, + shapeUtils: TLShapeUtils, size: number[], meta?: M, onChange?: TLCallbacks['onChange'] @@ -100,7 +105,7 @@ export function useShapeTree[] = [] + const tree: IShapeTreeNode[] = [] const info = { ...pageState, bindingTargetId } diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index 1cb77cf9c..33e4f462f 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -111,6 +111,9 @@ const tlcss = css` .tl-container { --tl-zoom: 1; --tl-scale: calc(1 / var(--tl-zoom)); + --tl-camera-x: 0px; + --tl-camera-y: 0px; + --tl-padding: calc(32px * var(--tl-scale)); position: relative; box-sizing: border-box; width: 100%; @@ -127,6 +130,37 @@ const tlcss = css` box-sizing: border-box; } + .tl-absolute { + position: absolute; + top: 0px; + left: 0px; + transform-origin: center center; + } + + .tl-positioned { + position: absolute; + top: 0px; + left: 0px; + transform-origin: center center; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + } + + .tl-positioned-svg { + width: 100%; + height: 100%; + pointer-events: none; + } + + .tl-layer { + transform: scale(var(--tl-zoom), var(--tl-zoom)) + translate(var(--tl-camera-x), var(--tl-camera-y)); + height: 0; + width: 0; + } + .tl-counter-scaled { transform: scale(var(--tl-scale)); } @@ -190,6 +224,10 @@ const tlcss = css` pointer-events: none; } + .tl-bounds { + pointer-events: none; + } + .tl-bounds-center { fill: transparent; stroke: var(--tl-selectStroke); @@ -210,10 +248,7 @@ const tlcss = css` } .tl-canvas { - position: absolute; overflow: hidden; - top: 0px; - left: 0px; width: 100%; height: 100%; touch-action: none; @@ -256,6 +291,7 @@ const tlcss = css` fill: transparent; stroke: none; pointer-events: all; + r: calc(20 / max(1, var(--tl-zoom))); } .tl-binding-indicator { @@ -264,18 +300,22 @@ const tlcss = css` stroke: var(--tl-selected); } - .tl-shape-group { + .tl-shape { outline: none; } - .tl-shape-group > *[data-shy='true'] { + .tl-shape > *[data-shy='true'] { opacity: 0; } - .tl-shape-group:hover > *[data-shy='true'] { + .tl-shape:hover > *[data-shy='true'] { opacity: 1; } + .tl-centered-g { + transform: translate(var(--tl-padding), var(--tl-padding)); + } + .tl-current-parent > *[data-shy='true'] { opacity: 1; } diff --git a/packages/core/src/hooks/useTLContext.tsx b/packages/core/src/hooks/useTLContext.tsx index ee100f9e0..375479f18 100644 --- a/packages/core/src/hooks/useTLContext.tsx +++ b/packages/core/src/hooks/useTLContext.tsx @@ -2,16 +2,18 @@ import * as React from 'react' import type { Inputs } from '+inputs' import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types' -export interface TLContextType { +export interface TLContextType { id?: string callbacks: Partial - shapeUtils: TLShapeUtils + shapeUtils: TLShapeUtils rPageState: React.MutableRefObject rScreenBounds: React.MutableRefObject inputs: Inputs } -export const TLContext = React.createContext({} as TLContextType) +export const TLContext = React.createContext>( + {} as TLContextType +) export function useTLContext() { const context = React.useContext(TLContext) diff --git a/packages/core/src/test/box.tsx b/packages/core/src/test/box.tsx index 42997c967..f1e18bc07 100644 --- a/packages/core/src/test/box.tsx +++ b/packages/core/src/test/box.tsx @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import * as React from 'react' -import { TLShapeUtil, TLShape, TLBounds, TLRenderInfo, TLTransformInfo } from '+types' +import { TLShapeUtil, TLShape, TLShapeProps, TLBounds, TLRenderInfo, TLTransformInfo } from '+types' import Utils, { Intersect } from '+utils' export interface BoxShape extends TLShape { size: number[] } -export class Box extends TLShapeUtil { +export class Box extends TLShapeUtil { type = 'box' defaultProps = { @@ -21,13 +21,15 @@ export class Box extends TLShapeUtil { rotation: 0, } - create(props: Partial) { - return { ...this.defaultProps, ...props } - } - - render(shape: BoxShape, info: TLRenderInfo): JSX.Element { - return - } + render = React.forwardRef>( + ({ shape, events }, ref) => { + return ( + + + + ) + } + ) renderIndicator(shape: BoxShape) { return diff --git a/packages/core/src/test/mockUtils.tsx b/packages/core/src/test/mockUtils.tsx index 79483abed..499a4e4a9 100644 --- a/packages/core/src/test/mockUtils.tsx +++ b/packages/core/src/test/mockUtils.tsx @@ -1,6 +1,6 @@ import type { TLShapeUtils } from '+types' import { Box, BoxShape } from './box' -export const mockUtils: TLShapeUtils = { +export const mockUtils: TLShapeUtils = { box: new Box(), } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 80482ef19..153268c98 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,6 +2,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* --------------------- Primary -------------------- */ +import React, { ForwardedRef } from 'react' + export type Patch = Partial<{ [P in keyof T]: T | Partial | Patch }> export interface TLPage { @@ -54,21 +56,35 @@ export interface TLShape { isAspectRatioLocked?: boolean } -export type TLShapeUtils = Record> +export type TLShapeUtils = Record< + string, + TLShapeUtil +> -export interface TLRenderInfo { - ref?: React.RefObject +export interface TLRenderInfo { isEditing: boolean isBinding: boolean isHovered: boolean isSelected: boolean isCurrentParent: boolean - onTextChange?: TLCallbacks['onTextChange'] - onTextBlur?: TLCallbacks['onTextBlur'] - onTextFocus?: TLCallbacks['onTextFocus'] - onTextKeyDown?: TLCallbacks['onTextKeyDown'] - onTextKeyUp?: TLCallbacks['onTextKeyUp'] meta: M extends any ? M : never + events: { + onPointerDown: (e: React.PointerEvent) => void + onPointerUp: (e: React.PointerEvent) => void + onPointerEnter: (e: React.PointerEvent) => void + onPointerMove: (e: React.PointerEvent) => void + onPointerLeave: (e: React.PointerEvent) => void + onTextChange?: TLCallbacks['onTextChange'] + onTextBlur?: TLCallbacks['onTextBlur'] + onTextFocus?: TLCallbacks['onTextFocus'] + onTextKeyDown?: TLCallbacks['onTextKeyDown'] + onTextKeyUp?: TLCallbacks['onTextKeyUp'] + } +} + +export interface TLShapeProps extends TLRenderInfo { + ref: ForwardedRef + shape: T } export interface TLTool { @@ -261,18 +277,26 @@ export interface TLBezierCurveSegment { /* Shape Utility */ /* -------------------------------------------------- */ -export abstract class TLShapeUtil { +export abstract class TLShapeUtil { + refMap = new Map>() + boundsCache = new WeakMap() + isEditableText = false + isAspectRatioLocked = false + canEdit = false + canBind = false abstract type: T['type'] abstract defaultProps: T - abstract render(shape: T, info: TLRenderInfo): JSX.Element | null + abstract render: React.ForwardRefExoticComponent< + { shape: T; ref: React.ForwardedRef } & TLRenderInfo & React.RefAttributes + > abstract renderIndicator(shape: T): JSX.Element | null @@ -303,6 +327,14 @@ export abstract class TLShapeUtil { return [bounds.width / 2, bounds.height / 2] } + getRef(shape: T): React.RefObject { + if (!this.refMap.has(shape.id)) { + this.refMap.set(shape.id, React.createRef()) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.refMap.get(shape.id)! + } + getBindingPoint( shape: T, fromShape: TLShape, @@ -315,7 +347,8 @@ export abstract class TLShapeUtil { return undefined } - create(props: Partial): T { + create(props: { id: string } & Partial): T { + this.refMap.set(props.id, React.createRef()) return { ...this.defaultProps, ...props } } @@ -380,15 +413,15 @@ export abstract class TLShapeUtil { /* -------------------- Internal -------------------- */ -export interface IShapeTreeNode> { - shape: TLShape - children?: IShapeTreeNode[] +export interface IShapeTreeNode> { + shape: T + children?: IShapeTreeNode[] isEditing: boolean isBinding: boolean isHovered: boolean isSelected: boolean isCurrentParent: boolean - meta?: M + meta?: M extends any ? M : never } /* -------------------------------------------------- */ diff --git a/packages/tldraw/src/shape/shape-utils.tsx b/packages/tldraw/src/shape/shape-utils.tsx index d03004b63..ee775a5a9 100644 --- a/packages/tldraw/src/shape/shape-utils.tsx +++ b/packages/tldraw/src/shape/shape-utils.tsx @@ -8,18 +8,31 @@ export const tldrawShapeUtils: TLDrawShapeUtils = { [TLDrawShapeType.Arrow]: new Arrow(), [TLDrawShapeType.Text]: new Text(), [TLDrawShapeType.Group]: new Group(), -} +} as TLDrawShapeUtils export type ShapeByType = TLDrawShapeUtils[T] -export function getShapeUtilsByType(shape: T): TLDrawShapeUtil { - return tldrawShapeUtils[shape.type as T['type']] as TLDrawShapeUtil +export function getShapeUtilsByType( + shape: T +): TLDrawShapeUtil { + return tldrawShapeUtils[shape.type as T['type']] as unknown as TLDrawShapeUtil< + T, + HTMLElement | SVGElement + > } -export function getShapeUtils(shape: T): TLDrawShapeUtil { - return tldrawShapeUtils[shape.type as T['type']] as TLDrawShapeUtil +export function getShapeUtils( + shape: T +): TLDrawShapeUtil { + return tldrawShapeUtils[shape.type as T['type']] as unknown as TLDrawShapeUtil< + T, + HTMLElement | SVGElement + > } -export function createShape(type: TLDrawShapeType, props: Partial) { +export function createShape( + type: TLDrawShapeType, + props: { id: string } & Partial +) { return tldrawShapeUtils[type].create(props) } diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index f59850296..6335772d0 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -7,6 +7,7 @@ import { Intersect, TLHandle, TLPointerInfo, + TLShapeProps, } from '@tldraw/core' import getStroke from 'perfect-freehand' import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles' @@ -22,7 +23,7 @@ import { TLDrawRenderInfo, } from '~types' -export class Arrow extends TLDrawShapeUtil { +export class Arrow extends TLDrawShapeUtil { type = TLDrawShapeType.Arrow as const toolType = TLDrawToolType.Handle canStyleFill = false @@ -70,62 +71,130 @@ export class Arrow extends TLDrawShapeUtil { return next.handles !== prev.handles || next.style !== prev.style } - render = (shape: ArrowShape, { meta }: TLDrawRenderInfo) => { - const { - handles: { start, bend, end }, - decorations = {}, - style, - } = shape + render = React.forwardRef>( + ({ shape, meta, events }) => { + const { + handles: { start, bend, end }, + decorations = {}, + style, + } = shape - const isDraw = style.dash === DashStyle.Draw + const isDraw = style.dash === DashStyle.Draw - // TODO: Improve drawn arrows + // TODO: Improve drawn arrows - const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1 + const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1 - const styles = getShapeStyle(style, meta.isDarkMode) + const styles = getShapeStyle(style, meta.isDarkMode) - const { strokeWidth } = styles + const { strokeWidth } = styles - const arrowDist = Vec.dist(start.point, end.point) + const arrowDist = Vec.dist(start.point, end.point) - const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8) + const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8) - let shaftPath: JSX.Element | null - let startArrowHead: { left: number[]; right: number[] } | undefined - let endArrowHead: { left: number[]; right: number[] } | undefined + let shaftPath: JSX.Element | null + let startArrowHead: { left: number[]; right: number[] } | undefined + let endArrowHead: { left: number[]; right: number[] } | undefined - if (isStraightLine) { - const sw = strokeWidth * (isDraw ? 1.25 : 1.618) + if (isStraightLine) { + const sw = strokeWidth * (isDraw ? 1.25 : 1.618) - const path = Utils.getFromCache(this.pathCache, shape, () => - isDraw - ? renderFreehandArrowShaft(shape) - : 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point) - ) + const path = Utils.getFromCache(this.pathCache, shape, () => + isDraw + ? renderFreehandArrowShaft(shape) + : 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point) + ) - const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( - arrowDist, - sw, - shape.style.dash, - 2 - ) + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + arrowDist, + sw, + shape.style.dash, + 2 + ) - if (decorations.start) { - startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength) - } + if (decorations.start) { + startArrowHead = getStraightArrowHeadPoints(start.point, end.point, arrowHeadLength) + } - if (decorations.end) { - endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength) - } + if (decorations.end) { + endArrowHead = getStraightArrowHeadPoints(end.point, start.point, arrowHeadLength) + } - // Straight arrow path - shaftPath = - arrowDist > 2 ? ( + // Straight arrow path + shaftPath = + arrowDist > 2 ? ( + <> + + + + ) : null + } else { + const circle = getCtp(shape) + + const sw = strokeWidth * (isDraw ? 1.25 : 1.618) + + const path = Utils.getFromCache(this.pathCache, shape, () => + isDraw + ? renderCurvedFreehandArrowShaft(shape, circle) + : getArrowArcPath(start, end, circle, shape.bend) + ) + + const { center, radius, length } = getArrowArc(shape) + + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + length - 1, + sw, + shape.style.dash, + 2 + ) + + if (decorations.start) { + startArrowHead = getCurvedArrowHeadPoints( + start.point, + arrowHeadLength, + center, + radius, + length < 0 + ) + } + + if (decorations.end) { + endArrowHead = getCurvedArrowHeadPoints( + end.point, + arrowHeadLength, + center, + radius, + length >= 0 + ) + } + + // Curved arrow path + shaftPath = ( <> { /> { pointerEvents="stroke" /> - ) : null - } else { - const circle = getCtp(shape) - - const sw = strokeWidth * (isDraw ? 1.25 : 1.618) - - const path = Utils.getFromCache(this.pathCache, shape, () => - isDraw - ? renderCurvedFreehandArrowShaft(shape, circle) - : getArrowArcPath(start, end, circle, shape.bend) - ) - - const { center, radius, length } = getArrowArc(shape) - - const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( - length - 1, - sw, - shape.style.dash, - 2 - ) - - if (decorations.start) { - startArrowHead = getCurvedArrowHeadPoints( - start.point, - arrowHeadLength, - center, - radius, - length < 0 ) } - if (decorations.end) { - endArrowHead = getCurvedArrowHeadPoints( - end.point, - arrowHeadLength, - center, - radius, - length >= 0 - ) - } + const sw = strokeWidth * 1.618 - // Curved arrow path - shaftPath = ( - <> - - - + return ( + + + {shaftPath} + {startArrowHead && ( + + )} + {endArrowHead && ( + + )} + + ) } - - const sw = strokeWidth * 1.618 - - return ( - - {shaftPath} - {startArrowHead && ( - - )} - {endArrowHead && ( - - )} - - ) - } + ) renderIndicator(shape: ArrowShape) { const path = Utils.getFromCache(this.simplePathCache, shape.handles, () => getArrowPath(shape)) diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index 553ceb3ca..abf7f58fe 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -1,17 +1,10 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' import getStroke, { getStrokePoints } from 'perfect-freehand' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' -import { - DrawShape, - DashStyle, - TLDrawShapeUtil, - TLDrawShapeType, - TLDrawToolType, - TLDrawRenderInfo, -} from '~types' +import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types' -export class Draw extends TLDrawShapeUtil { +export class Draw extends TLDrawShapeUtil { type = TLDrawShapeType.Draw as const toolType = TLDrawToolType.Draw @@ -37,118 +30,122 @@ export class Draw extends TLDrawShapeUtil { return next.points !== prev.points || next.style !== prev.style } - render(shape: DrawShape, { meta, isEditing }: TLDrawRenderInfo): JSX.Element { - const { points, style } = shape + render = React.forwardRef>( + ({ shape, meta, events, isEditing }) => { + const { points, style } = shape - const styles = getShapeStyle(style, meta.isDarkMode) + const styles = getShapeStyle(style, meta.isDarkMode) - const strokeWidth = styles.strokeWidth + const strokeWidth = styles.strokeWidth - // For very short lines, draw a point instead of a line - const bounds = this.getBounds(shape) + // For very short lines, draw a point instead of a line + const bounds = this.getBounds(shape) - const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2 + const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2 - if (!isEditing && verySmall) { - const sw = strokeWidth * 0.618 + if (!isEditing && verySmall) { + const sw = strokeWidth * 0.618 - return ( - - ) - } + return ( + + + + ) + } - const shouldFill = - style.isFilled && - points.length > 3 && - Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2 + const shouldFill = + style.isFilled && + points.length > 3 && + Vec.dist(points[0], points[points.length - 1]) < +styles.strokeWidth * 2 - // For drawn lines, draw a line from the path cache + // For drawn lines, draw a line from the path cache - if (shape.style.dash === DashStyle.Draw) { - const polygonPathData = Utils.getFromCache(this.polygonCache, points, () => - getFillPath(shape) - ) + if (shape.style.dash === DashStyle.Draw) { + const polygonPathData = Utils.getFromCache(this.polygonCache, points, () => + getFillPath(shape) + ) - const drawPathData = isEditing - ? getDrawStrokePath(shape, true) - : Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false)) + const drawPathData = isEditing + ? getDrawStrokePath(shape, true) + : Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false)) - return ( - <> - {shouldFill && ( + return ( + + {shouldFill && ( + + )} - )} + + ) + } + + // For solid, dash and dotted lines, draw a regular stroke path + + const strokeDasharray = { + [DashStyle.Draw]: 'none', + [DashStyle.Solid]: `none`, + [DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`, + [DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`, + }[style.dash] + + const strokeDashoffset = { + [DashStyle.Draw]: 'none', + [DashStyle.Solid]: `none`, + [DashStyle.Dotted]: `-${strokeWidth / 20}`, + [DashStyle.Dashed]: `-${strokeWidth}`, + }[style.dash] + + const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape)) + + const sw = strokeWidth * 1.618 + + return ( + - + + ) } - - // For solid, dash and dotted lines, draw a regular stroke path - - const strokeDasharray = { - [DashStyle.Draw]: 'none', - [DashStyle.Solid]: `none`, - [DashStyle.Dotted]: `${strokeWidth / 10} ${strokeWidth * 3}`, - [DashStyle.Dashed]: `${strokeWidth * 3} ${strokeWidth * 3}`, - }[style.dash] - - const strokeDashoffset = { - [DashStyle.Draw]: 'none', - [DashStyle.Solid]: `none`, - [DashStyle.Dotted]: `-${strokeWidth / 20}`, - [DashStyle.Dashed]: `-${strokeWidth}`, - }[style.dash] - - const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape)) - - const sw = strokeWidth * 1.618 - - return ( - <> - - - - ) - } + ) renderIndicator(shape: DrawShape): JSX.Element { const { points } = shape diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index 9328d2dac..16021e1f1 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Utils, TLTransformInfo, TLBounds, Intersect, Vec } from '@tldraw/core' +import { Utils, TLTransformInfo, TLBounds, Intersect, TLShapeProps, Vec } from '@tldraw/core' import { ArrowShape, DashStyle, @@ -15,7 +15,7 @@ import getStroke from 'perfect-freehand' // TODO // [ ] Improve indicator shape for drawn shapes -export class Ellipse extends TLDrawShapeUtil { +export class Ellipse extends TLDrawShapeUtil { type = TLDrawShapeType.Ellipse as const toolType = TLDrawToolType.Bounds pathCache = new WeakMap([]) @@ -37,32 +37,79 @@ export class Ellipse extends TLDrawShapeUtil { return next.radius !== prev.radius || next.style !== prev.style } - render(shape: EllipseShape, { meta, isBinding }: TLDrawRenderInfo) { - const { - radius: [radiusX, radiusY], - style, - } = shape + render = React.forwardRef>( + ({ shape, meta, isBinding, events }) => { + const { + radius: [radiusX, radiusY], + style, + } = shape - const styles = getShapeStyle(style, meta.isDarkMode) - const strokeWidth = +styles.strokeWidth + const styles = getShapeStyle(style, meta.isDarkMode) + const strokeWidth = +styles.strokeWidth - const rx = Math.max(0, radiusX - strokeWidth / 2) - const ry = Math.max(0, radiusY - strokeWidth / 2) + const rx = Math.max(0, radiusX - strokeWidth / 2) + const ry = Math.max(0, radiusY - strokeWidth / 2) - if (style.dash === DashStyle.Draw) { - const path = Utils.getFromCache(this.pathCache, shape, () => - renderPath(shape, this.getCenter(shape)) + if (style.dash === DashStyle.Draw) { + const path = Utils.getFromCache(this.pathCache, shape, () => + renderPath(shape, this.getCenter(shape)) + ) + + return ( + + {isBinding && ( + + )} + + + + ) + } + + const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2) + + const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h))) + + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + perimeter, + strokeWidth * 1.618, + shape.style.dash, + 4 ) + const sw = strokeWidth * 1.618 + return ( - <> + {isBinding && ( )} { cy={radiusY} rx={rx} ry={ry} - stroke="none" - fill={style.isFilled ? styles.fill : 'none'} - pointerEvents="all" - /> - - + ) } - - const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2) - - const perimeter = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h))) - - const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( - perimeter, - strokeWidth * 1.618, - shape.style.dash, - 4 - ) - - const sw = strokeWidth * 1.618 - - return ( - <> - {isBinding && ( - - )} - - - ) - } + ) renderIndicator(shape: EllipseShape) { const { diff --git a/packages/tldraw/src/shape/shapes/group/group.tsx b/packages/tldraw/src/shape/shapes/group/group.tsx index 562808fbe..0a95ea5d3 100644 --- a/packages/tldraw/src/shape/shapes/group/group.tsx +++ b/packages/tldraw/src/shape/shapes/group/group.tsx @@ -1,12 +1,11 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core' import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles' import { GroupShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, - TLDrawRenderInfo, ColorStyle, DashStyle, ArrowShape, @@ -15,7 +14,7 @@ import { // TODO // [ ] - Find bounds based on common bounds of descendants -export class Group extends TLDrawShapeUtil { +export class Group extends TLDrawShapeUtil { type = TLDrawShapeType.Group as const toolType = TLDrawToolType.Bounds canBind = true @@ -39,59 +38,68 @@ export class Group extends TLDrawShapeUtil { return next.size !== prev.size || next.style !== prev.style } - render(shape: GroupShape, { isBinding, isHovered, isSelected }: TLDrawRenderInfo) { - const { id, size } = shape + render = React.forwardRef>( + ({ shape, isBinding, isHovered, isSelected, events }) => { + const { id, size } = shape - const sw = 2 - const w = Math.max(0, size[0] - sw / 2) - const h = Math.max(0, size[1] - sw / 2) + const sw = 2 + const w = Math.max(0, size[0] - sw / 2) + const h = Math.max(0, size[1] - sw / 2) - const strokes: [number[], number[], number][] = [ - [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2], - [[w, sw / 2], [w, h], h - sw / 2], - [[w, h], [sw / 2, h], w - sw / 2], - [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2], - ] + const strokes: [number[], number[], number][] = [ + [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2], + [[w, sw / 2], [w, h], h - sw / 2], + [[w, h], [sw / 2, h], w - sw / 2], + [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2], + ] - const paths = strokes.map(([start, end, length], i) => { - const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( - length, - sw, - DashStyle.Dotted - ) + const paths = strokes.map(([start, end, length], i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + length, + sw, + DashStyle.Dotted + ) + + return ( + + ) + }) return ( - - ) - }) - - return ( - <> - {isBinding && ( + + {isBinding && ( + + )} - )} - - {paths} - - ) - } + {paths} + + ) + } + ) renderIndicator(shape: GroupShape) { const [width, height] = shape.size diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index 2d350efe0..9821e3df7 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' import getStroke from 'perfect-freehand' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' import { @@ -8,18 +8,16 @@ import { TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, - TLDrawRenderInfo, ArrowShape, } from '~types' // TODO // [ ] - Make sure that fill does not extend drawn shape at corners -export class Rectangle extends TLDrawShapeUtil { +export class Rectangle extends TLDrawShapeUtil { type = TLDrawShapeType.Rectangle as const toolType = TLDrawToolType.Bounds canBind = true - pathCache = new WeakMap([]) defaultProps: RectangleShape = { @@ -38,105 +36,111 @@ export class Rectangle extends TLDrawShapeUtil { return next.size !== prev.size || next.style !== prev.style } - render(shape: RectangleShape, { isBinding, meta }: TLDrawRenderInfo) { - const { id, size, style } = shape - const styles = getShapeStyle(style, meta.isDarkMode) - const strokeWidth = +styles.strokeWidth + render = React.forwardRef>( + ({ shape, isBinding, meta, events }, ref) => { + const { id, size, style } = shape + const styles = getShapeStyle(style, meta.isDarkMode) + const strokeWidth = +styles.strokeWidth - if (style.dash === DashStyle.Draw) { - const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape)) + React.useEffect(() => { + console.log(this.refMap.get(shape.id)) + }, []) + + if (style.dash === DashStyle.Draw) { + const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape)) + + return ( + + {isBinding && ( + + )} + + + + ) + } + + const sw = strokeWidth * 1.618 + + const w = Math.max(0, size[0] - sw / 2) + const h = Math.max(0, size[1] - sw / 2) + + const strokes: [number[], number[], number][] = [ + [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2], + [[w, sw / 2], [w, h], h - sw / 2], + [[w, h], [sw / 2, h], w - sw / 2], + [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2], + ] + + const paths = strokes.map(([start, end, length], i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + length, + sw, + shape.style.dash + ) + + return ( + + ) + }) return ( - <> + {isBinding && ( )} - - + {paths} + ) } - - const sw = strokeWidth * 1.618 - - const w = Math.max(0, size[0] - sw / 2) - const h = Math.max(0, size[1] - sw / 2) - - const strokes: [number[], number[], number][] = [ - [[sw / 2, sw / 2], [w, sw / 2], w - sw / 2], - [[w, sw / 2], [w, h], h - sw / 2], - [[w, h], [sw / 2, h], w - sw / 2], - [[sw / 2, h], [sw / 2, sw / 2], h - sw / 2], - ] - - const paths = strokes.map(([start, end, length], i) => { - const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( - length, - sw, - shape.style.dash - ) - - return ( - - ) - }) - - return ( - <> - {isBinding && ( - - )} - - {paths} - - ) - } + ) renderIndicator(shape: RectangleShape) { const { @@ -162,6 +166,7 @@ export class Rectangle extends TLDrawShapeUtil { } getBounds(shape: RectangleShape) { + console.log(this.refMap.get(shape.id)) const bounds = Utils.getFromCache(this.boundsCache, shape, () => { const [width, height] = shape.size return { diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index 7fdbb6329..e6ac8a7fe 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -1,14 +1,7 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles' -import { - TextShape, - TLDrawShapeUtil, - TLDrawShapeType, - TLDrawRenderInfo, - TLDrawToolType, - ArrowShape, -} from '~types' +import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types' import styled from '~styles' import TextAreaUtils from './text-utils' @@ -56,7 +49,7 @@ if (typeof window !== 'undefined') { melm = getMeasurementDiv() } -export class Text extends TLDrawShapeUtil { +export class Text extends TLDrawShapeUtil { type = TLDrawShapeType.Text as const toolType = TLDrawToolType.Text isAspectRatioLocked = true @@ -90,156 +83,144 @@ export class Text extends TLDrawShapeUtil { ) } - render( - shape: TextShape, - { - ref, - meta, - isEditing, - isBinding, - onTextBlur, - onTextChange, - onTextFocus, - onTextKeyDown, - onTextKeyUp, - }: TLDrawRenderInfo - ): JSX.Element { - const { id, text, style } = shape - const styles = getShapeStyle(style, meta.isDarkMode) - const font = getFontStyle(shape.style) - const bounds = this.getBounds(shape) + render = React.forwardRef>( + ({ shape, meta, isEditing, isBinding, events }, ref) => { + const rInput = React.useRef(null) + const { id, text, style } = shape + const styles = getShapeStyle(style, meta.isDarkMode) + const font = getFontStyle(shape.style) + const bounds = this.getBounds(shape) - function handleChange(e: React.ChangeEvent) { - onTextChange?.(id, normalizeText(e.currentTarget.value)) - } + function handleChange(e: React.ChangeEvent) { + events.onTextChange?.(id, normalizeText(e.currentTarget.value)) + } - function handleKeyDown(e: React.KeyboardEvent) { - onTextKeyDown?.(id, e.key) + function handleKeyDown(e: React.KeyboardEvent) { + events.onTextKeyDown?.(id, e.key) - if (e.key === 'Escape') return + if (e.key === 'Escape') return - e.stopPropagation() + e.stopPropagation() - if (e.key === 'Tab') { - e.preventDefault() - if (e.shiftKey) { - TextAreaUtils.unindent(e.currentTarget) - } else { - TextAreaUtils.indent(e.currentTarget) + if (e.key === 'Tab') { + e.preventDefault() + if (e.shiftKey) { + TextAreaUtils.unindent(e.currentTarget) + } else { + TextAreaUtils.indent(e.currentTarget) + } + + events.onTextChange?.(id, normalizeText(e.currentTarget.value)) + } + } + + function handleKeyUp(e: React.KeyboardEvent) { + events.onTextKeyUp?.(id, e.key) + } + + function handleBlur(e: React.FocusEvent) { + if (isEditing) { + e.currentTarget.focus() + e.currentTarget.select() + return } - onTextChange?.(id, normalizeText(e.currentTarget.value)) - } - } - - function handleKeyUp(e: React.KeyboardEvent) { - onTextKeyUp?.(id, e.key) - } - - function handleBlur(e: React.FocusEvent) { - if (isEditing) { - e.currentTarget.focus() - e.currentTarget.select() - return + setTimeout(() => { + events.onTextBlur?.(id) + }, 0) } - setTimeout(() => { - onTextBlur?.(id) - }, 0) - } - - function handleFocus(e: React.FocusEvent) { - if (document.activeElement === e.currentTarget) { - e.currentTarget.select() - onTextFocus?.(id) + function handleFocus(e: React.FocusEvent) { + if (document.activeElement === e.currentTarget) { + e.currentTarget.select() + events.onTextFocus?.(id) + } } - } - function handlePointerDown() { - if (ref && ref.current.selectionEnd !== 0) { - ref.current.selectionEnd = 0 + function handlePointerDown() { + const elm = rInput.current + if (!elm) return + if (elm.selectionEnd !== 0) { + elm.selectionEnd = 0 + } } - } - const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1) + const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1) - const lineHeight = fontSize * 1.3 + const lineHeight = fontSize * 1.3 + + if (!isEditing) { + return ( + + {isBinding && ( + + )} + {text.split('\n').map((str, i) => ( + + {str} + + ))} + + ) + } - if (!isEditing) { return ( - <> - {isBinding && ( - - )} - {text.split('\n').map((str, i) => ( - - {str} - - ))} - + e.stopPropagation()} + > + + ) } - - if (ref === undefined) { - throw Error('This component should receive a ref when editing.') - } - - return ( - e.stopPropagation()} - > - } - style={{ - font, - color: styles.stroke, - }} - name="text" - defaultValue={text} - tabIndex={-1} - autoComplete="false" - autoCapitalize="false" - autoCorrect="false" - autoSave="false" - placeholder="" - color={styles.stroke} - autoFocus={true} - onFocus={handleFocus} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - onChange={handleChange} - onPointerDown={handlePointerDown} - /> - - ) - } + ) renderIndicator(): JSX.Element | null { return null diff --git a/packages/tldraw/src/state/tldr.ts b/packages/tldraw/src/state/tldr.ts index 6205ff9eb..adab9f238 100644 --- a/packages/tldraw/src/state/tldr.ts +++ b/packages/tldraw/src/state/tldr.ts @@ -13,7 +13,9 @@ import type { } from '~types' export class TLDR { - static getShapeUtils(shape: T | T['type']): TLDrawShapeUtil { + static getShapeUtils( + shape: T | T['type'] + ): TLDrawShapeUtil { return getShapeUtils(typeof shape === 'string' ? ({ type: shape } as T) : shape) } diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 2ba44abfd..5b1651002 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -72,7 +72,7 @@ const defaultState: Data = { settings: { isPenMode: false, isDarkMode: false, - isZoomSnap: true, + isZoomSnap: false, isDebugMode: process.env.NODE_ENV === 'development', isReadonlyMode: false, nudgeDistanceLarge: 10, diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index ad416827d..03ea0d9ef 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -199,11 +199,17 @@ export type TLDrawShape = | TextShape | GroupShape -export abstract class TLDrawShapeUtil extends TLShapeUtil { +export abstract class TLDrawShapeUtil< + T extends TLDrawShape, + E extends HTMLElement | SVGElement +> extends TLShapeUtil { abstract toolType: TLDrawToolType } -export type TLDrawShapeUtils = Record> +export type TLDrawShapeUtils = Record< + TLDrawShapeType, + TLDrawShapeUtil +> export interface ArrowBinding extends TLBinding { type: 'arrow' From 9787cafc0697b440a6c3a8dfd211e74e7626b39e Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 17:01:59 +0100 Subject: [PATCH 02/14] Fix drawing session, avoids expensive iterations if we can --- .../tldraw/src/shape/shapes/arrow/arrow.tsx | 4 +-- .../tldraw/src/shape/shapes/draw/draw.tsx | 21 +++-------- .../src/shape/shapes/ellipse/ellipse.tsx | 6 ++-- .../tldraw/src/shape/shapes/group/group.tsx | 4 +-- .../src/shape/shapes/rectangle/rectangle.tsx | 5 --- .../session/sessions/draw/draw.session.ts | 36 +++++++++++++++---- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index 6335772d0..cdf4a9c7d 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -72,7 +72,7 @@ export class Arrow extends TLDrawShapeUtil { } render = React.forwardRef>( - ({ shape, meta, events }) => { + ({ shape, meta, events }, ref) => { const { handles: { start, bend, end }, decorations = {}, @@ -220,7 +220,7 @@ export class Arrow extends TLDrawShapeUtil { const sw = strokeWidth * 1.618 return ( - + {shaftPath} {startArrowHead && ( diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index abf7f58fe..a0f51185a 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -31,7 +31,7 @@ export class Draw extends TLDrawShapeUtil { } render = React.forwardRef>( - ({ shape, meta, events, isEditing }) => { + ({ shape, meta, events, isEditing }, ref) => { const { points, style } = shape const styles = getShapeStyle(style, meta.isDarkMode) @@ -47,7 +47,7 @@ export class Draw extends TLDrawShapeUtil { const sw = strokeWidth * 0.618 return ( - + { : Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false)) return ( - + {shouldFill && ( { const sw = strokeWidth * 1.618 return ( - + { ): Partial { return this.transform(shape, bounds, info) } - - onSessionComplete(shape: DrawShape): Partial { - const bounds = this.getBounds(shape) - - const [x1, y1] = Vec.round(Vec.sub([bounds.minX, bounds.minY], shape.point)) - - const points = shape.points.map(([x0, y0, p]) => Vec.round([x0 - x1, y0 - y1]).concat(p)) - - return { - points, - point: Vec.add(shape.point, [x1, y1]), - } - } } const simulatePressureSettings = { diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index 16021e1f1..9217e630b 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -38,7 +38,7 @@ export class Ellipse extends TLDrawShapeUtil { } render = React.forwardRef>( - ({ shape, meta, isBinding, events }) => { + ({ shape, meta, isBinding, events }, ref) => { const { radius: [radiusX, radiusY], style, @@ -56,7 +56,7 @@ export class Ellipse extends TLDrawShapeUtil { ) return ( - + {isBinding && ( { const sw = strokeWidth * 1.618 return ( - + {isBinding && ( { } render = React.forwardRef>( - ({ shape, isBinding, isHovered, isSelected, events }) => { + ({ shape, isBinding, isHovered, isSelected, events }, ref) => { const { id, size } = shape const sw = 2 @@ -77,7 +77,7 @@ export class Group extends TLDrawShapeUtil { }) return ( - + {isBinding && ( { const styles = getShapeStyle(style, meta.isDarkMode) const strokeWidth = +styles.strokeWidth - React.useEffect(() => { - console.log(this.refMap.get(shape.id)) - }, []) - if (style.dash === DashStyle.Draw) { const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape)) @@ -166,7 +162,6 @@ export class Rectangle extends TLDrawShapeUtil { } getBounds(shape: RectangleShape) { - console.log(this.refMap.get(shape.id)) const bounds = Utils.getFromCache(this.boundsCache, shape, () => { const [width, height] = shape.size return { diff --git a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts index 603f9295b..71a03ad7e 100644 --- a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts +++ b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts @@ -8,10 +8,12 @@ import { TLDR } from '~state/tldr' export class DrawSession implements Session { id = 'draw' status = TLDrawStatus.Creating + topLeft: number[] origin: number[] previous: number[] last: number[] points: number[][] + shiftedPoints: number[][] = [] snapshot: DrawSnapshot isLocked?: boolean lockedDirection?: 'horizontal' | 'vertical' @@ -20,6 +22,7 @@ export class DrawSession implements Session { this.origin = point this.previous = point this.last = point + this.topLeft = point this.snapshot = getDrawSnapshot(data, id) @@ -79,6 +82,12 @@ export class DrawSession implements Session { // The previous input (not adjusted) point this.previous = point + const prevTopLeft = [...this.topLeft] + + this.topLeft = [Math.min(this.topLeft[0], point[0]), Math.min(this.topLeft[1], point[1])] + + const delta = Vec.sub(this.topLeft, this.origin) + // The new adjusted point const newPoint = Vec.round(Vec.sub(this.previous, this.origin)).concat(pressure) @@ -89,16 +98,35 @@ export class DrawSession implements Session { // The new adjusted point is now the previous adjusted point. this.last = newPoint + let points: number[][] + // Add the new adjusted point to the points array this.points.push(newPoint) + // Time to shift some points! + + if (Vec.isEqual(prevTopLeft, this.topLeft)) { + // If the new top left is the same as the previous top left, + // we don't need to shift anything: we just shift the new point + // and add it to the shifted points array. + points = [...this.shiftedPoints, Vec.sub(newPoint, delta)] + } else { + // If we have a new top left, then we need to iterate through + // the "unshifted" points array and shift them based on the + // offset between the new top left and the original top left. + points = this.points.map((pt) => Vec.sub(pt, delta)) + } + + this.shiftedPoints = points + return { document: { pages: { [data.appState.currentPageId]: { shapes: { [snapshot.id]: { - points: [...this.points], // Set to a new array here + point: this.topLeft, + points, }, }, }, @@ -163,11 +191,7 @@ export class DrawSession implements Session { pages: { [pageId]: { shapes: { - [snapshot.id]: TLDR.onSessionComplete( - data, - { ...TLDR.getShape(data, snapshot.id, pageId), points: [...this.points] }, - pageId - ), + [snapshot.id]: TLDR.getShape(data, snapshot.id, pageId), }, }, }, From 4c41d98c8e0c9a3e651e792556c82b3eaee6ef12 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 17:06:28 +0100 Subject: [PATCH 03/14] Fix text shape --- .../components/shape/editing-text-shape.tsx | 54 +++++++++---------- packages/core/src/components/shape/shape.tsx | 6 +-- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/core/src/components/shape/editing-text-shape.tsx b/packages/core/src/components/shape/editing-text-shape.tsx index 7e5cb5072..3e1409e50 100644 --- a/packages/core/src/components/shape/editing-text-shape.tsx +++ b/packages/core/src/components/shape/editing-text-shape.tsx @@ -2,28 +2,29 @@ import { useTLContext } from '+hooks' import * as React from 'react' import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types' -interface EditingShapeProps - extends TLRenderInfo { - shape: T - utils: TLShapeUtil -} - -export function EditingTextShape({ +export function EditingTextShape< + T extends TLShape, + E extends SVGElement | HTMLElement, + M extends Record +>({ shape, - events, utils, isEditing, isBinding, isHovered, isSelected, isCurrentParent, + events, meta, -}: EditingShapeProps) { +}: TLRenderInfo & { + shape: T + utils: TLShapeUtil +}) { const { callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp }, } = useTLContext() - const ref = React.useRef(null) + const ref = utils.getRef(shape) React.useEffect(() => { // Firefox fix? @@ -34,22 +35,19 @@ export function EditingTextShape + ) } diff --git a/packages/core/src/components/shape/shape.tsx b/packages/core/src/components/shape/shape.tsx index 001f3b801..25d833971 100644 --- a/packages/core/src/components/shape/shape.tsx +++ b/packages/core/src/components/shape/shape.tsx @@ -52,19 +52,19 @@ export const Shape = < isEditing={true} isHovered={isHovered} isSelected={isSelected} - utils={utils} - meta={meta} + utils={utils as any} + meta={meta as any} events={events} /> ) : ( From 5359e92771d7abc2c23aa6f34552071d294726bf Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 17:21:10 +0100 Subject: [PATCH 04/14] Move SVG container to shape implementations --- packages/core/src/components/index.tsx | 1 + packages/core/src/components/shape/shape.tsx | 57 +++++++------ .../svg-container/svg-container.tsx | 18 +++-- packages/core/src/hooks/useStyle.tsx | 1 - .../tldraw/src/shape/shapes/arrow/arrow.tsx | 10 +-- .../tldraw/src/shape/shapes/draw/draw.tsx | 26 +++--- .../src/shape/shapes/ellipse/ellipse.tsx | 23 ++++-- .../tldraw/src/shape/shapes/group/group.tsx | 10 +-- .../src/shape/shapes/rectangle/rectangle.tsx | 18 +++-- .../tldraw/src/shape/shapes/text/text.tsx | 80 +++++++++++-------- 10 files changed, 138 insertions(+), 106 deletions(-) diff --git a/packages/core/src/components/index.tsx b/packages/core/src/components/index.tsx index d56c8b760..966c00473 100644 --- a/packages/core/src/components/index.tsx +++ b/packages/core/src/components/index.tsx @@ -1,2 +1,3 @@ export * from './renderer' export { brushUpdater } from './brush' +export * from './svg-container' diff --git a/packages/core/src/components/shape/shape.tsx b/packages/core/src/components/shape/shape.tsx index 25d833971..28290a8ce 100644 --- a/packages/core/src/components/shape/shape.tsx +++ b/packages/core/src/components/shape/shape.tsx @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' -import { usePosition, useShapeEvents } from '+hooks' -import type { IShapeTreeNode, TLBounds, TLShape, TLShapeUtil } from '+types' +import { useShapeEvents } from '+hooks' +import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types' import { RenderedShape } from './rendered-shape' import { EditingTextShape } from './editing-text-shape' import { Container } from '+components/container' -import { SVGContainer } from '+components/svg-container' // function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { // const transform = ` @@ -43,33 +42,31 @@ export const Shape = < bounds={bounds} rotation={shape.rotation} > - - {isEditing && utils.isEditableText ? ( - - ) : ( - - )} - + {isEditing && utils.isEditableText ? ( + + ) : ( + + )} ) } diff --git a/packages/core/src/components/svg-container/svg-container.tsx b/packages/core/src/components/svg-container/svg-container.tsx index 9d07203ef..3a7ac762b 100644 --- a/packages/core/src/components/svg-container/svg-container.tsx +++ b/packages/core/src/components/svg-container/svg-container.tsx @@ -1,13 +1,15 @@ import * as React from 'react' -interface SvgContainerProps { +interface SvgContainerProps extends React.SVGProps { children: React.ReactNode } -export const SVGContainer = React.memo(({ children }: SvgContainerProps) => { - return ( - - {children} - - ) -}) +export const SVGContainer = React.memo( + React.forwardRef(({ children, ...rest }, ref) => { + return ( + + {children} + + ) + }) +) diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index 33e4f462f..3036ab03a 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -151,7 +151,6 @@ const tlcss = css` .tl-positioned-svg { width: 100%; height: 100%; - pointer-events: none; } .tl-layer { diff --git a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx index cdf4a9c7d..c2ad77582 100644 --- a/packages/tldraw/src/shape/shapes/arrow/arrow.tsx +++ b/packages/tldraw/src/shape/shapes/arrow/arrow.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { + SVGContainer, TLBounds, Utils, Vec, @@ -20,10 +21,9 @@ import { DashStyle, TLDrawShape, ArrowBinding, - TLDrawRenderInfo, } from '~types' -export class Arrow extends TLDrawShapeUtil { +export class Arrow extends TLDrawShapeUtil { type = TLDrawShapeType.Arrow as const toolType = TLDrawToolType.Handle canStyleFill = false @@ -71,7 +71,7 @@ export class Arrow extends TLDrawShapeUtil { return next.handles !== prev.handles || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, meta, events }, ref) => { const { handles: { start, bend, end }, @@ -220,7 +220,7 @@ export class Arrow extends TLDrawShapeUtil { const sw = strokeWidth * 1.618 return ( - + {shaftPath} {startArrowHead && ( @@ -250,7 +250,7 @@ export class Arrow extends TLDrawShapeUtil { /> )} - + ) } ) diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index a0f51185a..c5149b85e 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -1,10 +1,18 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' +import { + SVGContainer, + TLBounds, + Utils, + Vec, + TLTransformInfo, + Intersect, + TLShapeProps, +} from '@tldraw/core' import getStroke, { getStrokePoints } from 'perfect-freehand' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types' -export class Draw extends TLDrawShapeUtil { +export class Draw extends TLDrawShapeUtil { type = TLDrawShapeType.Draw as const toolType = TLDrawToolType.Draw @@ -30,7 +38,7 @@ export class Draw extends TLDrawShapeUtil { return next.points !== prev.points || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, meta, events, isEditing }, ref) => { const { points, style } = shape @@ -47,7 +55,7 @@ export class Draw extends TLDrawShapeUtil { const sw = strokeWidth * 0.618 return ( - + { strokeWidth={sw} pointerEvents="all" /> - + ) } @@ -76,7 +84,7 @@ export class Draw extends TLDrawShapeUtil { : Utils.getFromCache(this.drawPathCache, points, () => getDrawStrokePath(shape, false)) return ( - + {shouldFill && ( { strokeLinecap="round" pointerEvents="all" /> - + ) } @@ -121,7 +129,7 @@ export class Draw extends TLDrawShapeUtil { const sw = strokeWidth * 1.618 return ( - + { strokeLinecap="round" pointerEvents="stroke" /> - + ) } ) diff --git a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx index 9217e630b..f10cb5605 100644 --- a/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx +++ b/packages/tldraw/src/shape/shapes/ellipse/ellipse.tsx @@ -1,10 +1,17 @@ import * as React from 'react' -import { Utils, TLTransformInfo, TLBounds, Intersect, TLShapeProps, Vec } from '@tldraw/core' +import { + SVGContainer, + Utils, + TLTransformInfo, + TLBounds, + Intersect, + TLShapeProps, + Vec, +} from '@tldraw/core' import { ArrowShape, DashStyle, EllipseShape, - TLDrawRenderInfo, TLDrawShapeType, TLDrawShapeUtil, TLDrawToolType, @@ -15,7 +22,7 @@ import getStroke from 'perfect-freehand' // TODO // [ ] Improve indicator shape for drawn shapes -export class Ellipse extends TLDrawShapeUtil { +export class Ellipse extends TLDrawShapeUtil { type = TLDrawShapeType.Ellipse as const toolType = TLDrawToolType.Bounds pathCache = new WeakMap([]) @@ -37,7 +44,7 @@ export class Ellipse extends TLDrawShapeUtil { return next.radius !== prev.radius || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, meta, isBinding, events }, ref) => { const { radius: [radiusX, radiusY], @@ -56,7 +63,7 @@ export class Ellipse extends TLDrawShapeUtil { ) return ( - + {isBinding && ( { strokeLinecap="round" strokeLinejoin="round" /> - + ) } @@ -102,7 +109,7 @@ export class Ellipse extends TLDrawShapeUtil { const sw = strokeWidth * 1.618 return ( - + {isBinding && ( { strokeLinecap="round" strokeLinejoin="round" /> - + ) } ) diff --git a/packages/tldraw/src/shape/shapes/group/group.tsx b/packages/tldraw/src/shape/shapes/group/group.tsx index b225e9cea..fc167d050 100644 --- a/packages/tldraw/src/shape/shapes/group/group.tsx +++ b/packages/tldraw/src/shape/shapes/group/group.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core' +import { SVGContainer, TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core' import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles' import { GroupShape, @@ -14,7 +14,7 @@ import { // TODO // [ ] - Find bounds based on common bounds of descendants -export class Group extends TLDrawShapeUtil { +export class Group extends TLDrawShapeUtil { type = TLDrawShapeType.Group as const toolType = TLDrawToolType.Bounds canBind = true @@ -38,7 +38,7 @@ export class Group extends TLDrawShapeUtil { return next.size !== prev.size || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, isBinding, isHovered, isSelected, events }, ref) => { const { id, size } = shape @@ -77,7 +77,7 @@ export class Group extends TLDrawShapeUtil { }) return ( - + {isBinding && ( { pointerEvents="all" /> {paths} - + ) } ) diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index d3c399162..eacec22ff 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -1,5 +1,13 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' +import { + TLBounds, + Utils, + Vec, + TLTransformInfo, + Intersect, + TLShapeProps, + SVGContainer, +} from '@tldraw/core' import getStroke from 'perfect-freehand' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' import { @@ -14,7 +22,7 @@ import { // TODO // [ ] - Make sure that fill does not extend drawn shape at corners -export class Rectangle extends TLDrawShapeUtil { +export class Rectangle extends TLDrawShapeUtil { type = TLDrawShapeType.Rectangle as const toolType = TLDrawToolType.Bounds canBind = true @@ -36,7 +44,7 @@ export class Rectangle extends TLDrawShapeUtil { return next.size !== prev.size || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, isBinding, meta, events }, ref) => { const { id, size, style } = shape const styles = getShapeStyle(style, meta.isDarkMode) @@ -46,7 +54,7 @@ export class Rectangle extends TLDrawShapeUtil { const pathData = Utils.getFromCache(this.pathCache, shape.size, () => renderPath(shape)) return ( - + {isBinding && ( { strokeWidth={styles.strokeWidth} pointerEvents="all" /> - + ) } diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index e6ac8a7fe..ad485fcc0 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -1,5 +1,13 @@ import * as React from 'react' -import { TLBounds, Utils, Vec, TLTransformInfo, Intersect, TLShapeProps } from '@tldraw/core' +import { + SVGContainer, + TLBounds, + Utils, + Vec, + TLTransformInfo, + Intersect, + TLShapeProps, +} from '@tldraw/core' import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles' import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types' import styled from '~styles' @@ -49,7 +57,7 @@ if (typeof window !== 'undefined') { melm = getMeasurementDiv() } -export class Text extends TLDrawShapeUtil { +export class Text extends TLDrawShapeUtil { type = TLDrawShapeType.Text as const toolType = TLDrawToolType.Text isAspectRatioLocked = true @@ -83,7 +91,7 @@ export class Text extends TLDrawShapeUtil { ) } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, meta, isEditing, isBinding, events }, ref) => { const rInput = React.useRef(null) const { id, text, style } = shape @@ -151,7 +159,7 @@ export class Text extends TLDrawShapeUtil { if (!isEditing) { return ( - + {isBinding && ( { {str} ))} - + ) } return ( - e.stopPropagation()} - > - - + + e.stopPropagation()} + > + + + ) } ) From f4e863148236e5b0bb8d790122342d14098b071a Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 17:23:38 +0100 Subject: [PATCH 05/14] Increase padding --- packages/core/src/hooks/useStyle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index 3036ab03a..3cec52f12 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -113,7 +113,7 @@ const tlcss = css` --tl-scale: calc(1 / var(--tl-zoom)); --tl-camera-x: 0px; --tl-camera-y: 0px; - --tl-padding: calc(32px * var(--tl-scale)); + --tl-padding: calc(64px * var(--tl-scale)); position: relative; box-sizing: border-box; width: 100%; From dea7d5c7d4c2a8ed05bf3b132e7d9c4a5ffe6070 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 18:07:53 +0100 Subject: [PATCH 06/14] Push a few more methods to the base shape utils class --- .../html-container/html-container.tsx | 15 ++ .../src/components/html-container/index.ts | 1 + packages/core/src/components/index.tsx | 1 + packages/core/src/hooks/useStyle.tsx | 7 + packages/core/src/types.ts | 53 +++- .../tldraw/src/components/tldraw/tldraw.tsx | 5 +- packages/tldraw/src/shape/shape-utils.tsx | 3 +- packages/tldraw/src/shape/shapes/index.ts | 1 + .../tldraw/src/shape/shapes/post-it/index.ts | 1 + .../src/shape/shapes/post-it/post-it.spec.tsx | 7 + .../src/shape/shapes/post-it/post-it.tsx | 250 ++++++++++++++++++ .../src/shape/shapes/rectangle/rectangle.tsx | 18 +- packages/tldraw/src/state/tlstate.ts | 12 +- packages/tldraw/src/types.ts | 8 + 14 files changed, 347 insertions(+), 35 deletions(-) create mode 100644 packages/core/src/components/html-container/html-container.tsx create mode 100644 packages/core/src/components/html-container/index.ts create mode 100644 packages/tldraw/src/shape/shapes/post-it/index.ts create mode 100644 packages/tldraw/src/shape/shapes/post-it/post-it.spec.tsx create mode 100644 packages/tldraw/src/shape/shapes/post-it/post-it.tsx diff --git a/packages/core/src/components/html-container/html-container.tsx b/packages/core/src/components/html-container/html-container.tsx new file mode 100644 index 000000000..1dbd576d8 --- /dev/null +++ b/packages/core/src/components/html-container/html-container.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +interface HTMLContainerProps extends React.HTMLProps { + children: React.ReactNode +} + +export const HTMLContainer = React.memo( + React.forwardRef(({ children, ...rest }, ref) => { + return ( +
+ {children} +
+ ) + }) +) diff --git a/packages/core/src/components/html-container/index.ts b/packages/core/src/components/html-container/index.ts new file mode 100644 index 000000000..c87e8e9b0 --- /dev/null +++ b/packages/core/src/components/html-container/index.ts @@ -0,0 +1 @@ +export * from './html-container' diff --git a/packages/core/src/components/index.tsx b/packages/core/src/components/index.tsx index 966c00473..8ede353af 100644 --- a/packages/core/src/components/index.tsx +++ b/packages/core/src/components/index.tsx @@ -1,3 +1,4 @@ export * from './renderer' export { brushUpdater } from './brush' export * from './svg-container' +export * from './html-container' diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index 3cec52f12..cb22bc4ea 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -153,6 +153,13 @@ const tlcss = css` height: 100%; } + .tl-positioned-div { + position: relative; + width: 100%; + height: 100%; + padding: var(--tl-padding); + } + .tl-layer { transform: scale(var(--tl-zoom), var(--tl-zoom)) translate(var(--tl-camera-x), var(--tl-camera-y)); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 153268c98..2b72e9d4c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* --------------------- Primary -------------------- */ +import { Intersect, Vec } from '+utils' import React, { ForwardedRef } from 'react' export type Patch = Partial<{ [P in keyof T]: T | Partial | Patch }> @@ -304,16 +305,6 @@ export abstract class TLShapeUtil): Partial - - transformSingle(shape: T, bounds: TLBounds, info: TLTransformInfo): Partial { - return this.transform(shape, bounds, info) - } - shouldRender(_prev: T, _next: T): boolean { return true } @@ -356,6 +347,14 @@ export abstract class TLShapeUtil): Partial | void { + return undefined + } + + transformSingle(shape: T, bounds: TLBounds, info: TLTransformInfo): Partial | void { + return this.transform(shape, bounds, info) + } + updateChildren(shape: T, children: K[]): Partial[] | void { return } @@ -409,6 +408,40 @@ export abstract class TLShapeUtil | void { return } + + hitTest(shape: T, point: number[]) { + const bounds = this.getBounds(shape) + return !( + point[0] < bounds.minX || + point[0] > bounds.maxX || + point[1] < bounds.minY || + point[1] > bounds.maxY + ) + } + + hitTestBounds(shape: T, bounds: TLBounds) { + const { minX, minY, maxX, maxY, width, height } = this.getBounds(shape) + const center = [minX + width / 2, minY + height / 2] + + const corners = [ + [minX, minY], + [maxX, minY], + [maxX, maxY], + [minX, maxY], + ].map((point) => Vec.rotWith(point, center, shape.rotation || 0)) + + return ( + corners.every( + (point) => + !( + point[0] < bounds.minX || + point[0] > bounds.maxX || + point[1] < bounds.minY || + point[1] > bounds.maxY + ) + ) || Intersect.polyline.bounds(corners, bounds).length > 0 + ) + } } /* -------------------- Internal -------------------- */ diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/components/tldraw/tldraw.tsx index 8709a13c5..d7b267f8f 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { IdProvider } from '@radix-ui/react-id' import { Renderer } from '@tldraw/core' import styled from '~styles' -import type { Data, TLDrawDocument } from '~types' +import { Data, TLDrawDocument, TLDrawStatus, TLDrawToolType } from '~types' import { TLDrawState } from '~state' import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks' import { tldrawShapeUtils } from '~shape' @@ -109,7 +109,8 @@ function InnerTldraw({ const hideHandles = isInSession || !isSelecting // Hide indicators when not using the select tool, or when in session - const hideIndicators = isInSession || !isSelecting + const hideIndicators = + (isInSession && tlstate.appState.status.current !== TLDrawStatus.Brushing) || !isSelecting // Custom rendering meta, with dark mode for shapes const meta = React.useMemo(() => ({ isDarkMode }), [isDarkMode]) diff --git a/packages/tldraw/src/shape/shape-utils.tsx b/packages/tldraw/src/shape/shape-utils.tsx index ee775a5a9..1f30a9c72 100644 --- a/packages/tldraw/src/shape/shape-utils.tsx +++ b/packages/tldraw/src/shape/shape-utils.tsx @@ -1,4 +1,4 @@ -import { Rectangle, Ellipse, Arrow, Draw, Text, Group } from './shapes' +import { Rectangle, Ellipse, Arrow, Draw, Text, Group, PostIt } from './shapes' import { TLDrawShapeType, TLDrawShape, TLDrawShapeUtil, TLDrawShapeUtils } from '~types' export const tldrawShapeUtils: TLDrawShapeUtils = { @@ -8,6 +8,7 @@ export const tldrawShapeUtils: TLDrawShapeUtils = { [TLDrawShapeType.Arrow]: new Arrow(), [TLDrawShapeType.Text]: new Text(), [TLDrawShapeType.Group]: new Group(), + [TLDrawShapeType.PostIt]: new PostIt(), } as TLDrawShapeUtils export type ShapeByType = TLDrawShapeUtils[T] diff --git a/packages/tldraw/src/shape/shapes/index.ts b/packages/tldraw/src/shape/shapes/index.ts index 559540f9a..7430dd01e 100644 --- a/packages/tldraw/src/shape/shapes/index.ts +++ b/packages/tldraw/src/shape/shapes/index.ts @@ -4,3 +4,4 @@ export * from './rectangle' export * from './ellipse' export * from './text' export * from './group' +export * from './post-it' diff --git a/packages/tldraw/src/shape/shapes/post-it/index.ts b/packages/tldraw/src/shape/shapes/post-it/index.ts new file mode 100644 index 000000000..bcc5c46a8 --- /dev/null +++ b/packages/tldraw/src/shape/shapes/post-it/index.ts @@ -0,0 +1 @@ +export * from './post-it' diff --git a/packages/tldraw/src/shape/shapes/post-it/post-it.spec.tsx b/packages/tldraw/src/shape/shapes/post-it/post-it.spec.tsx new file mode 100644 index 000000000..50cd911bc --- /dev/null +++ b/packages/tldraw/src/shape/shapes/post-it/post-it.spec.tsx @@ -0,0 +1,7 @@ +import { PostIt } from './post-it' + +describe('Post-It shape', () => { + it('Creates an instance', () => { + new PostIt() + }) +}) diff --git a/packages/tldraw/src/shape/shapes/post-it/post-it.tsx b/packages/tldraw/src/shape/shapes/post-it/post-it.tsx new file mode 100644 index 000000000..28656989a --- /dev/null +++ b/packages/tldraw/src/shape/shapes/post-it/post-it.tsx @@ -0,0 +1,250 @@ +import * as React from 'react' +import { + TLBounds, + Utils, + Vec, + TLTransformInfo, + Intersect, + TLShapeProps, + HTMLContainer, +} from '@tldraw/core' +import { defaultStyle, getShapeStyle } from '~shape/shape-styles' +import { PostItShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types' + +// TODO +// [ ] - Make sure that fill does not extend drawn shape at corners + +export class PostIt extends TLDrawShapeUtil { + type = TLDrawShapeType.PostIt as const + toolType = TLDrawToolType.Bounds + canBind = true + pathCache = new WeakMap([]) + + defaultProps: PostItShape = { + id: 'id', + type: TLDrawShapeType.PostIt as const, + name: 'PostIt', + parentId: 'page', + childIndex: 1, + point: [0, 0], + size: [1, 1], + text: '', + rotation: 0, + style: defaultStyle, + } + + shouldRender(prev: PostItShape, next: PostItShape) { + return next.size !== prev.size || next.style !== prev.style + } + + render = React.forwardRef>( + ({ shape, isBinding, meta, events }, ref) => { + const [count, setCount] = React.useState(0) + + return ( + +
+
e.preventDefault()}> + e.stopPropagation()} + /> + +
+
+
+ ) + } + ) + + renderIndicator(shape: PostItShape) { + const { + style, + size: [width, height], + } = shape + + const styles = getShapeStyle(style, false) + const strokeWidth = +styles.strokeWidth + + const sw = strokeWidth + + return ( + + ) + } + + getBounds(shape: PostItShape) { + const bounds = Utils.getFromCache(this.boundsCache, shape, () => { + const [width, height] = shape.size + return { + minX: 0, + maxX: width, + minY: 0, + maxY: height, + width, + height, + } + }) + + return Utils.translateBounds(bounds, shape.point) + } + + getRotatedBounds(shape: PostItShape) { + return Utils.getBoundsFromPoints(Utils.getRotatedCorners(this.getBounds(shape), shape.rotation)) + } + + getCenter(shape: PostItShape): number[] { + return Utils.getBoundsCenter(this.getBounds(shape)) + } + + getBindingPoint( + shape: PostItShape, + fromShape: ArrowShape, + point: number[], + origin: number[], + direction: number[], + padding: number, + anywhere: boolean + ) { + const bounds = this.getBounds(shape) + + const expandedBounds = Utils.expandBounds(bounds, padding) + + let bindingPoint: number[] + let distance: number + + // The point must be inside of the expanded bounding box + if (!Utils.pointInBounds(point, expandedBounds)) return + + // The point is inside of the shape, so we'll assume the user is + // indicating a specific point inside of the shape. + if (anywhere) { + if (Vec.dist(point, this.getCenter(shape)) < 12) { + bindingPoint = [0.5, 0.5] + } else { + bindingPoint = Vec.divV(Vec.sub(point, [expandedBounds.minX, expandedBounds.minY]), [ + expandedBounds.width, + expandedBounds.height, + ]) + } + + distance = 0 + } else { + // TODO: What if the shape has a curve? In that case, should we + // intersect the circle-from-three-points instead? + + // Find furthest intersection between ray from + // origin through point and expanded bounds. + + // TODO: Make this a ray vs rounded rect intersection + const intersection = Intersect.ray + .bounds(origin, direction, expandedBounds) + .filter((int) => int.didIntersect) + .map((int) => int.points[0]) + .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0] + // The anchor is a point between the handle and the intersection + const anchor = Vec.med(point, intersection) + + // If we're close to the center, snap to the center + if (Vec.distanceToLineSegment(point, anchor, this.getCenter(shape)) < 12) { + bindingPoint = [0.5, 0.5] + } else { + // Or else calculate a normalized point + bindingPoint = Vec.divV(Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [ + expandedBounds.width, + expandedBounds.height, + ]) + } + + if (Utils.pointInBounds(point, bounds)) { + distance = 16 + } else { + // If the binding point was close to the shape's center, snap to the center + // Find the distance between the point and the real bounds of the shape + distance = Math.max( + 16, + Utils.getBoundsSides(bounds) + .map((side) => Vec.distanceToLineSegment(side[1][0], side[1][1], point)) + .sort((a, b) => a - b)[0] + ) + } + } + + return { + point: Vec.clampV(bindingPoint, 0, 1), + distance, + } + } + + hitTestBounds(shape: PostItShape, bounds: TLBounds) { + const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation) + + return ( + rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || + Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 + ) + } + + transform( + shape: PostItShape, + bounds: TLBounds, + { initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo + ) { + if (!shape.rotation && !shape.isAspectRatioLocked) { + return { + point: Vec.round([bounds.minX, bounds.minY]), + size: Vec.round([bounds.width, bounds.height]), + } + } else { + const size = Vec.round( + Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY))) + ) + + const point = Vec.round([ + bounds.minX + + (bounds.width - shape.size[0]) * + (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]), + bounds.minY + + (bounds.height - shape.size[1]) * + (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]), + ]) + + const rotation = + (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0) + ? initialShape.rotation + ? -initialShape.rotation + : 0 + : initialShape.rotation + + return { + size, + point, + rotation, + } + } + } + + transformSingle(_shape: PostItShape, bounds: TLBounds) { + return { + size: Vec.round([bounds.width, bounds.height]), + point: Vec.round([bounds.minX, bounds.minY]), + } + } +} diff --git a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx index eacec22ff..4b55df950 100644 --- a/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx +++ b/packages/tldraw/src/shape/shapes/rectangle/rectangle.tsx @@ -7,6 +7,7 @@ import { Intersect, TLShapeProps, SVGContainer, + HTMLContainer, } from '@tldraw/core' import getStroke from 'perfect-freehand' import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles' @@ -120,7 +121,7 @@ export class Rectangle extends TLDrawShapeUtil { }) return ( - + {isBinding && ( { pointerEvents="all" /> {paths} - + ) } ) @@ -272,19 +273,6 @@ export class Rectangle extends TLDrawShapeUtil { } } - hitTest(shape: RectangleShape, point: number[]) { - return Utils.pointInBounds(point, this.getBounds(shape)) - } - - hitTestBounds(shape: RectangleShape, bounds: TLBounds) { - const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation) - - return ( - rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) || - Intersect.polyline.bounds(rotatedCorners, bounds).length > 0 - ) - } - transform( shape: RectangleShape, bounds: TLBounds, diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 5b1651002..4ce5ba894 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -2394,6 +2394,8 @@ export class TLDrawState extends StateManager { } // Start a brush session + // TODO: Don't start a brush session right away: we might + // be "maybe brushing" or "maybe double clicking" this.startBrushSession(this.getPagePoint(info.point)) break } @@ -2407,13 +2409,9 @@ export class TLDrawState extends StateManager { // Unused switch (this.appState.status.current) { case TLDrawStatus.Idle: { - switch (this.appState.activeTool) { - case TLDrawShapeType.Text: { - // Create a text shape - this.createActiveToolShape(info.point) - break - } - } + // TODO: Create a text shape + // this.selectTool(TLDrawShapeType.Text) + // this.createActiveToolShape(info.point) break } } diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index 03ea0d9ef..0cae8619c 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -134,6 +134,7 @@ export enum TLDrawToolType { } export enum TLDrawShapeType { + PostIt = 'post-it', Ellipse = 'ellipse', Rectangle = 'rectangle', Draw = 'draw', @@ -191,6 +192,12 @@ export interface GroupShape extends TLDrawBaseShape { children: string[] } +export interface PostItShape extends TLDrawBaseShape { + type: TLDrawShapeType.PostIt + size: number[] + text: string +} + export type TLDrawShape = | RectangleShape | EllipseShape @@ -198,6 +205,7 @@ export type TLDrawShape = | ArrowShape | TextShape | GroupShape + | PostItShape export abstract class TLDrawShapeUtil< T extends TLDrawShape, From c8c3ebce6845e68fa6277ed25413e90dcbc7de88 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 20:02:16 +0100 Subject: [PATCH 07/14] Fix bounds edge handles --- packages/core/src/components/bounds/bounds.tsx | 2 +- packages/core/src/components/bounds/edge-handle.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/src/components/bounds/bounds.tsx b/packages/core/src/components/bounds/bounds.tsx index 6f56a85a4..bc098a6c2 100644 --- a/packages/core/src/components/bounds/bounds.tsx +++ b/packages/core/src/components/bounds/bounds.tsx @@ -49,7 +49,7 @@ export function Bounds({ const size = 8 / zoom // Touch target size return ( - + {!isLocked && ( diff --git a/packages/core/src/components/bounds/edge-handle.tsx b/packages/core/src/components/bounds/edge-handle.tsx index 596c709dc..9928de15e 100644 --- a/packages/core/src/components/bounds/edge-handle.tsx +++ b/packages/core/src/components/bounds/edge-handle.tsx @@ -3,10 +3,10 @@ import { useBoundsHandleEvents } from '+hooks' import { TLBoundsEdge, TLBounds } from '+types' const edgeClassnames = { - [TLBoundsEdge.Top]: 'tl-transparent tl-cursor-ns', - [TLBoundsEdge.Right]: 'tl-transparent tl-cursor-ew', - [TLBoundsEdge.Bottom]: 'tl-transparent tl-cursor-ns', - [TLBoundsEdge.Left]: 'tl-transparent tl-cursor-ew', + [TLBoundsEdge.Top]: 'tl-transparent tl-edge-handle tl-cursor-ns', + [TLBoundsEdge.Right]: 'tl-transparent tl-edge-handle tl-cursor-ew', + [TLBoundsEdge.Bottom]: 'tl-transparent tl-edge-handle tl-cursor-ns', + [TLBoundsEdge.Left]: 'tl-transparent tl-edge-handle tl-cursor-ew', } interface EdgeHandleProps { @@ -26,6 +26,7 @@ export const EdgeHandle = React.memo(({ size, bounds, edge }: EdgeHandleProps): return ( Date: Sat, 11 Sep 2021 23:17:54 +0100 Subject: [PATCH 08/14] Fix text scrolling --- .../core/src/components/canvas/canvas.tsx | 13 +- packages/core/src/components/page/page.tsx | 11 +- .../core/src/components/renderer/renderer.tsx | 14 +- .../components/shape/editing-text-shape.tsx | 53 --- .../src/components/shape/rendered-shape.tsx | 26 +- .../core/src/components/shape/shape-node.tsx | 2 +- .../core/src/components/shape/shape.test.tsx | 2 +- packages/core/src/components/shape/shape.tsx | 47 +-- packages/core/src/hooks/index.ts | 1 - packages/core/src/hooks/useRenderOnResize.tsx | 14 - packages/core/src/hooks/useResizeObserver.ts | 2 +- .../core/src/hooks/useSafariFocusOutFix.tsx | 2 +- packages/core/src/hooks/useSelection.tsx | 2 +- packages/core/src/hooks/useShapeTree.tsx | 6 +- packages/core/src/hooks/useStyle.tsx | 44 ++- packages/core/src/hooks/useTLContext.tsx | 8 +- packages/core/src/hooks/useZoomEvents.ts | 2 +- packages/core/src/types.ts | 35 +- .../tldraw/src/components/tldraw/tldraw.tsx | 21 +- .../tldraw/src/shape/shapes/draw/draw.tsx | 21 +- .../tldraw/src/shape/shapes/text/text.tsx | 316 ++++++++---------- .../session/sessions/text/text.session.ts | 20 +- packages/tldraw/src/state/tlstate.ts | 50 ++- packages/tldraw/src/types.ts | 9 +- 24 files changed, 305 insertions(+), 416 deletions(-) delete mode 100644 packages/core/src/components/shape/editing-text-shape.tsx delete mode 100644 packages/core/src/hooks/useRenderOnResize.tsx diff --git a/packages/core/src/components/canvas/canvas.tsx b/packages/core/src/components/canvas/canvas.tsx index 4a9d30dc0..8d661ac63 100644 --- a/packages/core/src/components/canvas/canvas.tsx +++ b/packages/core/src/components/canvas/canvas.tsx @@ -51,11 +51,20 @@ export function Canvas>({ const rLayer = useCameraCss(rContainer, pageState) + const preventScrolling = React.useCallback((e: React.UIEvent) => { + e.currentTarget.scrollTo(0, 0) + }, []) + return (
-
+
- {/* */}
>({ }: PageProps): JSX.Element { const { callbacks, shapeUtils, inputs } = useTLContext() - const shapeTree = useShapeTree(page, pageState, shapeUtils, inputs.size, meta, callbacks.onChange) + const shapeTree = useShapeTree( + page, + pageState, + shapeUtils, + inputs.size, + meta, + callbacks.onRenderCountChange + ) const { shapeWithHandles } = useHandles(page, pageState) diff --git a/packages/core/src/components/renderer/renderer.tsx b/packages/core/src/components/renderer/renderer.tsx index 148ac52e3..8d005e572 100644 --- a/packages/core/src/components/renderer/renderer.tsx +++ b/packages/core/src/components/renderer/renderer.tsx @@ -15,9 +15,9 @@ import { useTLTheme, TLContext, TLContextType } from '../../hooks' export interface RendererProps< T extends TLShape, - E extends HTMLElement | SVGElement, + E extends Element, M extends Record -> extends Partial { +> extends Partial> { /** * An object containing instances of your shape classes. */ @@ -66,11 +66,7 @@ export interface RendererProps< * @param props * @returns */ -export function Renderer< - T extends TLShape, - E extends SVGElement | HTMLElement, - M extends Record ->({ +export function Renderer>({ shapeUtils, page, pageState, @@ -91,6 +87,8 @@ export function Renderer< rPageState.current = pageState }, [pageState]) + rest + const [context] = React.useState>(() => ({ callbacks: rest, shapeUtils, @@ -100,7 +98,7 @@ export function Renderer< })) return ( - }> + }> ->({ - shape, - utils, - isEditing, - isBinding, - isHovered, - isSelected, - isCurrentParent, - events, - meta, -}: TLRenderInfo & { - shape: T - utils: TLShapeUtil -}) { - const { - callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp }, - } = useTLContext() - - const ref = utils.getRef(shape) - - React.useEffect(() => { - // Firefox fix? - setTimeout(() => { - if (document.activeElement !== ref.current) { - ref.current?.focus() - } - }, 0) - }, [shape.id]) - - return ( - - ) -} diff --git a/packages/core/src/components/shape/rendered-shape.tsx b/packages/core/src/components/shape/rendered-shape.tsx index 7aeccdc78..762467c36 100644 --- a/packages/core/src/components/shape/rendered-shape.tsx +++ b/packages/core/src/components/shape/rendered-shape.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types' export const RenderedShape = React.memo( - >({ + >({ shape, utils, isEditing, @@ -11,9 +11,11 @@ export const RenderedShape = React.memo( isHovered, isSelected, isCurrentParent, + onShapeChange, + onShapeBlur, events, meta, - }: TLRenderInfo & { + }: TLRenderInfo & { shape: T utils: TLShapeUtil }) => { @@ -22,16 +24,16 @@ export const RenderedShape = React.memo( return ( ) }, diff --git a/packages/core/src/components/shape/shape-node.tsx b/packages/core/src/components/shape/shape-node.tsx index 17250f4e5..f3a134725 100644 --- a/packages/core/src/components/shape/shape-node.tsx +++ b/packages/core/src/components/shape/shape-node.tsx @@ -13,7 +13,7 @@ export const ShapeNode = React.memo( isSelected, isCurrentParent, meta, - }: { utils: TLShapeUtils } & IShapeTreeNode) => { + }: { utils: TLShapeUtils } & IShapeTreeNode) => { return ( <> { }) }) -// { shape: TLShape; ref: ForwardedRef; } & TLRenderInfo & RefAttributes +// { shape: TLShape; ref: ForwardedRef; } & TLRenderInfo & RefAttributes // { shape: BoxShape; ref: ForwardedRef; } & TLRenderInfo & RefAttributes' diff --git a/packages/core/src/components/shape/shape.tsx b/packages/core/src/components/shape/shape.tsx index 28290a8ce..c98496014 100644 --- a/packages/core/src/components/shape/shape.tsx +++ b/packages/core/src/components/shape/shape.tsx @@ -1,10 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' import { useShapeEvents } from '+hooks' import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types' import { RenderedShape } from './rendered-shape' -import { EditingTextShape } from './editing-text-shape' import { Container } from '+components/container' +import { useTLContext } from '+hooks' // function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { // const transform = ` @@ -16,11 +17,7 @@ import { Container } from '+components/container' // elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) // } -export const Shape = < - T extends TLShape, - E extends SVGElement | HTMLElement, - M extends Record ->({ +export const Shape = >({ shape, utils, isEditing, @@ -32,6 +29,7 @@ export const Shape = < }: IShapeTreeNode & { utils: TLShapeUtil }) => { + const { callbacks } = useTLContext() const bounds = utils.getBounds(shape) const events = useShapeEvents(shape.id, isCurrentParent) @@ -42,31 +40,18 @@ export const Shape = < bounds={bounds} rotation={shape.rotation} > - {isEditing && utils.isEditableText ? ( - - ) : ( - - )} + ) } diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index fc340110d..225afd5a0 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -8,7 +8,6 @@ export * from './useStyle' export * from './useCanvasEvents' export * from './useBoundsHandleEvents' export * from './useCameraCss' -export * from './useRenderOnResize' export * from './useSelection' export * from './useHandleEvents' export * from './useHandles' diff --git a/packages/core/src/hooks/useRenderOnResize.tsx b/packages/core/src/hooks/useRenderOnResize.tsx deleted file mode 100644 index 586971b7f..000000000 --- a/packages/core/src/hooks/useRenderOnResize.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react' -import Utils from '+utils' - -export function useRenderOnResize() { - const forceUpdate = React.useReducer((x) => x + 1, 0)[1] - - React.useEffect(() => { - const debouncedUpdate = Utils.debounce(forceUpdate, 96) - window.addEventListener('resize', debouncedUpdate) - return () => { - window.removeEventListener('resize', debouncedUpdate) - } - }, [forceUpdate]) -} diff --git a/packages/core/src/hooks/useResizeObserver.ts b/packages/core/src/hooks/useResizeObserver.ts index 292c6a0bc..55df48bdd 100644 --- a/packages/core/src/hooks/useResizeObserver.ts +++ b/packages/core/src/hooks/useResizeObserver.ts @@ -2,7 +2,7 @@ import { useTLContext } from '+hooks' import * as React from 'react' import { Utils } from '+utils' -export function useResizeObserver(ref: React.RefObject) { +export function useResizeObserver(ref: React.RefObject) { const { inputs } = useTLContext() const rIsMounted = React.useRef(false) const forceUpdate = React.useReducer((x) => x + 1, 0)[1] diff --git a/packages/core/src/hooks/useSafariFocusOutFix.tsx b/packages/core/src/hooks/useSafariFocusOutFix.tsx index 580f8f92b..182ef844a 100644 --- a/packages/core/src/hooks/useSafariFocusOutFix.tsx +++ b/packages/core/src/hooks/useSafariFocusOutFix.tsx @@ -9,7 +9,7 @@ export function useSafariFocusOutFix(): void { useEffect(() => { function handleFocusOut() { - callbacks.onBlurEditingShape?.() + callbacks.onShapeBlur?.() } if (Utils.isMobileSafari()) { diff --git a/packages/core/src/hooks/useSelection.tsx b/packages/core/src/hooks/useSelection.tsx index 73d1c39a8..78beaa81d 100644 --- a/packages/core/src/hooks/useSelection.tsx +++ b/packages/core/src/hooks/useSelection.tsx @@ -6,7 +6,7 @@ function canvasToScreen(point: number[], camera: TLPageState['camera']): number[ return [(point[0] + camera.point[0]) * camera.zoom, (point[1] + camera.point[1]) * camera.zoom] } -export function useSelection( +export function useSelection( page: TLPage, pageState: TLPageState, shapeUtils: TLShapeUtils diff --git a/packages/core/src/hooks/useShapeTree.tsx b/packages/core/src/hooks/useShapeTree.tsx index 78d90f6e5..155f31ad5 100644 --- a/packages/core/src/hooks/useShapeTree.tsx +++ b/packages/core/src/hooks/useShapeTree.tsx @@ -61,7 +61,7 @@ function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) { export function useShapeTree< T extends TLShape, - E extends SVGElement | HTMLElement, + E extends Element, M extends Record >( page: TLPage, @@ -69,7 +69,7 @@ export function useShapeTree< shapeUtils: TLShapeUtils, size: number[], meta?: M, - onChange?: TLCallbacks['onChange'] + onRenderCountChange?: TLCallbacks['onRenderCountChange'] ) { const rTimeout = React.useRef() const rPreviousCount = React.useRef(0) @@ -128,7 +128,7 @@ export function useShapeTree< clearTimeout(rTimeout.current as number) } rTimeout.current = setTimeout(() => { - onChange?.(Array.from(shapesIdsToRender.values())) + onRenderCountChange?.(Array.from(shapesIdsToRender.values())) }, 100) rPreviousCount.current = shapesToRender.size } diff --git a/packages/core/src/hooks/useStyle.tsx b/packages/core/src/hooks/useStyle.tsx index cb22bc4ea..992643b59 100644 --- a/packages/core/src/hooks/useStyle.tsx +++ b/packages/core/src/hooks/useStyle.tsx @@ -115,11 +115,16 @@ const tlcss = css` --tl-camera-y: 0px; --tl-padding: calc(64px * var(--tl-scale)); position: relative; - box-sizing: border-box; + top: 0px; + left: 0px; width: 100%; height: 100%; + max-width: 100%; + max-height: 100%; + box-sizing: border-box; padding: 0px; margin: 0px; + z-index: 100; touch-action: none; overscroll-behavior: none; background-color: var(--tl-background); @@ -130,6 +135,25 @@ const tlcss = css` box-sizing: border-box; } + .tl-canvas { + position: absolute; + overflow: hidden; + width: 100%; + height: 100%; + touch-action: none; + pointer-events: all; + } + + .tl-layer { + position: absolute; + top: 0; + left: 0; + height: 0; + width: 0; + transform: scale(var(--tl-zoom), var(--tl-zoom)) + translate(var(--tl-camera-x), var(--tl-camera-y)); + } + .tl-absolute { position: absolute; top: 0px; @@ -141,6 +165,7 @@ const tlcss = css` position: absolute; top: 0px; left: 0px; + overflow: hidden; transform-origin: center center; pointer-events: none; display: flex; @@ -151,22 +176,17 @@ const tlcss = css` .tl-positioned-svg { width: 100%; height: 100%; + overflow: hidden; } .tl-positioned-div { position: relative; width: 100%; height: 100%; + overflow: hidden; padding: var(--tl-padding); } - .tl-layer { - transform: scale(var(--tl-zoom), var(--tl-zoom)) - translate(var(--tl-camera-x), var(--tl-camera-y)); - height: 0; - width: 0; - } - .tl-counter-scaled { transform: scale(var(--tl-scale)); } @@ -253,14 +273,6 @@ const tlcss = css` pointer-events: none; } - .tl-canvas { - overflow: hidden; - width: 100%; - height: 100%; - touch-action: none; - pointer-events: all; - } - .tl-dot { fill: var(--tl-background); stroke: var(--tl-foreground); diff --git a/packages/core/src/hooks/useTLContext.tsx b/packages/core/src/hooks/useTLContext.tsx index 375479f18..c25907871 100644 --- a/packages/core/src/hooks/useTLContext.tsx +++ b/packages/core/src/hooks/useTLContext.tsx @@ -2,18 +2,16 @@ import * as React from 'react' import type { Inputs } from '+inputs' import type { TLCallbacks, TLShape, TLBounds, TLPageState, TLShapeUtils } from '+types' -export interface TLContextType { +export interface TLContextType { id?: string - callbacks: Partial + callbacks: Partial> shapeUtils: TLShapeUtils rPageState: React.MutableRefObject rScreenBounds: React.MutableRefObject inputs: Inputs } -export const TLContext = React.createContext>( - {} as TLContextType -) +export const TLContext = React.createContext({} as TLContextType) export function useTLContext() { const context = React.useContext(TLContext) diff --git a/packages/core/src/hooks/useZoomEvents.ts b/packages/core/src/hooks/useZoomEvents.ts index 60895911e..98c40f9d8 100644 --- a/packages/core/src/hooks/useZoomEvents.ts +++ b/packages/core/src/hooks/useZoomEvents.ts @@ -6,7 +6,7 @@ import Utils, { Vec } from '+utils' import { useGesture } from '@use-gesture/react' // Capture zoom gestures (pinches, wheels and pans) -export function useZoomEvents(ref: React.RefObject) { +export function useZoomEvents(ref: React.RefObject) { const rOriginPoint = React.useRef(undefined) const rPinchPoint = React.useRef(undefined) const rDelta = React.useRef([0, 0]) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2b72e9d4c..79efd67cf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -57,33 +57,27 @@ export interface TLShape { isAspectRatioLocked?: boolean } -export type TLShapeUtils = Record< - string, - TLShapeUtil -> +export type TLShapeUtils = Record> -export interface TLRenderInfo { +export interface TLRenderInfo { isEditing: boolean isBinding: boolean isHovered: boolean isSelected: boolean isCurrentParent: boolean meta: M extends any ? M : never + onShapeChange?: TLCallbacks['onShapeChange'] + onShapeBlur?: TLCallbacks['onShapeBlur'] events: { onPointerDown: (e: React.PointerEvent) => void onPointerUp: (e: React.PointerEvent) => void onPointerEnter: (e: React.PointerEvent) => void onPointerMove: (e: React.PointerEvent) => void onPointerLeave: (e: React.PointerEvent) => void - onTextChange?: TLCallbacks['onTextChange'] - onTextBlur?: TLCallbacks['onTextBlur'] - onTextFocus?: TLCallbacks['onTextFocus'] - onTextKeyDown?: TLCallbacks['onTextKeyDown'] - onTextKeyUp?: TLCallbacks['onTextKeyUp'] } } -export interface TLShapeProps extends TLRenderInfo { +export interface TLShapeProps extends TLRenderInfo { ref: ForwardedRef shape: T } @@ -131,9 +125,7 @@ export type TLBoundsHandleEventHandler = ( e: React.PointerEvent ) => void -export interface TLCallbacks { - onChange: (ids: string[]) => void - +export interface TLCallbacks { // Camera events onPinchStart: TLPinchEventHandler onPinchEnd: TLPinchEventHandler @@ -189,15 +181,10 @@ export interface TLCallbacks { onUnhoverHandle: TLPointerEventHandler onReleaseHandle: TLPointerEventHandler - // Text - onTextChange: (id: string, text: string) => void - onTextBlur: (id: string) => void - onTextFocus: (id: string) => void - onTextKeyDown: (id: string, key: string) => void - onTextKeyUp: (id: string, key: string) => void - // Misc - onBlurEditingShape: () => void + onRenderCountChange: (ids: string[]) => void + onShapeChange: (shape: { id: string } & Partial) => void + onShapeBlur: () => void onError: (error: Error) => void } @@ -278,7 +265,7 @@ export interface TLBezierCurveSegment { /* Shape Utility */ /* -------------------------------------------------- */ -export abstract class TLShapeUtil { +export abstract class TLShapeUtil { refMap = new Map>() boundsCache = new WeakMap() @@ -296,7 +283,7 @@ export abstract class TLShapeUtil } & TLRenderInfo & React.RefAttributes + { shape: T; ref: React.ForwardedRef } & TLRenderInfo & React.RefAttributes > abstract renderIndicator(shape: T): JSX.Element | null diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/components/tldraw/tldraw.tsx index d7b267f8f..b863803f9 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { IdProvider } from '@radix-ui/react-id' import { Renderer } from '@tldraw/core' import styled from '~styles' -import { Data, TLDrawDocument, TLDrawStatus, TLDrawToolType } from '~types' +import { Data, TLDrawDocument, TLDrawStatus } from '~types' import { TLDrawState } from '~state' import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks' import { tldrawShapeUtils } from '~shape' @@ -198,14 +198,10 @@ function InnerTldraw({ onHoverHandle={tlstate.onHoverHandle} onUnhoverHandle={tlstate.onUnhoverHandle} onReleaseHandle={tlstate.onReleaseHandle} - onChange={tlstate.onChange} onError={tlstate.onError} - onBlurEditingShape={tlstate.onBlurEditingShape} - onTextBlur={tlstate.onTextBlur} - onTextChange={tlstate.onTextChange} - onTextKeyDown={tlstate.onTextKeyDown} - onTextFocus={tlstate.onTextFocus} - onTextKeyUp={tlstate.onTextKeyUp} + onRenderCountChange={tlstate.onRenderCountChange} + onShapeChange={tlstate.onShapeChange} + onShapeBlur={tlstate.onShapeBlur} /> @@ -220,10 +216,14 @@ function InnerTldraw({ } const Layout = styled('div', { - overflow: 'hidden', position: 'absolute', height: '100%', width: '100%', + minHeight: 0, + minWidth: 0, + maxHeight: '100%', + maxWidth: '100%', + overflow: 'hidden', padding: '8px 8px 0 8px', display: 'flex', alignItems: 'flex-start', @@ -241,9 +241,6 @@ const Layout = styled('div', { position: 'absolute', top: 0, left: 0, - width: '100%', - height: '100%', - zIndex: 100, }, }) diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index c5149b85e..2cf4fd113 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -1,16 +1,15 @@ import * as React from 'react' -import { - SVGContainer, - TLBounds, - Utils, - Vec, - TLTransformInfo, - Intersect, - TLShapeProps, -} from '@tldraw/core' +import { SVGContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' import getStroke, { getStrokePoints } from 'perfect-freehand' import { defaultStyle, getShapeStyle } from '~shape/shape-styles' -import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types' +import { + DrawShape, + DashStyle, + TLDrawShapeUtil, + TLDrawShapeType, + TLDrawToolType, + TLDrawShapeProps, +} from '~types' export class Draw extends TLDrawShapeUtil { type = TLDrawShapeType.Draw as const @@ -38,7 +37,7 @@ export class Draw extends TLDrawShapeUtil { return next.points !== prev.points || next.style !== prev.style } - render = React.forwardRef>( + render = React.forwardRef>( ({ shape, meta, events, isEditing }, ref) => { const { points, style } = shape diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index ad485fcc0..cebb27f64 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -1,15 +1,15 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as React from 'react' +import { HTMLContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core' +import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles' import { - SVGContainer, - TLBounds, - Utils, - Vec, - TLTransformInfo, - Intersect, - TLShapeProps, -} from '@tldraw/core' -import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles' -import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types' + TextShape, + TLDrawShapeUtil, + TLDrawShapeType, + TLDrawToolType, + ArrowShape, + TLDrawShapeProps, +} from '~types' import styled from '~styles' import TextAreaUtils from './text-utils' @@ -57,7 +57,7 @@ if (typeof window !== 'undefined') { melm = getMeasurementDiv() } -export class Text extends TLDrawShapeUtil { +export class Text extends TLDrawShapeUtil { type = TLDrawShapeType.Text as const toolType = TLDrawToolType.Text isAspectRatioLocked = true @@ -91,118 +91,83 @@ export class Text extends TLDrawShapeUtil { ) } - render = React.forwardRef>( - ({ shape, meta, isEditing, isBinding, events }, ref) => { + render = React.forwardRef>( + ({ shape, meta, isEditing, isBinding, onShapeChange, onShapeBlur, events }, ref) => { const rInput = React.useRef(null) - const { id, text, style } = shape + const { text, style } = shape const styles = getShapeStyle(style, meta.isDarkMode) const font = getFontStyle(shape.style) - const bounds = this.getBounds(shape) - function handleChange(e: React.ChangeEvent) { - events.onTextChange?.(id, normalizeText(e.currentTarget.value)) - } + const handleChange = React.useCallback( + (e: React.ChangeEvent) => { + onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) + }, + [shape] + ) - function handleKeyDown(e: React.KeyboardEvent) { - events.onTextKeyDown?.(id, e.key) + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') return - if (e.key === 'Escape') return + e.stopPropagation() - e.stopPropagation() + if (e.key === 'Tab') { + e.preventDefault() + if (e.shiftKey) { + TextAreaUtils.unindent(e.currentTarget) + } else { + TextAreaUtils.indent(e.currentTarget) + } - if (e.key === 'Tab') { - e.preventDefault() - if (e.shiftKey) { - TextAreaUtils.unindent(e.currentTarget) - } else { - TextAreaUtils.indent(e.currentTarget) + onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) }) } + }, + [shape, onShapeChange] + ) - events.onTextChange?.(id, normalizeText(e.currentTarget.value)) - } - } + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + e.currentTarget.setSelectionRange(0, 0) + onShapeBlur?.() + }, + [isEditing, shape] + ) - function handleKeyUp(e: React.KeyboardEvent) { - events.onTextKeyUp?.(id, e.key) - } + const handleFocus = React.useCallback( + (e: React.FocusEvent) => { + if (!isEditing) return + if (document.activeElement === e.currentTarget) { + e.currentTarget.select() + } + }, + [isEditing] + ) - function handleBlur(e: React.FocusEvent) { + const handlePointerDown = React.useCallback( + (e) => { + if (isEditing) { + e.stopPropagation() + } + }, + [isEditing] + ) + + React.useEffect(() => { if (isEditing) { - e.currentTarget.focus() - e.currentTarget.select() - return + setTimeout(() => { + const elm = rInput.current! + elm.focus() + elm.select() + }, 0) + } else { + const elm = rInput.current! + elm.setSelectionRange(0, 0) } - - setTimeout(() => { - events.onTextBlur?.(id) - }, 0) - } - - function handleFocus(e: React.FocusEvent) { - if (document.activeElement === e.currentTarget) { - e.currentTarget.select() - events.onTextFocus?.(id) - } - } - - function handlePointerDown() { - const elm = rInput.current - if (!elm) return - if (elm.selectionEnd !== 0) { - elm.selectionEnd = 0 - } - } - - const fontSize = getFontSize(shape.style.size) * (shape.style.scale || 1) - - const lineHeight = fontSize * 1.3 - - if (!isEditing) { - return ( - - {isBinding && ( - - )} - {text.split('\n').map((str, i) => ( - - {str} - - ))} - - ) - } + }, [isEditing]) return ( - - e.stopPropagation()} - > + + { autoSave="false" placeholder="" color={styles.stroke} - autoFocus={true} onFocus={handleFocus} onBlur={handleBlur} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} onChange={handleChange} + onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} + autoFocus={isEditing} + isEditing={isEditing} + isBinding={isBinding} + readOnly={!isEditing} + wrap="off" + dir="auto" + datatype="wysiwyg" /> - - + + ) } ) @@ -252,7 +222,8 @@ export class Text extends TLDrawShapeUtil { melm.style.font = getFontStyle(shape.style) // In tests, offsetWidth and offsetHeight will be 0 - const [width, height] = [melm.offsetWidth || 1, melm.offsetHeight || 1] + const width = melm.offsetWidth || 1 + const height = melm.offsetHeight || 1 return { minX: 0, @@ -291,7 +262,7 @@ export class Text extends TLDrawShapeUtil { transform( _shape: TextShape, bounds: TLBounds, - { initialShape, scaleX, scaleY, transformOrigin }: TLTransformInfo + { initialShape, scaleX, scaleY }: TLTransformInfo ): Partial { const { rotation = 0, @@ -441,67 +412,30 @@ export class Text extends TLDrawShapeUtil { distance, } } - // getBindingPoint(shape, point, origin, direction, expandDistance) { - // const bounds = this.getBounds(shape) - - // const expandedBounds = expandBounds(bounds, expandDistance) - - // let bindingPoint: number[] - // let distance: number - - // if (!HitTest.bounds(point, expandedBounds)) return - - // // The point is inside of the box, so we'll assume the user is - // // indicating a specific point inside of the box. - // if (HitTest.bounds(point, bounds)) { - // bindingPoint = vec.divV(vec.sub(point, [expandedBounds.minX, expandedBounds.minY]), [ - // expandedBounds.width, - // expandedBounds.height, - // ]) - - // distance = 0 - // } else { - // // Find furthest intersection between ray from - // // origin through point and expanded bounds. - // const intersection = Intersect.ray - // .bounds(origin, direction, expandedBounds) - // .filter(int => int.didIntersect) - // .map(int => int.points[0]) - // .sort((a, b) => vec.dist(b, origin) - vec.dist(a, origin))[0] - - // // The anchor is a point between the handle and the intersection - // const anchor = vec.med(point, intersection) - - // // Find the distance between the point and the real bounds of the shape - // const distanceFromShape = getBoundsSides(bounds) - // .map(side => vec.distanceToLineSegment(side[1][0], side[1][1], point)) - // .sort((a, b) => a - b)[0] - - // if (vec.distanceToLineSegment(point, anchor, this.getCenter(shape)) < 12) { - // // If we're close to the center, snap to the center - // bindingPoint = [0.5, 0.5] - // } else { - // // Or else calculate a normalized point - // bindingPoint = vec.divV(vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [ - // expandedBounds.width, - // expandedBounds.height, - // ]) - // } - - // distance = distanceFromShape - // } - - // return { - // point: bindingPoint, - // distance, - // } - // } } -const StyledTextArea = styled('textarea', { - zIndex: 1, +const StyledWrapper = styled('div', { width: '100%', height: '100%', + variants: { + isEditing: { + false: { + pointerEvents: 'all', + }, + true: { + pointerEvents: 'none', + }, + }, + }, +}) + +const StyledTextArea = styled('textarea', { + position: 'absolute', + top: 'var(--tl-padding)', + left: 'var(--tl-padding)', + zIndex: 1, + width: 'calc(100% - (var(--tl-padding) * 2))', + height: 'calc(100% - (var(--tl-padding) * 2))', border: 'none', padding: '4px', whiteSpace: 'pre', @@ -514,12 +448,46 @@ const StyledTextArea = styled('textarea', { letterSpacing: LETTER_SPACING, outline: 0, fontWeight: '500', - backgroundColor: '$boundsBg', overflow: 'hidden', - pointerEvents: 'all', backfaceVisibility: 'hidden', display: 'inline-block', - userSelect: 'text', WebkitUserSelect: 'text', WebkitTouchCallout: 'none', + variants: { + isBinding: { + false: {}, + true: { + background: '$boundsBg', + }, + }, + isEditing: { + false: { + pointerEvents: 'none', + userSelect: 'none', + background: 'none', + WebkitUserSelect: 'none', + }, + true: { + pointerEvents: 'all', + userSelect: 'text', + background: '$boundsBg', + WebkitUserSelect: 'text', + }, + }, + }, +}) + +const NormalText = styled('div', { + display: 'block', + whiteSpace: 'pre', + alignmentBaseline: 'mathematical', + dominantBaseline: 'mathematical', + pointerEvents: 'none', + opacity: '0.5', + padding: '4px', + margin: '0', + outline: 0, + fontWeight: '500', + lineHeight: 1.4, + letterSpacing: LETTER_SPACING, }) diff --git a/packages/tldraw/src/state/session/sessions/text/text.session.ts b/packages/tldraw/src/state/session/sessions/text/text.session.ts index ddb29d231..75cb9ea7f 100644 --- a/packages/tldraw/src/state/session/sessions/text/text.session.ts +++ b/packages/tldraw/src/state/session/sessions/text/text.session.ts @@ -30,22 +30,24 @@ export class TextSession implements Session { const { initialShape } = this const pageId = data.appState.currentPageId - let nextShape: TextShape = { - ...TLDR.getShape(data, initialShape.id, pageId), - text, - } + // let nextShape: TextShape = { + // ...TLDR.getShape(data, initialShape.id, pageId), + // text, + // } - nextShape = { - ...nextShape, - ...TLDR.getShapeUtils(nextShape).onStyleChange(nextShape), - } as TextShape + // nextShape = { + // ...nextShape, + // ...TLDR.getShapeUtils(nextShape).onStyleChange(nextShape), + // } as TextShape return { document: { pages: { [pageId]: { shapes: { - [initialShape.id]: nextShape, + [initialShape.id]: { + text, + }, }, }, }, diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 4ce5ba894..3e95db1bd 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -16,7 +16,6 @@ import { TLPointerInfo, inputs, TLBounds, - Patch, } from '@tldraw/core' import { FlipType, @@ -2234,11 +2233,11 @@ export class TLDrawState extends StateManager { } onPinchEnd: TLPinchEventHandler = () => { - if (this.state.settings.isZoomSnap) { - const i = Math.round((this.pageState.camera.zoom * 100) / 25) - const nextZoom = TLDR.getCameraZoom(i * 0.25) - this.zoomTo(nextZoom, inputs.pointer?.point) - } + // if (this.state.settings.isZoomSnap) { + // const i = Math.round((this.pageState.camera.zoom * 100) / 25) + // const nextZoom = TLDR.getCameraZoom(i * 0.25) + // this.zoomTo(nextZoom, inputs.pointer?.point) + // } this.setStatus(TLDrawStatus.Idle) } @@ -2405,7 +2404,7 @@ export class TLDrawState extends StateManager { } } - onDoubleClickCanvas: TLCanvasEventHandler = (info) => { + onDoubleClickCanvas: TLCanvasEventHandler = () => { // Unused switch (this.appState.status.current) { case TLDrawStatus.Idle: { @@ -2704,31 +2703,24 @@ export class TLDrawState extends StateManager { // Unused } - onTextChange = (id: string, text: string) => { - this.updateTextSession(text) + onShapeChange = (shape: { id: string } & Partial) => { + switch (shape.type) { + case TLDrawShapeType.Text: { + this.updateTextSession(shape.text || '') + } + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars - onTextBlur = (id: string) => { - this.completeSession() + onShapeBlur = () => { + switch (this.appState.status.current) { + case TLDrawStatus.EditingText: { + this.completeSession() + } + } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onTextFocus = (id: string) => { - // Unused - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onTextKeyDown = (id: string, key: string) => { - // Unused - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onTextKeyUp = (id: string, key: string) => { - // Unused - } - - onChange = (ids: string[]) => { + onRenderCountChange = (ids: string[]) => { const appState = this.getAppState() if (appState.isEmptyCanvas && ids.length > 0) { this.patchState( @@ -2754,8 +2746,4 @@ export class TLDrawState extends StateManager { onError = () => { // TODO } - - onBlurEditingShape = () => { - this.completeSession() - } } diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index 0cae8619c..e596495a4 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ -import type { TLBinding, TLRenderInfo } from '@tldraw/core' +import type { TLBinding, TLShapeProps } from '@tldraw/core' import { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core' import type { TLPage, TLPageState } from '@tldraw/core' import type { StoreApi } from 'zustand' @@ -32,7 +32,11 @@ export interface TLDrawMeta { isDarkMode: boolean } -export type TLDrawRenderInfo = TLRenderInfo +export type TLDrawShapeProps = TLShapeProps< + T, + E, + TLDrawMeta +> export interface Data { document: TLDrawDocument @@ -171,6 +175,7 @@ export interface ArrowShape extends TLDrawBaseShape { middle?: Decoration } } + export interface EllipseShape extends TLDrawBaseShape { type: TLDrawShapeType.Ellipse radius: number[] From 0afd7f280638b18834544570b8f744146b9cc9fa Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 23:22:07 +0100 Subject: [PATCH 09/14] Fix impossible type --- packages/core/src/test/context-wrapper.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/test/context-wrapper.tsx b/packages/core/src/test/context-wrapper.tsx index bb63e48db..2a79496c9 100644 --- a/packages/core/src/test/context-wrapper.tsx +++ b/packages/core/src/test/context-wrapper.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import type { TLPageState, TLBounds } from '../types' +import type { TLPageState, TLBounds, TLShape } from '../types' import { mockDocument } from './mockDocument' import { mockUtils } from './mockUtils' -import { useTLTheme, TLContext } from '../hooks' +import { useTLTheme, TLContext, TLContextType } from '../hooks' import { Inputs } from '+inputs' export const ContextWrapper: React.FC = ({ children }) => { @@ -18,5 +18,6 @@ export const ContextWrapper: React.FC = ({ children }) => { inputs: new Inputs(), })) - return {children} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return {children} } From 4b7d9c2af967ad633cb2aac974d3fc36a674c970 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 23:24:49 +0100 Subject: [PATCH 10/14] Fix transforms --- packages/core/src/components/canvas/canvas.tsx | 1 - packages/core/src/test/context-wrapper.tsx | 4 ++-- packages/tldraw/src/shape/shapes/text/text.tsx | 15 --------------- .../transform-single/transform-single.session.ts | 6 +++++- packages/tldraw/src/state/tldr.ts | 13 ++++++------- packages/tldraw/src/state/tlstate.ts | 1 - 6 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/core/src/components/canvas/canvas.tsx b/packages/core/src/components/canvas/canvas.tsx index 8d661ac63..5cca00555 100644 --- a/packages/core/src/components/canvas/canvas.tsx +++ b/packages/core/src/components/canvas/canvas.tsx @@ -11,7 +11,6 @@ import type { TLBinding, TLPage, TLPageState, TLShape } from '+types' import { ErrorFallback } from '+components/error-fallback' import { ErrorBoundary } from '+components/error-boundary' import { Brush } from '+components/brush' -import { Defs } from '+components/defs' import { Page } from '+components/page' import { useResizeObserver } from '+hooks/useResizeObserver' diff --git a/packages/core/src/test/context-wrapper.tsx b/packages/core/src/test/context-wrapper.tsx index 2a79496c9..a97b54b0b 100644 --- a/packages/core/src/test/context-wrapper.tsx +++ b/packages/core/src/test/context-wrapper.tsx @@ -1,8 +1,8 @@ import * as React from 'react' -import type { TLPageState, TLBounds, TLShape } from '../types' +import type { TLPageState, TLBounds } from '../types' import { mockDocument } from './mockDocument' import { mockUtils } from './mockUtils' -import { useTLTheme, TLContext, TLContextType } from '../hooks' +import { useTLTheme, TLContext } from '../hooks' import { Inputs } from '+inputs' export const ContextWrapper: React.FC = ({ children }) => { diff --git a/packages/tldraw/src/shape/shapes/text/text.tsx b/packages/tldraw/src/shape/shapes/text/text.tsx index cebb27f64..0adc1cfd4 100644 --- a/packages/tldraw/src/shape/shapes/text/text.tsx +++ b/packages/tldraw/src/shape/shapes/text/text.tsx @@ -476,18 +476,3 @@ const StyledTextArea = styled('textarea', { }, }, }) - -const NormalText = styled('div', { - display: 'block', - whiteSpace: 'pre', - alignmentBaseline: 'mathematical', - dominantBaseline: 'mathematical', - pointerEvents: 'none', - opacity: '0.5', - padding: '4px', - margin: '0', - outline: 0, - fontWeight: '500', - lineHeight: 1.4, - letterSpacing: LETTER_SPACING, -}) diff --git a/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts b/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts index e61ea2531..588f25712 100644 --- a/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts +++ b/packages/tldraw/src/state/session/sessions/transform-single/transform-single.session.ts @@ -47,7 +47,7 @@ export class TransformSingleSession implements Session { isAspectRatioLocked || shape.isAspectRatioLocked || utils.isAspectRatioLocked ) - shapes[shape.id] = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { + const change = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, { initialShape, type: this.transformType, scaleX: newBounds.scaleX, @@ -55,6 +55,10 @@ export class TransformSingleSession implements Session { transformOrigin: [0.5, 0.5], }) + if (change) { + shapes[shape.id] = change + } + return { document: { pages: { diff --git a/packages/tldraw/src/state/tldr.ts b/packages/tldraw/src/state/tldr.ts index adab9f238..9168a48d1 100644 --- a/packages/tldraw/src/state/tldr.ts +++ b/packages/tldraw/src/state/tldr.ts @@ -666,7 +666,9 @@ export class TLDR { info: TLTransformInfo, pageId: string ) { - return this.mutate(data, shape, getShapeUtils(shape).transform(shape, bounds, info), pageId) + const change = getShapeUtils(shape).transform(shape, bounds, info) + if (!change) return shape + return this.mutate(data, shape, change, pageId) } static transformSingle( @@ -676,12 +678,9 @@ export class TLDR { info: TLTransformInfo, pageId: string ) { - return this.mutate( - data, - shape, - getShapeUtils(shape).transformSingle(shape, bounds, info), - pageId - ) + const change = getShapeUtils(shape).transformSingle(shape, bounds, info) + if (!change) return shape + return this.mutate(data, shape, change, pageId) } /* -------------------------------------------------- */ diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 3e95db1bd..655aaa610 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -14,7 +14,6 @@ import { Vec, brushUpdater, TLPointerInfo, - inputs, TLBounds, } from '@tldraw/core' import { From d79f66da4e4c11e8357452bf6e37e711fbc1dd28 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 11 Sep 2021 23:58:22 +0100 Subject: [PATCH 11/14] Fix null issues (more to do here) --- packages/core/src/hooks/useShapeTree.tsx | 12 ++--- packages/core/src/types.ts | 10 ++-- .../session/sessions/text/text.session.ts | 4 +- packages/tldraw/src/state/tlstate.ts | 47 ++++++++++--------- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/core/src/hooks/useShapeTree.tsx b/packages/core/src/hooks/useShapeTree.tsx index 155f31ad5..8645b0d64 100644 --- a/packages/core/src/hooks/useShapeTree.tsx +++ b/packages/core/src/hooks/useShapeTree.tsx @@ -18,13 +18,13 @@ function addToShapeTree>( branch: IShapeTreeNode[], shapes: TLPage['shapes'], pageState: { - bindingTargetId?: string - bindingId?: string - hoveredId?: string + bindingTargetId?: string | null + bindingId?: string | null + hoveredId?: string | null selectedIds: string[] - currentParentId?: string - editingId?: string - editingBindingId?: string + currentParentId?: string | null + editingId?: string | null + editingBindingId?: string | null }, meta?: M ) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 79efd67cf..1a3992d10 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -24,12 +24,12 @@ export interface TLPageState { zoom: number } brush?: TLBounds - pointedId?: string - hoveredId?: string - editingId?: string - bindingId?: string + pointedId?: string | null + hoveredId?: string | null + editingId?: string | null + bindingId?: string | null boundsRotation?: number - currentParentId?: string + currentParentId?: string | null } export interface TLHandle { diff --git a/packages/tldraw/src/state/session/sessions/text/text.session.ts b/packages/tldraw/src/state/session/sessions/text/text.session.ts index 75cb9ea7f..e5c958c96 100644 --- a/packages/tldraw/src/state/session/sessions/text/text.session.ts +++ b/packages/tldraw/src/state/session/sessions/text/text.session.ts @@ -146,7 +146,7 @@ export class TextSession implements Session { }, pageState: { [pageId]: { - editingId: undefined, + editingId: null, }, }, }, @@ -166,7 +166,7 @@ export class TextSession implements Session { }, pageState: { [pageId]: { - editingId: undefined, + editingId: null, selectedIds: [initialShape.id], }, }, diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 655aaa610..102372d6b 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -408,6 +408,7 @@ export class TLDrawState extends StateManager { */ selectTool = (tool: TLDrawShapeType | 'select'): this => { if (this.session) return this + return this.patchState( { appState: { @@ -462,10 +463,10 @@ export class TLDrawState extends StateManager { document: { pageStates: { [this.currentPageId]: { - bindingId: undefined, - editingId: undefined, - hoveredId: undefined, - pointedId: undefined, + bindingId: null, + editingId: null, + hoveredId: null, + pointedId: null, }, }, }, @@ -1356,6 +1357,7 @@ export class TLDrawState extends StateManager { if (result === undefined) { this.isCreating = false + return this.patchState( { appState: { @@ -1391,24 +1393,9 @@ export class TLDrawState extends StateManager { pageStates: { [this.currentPageId]: { selectedIds: [], - editingId: undefined, - bindingId: undefined, - hoveredId: undefined, - }, - }, - }, - } - - // ...and set editingId back to undefined - result.after = { - ...result.after, - document: { - ...result.after.document, - pageStates: { - ...result.after.document?.pageStates, - [this.currentPageId]: { - ...(result.after.document?.pageStates || {})[this.currentPageId], - editingId: undefined, + editingId: null, + bindingId: null, + hoveredId: null, }, }, }, @@ -1431,6 +1418,17 @@ export class TLDrawState extends StateManager { }, } + result.after.document = { + ...result.after.document, + pageStates: { + ...result.after.document?.pageStates, + [this.currentPageId]: { + ...(result.after.document?.pageStates || {})[this.currentPageId], + editingId: null, + }, + }, + } + this.setState(result, `session:complete:${session.id}`) } else { this.patchState( @@ -1446,7 +1444,7 @@ export class TLDrawState extends StateManager { document: { pageStates: { [this.currentPageId]: { - editingId: undefined, + editingId: null, }, }, }, @@ -2400,6 +2398,9 @@ export class TLDrawState extends StateManager { } break } + case TLDrawStatus.EditingText: { + this.completeSession() + } } } From af81a98fa4206303df683f49f07874637e522b87 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 12 Sep 2021 00:12:43 +0100 Subject: [PATCH 12/14] Fix jumpy shapes --- .../core/src/components/bounds/bounds.tsx | 21 ------------------- packages/core/src/components/shape/shape.tsx | 10 --------- packages/core/src/hooks/usePosition.ts | 10 +++++---- packages/core/src/utils/vec.tsx | 4 ++-- 4 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/core/src/components/bounds/bounds.tsx b/packages/core/src/components/bounds/bounds.tsx index bc098a6c2..7041016df 100644 --- a/packages/core/src/components/bounds/bounds.tsx +++ b/packages/core/src/components/bounds/bounds.tsx @@ -17,27 +17,6 @@ interface BoundsProps { viewportWidth: number } -// function setTransform(elm: SVGSVGElement, padding: number, bounds: TLBounds, rotation: number) { -// const center = Utils.getBoundsCenter(bounds) -// const transform = ` -// rotate(${rotation * (180 / Math.PI)},${center}) -// translate(${bounds.minX - padding},${bounds.minY - padding}) -// rotate(${(bounds.rotation || 0) * (180 / Math.PI)},0,0)` -// elm.setAttribute('transform', transform) -// elm.setAttribute('width', bounds.width + padding * 2 + 'px') -// elm.setAttribute('height', bounds.height + padding * 2 + 'px') -// } - -// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { -// const transform = ` -// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) -// rotate(${rotation + (bounds.rotation || 0)}rad) -// ` -// elm.style.setProperty('transform', transform) -// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) -// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) -// } - export function Bounds({ zoom, bounds, diff --git a/packages/core/src/components/shape/shape.tsx b/packages/core/src/components/shape/shape.tsx index c98496014..8422c97d5 100644 --- a/packages/core/src/components/shape/shape.tsx +++ b/packages/core/src/components/shape/shape.tsx @@ -7,16 +7,6 @@ import { RenderedShape } from './rendered-shape' import { Container } from '+components/container' import { useTLContext } from '+hooks' -// function setTransform(elm: HTMLDivElement, bounds: TLBounds, rotation = 0) { -// const transform = ` -// translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) -// rotate(${rotation + (bounds.rotation || 0)}rad) -// ` -// elm.style.setProperty('transform', transform) -// elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) -// elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) -// } - export const Shape = >({ shape, utils, diff --git a/packages/core/src/hooks/usePosition.ts b/packages/core/src/hooks/usePosition.ts index bc1e17732..0ff549a5c 100644 --- a/packages/core/src/hooks/usePosition.ts +++ b/packages/core/src/hooks/usePosition.ts @@ -9,11 +9,13 @@ export function usePosition(bounds: TLBounds, rotation = 0) { const elm = rBounds.current! const transform = ` translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding))) - rotate(${rotation + (bounds.rotation || 0)}rad) - ` + rotate(${rotation + (bounds.rotation || 0)}rad)` elm.style.setProperty('transform', transform) - elm.style.setProperty('width', `calc(${bounds.width}px + (var(--tl-padding) * 2))`) - elm.style.setProperty('height', `calc(${bounds.height}px + (var(--tl-padding) * 2))`) + elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`) + elm.style.setProperty( + 'height', + `calc(${Math.floor(bounds.height)}px + (var(--tl-padding) * 2))` + ) }, [rBounds, bounds, rotation]) return rBounds diff --git a/packages/core/src/utils/vec.tsx b/packages/core/src/utils/vec.tsx index 777094a87..ee3802428 100644 --- a/packages/core/src/utils/vec.tsx +++ b/packages/core/src/utils/vec.tsx @@ -360,8 +360,8 @@ export class Vec { return Vec.isLeft(p1, pc, p2) > 0 } - static round = (a: number[], d = 5): number[] => { - return a.map((v) => +v.toPrecision(d)) + static round = (a: number[], d = 2): number[] => { + return a.map((v) => +v.toFixed(d)) } /** From 915a7ba194f9cd28fb6b4b1925383a2407218679 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 12 Sep 2021 00:34:15 +0100 Subject: [PATCH 13/14] fix draw bug --- .../tldraw/src/shape/shapes/draw/draw.tsx | 4 +++- .../session/sessions/draw/draw.session.ts | 22 +++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index 2cf4fd113..23226ccaf 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -308,7 +308,9 @@ function getDrawStrokePath(shape: DrawShape, isEditing: boolean) { const stroke = getStroke(shape.points.slice(2), { size: 1 + styles.strokeWidth * 2, - thinning: 0.85, + thinning: 0.8, + streamline: 0.7, + smoothing: 0.6, end: { taper: +styles.strokeWidth * 50 }, start: { taper: +styles.strokeWidth * 50 }, ...options, diff --git a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts index 71a03ad7e..0397de7bb 100644 --- a/packages/tldraw/src/state/session/sessions/draw/draw.session.ts +++ b/packages/tldraw/src/state/session/sessions/draw/draw.session.ts @@ -51,7 +51,7 @@ export class DrawSession implements Session { const bounds = Utils.getBoundsFromPoints(this.points) if (bounds.width > 8 || bounds.height > 8) { this.isLocked = true - const returning = [...this.previous] + const returning = [...this.last] const isVertical = bounds.height > 8 @@ -80,16 +80,9 @@ export class DrawSession implements Session { } // The previous input (not adjusted) point - this.previous = point - - const prevTopLeft = [...this.topLeft] - - this.topLeft = [Math.min(this.topLeft[0], point[0]), Math.min(this.topLeft[1], point[1])] - - const delta = Vec.sub(this.topLeft, this.origin) // The new adjusted point - const newPoint = Vec.round(Vec.sub(this.previous, this.origin)).concat(pressure) + const newPoint = Vec.round(Vec.sub(point, this.origin)).concat(pressure) // Don't add duplicate points. Be sure to // test against the previous *adjusted* point. @@ -98,13 +91,20 @@ export class DrawSession implements Session { // The new adjusted point is now the previous adjusted point. this.last = newPoint - let points: number[][] + // Does the input point create a new top left? + const prevTopLeft = [...this.topLeft] + + this.topLeft = [Math.min(this.topLeft[0], point[0]), Math.min(this.topLeft[1], point[1])] + + const delta = Vec.sub(this.topLeft, this.origin) // Add the new adjusted point to the points array this.points.push(newPoint) // Time to shift some points! + let points: number[][] + if (Vec.isEqual(prevTopLeft, this.topLeft)) { // If the new top left is the same as the previous top left, // we don't need to shift anything: we just shift the new point @@ -114,7 +114,7 @@ export class DrawSession implements Session { // If we have a new top left, then we need to iterate through // the "unshifted" points array and shift them based on the // offset between the new top left and the original top left. - points = this.points.map((pt) => Vec.sub(pt, delta)) + points = this.points.map((pt) => [pt[0] - delta[0], pt[1] - delta[1], pt[2]]) } this.shiftedPoints = points From 8bcb2e1154841fa5793f8017477121c47d936298 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 12 Sep 2021 00:41:50 +0100 Subject: [PATCH 14/14] Fix tests --- packages/core/src/components/bounds/bounds.tsx | 1 - .../src/state/session/sessions/arrow/arrow.session.spec.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/bounds/bounds.tsx b/packages/core/src/components/bounds/bounds.tsx index 7041016df..d3a2bf2b4 100644 --- a/packages/core/src/components/bounds/bounds.tsx +++ b/packages/core/src/components/bounds/bounds.tsx @@ -5,7 +5,6 @@ import { CenterHandle } from './center-handle' import { RotateHandle } from './rotate-handle' import { CornerHandle } from './corner-handle' import { EdgeHandle } from './edge-handle' -import { usePosition } from '+hooks' import { Container } from '+components/container' import { SVGContainer } from '+components/svg-container' diff --git a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts index 7cbc4a618..e8e1769bc 100644 --- a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts +++ b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts @@ -100,7 +100,7 @@ describe('Arrow session', () => { .startHandleSession([200, 200], 'start') .updateHandleSession([91, 9]) - expect(tlstate.bindings[0].point).toStrictEqual([0.67839, 0.125]) + expect(tlstate.bindings[0].point).toStrictEqual([0.68, 0.13]) tlstate.updateHandleSession([91, 9], false, false, true) }) @@ -112,7 +112,7 @@ describe('Arrow session', () => { .startHandleSession([200, 200], 'start') .updateHandleSession([91, 9]) - expect(tlstate.bindings[0].point).toStrictEqual([0.67839, 0.125]) + expect(tlstate.bindings[0].point).toStrictEqual([0.68, 0.13]) tlstate.updateHandleSession([91, 9], false, false, true)