Refactor tools (#147)
* Refactor Tools * Update text.tsx * Passing tests * Error fixes * Fix re-selecting tool * Fix arrow
This commit is contained in:
parent
be271f3ca2
commit
1408ac2cbe
67 changed files with 2024 additions and 1616 deletions
|
@ -42,6 +42,7 @@ export const Shape = React.memo(
|
||||||
meta={meta}
|
meta={meta}
|
||||||
events={events}
|
events={events}
|
||||||
onShapeChange={callbacks.onShapeChange}
|
onShapeChange={callbacks.onShapeChange}
|
||||||
|
onShapeBlur={callbacks.onShapeBlur}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
@ -71,7 +71,7 @@ export function useZoomEvents<T extends Element>(zoom: number, ref: React.RefObj
|
||||||
onPinch: ({ origin, offset, event }) => {
|
onPinch: ({ origin, offset, event }) => {
|
||||||
const elm = ref.current
|
const elm = ref.current
|
||||||
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
if (!(event.target === elm || elm?.contains(event.target as Node))) return
|
||||||
if (!rOriginPoint.current) throw Error('No origin point!')
|
if (!rOriginPoint.current) return
|
||||||
|
|
||||||
const info = inputs.pinch(origin, rOriginPoint.current)
|
const info = inputs.pinch(origin, rOriginPoint.current)
|
||||||
|
|
||||||
|
|
541
packages/core/src/types.d.ts
vendored
541
packages/core/src/types.d.ts
vendored
|
@ -1,264 +1,357 @@
|
||||||
import type React from 'react';
|
import type React from 'react'
|
||||||
import type { ForwardedRef } from 'react';
|
import { MutableRefObject } from 'react-router/node_modules/@types/react'
|
||||||
export declare type Patch<T> = Partial<{
|
export declare type Patch<T> = Partial<{
|
||||||
[P in keyof T]: T | Partial<T> | Patch<T[P]>;
|
[P in keyof T]: T | Partial<T> | Patch<T[P]>
|
||||||
}>;
|
}>
|
||||||
export interface TLPage<T extends TLShape, B extends TLBinding> {
|
export interface TLPage<T extends TLShape, B extends TLBinding> {
|
||||||
id: string;
|
id: string
|
||||||
name?: string;
|
name?: string
|
||||||
childIndex?: number;
|
childIndex?: number
|
||||||
shapes: Record<string, T>;
|
shapes: Record<string, T>
|
||||||
bindings: Record<string, B>;
|
bindings: Record<string, B>
|
||||||
}
|
}
|
||||||
export interface TLPageState {
|
export interface TLPageState {
|
||||||
id: string;
|
id: string
|
||||||
selectedIds: string[];
|
selectedIds: string[]
|
||||||
camera: {
|
camera: {
|
||||||
point: number[];
|
point: number[]
|
||||||
zoom: number;
|
zoom: number
|
||||||
};
|
}
|
||||||
brush?: TLBounds;
|
brush?: TLBounds
|
||||||
pointedId?: string | null;
|
pointedId?: string | null
|
||||||
hoveredId?: string | null;
|
hoveredId?: string | null
|
||||||
editingId?: string | null;
|
editingId?: string | null
|
||||||
bindingId?: string | null;
|
bindingId?: string | null
|
||||||
boundsRotation?: number;
|
boundsRotation?: number
|
||||||
currentParentId?: string | null;
|
currentParentId?: string | null
|
||||||
}
|
}
|
||||||
export interface TLHandle {
|
export interface TLHandle {
|
||||||
id: string;
|
id: string
|
||||||
index: number;
|
index: number
|
||||||
point: number[];
|
point: number[]
|
||||||
canBind?: boolean;
|
canBind?: boolean
|
||||||
bindingId?: string;
|
bindingId?: string
|
||||||
}
|
}
|
||||||
export interface TLShape {
|
export interface TLShape {
|
||||||
id: string;
|
id: string
|
||||||
type: string;
|
type: string
|
||||||
parentId: string;
|
parentId: string
|
||||||
childIndex: number;
|
childIndex: number
|
||||||
name: string;
|
name: string
|
||||||
point: number[];
|
point: number[]
|
||||||
rotation?: number;
|
rotation?: number
|
||||||
children?: string[];
|
children?: string[]
|
||||||
handles?: Record<string, TLHandle>;
|
handles?: Record<string, TLHandle>
|
||||||
isLocked?: boolean;
|
isLocked?: boolean
|
||||||
isHidden?: boolean;
|
isHidden?: boolean
|
||||||
isEditing?: boolean;
|
isEditing?: boolean
|
||||||
isGenerated?: boolean;
|
isGenerated?: boolean
|
||||||
isAspectRatioLocked?: boolean;
|
isAspectRatioLocked?: boolean
|
||||||
}
|
}
|
||||||
export declare type TLShapeUtils<T extends TLShape = any, E extends Element = any, M = any, K = any> = Record<string, TLShapeUtil<T, E, M, K>>;
|
export declare type TLShapeUtils<
|
||||||
|
T extends TLShape = any,
|
||||||
|
E extends Element = any,
|
||||||
|
M = any,
|
||||||
|
K = any
|
||||||
|
> = Record<string, TLShapeUtil<T, E, M, K>>
|
||||||
export interface TLRenderInfo<T extends TLShape, E = any, M = any> {
|
export interface TLRenderInfo<T extends TLShape, E = any, M = any> {
|
||||||
shape: T;
|
shape: T
|
||||||
isEditing: boolean;
|
isEditing: boolean
|
||||||
isBinding: boolean;
|
isBinding: boolean
|
||||||
isHovered: boolean;
|
isHovered: boolean
|
||||||
isSelected: boolean;
|
isSelected: boolean
|
||||||
isCurrentParent: boolean;
|
isCurrentParent: boolean
|
||||||
meta: M extends any ? M : never;
|
meta: M extends any ? M : never
|
||||||
onShapeChange?: TLCallbacks<T>['onShapeChange'];
|
onShapeChange?: TLCallbacks<T>['onShapeChange']
|
||||||
onShapeBlur?: TLCallbacks<T>['onShapeBlur'];
|
onShapeBlur?: TLCallbacks<T>['onShapeBlur']
|
||||||
events: {
|
events: {
|
||||||
onPointerDown: (e: React.PointerEvent<E>) => void;
|
onPointerDown: (e: React.PointerEvent<E>) => void
|
||||||
onPointerUp: (e: React.PointerEvent<E>) => void;
|
onPointerUp: (e: React.PointerEvent<E>) => void
|
||||||
onPointerEnter: (e: React.PointerEvent<E>) => void;
|
onPointerEnter: (e: React.PointerEvent<E>) => void
|
||||||
onPointerMove: (e: React.PointerEvent<E>) => void;
|
onPointerMove: (e: React.PointerEvent<E>) => void
|
||||||
onPointerLeave: (e: React.PointerEvent<E>) => void;
|
onPointerLeave: (e: React.PointerEvent<E>) => void
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<T, E, M> {
|
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<T, E, M> {
|
||||||
ref: ForwardedRef<E>;
|
ref: MutableRefObject<E>
|
||||||
shape: T;
|
shape: T
|
||||||
}
|
}
|
||||||
export interface TLTool {
|
export interface TLTool {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
}
|
}
|
||||||
export interface TLBinding<M = any> {
|
export interface TLBinding<M = any> {
|
||||||
id: string;
|
id: string
|
||||||
type: string;
|
type: string
|
||||||
toId: string;
|
toId: string
|
||||||
fromId: string;
|
fromId: string
|
||||||
meta: M;
|
meta: M
|
||||||
}
|
}
|
||||||
export interface TLTheme {
|
export interface TLTheme {
|
||||||
brushFill?: string;
|
brushFill?: string
|
||||||
brushStroke?: string;
|
brushStroke?: string
|
||||||
selectFill?: string;
|
selectFill?: string
|
||||||
selectStroke?: string;
|
selectStroke?: string
|
||||||
background?: string;
|
background?: string
|
||||||
foreground?: string;
|
foreground?: string
|
||||||
}
|
}
|
||||||
export declare type TLWheelEventHandler = (info: TLPointerInfo<string>, e: React.WheelEvent<Element> | WheelEvent) => void;
|
export declare type TLWheelEventHandler = (
|
||||||
export declare type TLPinchEventHandler = (info: TLPointerInfo<string>, e: React.WheelEvent<Element> | WheelEvent | React.TouchEvent<Element> | TouchEvent | React.PointerEvent<Element> | PointerEventInit) => void;
|
info: TLPointerInfo<string>,
|
||||||
export declare type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void;
|
e: React.WheelEvent<Element> | WheelEvent
|
||||||
export declare type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void;
|
) => void
|
||||||
export declare type TLBoundsEventHandler = (info: TLPointerInfo<'bounds'>, e: React.PointerEvent) => void;
|
export declare type TLPinchEventHandler = (
|
||||||
export declare type TLBoundsHandleEventHandler = (info: TLPointerInfo<TLBoundsCorner | TLBoundsEdge | 'rotate'>, e: React.PointerEvent) => void;
|
info: TLPointerInfo<string>,
|
||||||
|
e:
|
||||||
|
| React.WheelEvent<Element>
|
||||||
|
| WheelEvent
|
||||||
|
| React.TouchEvent<Element>
|
||||||
|
| TouchEvent
|
||||||
|
| React.PointerEvent<Element>
|
||||||
|
| PointerEventInit
|
||||||
|
) => void
|
||||||
|
export declare type TLPointerEventHandler = (
|
||||||
|
info: TLPointerInfo<string>,
|
||||||
|
e: React.PointerEvent
|
||||||
|
) => void
|
||||||
|
export declare type TLCanvasEventHandler = (
|
||||||
|
info: TLPointerInfo<'canvas'>,
|
||||||
|
e: React.PointerEvent
|
||||||
|
) => void
|
||||||
|
export declare type TLBoundsEventHandler = (
|
||||||
|
info: TLPointerInfo<'bounds'>,
|
||||||
|
e: React.PointerEvent
|
||||||
|
) => void
|
||||||
|
export declare type TLBoundsHandleEventHandler = (
|
||||||
|
info: TLPointerInfo<TLBoundsCorner | TLBoundsEdge | 'rotate'>,
|
||||||
|
e: React.PointerEvent
|
||||||
|
) => void
|
||||||
export interface TLCallbacks<T extends TLShape> {
|
export interface TLCallbacks<T extends TLShape> {
|
||||||
onPinchStart: TLPinchEventHandler;
|
onPinchStart: TLPinchEventHandler
|
||||||
onPinchEnd: TLPinchEventHandler;
|
onPinchEnd: TLPinchEventHandler
|
||||||
onPinch: TLPinchEventHandler;
|
onPinch: TLPinchEventHandler
|
||||||
onPan: TLWheelEventHandler;
|
onPan: TLWheelEventHandler
|
||||||
onZoom: TLWheelEventHandler;
|
onZoom: TLWheelEventHandler
|
||||||
onPointerMove: TLPointerEventHandler;
|
onPointerMove: TLPointerEventHandler
|
||||||
onPointerUp: TLPointerEventHandler;
|
onPointerUp: TLPointerEventHandler
|
||||||
onPointerDown: TLPointerEventHandler;
|
onPointerDown: TLPointerEventHandler
|
||||||
onPointCanvas: TLCanvasEventHandler;
|
onPointCanvas: TLCanvasEventHandler
|
||||||
onDoubleClickCanvas: TLCanvasEventHandler;
|
onDoubleClickCanvas: TLCanvasEventHandler
|
||||||
onRightPointCanvas: TLCanvasEventHandler;
|
onRightPointCanvas: TLCanvasEventHandler
|
||||||
onDragCanvas: TLCanvasEventHandler;
|
onDragCanvas: TLCanvasEventHandler
|
||||||
onReleaseCanvas: TLCanvasEventHandler;
|
onReleaseCanvas: TLCanvasEventHandler
|
||||||
onPointShape: TLPointerEventHandler;
|
onPointShape: TLPointerEventHandler
|
||||||
onDoubleClickShape: TLPointerEventHandler;
|
onDoubleClickShape: TLPointerEventHandler
|
||||||
onRightPointShape: TLPointerEventHandler;
|
onRightPointShape: TLPointerEventHandler
|
||||||
onDragShape: TLPointerEventHandler;
|
onDragShape: TLPointerEventHandler
|
||||||
onHoverShape: TLPointerEventHandler;
|
onHoverShape: TLPointerEventHandler
|
||||||
onUnhoverShape: TLPointerEventHandler;
|
onUnhoverShape: TLPointerEventHandler
|
||||||
onReleaseShape: TLPointerEventHandler;
|
onReleaseShape: TLPointerEventHandler
|
||||||
onPointBounds: TLBoundsEventHandler;
|
onPointBounds: TLBoundsEventHandler
|
||||||
onDoubleClickBounds: TLBoundsEventHandler;
|
onDoubleClickBounds: TLBoundsEventHandler
|
||||||
onRightPointBounds: TLBoundsEventHandler;
|
onRightPointBounds: TLBoundsEventHandler
|
||||||
onDragBounds: TLBoundsEventHandler;
|
onDragBounds: TLBoundsEventHandler
|
||||||
onHoverBounds: TLBoundsEventHandler;
|
onHoverBounds: TLBoundsEventHandler
|
||||||
onUnhoverBounds: TLBoundsEventHandler;
|
onUnhoverBounds: TLBoundsEventHandler
|
||||||
onReleaseBounds: TLBoundsEventHandler;
|
onReleaseBounds: TLBoundsEventHandler
|
||||||
onPointBoundsHandle: TLBoundsHandleEventHandler;
|
onPointBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler;
|
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onRightPointBoundsHandle: TLBoundsHandleEventHandler;
|
onRightPointBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onDragBoundsHandle: TLBoundsHandleEventHandler;
|
onDragBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onHoverBoundsHandle: TLBoundsHandleEventHandler;
|
onHoverBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onUnhoverBoundsHandle: TLBoundsHandleEventHandler;
|
onUnhoverBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onReleaseBoundsHandle: TLBoundsHandleEventHandler;
|
onReleaseBoundsHandle: TLBoundsHandleEventHandler
|
||||||
onPointHandle: TLPointerEventHandler;
|
onPointHandle: TLPointerEventHandler
|
||||||
onDoubleClickHandle: TLPointerEventHandler;
|
onDoubleClickHandle: TLPointerEventHandler
|
||||||
onRightPointHandle: TLPointerEventHandler;
|
onRightPointHandle: TLPointerEventHandler
|
||||||
onDragHandle: TLPointerEventHandler;
|
onDragHandle: TLPointerEventHandler
|
||||||
onHoverHandle: TLPointerEventHandler;
|
onHoverHandle: TLPointerEventHandler
|
||||||
onUnhoverHandle: TLPointerEventHandler;
|
onUnhoverHandle: TLPointerEventHandler
|
||||||
onReleaseHandle: TLPointerEventHandler;
|
onReleaseHandle: TLPointerEventHandler
|
||||||
onRenderCountChange: (ids: string[]) => void;
|
onRenderCountChange: (ids: string[]) => void
|
||||||
onShapeChange: (shape: {
|
onShapeChange: (
|
||||||
id: string;
|
shape: {
|
||||||
} & Partial<T>) => void;
|
id: string
|
||||||
onShapeBlur: () => void;
|
} & Partial<T>
|
||||||
onError: (error: Error) => void;
|
) => void
|
||||||
|
onShapeBlur: () => void
|
||||||
|
onError: (error: Error) => void
|
||||||
}
|
}
|
||||||
export interface TLBounds {
|
export interface TLBounds {
|
||||||
minX: number;
|
minX: number
|
||||||
minY: number;
|
minY: number
|
||||||
maxX: number;
|
maxX: number
|
||||||
maxY: number;
|
maxY: number
|
||||||
width: number;
|
width: number
|
||||||
height: number;
|
height: number
|
||||||
rotation?: number;
|
rotation?: number
|
||||||
}
|
}
|
||||||
export declare type TLIntersection = {
|
export declare type TLIntersection = {
|
||||||
didIntersect: boolean;
|
didIntersect: boolean
|
||||||
message: string;
|
message: string
|
||||||
points: number[][];
|
points: number[][]
|
||||||
};
|
}
|
||||||
export declare enum TLBoundsEdge {
|
export declare enum TLBoundsEdge {
|
||||||
Top = "top_edge",
|
Top = 'top_edge',
|
||||||
Right = "right_edge",
|
Right = 'right_edge',
|
||||||
Bottom = "bottom_edge",
|
Bottom = 'bottom_edge',
|
||||||
Left = "left_edge"
|
Left = 'left_edge',
|
||||||
}
|
}
|
||||||
export declare enum TLBoundsCorner {
|
export declare enum TLBoundsCorner {
|
||||||
TopLeft = "top_left_corner",
|
TopLeft = 'top_left_corner',
|
||||||
TopRight = "top_right_corner",
|
TopRight = 'top_right_corner',
|
||||||
BottomRight = "bottom_right_corner",
|
BottomRight = 'bottom_right_corner',
|
||||||
BottomLeft = "bottom_left_corner"
|
BottomLeft = 'bottom_left_corner',
|
||||||
}
|
}
|
||||||
export interface TLPointerInfo<T extends string = string> {
|
export interface TLPointerInfo<T extends string = string> {
|
||||||
target: T;
|
target: T
|
||||||
pointerId: number;
|
pointerId: number
|
||||||
origin: number[];
|
origin: number[]
|
||||||
point: number[];
|
point: number[]
|
||||||
delta: number[];
|
delta: number[]
|
||||||
pressure: number;
|
pressure: number
|
||||||
shiftKey: boolean;
|
shiftKey: boolean
|
||||||
ctrlKey: boolean;
|
ctrlKey: boolean
|
||||||
metaKey: boolean;
|
metaKey: boolean
|
||||||
altKey: boolean;
|
altKey: boolean
|
||||||
}
|
}
|
||||||
export interface TLKeyboardInfo {
|
export interface TLKeyboardInfo {
|
||||||
origin: number[];
|
origin: number[]
|
||||||
point: number[];
|
point: number[]
|
||||||
key: string;
|
key: string
|
||||||
keys: string[];
|
keys: string[]
|
||||||
shiftKey: boolean;
|
shiftKey: boolean
|
||||||
ctrlKey: boolean;
|
ctrlKey: boolean
|
||||||
metaKey: boolean;
|
metaKey: boolean
|
||||||
altKey: boolean;
|
altKey: boolean
|
||||||
}
|
}
|
||||||
export interface TLTransformInfo<T extends TLShape> {
|
export interface TLTransformInfo<T extends TLShape> {
|
||||||
type: TLBoundsEdge | TLBoundsCorner;
|
type: TLBoundsEdge | TLBoundsCorner
|
||||||
initialShape: T;
|
initialShape: T
|
||||||
scaleX: number;
|
scaleX: number
|
||||||
scaleY: number;
|
scaleY: number
|
||||||
transformOrigin: number[];
|
transformOrigin: number[]
|
||||||
}
|
}
|
||||||
export interface TLBezierCurveSegment {
|
export interface TLBezierCurveSegment {
|
||||||
start: number[];
|
start: number[]
|
||||||
tangentStart: number[];
|
tangentStart: number[]
|
||||||
normalStart: number[];
|
normalStart: number[]
|
||||||
pressureStart: number;
|
pressureStart: number
|
||||||
end: number[];
|
end: number[]
|
||||||
tangentEnd: number[];
|
tangentEnd: number[]
|
||||||
normalEnd: number[];
|
normalEnd: number[]
|
||||||
pressureEnd: number;
|
pressureEnd: number
|
||||||
|
}
|
||||||
|
export declare type TLShapeUtil<
|
||||||
|
T extends TLShape,
|
||||||
|
E extends Element,
|
||||||
|
M = any,
|
||||||
|
K = {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
> = K & {
|
||||||
|
type: T['type']
|
||||||
|
defaultProps: T
|
||||||
|
Component(
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
props: TLRenderInfo<T, E, M>,
|
||||||
|
ref: React.MutableRefObject<E>
|
||||||
|
): React.ReactElement<TLRenderInfo<T, E, M>, E['tagName']>
|
||||||
|
Indicator(
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
props: {
|
||||||
|
shape: T
|
||||||
|
}
|
||||||
|
): React.ReactElement | null
|
||||||
|
getBounds(this: TLShapeUtil<T, E, M>, shape: T): TLBounds
|
||||||
|
refMap: Map<string, React.RefObject<E>>
|
||||||
|
boundsCache: WeakMap<TLShape, TLBounds>
|
||||||
|
isAspectRatioLocked: boolean
|
||||||
|
canEdit: boolean
|
||||||
|
canBind: boolean
|
||||||
|
getRotatedBounds(this: TLShapeUtil<T, E, M>, shape: T): TLBounds
|
||||||
|
hitTest(this: TLShapeUtil<T, E, M>, shape: T, point: number[]): boolean
|
||||||
|
hitTestBounds(this: TLShapeUtil<T, E, M>, shape: T, bounds: TLBounds): boolean
|
||||||
|
shouldRender(this: TLShapeUtil<T, E, M>, prev: T, next: T): boolean
|
||||||
|
getCenter(this: TLShapeUtil<T, E, M>, shape: T): number[]
|
||||||
|
getRef(this: TLShapeUtil<T, E, M>, shape: T): React.RefObject<E>
|
||||||
|
getBindingPoint<K extends TLShape>(
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
fromShape: K,
|
||||||
|
point: number[],
|
||||||
|
origin: number[],
|
||||||
|
direction: number[],
|
||||||
|
padding: number,
|
||||||
|
bindAnywhere: boolean
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
point: number[]
|
||||||
|
distance: number
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
create: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
props: {
|
||||||
|
id: string
|
||||||
|
} & Partial<T>
|
||||||
|
) => T
|
||||||
|
mutate: (this: TLShapeUtil<T, E, M>, shape: T, props: Partial<T>) => Partial<T>
|
||||||
|
transform: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
bounds: TLBounds,
|
||||||
|
info: TLTransformInfo<T>
|
||||||
|
) => Partial<T> | void
|
||||||
|
transformSingle: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
bounds: TLBounds,
|
||||||
|
info: TLTransformInfo<T>
|
||||||
|
) => Partial<T> | void
|
||||||
|
updateChildren: <K extends TLShape>(
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
children: K[]
|
||||||
|
) => Partial<K>[] | void
|
||||||
|
onChildrenChange: (this: TLShapeUtil<T, E, M>, shape: T, children: TLShape[]) => Partial<T> | void
|
||||||
|
onBindingChange: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
binding: TLBinding,
|
||||||
|
target: TLShape,
|
||||||
|
targetBounds: TLBounds,
|
||||||
|
center: number[]
|
||||||
|
) => Partial<T> | void
|
||||||
|
onHandleChange: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
handle: Partial<T['handles']>,
|
||||||
|
info: Partial<TLPointerInfo>
|
||||||
|
) => Partial<T> | void
|
||||||
|
onRightPointHandle: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
handle: Partial<T['handles']>,
|
||||||
|
info: Partial<TLPointerInfo>
|
||||||
|
) => Partial<T> | void
|
||||||
|
onDoubleClickHandle: (
|
||||||
|
this: TLShapeUtil<T, E, M>,
|
||||||
|
shape: T,
|
||||||
|
handle: Partial<T['handles']>,
|
||||||
|
info: Partial<TLPointerInfo>
|
||||||
|
) => Partial<T> | void
|
||||||
|
onDoubleClickBoundsHandle: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void
|
||||||
|
onSessionComplete: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void
|
||||||
|
onStyleChange: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void
|
||||||
|
_Component: React.ForwardRefExoticComponent<any>
|
||||||
}
|
}
|
||||||
export declare type TLShapeUtil<T extends TLShape, E extends Element, M = any, K = {
|
|
||||||
[key: string]: any;
|
|
||||||
}> = K & {
|
|
||||||
type: T['type'];
|
|
||||||
defaultProps: T;
|
|
||||||
Component(this: TLShapeUtil<T, E, M>, props: TLRenderInfo<T, E, M>, ref: React.ForwardedRef<E>): React.ReactElement<TLRenderInfo<T, E, M>, E['tagName']>;
|
|
||||||
Indicator(this: TLShapeUtil<T, E, M>, props: {
|
|
||||||
shape: T;
|
|
||||||
}): React.ReactElement | null;
|
|
||||||
getBounds(this: TLShapeUtil<T, E, M>, shape: T): TLBounds;
|
|
||||||
refMap: Map<string, React.RefObject<E>>;
|
|
||||||
boundsCache: WeakMap<TLShape, TLBounds>;
|
|
||||||
isAspectRatioLocked: boolean;
|
|
||||||
canEdit: boolean;
|
|
||||||
canBind: boolean;
|
|
||||||
getRotatedBounds(this: TLShapeUtil<T, E, M>, shape: T): TLBounds;
|
|
||||||
hitTest(this: TLShapeUtil<T, E, M>, shape: T, point: number[]): boolean;
|
|
||||||
hitTestBounds(this: TLShapeUtil<T, E, M>, shape: T, bounds: TLBounds): boolean;
|
|
||||||
shouldRender(this: TLShapeUtil<T, E, M>, prev: T, next: T): boolean;
|
|
||||||
getCenter(this: TLShapeUtil<T, E, M>, shape: T): number[];
|
|
||||||
getRef(this: TLShapeUtil<T, E, M>, shape: T): React.RefObject<E>;
|
|
||||||
getBindingPoint<K extends TLShape>(this: TLShapeUtil<T, E, M>, shape: T, fromShape: K, point: number[], origin: number[], direction: number[], padding: number, bindAnywhere: boolean): {
|
|
||||||
point: number[];
|
|
||||||
distance: number;
|
|
||||||
} | undefined;
|
|
||||||
create: (this: TLShapeUtil<T, E, M>, props: {
|
|
||||||
id: string;
|
|
||||||
} & Partial<T>) => T;
|
|
||||||
mutate: (this: TLShapeUtil<T, E, M>, shape: T, props: Partial<T>) => Partial<T>;
|
|
||||||
transform: (this: TLShapeUtil<T, E, M>, shape: T, bounds: TLBounds, info: TLTransformInfo<T>) => Partial<T> | void;
|
|
||||||
transformSingle: (this: TLShapeUtil<T, E, M>, shape: T, bounds: TLBounds, info: TLTransformInfo<T>) => Partial<T> | void;
|
|
||||||
updateChildren: <K extends TLShape>(this: TLShapeUtil<T, E, M>, shape: T, children: K[]) => Partial<K>[] | void;
|
|
||||||
onChildrenChange: (this: TLShapeUtil<T, E, M>, shape: T, children: TLShape[]) => Partial<T> | void;
|
|
||||||
onBindingChange: (this: TLShapeUtil<T, E, M>, shape: T, binding: TLBinding, target: TLShape, targetBounds: TLBounds, center: number[]) => Partial<T> | void;
|
|
||||||
onHandleChange: (this: TLShapeUtil<T, E, M>, shape: T, handle: Partial<T['handles']>, info: Partial<TLPointerInfo>) => Partial<T> | void;
|
|
||||||
onRightPointHandle: (this: TLShapeUtil<T, E, M>, shape: T, handle: Partial<T['handles']>, info: Partial<TLPointerInfo>) => Partial<T> | void;
|
|
||||||
onDoubleClickHandle: (this: TLShapeUtil<T, E, M>, shape: T, handle: Partial<T['handles']>, info: Partial<TLPointerInfo>) => Partial<T> | void;
|
|
||||||
onDoubleClickBoundsHandle: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void;
|
|
||||||
onSessionComplete: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void;
|
|
||||||
onStyleChange: (this: TLShapeUtil<T, E, M>, shape: T) => Partial<T> | void;
|
|
||||||
_Component: React.ForwardRefExoticComponent<any>;
|
|
||||||
};
|
|
||||||
export interface IShapeTreeNode<T extends TLShape, M = any> {
|
export interface IShapeTreeNode<T extends TLShape, M = any> {
|
||||||
shape: T;
|
shape: T
|
||||||
children?: IShapeTreeNode<TLShape, M>[];
|
children?: IShapeTreeNode<TLShape, M>[]
|
||||||
isEditing: boolean;
|
isEditing: boolean
|
||||||
isBinding: boolean;
|
isBinding: boolean
|
||||||
isHovered: boolean;
|
isHovered: boolean
|
||||||
isSelected: boolean;
|
isSelected: boolean
|
||||||
isCurrentParent: boolean;
|
isCurrentParent: boolean
|
||||||
meta?: M extends any ? M : never;
|
meta?: M extends any ? M : never
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,10 @@ function InnerTldraw({
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||||
const hideBounds =
|
const hideBounds =
|
||||||
(tlstate.session && tlstate.session.id !== 'brush') || !isSelecting || isSelectedHandlesShape
|
(isInSession && tlstate.session?.constructor.name !== 'BrushSession') ||
|
||||||
|
!isSelecting ||
|
||||||
|
isSelectedHandlesShape ||
|
||||||
|
!!pageState.editingId
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when in session
|
// Hide bounds when not using the select tool, or when in session
|
||||||
const hideHandles = isInSession || !isSelecting
|
const hideHandles = isInSession || !isSelecting
|
||||||
|
|
|
@ -150,6 +150,7 @@ export function PrimaryButton({
|
||||||
name: label,
|
name: label,
|
||||||
isActive,
|
isActive,
|
||||||
})}
|
})}
|
||||||
|
onPointerDown={onClick}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
>
|
>
|
||||||
|
|
|
@ -14,5 +14,6 @@ export const tldrawShapeUtils: Record<TLDrawShapeType, any> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShapeUtils<T extends TLDrawShape>(type: T['type']) {
|
export function getShapeUtils<T extends TLDrawShape>(type: T['type']) {
|
||||||
|
if (!tldrawShapeUtils[type]) throw Error(`Could not find a util of type ${type}`)
|
||||||
return tldrawShapeUtils[type] as TLDrawShapeUtil<T>
|
return tldrawShapeUtils[type] as TLDrawShapeUtil<T>
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
Decoration,
|
Decoration,
|
||||||
TLDrawShapeType,
|
TLDrawShapeType,
|
||||||
TLDrawToolType,
|
|
||||||
DashStyle,
|
DashStyle,
|
||||||
ArrowBinding,
|
ArrowBinding,
|
||||||
TLDrawMeta,
|
TLDrawMeta,
|
||||||
|
@ -26,8 +25,6 @@ import { EASINGS } from '~state/utils'
|
||||||
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
|
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Arrow,
|
type: TLDrawShapeType.Arrow,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Handle,
|
|
||||||
|
|
||||||
canStyleFill: false,
|
canStyleFill: false,
|
||||||
|
|
||||||
pathCache: new WeakMap<ArrowShape, string>(),
|
pathCache: new WeakMap<ArrowShape, string>(),
|
||||||
|
@ -100,7 +97,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
||||||
|
|
||||||
if (isStraightLine) {
|
if (isStraightLine) {
|
||||||
const path = isDraw
|
const path = isDraw
|
||||||
? renderFreehandArrowShaft(shape, arrowDist, easing)
|
? renderFreehandArrowShaft(shape)
|
||||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
|
@ -425,9 +422,20 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
||||||
// And passes through the dragging handle
|
// And passes through the dragging handle
|
||||||
const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin))
|
const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin))
|
||||||
|
|
||||||
if (
|
if (target.type === TLDrawShapeType.Ellipse) {
|
||||||
[TLDrawShapeType.Rectangle, TLDrawShapeType.Text].includes(target.type as TLDrawShapeType)
|
const hits = intersectRayEllipse(
|
||||||
) {
|
origin,
|
||||||
|
direction,
|
||||||
|
center,
|
||||||
|
(target as EllipseShape).radius[0] + binding.meta.distance,
|
||||||
|
(target as EllipseShape).radius[1] + binding.meta.distance,
|
||||||
|
target.rotation || 0
|
||||||
|
).points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
|
||||||
|
|
||||||
|
if (hits[0]) {
|
||||||
|
handlePoint = Vec.sub(hits[0], shape.point)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let hits = intersectRayBounds(origin, direction, intersectBounds, target.rotation)
|
let hits = intersectRayBounds(origin, direction, intersectBounds, target.rotation)
|
||||||
.filter((int) => int.didIntersect)
|
.filter((int) => int.didIntersect)
|
||||||
.map((int) => int.points[0])
|
.map((int) => int.points[0])
|
||||||
|
@ -440,27 +448,9 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
||||||
.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
|
.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hits[0]) {
|
if (hits[0]) {
|
||||||
console.warn('No intersection.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePoint = Vec.sub(hits[0], shape.point)
|
handlePoint = Vec.sub(hits[0], shape.point)
|
||||||
} else if (target.type === TLDrawShapeType.Ellipse) {
|
|
||||||
const hits = intersectRayEllipse(
|
|
||||||
origin,
|
|
||||||
direction,
|
|
||||||
center,
|
|
||||||
(target as EllipseShape).radius[0] + binding.meta.distance,
|
|
||||||
(target as EllipseShape).radius[1] + binding.meta.distance,
|
|
||||||
target.rotation || 0
|
|
||||||
).points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
|
|
||||||
|
|
||||||
if (!hits[0]) {
|
|
||||||
console.warn('No intersections')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePoint = Vec.sub(hits[0], shape.point)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -621,11 +611,7 @@ function getBendPoint(handles: ArrowShape['handles'], bend: number) {
|
||||||
return point
|
return point
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFreehandArrowShaft(
|
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||||
shape: ArrowShape,
|
|
||||||
length: number,
|
|
||||||
easing: (t: number) => number
|
|
||||||
) {
|
|
||||||
const { style, id } = shape
|
const { style, id } = shape
|
||||||
|
|
||||||
const { start, end } = shape.handles
|
const { start, end } = shape.handles
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Vec } from '@tldraw/vec'
|
||||||
import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect'
|
import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect'
|
||||||
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import { DrawShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
|
import { DrawShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||||
import { EASINGS } from '~state/utils'
|
import { EASINGS } from '~state/utils'
|
||||||
|
|
||||||
const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([])
|
const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([])
|
||||||
|
@ -15,8 +15,6 @@ const pointCache: Record<string, number[]> = {}
|
||||||
export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
|
export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Draw,
|
type: TLDrawShapeType.Draw,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Draw,
|
|
||||||
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
type: TLDrawShapeType.Draw,
|
type: TLDrawShapeType.Draw,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { DashStyle, EllipseShape, TLDrawShapeType, TLDrawMeta, TLDrawToolType } from '~types'
|
import { DashStyle, EllipseShape, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||||
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
|
||||||
import getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
import getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||||
import {
|
import {
|
||||||
|
@ -14,8 +14,6 @@ import { EASINGS } from '~state/utils'
|
||||||
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
|
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Ellipse,
|
type: TLDrawShapeType.Ellipse,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Bounds,
|
|
||||||
|
|
||||||
pathCache: new WeakMap<EllipseShape, string>([]),
|
pathCache: new WeakMap<EllipseShape, string>([]),
|
||||||
|
|
||||||
canBind: true,
|
canBind: true,
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { SVGContainer, ShapeUtil } from '@tldraw/core'
|
import { SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||||
import { defaultStyle } from '~shape/shape-styles'
|
import { defaultStyle } from '~shape/shape-styles'
|
||||||
import { GroupShape, TLDrawShapeType, TLDrawToolType, ColorStyle, TLDrawMeta } from '~types'
|
import { GroupShape, TLDrawShapeType, ColorStyle, TLDrawMeta } from '~types'
|
||||||
import { getBoundsRectangle } from '../shared'
|
import { getBoundsRectangle } from '../shared'
|
||||||
import css from '~styles'
|
import css from '~styles'
|
||||||
|
|
||||||
export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() => ({
|
export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Group,
|
type: TLDrawShapeType.Group,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Bounds,
|
|
||||||
|
|
||||||
canBind: true,
|
canBind: true,
|
||||||
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { HTMLContainer, ShapeUtil } from '@tldraw/core'
|
import { HTMLContainer, ShapeUtil } from '@tldraw/core'
|
||||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import { PostItShape, TLDrawMeta, TLDrawShapeType, TLDrawToolType } from '~types'
|
import { PostItShape, TLDrawMeta, TLDrawShapeType } from '~types'
|
||||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
||||||
|
|
||||||
export const PostIt = new ShapeUtil<PostItShape, HTMLDivElement, TLDrawMeta>(() => ({
|
export const PostIt = new ShapeUtil<PostItShape, HTMLDivElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.PostIt,
|
type: TLDrawShapeType.PostIt,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Bounds,
|
|
||||||
|
|
||||||
canBind: true,
|
canBind: true,
|
||||||
|
|
||||||
pathCache: new WeakMap<number[], string>([]),
|
pathCache: new WeakMap<number[], string>([]),
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||||
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
|
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
||||||
import { EASINGS } from '~state/utils'
|
import { EASINGS } from '~state/utils'
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ const pathCache = new WeakMap<number[], string>([])
|
||||||
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TLDrawShapeType.Rectangle,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Bounds,
|
|
||||||
|
|
||||||
canBind: true,
|
canBind: true,
|
||||||
|
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
||||||
import { HTMLContainer, TLBounds, Utils, ShapeUtil } from '@tldraw/core'
|
import { HTMLContainer, TLBounds, Utils, ShapeUtil } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles'
|
import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles'
|
||||||
import { TextShape, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
|
import { TextShape, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||||
import css from '~styles'
|
import css from '~styles'
|
||||||
import TextAreaUtils from './text-utils'
|
import TextAreaUtils from './text-utils'
|
||||||
|
|
||||||
|
@ -54,11 +54,9 @@ if (typeof window !== 'undefined') {
|
||||||
export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => ({
|
export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => ({
|
||||||
type: TLDrawShapeType.Text,
|
type: TLDrawShapeType.Text,
|
||||||
|
|
||||||
toolType: TLDrawToolType.Text,
|
|
||||||
|
|
||||||
isAspectRatioLocked: true,
|
isAspectRatioLocked: true,
|
||||||
|
|
||||||
isEditableText: true,
|
canEdit: true,
|
||||||
|
|
||||||
canBind: true,
|
canBind: true,
|
||||||
|
|
||||||
|
@ -86,6 +84,8 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
||||||
const styles = getShapeStyle(style, meta.isDarkMode)
|
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||||
const font = getFontStyle(shape.style)
|
const font = getFontStyle(shape.style)
|
||||||
|
|
||||||
|
const rIsMounted = React.useRef(false)
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
|
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
|
||||||
|
@ -115,15 +115,20 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
||||||
|
|
||||||
const handleBlur = React.useCallback(
|
const handleBlur = React.useCallback(
|
||||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!isEditing) return
|
||||||
|
if (rIsMounted.current) {
|
||||||
e.currentTarget.setSelectionRange(0, 0)
|
e.currentTarget.setSelectionRange(0, 0)
|
||||||
onShapeBlur?.()
|
onShapeBlur?.()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[isEditing, shape]
|
[isEditing]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFocus = React.useCallback(
|
const handleFocus = React.useCallback(
|
||||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isEditing) return
|
if (!isEditing) return
|
||||||
|
if (!rIsMounted.current) return
|
||||||
|
|
||||||
if (document.activeElement === e.currentTarget) {
|
if (document.activeElement === e.currentTarget) {
|
||||||
e.currentTarget.select()
|
e.currentTarget.select()
|
||||||
}
|
}
|
||||||
|
@ -143,6 +148,7 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
rIsMounted.current = true
|
||||||
const elm = rInput.current!
|
const elm = rInput.current!
|
||||||
elm.focus()
|
elm.focus()
|
||||||
elm.select()
|
elm.select()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { TLDR } from '~state/tldr'
|
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDrawShape, TLDrawShapeType } from '~types'
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
describe('Delete command', () => {
|
describe('Delete command', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -57,14 +56,14 @@ describe('Delete command', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deletes bound shapes, undoes and redoes', () => {
|
it('deletes bound shapes, undoes and redoes', () => {
|
||||||
const tlstate = new TLDrawState()
|
new TLDrawState()
|
||||||
.createShapes(
|
.createShapes(
|
||||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
.delete()
|
.delete()
|
||||||
.undo()
|
.undo()
|
||||||
|
@ -80,8 +79,8 @@ describe('Delete command', () => {
|
||||||
type: TLDrawShapeType.Arrow,
|
type: TLDrawShapeType.Arrow,
|
||||||
})
|
})
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([0, 0], 'start')
|
.startSession(SessionType.Arrow, [0, 0], 'start')
|
||||||
.updateHandleSession([110, 110])
|
.updateSession([110, 110])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const binding = Object.values(tlstate.page.bindings)[0]
|
const binding = Object.values(tlstate.page.bindings)[0]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
describe('Duplicate command', () => {
|
describe('Duplicate command', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -62,8 +62,8 @@ describe('Duplicate command', () => {
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const beforeArrow = tlstate.getShape<ArrowShape>('arrow1')
|
const beforeArrow = tlstate.getShape<ArrowShape>('arrow1')
|
||||||
|
@ -102,8 +102,8 @@ describe('Duplicate command', () => {
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const oldBindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
const oldBindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
describe('Move to page command', () => {
|
describe('Move to page command', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -67,8 +67,8 @@ describe('Move to page command', () => {
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const bindingId = tlstate.bindings[0].id
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
@ -108,8 +108,8 @@ describe('Move to page command', () => {
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const bindingId = tlstate.bindings[0].id
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
@ -149,8 +149,8 @@ describe('Move to page command', () => {
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const bindingId = tlstate.bindings[0].id
|
const bindingId = tlstate.bindings[0].id
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { TLBoundsCorner, Utils } from '@tldraw/core'
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDrawShapeType } from '~types'
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
describe('Reset bounds command', () => {
|
describe('Reset bounds command', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -26,8 +26,8 @@ describe('Reset bounds command', () => {
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('text1')
|
.select('text1')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([-100, -100], false, false)
|
.updateSession([-100, -100], false, false)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const scale = tlstate.getShape('text1').style.scale
|
const scale = tlstate.getShape('text1').style.scale
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
describe('Translate command', () => {
|
describe('Translate command', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -59,8 +59,8 @@ describe('Translate command', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||||
|
@ -98,8 +98,8 @@ describe('Translate command', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||||
|
|
|
@ -1 +1,43 @@
|
||||||
export * from './sessions'
|
export * from './sessions'
|
||||||
|
import { SessionType } from '~types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowSession,
|
||||||
|
BrushSession,
|
||||||
|
DrawSession,
|
||||||
|
HandleSession,
|
||||||
|
RotateSession,
|
||||||
|
TransformSession,
|
||||||
|
TransformSingleSession,
|
||||||
|
TranslateSession,
|
||||||
|
} from './sessions'
|
||||||
|
|
||||||
|
export interface SessionsMap {
|
||||||
|
[SessionType.Arrow]: typeof ArrowSession
|
||||||
|
[SessionType.Brush]: typeof BrushSession
|
||||||
|
[SessionType.Draw]: typeof DrawSession
|
||||||
|
[SessionType.Handle]: typeof HandleSession
|
||||||
|
[SessionType.Rotate]: typeof RotateSession
|
||||||
|
[SessionType.Transform]: typeof TransformSession
|
||||||
|
[SessionType.TransformSingle]: typeof TransformSingleSession
|
||||||
|
[SessionType.Translate]: typeof TranslateSession
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionOfType<K extends SessionType> = SessionsMap[K]
|
||||||
|
|
||||||
|
export type ArgsOfType<K extends SessionType> = ConstructorParameters<SessionOfType<K>>
|
||||||
|
|
||||||
|
export const sessions: { [K in SessionType]: SessionsMap[K] } = {
|
||||||
|
[SessionType.Arrow]: ArrowSession,
|
||||||
|
[SessionType.Brush]: BrushSession,
|
||||||
|
[SessionType.Draw]: DrawSession,
|
||||||
|
[SessionType.Handle]: HandleSession,
|
||||||
|
[SessionType.Rotate]: RotateSession,
|
||||||
|
[SessionType.Transform]: TransformSession,
|
||||||
|
[SessionType.TransformSingle]: TransformSingleSession,
|
||||||
|
[SessionType.Translate]: TranslateSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSession = <K extends SessionType>(type: K): SessionOfType<K> => {
|
||||||
|
return sessions[type]
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { ArrowShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
import { ArrowShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
describe('Arrow session', () => {
|
describe('Arrow session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.selectAll()
|
.selectAll()
|
||||||
|
@ -15,12 +16,12 @@ describe('Arrow session', () => {
|
||||||
|
|
||||||
const restoreDoc = tlstate.document
|
const restoreDoc = tlstate.document
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const binding = tlstate.bindings[0]
|
const binding = tlstate.bindings[0]
|
||||||
|
@ -44,11 +45,11 @@ describe('Arrow session', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cancels session', () => {
|
it('cancels session', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.cancelSession()
|
.cancelSession()
|
||||||
|
|
||||||
expect(tlstate.bindings[0]).toBe(undefined)
|
expect(tlstate.bindings[0]).toBe(undefined)
|
||||||
|
@ -57,78 +58,78 @@ describe('Arrow session', () => {
|
||||||
|
|
||||||
describe('arrow binding', () => {
|
describe('arrow binding', () => {
|
||||||
it('points to the center', () => {
|
it('points to the center', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Snaps to the center', () => {
|
it('Snaps to the center', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([55, 55])
|
.updateSession([55, 55])
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Binds at the bottom left', () => {
|
it('Binds at the bottom left', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([132, -32])
|
.updateSession([132, -32])
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([1, 0])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([1, 0])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Cancels the bind when off of the expanded bounds', () => {
|
it('Cancels the bind when off of the expanded bounds', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([133, 133])
|
.updateSession([133, 133])
|
||||||
|
|
||||||
expect(tlstate.bindings[0]).toBe(undefined)
|
expect(tlstate.bindings[0]).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('binds on the inside of a shape while meta is held', () => {
|
it('binds on the inside of a shape while meta is held', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([91, 9])
|
.updateSession([91, 9])
|
||||||
|
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
||||||
|
|
||||||
tlstate.updateHandleSession([91, 9], false, false, true)
|
tlstate.updateSession([91, 9], false, false, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('snaps to the center when the point is close to the center', () => {
|
it('snaps to the center when the point is close to the center', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([91, 9])
|
.updateSession([91, 9])
|
||||||
|
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
||||||
|
|
||||||
tlstate.updateHandleSession([91, 9], false, false, true)
|
tlstate.updateSession([91, 9], false, false, true)
|
||||||
|
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.75, 0.25])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.75, 0.25])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignores binding when alt is held', () => {
|
it('ignores binding when alt is held', () => {
|
||||||
tlstate
|
const tlstate = new TLDrawState()
|
||||||
.loadDocument(restoreDoc)
|
.loadDocument(restoreDoc)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([55, 45])
|
.updateSession([55, 45])
|
||||||
|
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||||
|
|
||||||
tlstate.updateHandleSession([55, 45], false, false, true)
|
tlstate.updateSession([55, 45], false, false, true)
|
||||||
|
|
||||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||||
})
|
})
|
||||||
|
@ -136,11 +137,13 @@ describe('Arrow session', () => {
|
||||||
|
|
||||||
describe('when dragging a bound shape', () => {
|
describe('when dragging a bound shape', () => {
|
||||||
it('updates the arrow', () => {
|
it('updates the arrow', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
tlstate.loadDocument(restoreDoc)
|
tlstate.loadDocument(restoreDoc)
|
||||||
// Select the arrow and begin a session on the handle's start handle
|
// Select the arrow and begin a session on the handle's start handle
|
||||||
tlstate.select('arrow1').startHandleSession([200, 200], 'start')
|
tlstate.select('arrow1').startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
// Move to [50,50]
|
// Move to [50,50]
|
||||||
tlstate.updateHandleSession([50, 50]).completeSession()
|
tlstate.updateSession([50, 50])
|
||||||
// Both handles will keep the same screen positions, but their points will have changed.
|
// Both handles will keep the same screen positions, but their points will have changed.
|
||||||
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
||||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
||||||
|
|
|
@ -6,14 +6,14 @@ import {
|
||||||
Data,
|
Data,
|
||||||
Session,
|
Session,
|
||||||
TLDrawStatus,
|
TLDrawStatus,
|
||||||
|
SessionType,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import { ThickArrowDownIcon } from '@radix-ui/react-icons'
|
|
||||||
|
|
||||||
export class ArrowSession implements Session {
|
export class ArrowSession implements Session {
|
||||||
id = 'transform_single'
|
static type = SessionType.Arrow
|
||||||
status = TLDrawStatus.TranslatingHandle
|
status = TLDrawStatus.TranslatingHandle
|
||||||
newBindingId = Utils.uniqueId()
|
newBindingId = Utils.uniqueId()
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
@ -26,7 +26,7 @@ export class ArrowSession implements Session {
|
||||||
initialBinding: TLDrawBinding | undefined
|
initialBinding: TLDrawBinding | undefined
|
||||||
didBind = false
|
didBind = false
|
||||||
|
|
||||||
constructor(data: Data, handleId: 'start' | 'end', point: number[]) {
|
constructor(data: Data, point: number[], handleId: 'start' | 'end') {
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
const page = data.document.pages[currentPageId]
|
const page = data.document.pages[currentPageId]
|
||||||
const pageState = data.document.pageStates[currentPageId]
|
const pageState = data.document.pageStates[currentPageId]
|
||||||
|
|
|
@ -1,52 +1,61 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDrawStatus } from '~types'
|
import { SessionType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
describe('Brush session', () => {
|
describe('Brush session', () => {
|
||||||
|
it('begins, updateSession', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
tlstate.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
|
.deselectAll()
|
||||||
it('begins, updates and completes session', () => {
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
tlstate.deselectAll()
|
.updateSession([10, 10])
|
||||||
tlstate.startBrushSession([-10, -10])
|
.completeSession()
|
||||||
tlstate.updateBrushSession([10, 10])
|
|
||||||
tlstate.completeSession()
|
|
||||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||||
expect(tlstate.selectedIds.length).toBe(1)
|
expect(tlstate.selectedIds.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selects multiple shapes', () => {
|
it('selects multiple shapes', () => {
|
||||||
tlstate.deselectAll()
|
const tlstate = new TLDrawState()
|
||||||
tlstate.startBrushSession([-10, -10])
|
.loadDocument(mockDocument)
|
||||||
tlstate.updateBrushSession([110, 110])
|
.deselectAll()
|
||||||
tlstate.completeSession()
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
|
.updateSession([110, 110])
|
||||||
|
.completeSession()
|
||||||
expect(tlstate.selectedIds.length).toBe(3)
|
expect(tlstate.selectedIds.length).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not de-select original shapes', () => {
|
it('does not de-select original shapes', () => {
|
||||||
tlstate.deselectAll()
|
const tlstate = new TLDrawState()
|
||||||
tlstate
|
.loadDocument(mockDocument)
|
||||||
|
.deselectAll()
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startBrushSession([300, 300])
|
.startSession(SessionType.Brush, [300, 300])
|
||||||
.updateBrushSession([301, 301])
|
.updateSession([301, 301])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
expect(tlstate.selectedIds.length).toBe(1)
|
expect(tlstate.selectedIds.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not select hidden shapes', () => {
|
// it('does not select hidden shapes', () => {
|
||||||
tlstate.toggleHidden(['rect1'])
|
// const tlstate = new TLDrawState()
|
||||||
tlstate.deselectAll()
|
// .loadDocument(mockDocument)
|
||||||
tlstate.startBrushSession([-10, -10])
|
// .deselectAll()
|
||||||
tlstate.updateBrushSession([10, 10])
|
// .toggleHidden(['rect1'])
|
||||||
tlstate.completeSession()
|
// .deselectAll()
|
||||||
expect(tlstate.selectedIds.length).toBe(0)
|
// .startSession(SessionType.Brush, [-10, -10])
|
||||||
})
|
// .updateSession([10, 10])
|
||||||
|
// .completeSession()
|
||||||
|
// })
|
||||||
|
|
||||||
it('when command is held, require the entire shape to be selected', () => {
|
it('when command is held, require the entire shape to be selected', () => {
|
||||||
tlstate.loadDocument(mockDocument)
|
const tlstate = new TLDrawState()
|
||||||
tlstate.deselectAll()
|
.loadDocument(mockDocument)
|
||||||
tlstate.startBrushSession([-10, -10])
|
.deselectAll()
|
||||||
tlstate.updateBrushSession([10, 10])
|
.loadDocument(mockDocument)
|
||||||
tlstate.completeSession()
|
.deselectAll()
|
||||||
|
.startSession(SessionType.Brush, [-10, -10])
|
||||||
|
.updateSession([10, 10], false, false, true)
|
||||||
|
.completeSession()
|
||||||
|
|
||||||
|
expect(tlstate.selectedIds.length).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { Data, Session, TLDrawPatch, TLDrawStatus } from '~types'
|
import { Data, Session, SessionType, TLDrawPatch, TLDrawStatus } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
export class BrushSession implements Session {
|
export class BrushSession implements Session {
|
||||||
id = 'brush'
|
static type = SessionType.Brush
|
||||||
status = TLDrawStatus.Brushing
|
status = TLDrawStatus.Brushing
|
||||||
origin: number[]
|
origin: number[]
|
||||||
snapshot: BrushSnapshot
|
snapshot: BrushSnapshot
|
||||||
|
@ -16,7 +16,13 @@ export class BrushSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
update = (data: Data, point: number[], containMode = false): TLDrawPatch => {
|
update = (
|
||||||
|
data: Data,
|
||||||
|
point: number[],
|
||||||
|
shiftKey = false,
|
||||||
|
altKey = false,
|
||||||
|
metaKey = false
|
||||||
|
): TLDrawPatch => {
|
||||||
const { snapshot, origin } = this
|
const { snapshot, origin } = this
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
|
|
||||||
|
@ -36,7 +42,7 @@ export class BrushSession implements Session {
|
||||||
|
|
||||||
if (!hits.has(selectId)) {
|
if (!hits.has(selectId)) {
|
||||||
if (
|
if (
|
||||||
containMode
|
metaKey
|
||||||
? Utils.boundsContain(brush, util.getBounds(shape))
|
? Utils.boundsContain(brush, util.getBounds(shape))
|
||||||
: util.hitTestBounds(shape, brush)
|
: util.hitTestBounds(shape, brush)
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType, TLDrawStatus } from '~types'
|
import {
|
||||||
|
ColorStyle,
|
||||||
|
DashStyle,
|
||||||
|
SessionType,
|
||||||
|
SizeStyle,
|
||||||
|
TLDrawShapeType,
|
||||||
|
TLDrawStatus,
|
||||||
|
} from '~types'
|
||||||
|
|
||||||
describe('Draw session', () => {
|
describe('Draw session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate.loadDocument(mockDocument)
|
tlstate.loadDocument(mockDocument)
|
||||||
|
|
||||||
expect(tlstate.getShape('draw1')).toBe(undefined)
|
expect(tlstate.getShape('draw1')).toBe(undefined)
|
||||||
|
@ -26,8 +33,8 @@ describe('Draw session', () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.select('draw1')
|
.select('draw1')
|
||||||
.startDrawSession('draw1', [0, 0])
|
.startSession(SessionType.Draw, [0, 0], 'draw1')
|
||||||
.updateDrawSession([10, 10], 0.5)
|
.updateSession([10, 10, 0.5])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { Data, DrawShape, Session, TLDrawStatus } from '~types'
|
import { Data, DrawShape, Session, SessionType, TLDrawStatus } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
export class DrawSession implements Session {
|
export class DrawSession implements Session {
|
||||||
id = 'draw'
|
static type = SessionType.Draw
|
||||||
status = TLDrawStatus.Creating
|
status = TLDrawStatus.Creating
|
||||||
topLeft: number[]
|
topLeft: number[]
|
||||||
origin: number[]
|
origin: number[]
|
||||||
|
@ -12,17 +12,16 @@ export class DrawSession implements Session {
|
||||||
last: number[]
|
last: number[]
|
||||||
points: number[][]
|
points: number[][]
|
||||||
shiftedPoints: number[][] = []
|
shiftedPoints: number[][] = []
|
||||||
snapshot: DrawSnapshot
|
shapeId: string
|
||||||
isLocked?: boolean
|
isLocked?: boolean
|
||||||
lockedDirection?: 'horizontal' | 'vertical'
|
lockedDirection?: 'horizontal' | 'vertical'
|
||||||
|
|
||||||
constructor(data: Data, id: string, point: number[]) {
|
constructor(data: Data, point: number[], id: string) {
|
||||||
this.origin = point
|
this.origin = point
|
||||||
this.previous = point
|
this.previous = point
|
||||||
this.last = point
|
this.last = point
|
||||||
this.topLeft = point
|
this.topLeft = point
|
||||||
|
this.shapeId = id
|
||||||
this.snapshot = getDrawSnapshot(data, id)
|
|
||||||
|
|
||||||
// Add a first point but don't update the shape yet. We'll update
|
// Add a first point but don't update the shape yet. We'll update
|
||||||
// when the draw session ends; if the user hasn't added additional
|
// when the draw session ends; if the user hasn't added additional
|
||||||
|
@ -32,8 +31,8 @@ export class DrawSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
update = (data: Data, point: number[], pressure: number, isLocked = false) => {
|
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||||
const { snapshot } = this
|
const { shapeId } = this
|
||||||
|
|
||||||
// Even if we're not locked yet, we base the future locking direction
|
// Even if we're not locked yet, we base the future locking direction
|
||||||
// on the first dimension to reach a threshold, or the bigger dimension
|
// on the first dimension to reach a threshold, or the bigger dimension
|
||||||
|
@ -47,7 +46,7 @@ export class DrawSession implements Session {
|
||||||
|
|
||||||
// Drawing while holding shift will "lock" the pen to either the
|
// Drawing while holding shift will "lock" the pen to either the
|
||||||
// x or y axis, depending on the locking direction.
|
// x or y axis, depending on the locking direction.
|
||||||
if (isLocked) {
|
if (shiftKey) {
|
||||||
if (!this.isLocked && this.points.length > 2) {
|
if (!this.isLocked && this.points.length > 2) {
|
||||||
// If we're locking before knowing what direction we're in, set it
|
// If we're locking before knowing what direction we're in, set it
|
||||||
// early based on the bigger dimension.
|
// early based on the bigger dimension.
|
||||||
|
@ -67,7 +66,7 @@ export class DrawSession implements Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previous = returning
|
this.previous = returning
|
||||||
this.points.push(returning.concat(pressure))
|
this.points.push(returning.concat(point[2]))
|
||||||
}
|
}
|
||||||
} else if (this.isLocked) {
|
} else if (this.isLocked) {
|
||||||
this.isLocked = false
|
this.isLocked = false
|
||||||
|
@ -82,7 +81,7 @@ export class DrawSession implements Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The new adjusted point
|
// The new adjusted point
|
||||||
const newPoint = Vec.round(Vec.sub(point, this.origin)).concat(pressure)
|
const newPoint = Vec.round(Vec.sub(point, this.origin)).concat(point[2])
|
||||||
|
|
||||||
// Don't add duplicate points.
|
// Don't add duplicate points.
|
||||||
if (Vec.isEqual(this.last, newPoint)) return
|
if (Vec.isEqual(this.last, newPoint)) return
|
||||||
|
@ -129,7 +128,7 @@ export class DrawSession implements Session {
|
||||||
pages: {
|
pages: {
|
||||||
[data.appState.currentPageId]: {
|
[data.appState.currentPageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[snapshot.id]: {
|
[shapeId]: {
|
||||||
point: this.topLeft,
|
point: this.topLeft,
|
||||||
points,
|
points,
|
||||||
},
|
},
|
||||||
|
@ -138,7 +137,7 @@ export class DrawSession implements Session {
|
||||||
},
|
},
|
||||||
pageStates: {
|
pageStates: {
|
||||||
[data.appState.currentPageId]: {
|
[data.appState.currentPageId]: {
|
||||||
selectedIds: [snapshot.id],
|
selectedIds: [shapeId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -146,7 +145,7 @@ export class DrawSession implements Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
cancel = (data: Data) => {
|
||||||
const { snapshot } = this
|
const { shapeId } = this
|
||||||
const pageId = data.appState.currentPageId
|
const pageId = data.appState.currentPageId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -154,7 +153,7 @@ export class DrawSession implements Session {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[snapshot.id]: undefined,
|
[shapeId]: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -168,7 +167,7 @@ export class DrawSession implements Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (data: Data) => {
|
complete = (data: Data) => {
|
||||||
const { snapshot } = this
|
const { shapeId } = this
|
||||||
const pageId = data.appState.currentPageId
|
const pageId = data.appState.currentPageId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -178,7 +177,7 @@ export class DrawSession implements Session {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[snapshot.id]: undefined,
|
[shapeId]: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -194,7 +193,7 @@ export class DrawSession implements Session {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
[snapshot.id]: TLDR.getShape(data, snapshot.id, pageId),
|
[shapeId]: TLDR.getShape(data, shapeId, pageId),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -208,18 +207,3 @@ export class DrawSession implements Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function getDrawSnapshot(data: Data, shapeId: string) {
|
|
||||||
const page = { ...TLDR.getPage(data, data.appState.currentPageId) }
|
|
||||||
|
|
||||||
const { points, point } = Utils.deepClone(page.shapes[shapeId]) as DrawShape
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: shapeId,
|
|
||||||
point,
|
|
||||||
points,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DrawSnapshot = ReturnType<typeof getDrawSnapshot>
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDR } from '~state/tldr'
|
import { SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||||
import { TLDrawShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
|
||||||
|
|
||||||
describe('Handle session', () => {
|
describe('Handle session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.createShapes({
|
.createShapes({
|
||||||
|
@ -14,8 +13,8 @@ describe('Handle session', () => {
|
||||||
type: TLDrawShapeType.Arrow,
|
type: TLDrawShapeType.Arrow,
|
||||||
})
|
})
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([-10, -10], 'end')
|
.startSession(SessionType.Arrow, [-10, -10], 'end')
|
||||||
.updateHandleSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||||
|
@ -31,8 +30,8 @@ describe('Handle session', () => {
|
||||||
id: 'arrow1',
|
id: 'arrow1',
|
||||||
})
|
})
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([-10, -10], 'end')
|
.startSession(SessionType.Arrow, [-10, -10], 'end')
|
||||||
.updateHandleSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.cancelSession()
|
.cancelSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { ShapesWithProp, TLDrawStatus } from '~types'
|
import { SessionType, ShapesWithProp, TLDrawStatus } from '~types'
|
||||||
import type { Session } from '~types'
|
import type { Session } from '~types'
|
||||||
import type { Data } from '~types'
|
import type { Data } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
export class HandleSession implements Session {
|
export class HandleSession implements Session {
|
||||||
id = 'transform_single'
|
static type = SessionType.Handle
|
||||||
status = TLDrawStatus.TranslatingHandle
|
status = TLDrawStatus.TranslatingHandle
|
||||||
commandId: string
|
commandId: string
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
@ -15,7 +15,7 @@ export class HandleSession implements Session {
|
||||||
initialShape: ShapesWithProp<'handles'>
|
initialShape: ShapesWithProp<'handles'>
|
||||||
handleId: string
|
handleId: string
|
||||||
|
|
||||||
constructor(data: Data, handleId: string, point: number[], commandId = 'move_handle') {
|
constructor(data: Data, point: number[], handleId: string, commandId = 'move_handle') {
|
||||||
const { currentPageId } = data.appState
|
const { currentPageId } = data.appState
|
||||||
const shapeId = TLDR.getSelectedIds(data, currentPageId)[0]
|
const shapeId = TLDR.getSelectedIds(data, currentPageId)[0]
|
||||||
this.topLeft = point
|
this.topLeft = point
|
||||||
|
|
|
@ -5,5 +5,4 @@ export * from './transform'
|
||||||
export * from './draw'
|
export * from './draw'
|
||||||
export * from './rotate'
|
export * from './rotate'
|
||||||
export * from './handle'
|
export * from './handle'
|
||||||
export * from './text'
|
|
||||||
export * from './arrow'
|
export * from './arrow'
|
||||||
|
|
|
@ -1,37 +1,34 @@
|
||||||
import Vec from '@tldraw/vec'
|
import Vec from '@tldraw/vec'
|
||||||
import Utils from '~../../core/src/utils'
|
import { Utils } from '@tldraw/core'
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLDrawStatus } from '~types'
|
import { SessionType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
describe('Rotate session', () => {
|
describe('Rotate session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate.loadDocument(mockDocument)
|
tlstate.loadDocument(mockDocument)
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||||
|
|
||||||
tlstate
|
tlstate.select('rect1').startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50])
|
||||||
.select('rect1')
|
|
||||||
.startTransformSession([50, 0], 'rotate')
|
|
||||||
.updateTransformSession([100, 50])
|
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI / 2)
|
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI / 2)
|
||||||
|
|
||||||
tlstate.updateTransformSession([50, 100])
|
tlstate.updateSession([50, 100])
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI)
|
expect(tlstate.getShape('rect1').rotation).toBe(Math.PI)
|
||||||
|
|
||||||
tlstate.updateTransformSession([0, 50])
|
tlstate.updateSession([0, 50])
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||||
|
|
||||||
tlstate.updateTransformSession([50, 0])
|
tlstate.updateSession([50, 0])
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe(0)
|
expect(tlstate.getShape('rect1').rotation).toBe(0)
|
||||||
|
|
||||||
tlstate.updateTransformSession([0, 50])
|
tlstate.updateSession([0, 50])
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||||
|
|
||||||
|
@ -52,8 +49,8 @@ describe('Rotate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([50, 0], 'rotate')
|
.startSession(SessionType.Rotate, [50, 0])
|
||||||
.updateTransformSession([100, 50])
|
.updateSession([100, 50])
|
||||||
.cancel()
|
.cancel()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
@ -64,10 +61,12 @@ describe('Rotate session', () => {
|
||||||
describe('when rotating a single shape while pressing shift', () => {
|
describe('when rotating a single shape while pressing shift', () => {
|
||||||
it('Clamps rotation to 15 degrees', () => {
|
it('Clamps rotation to 15 degrees', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], 'rotate')
|
.startSession(SessionType.Rotate, [0, 0])
|
||||||
.updateTransformSession([20, 10], true)
|
.updateSession([20, 10], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||||
|
@ -78,17 +77,19 @@ describe('Rotate session', () => {
|
||||||
it('Clamps rotation to 15 degrees when starting from a rotation', () => {
|
it('Clamps rotation to 15 degrees when starting from a rotation', () => {
|
||||||
// Rect 1 is a little rotated
|
// Rect 1 is a little rotated
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], 'rotate')
|
.startSession(SessionType.Rotate, [0, 0])
|
||||||
.updateTransformSession([5, 5])
|
.updateSession([5, 5])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
// Rect 1 clamp rotated, starting from a little rotation
|
// Rect 1 clamp rotated, starting from a little rotation
|
||||||
tlstate
|
tlstate
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], 'rotate')
|
.startSession(SessionType.Rotate, [0, 0])
|
||||||
.updateTransformSession([100, 200], true)
|
.updateSession([100, 200], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||||
|
@ -98,8 +99,8 @@ describe('Rotate session', () => {
|
||||||
// Try again, too.
|
// Try again, too.
|
||||||
tlstate
|
tlstate
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], 'rotate')
|
.startSession(SessionType.Rotate, [0, 0])
|
||||||
.updateTransformSession([-100, 5000], true)
|
.updateSession([-100, 5000], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||||
|
@ -118,7 +119,7 @@ describe('Rotate session', () => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
tlstate.startTransformSession([50, 0], 'rotate').updateTransformSession([100, 50])
|
tlstate.startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50])
|
||||||
|
|
||||||
const centerAfter = Vec.round(
|
const centerAfter = Vec.round(
|
||||||
Utils.getBoundsCenter(
|
Utils.getBoundsCenter(
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { Session, TLDrawShape, TLDrawStatus } from '~types'
|
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||||
import type { Data } from '~types'
|
import type { Data } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
const centerCache = new WeakMap<string[], number[]>()
|
const centerCache = new WeakMap<string[], number[]>()
|
||||||
|
|
||||||
export class RotateSession implements Session {
|
export class RotateSession implements Session {
|
||||||
id = 'rotate'
|
static type = SessionType.Rotate
|
||||||
status = TLDrawStatus.Transforming
|
status = TLDrawStatus.Transforming
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
origin: number[]
|
origin: number[]
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './text.session'
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { TLDrawState } from '~state'
|
|
||||||
import { mockDocument } from '~test'
|
|
||||||
import { TLDR } from '~state/tldr'
|
|
||||||
import { TextShape, TLDrawShape, TLDrawShapeType } from '~types'
|
|
||||||
|
|
||||||
describe('Text session', () => {
|
|
||||||
const tlstate = new TLDrawState()
|
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
|
||||||
tlstate
|
|
||||||
.loadDocument(mockDocument)
|
|
||||||
.createShapes({
|
|
||||||
id: 'text1',
|
|
||||||
type: TLDrawShapeType.Text,
|
|
||||||
})
|
|
||||||
.select('text1')
|
|
||||||
.startTextSession('text1')
|
|
||||||
.updateTextSession('Hello world')
|
|
||||||
.completeSession()
|
|
||||||
.undo()
|
|
||||||
.redo()
|
|
||||||
|
|
||||||
expect(tlstate.getShape<TextShape>('text1').text).toStrictEqual('Hello world')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cancels session', () => {
|
|
||||||
tlstate
|
|
||||||
.loadDocument(mockDocument)
|
|
||||||
.createShapes({
|
|
||||||
id: 'text1',
|
|
||||||
type: TLDrawShapeType.Text,
|
|
||||||
})
|
|
||||||
.select('text1')
|
|
||||||
.startTextSession('text1')
|
|
||||||
.updateTextSession('Hello world')
|
|
||||||
.cancelSession()
|
|
||||||
|
|
||||||
expect(tlstate.getShape<TextShape>('text1').text).toStrictEqual('Hello world')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,173 +0,0 @@
|
||||||
import { TextShape, TLDrawStatus } from '~types'
|
|
||||||
import type { Session } from '~types'
|
|
||||||
import type { Data } from '~types'
|
|
||||||
import { TLDR } from '~state/tldr'
|
|
||||||
|
|
||||||
export class TextSession implements Session {
|
|
||||||
id = 'text'
|
|
||||||
status = TLDrawStatus.EditingText
|
|
||||||
initialShape: TextShape
|
|
||||||
|
|
||||||
constructor(data: Data, id?: string) {
|
|
||||||
const pageId = data.appState.currentPageId
|
|
||||||
this.initialShape = TLDR.getShape(data, id || TLDR.getSelectedIds(data, pageId)[0], pageId)
|
|
||||||
}
|
|
||||||
|
|
||||||
start = (data: Data) => {
|
|
||||||
const pageId = data.appState.currentPageId
|
|
||||||
return {
|
|
||||||
document: {
|
|
||||||
pageStates: {
|
|
||||||
[pageId]: {
|
|
||||||
editingId: this.initialShape.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update = (data: Data, text: string) => {
|
|
||||||
const { initialShape } = this
|
|
||||||
const pageId = data.appState.currentPageId
|
|
||||||
|
|
||||||
// let nextShape: TextShape = {
|
|
||||||
// ...TLDR.getShape<TextShape>(data, initialShape.id, pageId),
|
|
||||||
// text,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// nextShape = {
|
|
||||||
// ...nextShape,
|
|
||||||
// ...TLDR.getShapeUtils(nextShape).onStyleChange(nextShape),
|
|
||||||
// } as TextShape
|
|
||||||
|
|
||||||
return {
|
|
||||||
document: {
|
|
||||||
pages: {
|
|
||||||
[pageId]: {
|
|
||||||
shapes: {
|
|
||||||
[initialShape.id]: {
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
|
||||||
const { initialShape } = this
|
|
||||||
const pageId = data.appState.currentPageId
|
|
||||||
|
|
||||||
return {
|
|
||||||
document: {
|
|
||||||
pages: {
|
|
||||||
[pageId]: {
|
|
||||||
shapes: {
|
|
||||||
[initialShape.id]: TLDR.onSessionComplete(
|
|
||||||
TLDR.getShape(data, initialShape.id, pageId)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pageState: {
|
|
||||||
[pageId]: {
|
|
||||||
editingId: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
complete(data: Data) {
|
|
||||||
const { initialShape } = this
|
|
||||||
const pageId = data.appState.currentPageId
|
|
||||||
|
|
||||||
const shape = TLDR.getShape<TextShape>(data, initialShape.id, pageId)
|
|
||||||
|
|
||||||
// TODO: Delete text shape if its content is empty
|
|
||||||
// TODO: ...and prevent `isCreating` from selecting the deleted shape
|
|
||||||
|
|
||||||
// if (initialShape.text.trim() === '' && shape.text.trim() === '') {
|
|
||||||
// // delete shape
|
|
||||||
// return {
|
|
||||||
// id: 'text',
|
|
||||||
// before: {
|
|
||||||
// document: {
|
|
||||||
// pages: {
|
|
||||||
// [pageId]: {
|
|
||||||
// shapes: {
|
|
||||||
// [initialShape.id]: undefined,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// pageState: {
|
|
||||||
// [pageId]: {
|
|
||||||
// editingId: undefined,
|
|
||||||
// selectedIds: [initialShape.id],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// after: {
|
|
||||||
// document: {
|
|
||||||
// pages: {
|
|
||||||
// [pageId]: {
|
|
||||||
// shapes: {
|
|
||||||
// [initialShape.id]: undefined,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// pageState: {
|
|
||||||
// [pageId]: {
|
|
||||||
// editingId: undefined,
|
|
||||||
// selectedIds: [],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (shape.text === initialShape.text) return undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'text',
|
|
||||||
before: {
|
|
||||||
document: {
|
|
||||||
pages: {
|
|
||||||
[pageId]: {
|
|
||||||
shapes: {
|
|
||||||
[initialShape.id]: initialShape,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pageState: {
|
|
||||||
[pageId]: {
|
|
||||||
editingId: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
after: {
|
|
||||||
document: {
|
|
||||||
pages: {
|
|
||||||
[pageId]: {
|
|
||||||
shapes: {
|
|
||||||
[initialShape.id]: TLDR.onSessionComplete(
|
|
||||||
TLDR.getShape(data, initialShape.id, pageId)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pageState: {
|
|
||||||
[pageId]: {
|
|
||||||
editingId: null,
|
|
||||||
selectedIds: [initialShape.id],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLBoundsCorner } from '@tldraw/core'
|
import { TLBoundsCorner } from '@tldraw/core'
|
||||||
import { TLDrawStatus } from '~types'
|
import { SessionType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
describe('Transform single session', () => {
|
describe('Transform single session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([-10, -10], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.TransformSingle, [-10, -10], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||||
|
@ -23,8 +23,8 @@ describe('Transform single session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([5, 5], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.TransformSingle, [5, 5], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.cancelSession()
|
.cancelSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { TLDrawShape, TLDrawStatus } from '~types'
|
import { SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||||
import type { Session } from '~types'
|
import type { Session } from '~types'
|
||||||
import type { Data } from '~types'
|
import type { Data } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
export class TransformSingleSession implements Session {
|
export class TransformSingleSession implements Session {
|
||||||
id = 'transform_single'
|
type = SessionType.TransformSingle
|
||||||
status = TLDrawStatus.Transforming
|
status = TLDrawStatus.Transforming
|
||||||
commandId: string
|
commandId: string
|
||||||
transformType: TLBoundsEdge | TLBoundsCorner
|
transformType: TLBoundsEdge | TLBoundsCorner
|
||||||
|
@ -29,7 +29,7 @@ export class TransformSingleSession implements Session {
|
||||||
|
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
update = (data: Data, point: number[], isAspectRatioLocked = false) => {
|
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||||
const { transformType } = this
|
const { transformType } = this
|
||||||
|
|
||||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||||
|
@ -45,7 +45,7 @@ export class TransformSingleSession implements Session {
|
||||||
transformType,
|
transformType,
|
||||||
Vec.sub(point, this.origin),
|
Vec.sub(point, this.origin),
|
||||||
shape.rotation,
|
shape.rotation,
|
||||||
isAspectRatioLocked || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
shiftKey || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||||
)
|
)
|
||||||
|
|
||||||
const change = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
|
const change = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { TLBoundsCorner, Utils } from '@tldraw/core'
|
import { TLBoundsCorner, Utils } from '@tldraw/core'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import { TLDrawStatus } from '~types'
|
import { SessionType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
||||||
return Utils.getCommonBounds(
|
return Utils.getCommonBounds(
|
||||||
|
@ -13,7 +13,7 @@ function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
||||||
describe('Transform session', () => {
|
describe('Transform session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate.loadDocument(mockDocument)
|
tlstate.loadDocument(mockDocument)
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||||
|
@ -27,8 +27,8 @@ describe('Transform session', () => {
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||||
|
@ -60,8 +60,8 @@ describe('Transform session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTransformSession([5, 5], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.cancelSession()
|
.cancelSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
@ -72,8 +72,8 @@ describe('Transform session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate)).toMatchObject({
|
expect(getShapeBounds(tlstate)).toMatchObject({
|
||||||
|
@ -90,8 +90,8 @@ describe('Transform session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([20, 10], true)
|
.updateSession([20, 10], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||||
|
@ -108,8 +108,8 @@ describe('Transform session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||||
|
@ -135,8 +135,8 @@ describe('Transform session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([20, 10], true)
|
.updateSession([20, 10], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||||
|
@ -194,8 +194,8 @@ describe('Transform session', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.select('groupA')
|
.select('groupA')
|
||||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||||
.updateTransformSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { Session, TLDrawShape, TLDrawStatus } from '~types'
|
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||||
import type { Data } from '~types'
|
import type { Data } from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
export class TransformSession implements Session {
|
export class TransformSession implements Session {
|
||||||
id = 'transform'
|
static type = SessionType.Transform
|
||||||
status = TLDrawStatus.Transforming
|
status = TLDrawStatus.Transforming
|
||||||
scaleX = 1
|
scaleX = 1
|
||||||
scaleY = 1
|
scaleY = 1
|
||||||
|
@ -26,7 +26,7 @@ export class TransformSession implements Session {
|
||||||
start = () => void null
|
start = () => void null
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
update = (data: Data, point: number[], isAspectRatioLocked = false, _altKey = false) => {
|
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean, metaKey: boolean) => {
|
||||||
const {
|
const {
|
||||||
transformType,
|
transformType,
|
||||||
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
||||||
|
@ -41,7 +41,7 @@ export class TransformSession implements Session {
|
||||||
transformType,
|
transformType,
|
||||||
Vec.sub(point, this.origin),
|
Vec.sub(point, this.origin),
|
||||||
pageState.boundsRotation,
|
pageState.boundsRotation,
|
||||||
isAspectRatioLocked || isAllAspectRatioLocked
|
shiftKey || isAllAspectRatioLocked
|
||||||
)
|
)
|
||||||
|
|
||||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { TLDrawState } from '~state'
|
import { TLDrawState } from '~state'
|
||||||
import { mockDocument } from '~test'
|
import { mockDocument } from '~test'
|
||||||
import { GroupShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
import { GroupShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||||
|
|
||||||
describe('Translate session', () => {
|
describe('Translate session', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
|
||||||
it('begins, updates and completes session', () => {
|
it('begins, updateSession', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTranslateSession([5, 5])
|
.startSession(SessionType.Translate, [5, 5])
|
||||||
.updateTranslateSession([10, 10])
|
.updateSession([10, 10])
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTranslateSession([5, 5])
|
.startSession(SessionType.Translate, [5, 5])
|
||||||
.updateTranslateSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.cancelSession()
|
.cancelSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
@ -44,8 +44,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20])
|
.updateSession([20, 20])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
||||||
|
@ -55,8 +55,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], true)
|
.updateSession([20, 20], true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 0])
|
||||||
|
@ -66,8 +66,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20])
|
.updateSession([20, 20])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
||||||
|
@ -78,8 +78,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
@ -100,8 +100,8 @@ describe('Translate session', () => {
|
||||||
tlstate
|
tlstate
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||||
|
@ -117,12 +117,12 @@ describe('Translate session', () => {
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
|
|
||||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(5)
|
expect(Object.keys(tlstate.getPage().shapes).length).toBe(5)
|
||||||
|
|
||||||
tlstate.updateTranslateSession([30, 30], false, false)
|
tlstate.updateSession([30, 30], false, false)
|
||||||
|
|
||||||
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
|
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
|
||||||
|
|
||||||
|
@ -156,16 +156,16 @@ describe('Translate session', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([50, 50])
|
.updateSession([50, 50])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.bindings.length).toBe(1)
|
expect(tlstate.bindings.length).toBe(1)
|
||||||
|
|
||||||
tlstate
|
tlstate
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([30, 30])
|
.updateSession([30, 30])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
// expect(tlstate.bindings.length).toBe(0)
|
// expect(tlstate.bindings.length).toBe(0)
|
||||||
|
@ -191,8 +191,8 @@ describe('Translate session', () => {
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, false)
|
.updateSession([20, 20], false, false)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
||||||
|
@ -218,8 +218,8 @@ describe('Translate session', () => {
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.select('rect1')
|
.select('rect1')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
const children = tlstate.getShape<GroupShape>('groupA').children
|
const children = tlstate.getShape<GroupShape>('groupA').children
|
||||||
|
@ -257,8 +257,8 @@ describe('Translate session', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, false)
|
.updateSession([20, 20], false, false)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
||||||
|
@ -283,8 +283,8 @@ describe('Translate session', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group()
|
.group()
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -293,10 +293,10 @@ describe('Translate session', () => {
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group()
|
.group()
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.updateTranslateSession([20, 20], false, false)
|
.updateSession([20, 20], false, false)
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -306,8 +306,8 @@ describe('Translate session', () => {
|
||||||
.select('rect1', 'rect2')
|
.select('rect1', 'rect2')
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.select('groupA', 'rect3')
|
.select('groupA', 'rect3')
|
||||||
.startTranslateSession([10, 10])
|
.startSession(SessionType.Translate, [10, 10])
|
||||||
.updateTranslateSession([20, 20], false, true)
|
.updateSession([20, 20], false, true)
|
||||||
.completeSession()
|
.completeSession()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,12 +10,13 @@ import {
|
||||||
TLDrawStatus,
|
TLDrawStatus,
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
GroupShape,
|
GroupShape,
|
||||||
|
SessionType,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import { TLDR } from '~state/tldr'
|
import { TLDR } from '~state/tldr'
|
||||||
import type { Patch } from 'rko'
|
import type { Patch } from 'rko'
|
||||||
|
|
||||||
export class TranslateSession implements Session {
|
export class TranslateSession implements Session {
|
||||||
id = 'translate'
|
type = SessionType.Translate
|
||||||
status = TLDrawStatus.Translating
|
status = TLDrawStatus.Translating
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
prev = [0, 0]
|
prev = [0, 0]
|
||||||
|
@ -48,7 +49,7 @@ export class TranslateSession implements Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update = (data: Data, point: number[], isAligned = false, isCloning = false) => {
|
update = (data: Data, point: number[], shiftKey: boolean, altKey: boolean) => {
|
||||||
const { selectedIds, initialParentChildren, clones, initialShapes, bindingsToDelete } =
|
const { selectedIds, initialParentChildren, clones, initialShapes, bindingsToDelete } =
|
||||||
this.snapshot
|
this.snapshot
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ export class TranslateSession implements Session {
|
||||||
|
|
||||||
const delta = Vec.sub(point, this.origin)
|
const delta = Vec.sub(point, this.origin)
|
||||||
|
|
||||||
if (isAligned) {
|
if (shiftKey) {
|
||||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||||
delta[0] = 0
|
delta[0] = 0
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,7 +74,7 @@ export class TranslateSession implements Session {
|
||||||
this.prev = delta
|
this.prev = delta
|
||||||
|
|
||||||
// If cloning...
|
// If cloning...
|
||||||
if (isCloning) {
|
if (altKey) {
|
||||||
// Not Cloning -> Cloning
|
// Not Cloning -> Cloning
|
||||||
if (!this.isCloning) {
|
if (!this.isCloning) {
|
||||||
this.isCloning = true
|
this.isCloning = true
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { TLDrawState } from './tlstate'
|
import { TLDrawState } from './tlstate'
|
||||||
import { mockDocument, TLStateUtils } from '~test'
|
import { mockDocument, TLStateUtils } from '~test'
|
||||||
import { ArrowShape, ColorStyle, TLDrawShapeType } from '~types'
|
import { ArrowShape, ColorStyle, SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import type { TextTool } from './tool/TextTool'
|
||||||
|
import type { SelectTool } from './tool/SelectTool'
|
||||||
|
import { Shape } from '~../../core/src/components/shape/shape'
|
||||||
|
|
||||||
describe('TLDrawState', () => {
|
describe('TLDrawState', () => {
|
||||||
const tlstate = new TLDrawState()
|
const tlstate = new TLDrawState()
|
||||||
|
@ -61,8 +65,8 @@ describe('TLDrawState', () => {
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([55, 55])
|
.updateSession([55, 55])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.bindings.length).toBe(1)
|
expect(tlstate.bindings.length).toBe(1)
|
||||||
|
@ -87,8 +91,8 @@ describe('TLDrawState', () => {
|
||||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||||
)
|
)
|
||||||
.select('arrow1')
|
.select('arrow1')
|
||||||
.startHandleSession([200, 200], 'start')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([55, 55])
|
.updateSession([55, 55])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
|
|
||||||
expect(tlstate.bindings.length).toBe(1)
|
expect(tlstate.bindings.length).toBe(1)
|
||||||
|
@ -138,8 +142,8 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
it('clears selection when clicking bounds', () => {
|
it('clears selection when clicking bounds', () => {
|
||||||
tlstate.loadDocument(mockDocument).deselectAll()
|
tlstate.loadDocument(mockDocument).deselectAll()
|
||||||
tlstate.startBrushSession([-10, -10])
|
tlstate.startSession(SessionType.Brush, [-10, -10])
|
||||||
tlstate.updateBrushSession([110, 110])
|
tlstate.updateSession([110, 110])
|
||||||
tlstate.completeSession()
|
tlstate.completeSession()
|
||||||
expect(tlstate.selectedIds.length).toBe(3)
|
expect(tlstate.selectedIds.length).toBe(3)
|
||||||
})
|
})
|
||||||
|
@ -301,8 +305,8 @@ describe('TLDrawState', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.select('arrow')
|
.select('arrow')
|
||||||
.startHandleSession([200, 200], 'start', 'arrow')
|
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||||
.updateHandleSession([10, 10])
|
.updateSession([10, 10])
|
||||||
.completeSession()
|
.completeSession()
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.style({ color: ColorStyle.Red })
|
.style({ color: ColorStyle.Red })
|
||||||
|
@ -340,7 +344,7 @@ describe('TLDrawState', () => {
|
||||||
|
|
||||||
const tlu = new TLStateUtils(tlstate)
|
const tlu = new TLStateUtils(tlstate)
|
||||||
tlu.doubleClickShape('rect1')
|
tlu.doubleClickShape('rect1')
|
||||||
expect(tlstate.selectedGroupId).toStrictEqual('groupA')
|
expect((tlstate.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -407,17 +411,35 @@ describe('TLDrawState', () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.selectTool(TLDrawShapeType.Rectangle)
|
.selectTool(TLDrawShapeType.Rectangle)
|
||||||
.createActiveToolShape([0, 0], 'rect4')
|
|
||||||
|
|
||||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
const tlu = new TLStateUtils(tlstate)
|
||||||
|
|
||||||
tlstate
|
const prevA = tlstate.shapes.map((shape) => shape.id)
|
||||||
.group(['rect2', 'rect3', 'rect4'], 'groupA')
|
|
||||||
.selectTool(TLDrawShapeType.Rectangle)
|
tlu.pointCanvas({ x: 0, y: 0 })
|
||||||
.createActiveToolShape([0, 0], 'rect5')
|
tlu.movePointer({ x: 100, y: 100 })
|
||||||
|
tlu.stopPointing()
|
||||||
|
|
||||||
|
const newIdA = tlstate.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))!
|
||||||
|
const shapeA = tlstate.getShape(newIdA)
|
||||||
|
expect(shapeA.childIndex).toBe(4)
|
||||||
|
|
||||||
|
tlstate.group(['rect2', 'rect3', newIdA], 'groupA')
|
||||||
|
|
||||||
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
expect(tlstate.getShape('groupA').childIndex).toBe(2)
|
||||||
expect(tlstate.getShape('rect5').childIndex).toBe(3)
|
|
||||||
|
tlstate.deselectAll()
|
||||||
|
tlstate.selectTool(TLDrawShapeType.Rectangle)
|
||||||
|
|
||||||
|
const prevB = tlstate.shapes.map((shape) => shape.id)
|
||||||
|
|
||||||
|
tlu.pointCanvas({ x: 0, y: 0 })
|
||||||
|
tlu.movePointer({ x: 100, y: 100 })
|
||||||
|
tlu.stopPointing()
|
||||||
|
|
||||||
|
const newIdB = tlstate.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))!
|
||||||
|
const shapeB = tlstate.getShape(newIdB)
|
||||||
|
expect(shapeB.childIndex).toBe(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { ArrowTool } from '.'
|
||||||
|
|
||||||
|
describe('ArrowTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new ArrowTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
65
packages/tldraw/src/state/tool/ArrowTool/ArrowTool.ts
Normal file
65
packages/tldraw/src/state/tool/ArrowTool/ArrowTool.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||||
|
import { Arrow } from '~shape/shapes'
|
||||||
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArrowTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.Arrow
|
||||||
|
|
||||||
|
status = Status.Idle
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
|
||||||
|
const {
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const childIndex = this.getNextChildIndex()
|
||||||
|
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
|
const newShape = Arrow.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point: pagePoint,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.createShapes(newShape)
|
||||||
|
|
||||||
|
this.state.startSession(SessionType.Arrow, pagePoint, 'end')
|
||||||
|
|
||||||
|
this.setStatus(Status.Creating)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerUp: TLPointerEventHandler = () => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
this.state.completeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/ArrowTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/ArrowTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ArrowTool'
|
98
packages/tldraw/src/state/tool/BaseTool/BaseTool.ts
Normal file
98
packages/tldraw/src/state/tool/BaseTool/BaseTool.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import type {
|
||||||
|
TLBoundsEventHandler,
|
||||||
|
TLBoundsHandleEventHandler,
|
||||||
|
TLCanvasEventHandler,
|
||||||
|
TLKeyboardEventHandler,
|
||||||
|
TLPinchEventHandler,
|
||||||
|
TLPointerEventHandler,
|
||||||
|
TLWheelEventHandler,
|
||||||
|
} from '~../../core/src/types'
|
||||||
|
import type { TLDrawState } from '~state'
|
||||||
|
import type { TLDrawShapeType } from '~types'
|
||||||
|
|
||||||
|
export abstract class BaseTool {
|
||||||
|
abstract type: TLDrawShapeType | 'select'
|
||||||
|
|
||||||
|
state: TLDrawState
|
||||||
|
|
||||||
|
constructor(state: TLDrawState) {
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextChildIndex = () => {
|
||||||
|
const {
|
||||||
|
shapes,
|
||||||
|
appState: { currentPageId },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return shapes.length === 0
|
||||||
|
? 1
|
||||||
|
: shapes
|
||||||
|
.filter((shape) => shape.parentId === currentPageId)
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel?: () => void
|
||||||
|
|
||||||
|
// Keyboard events
|
||||||
|
onKeyDown?: TLKeyboardEventHandler
|
||||||
|
onKeyUp?: TLKeyboardEventHandler
|
||||||
|
|
||||||
|
// Camera Events
|
||||||
|
onPinchStart?: TLPinchEventHandler
|
||||||
|
onPinchEnd?: TLPinchEventHandler
|
||||||
|
onPinch?: TLPinchEventHandler
|
||||||
|
onPan?: TLWheelEventHandler
|
||||||
|
onZoom?: TLWheelEventHandler
|
||||||
|
|
||||||
|
// Pointer Events
|
||||||
|
onPointerMove?: TLPointerEventHandler
|
||||||
|
onPointerUp?: TLPointerEventHandler
|
||||||
|
onPointerDown?: TLPointerEventHandler
|
||||||
|
|
||||||
|
// Canvas (background)
|
||||||
|
onPointCanvas?: TLCanvasEventHandler
|
||||||
|
onDoubleClickCanvas?: TLCanvasEventHandler
|
||||||
|
onRightPointCanvas?: TLCanvasEventHandler
|
||||||
|
onDragCanvas?: TLCanvasEventHandler
|
||||||
|
onReleaseCanvas?: TLCanvasEventHandler
|
||||||
|
|
||||||
|
// Shape
|
||||||
|
onPointShape?: TLPointerEventHandler
|
||||||
|
onDoubleClickShape?: TLPointerEventHandler
|
||||||
|
onRightPointShape?: TLPointerEventHandler
|
||||||
|
onDragShape?: TLPointerEventHandler
|
||||||
|
onHoverShape?: TLPointerEventHandler
|
||||||
|
onUnhoverShape?: TLPointerEventHandler
|
||||||
|
onReleaseShape?: TLPointerEventHandler
|
||||||
|
|
||||||
|
// Bounds (bounding box background)
|
||||||
|
onPointBounds?: TLBoundsEventHandler
|
||||||
|
onDoubleClickBounds?: TLBoundsEventHandler
|
||||||
|
onRightPointBounds?: TLBoundsEventHandler
|
||||||
|
onDragBounds?: TLBoundsEventHandler
|
||||||
|
onHoverBounds?: TLBoundsEventHandler
|
||||||
|
onUnhoverBounds?: TLBoundsEventHandler
|
||||||
|
onReleaseBounds?: TLBoundsEventHandler
|
||||||
|
|
||||||
|
// Bounds handles (corners, edges)
|
||||||
|
onPointBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onDoubleClickBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onRightPointBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onDragBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onHoverBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onUnhoverBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
onReleaseBoundsHandle?: TLBoundsHandleEventHandler
|
||||||
|
|
||||||
|
// Handles (ie the handles of a selected arrow)
|
||||||
|
onPointHandle?: TLPointerEventHandler
|
||||||
|
onDoubleClickHandle?: TLPointerEventHandler
|
||||||
|
onRightPointHandle?: TLPointerEventHandler
|
||||||
|
onDragHandle?: TLPointerEventHandler
|
||||||
|
onHoverHandle?: TLPointerEventHandler
|
||||||
|
onUnhoverHandle?: TLPointerEventHandler
|
||||||
|
onReleaseHandle?: TLPointerEventHandler
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
onShapeBlur?: () => void
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/BaseTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/BaseTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './BaseTool'
|
9
packages/tldraw/src/state/tool/DrawTool/DrawTool.spec.ts
Normal file
9
packages/tldraw/src/state/tool/DrawTool/DrawTool.spec.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { DrawTool } from '.'
|
||||||
|
|
||||||
|
describe('DrawTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new DrawTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
72
packages/tldraw/src/state/tool/DrawTool/DrawTool.ts
Normal file
72
packages/tldraw/src/state/tool/DrawTool/DrawTool.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import type { TLPointerEventHandler } from '~../../core/src/types'
|
||||||
|
import Utils from '~../../core/src/utils'
|
||||||
|
import { Draw } from '~shape/shapes'
|
||||||
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DrawTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.Draw
|
||||||
|
|
||||||
|
status = Status.Idle
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
|
||||||
|
const {
|
||||||
|
shapes,
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const childIndex =
|
||||||
|
shapes.length === 0
|
||||||
|
? 1
|
||||||
|
: shapes
|
||||||
|
.filter((shape) => shape.parentId === currentPageId)
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||||
|
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
|
const newShape = Draw.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point: pagePoint,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.createShapes(newShape)
|
||||||
|
|
||||||
|
this.state.startSession(SessionType.Draw, pagePoint, id)
|
||||||
|
|
||||||
|
this.setStatus(Status.Creating)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerUp: TLPointerEventHandler = () => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
this.state.completeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/DrawTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/DrawTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './DrawTool'
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { EllipseTool } from '.'
|
||||||
|
|
||||||
|
describe('EllipseTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new EllipseTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
64
packages/tldraw/src/state/tool/EllipseTool/EllipseTool.ts
Normal file
64
packages/tldraw/src/state/tool/EllipseTool/EllipseTool.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||||
|
import { Ellipse } from '~shape/shapes'
|
||||||
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
}
|
||||||
|
export class EllipseTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.Ellipse
|
||||||
|
|
||||||
|
status = Status.Idle
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
|
||||||
|
const {
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const childIndex = this.getNextChildIndex()
|
||||||
|
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
|
const newShape = Ellipse.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point: pagePoint,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.createShapes(newShape)
|
||||||
|
|
||||||
|
this.state.startSession(SessionType.Transform, pagePoint, TLBoundsCorner.BottomRight)
|
||||||
|
|
||||||
|
this.setStatus(Status.Creating)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerUp: TLPointerEventHandler = () => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
this.state.completeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/EllipseTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/EllipseTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './EllipseTool'
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { RectangleTool } from '.'
|
||||||
|
|
||||||
|
describe('RectangleTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new RectangleTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,65 @@
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core'
|
||||||
|
import { Rectangle } from '~shape/shapes'
|
||||||
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RectangleTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.Rectangle
|
||||||
|
|
||||||
|
status = Status.Idle
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
|
||||||
|
const {
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const childIndex = this.getNextChildIndex()
|
||||||
|
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
|
const newShape = Rectangle.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point: pagePoint,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.createShapes(newShape)
|
||||||
|
|
||||||
|
this.state.startSession(SessionType.Transform, pagePoint, TLBoundsCorner.BottomRight)
|
||||||
|
|
||||||
|
this.setStatus(Status.Creating)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
this.state.updateSession(pagePoint, info.shiftKey, info.altKey, info.metaKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerUp: TLPointerEventHandler = () => {
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
this.state.completeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/RectangleTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/RectangleTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './RectangleTool'
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { SelectTool } from '.'
|
||||||
|
|
||||||
|
describe('SelectTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new SelectTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
442
packages/tldraw/src/state/tool/SelectTool/SelectTool.ts
Normal file
442
packages/tldraw/src/state/tool/SelectTool/SelectTool.ts
Normal file
|
@ -0,0 +1,442 @@
|
||||||
|
import type {
|
||||||
|
TLBoundsCorner,
|
||||||
|
TLBoundsEdge,
|
||||||
|
TLBoundsEventHandler,
|
||||||
|
TLBoundsHandleEventHandler,
|
||||||
|
TLCanvasEventHandler,
|
||||||
|
TLPointerEventHandler,
|
||||||
|
TLKeyboardEventHandler,
|
||||||
|
} from '@tldraw/core'
|
||||||
|
import { SessionType, TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import { TLDR } from '~state/tldr'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
PointingCanvas = 'pointingCanvas',
|
||||||
|
PointingHandle = 'pointingHandle',
|
||||||
|
PointingBounds = 'pointingBounds',
|
||||||
|
PointingBoundsHandle = 'pointingBoundsHandle',
|
||||||
|
TranslatingHandle = 'translatingHandle',
|
||||||
|
Translating = 'translating',
|
||||||
|
Transforming = 'transforming',
|
||||||
|
Rotating = 'rotating',
|
||||||
|
Pinching = 'pinching',
|
||||||
|
Brushing = 'brushing',
|
||||||
|
Creating = 'creating',
|
||||||
|
EditingText = 'editing-text',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SelectTool extends BaseTool {
|
||||||
|
type = 'select' as const
|
||||||
|
|
||||||
|
status: Status = Status.Idle
|
||||||
|
|
||||||
|
pointedId?: string
|
||||||
|
|
||||||
|
selectedGroupId?: string
|
||||||
|
|
||||||
|
pointedHandleId?: 'start' | 'end'
|
||||||
|
|
||||||
|
pointedBoundsHandle?: TLBoundsCorner | TLBoundsEdge | 'rotate'
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
private deselect(id: string) {
|
||||||
|
this.state.select(...this.state.selectedIds.filter((oid) => oid !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
private select(id: string) {
|
||||||
|
this.state.select(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushSelect(id: string) {
|
||||||
|
const shape = this.state.getShape(id)
|
||||||
|
this.state.select(...this.state.selectedIds.filter((oid) => oid !== shape.parentId), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private deselectAll() {
|
||||||
|
this.state.deselectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onCancel = () => {
|
||||||
|
this.deselectAll()
|
||||||
|
// TODO: Make all cancel sessions have no arguments
|
||||||
|
this.state.cancelSession()
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown: TLKeyboardEventHandler = (key, info) => {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
this.onCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'Meta' || key === 'Control') {
|
||||||
|
// TODO: Make all sessions have all of these arguments
|
||||||
|
this.state.updateSession(
|
||||||
|
this.state.getPagePoint(info.point),
|
||||||
|
info.shiftKey,
|
||||||
|
info.altKey,
|
||||||
|
info.metaKey
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUp: TLKeyboardEventHandler = () => {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer Events (generic)
|
||||||
|
|
||||||
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
const point = this.state.getPagePoint(info.origin)
|
||||||
|
|
||||||
|
if (this.status === Status.PointingBoundsHandle) {
|
||||||
|
if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')
|
||||||
|
if (Vec.dist(info.origin, info.point) > 4) {
|
||||||
|
if (this.pointedBoundsHandle === 'rotate') {
|
||||||
|
// Stat a rotate session
|
||||||
|
this.setStatus(Status.Rotating)
|
||||||
|
|
||||||
|
this.state.startSession(SessionType.Rotate, point)
|
||||||
|
} else {
|
||||||
|
// Stat a transform session
|
||||||
|
this.setStatus(Status.Transforming)
|
||||||
|
|
||||||
|
const idsToTransform = this.state.selectedIds.flatMap((id) =>
|
||||||
|
TLDR.getDocumentBranch(this.state.state, id, this.state.currentPageId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (idsToTransform.length === 1) {
|
||||||
|
// if only one shape is selected, transform single
|
||||||
|
this.state.startSession(SessionType.TransformSingle, point, this.pointedBoundsHandle)
|
||||||
|
} else {
|
||||||
|
// otherwise, transform
|
||||||
|
this.state.startSession(SessionType.Transform, point, this.pointedBoundsHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.PointingCanvas) {
|
||||||
|
if (Vec.dist(info.origin, info.point) > 4) {
|
||||||
|
const point = this.state.getPagePoint(info.point)
|
||||||
|
this.state.startSession(SessionType.Brush, point)
|
||||||
|
this.setStatus(Status.Brushing)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.PointingBounds) {
|
||||||
|
if (Vec.dist(info.origin, info.point) > 4) {
|
||||||
|
this.setStatus(Status.Translating)
|
||||||
|
const point = this.state.getPagePoint(info.origin)
|
||||||
|
this.state.startSession(SessionType.Translate, point)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.PointingHandle) {
|
||||||
|
if (!this.pointedHandleId) throw Error('No pointed handle')
|
||||||
|
if (Vec.dist(info.origin, info.point) > 4) {
|
||||||
|
this.setStatus(Status.TranslatingHandle)
|
||||||
|
|
||||||
|
const selectedShape = this.state.getShape(this.state.selectedIds[0])
|
||||||
|
|
||||||
|
if (!selectedShape) return
|
||||||
|
|
||||||
|
const point = this.state.getPagePoint(info.origin)
|
||||||
|
|
||||||
|
if (selectedShape.type === TLDrawShapeType.Arrow) {
|
||||||
|
this.state.startSession(SessionType.Arrow, point, this.pointedHandleId)
|
||||||
|
} else {
|
||||||
|
this.state.startSession(SessionType.Handle, point, this.pointedHandleId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.session) {
|
||||||
|
return this.state.updateSession(
|
||||||
|
this.state.getPagePoint(info.point),
|
||||||
|
info.shiftKey,
|
||||||
|
info.altKey,
|
||||||
|
info.metaKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = () => {
|
||||||
|
if (this.status === Status.Idle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.EditingText) {
|
||||||
|
this.state.completeSession()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerUp: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.PointingBounds) {
|
||||||
|
if (info.target === 'bounds') {
|
||||||
|
// If we just clicked the selecting bounds's background,
|
||||||
|
// clear the selection
|
||||||
|
this.deselectAll()
|
||||||
|
} else if (this.state.isSelected(info.target)) {
|
||||||
|
// If we're holding shift...
|
||||||
|
if (info.shiftKey) {
|
||||||
|
// unless we just shift-selected the shape, remove it from
|
||||||
|
// the selected shapes
|
||||||
|
if (this.pointedId !== info.target) {
|
||||||
|
this.deselect(info.target)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we have other selected shapes, select this one instead
|
||||||
|
if (this.pointedId !== info.target && this.state.selectedIds.length > 1) {
|
||||||
|
this.select(info.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.pointedId === info.target) {
|
||||||
|
// If the target is not selected and was just pointed
|
||||||
|
// on pointer down...
|
||||||
|
if (info.shiftKey) {
|
||||||
|
this.pushSelect(info.target)
|
||||||
|
} else {
|
||||||
|
this.select(info.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the current session, if any; and reset the status
|
||||||
|
this.state.completeSession()
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
this.pointedBoundsHandle = undefined
|
||||||
|
this.pointedHandleId = undefined
|
||||||
|
this.pointedId = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas
|
||||||
|
|
||||||
|
onPointCanvas: TLCanvasEventHandler = (info) => {
|
||||||
|
// Unless the user is holding shift or meta, clear the current selection
|
||||||
|
if (!info.shiftKey) {
|
||||||
|
this.deselectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.PointingCanvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClickCanvas: TLCanvasEventHandler = (info) => {
|
||||||
|
const pagePoint = this.state.getPagePoint(info.point)
|
||||||
|
this.state.selectTool(TLDrawShapeType.Text)
|
||||||
|
const tool = this.state.tools[TLDrawShapeType.Text]
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
tool.createTextShapeAtPoint(pagePoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape
|
||||||
|
|
||||||
|
onPointShape: TLPointerEventHandler = (info) => {
|
||||||
|
const { hoveredId } = this.state.pageState
|
||||||
|
|
||||||
|
// While holding command and shift, select or deselect
|
||||||
|
// the shape, ignoring any group that may contain it. Yikes!
|
||||||
|
if (
|
||||||
|
(this.status === Status.Idle || this.status === Status.PointingBounds) &&
|
||||||
|
info.metaKey &&
|
||||||
|
info.shiftKey &&
|
||||||
|
hoveredId
|
||||||
|
) {
|
||||||
|
this.pointedId = hoveredId
|
||||||
|
|
||||||
|
if (this.state.isSelected(hoveredId)) {
|
||||||
|
this.deselect(hoveredId)
|
||||||
|
} else {
|
||||||
|
this.pushSelect(hoveredId)
|
||||||
|
this.setStatus(Status.PointingBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.PointingBounds) {
|
||||||
|
// The pointed id should be the shape's group, if it belongs
|
||||||
|
// to a group, or else the shape itself, if it is on the page.
|
||||||
|
const { parentId } = this.state.getShape(info.target)
|
||||||
|
this.pointedId = parentId === this.state.currentPageId ? info.target : parentId
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.Idle) {
|
||||||
|
this.setStatus(Status.PointingBounds)
|
||||||
|
|
||||||
|
if (info.metaKey) {
|
||||||
|
if (!info.shiftKey) {
|
||||||
|
this.deselectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = this.state.getPagePoint(info.point)
|
||||||
|
this.state.startSession(SessionType.Brush, point)
|
||||||
|
|
||||||
|
this.setStatus(Status.Brushing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've clicked on a shape that is inside of a group,
|
||||||
|
// then select the group rather than the shape.
|
||||||
|
let shapeIdToSelect: string
|
||||||
|
const { parentId } = this.state.getShape(info.target)
|
||||||
|
|
||||||
|
// If the pointed shape is a child of the page, select the
|
||||||
|
// target shape and clear the selected group id.
|
||||||
|
if (parentId === this.state.currentPageId) {
|
||||||
|
shapeIdToSelect = info.target
|
||||||
|
this.selectedGroupId = undefined
|
||||||
|
} else {
|
||||||
|
// If the parent is some other group...
|
||||||
|
if (parentId === this.selectedGroupId) {
|
||||||
|
// If that group is the selected group, then select
|
||||||
|
// the target shape.
|
||||||
|
shapeIdToSelect = info.target
|
||||||
|
} else {
|
||||||
|
// Otherwise, select the group and clear the selected
|
||||||
|
// group id.
|
||||||
|
shapeIdToSelect = parentId
|
||||||
|
|
||||||
|
this.selectedGroupId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.isSelected(shapeIdToSelect)) {
|
||||||
|
// Set the pointed ID to the shape that was clicked.
|
||||||
|
this.pointedId = shapeIdToSelect
|
||||||
|
|
||||||
|
// If the shape is not selected: then if the user is pressing shift,
|
||||||
|
// add the shape to the current selection; otherwise, set the shape as
|
||||||
|
// the only selected shape.
|
||||||
|
if (info.shiftKey) {
|
||||||
|
this.pushSelect(shapeIdToSelect)
|
||||||
|
} else {
|
||||||
|
this.select(shapeIdToSelect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClickShape: TLPointerEventHandler = (info) => {
|
||||||
|
// if (this.status !== Status.Idle) return
|
||||||
|
|
||||||
|
const shape = this.state.getShape(info.target)
|
||||||
|
|
||||||
|
const utils = TLDR.getShapeUtils(shape.type)
|
||||||
|
|
||||||
|
if (utils.canEdit) {
|
||||||
|
this.state.setEditingId(info.target)
|
||||||
|
// this.state.startTextSession(info.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the shape is the child of a group, then drill
|
||||||
|
// into the group?
|
||||||
|
if (shape.parentId !== this.state.currentPageId) {
|
||||||
|
this.selectedGroupId = shape.parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.select(info.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightPointShape: TLPointerEventHandler = (info) => {
|
||||||
|
if (!this.state.isSelected(info.target)) {
|
||||||
|
this.state.select(info.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHoverShape: TLPointerEventHandler = (info) => {
|
||||||
|
this.state.setHoveredId(info.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnhoverShape: TLPointerEventHandler = (info) => {
|
||||||
|
const { currentPageId: oldCurrentPageId } = this.state
|
||||||
|
|
||||||
|
// Wait a frame; and if we haven't changed the hovered id,
|
||||||
|
// clear the current hovered id
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (
|
||||||
|
oldCurrentPageId === this.state.currentPageId &&
|
||||||
|
this.state.pageState.hoveredId === info.target
|
||||||
|
) {
|
||||||
|
this.state.setHoveredId(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Bounds --------------------- */
|
||||||
|
|
||||||
|
onPointBounds: TLBoundsEventHandler = (info) => {
|
||||||
|
if (info.metaKey) {
|
||||||
|
if (!info.shiftKey) {
|
||||||
|
this.deselectAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = this.state.getPagePoint(info.point)
|
||||||
|
this.state.startSession(SessionType.Brush, point)
|
||||||
|
|
||||||
|
this.setStatus(Status.Brushing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.PointingBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleaseBounds: TLBoundsEventHandler = () => {
|
||||||
|
if (this.status === Status.Translating || this.status === Status.Brushing) {
|
||||||
|
this.state.completeSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Bounds Handles ----------------- */
|
||||||
|
|
||||||
|
onPointBoundsHandle: TLBoundsHandleEventHandler = (info) => {
|
||||||
|
this.pointedBoundsHandle = info.target
|
||||||
|
this.setStatus(Status.PointingBoundsHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = () => {
|
||||||
|
if (this.state.selectedIds.length === 1) {
|
||||||
|
this.state.resetBounds(this.state.selectedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleaseBoundsHandle: TLBoundsHandleEventHandler = () => {
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Handles -------------------- */
|
||||||
|
|
||||||
|
onPointHandle: TLPointerEventHandler = (info) => {
|
||||||
|
this.pointedHandleId = info.target as 'start' | 'end'
|
||||||
|
this.setStatus(Status.PointingHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClickHandle: TLPointerEventHandler = (info) => {
|
||||||
|
this.state.toggleDecoration(info.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleaseHandle: TLPointerEventHandler = () => {
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/SelectTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/SelectTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './SelectTool'
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { StickyTool } from '.'
|
||||||
|
|
||||||
|
describe('StickyTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new StickyTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
6
packages/tldraw/src/state/tool/StickyTool/StickyTool.ts
Normal file
6
packages/tldraw/src/state/tool/StickyTool/StickyTool.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
export class StickyTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.PostIt
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/StickyTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/StickyTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './StickyTool'
|
9
packages/tldraw/src/state/tool/TextTool/TextTool.spec.ts
Normal file
9
packages/tldraw/src/state/tool/TextTool/TextTool.spec.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { TLDrawState } from '~state'
|
||||||
|
import { TextTool } from '.'
|
||||||
|
|
||||||
|
describe('TextTool', () => {
|
||||||
|
it('creates tool', () => {
|
||||||
|
const tlstate = new TLDrawState()
|
||||||
|
new TextTool(tlstate)
|
||||||
|
})
|
||||||
|
})
|
86
packages/tldraw/src/state/tool/TextTool/TextTool.ts
Normal file
86
packages/tldraw/src/state/tool/TextTool/TextTool.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
import { Utils, TLPointerEventHandler } from '@tldraw/core'
|
||||||
|
import { Text } from '~shape/shapes'
|
||||||
|
import { TLDrawShapeType } from '~types'
|
||||||
|
import { BaseTool } from '../BaseTool'
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
Idle = 'idle',
|
||||||
|
Creating = 'creating',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextTool extends BaseTool {
|
||||||
|
type = TLDrawShapeType.Text
|
||||||
|
|
||||||
|
status = Status.Idle
|
||||||
|
|
||||||
|
/* --------------------- Methods -------------------- */
|
||||||
|
|
||||||
|
private setStatus(status: Status) {
|
||||||
|
this.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEditingShape = () => {
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
|
||||||
|
if (!this.state.appState.isToolLocked) {
|
||||||
|
this.state.selectTool('select')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTextShapeAtPoint = (point: number[]) => {
|
||||||
|
const {
|
||||||
|
shapes,
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const childIndex =
|
||||||
|
shapes.length === 0
|
||||||
|
? 1
|
||||||
|
: shapes
|
||||||
|
.filter((shape) => shape.parentId === currentPageId)
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||||
|
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
|
||||||
|
const newShape = Text.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
})
|
||||||
|
|
||||||
|
const bounds = Text.getBounds(newShape)
|
||||||
|
|
||||||
|
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||||
|
|
||||||
|
this.state.createShapes(newShape)
|
||||||
|
|
||||||
|
this.state.setEditingId(id)
|
||||||
|
|
||||||
|
this.setStatus(Status.Creating)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
|
onPointerDown: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.status === Status.Idle) {
|
||||||
|
const pagePoint = Vec.round(this.state.getPagePoint(info.point))
|
||||||
|
this.createTextShapeAtPoint(pagePoint)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === Status.Creating) {
|
||||||
|
this.stopEditingShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointShape: TLPointerEventHandler = (info) => {
|
||||||
|
const shape = this.state.getShape(info.target)
|
||||||
|
if (shape.type === TLDrawShapeType.Text) {
|
||||||
|
this.setStatus(Status.Idle)
|
||||||
|
this.state.setEditingId(shape.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
packages/tldraw/src/state/tool/TextTool/index.ts
Normal file
1
packages/tldraw/src/state/tool/TextTool/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './TextTool'
|
58
packages/tldraw/src/state/tool/index.ts
Normal file
58
packages/tldraw/src/state/tool/index.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import type { TLDrawState } from '~state'
|
||||||
|
import { TLDrawShapeType } from '~types'
|
||||||
|
import { ArrowTool } from './ArrowTool'
|
||||||
|
import { DrawTool } from './DrawTool'
|
||||||
|
import { EllipseTool } from './EllipseTool'
|
||||||
|
import { RectangleTool } from './RectangleTool'
|
||||||
|
import { SelectTool } from './SelectTool'
|
||||||
|
import { StickyTool } from './StickyTool'
|
||||||
|
import { TextTool } from './TextTool'
|
||||||
|
|
||||||
|
export type ToolType =
|
||||||
|
| 'select'
|
||||||
|
| TLDrawShapeType.Text
|
||||||
|
| TLDrawShapeType.Draw
|
||||||
|
| TLDrawShapeType.Ellipse
|
||||||
|
| TLDrawShapeType.Rectangle
|
||||||
|
| TLDrawShapeType.Arrow
|
||||||
|
| TLDrawShapeType.PostIt
|
||||||
|
|
||||||
|
export interface ToolsMap {
|
||||||
|
select: typeof SelectTool
|
||||||
|
[TLDrawShapeType.Text]: typeof TextTool
|
||||||
|
[TLDrawShapeType.Draw]: typeof DrawTool
|
||||||
|
[TLDrawShapeType.Ellipse]: typeof EllipseTool
|
||||||
|
[TLDrawShapeType.Rectangle]: typeof RectangleTool
|
||||||
|
[TLDrawShapeType.Arrow]: typeof ArrowTool
|
||||||
|
[TLDrawShapeType.PostIt]: typeof StickyTool
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToolOfType<K extends ToolType> = ToolsMap[K]
|
||||||
|
|
||||||
|
export type ArgsOfType<K extends ToolType> = ConstructorParameters<ToolOfType<K>>
|
||||||
|
|
||||||
|
export const tools: { [K in ToolType]: ToolsMap[K] } = {
|
||||||
|
select: SelectTool,
|
||||||
|
[TLDrawShapeType.Text]: TextTool,
|
||||||
|
[TLDrawShapeType.Draw]: DrawTool,
|
||||||
|
[TLDrawShapeType.Ellipse]: EllipseTool,
|
||||||
|
[TLDrawShapeType.Rectangle]: RectangleTool,
|
||||||
|
[TLDrawShapeType.Arrow]: ArrowTool,
|
||||||
|
[TLDrawShapeType.PostIt]: StickyTool,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTool = <K extends ToolType>(type: K): ToolOfType<K> => {
|
||||||
|
return tools[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTools(state: TLDrawState) {
|
||||||
|
return {
|
||||||
|
select: new SelectTool(state),
|
||||||
|
[TLDrawShapeType.Text]: new TextTool(state),
|
||||||
|
[TLDrawShapeType.Draw]: new DrawTool(state),
|
||||||
|
[TLDrawShapeType.Ellipse]: new EllipseTool(state),
|
||||||
|
[TLDrawShapeType.Rectangle]: new RectangleTool(state),
|
||||||
|
[TLDrawShapeType.Arrow]: new ArrowTool(state),
|
||||||
|
[TLDrawShapeType.PostIt]: new StickyTool(state),
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,11 @@ export class TLStateUtils {
|
||||||
this.tlstate = tlstate
|
this.tlstate = tlstate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
movePointer = (options: PointerOptions = {}) => {
|
||||||
|
const { tlstate } = this
|
||||||
|
tlstate.onPointerMove(inputs.pointerMove(this.getPoint(options), ''), {} as React.PointerEvent)
|
||||||
|
}
|
||||||
|
|
||||||
hoverShape = (id: string, options: PointerOptions = {}) => {
|
hoverShape = (id: string, options: PointerOptions = {}) => {
|
||||||
const { tlstate } = this
|
const { tlstate } = this
|
||||||
tlstate.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent)
|
tlstate.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent)
|
||||||
|
@ -27,6 +32,10 @@ export class TLStateUtils {
|
||||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
{} as React.PointerEvent
|
{} as React.PointerEvent
|
||||||
)
|
)
|
||||||
|
this.tlstate.onPointerDown(
|
||||||
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
|
{} as React.PointerEvent
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +44,10 @@ export class TLStateUtils {
|
||||||
inputs.pointerDown(this.getPoint(options), id),
|
inputs.pointerDown(this.getPoint(options), id),
|
||||||
{} as React.PointerEvent
|
{} as React.PointerEvent
|
||||||
)
|
)
|
||||||
|
this.tlstate.onPointerDown(
|
||||||
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
|
{} as React.PointerEvent
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +56,10 @@ export class TLStateUtils {
|
||||||
inputs.pointerDown(this.getPoint(options), id),
|
inputs.pointerDown(this.getPoint(options), id),
|
||||||
{} as React.PointerEvent
|
{} as React.PointerEvent
|
||||||
)
|
)
|
||||||
|
this.tlstate.onPointerDown(
|
||||||
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
|
{} as React.PointerEvent
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +68,10 @@ export class TLStateUtils {
|
||||||
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
||||||
{} as React.PointerEvent
|
{} as React.PointerEvent
|
||||||
)
|
)
|
||||||
|
this.tlstate.onPointerDown(
|
||||||
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
|
{} as React.PointerEvent
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +83,10 @@ export class TLStateUtils {
|
||||||
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
||||||
{} as React.PointerEvent
|
{} as React.PointerEvent
|
||||||
)
|
)
|
||||||
|
this.tlstate.onPointerDown(
|
||||||
|
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||||
|
{} as React.PointerEvent
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,6 @@ export interface Data {
|
||||||
pages: Pick<TLPage<TLDrawShape, TLDrawBinding>, 'id' | 'name' | 'childIndex'>[]
|
pages: Pick<TLPage<TLDrawShape, TLDrawBinding>, 'id' | 'name' | 'childIndex'>[]
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
activeTool: TLDrawShapeType | 'select'
|
activeTool: TLDrawShapeType | 'select'
|
||||||
activeToolType?: TLDrawToolType | 'select'
|
|
||||||
isToolLocked: boolean
|
isToolLocked: boolean
|
||||||
isStyleOpen: boolean
|
isStyleOpen: boolean
|
||||||
isEmptyCanvas: boolean
|
isEmptyCanvas: boolean
|
||||||
|
@ -87,13 +86,30 @@ export interface SelectHistory {
|
||||||
stack: string[][]
|
stack: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Session {
|
export enum SessionType {
|
||||||
id: string
|
Transform = 'transform',
|
||||||
status: TLDrawStatus
|
Translate = 'translate',
|
||||||
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
TransformSingle = 'transformSingle',
|
||||||
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
Brush = 'brush',
|
||||||
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | TLDrawCommand | undefined
|
Arrow = 'arrow',
|
||||||
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
Draw = 'draw',
|
||||||
|
Rotate = 'rotate',
|
||||||
|
Handle = 'handle',
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Session {
|
||||||
|
static type: SessionType
|
||||||
|
abstract status: TLDrawStatus
|
||||||
|
abstract start: (data: Readonly<Data>) => TLDrawPatch | undefined
|
||||||
|
abstract update: (
|
||||||
|
data: Readonly<Data>,
|
||||||
|
point: number[],
|
||||||
|
shiftKey: boolean,
|
||||||
|
altKey: boolean,
|
||||||
|
metaKey: boolean
|
||||||
|
) => TLDrawPatch | undefined
|
||||||
|
abstract complete: (data: Readonly<Data>) => TLDrawPatch | TLDrawCommand | undefined
|
||||||
|
abstract cancel: (data: Readonly<Data>) => TLDrawPatch | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TLDrawStatus {
|
export enum TLDrawStatus {
|
||||||
|
@ -114,6 +130,8 @@ export enum TLDrawStatus {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never
|
export type ParametersExceptFirst<F> = F extends (arg0: any, ...rest: infer R) => any ? R : never
|
||||||
|
|
||||||
|
export type ExceptFirst<T extends unknown[]> = T extends [any, ...infer U] ? U : never
|
||||||
|
|
||||||
export enum MoveType {
|
export enum MoveType {
|
||||||
Backward = 'backward',
|
Backward = 'backward',
|
||||||
Forward = 'forward',
|
Forward = 'forward',
|
||||||
|
@ -145,15 +163,6 @@ export enum FlipType {
|
||||||
Vertical = 'vertical',
|
Vertical = 'vertical',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TLDrawToolType {
|
|
||||||
Draw = 'draw',
|
|
||||||
Bounds = 'bounds',
|
|
||||||
Point = 'point',
|
|
||||||
Handle = 'handle',
|
|
||||||
Points = 'points',
|
|
||||||
Text = 'text',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TLDrawShapeType {
|
export enum TLDrawShapeType {
|
||||||
PostIt = 'post-it',
|
PostIt = 'post-it',
|
||||||
Ellipse = 'ellipse',
|
Ellipse = 'ellipse',
|
||||||
|
@ -229,14 +238,7 @@ export type TLDrawShape =
|
||||||
| GroupShape
|
| GroupShape
|
||||||
| PostItShape
|
| PostItShape
|
||||||
|
|
||||||
export type TLDrawShapeUtil<T extends TLDrawShape> = TLShapeUtil<
|
export type TLDrawShapeUtil<T extends TLDrawShape> = TLShapeUtil<T, any, TLDrawMeta>
|
||||||
T,
|
|
||||||
any,
|
|
||||||
TLDrawMeta,
|
|
||||||
{
|
|
||||||
toolType: TLDrawToolType
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
export type ArrowBinding = TLBinding<{
|
export type ArrowBinding = TLBinding<{
|
||||||
handleId: keyof ArrowShape['handles']
|
handleId: keyof ArrowShape['handles']
|
||||||
|
|
Loading…
Reference in a new issue