[improvement] More selection logic (#1806)

This PR includes further UX improvements to selection.

- clicking inside of a hollow shape will no longer select it on pointer
up
- clicking a shape's filled label will select it on pointer down
- clicking a shape's empty label will select it on pointer up
- clicking and dragging a selected arrow is now better limited to its
body, not its bounds
- arrows will no longer bind to labels

### Text labels

A big change here relates to text labels. Previously, we had listeners
set on the text label elements; I've removed these and we now check the
actual label bounds geometry for a hit. For geo shapes, this geometry is
now placed correctly based on the alignment / vertical alignment of the
label.

- Clicking on a label with text in it will select the shape on pointer
down.
- Clicking on an empty text label will select the shape on pointer up.

## Hollow shapes

Previously, shapes with `fill: none` were also being selected on pointer
up. I've removed that logic because it was producing wrong-feeling
selections too often. We now select these shapes only when clicking on
the label (as mentioned above) or when clicking on the edges of the
shape. This is in line with the original behavior (currently on
tldraw.com, prior to the earlier PR that updated selection logic).

## Arrows

Arrows still hit the inside of hollow shapes, using the "smallest
hovered" logic previously used for pointer-up selection on hollow
shapes. They also now correctly do so while ignoring text labels.

### Change Type

- [x] `minor` — New feature

### Test Plan

1. try selecting geo shapes, nested geo shapes, arrows and shapes with
labels or without labels

- [x] Unit Tests
fix-pinch-safari-desktop
Steve Ruiz 2023-08-13 16:55:24 +01:00 zatwierdzone przez GitHub
rodzic eaba3c8f2a
commit 22329c51fc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
58 zmienionych plików z 1173 dodań i 1137 usunięć

Wyświetl plik

@ -91,7 +91,7 @@ const InsideOfEditorContext = () => {
const selection = [...editor.selectedShapeIds]
editor.selectAll()
editor.setStyle(DefaultColorStyle, i % 2 ? 'blue' : 'light-blue')
editor.setSelectedShapeIds(selection)
editor.setSelectedShapes(selection)
i++
}, 1000)

Wyświetl plik

