[fix] bound shape size undo (#191)
* fix text centering, double click grouped text to select rather than edit * Fix selecting grouped text * Writes tests for select tool, fixes undo behavior while in session * reduces binding distance, adds constants for binding distance and cloning distances * adjust text sizes * Update arrow.session.spec.ts
This commit is contained in:
parent
efbded7a06
commit
ff50aa6ad5
19 changed files with 418 additions and 412 deletions
|
@ -191,10 +191,6 @@ export const ShapeUtil = function <T extends TLShape, E extends Element, M = any
|
|||
return
|
||||
},
|
||||
|
||||
onStyleChange() {
|
||||
return
|
||||
},
|
||||
|
||||
onBindingChange() {
|
||||
return
|
||||
},
|
||||
|
|
322
packages/core/src/types.d.ts
vendored
322
packages/core/src/types.d.ts
vendored
|
@ -1,322 +0,0 @@
|
|||
import type React from 'react';
|
||||
export declare type Patch<T> = Partial<{
|
||||
[P in keyof T]: T | Partial<T> | Patch<T[P]>;
|
||||
}>;
|
||||
declare type ForwardedRef<T> = ((instance: T | null) => void) | React.MutableRefObject<T | null> | null;
|
||||
export interface TLPage<T extends TLShape, B extends TLBinding> {
|
||||
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 | null;
|
||||
pointedId?: string | null;
|
||||
hoveredId?: string | null;
|
||||
editingId?: string | null;
|
||||
bindingId?: string | null;
|
||||
boundsRotation?: number;
|
||||
currentParentId?: string | null;
|
||||
}
|
||||
export interface TLUser<T extends TLShape> {
|
||||
id: string;
|
||||
color: string;
|
||||
point: number[];
|
||||
selectedIds: string[];
|
||||
activeShapes: T[];
|
||||
}
|
||||
export declare type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>;
|
||||
export declare type TLSnapLine = number[][];
|
||||
export interface TLHandle {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
};
|
||||
}
|
||||
export interface TLShapeProps<T extends TLShape, E = any, M = any> extends TLRenderInfo<T, E, M> {
|
||||
ref: ForwardedRef<E>;
|
||||
shape: T;
|
||||
}
|
||||
export interface TLTool {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export interface TLBinding<M = any> {
|
||||
id: string;
|
||||
type: string;
|
||||
toId: string;
|
||||
fromId: string;
|
||||
meta: M;
|
||||
}
|
||||
export interface TLTheme {
|
||||
accent?: 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 TLShapeChangeHandler<T, K = any> = (shape: {
|
||||
id: string;
|
||||
} & Partial<T>, info?: K) => void;
|
||||
export declare type TLShapeBlurHandler<K = any> = (info?: K) => void;
|
||||
export declare type TLKeyboardEventHandler = (key: string, info: TLKeyboardInfo, e: KeyboardEvent) => void;
|
||||
export declare type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void;
|
||||
export declare type TLShapeCloneHandler = (info: TLPointerInfo<'top' | 'right' | 'bottom' | 'left' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'>, e: React.PointerEvent) => void;
|
||||
export declare type TLShapeLinkHandler = (info: TLPointerInfo<'link'>, 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' | 'center' | 'left' | 'right'>, 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;
|
||||
onShapeChange: TLShapeChangeHandler<T, any>;
|
||||
onShapeBlur: TLShapeBlurHandler<any>;
|
||||
onShapeClone: TLShapeCloneHandler;
|
||||
onRenderCountChange: (ids: string[]) => void;
|
||||
onError: (error: Error) => void;
|
||||
onBoundsChange: (bounds: TLBounds) => void;
|
||||
onKeyDown: TLKeyboardEventHandler;
|
||||
onKeyUp: TLKeyboardEventHandler;
|
||||
}
|
||||
export interface TLBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation?: number;
|
||||
}
|
||||
export interface TLBoundsWithCenter extends TLBounds {
|
||||
midX: number;
|
||||
midY: number;
|
||||
}
|
||||
export declare type TLIntersection = {
|
||||
didIntersect: boolean;
|
||||
message: string;
|
||||
points: number[][];
|
||||
};
|
||||
export declare enum TLBoundsEdge {
|
||||
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"
|
||||
}
|
||||
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;
|
||||
spaceKey: boolean;
|
||||
}
|
||||
export interface TLKeyboardInfo {
|
||||
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[];
|
||||
}
|
||||
export interface TLBezierCurveSegment {
|
||||
start: number[];
|
||||
tangentStart: number[];
|
||||
normalStart: number[];
|
||||
pressureStart: number;
|
||||
end: number[];
|
||||
tangentEnd: number[];
|
||||
normalEnd: number[];
|
||||
pressureEnd: number;
|
||||
}
|
||||
export declare enum SnapPoints {
|
||||
minX = "minX",
|
||||
midX = "midX",
|
||||
maxX = "maxX",
|
||||
minY = "minY",
|
||||
midY = "midY",
|
||||
maxY = "maxY"
|
||||
}
|
||||
export declare type Snap = {
|
||||
id: SnapPoints;
|
||||
isSnapped: false;
|
||||
} | {
|
||||
id: SnapPoints;
|
||||
isSnapped: true;
|
||||
to: number;
|
||||
B: TLBoundsWithCenter;
|
||||
distance: 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: ForwardedRef<E>): React.ReactElement<TLRenderInfo<T, E, M>, E['tagName']>;
|
||||
Indicator(this: TLShapeUtil<T, E, M>, props: {
|
||||
shape: T;
|
||||
meta: M;
|
||||
isHovered: boolean;
|
||||
isSelected: boolean;
|
||||
}): 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;
|
||||
canClone: boolean;
|
||||
canBind: boolean;
|
||||
isStateful: boolean;
|
||||
showBounds: 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;
|
||||
}
|
||||
export declare type MappedByType<K extends string, T extends {
|
||||
type: K;
|
||||
}> = {
|
||||
[P in T['type']]: T extends any ? (P extends T['type'] ? T : never) : never;
|
||||
};
|
||||
export declare type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
export {};
|
||||
//# sourceMappingURL=types.d.ts.map
|
File diff suppressed because one or more lines are too long
|
@ -164,7 +164,7 @@ export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.Poin
|
|||
export type TLBoundsEventHandler = (info: TLPointerInfo<'bounds'>, e: React.PointerEvent) => void
|
||||
|
||||
export type TLBoundsHandleEventHandler = (
|
||||
info: TLPointerInfo<TLBoundsCorner | TLBoundsEdge | 'rotate' | 'center' | 'left' | 'right'>,
|
||||
info: TLPointerInfo<TLBoundsHandle>,
|
||||
e: React.PointerEvent
|
||||
) => void
|
||||
|
||||
|
@ -272,6 +272,8 @@ export enum TLBoundsCorner {
|
|||
BottomLeft = 'bottom_left_corner',
|
||||
}
|
||||
|
||||
export type TLBoundsHandle = TLBoundsCorner | TLBoundsEdge | 'rotate' | 'center' | 'left' | 'right'
|
||||
|
||||
export interface TLPointerInfo<T extends string = string> {
|
||||
target: T
|
||||
pointerId: number
|
||||
|
@ -461,8 +463,6 @@ export type TLShapeUtil<
|
|||
|
||||
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>
|
||||
}
|
||||
|
||||
|
|
2
packages/tldraw/src/constants.ts
Normal file
2
packages/tldraw/src/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const BINDING_DISTANCE = 24
|
||||
export const CLONING_DISTANCE = 32
|
|
@ -120,7 +120,13 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+z,ctrl+z',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.undo()
|
||||
if (canHandleEvent()) {
|
||||
if (tlstate.session) {
|
||||
tlstate.cancelSession()
|
||||
} else {
|
||||
tlstate.undo()
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
|
@ -129,7 +135,13 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+shift-z,command+shift+z',
|
||||
() => {
|
||||
if (canHandleEvent()) tlstate.redo()
|
||||
if (canHandleEvent()) {
|
||||
if (tlstate.session) {
|
||||
tlstate.cancelSession()
|
||||
} else {
|
||||
tlstate.redo()
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[tlstate]
|
||||
|
|
|
@ -71,9 +71,9 @@ const strokeWidths = {
|
|||
}
|
||||
|
||||
const fontSizes = {
|
||||
[SizeStyle.Small]: 32,
|
||||
[SizeStyle.Medium]: 64,
|
||||
[SizeStyle.Large]: 128,
|
||||
[SizeStyle.Small]: 28,
|
||||
[SizeStyle.Medium]: 48,
|
||||
[SizeStyle.Large]: 96,
|
||||
auto: 'auto',
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
intersectRayEllipse,
|
||||
} from '@tldraw/intersect'
|
||||
import { EASINGS } from '~state/utils'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Arrow,
|
||||
|
@ -394,7 +395,7 @@ export const Arrow = new ShapeUtil<ArrowShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
onBindingChange(shape, binding: ArrowBinding, target, targetBounds, center) {
|
||||
const handle = shape.handles[binding.meta.handleId as keyof ArrowShape['handles']]
|
||||
|
||||
const expandedBounds = Utils.expandBounds(targetBounds, 32)
|
||||
const expandedBounds = Utils.expandBounds(targetBounds, BINDING_DISTANCE)
|
||||
|
||||
// The anchor is the "actual" point in the target shape
|
||||
// (Remember that the binding.point is normalized)
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
intersectRayEllipse,
|
||||
} from '@tldraw/intersect'
|
||||
import { EASINGS } from '~state/utils'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Ellipse,
|
||||
|
@ -186,7 +187,15 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
let bindingPoint: number[]
|
||||
let distance: number
|
||||
|
||||
if (!Utils.pointInEllipse(point, center, shape.radius[0] + 32, shape.radius[1] + 32)) return
|
||||
if (
|
||||
!Utils.pointInEllipse(
|
||||
point,
|
||||
center,
|
||||
shape.radius[0] + BINDING_DISTANCE,
|
||||
shape.radius[1] + BINDING_DISTANCE
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (anywhere) {
|
||||
if (Vec.dist(point, this.getCenter(shape)) < 12) {
|
||||
|
@ -238,7 +247,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
Utils.pointInEllipse(point, center, shape.radius[0], shape.radius[1], shape.rotation || 0)
|
||||
) {
|
||||
// Pad the arrow out by 16 points
|
||||
distance = 16
|
||||
distance = BINDING_DISTANCE / 2
|
||||
} else {
|
||||
// Find the distance between the point and the ellipse
|
||||
const innerIntersection = intersectLineSegmentEllipse(
|
||||
|
@ -254,7 +263,7 @@ export const Ellipse = new ShapeUtil<EllipseShape, SVGSVGElement, TLDrawMeta>(()
|
|||
return undefined
|
||||
}
|
||||
|
||||
distance = Math.max(16, Vec.dist(point, innerIntersection))
|
||||
distance = Math.max(BINDING_DISTANCE / 2, Vec.dist(point, innerIntersection))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { defaultStyle } from '~shape/shape-styles'
|
|||
import { GroupShape, TLDrawShapeType, ColorStyle, TLDrawMeta } from '~types'
|
||||
import { getBoundsRectangle } from '../shared'
|
||||
import css from '~styles'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Group,
|
||||
|
@ -46,10 +47,10 @@ export const Group = new ShapeUtil<GroupShape, SVGSVGElement, TLDrawMeta>(() =>
|
|||
{isBinding && (
|
||||
<rect
|
||||
className="tl-binding-indicator"
|
||||
x={-32}
|
||||
y={-32}
|
||||
width={size[0] + 64}
|
||||
height={size[1] + 64}
|
||||
x={-BINDING_DISTANCE}
|
||||
y={-BINDING_DISTANCE}
|
||||
width={size[0] + BINDING_DISTANCE * 2}
|
||||
height={size[1] + BINDING_DISTANCE * 2}
|
||||
/>
|
||||
)}
|
||||
<rect x={0} y={0} width={size[0]} height={size[1]} fill="transparent" pointerEvents="all" />
|
||||
|
|
|
@ -5,6 +5,7 @@ import getStroke, { getStrokePoints } from 'perfect-freehand'
|
|||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import { RectangleShape, DashStyle, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||
import { getBoundsRectangle, transformRectangle, transformSingleRectangle } from '../shared'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
|
@ -40,10 +41,10 @@ export const Rectangle = new ShapeUtil<RectangleShape, SVGSVGElement, TLDrawMeta
|
|||
{isBinding && (
|
||||
<rect
|
||||
className="tl-binding-indicator"
|
||||
x={strokeWidth / 2 - 32}
|
||||
y={strokeWidth / 2 - 32}
|
||||
width={Math.max(0, size[0] - strokeWidth / 2) + 64}
|
||||
height={Math.max(0, size[1] - strokeWidth / 2) + 64}
|
||||
x={strokeWidth / 2 - BINDING_DISTANCE}
|
||||
y={strokeWidth / 2 - BINDING_DISTANCE}
|
||||
width={Math.max(0, size[0] - strokeWidth / 2) + BINDING_DISTANCE * 2}
|
||||
height={Math.max(0, size[1] - strokeWidth / 2) + BINDING_DISTANCE * 2}
|
||||
/>
|
||||
)}
|
||||
<path
|
||||
|
|
|
@ -6,6 +6,7 @@ import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles'
|
|||
import { TextShape, TLDrawShapeType, TLDrawMeta } from '~types'
|
||||
import css from '~styles'
|
||||
import { TextAreaUtils } from '../shared'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
const LETTER_SPACING = -1.5
|
||||
|
||||
|
@ -169,6 +170,19 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
|||
color: styles.stroke,
|
||||
}}
|
||||
>
|
||||
{isBinding && (
|
||||
<div
|
||||
className="tl-binding-indicator"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -BINDING_DISTANCE,
|
||||
left: -BINDING_DISTANCE,
|
||||
width: `calc(100% + ${BINDING_DISTANCE * 2}px)`,
|
||||
height: `calc(100% + ${BINDING_DISTANCE * 2}px)`,
|
||||
backgroundColor: 'var(--tl-selectFill)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
className={textArea({ isBinding })}
|
||||
|
@ -289,18 +303,6 @@ export const Text = new ShapeUtil<TextShape, HTMLDivElement, TLDrawMeta>(() => (
|
|||
point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
|
||||
}
|
||||
},
|
||||
|
||||
onStyleChange(shape) {
|
||||
const center = this.getCenter(shape)
|
||||
|
||||
this.boundsCache.delete(shape)
|
||||
|
||||
const newCenter = this.getCenter(shape)
|
||||
|
||||
return {
|
||||
point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { mockDocument } from '~test'
|
||||
import { SizeStyle } from '~types'
|
||||
import { SizeStyle, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Style command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -46,4 +47,32 @@ describe('Style command', () => {
|
|||
expect(tlstate.getShape('rect2').style.size).toEqual(SizeStyle.Small)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When styling text', () => {
|
||||
it('recenters the shape if the size changed', () => {
|
||||
const tlstate = new TLDrawState().createShapes({
|
||||
id: 'text1',
|
||||
type: TLDrawShapeType.Text,
|
||||
text: 'Hello world',
|
||||
})
|
||||
|
||||
const centerA = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
|
||||
tlstate.select('text1').style({ size: SizeStyle.Large })
|
||||
|
||||
const centerB = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
|
||||
tlstate.style({ size: SizeStyle.Small })
|
||||
|
||||
const centerC = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
|
||||
tlstate.style({ size: SizeStyle.Medium })
|
||||
|
||||
const centerD = TLDR.getShapeUtils(TLDrawShapeType.Text).getCenter(tlstate.getShape('text1'))
|
||||
|
||||
expect(centerA).toEqual(centerB)
|
||||
expect(centerA).toEqual(centerC)
|
||||
expect(centerB).toEqual(centerD)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,17 +1,55 @@
|
|||
import type { ShapeStyles, TLDrawCommand, Data } from '~types'
|
||||
import { ShapeStyles, TLDrawCommand, Data, TLDrawShape, TLDrawShapeType, TextShape } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import type { Patch } from 'rko'
|
||||
import Vec from '@tldraw/vec'
|
||||
|
||||
export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const shapeIdsToMutate = ids.flatMap((id) => TLDR.getDocumentBranch(data, id, currentPageId))
|
||||
|
||||
const { before, after } = TLDR.mutateShapes(
|
||||
data,
|
||||
shapeIdsToMutate,
|
||||
(shape) => ({ style: { ...shape.style, ...changes } }),
|
||||
currentPageId
|
||||
)
|
||||
// const { before, after } = TLDR.mutateShapes(
|
||||
// data,
|
||||
// shapeIdsToMutate,
|
||||
// (shape) => ({ style: { ...shape.style, ...changes } }),
|
||||
// currentPageId
|
||||
// )
|
||||
|
||||
const beforeShapes: Record<string, Patch<TLDrawShape>> = {}
|
||||
const afterShapes: Record<string, Patch<TLDrawShape>> = {}
|
||||
|
||||
shapeIdsToMutate
|
||||
.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
.filter((shape) => !shape.isLocked)
|
||||
.forEach((shape) => {
|
||||
beforeShapes[shape.id] = {
|
||||
style: {
|
||||
...Object.fromEntries(
|
||||
Object.keys(changes).map((key) => [key, shape.style[key as keyof typeof shape.style]])
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
style: changes,
|
||||
}
|
||||
|
||||
if (shape.type === TLDrawShapeType.Text) {
|
||||
beforeShapes[shape.id].point = shape.point
|
||||
afterShapes[shape.id].point = Vec.round(
|
||||
Vec.add(
|
||||
shape.point,
|
||||
Vec.sub(
|
||||
TLDR.getShapeUtils(shape).getCenter(shape),
|
||||
TLDR.getShapeUtils(shape).getCenter({
|
||||
...shape,
|
||||
style: { ...shape.style, ...changes },
|
||||
} as TextShape)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: 'style',
|
||||
|
@ -19,7 +57,7 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
|
|||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: before,
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -31,7 +69,7 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
|
|||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: after,
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,18 +3,14 @@ import { mockDocument } from '~test'
|
|||
import { ArrowShape, SessionType, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Arrow session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
tlstate
|
||||
const restoreDoc = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
|
||||
const restoreDoc = tlstate.document
|
||||
).document
|
||||
|
||||
it('begins, updateSession', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -80,7 +76,7 @@ describe('Arrow session', () => {
|
|||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([132, -32])
|
||||
.updateSession([124, -24])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([1, 0])
|
||||
})
|
||||
|
||||
|
@ -101,7 +97,7 @@ describe('Arrow session', () => {
|
|||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9])
|
||||
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.71, 0.11])
|
||||
|
||||
tlstate.updateSession([91, 9], false, false, true)
|
||||
})
|
||||
|
@ -111,13 +107,9 @@ describe('Arrow session', () => {
|
|||
.loadDocument(restoreDoc)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([91, 9])
|
||||
.updateSession([91, 9], false, false, true)
|
||||
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.68, 0.13])
|
||||
|
||||
tlstate.updateSession([91, 9], false, false, true)
|
||||
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.75, 0.25])
|
||||
expect(tlstate.bindings[0].meta.point).toStrictEqual([0.78, 0.22])
|
||||
})
|
||||
|
||||
it('ignores binding when alt is held', () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import { Utils, TLBounds } from '@tldraw/core'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { BINDING_DISTANCE } from '~constants'
|
||||
|
||||
export class ArrowSession extends Session {
|
||||
static type = SessionType.Arrow
|
||||
|
@ -437,7 +438,7 @@ export class ArrowSession extends Session {
|
|||
point,
|
||||
origin,
|
||||
direction,
|
||||
32,
|
||||
BINDING_DISTANCE,
|
||||
bindAnywhere
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument, TLStateUtils } from '~test'
|
||||
import { SessionType, TLDrawShapeType } from '~types'
|
||||
import { SelectTool } from '.'
|
||||
|
||||
describe('SelectTool', () => {
|
||||
|
@ -9,7 +11,211 @@ describe('SelectTool', () => {
|
|||
})
|
||||
|
||||
describe('When double clicking link controls', () => {
|
||||
it.todo('selects all linked shapes when center is double clicked')
|
||||
it.todo('selects all upstream linked shapes when left is double clicked')
|
||||
it.todo('selects all downstream linked shapes when right is double clicked')
|
||||
const doc = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
{
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [100, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
{
|
||||
id: 'rect3',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [200, 0],
|
||||
size: [100, 100],
|
||||
},
|
||||
{
|
||||
id: 'arrow1',
|
||||
type: TLDrawShapeType.Arrow,
|
||||
point: [200, 200],
|
||||
},
|
||||
{
|
||||
id: 'arrow2',
|
||||
type: TLDrawShapeType.Arrow,
|
||||
point: [200, 200],
|
||||
}
|
||||
)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([50, 50])
|
||||
.completeSession()
|
||||
.startSession(SessionType.Arrow, [200, 200], 'end')
|
||||
.updateSession([150, 50])
|
||||
.completeSession()
|
||||
.select('arrow2')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([150, 50])
|
||||
.completeSession()
|
||||
.startSession(SessionType.Arrow, [200, 200], 'end')
|
||||
.updateSession([250, 50])
|
||||
.completeSession()
|
||||
.deselectAll().document
|
||||
|
||||
it('moves all linked shapes when center is dragged', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu
|
||||
.pointBoundsHandle('center')
|
||||
.movePointer({ x: 100, y: 100 })
|
||||
.expectShapesToBeAtPoints({
|
||||
rect1: [100, 100],
|
||||
rect2: [200, 100],
|
||||
rect3: [300, 100],
|
||||
})
|
||||
|
||||
tlstate.completeSession().undo()
|
||||
|
||||
tlu.expectShapesToBeAtPoints({
|
||||
rect1: [0, 0],
|
||||
rect2: [100, 0],
|
||||
rect3: [200, 0],
|
||||
})
|
||||
})
|
||||
|
||||
it('moves all upstream shapes when center is dragged', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu.pointBoundsHandle('left').movePointer({ x: 100, y: 100 })
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([100, 100])
|
||||
expect(tlstate.getShape('rect2').point).toEqual([200, 100])
|
||||
expect(tlstate.getShape('rect3').point).toEqual([200, 0])
|
||||
})
|
||||
|
||||
it('moves all downstream shapes when center is dragged', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu.pointBoundsHandle('right').movePointer({ x: 100, y: 100 })
|
||||
|
||||
expect(tlstate.getShape('rect1').point).toEqual([0, 0])
|
||||
expect(tlstate.getShape('rect2').point).toEqual([200, 100])
|
||||
expect(tlstate.getShape('rect3').point).toEqual([300, 100])
|
||||
})
|
||||
|
||||
it('selects all linked shapes when center is double clicked', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu.doubleClickBoundHandle('center').expectSelectedIdsToBe(['rect2', 'rect1', 'rect3'])
|
||||
})
|
||||
|
||||
it('selects all linked shapes and arrows when center is double clicked while holding shift', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu
|
||||
.doubleClickBoundHandle('center', { shiftKey: true })
|
||||
.expectSelectedIdsToBe(['rect2', 'rect1', 'rect3', 'arrow1', 'arrow2'])
|
||||
})
|
||||
|
||||
it('selects all upstream linked shapes when left is double clicked', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu.doubleClickBoundHandle('left').expectSelectedIdsToBe(['rect1', 'rect2'])
|
||||
})
|
||||
|
||||
it('selects all upstream linked shapes and arrows when left is double clicked with shift', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu
|
||||
.doubleClickBoundHandle('left', { shiftKey: true })
|
||||
.expectSelectedIdsToBe(['rect1', 'rect2', 'arrow1'])
|
||||
})
|
||||
|
||||
it('selects all downstream linked shapes when right is double clicked', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu.doubleClickBoundHandle('right').expectSelectedIdsToBe(['rect2', 'rect3'])
|
||||
})
|
||||
|
||||
it('selects all downstream linked shapes and arrows when right is double clicked with shift', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(doc).select('rect2')
|
||||
const tlu = new TLStateUtils(tlstate)
|
||||
|
||||
tlu
|
||||
.doubleClickBoundHandle('right', { shiftKey: true })
|
||||
.expectSelectedIdsToBe(['rect2', 'rect3', 'arrow2'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('When selecting grouped shapes', () => {
|
||||
it('Selects the group on single click', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).clickShape('rect1')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['groupA'])
|
||||
})
|
||||
|
||||
it('Drills in and selects the child on double click', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).doubleClickShape('rect1')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1'])
|
||||
})
|
||||
|
||||
it('Selects a sibling on single click after drilling', () => {
|
||||
const tlstate = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).doubleClickShape('rect1').clickShape('rect2')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect2'])
|
||||
})
|
||||
|
||||
it('Selects the group again after selecting a different shape', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).doubleClickShape('rect1').clickShape('rect3').clickShape('rect1')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['groupA'])
|
||||
})
|
||||
|
||||
it('Selects grouped text on double click', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'text1',
|
||||
type: TLDrawShapeType.Text,
|
||||
text: 'Hello world',
|
||||
})
|
||||
.group(['rect1', 'rect2', 'text1'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).doubleClickShape('text1')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['text1'])
|
||||
expect(tlstate.pageState.editingId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('Edits grouped text on double click after selecting', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'text1',
|
||||
type: TLDrawShapeType.Text,
|
||||
text: 'Hello world',
|
||||
})
|
||||
.group(['rect1', 'rect2', 'text1'], 'groupA')
|
||||
|
||||
new TLStateUtils(tlstate).doubleClickShape('text1').doubleClickShape('text1')
|
||||
|
||||
expect(tlstate.selectedIds).toStrictEqual(['text1'])
|
||||
expect(tlstate.pageState.editingId).toBe('text1')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SessionType, TLDrawShapeType } from '~types'
|
|||
import { BaseTool } from '../BaseTool'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { CLONING_DISTANCE } from '~constants'
|
||||
|
||||
enum Status {
|
||||
Idle = 'idle',
|
||||
|
@ -123,14 +124,23 @@ export class SelectTool extends BaseTool<Status> {
|
|||
const center = utils.getCenter(shape)
|
||||
|
||||
let point = {
|
||||
top: [bounds.minX, bounds.minY - (bounds.height + 32)],
|
||||
right: [bounds.maxX + 32, bounds.minY],
|
||||
bottom: [bounds.minX, bounds.maxY + 32],
|
||||
left: [bounds.minX - (bounds.width + 32), bounds.minY],
|
||||
topLeft: [bounds.minX - (bounds.width + 32), bounds.minY - (bounds.height + 32)],
|
||||
topRight: [bounds.maxX + 32, bounds.minY - (bounds.height + 32)],
|
||||
bottomLeft: [bounds.minX - (bounds.width + 32), bounds.maxY + 32],
|
||||
bottomRight: [bounds.maxX + 32, bounds.maxY + 32],
|
||||
top: [bounds.minX, bounds.minY - (bounds.height + CLONING_DISTANCE)],
|
||||
right: [bounds.maxX + CLONING_DISTANCE, bounds.minY],
|
||||
bottom: [bounds.minX, bounds.maxY + CLONING_DISTANCE],
|
||||
left: [bounds.minX - (bounds.width + CLONING_DISTANCE), bounds.minY],
|
||||
topLeft: [
|
||||
bounds.minX - (bounds.width + CLONING_DISTANCE),
|
||||
bounds.minY - (bounds.height + CLONING_DISTANCE),
|
||||
],
|
||||
topRight: [
|
||||
bounds.maxX + CLONING_DISTANCE,
|
||||
bounds.minY - (bounds.height + CLONING_DISTANCE),
|
||||
],
|
||||
bottomLeft: [
|
||||
bounds.minX - (bounds.width + CLONING_DISTANCE),
|
||||
bounds.maxY + CLONING_DISTANCE,
|
||||
],
|
||||
bottomRight: [bounds.maxX + CLONING_DISTANCE, bounds.maxY + CLONING_DISTANCE],
|
||||
}[side]
|
||||
|
||||
if (shape.rotation !== 0) {
|
||||
|
@ -247,15 +257,14 @@ export class SelectTool extends BaseTool<Status> {
|
|||
if (this.pointedBoundsHandle === 'rotate') {
|
||||
// Stat a rotate session
|
||||
this.setStatus(Status.Rotating)
|
||||
|
||||
this.state.startSession(SessionType.Rotate, point)
|
||||
} else if (
|
||||
this.pointedBoundsHandle === 'center' ||
|
||||
this.pointedBoundsHandle === 'left' ||
|
||||
this.pointedBoundsHandle === 'right'
|
||||
) {
|
||||
this.setStatus(Status.Translating)
|
||||
const point = this.state.getPagePoint(info.origin)
|
||||
this.setStatus(Status.Translating)
|
||||
this.state.startSession(SessionType.Translate, point, false, this.pointedBoundsHandle)
|
||||
} else {
|
||||
// Stat a transform session
|
||||
|
@ -273,6 +282,14 @@ export class SelectTool extends BaseTool<Status> {
|
|||
this.state.startSession(SessionType.Transform, point, this.pointedBoundsHandle)
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the session with the current point
|
||||
this.state.updateSession(
|
||||
this.state.getPagePoint(info.point),
|
||||
info.shiftKey,
|
||||
info.altKey,
|
||||
info.metaKey
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -529,19 +546,18 @@ export class SelectTool extends BaseTool<Status> {
|
|||
}
|
||||
|
||||
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) {
|
||||
// If we can edit the shape (and if we can select the shape) then
|
||||
// start editing
|
||||
if (
|
||||
TLDR.getShapeUtils(shape.type).canEdit &&
|
||||
(shape.parentId === this.state.currentPageId || shape.parentId === this.selectedGroupId)
|
||||
) {
|
||||
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 the shape is the child of a group, then drill into the group?
|
||||
if (shape.parentId !== this.state.currentPageId) {
|
||||
this.selectedGroupId = shape.parentId
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { inputs, TLBoundsEdge, TLBoundsCorner } from '@tldraw/core'
|
||||
import { inputs, TLBoundsEdge, TLBoundsCorner, TLBoundsHandle } from '@tldraw/core'
|
||||
import type { TLDrawState } from '~state'
|
||||
|
||||
interface PointerOptions {
|
||||
|
@ -20,11 +20,13 @@ export class TLStateUtils {
|
|||
movePointer = (options: PointerOptions = {}) => {
|
||||
const { tlstate } = this
|
||||
tlstate.onPointerMove(inputs.pointerMove(this.getPoint(options), ''), {} as React.PointerEvent)
|
||||
return this
|
||||
}
|
||||
|
||||
hoverShape = (id: string, options: PointerOptions = {}) => {
|
||||
const { tlstate } = this
|
||||
tlstate.onHoverShape(inputs.pointerDown(this.getPoint(options), id), {} as React.PointerEvent)
|
||||
return this
|
||||
}
|
||||
|
||||
pointCanvas = (options: PointerOptions = {}) => {
|
||||
|
@ -75,12 +77,21 @@ export class TLStateUtils {
|
|||
return this
|
||||
}
|
||||
|
||||
pointBoundsHandle = (
|
||||
id: TLBoundsCorner | TLBoundsEdge | 'rotate',
|
||||
options: PointerOptions = {}
|
||||
) => {
|
||||
this.tlstate.onPointBounds(
|
||||
inputs.pointerDown(this.getPoint(options), 'bounds'),
|
||||
pointBoundsHandle = (id: TLBoundsHandle, options: PointerOptions = {}) => {
|
||||
this.tlstate.onPointBoundsHandle(
|
||||
inputs.pointerDown(this.getPoint(options), id),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
doubleClickBoundHandle = (id: TLBoundsHandle, options: PointerOptions = {}) => {
|
||||
this.tlstate.onDoubleClickBoundsHandle(
|
||||
inputs.pointerDown(this.getPoint(options), id),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.tlstate.onPointerDown(
|
||||
|
@ -138,4 +149,16 @@ export class TLStateUtils {
|
|||
clientY: y,
|
||||
} as PointerEvent
|
||||
}
|
||||
|
||||
expectSelectedIdsToBe = (b: string[]) => {
|
||||
expect(new Set(this.tlstate.selectedIds)).toEqual(new Set(b))
|
||||
return this
|
||||
}
|
||||
|
||||
expectShapesToBeAtPoints = (shapes: Record<string, number[]>) => {
|
||||
Object.entries(shapes).forEach(([id, point]) => {
|
||||
expect(this.tlstate.getShape(id).point).toEqual(point)
|
||||
})
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue