kopia lustrzana https://github.com/Tldraw/Tldraw
[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 Testsfix-pinch-safari-desktop
rodzic
eaba3c8f2a
commit
22329c51fc
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 && (
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -45,7 +45,6 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|||
points,
|
||||
})
|
||||
}),
|
||||
operation: 'union',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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', {})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
* })
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -361,7 +361,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
|
|||
}
|
||||
})
|
||||
)
|
||||
editor.setSelectedShapeIds(rootShapeIds)
|
||||
editor.setSelectedShapes(rootShapeIds)
|
||||
}
|
||||
|
||||
/* --------------- Translating Helpers --------_------ */
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)!,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)!
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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', () => {
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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...
|
||||
|
|
Ładowanie…
Reference in New Issue