@ -52,7 +52,7 @@ export const SelectedShapeIdsCount = track(() => {
### Changing the state
The [`Editor`](/gen/editor/Editor) class has many methods for updating its state. For example, you can change the current page's selection using [`editor.setSelectedShapeIds`](/gen/editor/Editor#setSelectedShapeIds). You can also use other convenience methods, such as [`editor.select`](/gen/editor/Editor#), [`editor.deselect`](/gen/editor/Editor#deselect), [`editor.selectAll`](/gen/editor/Editor#selectAll), or [`editor.selectNone`](/gen/editor/Editor#selectNone).
The [`Editor`](/gen/editor/Editor) class has many methods for updating its state. For example, you can change the current page's selection using [`editor.setSelectedShapes`](/gen/editor/Editor#setSelectedShapes). You can also use other convenience methods, such as [`editor.select`](/gen/editor/Editor#), [`editor.deselect`](/gen/editor/Editor#deselect), [`editor.selectAll`](/gen/editor/Editor#selectAll), or [`editor.selectNone`](/gen/editor/Editor#selectNone).
```ts
editor.selectNone()

Wyświetl plik

@ -43,8 +43,8 @@ Every shape contains some **base information**. These include the shape's type,
```js
const shape = editor.getShape('some-shape-id')
if (shape) {
console.log(shape.type) // The shape's type
console.log(shape.opacity) // The shape's opacity
shape.type // The shape's type
shape.opacity // The shape's opacity
}
```
@ -53,8 +53,8 @@ Every shape also contains some **shape-specific information**. We call these `pr
```js
const textShape = editor.getShape('some-text-shape-id')
if (textShape) {
console.log(textShape.props.text) // The shape's text
console.log(textShape.props.font) // The shape's font
textShape.props.text // The shape's text
textShape.props.font // The shape's font
}
```
@ -66,7 +66,7 @@ While tldraw's shapes themselves are simple JSON objects, we use [`ShapeUtil`](/
const util = this.getShapeUtil(myShape)
if (util) {
const bounds = util.getBounds(myShape)
console.log(bounds) // The shape's bounding box
bounds) // The shape's bounding box
}
```

Wyświetl plik

@ -528,9 +528,7 @@ export class Edge2d extends Geometry2d {
export class Editor extends EventEmitter<TLEventMap> {
constructor({ store, user, shapeUtils, tools, getContainer, initialState }: TLEditorOptions);
addOpenMenu(id: string): this;
alignShapes(shapes: TLShape[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
// (undocumented)
alignShapes(ids: TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
animateShape(partial: null | TLShapePartial | undefined, animationOptions?: TLAnimationOptions): this;
animateShapes(partials: (null | TLShapePartial | undefined)[], animationOptions?: Partial<{
duration: number;
@ -549,12 +547,8 @@ export class Editor extends EventEmitter<TLEventMap> {
bail(): this;
bailToMark(id: string): this;
batch(fn: () => void): this;
bringForward(shapes: TLShape[]): this;
// (undocumented)
bringForward(ids: TLShapeId[]): this;
bringToFront(shapes: TLShape[]): this;
// (undocumented)
bringToFront(ids: TLShapeId[]): this;
bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this;
get camera(): TLCamera;
get cameraState(): "idle" | "moving";
cancel(): this;
@ -598,35 +592,23 @@ export class Editor extends EventEmitter<TLEventMap> {
get currentPageState(): TLInstancePageState;
get currentTool(): StateNode | undefined;
get currentToolId(): string;
deleteAssets(assets: TLAsset[]): this;
// (undocumented)
deleteAssets(ids: TLAssetId[]): this;
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
deleteOpenMenu(id: string): this;
deletePage(page: TLPage): this;
// (undocumented)
deletePage(id: TLPageId): this;
deletePage(page: TLPage | TLPageId): this;
deleteShape(id: TLShapeId): this;
// (undocumented)
deleteShape(shape: TLShape): this;
deleteShapes(ids: TLShapeId[]): this;
// (undocumented)
deleteShapes(shapes: TLShape[]): this;
deselect(...ids: TLShapeId[]): this;
// (undocumented)
deselect(...shapes: TLShape[]): this;
deselect(...shapes: TLShape[] | TLShapeId[]): this;
dispatch: (info: TLEventInfo) => this;
readonly disposables: Set<() => void>;
dispose(): void;
distributeShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
distributeShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical'): this;
distributeShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
get documentSettings(): TLDocument;
duplicatePage(page: TLPage, createId?: TLPageId): this;
// (undocumented)
duplicatePage(id: TLPageId, createId?: TLPageId): this;
duplicateShapes(shapes: TLShape[], offset?: VecLike): this;
// (undocumented)
duplicateShapes(ids: TLShapeId[], offset?: VecLike): this;
duplicatePage(page: TLPage | TLPageId, createId?: TLPageId): this;
duplicateShapes(shapes: TLShape[] | TLShapeId[], offset?: VecLike): this;
get editingShape(): TLShape | undefined;
get editingShapeId(): null | TLShapeId;
readonly environment: EnvironmentManager;
@ -648,103 +630,51 @@ export class Editor extends EventEmitter<TLEventMap> {
}) => void) | null;
}[K];
};
findCommonAncestor(shapes: TLShape[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
// (undocumented)
findCommonAncestor(ids: TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
findShapeAncestor(shape: TLShape, predicate: (parent: TLShape) => boolean): TLShape | undefined;
// (undocumented)
findShapeAncestor(id: TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
flipShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
flipShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical'): this;
findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
get focusedGroup(): TLShape | undefined;
get focusedGroupId(): TLPageId | TLShapeId;
getAncestorPageId(shape?: TLShape): TLPageId | undefined;
// (undocumented)
getAncestorPageId(shapeId?: TLShapeId): TLPageId | undefined;
getArrowInfo(shape: TLArrowShape): TLArrowInfo | undefined;
// (undocumented)
getArrowInfo(id: TLShapeId): TLArrowInfo | undefined;
getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined;
getArrowInfo(shape: TLArrowShape | TLShapeId): TLArrowInfo | undefined;
getArrowsBoundTo(shapeId: TLShapeId): {
arrowId: TLShapeId;
handleId: "end" | "start";
}[];
getAsset(asset: TLAsset): TLAsset | undefined;
// (undocumented)
getAsset(id: TLAssetId): TLAsset | undefined;
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
getAssetForExternalContent(info: TLExternalAssetContent_2): Promise<TLAsset | undefined>;
getContainer: () => HTMLElement;
getContentFromCurrentPage(ids: TLShapeId[]): TLContent | undefined;
// (undocumented)
getContentFromCurrentPage(shapes: TLShape[]): TLContent | undefined;
getCurrentPageShapeIds(pageId: TLPageId): Set<TLShapeId>;
// (undocumented)
getCurrentPageShapeIds(page: TLPage): Set<TLShapeId>;
getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined;
getDroppingOverShape(point: VecLike, droppingShapes?: TLShape[]): TLShape | undefined;
getHighestIndexForParent(parent: TLPage | TLShape): string;
// (undocumented)
getHighestIndexForParent(parentId: TLParentId): string;
getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): string;
getInitialMetaForShape(_shape: TLShape): JsonObject;
getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape;
// (undocumented)
getOutermostSelectableShape(id: TLShapeId, filter?: (shape: TLShape) => boolean): TLShape;
getPage(page: TLPage): TLPage | undefined;
// (undocumented)
getPage(id: TLPageId): TLPage | undefined;
getPointInParentSpace(shape: TLShape, point: VecLike): Vec2d;
// (undocumented)
getPointInParentSpace(id: TLShapeId, point: VecLike): Vec2d;
getPointInShapeSpace(shape: TLShape, point: VecLike): Vec2d;
// (undocumented)
getPointInShapeSpace(id: TLShapeId, point: VecLike): Vec2d;
getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape;
getPage(page: TLPage | TLPageId): TLPage | undefined;
getPageShapeIds(page: TLPage | TLPageId): Set<TLShapeId>;
getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec2d;
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec2d;
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined;
getShape<T extends TLShape = TLShape>(id: TLParentId): T | undefined;
// (undocumented)
getShape<T extends TLShape = TLShape>(shape: TLShape): T | undefined;
getShapeAncestors(shape: TLShape, acc?: TLShape[]): TLShape[];
// (undocumented)
getShapeAncestors(id: TLShapeId, acc?: TLShape[]): TLShape[];
getShape<T extends TLShape = TLShape>(shape: TLParentId | TLShape): T | undefined;
getShapeAncestors(shape: TLShape | TLShapeId, acc?: TLShape[]): TLShape[];
getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId>;
getShapeAtPoint(point: VecLike, opts?: {
hitInside?: boolean | undefined;
margin?: number | undefined;
ignoreGroups?: boolean | undefined;
hitInside?: boolean | undefined;
hitLabels?: boolean | undefined;
hitFrameInside?: boolean | undefined;
filter?: ((shape: TLShape) => boolean) | undefined;
}): TLShape | undefined;
getShapeClipPath(shape: TLShape): string | undefined;
// (undocumented)
getShapeClipPath(id: TLShapeId): string | undefined;
getShapeGeometry<T extends Geometry2d>(id: TLShapeId): T;
// (undocumented)
getShapeGeometry<T extends Geometry2d>(shape: TLShape): T;
getShapeHandles<T extends TLShape>(id: T['id']): TLHandle[] | undefined;
// (undocumented)
getShapeHandles<T extends TLShape>(shape: T): TLHandle[] | undefined;
getShapeLocalTransform(shape: TLShape): Matrix2d;
// (undocumented)
getShapeLocalTransform(id: TLShapeId): Matrix2d;
getShapeMask(id: TLShapeId): undefined | VecLike[];
// (undocumented)
getShapeMask(shape: TLShape): undefined | VecLike[];
getShapeMaskedPageBounds(id: TLShapeId): Box2d | undefined;
// (undocumented)
getShapeMaskedPageBounds(shape: TLShape): Box2d | undefined;
getShapeOutlineSegments<T extends TLShape>(shape: T): Vec2d[][];
// (undocumented)
getShapeOutlineSegments<T extends TLShape>(id: T['id']): Vec2d[][];
getShapePageBounds(shape: TLShape): Box2d | undefined;
// (undocumented)
getShapePageBounds(id: TLShapeId): Box2d | undefined;
getShapePageTransform(id: TLShapeId): Matrix2d;
// (undocumented)
getShapePageTransform(shape: TLShape): Matrix2d;
getShapeParent(shape?: TLShape): TLShape | undefined;
// (undocumented)
getShapeParent(shapeId?: TLShapeId): TLShape | undefined;
getShapeParentTransform(shape: TLShape): Matrix2d;
// (undocumented)
getShapeParentTransform(id: TLShapeId): Matrix2d;
getShapeClipPath(shape: TLShape | TLShapeId): string | undefined;
getShapeGeometry<T extends Geometry2d>(shape: TLShape | TLShapeId): T;
getShapeHandles<T extends TLShape>(shape: T | T['id']): TLHandle[] | undefined;
getShapeLocalTransform(shape: TLShape | TLShapeId): Matrix2d;
getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[];
getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box2d | undefined;
getShapeOutlineSegments<T extends TLShape>(shape: T | T['id']): Vec2d[][];
getShapePageBounds(shape: TLShape | TLShapeId): Box2d | undefined;
getShapePageTransform(shape: TLShape | TLShapeId): Matrix2d;
getShapeParent(shape?: TLShape | TLShapeId): TLShape | undefined;
getShapeParentTransform(shape: TLShape | TLShapeId): Matrix2d;
getShapesAtPoint(point: VecLike, opts?: {
margin?: number | undefined;
hitInside?: boolean | undefined;
@ -756,9 +686,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getShapeUtil<S extends TLUnknownShape>(type: S['type']): ShapeUtil<S>;
// (undocumented)
getShapeUtil<T extends ShapeUtil>(type: T extends ShapeUtil<infer R> ? R['type'] : string): T;
getSortedChildIdsForParent(parent: TLPage | TLShape): TLShapeId[];
// (undocumented)
getSortedChildIdsForParent(parentId: TLParentId): TLShapeId[];
getSortedChildIdsForParent(parent: TLPage | TLParentId | TLShape): TLShapeId[];
getStateDescendant(path: string): StateNode | undefined;
// @internal (undocumented)
getStyleForNextShape<T>(style: StyleProp<T>): T;
@ -769,12 +697,8 @@ export class Editor extends EventEmitter<TLEventMap> {
darkMode?: boolean | undefined;
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
}>): Promise<SVGSVGElement | undefined>;
groupShapes(shapes: TLShape[], groupId?: TLShapeId): this;
// (undocumented)
groupShapes(ids: TLShapeId[], groupId?: TLShapeId): this;
hasAncestor(shape: TLShape | undefined, ancestorId: TLShapeId): boolean;
// (undocumented)
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
groupShapes(shapes: TLShape[] | TLShapeId[], groupId?: TLShapeId): this;
hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean;
get hintingShapeIds(): TLShapeId[];
get hintingShapes(): NonNullable<TLShape | undefined>[];
readonly history: HistoryManager<this>;
@ -802,24 +726,15 @@ export class Editor extends EventEmitter<TLEventMap> {
};
get instanceState(): TLInstance;
interrupt(): this;
isAncestorSelected(id: TLShapeId): boolean;
// (undocumented)
isAncestorSelected(shape: TLShape): boolean;
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
isIn(path: string): boolean;
isInAny(...paths: string[]): boolean;
get isMenuOpen(): boolean;
isPointInShape(shape: TLShape, point: VecLike, opts?: {
margin?: number;
hitInside?: boolean;
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
margin?: number | undefined;
hitInside?: boolean | undefined;
}): boolean;
// (undocumented)
isPointInShape(id: TLShapeId, point: VecLike, opts?: {
margin?: number;
hitInside?: boolean;
}): boolean;
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
// (undocumented)
isShapeInPage(shapeId: TLShapeId, pageId?: TLPageId): boolean;
isShapeInPage(shape: TLShape | TLShapeId, pageId?: TLPageId): boolean;
isShapeOfType<T extends TLUnknownShape>(shape: TLUnknownShape, type: T['type']): shape is T;
// (undocumented)
isShapeOfType<T extends TLUnknownShape>(shapeId: TLUnknownShape['id'], type: T['type']): shapeId is T['id'];
@ -827,17 +742,11 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented)
isShapeOrAncestorLocked(id?: TLShapeId): boolean;
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this;
moveShapesToPage(shapes: TLShape[], pageId: TLPageId): this;
// (undocumented)
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented)
nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this;
nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
get onlySelectedShape(): null | TLShape;
get openMenus(): string[];
packShapes(shapes: TLShape[], gap: number): this;
// (undocumented)
packShapes(ids: TLShapeId[], gap: number): this;
packShapes(shapes: TLShape[] | TLShapeId[], gap: number): this;
get pages(): TLPage[];
get pageStates(): TLInstancePageState[];
pageToScreen(point: VecLike): {
@ -862,9 +771,7 @@ export class Editor extends EventEmitter<TLEventMap> {
registerExternalContentHandler<T extends TLExternalContent_2['type']>(type: T, handler: ((info: T extends TLExternalContent_2['type'] ? TLExternalContent_2 & {
type: T;
} : TLExternalContent_2) => void) | null): this;
renamePage(page: TLPage, name: string, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented)
renamePage(id: TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this;
get renderingBounds(): Box2d;
get renderingBoundsExpanded(): Box2d;
renderingBoundsMargin: number;
@ -878,25 +785,17 @@ export class Editor extends EventEmitter<TLEventMap> {
isCulled: boolean;
maskedPageBounds: Box2d | undefined;
}[];
reparentShapes(shapes: TLShape[], parentId: TLParentId, insertIndex?: string): this;
// (undocumented)
reparentShapes(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
resetZoom(point?: Vec2d, animation?: TLAnimationOptions): this;
resizeShape(shape: TLShape, scale: VecLike, options?: TLResizeShapeOptions): this;
// (undocumented)
resizeShape(id: TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
resizeShape(shape: TLShape | TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
readonly root: RootState;
rotateShapesBy(shapes: TLShape[], delta: number): this;
// (undocumented)
rotateShapesBy(ids: TLShapeId[], delta: number): this;
rotateShapesBy(shapes: TLShape[] | TLShapeId[], delta: number): this;
screenToPage(point: VecLike): {
x: number;
y: number;
z: number;
};
select(...ids: TLShapeId[]): this;
// (undocumented)
select(...shapes: TLShape[]): this;
select(...shapes: TLShape[] | TLShapeId[]): this;
selectAll(): this;
get selectedShapeIds(): TLShapeId[];
get selectedShapes(): TLShape[];
@ -904,38 +803,20 @@ export class Editor extends EventEmitter<TLEventMap> {
get selectionRotatedPageBounds(): Box2d | undefined;
get selectionRotation(): number;
selectNone(): this;
sendBackward(shapes: TLShape[]): this;
// (undocumented)
sendBackward(ids: TLShapeId[]): this;
sendToBack(shapes: TLShape[]): this;
// (undocumented)
sendToBack(ids: TLShapeId[]): this;
sendBackward(shapes: TLShape[] | TLShapeId[]): this;
sendToBack(shapes: TLShape[] | TLShapeId[]): this;
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
setCroppingShape(shape: null | TLShape): this;
// (undocumented)
setCroppingShape(id: null | TLShapeId): this;
setCurrentPage(page: TLPage, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented)
setCurrentPage(pageId: TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCroppingShape(shape: null | TLShape | TLShapeId): this;
setCurrentPage(page: TLPage | TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCurrentTool(id: string, info?: {}): this;
setCursor: (cursor: Partial<TLCursor>) => this;
setEditingShape(shape: null | TLShape): this;
// (undocumented)
setEditingShape(id: null | TLShapeId): this;
setErasingShapes(shapes: TLShape[]): this;
// (undocumented)
setErasingShapes(ids: TLShapeId[]): this;
setFocusedGroup(shape: null | TLGroupShape): this;
// (undocumented)
setFocusedGroup(id: null | TLShapeId): this;
setHintingShapes(shapes: TLShape[]): this;
// (undocumented)
setHintingShapes(ids: TLShapeId[]): this;
setHoveredShape(shape: null | TLShape): this;
// (undocumented)
setHoveredShape(id: null | TLShapeId): this;
setEditingShape(shape: null | TLShape | TLShapeId): this;
setErasingShapes(shapes: TLShape[] | TLShapeId[]): this;
setFocusedGroup(shape: null | TLGroupShape | TLShapeId): this;
setHintingShapes(shapes: TLShape[] | TLShapeId[]): this;
setHoveredShape(shape: null | TLShape | TLShapeId): this;
setOpacity(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
setSelectedShapeIds(ids: TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
setSelectedShapes(shapes: TLShape[] | TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
@ -950,24 +831,18 @@ export class Editor extends EventEmitter<TLEventMap> {
speedThreshold?: number | undefined;
}): this;
readonly snaps: SnapManager;
stackShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical', gap: number): this;
// (undocumented)
stackShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
stackShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
startFollowingUser(userId: string): this;
stopCameraAnimation(): this;
stopFollowingUser(): this;
readonly store: TLStore;
stretchShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
stretchShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical'): this;
stretchShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
styleProps: {
[key: string]: Map<StyleProp<unknown>, string>;
};
readonly textMeasure: TextManager;
toggleLock(shapes: TLShape[]): this;
// (undocumented)
toggleLock(ids: TLShapeId[]): this;
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
undo(): this;
ungroupShapes(ids: TLShapeId[]): this;
// (undocumented)
@ -987,9 +862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
get viewportPageCenter(): Vec2d;
get viewportScreenBounds(): Box2d;
get viewportScreenCenter(): Vec2d;
visitDescendants(parent: TLPage | TLShape, visitor: (id: TLShapeId) => false | void): this;
// (undocumented)
visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => false | void): this;
visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this;
zoomIn(point?: Vec2d, animation?: TLAnimationOptions): this;
get zoomLevel(): number;
zoomOut(point?: Vec2d, animation?: TLAnimationOptions): this;
@ -1239,7 +1112,6 @@ export const GRID_STEPS: {
export class Group2d extends Geometry2d {
constructor(config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {
children: Geometry2d[];
operation: 'exclude' | 'intersect' | 'subtract' | 'union';
});
// (undocumented)
children: Geometry2d[];
@ -1256,8 +1128,6 @@ export class Group2d extends Geometry2d {
// (undocumented)
nearestPoint(point: Vec2d): Vec2d;
// (undocumented)
operation: 'exclude' | 'intersect' | 'subtract' | 'union';
// (undocumented)
get outerVertices(): Vec2d[];
// (undocumented)
toSimpleSvgPath(): string;

Wyświetl plik

@ -33,6 +33,9 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
>
{renderingShapes.map((result) => {
const shape = editor.getShape(result.id)!
if (shape.type === 'group') return null
const geometry = editor.getShapeGeometry(shape)
const pageTransform = editor.getShapePageTransform(shape)!
@ -46,10 +49,10 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
<g key={result.id + '_outline'} transform={pageTransform.toCssString()}>
{showStroke && (
<path
stroke="dodgerblue"
stroke="red"
strokeWidth={2}
fill="none"
opacity={0.25}
opacity={0.5}
d={geometry.toSimpleSvgPath()}
/>
)}
@ -61,6 +64,8 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
cy={v.y}
r={2}
fill={`hsl(${modulate(i, [0, vertices.length - 1], [120, 0])}, 100%, 50%)`}
stroke="black"
strokeWidth="1"
/>
))}
{distanceToPoint > 0 && showClosestPointOnOutline && (

Wyświetl plik

@ -45,7 +45,6 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
points,
})
}),
operation: 'union',
})
}

Wyświetl plik

@ -110,7 +110,7 @@ export class Pointing extends StateNode {
},
])
this.editor.setSelectedShapeIds([id])
this.editor.setSelectedShapes([id])
if (this.editor.instanceState.isToolLocked) {
this.parent.transition('idle', {})

Wyświetl plik

@ -5,20 +5,17 @@ import { Geometry2d, Geometry2dOptions } from './Geometry2d'
/** @public */
export class Group2d extends Geometry2d {
children: Geometry2d[]
operation: 'union' | 'subtract' | 'exclude' | 'intersect'
constructor(
config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & {
children: Geometry2d[]
operation: 'union' | 'subtract' | 'exclude' | 'intersect'
}
) {
super({ ...config, isClosed: true, isFilled: false })
const { children, operation } = config
const { children } = config
if (children.length === 0) throw Error('Group2d must have at least one child')
this.operation = operation
this.children = children
}
@ -30,35 +27,22 @@ export class Group2d extends Geometry2d {
let d = Infinity
let p: Vec2d | undefined
const { children, operation } = this
const { children } = this
if (children.length === 0) {
throw Error('no children')
}
switch (operation) {
case 'union': {
for (const child of children) {
const nearest = child.nearestPoint(point)
const dist = nearest.dist(point)
if (dist < d) {
d = dist
p = nearest
}
}
if (!p) throw Error('nearest point not found')
return p
}
case 'subtract': {
throw Error('not implemented')
}
case 'exclude': {
throw Error('not implemented')
}
case 'intersect': {
throw Error('not implemented')
for (const child of children) {
const nearest = child.nearestPoint(point)
const dist = nearest.dist(point)
if (dist < d) {
d = dist
p = nearest
}
}
if (!p) throw Error('nearest point not found')
return p
}
override distanceToPoint(point: Vec2d, hitInside = false) {
@ -66,57 +50,7 @@ export class Group2d extends Geometry2d {
}
override hitTestPoint(point: Vec2d, margin: number, hitInside: boolean): boolean {
const { operation } = this
if (hitInside) {
return this.bounds.containsPoint(point, margin)
}
const dist = this.distanceToPoint(point, hitInside)
const isCloseEnough = dist <= margin
if (!isCloseEnough) return false
switch (operation) {
case 'union': {
return true
}
case 'subtract': {
throw Error(`not implemented`)
// for (let i = 0, child: Geometry2d, n = children.length; i < n; i++) {
// child = children[i]
// const nearest = child.nearestPoint(point)
// const dist = nearest.dist(point)
// if (i === 0) {
// if (dist > margin) return false
// } else {
// if (dist < -margin) {
// return false
// }
// }
// }
// return true
}
case 'exclude': {
throw Error(`not implemented`)
// let hits = 0
// for (let i = 0, child: Geometry2d, n = children.length; i < n; i++) {
// child = children[i]
// const nearest = child.nearestPoint(point)
// const dist = nearest.dist(point)
// if (dist < -margin) {
// hits++
// }
// }
// return hits % 2 === 1
}
case 'intersect': {
throw Error(`not implemented`)
// return children.every((child) => child.distanceToPoint(point) <= margin / zoom)
}
}
return this.children[0].hitTestPoint(point, margin, hitInside)
}
override hitTestLineSegment(A: Vec2d, B: Vec2d, zoom: number): boolean {

Wyświetl plik

@ -44,7 +44,7 @@ export interface AtomOptions<Value, Diff> {
* ```ts
* const name = atom('name', 'John')
*
* console.log(name.value) // 'John'
* print(name.value) // 'John'
* ```
*
* @public

Wyświetl plik

@ -26,7 +26,7 @@ type UNINITIALIZED = typeof UNINITIALIZED
* const count = atom('count', 0)
* const double = computed('double', (prevValue) => {
* if (isUninitialized(prevValue)) {
* console.log('First time!')
* print('First time!')
* }
* return count.value * 2
* })

Wyświetl plik

@ -354,7 +354,6 @@ const NUM_OPS_PER_TEST = 1000
function runTest(seed: number) {
const test = new Test(seed)
// console.log(test.systemState)
for (let i = 0; i < NUM_OPS_PER_TEST; i++) {
test.tick()
const { expected, actual } = test.getResultComparisons()

Wyświetl plik

@ -39,7 +39,7 @@ let stack: CaptureStackFrame | null = null
* })
*
* react('log name changes', () => {
* console.log(name.value, 'was changed at', unsafe__withoutCapture(() => time.value))
* print(name.value, 'was changed at', unsafe__withoutCapture(() => time.value))
* })
*
* ```
@ -134,7 +134,7 @@ export function maybeCaptureParent(p: Signal<any, any>) {
* const name = atom('name', 'Bob')
* react('greeting', () => {
* whyAmIRunning()
* console.log('Hello', name.value)
* print('Hello', name.value)
* })
*
* name.set('Alice')

Wyświetl plik

@ -143,7 +143,7 @@ export let currentTransaction = null as Transaction | null
* const lastName = atom('Doe')
*
* react('greet', () => {
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
* print(`Hello, ${firstName.value} ${lastName.value}!`)
* })
*
* // Logs "Hello, John Doe!"
@ -164,7 +164,7 @@ export let currentTransaction = null as Transaction | null
* const lastName = atom('Doe')
*
* react('greet', () => {
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
* print(`Hello, ${firstName.value} ${lastName.value}!`)
* })
*
* // Logs "Hello, John Doe!"
@ -187,7 +187,7 @@ export let currentTransaction = null as Transaction | null
* const lastName = atom('Doe')
*
* react('greet', () => {
* console.log(`Hello, ${firstName.value} ${lastName.value}!`)
* print(`Hello, ${firstName.value} ${lastName.value}!`)
* })
*
* // Logs "Hello, John Doe!"

Wyświetl plik

@ -6,7 +6,6 @@
/// <reference types="react" />
import { Arc2d } from '@tldraw/editor';
import { ArrayOfValidator } from '@tldraw/editor';
import { BaseBoxShapeTool } from '@tldraw/editor';
import { BaseBoxShapeUtil } from '@tldraw/editor';
@ -14,7 +13,6 @@ import { Box2d } from '@tldraw/editor';
import { Circle2d } from '@tldraw/editor';
import { CubicSpline2d } from '@tldraw/editor';
import { DictValidator } from '@tldraw/editor';
import { Edge2d } from '@tldraw/editor';
import { Editor } from '@tldraw/editor';
import { EMBED_DEFINITIONS } from '@tldraw/editor';
import { EmbedDefinition } from '@tldraw/editor';
@ -121,7 +119,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented)
getDefaultProps(): TLArrowShape['props'];
// (undocumented)
getGeometry(shape: TLArrowShape): Arc2d | Edge2d | Group2d;
getGeometry(shape: TLArrowShape): Group2d;
// (undocumented)
getHandles(shape: TLArrowShape): TLHandle[];
// (undocumented)

Wyświetl plik

@ -412,10 +412,23 @@ describe('reparenting issue', () => {
editor.expectPathToBe('root.select.pointing_handle')
editor.pointerMove(320, 320) // over box 2
editor.expectShapeToMatch({ id: arrowId, index: 'a3V' }) // between box 2 (a3) and 3 (a4)
editor.expectPathToBe('root.select.dragging_handle')
editor.expectShapeToMatch({
id: arrowId,
index: 'a3V',
props: { end: { boundShapeId: ids.box2 } },
}) // between box 2 (a3) and 3 (a4)
editor.pointerMove(350, 350) // over box 3
editor.expectShapeToMatch({ id: arrowId, index: 'a5' }) // above box 3 (a4)
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, { hitInside: true })).toMatchObject({
id: ids.box3,
})
editor.pointerMove(350, 350) // over box 3 and box 2, but box 3 is smaller
editor.expectShapeToMatch({
id: arrowId,
index: 'a5',
props: { end: { boundShapeId: ids.box3 } },
}) // above box 3 (a4)
editor.pointerMove(150, 150) // over box 1
editor.expectShapeToMatch({ id: arrowId, index: 'a2V' }) // between box 1 (a2) and box 3 (a3)

Wyświetl plik

@ -101,67 +101,66 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
largeArcFlag: info.bodyArc.largeArcFlag,
})
if (!shape.props.text.trim()) {
return bodyGeom
let labelGeom: Rectangle2d | undefined
if (shape.props.text.trim()) {
const bodyBounds = bodyGeom.bounds
const { w, h } = this.editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: 'fit-content',
})
let width = w
let height = h
if (bodyBounds.width > bodyBounds.height) {
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w))
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px',
}
)
width = squishedWidth
height = squishedHeight
}
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px',
}
)
width = squishedWidth
height = squishedHeight
}
labelGeom = new Rectangle2d({
x: info.middle.x - width / 2 - 4.25,
y: info.middle.y - height / 2 - 4.25,
width: width + 8.5,
height: height + 8.5,
isFilled: true,
})
}
const bodyBounds = bodyGeom.bounds
const { w, h } = this.editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: 'fit-content',
})
let width = w
let height = h
if (bodyBounds.width > bodyBounds.height) {
width = Math.max(Math.min(w, 64), Math.min(bodyBounds.width - 64, w))
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px',
}
)
width = squishedWidth
height = squishedHeight
}
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
const { w: squishedWidth, h: squishedHeight } = this.editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
width: width + 'px',
}
)
width = squishedWidth
height = squishedHeight
}
const labelGeom = new Rectangle2d({
x: info.middle.x - width / 2 - 4.25,
y: info.middle.y - height / 2 - 4.25,
width: width + 8.5,
height: height + 8.5,
isFilled: true,
})
return new Group2d({
children: [bodyGeom, labelGeom],
operation: 'union',
children: labelGeom ? [bodyGeom, labelGeom] : [bodyGeom],
isSnappable: false,
})
}

