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}
|
||||
events={events}
|
||||
onShapeChange={callbacks.onShapeChange}
|
||||
onShapeBlur={callbacks.onShapeBlur}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
|
|
@ -71,7 +71,7 @@ export function useZoomEvents<T extends Element>(zoom: number, ref: React.RefObj
|
|||
onPinch: ({ origin, offset, event }) => {
|
||||
const elm = ref.current
|
||||
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)
|
||||
|
||||
|
|
545
packages/core/src/types.d.ts
vendored
545
packages/core/src/types.d.ts
vendored
|
@ -1,264 +1,357 @@
|
|||
import type React from 'react';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import type React from 'react'
|
||||
import { MutableRefObject } from 'react-router/node_modules/@types/react'
|
||||
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> {
|
||||
id: string;
|
||||
name?: string;
|
||||
childIndex?: number;
|
||||
shapes: Record<string, T>;
|
||||
bindings: Record<string, B>;
|
||||
id: string
|
||||
name?: string
|
||||
childIndex?: number
|
||||
shapes: Record<string, T>
|
||||
bindings: Record<string, B>
|
||||
}
|
||||
export interface TLPageState {
|
||||
id: string;
|
||||
selectedIds: string[];
|
||||
camera: {
|
||||
point: number[];
|
||||
zoom: number;
|
||||
};
|
||||
brush?: TLBounds;
|
||||
pointedId?: string | null;
|
||||
hoveredId?: string | null;
|
||||
editingId?: string | null;
|
||||
bindingId?: string | null;
|
||||
boundsRotation?: number;
|
||||
currentParentId?: string | null;
|
||||
id: string
|
||||
selectedIds: string[]
|
||||
camera: {
|
||||
point: number[]
|
||||
zoom: number
|
||||
}
|
||||
brush?: TLBounds
|
||||
pointedId?: string | null
|
||||
hoveredId?: string | null
|
||||
editingId?: string | null
|
||||
bindingId?: string | null
|
||||
boundsRotation?: number
|
||||
currentParentId?: string | null
|
||||
}
|
||||
export interface TLHandle {
|
||||
id: string;
|
||||
index: number;
|
||||
point: number[];
|
||||
canBind?: boolean;
|
||||
bindingId?: string;
|
||||
id: string
|
||||
index: number
|
||||
point: number[]
|
||||
canBind?: boolean
|
||||
bindingId?: string
|
||||
}
|
||||
export interface TLShape {
|
||||
id: string;
|
||||
type: string;
|
||||
parentId: string;
|
||||
childIndex: number;
|
||||
name: string;
|
||||
point: number[];
|
||||
rotation?: number;
|
||||
children?: string[];
|
||||
handles?: Record<string, TLHandle>;
|
||||
isLocked?: boolean;
|
||||
isHidden?: boolean;
|
||||
isEditing?: boolean;
|
||||
isGenerated?: boolean;
|
||||
isAspectRatioLocked?: boolean;
|
||||
id: string
|
||||
type: string
|
||||
parentId: string
|
||||
childIndex: number
|
||||
name: string
|
||||
point: number[]
|
||||
rotation?: number
|
||||
children?: string[]
|
||||
handles?: Record<string, TLHandle>
|
||||
isLocked?: boolean
|
||||
isHidden?: boolean
|
||||
isEditing?: boolean
|
||||
isGenerated?: 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> {
|
||||
shape: T;
|
||||
isEditing: boolean;
|
||||
isBinding: boolean;
|
||||
isHovered: boolean;
|
||||
isSelected: boolean;
|
||||
isCurrentParent: boolean;
|
||||
meta: M extends any ? M : never;
|
||||
onShapeChange?: TLCallbacks<T>['onShapeChange'];
|
||||
onShapeBlur?: TLCallbacks<T>['onShapeBlur'];
|
||||
events: {
|
||||
onPointerDown: (e: React.PointerEvent<E>) => void;
|
||||
onPointerUp: (e: React.PointerEvent<E>) => void;
|
||||
onPointerEnter: (e: React.PointerEvent<E>) => void;
|
||||
onPointerMove: (e: React.PointerEvent<E>) => void;
|
||||
onPointerLeave: (e: React.PointerEvent<E>) => void;
|
||||
};
|
||||
shape: T
|
||||
isEditing: boolean
|
||||
isBinding: boolean
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
isCurrentParent: boolean
|
||||
meta: M extends any ? M : never
|
||||
onShapeChange?: TLCallbacks<T>['onShapeChange']
|
||||
onShapeBlur?: TLCallbacks<T>['onShapeBlur']
|
||||
events: {
|
||||
onPointerDown: (e: React.PointerEvent<E>) => void
|
||||
onPointerUp: (e: React.PointerEvent<E>) => void
|
||||
onPointerEnter: (e: React.PointerEvent<E>) => void
|
||||
onPointerMove: (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> {
|
||||
ref: ForwardedRef<E>;
|
||||
shape: T;
|
||||
ref: MutableRefObject<E>
|
||||
shape: T
|
||||
}
|
||||
export interface TLTool {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
export interface TLBinding<M = any> {
|
||||
id: string;
|
||||
type: string;
|
||||
toId: string;
|
||||
fromId: string;
|
||||
meta: M;
|
||||
id: string
|
||||
type: string
|
||||
toId: string
|
||||
fromId: string
|
||||
meta: M
|
||||
}
|
||||
export interface TLTheme {
|
||||
brushFill?: string;
|
||||
brushStroke?: string;
|
||||
selectFill?: string;
|
||||
selectStroke?: string;
|
||||
background?: string;
|
||||
foreground?: string;
|
||||
brushFill?: string
|
||||
brushStroke?: string
|
||||
selectFill?: string
|
||||
selectStroke?: string
|
||||
background?: string
|
||||
foreground?: string
|
||||
}
|
||||
export declare type TLWheelEventHandler = (info: TLPointerInfo<string>, e: React.WheelEvent<Element> | WheelEvent) => void;
|
||||
export declare type TLPinchEventHandler = (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 declare type TLWheelEventHandler = (
|
||||
info: TLPointerInfo<string>,
|
||||
e: React.WheelEvent<Element> | WheelEvent
|
||||
) => void
|
||||
export declare type TLPinchEventHandler = (
|
||||
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> {
|
||||
onPinchStart: TLPinchEventHandler;
|
||||
onPinchEnd: TLPinchEventHandler;
|
||||
onPinch: TLPinchEventHandler;
|
||||
onPan: TLWheelEventHandler;
|
||||
onZoom: TLWheelEventHandler;
|
||||
onPointerMove: TLPointerEventHandler;
|
||||
onPointerUp: TLPointerEventHandler;
|
||||
onPointerDown: TLPointerEventHandler;
|
||||
onPointCanvas: TLCanvasEventHandler;
|
||||
onDoubleClickCanvas: TLCanvasEventHandler;
|
||||
onRightPointCanvas: TLCanvasEventHandler;
|
||||
onDragCanvas: TLCanvasEventHandler;
|
||||
onReleaseCanvas: TLCanvasEventHandler;
|
||||
onPointShape: TLPointerEventHandler;
|
||||
onDoubleClickShape: TLPointerEventHandler;
|
||||
onRightPointShape: TLPointerEventHandler;
|
||||
onDragShape: TLPointerEventHandler;
|
||||
onHoverShape: TLPointerEventHandler;
|
||||
onUnhoverShape: TLPointerEventHandler;
|
||||
onReleaseShape: TLPointerEventHandler;
|
||||
onPointBounds: TLBoundsEventHandler;
|
||||
onDoubleClickBounds: TLBoundsEventHandler;
|
||||
onRightPointBounds: TLBoundsEventHandler;
|
||||
onDragBounds: TLBoundsEventHandler;
|
||||
onHoverBounds: TLBoundsEventHandler;
|
||||
onUnhoverBounds: TLBoundsEventHandler;
|
||||
onReleaseBounds: TLBoundsEventHandler;
|
||||
onPointBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onRightPointBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onDragBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onHoverBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onUnhoverBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onReleaseBoundsHandle: TLBoundsHandleEventHandler;
|
||||
onPointHandle: TLPointerEventHandler;
|
||||
onDoubleClickHandle: TLPointerEventHandler;
|
||||
onRightPointHandle: TLPointerEventHandler;
|
||||
onDragHandle: TLPointerEventHandler;
|
||||
onHoverHandle: TLPointerEventHandler;
|
||||
onUnhoverHandle: TLPointerEventHandler;
|
||||
onReleaseHandle: TLPointerEventHandler;
|
||||
onRenderCountChange: (ids: string[]) => void;
|
||||
onShapeChange: (shape: {
|
||||
id: string;
|
||||
} & Partial<T>) => void;
|
||||
onShapeBlur: () => void;
|
||||
onError: (error: Error) => void;
|
||||
onPinchStart: TLPinchEventHandler
|
||||
onPinchEnd: TLPinchEventHandler
|
||||
onPinch: TLPinchEventHandler
|
||||
onPan: TLWheelEventHandler
|
||||
onZoom: TLWheelEventHandler
|
||||
onPointerMove: TLPointerEventHandler
|
||||
onPointerUp: TLPointerEventHandler
|
||||
onPointerDown: TLPointerEventHandler
|
||||
onPointCanvas: TLCanvasEventHandler
|
||||
onDoubleClickCanvas: TLCanvasEventHandler
|
||||
onRightPointCanvas: TLCanvasEventHandler
|
||||
onDragCanvas: TLCanvasEventHandler
|
||||
onReleaseCanvas: TLCanvasEventHandler
|
||||
onPointShape: TLPointerEventHandler
|
||||
onDoubleClickShape: TLPointerEventHandler
|
||||
onRightPointShape: TLPointerEventHandler
|
||||
onDragShape: TLPointerEventHandler
|
||||
onHoverShape: TLPointerEventHandler
|
||||
onUnhoverShape: TLPointerEventHandler
|
||||
onReleaseShape: TLPointerEventHandler
|
||||
onPointBounds: TLBoundsEventHandler
|
||||
onDoubleClickBounds: TLBoundsEventHandler
|
||||
onRightPointBounds: TLBoundsEventHandler
|
||||
onDragBounds: TLBoundsEventHandler
|
||||
onHoverBounds: TLBoundsEventHandler
|
||||
onUnhoverBounds: TLBoundsEventHandler
|
||||
onReleaseBounds: TLBoundsEventHandler
|
||||
onPointBoundsHandle: TLBoundsHandleEventHandler
|
||||
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler
|
||||
onRightPointBoundsHandle: TLBoundsHandleEventHandler
|
||||
onDragBoundsHandle: TLBoundsHandleEventHandler
|
||||
onHoverBoundsHandle: TLBoundsHandleEventHandler
|
||||
onUnhoverBoundsHandle: TLBoundsHandleEventHandler
|
||||
onReleaseBoundsHandle: TLBoundsHandleEventHandler
|
||||
onPointHandle: TLPointerEventHandler
|
||||
onDoubleClickHandle: TLPointerEventHandler
|
||||
onRightPointHandle: TLPointerEventHandler
|
||||
onDragHandle: TLPointerEventHandler
|
||||
onHoverHandle: TLPointerEventHandler
|
||||
onUnhoverHandle: TLPointerEventHandler
|
||||
onReleaseHandle: TLPointerEventHandler
|
||||
onRenderCountChange: (ids: string[]) => void
|
||||
onShapeChange: (
|
||||
shape: {
|
||||
id: string
|
||||
} & Partial<T>
|
||||
) => void
|
||||
onShapeBlur: () => void
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
export interface TLBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation?: number;
|
||||
minX: number
|
||||
minY: number
|
||||
maxX: number
|
||||
maxY: number
|
||||
width: number
|
||||
height: number
|
||||
rotation?: number
|
||||
}
|
||||
export declare type TLIntersection = {
|
||||
didIntersect: boolean;
|
||||
message: string;
|
||||
points: number[][];
|
||||
};
|
||||
didIntersect: boolean
|
||||
message: string
|
||||
points: number[][]
|
||||
}
|
||||
export declare enum TLBoundsEdge {
|
||||
Top = "top_edge",
|
||||
Right = "right_edge",
|
||||
Bottom = "bottom_edge",
|
||||
Left = "left_edge"
|
||||
Top = 'top_edge',
|
||||
Right = 'right_edge',
|
||||
Bottom = 'bottom_edge',
|
||||
Left = 'left_edge',
|
||||
}
|
||||
export declare enum TLBoundsCorner {
|
||||
TopLeft = "top_left_corner",
|
||||
TopRight = "top_right_corner",
|
||||
BottomRight = "bottom_right_corner",
|
||||
BottomLeft = "bottom_left_corner"
|
||||
TopLeft = 'top_left_corner',
|
||||
TopRight = 'top_right_corner',
|
||||
BottomRight = 'bottom_right_corner',
|
||||
BottomLeft = 'bottom_left_corner',
|
||||
}
|
||||
export interface TLPointerInfo<T extends string = string> {
|
||||
target: T;
|
||||
pointerId: number;
|
||||
origin: number[];
|
||||
point: number[];
|
||||
delta: number[];
|
||||
pressure: number;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
altKey: boolean;
|
||||
target: T
|
||||
pointerId: number
|
||||
origin: number[]
|
||||
point: number[]
|
||||
delta: number[]
|
||||
pressure: number
|
||||
shiftKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
}
|
||||
export interface TLKeyboardInfo {
|
||||
origin: number[];
|
||||
point: number[];
|
||||
key: string;
|
||||
keys: string[];
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
altKey: boolean;
|
||||
origin: number[]
|
||||
point: number[]
|
||||
key: string
|
||||
keys: string[]
|
||||
shiftKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
}
|
||||
export interface TLTransformInfo<T extends TLShape> {
|
||||
type: TLBoundsEdge | TLBoundsCorner;
|
||||
initialShape: T;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
transformOrigin: number[];
|
||||
type: TLBoundsEdge | TLBoundsCorner
|
||||
initialShape: T
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
transformOrigin: number[]
|
||||
}
|
||||
export interface TLBezierCurveSegment {
|
||||
start: number[];
|
||||
tangentStart: number[];
|
||||
normalStart: number[];
|
||||
pressureStart: number;
|
||||
end: number[];
|
||||
tangentEnd: number[];
|
||||
normalEnd: number[];
|
||||
pressureEnd: number;
|
||||
start: number[]
|
||||
tangentStart: number[]
|
||||
normalStart: number[]
|
||||
pressureStart: number
|
||||
end: number[]
|
||||
tangentEnd: number[]
|
||||
normalEnd: 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> {
|
||||
shape: T;
|
||||
children?: IShapeTreeNode<TLShape, M>[];
|
||||
isEditing: boolean;
|
||||
isBinding: boolean;
|
||||
isHovered: boolean;
|
||||
isSelected: boolean;
|
||||
isCurrentParent: boolean;
|
||||
meta?: M extends any ? M : never;
|
||||
shape: T
|
||||
children?: IShapeTreeNode<TLShape, M>[]
|
||||
isEditing: boolean
|
||||
isBinding: boolean
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
isCurrentParent: boolean
|
||||
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
|
||||
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
|
||||
const hideHandles = isInSession || !isSelecting
|
||||
|
|
|
@ -150,6 +150,7 @@ export function PrimaryButton({
|
|||
name: label,
|
||||
isActive,
|
||||
})}
|
||||
onPointerDown={onClick}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
|
|
|
@ -14,5 +14,6 @@ export const tldrawShapeUtils: Record<TLDrawShapeType, any> = {
|
|||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
ArrowShape,
|
||||
Decoration,
|
||||
TLDrawShapeType,
|
||||
TLDrawToolType,
|
||||
DashStyle,
|
||||
ArrowBinding,
|
||||
TLDrawMeta,
|
||||
|
@ -26,8 +25,6 @@ import { EASINGS } from '~state/utils'
|
|||
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Arrow,
|
||||
|
||||
toolType: TLDrawToolType.Handle,
|
||||
|
||||
canStyleFill: false,
|
||||
|
||||
pathCache: new WeakMap<ArrowShape, string>(),
|
||||
|
@ -100,7 +97,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
|
||||
if (isStraightLine) {
|
||||
const path = isDraw
|
||||
? renderFreehandArrowShaft(shape, arrowDist, easing)
|
||||
? renderFreehandArrowShaft(shape)
|
||||
: 'M' + Vec.round(start.point) + 'L' + Vec.round(end.point)
|
||||
|
||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||
|
@ -425,9 +422,20 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
// And passes through the dragging handle
|
||||
const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin))
|
||||
|
||||
if (
|
||||
[TLDrawShapeType.Rectangle, TLDrawShapeType.Text].includes(target.type as TLDrawShapeType)
|
||||
) {
|
||||
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]) {
|
||||
handlePoint = Vec.sub(hits[0], shape.point)
|
||||
}
|
||||
} else {
|
||||
let hits = intersectRayBounds(origin, direction, intersectBounds, target.rotation)
|
||||
.filter((int) => int.didIntersect)
|
||||
.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))
|
||||
}
|
||||
|
||||
if (!hits[0]) {
|
||||
console.warn('No intersection.')
|
||||
return
|
||||
if (hits[0]) {
|
||||
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
|
||||
}
|
||||
|
||||
function renderFreehandArrowShaft(
|
||||
shape: ArrowShape,
|
||||
length: number,
|
||||
easing: (t: number) => number
|
||||
) {
|
||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||
const { style, id } = shape
|
||||
|
||||
const { start, end } = shape.handles
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Vec } from '@tldraw/vec'
|
|||
import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect'
|
||||
import { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||
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'
|
||||
|
||||
const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([])
|
||||
|
@ -15,8 +15,6 @@ const pointCache: Record<string, number[]> = {}
|
|||
export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Draw,
|
||||
|
||||
toolType: TLDrawToolType.Draw,
|
||||
|
||||
defaultProps: {
|
||||
id: 'id',
|
||||
type: TLDrawShapeType.Draw,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
import { SVGContainer, Utils, ShapeUtil, TLTransformInfo, TLBounds } from '@tldraw/core'
|
||||
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 getStroke, { getStrokeOutlinePoints, getStrokePoints } from 'perfect-freehand'
|
||||
import {
|
||||
|
@ -14,8 +14,6 @@ import { EASINGS } from '~state/utils'
|
|||
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Ellipse,
|
||||
|
||||
toolType: TLDrawToolType.Bounds,
|
||||
|
||||
pathCache: new WeakMap<EllipseShape, string>([]),
|
||||
|
||||
canBind: true,
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import * as React from 'react'
|
||||
import { SVGContainer, ShapeUtil } from '@tldraw/core'
|
||||
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 css from '~styles'
|
||||
|
||||
export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Group,
|
||||
|
||||
toolType: TLDrawToolType.Bounds,
|
||||
|
||||
canBind: true,
|
||||
|
||||
defaultProps: {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import { HTMLContainer, ShapeUtil } from '@tldraw/core'
|
||||
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'
|
||||
|
||||
export const PostIt = new ShapeUtil<PostItShape, HTMLDivElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.PostIt,
|
||||
|
||||
toolType: TLDrawToolType.Bounds,
|
||||
|
||||
canBind: true,
|
||||
|
||||
pathCache: new WeakMap<number[], string>([]),
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Utils, SVGContainer, ShapeUtil } from '@tldraw/core'
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||
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 { EASINGS } from '~state/utils'
|
||||
|
||||
|
@ -12,8 +12,6 @@ const pathCache = new WeakMap<number[], string>([])
|
|||
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
||||
toolType: TLDrawToolType.Bounds,
|
||||
|
||||
canBind: true,
|
||||
|
||||
defaultProps: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
|||
import { HTMLContainer, TLBounds, Utils, ShapeUtil } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
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 TextAreaUtils from './text-utils'
|
||||
|
||||
|
@ -54,11 +54,9 @@ if (typeof window !== 'undefined') {
|
|||
export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Text,
|
||||
|
||||
toolType: TLDrawToolType.Text,
|
||||
|
||||
isAspectRatioLocked: true,
|
||||
|
||||
isEditableText: true,
|
||||
canEdit: true,
|
||||
|
||||
canBind: true,
|
||||
|
||||
|
@ -86,6 +84,8 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
|||
const styles = getShapeStyle(style, meta.isDarkMode)
|
||||
const font = getFontStyle(shape.style)
|
||||
|
||||
const rIsMounted = React.useRef(false)
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onShapeChange?.({ ...shape, text: normalizeText(e.currentTarget.value) })
|
||||
|
@ -115,15 +115,20 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
|||
|
||||
const handleBlur = React.useCallback(
|
||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
e.currentTarget.setSelectionRange(0, 0)
|
||||
onShapeBlur?.()
|
||||
if (!isEditing) return
|
||||
if (rIsMounted.current) {
|
||||
e.currentTarget.setSelectionRange(0, 0)
|
||||
onShapeBlur?.()
|
||||
}
|
||||
},
|
||||
[isEditing, shape]
|
||||
[isEditing]
|
||||
)
|
||||
|
||||
const handleFocus = React.useCallback(
|
||||
(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
if (!isEditing) return
|
||||
if (!rIsMounted.current) return
|
||||
|
||||
if (document.activeElement === e.currentTarget) {
|
||||
e.currentTarget.select()
|
||||
}
|
||||
|
@ -143,6 +148,7 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
|||
React.useEffect(() => {
|
||||
if (isEditing) {
|
||||
requestAnimationFrame(() => {
|
||||
rIsMounted.current = true
|
||||
const elm = rInput.current!
|
||||
elm.focus()
|
||||
elm.select()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { TLDR } from '~state/tldr'
|
||||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { TLDrawShape, TLDrawShapeType } from '~types'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Delete command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -57,14 +56,14 @@ describe('Delete command', () => {
|
|||
})
|
||||
|
||||
it('deletes bound shapes, undoes and redoes', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
new TLDrawState()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
.delete()
|
||||
.undo()
|
||||
|
@ -80,8 +79,8 @@ describe('Delete command', () => {
|
|||
type: TLDrawShapeType.Arrow,
|
||||
})
|
||||
.select('arrow1')
|
||||
.startHandleSession([0, 0], 'start')
|
||||
.updateHandleSession([110, 110])
|
||||
.startSession(SessionType.Arrow, [0, 0], 'start')
|
||||
.updateSession([110, 110])
|
||||
.completeSession()
|
||||
|
||||
const binding = Object.values(tlstate.page.bindings)[0]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
||||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Duplicate command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -62,8 +62,8 @@ describe('Duplicate command', () => {
|
|||
|
||||
tlstate
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const beforeArrow = tlstate.getShape<ArrowShape>('arrow1')
|
||||
|
@ -102,8 +102,8 @@ describe('Duplicate command', () => {
|
|||
|
||||
tlstate
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const oldBindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
||||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Move to page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -67,8 +67,8 @@ describe('Move to page command', () => {
|
|||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
|
@ -108,8 +108,8 @@ describe('Move to page command', () => {
|
|||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
|
@ -149,8 +149,8 @@ describe('Move to page command', () => {
|
|||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.bindings[0].id
|
||||
|
|
|
@ -2,7 +2,7 @@ import { TLBoundsCorner, Utils } from '@tldraw/core'
|
|||
import { TLDrawState } from '~state'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { mockDocument } from '~test'
|
||||
import { TLDrawShapeType } from '~types'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Reset bounds command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -26,8 +26,8 @@ describe('Reset bounds command', () => {
|
|||
|
||||
tlstate
|
||||
.select('text1')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([-100, -100], false, false)
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([-100, -100], false, false)
|
||||
.completeSession()
|
||||
|
||||
const scale = tlstate.getShape('text1').style.scale
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
||||
import { ArrowShape, SessionType, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Translate command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -59,8 +59,8 @@ describe('Translate command', () => {
|
|||
}
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
|
@ -98,8 +98,8 @@ describe('Translate command', () => {
|
|||
}
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const bindingId = tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId!
|
||||
|
|
|
@ -1 +1,43 @@
|
|||
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 { mockDocument } from '~test'
|
||||
import { ArrowShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
import { ArrowShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Arrow session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
|
@ -15,12 +16,12 @@ describe('Arrow session', () => {
|
|||
|
||||
const restoreDoc = tlstate.document
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
tlstate
|
||||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
const binding = tlstate.bindings[0]
|
||||
|
@ -44,11 +45,11 @@ describe('Arrow session', () => {
|
|||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.bindings[0]).toBe(undefined)
|
||||
|
@ -57,78 +58,78 @@ describe('Arrow session', () => {
|
|||
|
||||
describe('arrow binding', () => {
|
||||
it('points to the center', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||
})
|
||||
|
||||
it('Snaps to the center', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([55, 55])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.5, 0.5])
|
||||
})
|
||||
|
||||
it('Binds at the bottom left', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([132, -32])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([132, -32])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([1, 0])
|
||||
})
|
||||
|
||||
it('Cancels the bind when off of the expanded bounds', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([133, 133])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([133, 133])
|
||||
|
||||
expect(tlstate.bindings[0]).toBe(undefined)
|
||||
})
|
||||
|
||||
it('binds on the inside of a shape while meta is held', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([91, 9])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9])
|
||||
|
||||
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', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([91, 9])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9])
|
||||
|
||||
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])
|
||||
})
|
||||
|
||||
it('ignores binding when alt is held', () => {
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([55, 45])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 45])
|
||||
|
||||
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])
|
||||
})
|
||||
|
@ -136,11 +137,13 @@ describe('Arrow session', () => {
|
|||
|
||||
describe('when dragging a bound shape', () => {
|
||||
it('updates the arrow', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
tlstate.loadDocument(restoreDoc)
|
||||
// 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]
|
||||
tlstate.updateHandleSession([50, 50]).completeSession()
|
||||
tlstate.updateSession([50, 50])
|
||||
// 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').handles.start.point).toStrictEqual([0, 0])
|
||||
|
|
|
@ -6,14 +6,14 @@ import {
|
|||
Data,
|
||||
Session,
|
||||
TLDrawStatus,
|
||||
SessionType,
|
||||
} from '~types'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { ThickArrowDownIcon } from '@radix-ui/react-icons'
|
||||
|
||||
export class ArrowSession implements Session {
|
||||
id = 'transform_single'
|
||||
static type = SessionType.Arrow
|
||||
status = TLDrawStatus.TranslatingHandle
|
||||
newBindingId = Utils.uniqueId()
|
||||
delta = [0, 0]
|
||||
|
@ -26,7 +26,7 @@ export class ArrowSession implements Session {
|
|||
initialBinding: TLDrawBinding | undefined
|
||||
didBind = false
|
||||
|
||||
constructor(data: Data, handleId: 'start' | 'end', point: number[]) {
|
||||
constructor(data: Data, point: number[], handleId: 'start' | 'end') {
|
||||
const { currentPageId } = data.appState
|
||||
const page = data.document.pages[currentPageId]
|
||||
const pageState = data.document.pageStates[currentPageId]
|
||||
|
|
|
@ -1,52 +1,61 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Brush session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
tlstate.loadDocument(mockDocument)
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate.startBrushSession([-10, -10])
|
||||
tlstate.updateBrushSession([10, 10])
|
||||
tlstate.completeSession()
|
||||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||
expect(tlstate.selectedIds.length).toBe(1)
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate.startBrushSession([-10, -10])
|
||||
tlstate.updateBrushSession([110, 110])
|
||||
tlstate.completeSession()
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.startSession(SessionType.Brush, [-10, -10])
|
||||
.updateSession([110, 110])
|
||||
.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(3)
|
||||
})
|
||||
|
||||
it('does not de-select original shapes', () => {
|
||||
tlstate.deselectAll()
|
||||
tlstate
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.select('rect1')
|
||||
.startBrushSession([300, 300])
|
||||
.updateBrushSession([301, 301])
|
||||
.startSession(SessionType.Brush, [300, 300])
|
||||
.updateSession([301, 301])
|
||||
.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(1)
|
||||
})
|
||||
|
||||
it('does not select hidden shapes', () => {
|
||||
tlstate.toggleHidden(['rect1'])
|
||||
tlstate.deselectAll()
|
||||
tlstate.startBrushSession([-10, -10])
|
||||
tlstate.updateBrushSession([10, 10])
|
||||
tlstate.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(0)
|
||||
})
|
||||
// it('does not select hidden shapes', () => {
|
||||
// const tlstate = new TLDrawState()
|
||||
// .loadDocument(mockDocument)
|
||||
// .deselectAll()
|
||||
// .toggleHidden(['rect1'])
|
||||
// .deselectAll()
|
||||
// .startSession(SessionType.Brush, [-10, -10])
|
||||
// .updateSession([10, 10])
|
||||
// .completeSession()
|
||||
// })
|
||||
|
||||
it('when command is held, require the entire shape to be selected', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
tlstate.deselectAll()
|
||||
tlstate.startBrushSession([-10, -10])
|
||||
tlstate.updateBrushSession([10, 10])
|
||||
tlstate.completeSession()
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.deselectAll()
|
||||
.loadDocument(mockDocument)
|
||||
.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 { Vec } from '@tldraw/vec'
|
||||
import { Data, Session, TLDrawPatch, TLDrawStatus } from '~types'
|
||||
import { Data, Session, SessionType, TLDrawPatch, TLDrawStatus } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
|
||||
export class BrushSession implements Session {
|
||||
id = 'brush'
|
||||
static type = SessionType.Brush
|
||||
status = TLDrawStatus.Brushing
|
||||
origin: number[]
|
||||
snapshot: BrushSnapshot
|
||||
|
@ -16,7 +16,13 @@ export class BrushSession implements Session {
|
|||
|
||||
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 { currentPageId } = data.appState
|
||||
|
||||
|
@ -36,7 +42,7 @@ export class BrushSession implements Session {
|
|||
|
||||
if (!hits.has(selectId)) {
|
||||
if (
|
||||
containMode
|
||||
metaKey
|
||||
? Utils.boundsContain(brush, util.getBounds(shape))
|
||||
: util.hitTestBounds(shape, brush)
|
||||
) {
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { ColorStyle, DashStyle, SizeStyle, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
import {
|
||||
ColorStyle,
|
||||
DashStyle,
|
||||
SessionType,
|
||||
SizeStyle,
|
||||
TLDrawShapeType,
|
||||
TLDrawStatus,
|
||||
} from '~types'
|
||||
|
||||
describe('Draw session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
|
||||
expect(tlstate.getShape('draw1')).toBe(undefined)
|
||||
|
@ -26,8 +33,8 @@ describe('Draw session', () => {
|
|||
},
|
||||
})
|
||||
.select('draw1')
|
||||
.startDrawSession('draw1', [0, 0])
|
||||
.updateDrawSession([10, 10], 0.5)
|
||||
.startSession(SessionType.Draw, [0, 0], 'draw1')
|
||||
.updateSession([10, 10, 0.5])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Utils } from '@tldraw/core'
|
||||
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'
|
||||
|
||||
export class DrawSession implements Session {
|
||||
id = 'draw'
|
||||
static type = SessionType.Draw
|
||||
status = TLDrawStatus.Creating
|
||||
topLeft: number[]
|
||||
origin: number[]
|
||||
|
@ -12,17 +12,16 @@ export class DrawSession implements Session {
|
|||
last: number[]
|
||||
points: number[][]
|
||||
shiftedPoints: number[][] = []
|
||||
snapshot: DrawSnapshot
|
||||
shapeId: string
|
||||
isLocked?: boolean
|
||||
lockedDirection?: 'horizontal' | 'vertical'
|
||||
|
||||
constructor(data: Data, id: string, point: number[]) {
|
||||
constructor(data: Data, point: number[], id: string) {
|
||||
this.origin = point
|
||||
this.previous = point
|
||||
this.last = point
|
||||
this.topLeft = point
|
||||
|
||||
this.snapshot = getDrawSnapshot(data, id)
|
||||
this.shapeId = id
|
||||
|
||||
// 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
|
||||
|
@ -32,8 +31,8 @@ export class DrawSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], pressure: number, isLocked = false) => {
|
||||
const { snapshot } = this
|
||||
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||
const { shapeId } = this
|
||||
|
||||
// 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
|
||||
|
@ -47,7 +46,7 @@ export class DrawSession implements Session {
|
|||
|
||||
// Drawing while holding shift will "lock" the pen to either the
|
||||
// x or y axis, depending on the locking direction.
|
||||
if (isLocked) {
|
||||
if (shiftKey) {
|
||||
if (!this.isLocked && this.points.length > 2) {
|
||||
// If we're locking before knowing what direction we're in, set it
|
||||
// early based on the bigger dimension.
|
||||
|
@ -67,7 +66,7 @@ export class DrawSession implements Session {
|
|||
}
|
||||
|
||||
this.previous = returning
|
||||
this.points.push(returning.concat(pressure))
|
||||
this.points.push(returning.concat(point[2]))
|
||||
}
|
||||
} else if (this.isLocked) {
|
||||
this.isLocked = false
|
||||
|
@ -82,7 +81,7 @@ export class DrawSession implements Session {
|
|||
}
|
||||
|
||||
// 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.
|
||||
if (Vec.isEqual(this.last, newPoint)) return
|
||||
|
@ -129,7 +128,7 @@ export class DrawSession implements Session {
|
|||
pages: {
|
||||
[data.appState.currentPageId]: {
|
||||
shapes: {
|
||||
[snapshot.id]: {
|
||||
[shapeId]: {
|
||||
point: this.topLeft,
|
||||
points,
|
||||
},
|
||||
|
@ -138,7 +137,7 @@ export class DrawSession implements Session {
|
|||
},
|
||||
pageStates: {
|
||||
[data.appState.currentPageId]: {
|
||||
selectedIds: [snapshot.id],
|
||||
selectedIds: [shapeId],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -146,7 +145,7 @@ export class DrawSession implements Session {
|
|||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
const { snapshot } = this
|
||||
const { shapeId } = this
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
return {
|
||||
|
@ -154,7 +153,7 @@ export class DrawSession implements Session {
|
|||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
[snapshot.id]: undefined,
|
||||
[shapeId]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -168,7 +167,7 @@ export class DrawSession implements Session {
|
|||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
const { snapshot } = this
|
||||
const { shapeId } = this
|
||||
const pageId = data.appState.currentPageId
|
||||
|
||||
return {
|
||||
|
@ -178,7 +177,7 @@ export class DrawSession implements Session {
|
|||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
[snapshot.id]: undefined,
|
||||
[shapeId]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -194,7 +193,7 @@ export class DrawSession implements Session {
|
|||
pages: {
|
||||
[pageId]: {
|
||||
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 { mockDocument } from '~test'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { TLDrawShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Handle session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
|
@ -14,8 +13,8 @@ describe('Handle session', () => {
|
|||
type: TLDrawShapeType.Arrow,
|
||||
})
|
||||
.select('arrow1')
|
||||
.startHandleSession([-10, -10], 'end')
|
||||
.updateHandleSession([10, 10])
|
||||
.startSession(SessionType.Arrow, [-10, -10], 'end')
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||
|
@ -31,8 +30,8 @@ describe('Handle session', () => {
|
|||
id: 'arrow1',
|
||||
})
|
||||
.select('arrow1')
|
||||
.startHandleSession([-10, -10], 'end')
|
||||
.updateHandleSession([10, 10])
|
||||
.startSession(SessionType.Arrow, [-10, -10], 'end')
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import { ShapesWithProp, TLDrawStatus } from '~types'
|
||||
import { SessionType, ShapesWithProp, TLDrawStatus } from '~types'
|
||||
import type { Session } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
|
||||
export class HandleSession implements Session {
|
||||
id = 'transform_single'
|
||||
static type = SessionType.Handle
|
||||
status = TLDrawStatus.TranslatingHandle
|
||||
commandId: string
|
||||
delta = [0, 0]
|
||||
|
@ -15,7 +15,7 @@ export class HandleSession implements Session {
|
|||
initialShape: ShapesWithProp<'handles'>
|
||||
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 shapeId = TLDR.getSelectedIds(data, currentPageId)[0]
|
||||
this.topLeft = point
|
||||
|
|
|
@ -5,5 +5,4 @@ export * from './transform'
|
|||
export * from './draw'
|
||||
export * from './rotate'
|
||||
export * from './handle'
|
||||
export * from './text'
|
||||
export * from './arrow'
|
||||
|
|
|
@ -1,37 +1,34 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import Utils from '~../../core/src/utils'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Rotate session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
|
||||
|
||||
tlstate
|
||||
.select('rect1')
|
||||
.startTransformSession([50, 0], 'rotate')
|
||||
.updateTransformSession([100, 50])
|
||||
tlstate.select('rect1').startSession(SessionType.Rotate, [50, 0]).updateSession([100, 50])
|
||||
|
||||
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)
|
||||
|
||||
tlstate.updateTransformSession([0, 50])
|
||||
tlstate.updateSession([0, 50])
|
||||
|
||||
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)
|
||||
|
||||
tlstate.updateTransformSession([0, 50])
|
||||
tlstate.updateSession([0, 50])
|
||||
|
||||
expect(tlstate.getShape('rect1').rotation).toBe((Math.PI * 3) / 2)
|
||||
|
||||
|
@ -52,8 +49,8 @@ describe('Rotate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([50, 0], 'rotate')
|
||||
.updateTransformSession([100, 50])
|
||||
.startSession(SessionType.Rotate, [50, 0])
|
||||
.updateSession([100, 50])
|
||||
.cancel()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
@ -64,10 +61,12 @@ describe('Rotate session', () => {
|
|||
describe('when rotating a single shape while pressing shift', () => {
|
||||
it('Clamps rotation to 15 degrees', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], 'rotate')
|
||||
.updateTransformSession([20, 10], true)
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
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', () => {
|
||||
// Rect 1 is a little rotated
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], 'rotate')
|
||||
.updateTransformSession([5, 5])
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([5, 5])
|
||||
.completeSession()
|
||||
|
||||
// Rect 1 clamp rotated, starting from a little rotation
|
||||
tlstate
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], 'rotate')
|
||||
.updateTransformSession([100, 200], true)
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([100, 200], true)
|
||||
.completeSession()
|
||||
|
||||
expect(Math.round((tlstate.getShape('rect1').rotation || 0) * (180 / Math.PI)) % 15).toEqual(
|
||||
|
@ -98,8 +99,8 @@ describe('Rotate session', () => {
|
|||
// Try again, too.
|
||||
tlstate
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], 'rotate')
|
||||
.updateTransformSession([-100, 5000], true)
|
||||
.startSession(SessionType.Rotate, [0, 0])
|
||||
.updateSession([-100, 5000], true)
|
||||
.completeSession()
|
||||
|
||||
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(
|
||||
Utils.getBoundsCenter(
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Session, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
|
||||
const centerCache = new WeakMap<string[], number[]>()
|
||||
|
||||
export class RotateSession implements Session {
|
||||
id = 'rotate'
|
||||
static type = SessionType.Rotate
|
||||
status = TLDrawStatus.Transforming
|
||||
delta = [0, 0]
|
||||
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 { mockDocument } from '~test'
|
||||
import { TLBoundsCorner } from '@tldraw/core'
|
||||
import { TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Transform single session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([-10, -10], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.TransformSingle, [-10, -10], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||
|
@ -23,8 +23,8 @@ describe('Transform single session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([5, 5], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.TransformSingle, [5, 5], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { TLDrawShape, TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Session } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
|
||||
export class TransformSingleSession implements Session {
|
||||
id = 'transform_single'
|
||||
type = SessionType.TransformSingle
|
||||
status = TLDrawStatus.Transforming
|
||||
commandId: string
|
||||
transformType: TLBoundsEdge | TLBoundsCorner
|
||||
|
@ -29,7 +29,7 @@ export class TransformSingleSession implements Session {
|
|||
|
||||
start = () => void null
|
||||
|
||||
update = (data: Data, point: number[], isAspectRatioLocked = false) => {
|
||||
update = (data: Data, point: number[], shiftKey: boolean) => {
|
||||
const { transformType } = this
|
||||
|
||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||
|
@ -45,7 +45,7 @@ export class TransformSingleSession implements Session {
|
|||
transformType,
|
||||
Vec.sub(point, this.origin),
|
||||
shape.rotation,
|
||||
isAspectRatioLocked || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||
shiftKey || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||
)
|
||||
|
||||
const change = TLDR.getShapeUtils(shape).transformSingle(shape, newBounds, {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { TLDrawState } from '~state'
|
|||
import { mockDocument } from '~test'
|
||||
import { TLBoundsCorner, Utils } from '@tldraw/core'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { TLDrawStatus } from '~types'
|
||||
import { SessionType, TLDrawStatus } from '~types'
|
||||
|
||||
function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
||||
return Utils.getCommonBounds(
|
||||
|
@ -13,7 +13,7 @@ function getShapeBounds(tlstate: TLDrawState, ...ids: string[]) {
|
|||
describe('Transform session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate.loadDocument(mockDocument)
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
|
@ -27,8 +27,8 @@ describe('Transform session', () => {
|
|||
|
||||
tlstate
|
||||
.select('rect1', 'rect2')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.appState.status.current).toBe(TLDrawStatus.Idle)
|
||||
|
@ -60,8 +60,8 @@ describe('Transform session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTransformSession([5, 5], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.Transform, [5, 5], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
@ -72,8 +72,8 @@ describe('Transform session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate)).toMatchObject({
|
||||
|
@ -90,8 +90,8 @@ describe('Transform session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([20, 10], true)
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
|
@ -108,8 +108,8 @@ describe('Transform session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
|
@ -135,8 +135,8 @@ describe('Transform session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([20, 10], true)
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([20, 10], true)
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
|
@ -194,8 +194,8 @@ describe('Transform session', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.startTransformSession([0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateTransformSession([10, 10])
|
||||
.startSession(SessionType.Transform, [0, 0], TLBoundsCorner.TopLeft)
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
|
||||
expect(getShapeBounds(tlstate, 'rect1')).toMatchObject({
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { Session, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import { Session, SessionType, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import type { Data } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
|
||||
export class TransformSession implements Session {
|
||||
id = 'transform'
|
||||
static type = SessionType.Transform
|
||||
status = TLDrawStatus.Transforming
|
||||
scaleX = 1
|
||||
scaleY = 1
|
||||
|
@ -26,7 +26,7 @@ export class TransformSession implements Session {
|
|||
start = () => void null
|
||||
|
||||
// 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 {
|
||||
transformType,
|
||||
snapshot: { shapeBounds, initialBounds, isAllAspectRatioLocked },
|
||||
|
@ -41,7 +41,7 @@ export class TransformSession implements Session {
|
|||
transformType,
|
||||
Vec.sub(point, this.origin),
|
||||
pageState.boundsRotation,
|
||||
isAspectRatioLocked || isAllAspectRatioLocked
|
||||
shiftKey || isAllAspectRatioLocked
|
||||
)
|
||||
|
||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { GroupShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
import { GroupShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Translate session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
it('begins, updates and completes session', () => {
|
||||
it('begins, updateSession', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTranslateSession([5, 5])
|
||||
.updateTranslateSession([10, 10])
|
||||
.startSession(SessionType.Translate, [5, 5])
|
||||
.updateSession([10, 10])
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([5, 5])
|
||||
|
||||
|
@ -33,8 +33,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTranslateSession([5, 5])
|
||||
.updateTranslateSession([10, 10])
|
||||
.startSession(SessionType.Translate, [5, 5])
|
||||
.updateSession([10, 10])
|
||||
.cancelSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
@ -44,8 +44,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20])
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
||||
|
@ -55,8 +55,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], true)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 0])
|
||||
|
@ -66,8 +66,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20])
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([10, 10])
|
||||
|
@ -78,8 +78,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
@ -100,8 +100,8 @@ describe('Translate session', () => {
|
|||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
|
||||
|
@ -117,12 +117,12 @@ describe('Translate session', () => {
|
|||
|
||||
tlstate
|
||||
.select('rect1', 'rect2')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -156,16 +156,16 @@ describe('Translate session', () => {
|
|||
}
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
|
||||
tlstate
|
||||
.select('arrow1')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([30, 30])
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([30, 30])
|
||||
.completeSession()
|
||||
|
||||
// expect(tlstate.bindings.length).toBe(0)
|
||||
|
@ -191,8 +191,8 @@ describe('Translate session', () => {
|
|||
.select('rect1', 'rect2')
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('rect1')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, false)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, false)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
||||
|
@ -218,8 +218,8 @@ describe('Translate session', () => {
|
|||
.select('rect1', 'rect2')
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('rect1')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
|
||||
const children = tlstate.getShape<GroupShape>('groupA').children
|
||||
|
@ -257,8 +257,8 @@ describe('Translate session', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, false)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, false)
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.getShape('groupA').point).toStrictEqual([10, 10])
|
||||
|
@ -283,8 +283,8 @@ describe('Translate session', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group()
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
})
|
||||
|
||||
|
@ -293,10 +293,10 @@ describe('Translate session', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group()
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.updateTranslateSession([20, 20], false, false)
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.updateSession([20, 20], false, false)
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
})
|
||||
|
||||
|
@ -306,8 +306,8 @@ describe('Translate session', () => {
|
|||
.select('rect1', 'rect2')
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA', 'rect3')
|
||||
.startTranslateSession([10, 10])
|
||||
.updateTranslateSession([20, 20], false, true)
|
||||
.startSession(SessionType.Translate, [10, 10])
|
||||
.updateSession([20, 20], false, true)
|
||||
.completeSession()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,12 +10,13 @@ import {
|
|||
TLDrawStatus,
|
||||
ArrowShape,
|
||||
GroupShape,
|
||||
SessionType,
|
||||
} from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import type { Patch } from 'rko'
|
||||
|
||||
export class TranslateSession implements Session {
|
||||
id = 'translate'
|
||||
type = SessionType.Translate
|
||||
status = TLDrawStatus.Translating
|
||||
delta = [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 } =
|
||||
this.snapshot
|
||||
|
||||
|
@ -59,7 +60,7 @@ export class TranslateSession implements Session {
|
|||
|
||||
const delta = Vec.sub(point, this.origin)
|
||||
|
||||
if (isAligned) {
|
||||
if (shiftKey) {
|
||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||
delta[0] = 0
|
||||
} else {
|
||||
|
@ -73,7 +74,7 @@ export class TranslateSession implements Session {
|
|||
this.prev = delta
|
||||
|
||||
// If cloning...
|
||||
if (isCloning) {
|
||||
if (altKey) {
|
||||
// Not Cloning -> Cloning
|
||||
if (!this.isCloning) {
|
||||
this.isCloning = true
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLDrawState } from './tlstate'
|
||||
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', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -61,8 +65,8 @@ describe('TLDrawState', () => {
|
|||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([55, 55])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
|
@ -87,8 +91,8 @@ describe('TLDrawState', () => {
|
|||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([55, 55])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(tlstate.bindings.length).toBe(1)
|
||||
|
@ -138,8 +142,8 @@ describe('TLDrawState', () => {
|
|||
|
||||
it('clears selection when clicking bounds', () => {
|
||||
tlstate.loadDocument(mockDocument).deselectAll()
|
||||
tlstate.startBrushSession([-10, -10])
|
||||
tlstate.updateBrushSession([110, 110])
|
||||
tlstate.startSession(SessionType.Brush, [-10, -10])
|
||||
tlstate.updateSession([110, 110])
|
||||
tlstate.completeSession()
|
||||
expect(tlstate.selectedIds.length).toBe(3)
|
||||
})
|
||||
|
@ -301,8 +305,8 @@ describe('TLDrawState', () => {
|
|||
}
|
||||
)
|
||||
.select('arrow')
|
||||
.startHandleSession([200, 200], 'start', 'arrow')
|
||||
.updateHandleSession([10, 10])
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
.selectAll()
|
||||
.style({ color: ColorStyle.Red })
|
||||
|
@ -340,7 +344,7 @@ describe('TLDrawState', () => {
|
|||
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
tlu.doubleClickShape('rect1')
|
||||
expect(tlstate.selectedGroupId).toStrictEqual('groupA')
|
||||
expect((tlstate.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
})
|
||||
|
||||
|
@ -407,17 +411,35 @@ describe('TLDrawState', () => {
|
|||
}
|
||||
)
|
||||
.selectTool(TLDrawShapeType.Rectangle)
|
||||
.createActiveToolShape([0, 0], 'rect4')
|
||||
|
||||
expect(tlstate.getShape('rect4').childIndex).toBe(4)
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlstate
|
||||
.group(['rect2', 'rect3', 'rect4'], 'groupA')
|
||||
.selectTool(TLDrawShapeType.Rectangle)
|
||||
.createActiveToolShape([0, 0], 'rect5')
|
||||
const prevA = tlstate.shapes.map((shape) => shape.id)
|
||||
|
||||
tlu.pointCanvas({ x: 0, y: 0 })
|
||||
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('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
|
||||
}
|
||||
|
||||
movePointer = (options: PointerOptions = {}) => {
|
||||
const { tlstate } = this
|
||||
tlstate.onPointerMove(inputs.pointerMove(this.getPoint(options), ''), {} as React.PointerEvent)
|
||||
}
|
||||
|
||||
hoverShape = (id: string, options: PointerOptions = {}) => {
|
||||
const { tlstate } = this
|
||||
tlstate.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent)
|
||||
|
@ -27,6 +32,10 @@ export class TLStateUtils {
|
|||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -35,6 +44,10 @@ export class TLStateUtils {
|
|||
inputs.pointerDown(this.getPoint(options), id),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -43,6 +56,10 @@ export class TLStateUtils {
|
|||
inputs.pointerDown(this.getPoint(options), id),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -51,6 +68,10 @@ export class TLStateUtils {
|
|||
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -62,6 +83,10 @@ export class TLStateUtils {
|
|||
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,6 @@ export interface Data {
|
|||
pages: Pick<TLPage<TLDrawShape, TLDrawBinding>, 'id' | 'name' | 'childIndex'>[]
|
||||
hoveredId?: string
|
||||
activeTool: TLDrawShapeType | 'select'
|
||||
activeToolType?: TLDrawToolType | 'select'
|
||||
isToolLocked: boolean
|
||||
isStyleOpen: boolean
|
||||
isEmptyCanvas: boolean
|
||||
|
@ -87,13 +86,30 @@ export interface SelectHistory {
|
|||
stack: string[][]
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
status: TLDrawStatus
|
||||
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
||||
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
||||
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | TLDrawCommand | undefined
|
||||
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
|
||||
export enum SessionType {
|
||||
Transform = 'transform',
|
||||
Translate = 'translate',
|
||||
TransformSingle = 'transformSingle',
|
||||
Brush = 'brush',
|
||||
Arrow = 'arrow',
|
||||
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 {
|
||||
|
@ -114,6 +130,8 @@ export enum TLDrawStatus {
|
|||
// 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 ExceptFirst<T extends unknown[]> = T extends [any, ...infer U] ? U : never
|
||||
|
||||
export enum MoveType {
|
||||
Backward = 'backward',
|
||||
Forward = 'forward',
|
||||
|
@ -145,15 +163,6 @@ export enum FlipType {
|
|||
Vertical = 'vertical',
|
||||
}
|
||||
|
||||
export enum TLDrawToolType {
|
||||
Draw = 'draw',
|
||||
Bounds = 'bounds',
|
||||
Point = 'point',
|
||||
Handle = 'handle',
|
||||
Points = 'points',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum TLDrawShapeType {
|
||||
PostIt = 'post-it',
|
||||
Ellipse = 'ellipse',
|
||||
|
@ -229,14 +238,7 @@ export type TLDrawShape =
|
|||
| GroupShape
|
||||
| PostItShape
|
||||
|
||||
export type TLDrawShapeUtil<T extends TLDrawShape> = TLShapeUtil<
|
||||
T,
|
||||
any,
|
||||
TLDrawMeta,
|
||||
{
|
||||
toolType: TLDrawToolType
|
||||
}
|
||||
>
|
||||
export type TLDrawShapeUtil<T extends TLDrawShape> = TLShapeUtil<T, any, TLDrawMeta>
|
||||
|
||||
export type ArrowBinding = TLBinding<{
|
||||
handleId: keyof ArrowShape['handles']
|
||||
|
|
Loading…
Reference in a new issue