Wyświetl plik

@ -25,7 +25,6 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
handleChange,
isEmpty,
handleInputPointerDown,
handleContentPointerDown,
} = useEditableText(id, 'arrow', text)
if (!isEditing && isEmpty) {
@ -48,7 +47,7 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
}}
>
<div className="tl-arrow-label__inner">
<p style={{ width: width ? width : '9px' }} onPointerDown={handleContentPointerDown}>
<p style={{ width: width ? width : '9px' }}>
{text ? TextHelpers.normalizeTextForDom(text) : ' '}
</p>
{isEditing && (

Wyświetl plik

@ -14,4 +14,23 @@ export class Idle extends StateNode {
override onCancel = () => {
this.editor.setCurrentTool('select')
}
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') {
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (
onlySelectedShape &&
this.editor.getShapeUtil(onlySelectedShape).canEdit(onlySelectedShape)
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.current.value!.transition('editing_shape', {
...info,
target: 'shape',
shape: onlySelectedShape,
})
}
}
}
}

Wyświetl plik

@ -318,28 +318,35 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
}
}
// const labelSize = getLabelSize(this.editor, shape)
// const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(32, Math.max(1, w - 8))))
// const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(32, Math.max(1, w - 8))))
const labelSize = getLabelSize(this.editor, shape)
const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(32, Math.max(1, w - 8))))
const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(32, Math.max(1, w - 8))))
const lines = getLines(shape.props, strokeWidth)
const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []
return new Group2d({
children: [
body,
// new Rectangle2d({
// x: w / 2 - labelWidth / 2,
// y: h / 2 - labelHeight / 2,
// width: labelWidth,
// height: labelHeight,
// isFilled: true,
// isSnappable: false,
// margin: 12,
// }),
new Rectangle2d({
x:
shape.props.align === 'start'
? 0
: shape.props.align === 'end'
? w - labelWidth
: (w - labelWidth) / 2,
y:
shape.props.verticalAlign === 'start'
? 0
: shape.props.verticalAlign === 'end'
? h - labelHeight
: (h - labelHeight) / 2,
width: labelWidth,
height: labelHeight,
isFilled: true,
isSnappable: false,
}),
...edges,
],
operation: 'union',
isSnappable: false,
})
}

Wyświetl plik

@ -1,4 +1,4 @@
import { StateNode, TLEventHandlers, TLGeoShape } from '@tldraw/editor'
import { StateNode, TLEventHandlers } from '@tldraw/editor'
export class Idle extends StateNode {
static override id = 'idle'
@ -13,15 +13,18 @@ export class Idle extends StateNode {
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') {
const shape = this.editor.onlySelectedShape
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
this.editor.mark('editing shape')
this.editor.setEditingShape(shape.id)
this.editor.setCurrentTool('select.editing_shape', {
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (
onlySelectedShape &&
this.editor.getShapeUtil(onlySelectedShape).canEdit(onlySelectedShape)
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.current.value!.transition('editing_shape', {
...info,
target: 'shape',
shape,
shape: onlySelectedShape,
})
}
}

Wyświetl plik

@ -49,7 +49,6 @@ export const TextLabel = React.memo(function TextLabel<
handleKeyDown,
handleBlur,
handleInputPointerDown,
handleContentPointerDown,
handleDoubleClick,
} = useEditableText(id, type, text)
@ -84,7 +83,7 @@ export const TextLabel = React.memo(function TextLabel<
color: theme[labelColor].solid,
}}
>
<div className="tl-text tl-text-content" dir="ltr" onPointerDown={handleContentPointerDown}>
<div className="tl-text tl-text-content" dir="ltr">
{finalText}
</div>
{isInteractive && (

Wyświetl plik

@ -27,32 +27,22 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
id,
])
const isInEditingShapePath = useValue(
'isInEditingShapePath',
() => {
return editor.isIn('select.editing_shape')
},
[editor]
)
const rSkipSelectOnFocus = useRef(false)
const rSelectionRanges = useRef<Range[] | null>()
const isEditableFromHover = useValue(
'is editable hovering',
() => {
const { hoveredShapeId, editingShapeId } = editor
const { hoveredShapeId, editingShape } = editor
if (type === 'text' && editor.isIn('text') && hoveredShapeId === id) {
return true
}
if (isInEditingShapePath) {
if (!editingShapeId) return false
const editingShape = editor.getShape(editingShapeId)
if (!editingShape) return false
if (editingShape) {
return (
(hoveredShapeId === id || editingShapeId !== id) &&
// We must be hovering over this shape and not editing it
hoveredShapeId === id &&
editingShape.id !== id &&
// the editing shape must be the same type as this shape
editingShape.type === type &&
// and this shape must be capable of being editing in its current form
@ -62,7 +52,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
return false
},
[type, id, isInEditingShapePath]
[type, id]
)
// When the label receives focus, set the value to the most
@ -98,7 +88,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
requestAnimationFrame(() => {
const elm = rInput.current
// Did we move to a different shape?
if (elm && editor.isIn('select.editing_shape')) {
if (elm && editor.editingShapeId) {
// important! these ^v are two different things
// is that shape OUR shape?
if (editor.editingShapeId === id) {
@ -213,7 +203,15 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
transact(() => {
editor.setEditingShape(id)
editor.setHoveredShape(id)
editor.setSelectedShapeIds([id])
editor.setSelectedShapes([id])
})
} else {
editor.dispatch({
...getPointerInfo(e),
type: 'pointer',
name: 'pointer_down',
target: 'shape',
shape: editor.getShape(id)!,
})
}
@ -222,21 +220,6 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
[editor, isEditableFromHover, id]
)
const handleContentPointerDown = useCallback(
(e: React.PointerEvent) => {
editor.dispatch({
...getPointerInfo(e),
type: 'pointer',
name: 'pointer_down',
target: 'shape',
shape: editor.getShape(id)!,
})
stopEventPropagation(e)
},
[editor, id]
)
useEffect(() => {
const elm = rInput.current
if (elm && isEditing && document.activeElement !== elm) {
@ -255,7 +238,6 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
handleKeyDown,
handleChange,
handleInputPointerDown,
handleContentPointerDown,
handleDoubleClick,
isEmpty,
}

Wyświetl plik

@ -83,7 +83,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
handleKeyDown,
handleBlur,
handleInputPointerDown,
handleContentPointerDown,
} = useEditableText(id, type, text)
return (
@ -105,11 +104,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
color: theme[color].solid,
}}
>
<div
className="tl-text tl-text-content"
dir="ltr"
onPointerDown={handleContentPointerDown}
>
<div className="tl-text tl-text-content" dir="ltr">
{text}
</div>
{isEditing || isEditableFromHover ? (

Wyświetl plik

@ -1,4 +1,4 @@
import { StateNode, TLEventHandlers, TLGeoShape, TLGroupShape, TLTextShape } from '@tldraw/editor'
import { StateNode, TLEventHandlers, TLGroupShape, TLTextShape } from '@tldraw/editor'
import { updateHoveredId } from '../../../tools/selection-logic/updateHoveredId'
export class Idle extends StateNode {
@ -22,7 +22,7 @@ export class Idle extends StateNode {
if (hitShape) {
if (this.editor.isShapeOfType<TLTextShape>(hitShape, 'text')) {
requestAnimationFrame(() => {
this.editor.setSelectedShapeIds([hitShape.id])
this.editor.setSelectedShapes([hitShape.id])
this.editor.setEditingShape(hitShape.id)
this.editor.setCurrentTool('select.editing_shape', {
...info,
@ -43,14 +43,18 @@ export class Idle extends StateNode {
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
if (info.key === 'Enter') {
const shape = this.editor.selectedShapes[0]
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (
onlySelectedShape &&
this.editor.getShapeUtil(onlySelectedShape).canEdit(onlySelectedShape)
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(shape.id)
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.current.value!.transition('editing_shape', {
...info,
target: 'shape',
shape,
shape: onlySelectedShape,
})
}
}

Wyświetl plik

@ -27,16 +27,19 @@ export class Erasing extends StateNode {
const { originPagePoint } = this.editor.inputs
this.excludedShapeIds = new Set(
this.editor.currentPageShapes
.filter(
(shape) =>
.filter((shape) => {
if (
this.editor.isShapeOrAncestorLocked(shape) ||
((this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')) &&
this.editor.isPointInShape(shape, originPagePoint, {
hitInside: true,
margin: 0,
}))
)
this.editor.isShapeOfType<TLGroupShape>(shape, 'group') ||
this.editor.isShapeOfType<TLFrameShape>(shape, 'frame')
) {
const pointInShapeShape = this.editor.getPointInShapeSpace(shape, originPagePoint)
const geometry = this.editor.getShapeGeometry(shape)
return geometry.bounds.containsPoint(pointInShapeShape)
}
return false
})
.map((shape) => shape.id)
)

Wyświetl plik

@ -72,7 +72,7 @@ export class Brushing extends StateNode {
}
override onCancel?: TLCancelEvent | undefined = (info) => {
this.editor.setSelectedShapeIds(this.initialSelectedShapeIds, { squashing: true })
this.editor.setSelectedShapes(this.initialSelectedShapeIds, { squashing: true })
this.parent.transition('idle', info)
}
@ -168,7 +168,7 @@ export class Brushing extends StateNode {
}
this.editor.updateInstanceState({ brush: { ...this.brush.toJson() } })
this.editor.setSelectedShapeIds(Array.from(results), { squashing: true })
this.editor.setSelectedShapes(Array.from(results), { squashing: true })
}
override onInterrupt: TLInterruptEvent = () => {

Wyświetl plik

@ -72,7 +72,7 @@ export class Idle extends StateNode {
} else {
if (this.editor.getShapeUtil(info.shape)?.canCrop(info.shape)) {
this.editor.setCroppingShape(info.shape.id)
this.editor.setSelectedShapeIds([info.shape.id])
this.editor.setSelectedShapes([info.shape.id])
this.editor.setCurrentTool('select.crop.pointing_crop', info)
} else {
this.cancel()

Wyświetl plik

@ -1,20 +1,11 @@
import { StateNode, TLEventHandlers } from '@tldraw/editor'
import { StateNode, TLArrowShape, TLEventHandlers, TLGeoShape } from '@tldraw/editor'
import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShapeOnCanvasPointerDown'
import { updateHoveredId } from '../../selection-logic/updateHoveredId'
export class EditingShape extends StateNode {
static override id = 'editing_shape'
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
switch (info.target) {
case 'shape':
case 'canvas': {
updateHoveredId(this.editor)
}
}
}
override onExit = () => {
if (!this.editor.currentPageState.editingShapeId) return
const { editingShapeId } = this.editor.currentPageState
if (!editingShapeId) return
@ -28,7 +19,60 @@ export class EditingShape extends StateNode {
util.onEditEnd?.(shape)
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
switch (info.target) {
case 'shape':
case 'canvas': {
updateHoveredId(this.editor)
return
}
}
}
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
// This is pretty tricky.
// Most of the time, we wouldn't want pointer events inside of an editing
// shape to de-select the shape or change the editing state. We would just
// ignore those pointer events.
// The exception to this is shapes that have only parts of themselves that are
// editable, such as the label on a geo shape. In this case, we would want clicks
// that are outside of the label but inside of the shape to end the editing session
// and select the shape instead.
// What we'll do here (for now at least) is have the text label / input element
// have a pointer event handler (in useEditableText) that dispatches its own
// "shape" type event, which lets us know to ignore the event. If we instead get
// a "canvas" type event, then we'll check to see if the hovered shape is a geo
// shape and if so, we'll end the editing session and select the shape.
switch (info.target) {
case 'shape': {
if (info.shape.id === this.editor.editingShapeId) {
return
}
break
}
case 'canvas': {
const hitShape = getHitShapeOnCanvasPointerDown(this.editor)
if (
hitShape &&
!(
this.editor.isShapeOfType<TLGeoShape>(hitShape, 'geo') ||
this.editor.isShapeOfType<TLArrowShape>(hitShape, 'arrow')
)
) {
this.onPointerDown({
...info,
shape: hitShape,
target: 'shape',
})
return
}
}
}
this.parent.transition('idle', info)
this.parent.current.value?.onPointerDown?.(info)
}

Wyświetl plik

@ -159,13 +159,21 @@ export class Idle extends StateNode {
switch (info.target) {
case 'canvas': {
const { hoveredShape } = this.editor
// todo
// double clicking on the middle of a hollow geo shape without a label, or
// over the label of a hollwo shape that has a label, should start editing
// that shape's label. We can't support "double click anywhere inside"
// of the shape yet because that also creates text shapes, and can product
// unexpected results when working "inside of" a hollow shape.
const hitShape =
hoveredShape && !this.editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
? hoveredShape
: this.editor.getSelectedShapeAtPoint(this.editor.inputs.currentPagePoint) ??
this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, {
margin: HIT_TEST_MARGIN / this.editor.zoomLevel,
hitInside: true,
hitInside: false,
})
const { focusedGroupId } = this.editor
@ -337,7 +345,7 @@ export class Idle extends StateNode {
if (!selectedShapeIds.includes(targetShape.id)) {
this.editor.mark('selecting shape')
this.editor.setSelectedShapeIds([targetShape.id])
this.editor.setSelectedShapes([targetShape.id])
}
break
}
@ -405,7 +413,7 @@ export class Idle extends StateNode {
if (
selectedShapes.every((shape) => this.editor.isShapeOfType<TLGroupShape>(shape, 'group'))
) {
this.editor.setSelectedShapeIds(
this.editor.setSelectedShapes(
selectedShapes.flatMap((shape) => this.editor.getSortedChildIdsForParent(shape.id))
)
return

Wyświetl plik

@ -1,7 +1,10 @@
import {
Group2d,
HIT_TEST_MARGIN,
StateNode,
TLArrowShape,
TLEventHandlers,
TLGeoShape,
TLPointerEventInfo,
TLShape,
} from '@tldraw/editor'
@ -48,17 +51,15 @@ export class PointingShape extends StateNode {
this.editor.cancelDoubleClick()
if (!selectedShapeIds.includes(outermostSelectingShape.id)) {
this.editor.mark('shift selecting shape')
this.editor.setSelectedShapeIds([...selectedShapeIds, outermostSelectingShape.id])
this.editor.setSelectedShapes([...selectedShapeIds, outermostSelectingShape.id])
}
} else {
this.editor.mark('selecting shape')
this.editor.setSelectedShapeIds([outermostSelectingShape.id])
this.editor.setSelectedShapes([outermostSelectingShape.id])
}
}
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
// const { shape } = info
const {
zoomLevel,
focusedGroupId,
@ -92,7 +93,7 @@ export class PointingShape extends StateNode {
if (selectingShape.id === focusedGroupId) {
if (selectedShapeIds.length > 0) {
this.editor.mark('clearing shape ids')
this.editor.setSelectedShapeIds([])
this.editor.setSelectedShapes([])
} else {
this.editor.popFocusedGroupId()
}
@ -119,8 +120,41 @@ export class PointingShape extends StateNode {
this.editor.mark('deselecting on pointer up')
this.editor.deselect(selectingShape)
} else {
this.editor.mark('selecting on pointer up')
this.editor.select(selectingShape)
if (selectedShapeIds.includes(selectingShape.id)) {
// todo
// if the shape is editable and we're inside of an editable part of that shape, e.g. the label of a geo shape,
// then we would want to begin editing the shape. At the moment we're relying on the shape label's onPointerUp
// handler to do this logic, and prevent the regular pointer up event, so we won't be here in that case.
// ! tldraw hack
// if the shape is a geo shape, and we're inside of the label, then we want to begin editing the label
if (
this.editor.isShapeOfType<TLGeoShape>(selectingShape, 'geo') ||
this.editor.isShapeOfType<TLArrowShape>(selectingShape, 'arrow')
) {
const geometry = this.editor.getShapeGeometry(selectingShape)
const labelGeometry = (geometry as Group2d).children[1]
if (labelGeometry) {
const pointInShapeSpace = this.editor.getPointInShapeSpace(
selectingShape,
currentPagePoint
)
if (labelGeometry.hitTestPoint(pointInShapeSpace)) {
this.editor.batch(() => {
this.editor.mark('editing on pointer up')
this.editor.select(selectingShape.id)
this.editor.setCurrentTool('select.editing_shape')
this.editor.setEditingShape(selectingShape.id)
})
return
}
}
}
} else {
this.editor.mark('selecting on pointer up')
this.editor.select(selectingShape)
}
}
} else if (shiftKey) {
// Different shape, so we are drilling down into a group with shift key held.
@ -128,14 +162,14 @@ export class PointingShape extends StateNode {
const ancestors = this.editor.getShapeAncestors(outermostSelectableShape)
this.editor.mark('shift deselecting on pointer up')
this.editor.setSelectedShapeIds([
this.editor.setSelectedShapes([
...this.editor.selectedShapeIds.filter((id) => !ancestors.find((a) => a.id === id)),
outermostSelectableShape.id,
])
} else {
this.editor.mark('selecting on pointer up')
// different shape and we are drilling down, but no shift held so just select it straight up
this.editor.setSelectedShapeIds([outermostSelectableShape.id])
this.editor.setSelectedShapes([outermostSelectableShape.id])
}
}

Wyświetl plik

@ -162,7 +162,7 @@ export class ScribbleBrushing extends StateNode {
}
}
this.editor.setSelectedShapeIds(
this.editor.setSelectedShapes(
[
...new Set<TLShapeId>(
shiftKey
@ -179,7 +179,7 @@ export class ScribbleBrushing extends StateNode {
}
private cancel() {
this.editor.setSelectedShapeIds([...this.initialSelectedShapeIds], { squashing: true })
this.editor.setSelectedShapes([...this.initialSelectedShapeIds], { squashing: true })
this.parent.transition('idle', {})
}
}

Wyświetl plik

@ -10,6 +10,7 @@ export function getHitShapeOnCanvasPointerDown(editor: Editor): TLShape | undefi
// hovered shape at point
editor.getShapeAtPoint(currentPagePoint, {
hitInside: false,
hitLabels: false,
margin: HIT_TEST_MARGIN / zoomLevel,
}) ??
// selected shape at point

Wyświetl plik

@ -5,8 +5,9 @@ export function selectOnCanvasPointerUp(editor: Editor) {
const { shiftKey, altKey, currentPagePoint } = editor.inputs
const hitShape = editor.getShapeAtPoint(currentPagePoint, {
hitInside: true,
hitInside: false,
margin: HIT_TEST_MARGIN / editor.zoomLevel,
hitLabels: true,
})
// Note at the start: if we select a shape that is inside of a group,
@ -31,7 +32,7 @@ export function selectOnCanvasPointerUp(editor: Editor) {
} else {
// Add it to selected shapes
editor.mark('shift selecting shape')
editor.setSelectedShapeIds([...selectedShapeIds, outermostSelectableShape.id])
editor.setSelectedShapes([...selectedShapeIds, outermostSelectableShape.id])
}
} else {
let shapeToSelect: TLShape | undefined = undefined

Wyświetl plik

@ -1,39 +0,0 @@
import { Editor, HIT_TEST_MARGIN, TLGroupShape } from '@tldraw/editor'
import { selectOnCanvasPointerUp } from './selectOnCanvasPointerUp'
export function selectOnDoubleClick(editor: Editor) {
const { hoveredShape } = editor
const hitShape =
hoveredShape && !editor.isShapeOfType<TLGroupShape>(hoveredShape, 'group')
? hoveredShape
: editor.getSelectedShapeAtPoint(editor.inputs.currentPagePoint) ??
editor.getShapeAtPoint(editor.inputs.currentPagePoint, {
margin: HIT_TEST_MARGIN / editor.zoomLevel,
hitInside: true,
})
const { focusedGroupId } = editor
if (hitShape) {
if (editor.isShapeOfType<TLGroupShape>(hitShape, 'group')) {
// Probably select the shape
selectOnCanvasPointerUp(editor)
return
} else {
const parent = editor.getShape(hitShape.parentId)
if (parent && editor.isShapeOfType<TLGroupShape>(parent, 'group')) {
// The shape is the direct child of a group. If the group is
// selected, then we can select the shape. If the group is the
// focus layer id, then we can double click into it as usual.
if (focusedGroupId && parent.id === focusedGroupId) {
// noop, double click on the shape as normal below
} else {
// The shape is the child of some group other than our current
// focus layer. We should probably select the group instead.
selectOnCanvasPointerUp(editor)
return
}
}
}
}
}

Wyświetl plik

@ -30,7 +30,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
const { onlySelectedShape } = editor
if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) {
editor.setSelectedShapeIds([])
editor.setSelectedShapes([])
}
} else {
// Weird route: selecting locked shapes on long press

Wyświetl plik

@ -36,7 +36,7 @@ function createNShapes(editor: Editor, n: number) {
}
editor.batch(() => {
editor.createShapes(shapesToCreate).setSelectedShapeIds(shapesToCreate.map((s) => s.id))
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
})
}

Wyświetl plik

@ -361,7 +361,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
}
})
)
editor.setSelectedShapeIds(rootShapeIds)
editor.setSelectedShapes(rootShapeIds)
}
/* --------------- Translating Helpers --------_------ */

Wyświetl plik

@ -22,6 +22,18 @@ export function useSideEffects() {
}
}
}
if (prev.editingShapeId !== next.editingShapeId) {
if (!prev.editingShapeId && next.editingShapeId) {
if (!editor.isIn('select.editing_shape')) {
editor.setCurrentTool('select.editing_shape')
}
} else if (prev.editingShapeId && !next.editingShapeId) {
if (editor.isIn('select.editing_shape')) {
editor.setCurrentTool('select.idle')
}
}
}
})
}, [editor])
}

Wyświetl plik

@ -110,13 +110,13 @@ describe('shapes that are moved to another page', () => {
describe("should be excluded from the previous page's selectedShapeIds", () => {
test('[boxes]', () => {
editor.setSelectedShapeIds([ids.box1, ids.box2, ids.box3])
editor.setSelectedShapes([ids.box1, ids.box2, ids.box3])
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.box2, ids.box3])
moveShapesToPage2()
expect(editor.selectedShapeIds).toEqual([])
})
test('[frame that does not move]', () => {
editor.setSelectedShapeIds([ids.frame1])
editor.setSelectedShapes([ids.frame1])
expect(editor.selectedShapeIds).toEqual([ids.frame1])
moveShapesToPage2()
expect(editor.selectedShapeIds).toEqual([ids.frame1])
@ -156,7 +156,7 @@ describe('Editor.sharedOpacity', () => {
it('should return opacity for a single selected shape', () => {
const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
editor.setSelectedShapeIds([A])
editor.setSelectedShapes([A])
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
@ -165,7 +165,7 @@ describe('Editor.sharedOpacity', () => {
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.3} x={0} y={0} />,
])
editor.setSelectedShapeIds([A, B])
editor.setSelectedShapes([A, B])
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
@ -174,7 +174,7 @@ describe('Editor.sharedOpacity', () => {
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.5} x={0} y={0} />,
])
editor.setSelectedShapeIds([A, B])
editor.setSelectedShapes([A, B])
expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
})
@ -184,7 +184,7 @@ describe('Editor.sharedOpacity', () => {
<TL.geo ref="A" opacity={0.3} x={0} y={0} />
</TL.group>,
])
editor.setSelectedShapeIds([ids.group])
editor.setSelectedShapes([ids.group])
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
})
@ -196,7 +196,7 @@ describe('Editor.setOpacity', () => {
<TL.geo ref="B" opacity={0.4} x={0} y={0} />,
])
editor.setSelectedShapeIds([ids.A, ids.B])
editor.setSelectedShapes([ids.A, ids.B])
editor.setOpacity(0.5)
expect(editor.getShape(ids.A)!.opacity).toBe(0.5)
@ -215,7 +215,7 @@ describe('Editor.setOpacity', () => {
</TL.group>,
])
editor.setSelectedShapeIds([ids.groupA])
editor.setSelectedShapes([ids.groupA])
editor.setOpacity(0.5)
// a wasn't selected...

Wyświetl plik

@ -370,7 +370,8 @@ describe('When clicking and dragging', () => {
it('Only erases masked shapes when pointer is inside the mask', () => {
editor.setCurrentTool('eraser')
editor.pointerDown(425, 0) // Above the masked part of box3
editor.pointerMove(425, 0)
editor.pointerDown() // Above the masked part of box3
expect(editor.erasingShapeIds).toEqual([])
editor.pointerMove(425, 500) // Through the masked part of box3
jest.advanceTimersByTime(16)
@ -379,7 +380,8 @@ describe('When clicking and dragging', () => {
editor.pointerUp()
expect(editor.getShape(ids.box3)).toBeDefined()
editor.pointerDown(375, 0) // Above the not-masked part of box3
editor.pointerMove(375, 0)
editor.pointerDown() // Above the not-masked part of box3
editor.pointerMove(375, 500) // Through the masked part of box3
expect(editor.instanceState.scribble).not.toBe(null)
expect(editor.erasingShapeIds).toEqual([ids.box3])

Wyświetl plik

@ -435,7 +435,7 @@ describe('When in readonly mode', () => {
expect(editor.selectedShapeIds.length).toBe(0)
expect(editor.instanceState.isReadonly).toBe(true)
editor.setSelectedShapeIds([ids.embed1])
editor.setSelectedShapes([ids.embed1])
expect(editor.selectedShapeIds.length).toBe(1)
editor.keyUp('Enter')

Wyświetl plik

@ -526,3 +526,7 @@ describe('When starting an arrow inside of multiple shapes', () => {
})
})
})
it.todo(
'after creating an arrow while tool lock is enabled, pressing enter will begin editing that shape'
)

Wyświetl plik

@ -77,8 +77,6 @@ describe('restoring bound arrows multiplayer', () => {
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
// console.log(JSON.stringify(editor.history._undos.value.toArray(), null, 2))
expect(arrow().props.start.type).toBe('point')
expect(arrow().props.end.type).toBe('binding')

Wyświetl plik

@ -20,7 +20,7 @@ beforeEach(() => {
describe('when less than two shapes are selected', () => {
it('does nothing', () => {
editor.setSelectedShapeIds([ids.boxB])
editor.setSelectedShapes([ids.boxB])
const fn = jest.fn()
editor.on('update', fn)
@ -206,7 +206,7 @@ describe('when multiple shapes are selected', () => {
},
])
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC])
editor.alignShapes(editor.selectedShapeIds, 'bottom')
jest.advanceTimersByTime(1000)
editor.alignShapes(editor.selectedShapeIds, 'right')
@ -243,7 +243,7 @@ describe('When shapes are parented to other shapes...', () => {
})
it('Aligns to the top left.', () => {
editor.setSelectedShapeIds([ids.boxC, ids.boxB])
editor.setSelectedShapes([ids.boxC, ids.boxB])
const commonBoundsBefore = Box2d.Common([
editor.getShapePageBounds(ids.boxC)!,
@ -265,7 +265,7 @@ describe('When shapes are parented to other shapes...', () => {
})
it('Aligns to the bottom right.', () => {
editor.setSelectedShapeIds([ids.boxC, ids.boxB])
editor.setSelectedShapes([ids.boxC, ids.boxB])
const commonBoundsBefore = Box2d.Common([
editor.getShapePageBounds(ids.boxC)!,
@ -331,7 +331,7 @@ describe('When shapes are parented to a rotated shape...', () => {
})
it('Aligns to the top left.', () => {
editor.setSelectedShapeIds([ids.boxC, ids.boxB])
editor.setSelectedShapes([ids.boxC, ids.boxB])
const commonBoundsBefore = Box2d.Common([
editor.getShapePageBounds(ids.boxC)!,
@ -359,7 +359,7 @@ describe('When shapes are parented to a rotated shape...', () => {
})
it('Aligns to the bottom right.', () => {
editor.setSelectedShapeIds([ids.boxC, ids.boxB])
editor.setSelectedShapes([ids.boxC, ids.boxB])
const commonBoundsBefore = Box2d.Common([
editor.getShapePageBounds(ids.boxC)!,

Wyświetl plik

@ -458,7 +458,7 @@ describe('When copying and pasting', () => {
])
// Copy a shape from within the group
.selectNone()
.setSelectedShapeIds([ids.box1])
.setSelectedShapes([ids.box1])
.copy()
await assertClipboardOfCorrectShape(mockClipboard.current)

Wyświetl plik

@ -44,7 +44,7 @@ describe('distributeShapes command', () => {
describe('when less than three shapes are selected', () => {
it('does nothing', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB])
editor.setSelectedShapes([ids.boxA, ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
@ -131,7 +131,7 @@ describe('distributeShapes command', () => {
y: 100,
},
])
editor.setSelectedShapeIds([ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
jest.advanceTimersByTime(1000)
@ -173,7 +173,7 @@ describe('distributeShapes command', () => {
y: 200,
},
])
editor.setSelectedShapeIds([ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
jest.advanceTimersByTime(1000)
@ -218,7 +218,7 @@ describe('distributeShapes command', () => {
},
])
editor.setSelectedShapeIds([ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
jest.advanceTimersByTime(1000)

Wyświetl plik

@ -82,7 +82,7 @@ beforeEach(() => {
describe('Locking', () => {
it('Can lock shapes', () => {
editor.setSelectedShapeIds([ids.unlockedShapeA])
editor.setSelectedShapes([ids.unlockedShapeA])
editor.toggleLock(editor.selectedShapeIds)
expect(editor.getShape(ids.unlockedShapeA)!.isLocked).toBe(true)
// Locking deselects the shape
@ -164,7 +164,7 @@ describe('Locked shapes', () => {
describe('Unlocking', () => {
it('Can unlock shapes', () => {
editor.setSelectedShapeIds([ids.lockedShapeA, ids.lockedShapeB])
editor.setSelectedShapes([ids.lockedShapeA, ids.lockedShapeB])
let lockedStatus = [ids.lockedShapeA, ids.lockedShapeB].map(
(id) => editor.getShape(id)!.isLocked
)

Wyświetl plik

@ -70,7 +70,7 @@ function getShape(ids: TLShapeId[]) {
describe('When a shape is selected...', () => {
it('nudges and undoes', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
editor.keyDown('ArrowUp')
expect(editor.selectionPageBounds).toMatchObject({ x: 10, y: 9 })
@ -84,7 +84,7 @@ describe('When a shape is selected...', () => {
})
it('nudges and holds', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
editor.keyDown('ArrowUp')
editor.keyRepeat('ArrowUp')
@ -105,7 +105,7 @@ describe('When a shape is selected...', () => {
})
it('nudges a shape correctly', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
editor.keyDown('ArrowUp')
expect(editor.selectionPageBounds).toMatchObject({ x: 10, y: 9 })
@ -140,7 +140,7 @@ describe('When a shape is rotated...', () => {
// editor.updateShapes([{ id: ids.boxB, type: 'geo', x: 10, y: 10 }])
// Here's the selection page bounds and shape before we nudge it
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(editor.selectionPageBounds).toCloselyMatchObject({ x: 10, y: 10, w: 100, h: 100 })
expect(editor.getShape(ids.boxA)).toCloselyMatchObject({ x: 10, y: -10 })
@ -155,7 +155,7 @@ describe('When a shape is rotated...', () => {
describe('When a shape is selected...', () => {
it('nudges a shape correctly', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(nudgeAndGet([ids.boxA], 'ArrowUp', false)).toMatchObject([{ x: 10, y: 9 }])
expect(nudgeAndGet([ids.boxA], 'ArrowRight', false)).toMatchObject([{ x: 11, y: 9 }])
@ -164,7 +164,7 @@ describe('When a shape is selected...', () => {
})
it('nudges a shape with shift key pressed', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(nudgeAndGet([ids.boxA], 'ArrowUp', true)).toMatchObject([{ x: 10, y: 0 }])
expect(nudgeAndGet([ids.boxA], 'ArrowRight', true)).toMatchObject([{ x: 20, y: 0 }])
@ -178,7 +178,7 @@ describe('When a shape is selected...', () => {
describe('When grid is enabled...', () => {
it('nudges a shape correctly', () => {
editor.updateInstanceState({ isGridMode: true })
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(nudgeAndGet([ids.boxA], 'ArrowUp', false)).toMatchObject([{ x: 10, y: 0 }])
expect(nudgeAndGet([ids.boxA], 'ArrowRight', false)).toMatchObject([{ x: 20, y: 0 }])
@ -188,7 +188,7 @@ describe('When grid is enabled...', () => {
it('nudges a shape with shift key pressed', () => {
editor.updateInstanceState({ isGridMode: true })
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(nudgeAndGet([ids.boxA], 'ArrowUp', true)).toMatchObject([{ x: 10, y: -40 }])
expect(nudgeAndGet([ids.boxA], 'ArrowRight', true)).toMatchObject([{ x: 60, y: -40 }])
@ -199,7 +199,7 @@ describe('When grid is enabled...', () => {
describe('When multiple shapes are selected...', () => {
it('Nudges all shapes correctly', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB])
editor.setSelectedShapes([ids.boxA, ids.boxB])
expect(nudgeAndGet([ids.boxA, ids.boxB], 'ArrowUp', false)).toMatchObject([
{ x: 10, y: 9 },
@ -222,7 +222,7 @@ describe('When multiple shapes are selected...', () => {
describe('When undo redo is on...', () => {
it('Does not nudge any shapes', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
expect(nudgeAndGet([ids.boxA], 'ArrowUp', false)).toMatchObject([{ x: 10, y: 9 }])
editor.undo()
@ -240,7 +240,7 @@ describe('When undo redo is on...', () => {
describe('When nudging a rotated shape...', () => {
it('Moves the page point correctly', () => {
editor.setSelectedShapeIds([ids.boxA])
editor.setSelectedShapes([ids.boxA])
const shapeA = editor.getShape(ids.boxA)!
editor.updateShapes([{ id: ids.boxA, type: shapeA.type, rotation: 90 }])
@ -253,7 +253,7 @@ describe('When nudging a rotated shape...', () => {
describe('When nudging multiple rotated shapes...', () => {
it('Moves the page point correctly', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB])
editor.setSelectedShapes([ids.boxA, ids.boxB])
const shapeA = editor.getShape(ids.boxA)!
const shapeB = editor.getShape(ids.boxB)!

Wyświetl plik

@ -16,7 +16,7 @@ beforeEach(() => {
it('Sets selected shapes', () => {
expect(editor.selectedShapeIds).toMatchObject([])
editor.setSelectedShapeIds([ids.box1, ids.box2])
editor.setSelectedShapes([ids.box1, ids.box2])
expect(editor.selectedShapeIds).toMatchObject([ids.box1, ids.box2])
editor.undo()
expect(editor.selectedShapeIds).toMatchObject([])
@ -25,12 +25,12 @@ it('Sets selected shapes', () => {
})
it('Prevents parent and child from both being selected', () => {
editor.setSelectedShapeIds([ids.box2, ids.ellipse1]) // ellipse1 is child of box2
editor.setSelectedShapes([ids.box2, ids.ellipse1]) // ellipse1 is child of box2
expect(editor.selectedShapeIds).toMatchObject([ids.box2])
})
it('Deleting the parent also deletes descendants', () => {
editor.setSelectedShapeIds([ids.box2])
editor.setSelectedShapes([ids.box2])
expect(editor.selectedShapeIds).toMatchObject([ids.box2])
expect(editor.getShape(ids.box2)).not.toBeUndefined()

Wyświetl plik

@ -50,7 +50,7 @@ describe('distributeShapes command', () => {
describe('when less than three shapes are selected', () => {
it('does nothing', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB])
editor.setSelectedShapes([ids.boxA, ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 0)
@ -61,7 +61,7 @@ describe('distributeShapes command', () => {
describe('when stacking horizontally', () => {
it('stacks the shapes based on a given value', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 10)
jest.advanceTimersByTime(1000)
// 200 distance gap between c and d
@ -88,7 +88,7 @@ describe('distributeShapes command', () => {
})
it('stacks the shapes based on the most common gap', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 0)
jest.advanceTimersByTime(1000)
// 200 distance gap between c and d
@ -116,7 +116,7 @@ describe('distributeShapes command', () => {
it('stacks the shapes based on the average', () => {
editor.updateShapes([{ id: ids.boxD, type: 'geo', x: 540, y: 700 }])
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 0)
jest.advanceTimersByTime(1000)
editor.expectShapeToMatch({
@ -144,7 +144,7 @@ describe('distributeShapes command', () => {
describe('when stacking vertically', () => {
it('stacks the shapes based on a given value', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'vertical', 10)
jest.advanceTimersByTime(1000)
// 200 distance gap between c and d
@ -171,7 +171,7 @@ describe('distributeShapes command', () => {
})
it('stacks the shapes based on the most common gap', () => {
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'vertical', 0)
jest.advanceTimersByTime(1000)
// 200 distance gap between c and d
@ -199,7 +199,7 @@ describe('distributeShapes command', () => {
it('stacks the shapes based on the average', () => {
editor.updateShapes([{ id: ids.boxD, type: 'geo', x: 700, y: 540 }])
editor.setSelectedShapeIds([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.setSelectedShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD])
editor.stackShapes(editor.selectedShapeIds, 'vertical', 0)
jest.advanceTimersByTime(1000)
editor.expectShapeToMatch({

Wyświetl plik

@ -25,7 +25,7 @@ beforeEach(() => {
describe('when less than two shapes are selected', () => {
it('does nothing', () => {
editor.setSelectedShapeIds([ids.boxB])
editor.setSelectedShapes([ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.stretchShapes(editor.selectedShapeIds, 'horizontal')

Wyświetl plik

@ -14,7 +14,7 @@ beforeEach(() => {
})
it('zooms to selection bounds', () => {
editor.setSelectedShapeIds([ids.box1, ids.box2])
editor.setSelectedShapes([ids.box1, ids.box2])
editor.zoomToSelection()
editor.expectCameraToBe(354.64, 139.29, 1)
})
@ -36,7 +36,7 @@ it('does not zoom past min', () => {
it('does not zoom to selection when camera is frozen', () => {
const cameraBefore = { ...editor.camera }
editor.updateInstanceState({ canMoveCamera: false })
editor.setSelectedShapeIds([ids.box1, ids.box2])
editor.setSelectedShapes([ids.box1, ids.box2])
editor.zoomToSelection()
expect(editor.camera).toMatchObject(cameraBefore)
})

Wyświetl plik

@ -0,0 +1,165 @@
import { TLShape, createShapeId } from '@tldraw/editor'
import { TestEditor } from './TestEditor'
let editor: TestEditor
const ids = {
box1: createShapeId('box1'),
box2: createShapeId('box2'),
box3: createShapeId('box3'),
box4: createShapeId('box4'),
frame1: createShapeId('frame1'),
}
let opts = {} as {
hitInside?: boolean | undefined
margin?: number | undefined
hitLabels?: boolean | undefined
hitFrameInside?: boolean | undefined
filter?: ((shape: TLShape) => boolean) | undefined
}
beforeEach(() => {
editor = new TestEditor()
editor.createShapes([
{ id: ids.box4, type: 'geo', x: 75, y: -50, props: { w: 50, h: 100, fill: 'solid' } }, // overlapping box2
{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
{ id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } },
{ id: ids.box3, type: 'geo', x: 350, y: 350, props: { w: 90, h: 90 } }, // overlapping box2
{ id: ids.frame1, type: 'frame', x: 0, y: 500, props: { w: 500, h: 500 } }, // frame
])
})
describe('with default options', () => {
beforeEach(() => {
opts = {}
})
it('misses shape', () => {
expect(editor.getShapeAtPoint({ x: 0, y: 0 }, opts)?.id).toBe(undefined)
})
it('gets shape on edge', () => {
expect(editor.getShapeAtPoint({ x: 100, y: 100 }, opts)?.id).toBe(ids.box1)
})
it('misses shape in empty space', () => {
expect(editor.getShapeAtPoint({ x: 125, y: 125 }, opts)?.id).toBe(undefined)
})
it('misses shape in geo shape label', () => {
expect(editor.getShapeAtPoint({ x: 150, y: 150 }, opts)?.id).toBe(undefined)
})
it('misses geo shape label behind overlapping hollow shape', () => {
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, opts)?.id).toBe(ids.box3)
})
it('hits solid shape behind overlapping hollow shape', () => {
expect(editor.getShapeAtPoint({ x: 90, y: 10 }, opts)?.id).toBe(ids.box4)
})
it('missed overlapping shapes', () => {
expect(editor.getShapeAtPoint({ x: 375, y: 375 }, opts)?.id).toBe(undefined)
})
it('does not hit frame inside', () => {
expect(editor.getShapeAtPoint({ x: 50, y: 550 }, opts)?.id).toBe(undefined)
})
})
describe('with hitInside=true', () => {
beforeEach(() => {
opts = {
hitInside: true,
}
})
it('misses shape', () => {
expect(editor.getShapeAtPoint({ x: 0, y: 0 }, opts)?.id).toBe(undefined)
})
it('gets shape on edge', () => {
expect(editor.getShapeAtPoint({ x: 100, y: 100 }, opts)?.id).toBe(ids.box1)
})
it('hits shape in empty space', () => {
expect(editor.getShapeAtPoint({ x: 125, y: 125 }, opts)?.id).toBe(ids.box1)
})
it('gets shape in geo shape label', () => {
expect(editor.getShapeAtPoint({ x: 150, y: 150 }, opts)?.id).toBe(ids.box1)
})
it('misses geo shape label behind overlapping hollow shape', () => {
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, opts)?.id).toBe(ids.box3)
})
it('hits solid shape behind overlapping hollow shape', () => {
expect(editor.getShapeAtPoint({ x: 90, y: 10 }, opts)?.id).toBe(ids.box4)
})
it('hits overlapping shape', () => {
expect(editor.getShapeAtPoint({ x: 375, y: 375 }, opts)?.id).toBe(ids.box3)
})
it('does not hit frame inside', () => {
expect(editor.getShapeAtPoint({ x: 50, y: 550 }, opts)?.id).toBe(undefined)
})
})
describe('with hitFrameInside=true', () => {
beforeEach(() => {
opts = {
hitFrameInside: true,
}
})
it('misses shape', () => {
expect(editor.getShapeAtPoint({ x: 0, y: 0 }, opts)?.id).toBe(undefined)
})
it('gets shape on edge', () => {
expect(editor.getShapeAtPoint({ x: 100, y: 100 }, opts)?.id).toBe(ids.box1)
})
it('misses shape in empty space', () => {
expect(editor.getShapeAtPoint({ x: 125, y: 125 }, opts)?.id).toBe(undefined)
})
it('does not hit frame inside', () => {
expect(editor.getShapeAtPoint({ x: 50, y: 550 }, opts)?.id).toBe(ids.frame1)
})
})
describe('with hitLabels=true', () => {
beforeEach(() => {
opts = {
hitLabels: true,
}
})
it('gets shape in geo shape label', () => {
expect(editor.getShapeAtPoint({ x: 150, y: 150 }, opts)?.id).toBe(ids.box1)
})
it('hits geo shape label behind overlapping hollow shape', () => {
expect(editor.getShapeAtPoint({ x: 350, y: 350 }, opts)?.id).toBe(ids.box2)
})
})
describe('with filter', () => {
beforeEach(() => {
opts = {
filter: (shape) => shape.id !== ids.box2,
}
})
it('hits filtered in', () => {
expect(editor.getShapeAtPoint({ x: 100, y: 100 }, opts)?.id).toBe(ids.box1)
})
it('misses filtered out', () => {
expect(editor.getShapeAtPoint({ x: 310, y: 310 }, opts)?.id).toBe(undefined)
})
})

Wyświetl plik

@ -833,11 +833,11 @@ describe('focus layers', () => {
expect(editor.focusedGroupId).toBe(editor.currentPageId)
editor.select(ids.boxA)
expect(editor.focusedGroupId).toBe(groupAId)
editor.setSelectedShapeIds([...editor.selectedShapeIds, ids.boxC])
editor.setSelectedShapes([...editor.selectedShapeIds, ids.boxC])
expect(editor.focusedGroupId).toBe(groupCId)
editor.deselect(ids.boxA)
expect(editor.focusedGroupId).toBe(groupBId)
editor.setSelectedShapeIds([...editor.selectedShapeIds, ids.boxB])
editor.setSelectedShapes([...editor.selectedShapeIds, ids.boxB])
expect(editor.focusedGroupId).toBe(groupCId)
})
it('should not adjust the focus layer when clearing the selection', () => {
@ -952,15 +952,16 @@ describe('the select tool', () => {
expect(editor.selectedShapeIds).toHaveLength(0)
})
it('if a shape inside a focused group is selected and you click an empty space inside the group it should deselect the shape', () => {
editor.select(ids.boxA)
expect(editor.focusedGroupId).toBe(groupAId)
expect(editor.selectedShapeIds).toEqual([ids.boxA])
editor.pointerDown(20, 5)
editor.pointerUp(20, 5)
expect(editor.focusedGroupId).toBe(groupAId)
expect(editor.selectedShapeIds).toEqual([])
})
// ! Removed with hollow shape clicking feature
// it('if a shape inside a focused group is selected and you click an empty space inside the group it should deselect the shape', () => {
// editor.select(ids.boxA)
// expect(editor.focusedGroupId).toBe(groupAId)
// expect(editor.selectedShapeIds).toEqual([ids.boxA])
// editor.pointerDown(20, 5)
// editor.pointerUp(20, 5)
// expect(editor.focusedGroupId).toBe(groupAId)
// expect(editor.selectedShapeIds).toEqual([])
// })
// ! Removed
// it('if you click inside the empty space of a focused group while there are no selected shapes, it should pop the focus layer and select the group', () => {

Wyświetl plik

@ -87,7 +87,8 @@ describe('When pointing a shape behind the current selection', () => {
<TL.geo ref="C" x={100} y={100} w={50} h={50} />,
])
editor.select(ids.A, ids.C)
// don't select it yet! It's behind the current selection
// don't select B yet! It's behind the current selection
editor.pointerDown(75, 75, { target: 'canvas' }, { shiftKey: true })
editor.expectToBeIn('select.pointing_selection')
expect(editor.selectedShapeIds).toMatchObject([ids.A, ids.C])

Wyświetl plik

@ -55,11 +55,14 @@ describe('Hovering shapes', () => {
expect(editor.hoveredShapeId).toBe(null)
editor.pointerMove(4, 50)
expect(editor.hoveredShapeId).toBe(ids.box1)
editor.pointerMove(75, 75)
expect(editor.hoveredShapeId).toBe(null)
// does not hover the label of a geo shape
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
})
it('hovers the margins or inside of hollow shapes', () => {
it('hovers the margins or inside of filled shapes', () => {
editor.updateShape({ id: ids.box1, type: 'geo', props: { fill: 'solid' } })
expect(editor.hoveredShapeId).toBe(null)
editor.pointerMove(-4, 50)
@ -149,13 +152,31 @@ describe('when shape is hollow', () => {
box1 = editor.getShape<TLGeoShape>(ids.box1)!
})
it('misses on pointer down over shape, hits on pointer up', () => {
editor.pointerMove(50, 50)
it('misses on pointer down over shape, misses on pointer up', () => {
editor.pointerMove(75, 75)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box1.id])
expect(editor.selectedShapeIds).toEqual([])
})
it('hits on the label', () => {
editor.pointerMove(-100, -100)
expect(editor.hoveredShapeId).toBe(null)
expect(editor.selectedShapeIds).toEqual([])
editor.pointerMove(50, 50)
// no hover over label...
expect(editor.hoveredShapeId).toBe(null)
expect(editor.selectedShapeIds).toEqual([])
editor.pointerDown()
// will select on pointer up
expect(editor.hoveredShapeId).toBe(null)
expect(editor.selectedShapeIds).toEqual([])
// selects on pointer up
editor.pointerUp()
expect(editor.hoveredShapeId).toBe(null)
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('hits on pointer down over shape margin (inside)', () => {
@ -186,11 +207,11 @@ describe('when shape is hollow', () => {
})
it('brushes on point inside and drag', () => {
editor.pointerMove(50, 50)
editor.pointerMove(75, 75)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerMove(55, 55)
editor.pointerMove(80, 80)
editor.expectToBeIn('select.brushing')
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([])
@ -375,13 +396,13 @@ describe('when shape is inside of a frame', () => {
expect(editor.selectedShapeIds).toEqual([])
})
it('misses on pointer down over shape, hits on pointer up', () => {
it('misses on pointer down over shape, misses on pointer up', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown() // inside of box1
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box1.id])
expect(editor.selectedShapeIds).toEqual([])
})
it('misses when shape is masked by frame on pointer down over shape, misses on pointer up', () => {
@ -497,28 +518,30 @@ describe('when a frame has multiple children', () => {
box2 = editor.getShape<TLGeoShape>(ids.box2)!
})
it('selects the smaller of two overlapping hollow shapes on pointer up when both are the child of a frame', () => {
// make box2 smaller
editor.updateShape({ ...box2, props: { w: 99, h: 99 } })
// This is no longer the case; it will be true for arrows though
editor.pointerMove(64, 64)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box2])
// it('selects the smaller of two overlapping hollow shapes on pointer up when both are the child of a frame', () => {
// // make box2 smaller
// editor.updateShape({ ...box2, props: { w: 99, h: 99 } })
// make box2 bigger...
editor.selectNone()
editor.updateShape({ ...box2, props: { w: 101, h: 101 } })
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box2])
editor.pointerMove(64, 64)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
// // make box2 bigger...
// editor.selectNone()
// editor.updateShape({ ...box2, props: { w: 101, h: 101 } })
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// })
it('brush does not select a shape when brushing its masked parts', () => {
editor.pointerMove(110, 0)
@ -601,27 +624,20 @@ describe('when a frame has multiple children', () => {
})
describe('when shape is selected', () => {
let box1: TLGeoShape
beforeEach(() => {
it('hits on pointer down over shape, misses on pointer up', () => {
editor.createShapes([{ id: ids.box1, type: 'geo', props: { fill: 'none' } }])
box1 = editor.getShape<TLGeoShape>(ids.box1)!
editor.select(ids.box1)
})
it('misses on pointer down over shape, hits on pointer up', () => {
editor.pointerMove(50, 50)
editor.pointerMove(75, 75)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([box1.id])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box1.id])
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
})
describe('When shapes are overlapping', () => {
let box1: TLGeoShape
let box2: TLGeoShape
let box3: TLGeoShape
let box4: TLGeoShape
let box5: TLGeoShape
beforeEach(() => {
@ -680,9 +696,7 @@ describe('When shapes are overlapping', () => {
},
])
box1 = editor.getShape<TLGeoShape>(ids.box1)!
box2 = editor.getShape<TLGeoShape>(ids.box2)!
box3 = editor.getShape<TLGeoShape>(ids.box3)!
box4 = editor.getShape<TLGeoShape>(ids.box4)!
box5 = editor.getShape<TLGeoShape>(ids.box5)!
@ -734,31 +748,31 @@ describe('When shapes are overlapping', () => {
expect(editor.selectedShapeIds).toEqual([box5.id])
})
it('selects the smallest overlapping hollow shape', () => {
editor.pointerMove(125, 150)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box3.id])
editor.selectNone()
expect(editor.hoveredShapeId).toBe(null)
// it('selects the smallest overlapping hollow shape', () => {
// editor.pointerMove(125, 175)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box3.id])
// editor.selectNone()
// expect(editor.hoveredShapeId).toBe(null)
editor.pointerMove(64, 64)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box2.id])
editor.selectNone()
// editor.pointerMove(64, 64)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box2.id])
// editor.selectNone()
editor.pointerMove(35, 35)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([box1.id])
})
// editor.pointerMove(35, 35)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([box1.id])
// })
})
describe('Selects inside of groups', () => {
@ -837,19 +851,19 @@ describe('Selects inside of groups', () => {
expect(editor.selectedShapeIds).toEqual([ids.box2])
})
it('selects child when pointing inside of a hollow child shape', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.group1])
editor.pointerDown()
editor.expectToBeIn('select.pointing_selection')
expect(editor.selectedShapeIds).toEqual([ids.group1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
// it('selects child when pointing inside of a hollow child shape', () => {
// editor.pointerMove(75, 75)
// expect(editor.hoveredShapeId).toBe(null)
// editor.pointerDown()
// expect(editor.selectedShapeIds).toEqual([])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.group1])
// editor.pointerDown()
// editor.expectToBeIn('select.pointing_selection')
// expect(editor.selectedShapeIds).toEqual([ids.group1])
// editor.pointerUp()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// })
it('selects a solid shape in a group when double clicking it', () => {
editor.pointerMove(250, 50)
@ -867,13 +881,13 @@ describe('Selects inside of groups', () => {
expect(editor.focusedGroupId).toBe(ids.group1)
})
it('selects a hollow shape in a group when double clicking it', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.doubleClick()
expect(editor.selectedShapeIds).toEqual([ids.box1])
expect(editor.focusedGroupId).toBe(ids.group1)
})
// it('selects a hollow shape in a group when double clicking it', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// expect(editor.selectedShapeIds).toEqual([ids.box1])
// expect(editor.focusedGroupId).toBe(ids.group1)
// })
it('selects a hollow shape in a group when double clicking its edge', () => {
editor.pointerMove(102, 50)
@ -883,14 +897,14 @@ describe('Selects inside of groups', () => {
expect(editor.focusedGroupId).toBe(ids.group1)
})
it('double clicks a hollow shape when the focus layer is the shapes parent', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.doubleClick()
editor.doubleClick()
expect(editor.editingShapeId).toBe(ids.box1)
editor.expectToBeIn('select.editing_shape')
})
// it('double clicks a hollow shape when the focus layer is the shapes parent', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box1)
// editor.expectToBeIn('select.editing_shape')
// })
it('double clicks a solid shape to edit it when the focus layer is the shapes parent', () => {
editor.pointerMove(250, 50)
@ -901,40 +915,40 @@ describe('Selects inside of groups', () => {
editor.expectToBeIn('select.editing_shape')
})
it('double clicks a sibling shape to edit it when the focus layer is the shapes parent', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.doubleClick()
// it('double clicks a sibling shape to edit it when the focus layer is the shapes parent', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
editor.pointerMove(250, 50)
expect(editor.hoveredShapeId).toBe(ids.box2)
editor.doubleClick()
expect(editor.editingShapeId).toBe(ids.box2)
editor.expectToBeIn('select.editing_shape')
})
// editor.pointerMove(250, 50)
// expect(editor.hoveredShapeId).toBe(ids.box2)
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box2)
// editor.expectToBeIn('select.editing_shape')
// })
it('selects a different sibling shape when editing a layer', () => {
editor.pointerMove(50, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.doubleClick()
editor.doubleClick()
expect(editor.editingShapeId).toBe(ids.box1)
editor.expectToBeIn('select.editing_shape')
// it('selects a different sibling shape when editing a layer', () => {
// editor.pointerMove(50, 50)
// expect(editor.hoveredShapeId).toBe(null)
// editor.doubleClick()
// editor.doubleClick()
// expect(editor.editingShapeId).toBe(ids.box1)
// editor.expectToBeIn('select.editing_shape')
editor.pointerMove(250, 50)
expect(editor.hoveredShapeId).toBe(ids.box2)
editor.pointerDown()
editor.expectToBeIn('select.pointing_shape')
expect(editor.editingShapeId).toBe(null)
expect(editor.selectedShapeIds).toEqual([ids.box2])
})
// editor.pointerMove(250, 50)
// expect(editor.hoveredShapeId).toBe(ids.box2)
// editor.pointerDown()
// editor.expectToBeIn('select.pointing_shape')
// expect(editor.editingShapeId).toBe(null)
// expect(editor.selectedShapeIds).toEqual([ids.box2])
// })
})
describe('when selecting behind selection', () => {
beforeEach(() => {
editor
.createShapes([
{ id: ids.box1, type: 'geo', x: 100, y: 0 },
{ id: ids.box1, type: 'geo', x: 100, y: 0, props: { fill: 'solid' } },
{ id: ids.box2, type: 'geo', x: 0, y: 0 },
{ id: ids.box3, type: 'geo', x: 200, y: 0 },
])
@ -942,8 +956,8 @@ describe('when selecting behind selection', () => {
})
it('does not select on pointer down, only on pointer up', () => {
editor.pointerMove(150, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerMove(175, 75)
expect(editor.hoveredShapeId).toBe(ids.box1)
editor.pointerDown() // inside of box 1
expect(editor.selectedShapeIds).toEqual([ids.box2, ids.box3])
editor.pointerUp()
@ -951,8 +965,8 @@ describe('when selecting behind selection', () => {
})
it('can drag the selection', () => {
editor.pointerMove(150, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerMove(175, 75)
expect(editor.hoveredShapeId).toBe(ids.box1)
editor.pointerDown() // inside of box 1
expect(editor.selectedShapeIds).toEqual([ids.box2, ids.box3])
editor.pointerMove(250, 50)
@ -1032,9 +1046,21 @@ describe('when shift+selecting', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('adds hollow shape to selection on pointer up', () => {
it('does not add hollow shape to selection on pointer up when in empty space', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.keyDown('Shift')
editor.pointerMove(250, 50) // above box 2
editor.pointerMove(275, 75) // above box 2
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('does not add hollow shape to selection on pointer up when over the edge/label, but select on pointer up', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.keyDown('Shift')
editor.pointerMove(250, 50) // above box 2's label
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([ids.box1])
@ -1042,26 +1068,26 @@ describe('when shift+selecting', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.box2])
})
it('adds and removes hollow shape from selection on pointer up (without causing an edit by double clicks)', () => {
it('does not add and remove hollow shape from selection on pointer up (without causing an edit by double clicks)', () => {
editor.keyDown('Shift')
editor.pointerMove(250, 50) // above box 2
editor.pointerMove(275, 75) // above box 2, empty space
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.box2])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.box2])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('adds and removes hollow shape from selection on double clicks (without causing an edit by double clicks)', () => {
it('does not add and remove hollow shape from selection on double clicks (without causing an edit by double clicks)', () => {
editor.keyDown('Shift')
editor.pointerMove(250, 50) // above box 2
editor.pointerMove(275, 75) // above box 2, empty space
expect(editor.hoveredShapeId).toBe(null)
editor.doubleClick()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.box2])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.doubleClick()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
@ -1090,14 +1116,14 @@ describe('when shift+selecting a group', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('adds to selection on shift + on pointer up when clicking in hollow shape', () => {
it('does not add to selection on shift + on pointer up when clicking in hollow shape', () => {
editor.keyDown('Shift')
editor.pointerMove(250, 50)
editor.pointerMove(275, 75)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown() // inside of box 2, inside of group 1
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.group1])
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('adds to selection on pointer down when clicking in margin', () => {
@ -1120,31 +1146,34 @@ describe('when shift+selecting a group', () => {
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.group1])
})
it('brushes on down + move', () => {
it('does not select when shift+clicking into hollow shape inside of a group', () => {
editor.pointerMove(275, 75)
editor.keyDown('Shift')
editor.pointerMove(250, 50)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown() // inside of box 2, inside of group 1
editor.pointerDown() // inside of box 2, empty space, inside of group 1
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.group1])
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
it('deselects on pointer up', () => {
it('does not deselect on pointer up when clicking into empty space in hollow shape', () => {
editor.keyDown('Shift')
editor.pointerMove(250, 50)
editor.pointerMove(275, 75)
expect(editor.hoveredShapeId).toBe(null)
editor.pointerDown() // inside of box 2, inside of group 1
editor.pointerDown() // inside of box 2, empty space, inside of group 1
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.group1])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerDown()
expect(editor.selectedShapeIds).toEqual([ids.box1, ids.group1])
expect(editor.selectedShapeIds).toEqual([ids.box1])
editor.pointerUp()
expect(editor.selectedShapeIds).toEqual([ids.box1])
})
})
// some of these tests are adapted from the "select hollow shape on pointer up" logic, which was removed.
// the tests may seem arbitrary but their mostly negating the logic that was introduced in that feature.
describe('When children / descendants of a group are selected', () => {
beforeEach(() => {
editor
@ -1305,15 +1334,17 @@ describe('When double clicking an editable shape', () => {
editor.expectToBeIn('select.editing_shape')
})
it('starts editing a child of a group on double click', () => {
it('starts editing a child of a group on triple (not double!) click', () => {
editor.createShape({ id: ids.box2, type: 'geo', x: 300, y: 0 })
editor.groupShapes([ids.box1, ids.box2], ids.group1)
editor.selectNone()
editor.pointerMove(50, 50).doubleClick()
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.selectedShapeIds).toEqual([ids.group1])
expect(editor.editingShapeId).toBe(null)
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.selectedShapeIds).toEqual([ids.box1])
expect(editor.editingShapeId).toBe(null)
editor.expectToBeIn('select.idle')
editor.pointerMove(50, 50).doubleClick()
editor.pointerMove(50, 50).click() // clicks on the shape label
expect(editor.selectedShapeIds).toEqual([ids.box1])
expect(editor.editingShapeId).toBe(ids.box1)
editor.expectToBeIn('select.editing_shape')

Wyświetl plik

@ -151,7 +151,7 @@ describe('Editor.setStyle', () => {
<TL.geo ref="B" x={0} y={0} color="green" />,
])
editor.setSelectedShapeIds([ids.A, ids.B])
editor.setSelectedShapes([ids.A, ids.B])
editor.setStyle(DefaultColorStyle, 'red')
expect(editor.getShape<TLGeoShape>(ids.A)!.props.color).toBe('red')
@ -170,7 +170,7 @@ describe('Editor.setStyle', () => {
</TL.group>,
])
editor.setSelectedShapeIds([ids.groupA])
editor.setSelectedShapes([ids.groupA])
editor.setStyle(DefaultColorStyle, 'red')
// a wasn't selected...