fix(editor): shape tool should be the last used shape (#12425)
Close [BS-3305](https://linear.app/affine-design/issue/BS-3305/白板按s应该使用上次的shape形状) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a method to cycle through shape types when using the shape tool. - **Bug Fixes** - Improved shape tool behavior to ensure the selected shape type does not change unexpectedly after adding a new shape. - **Tests** - Added an end-to-end test to verify that the shape tool retains the selected shape type after adding a new shape. - Enhanced shortcut tests to verify cycling shapes forward and backward using 's' and 'Shift+s' keys. - **Refactor** - Streamlined the logic for cycling through shape types and connector modes for improved maintainability. - Removed external utility for cycling shapes, integrating the functionality directly into the shape tool. - Updated keyboard shortcut handling to use the new cycling method within the shape tool. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -66,7 +66,6 @@ import {
|
|||||||
DEFAULT_NOTE_TIP,
|
DEFAULT_NOTE_TIP,
|
||||||
} from './utils/consts.js';
|
} from './utils/consts.js';
|
||||||
import { deleteElements } from './utils/crud.js';
|
import { deleteElements } from './utils/crud.js';
|
||||||
import { getNextShapeType } from './utils/hotkey-utils.js';
|
|
||||||
import { isCanvasElement } from './utils/query.js';
|
import { isCanvasElement } from './utils/query.js';
|
||||||
|
|
||||||
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||||
@@ -216,10 +215,8 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { shapeName } = controller.activatedOption;
|
|
||||||
const nextShapeName = getNextShapeType(shapeName);
|
|
||||||
this._setEdgelessTool(ShapeTool, {
|
this._setEdgelessTool(ShapeTool, {
|
||||||
shapeName: nextShapeName,
|
shapeName: controller.cycleShapeName('prev'),
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.createOverlay();
|
controller.createOverlay();
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape';
|
|
||||||
import { ShapeType } from '@blocksuite/affine-model';
|
|
||||||
|
|
||||||
const shapeMap: Record<ShapeToolOption['shapeName'], number> = {
|
|
||||||
[ShapeType.Rect]: 0,
|
|
||||||
[ShapeType.Ellipse]: 1,
|
|
||||||
[ShapeType.Diamond]: 2,
|
|
||||||
[ShapeType.Triangle]: 3,
|
|
||||||
roundedRect: 4,
|
|
||||||
};
|
|
||||||
const shapes = Object.keys(shapeMap) as ShapeToolOption['shapeName'][];
|
|
||||||
|
|
||||||
export function getNextShapeType(cur: ShapeToolOption['shapeName']) {
|
|
||||||
return shapes[(shapeMap[cur] + 1) % shapes.length];
|
|
||||||
}
|
|
||||||
@@ -223,13 +223,15 @@ export class ConnectorTool extends BaseTool<ConnectorToolOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNextMode() {
|
getNextMode() {
|
||||||
switch (this.activatedOption.mode) {
|
// reorder the enum values
|
||||||
case ConnectorMode.Curve:
|
const modes = [
|
||||||
return ConnectorMode.Orthogonal;
|
ConnectorMode.Curve,
|
||||||
case ConnectorMode.Orthogonal:
|
ConnectorMode.Orthogonal,
|
||||||
return ConnectorMode.Straight;
|
ConnectorMode.Straight,
|
||||||
case ConnectorMode.Straight:
|
];
|
||||||
return ConnectorMode.Curve;
|
|
||||||
}
|
const currentIndex = modes.indexOf(this.activatedOption.mode);
|
||||||
|
const nextIndex = (currentIndex + 1) % modes.length;
|
||||||
|
return modes[nextIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,21 +235,18 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
|||||||
const locked = this.gfx.viewport.locked;
|
const locked = this.gfx.viewport.locked;
|
||||||
const selection = this.gfx.selection;
|
const selection = this.gfx.selection;
|
||||||
if (locked || selection.editing) return;
|
if (locked || selection.editing) return;
|
||||||
if (
|
|
||||||
this.gfx.tool.dragging$.peek() &&
|
|
||||||
this.gfx.tool.currentToolName$.peek() === 'shape'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeIndex = shapes.findIndex(
|
const currentTool = this.gfx.tool.currentToolName$.peek();
|
||||||
s => s.name === this.draggingShape
|
|
||||||
);
|
|
||||||
const nextIndex = (activeIndex + 1) % shapes.length;
|
|
||||||
const next = shapes[nextIndex];
|
|
||||||
this.draggingShape = next.name;
|
|
||||||
|
|
||||||
if (this.readyToDrop) {
|
if (this.readyToDrop) {
|
||||||
|
if (currentTool === ShapeTool.toolName) {
|
||||||
|
const activeIndex = shapes.findIndex(
|
||||||
|
s => s.name === this.draggingShape
|
||||||
|
);
|
||||||
|
const nextIndex = (activeIndex + 1) % shapes.length;
|
||||||
|
const next = shapes[nextIndex];
|
||||||
|
this.draggingShape = next.name;
|
||||||
|
}
|
||||||
this.draggableController.cancelWithoutAnimation();
|
this.draggableController.cancelWithoutAnimation();
|
||||||
const el = this.shapeContainer.querySelector(
|
const el = this.shapeContainer.querySelector(
|
||||||
`.shape.${this.draggingShape}`
|
`.shape.${this.draggingShape}`
|
||||||
@@ -264,8 +261,14 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
|||||||
const clientPos = { x: x + left, y: y + top };
|
const clientPos = { x: x + left, y: y + top };
|
||||||
this.draggableController.dragAndMoveTo(el, clientPos);
|
this.draggableController.dragAndMoveTo(el, clientPos);
|
||||||
} else {
|
} else {
|
||||||
|
if (this.gfx.tool.dragging$.peek()) return;
|
||||||
|
let shapeName =
|
||||||
|
this.gfx.tool.get(ShapeTool).activatedOption.shapeName;
|
||||||
|
if (currentTool === ShapeTool.toolName) {
|
||||||
|
shapeName = this.gfx.tool.get(ShapeTool).cycleShapeName('next');
|
||||||
|
}
|
||||||
this.setEdgelessTool(ShapeTool, {
|
this.setEdgelessTool(ShapeTool, {
|
||||||
shapeName: this.draggingShape,
|
shapeName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import {
|
|||||||
type SurfaceBlockComponent,
|
type SurfaceBlockComponent,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import type { ShapeElementModel, ShapeName } from '@blocksuite/affine-model';
|
import type { ShapeElementModel, ShapeName } from '@blocksuite/affine-model';
|
||||||
import { DefaultTheme, getShapeType } from '@blocksuite/affine-model';
|
import {
|
||||||
|
DefaultTheme,
|
||||||
|
getShapeType,
|
||||||
|
ShapeType,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EditPropsStore,
|
EditPropsStore,
|
||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
@@ -15,7 +19,7 @@ import { hasClassNameInList } from '@blocksuite/affine-shared/utils';
|
|||||||
import type { IBound } from '@blocksuite/global/gfx';
|
import type { IBound } from '@blocksuite/global/gfx';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import { BaseTool } from '@blocksuite/std/gfx';
|
import { BaseTool, type GfxController } from '@blocksuite/std/gfx';
|
||||||
import { effect } from '@preact/signals-core';
|
import { effect } from '@preact/signals-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -159,6 +163,13 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
|
|||||||
this._surfaceComponent?.refresh();
|
this._surfaceComponent?.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(gfx: GfxController) {
|
||||||
|
super(gfx);
|
||||||
|
this.activatedOption = {
|
||||||
|
shapeName: ShapeType.Rect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
override activate() {
|
override activate() {
|
||||||
this.createOverlay();
|
this.createOverlay();
|
||||||
}
|
}
|
||||||
@@ -337,4 +348,20 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
|
|||||||
setDisableOverlay(disable: boolean) {
|
setDisableOverlay(disable: boolean) {
|
||||||
this._disableOverlay = disable;
|
this._disableOverlay = disable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cycleShapeName(dir: 'prev' | 'next' = 'next'): ShapeName {
|
||||||
|
const shapeNames: ShapeName[] = [
|
||||||
|
ShapeType.Rect,
|
||||||
|
ShapeType.Ellipse,
|
||||||
|
ShapeType.Diamond,
|
||||||
|
ShapeType.Triangle,
|
||||||
|
'roundedRect',
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentIndex = shapeNames.indexOf(this.activatedOption.shapeName);
|
||||||
|
const nextIndex =
|
||||||
|
(currentIndex + (dir === 'prev' ? -1 : 1) + shapeNames.length) %
|
||||||
|
shapeNames.length;
|
||||||
|
return shapeNames[nextIndex];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export enum ShapeTextFontSize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ShapeType {
|
export enum ShapeType {
|
||||||
Diamond = 'diamond',
|
|
||||||
Ellipse = 'ellipse',
|
|
||||||
Rect = 'rect',
|
Rect = 'rect',
|
||||||
|
Ellipse = 'ellipse',
|
||||||
|
Diamond = 'diamond',
|
||||||
Triangle = 'triangle',
|
Triangle = 'triangle',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { expect, type Page } from '@playwright/test';
|
|||||||
import { lightThemeV2 } from '@toeverything/theme/v2';
|
import { lightThemeV2 } from '@toeverything/theme/v2';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
assertEdgelessShapeType,
|
||||||
assertEdgelessTool,
|
assertEdgelessTool,
|
||||||
changeShapeFillColor,
|
changeShapeFillColor,
|
||||||
changeShapeFillColorToTransparent,
|
changeShapeFillColorToTransparent,
|
||||||
@@ -788,3 +789,21 @@ test('shape should be editable when re-enter canvas', async ({ page }) => {
|
|||||||
await dblclickView(page, [50, 50]);
|
await dblclickView(page, [50, 50]);
|
||||||
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
|
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shape tool should not be changed after adding new shape', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await enterPlaygroundRoom(page);
|
||||||
|
await initEmptyEdgelessState(page);
|
||||||
|
await switchEditorMode(page);
|
||||||
|
|
||||||
|
await setEdgelessTool(page, 'shape');
|
||||||
|
await page.keyboard.press('s');
|
||||||
|
await waitNextFrame(page);
|
||||||
|
await assertEdgelessShapeType(page, 'ellipse');
|
||||||
|
await clickView(page, [0, 0]);
|
||||||
|
|
||||||
|
await page.keyboard.press('s');
|
||||||
|
await waitNextFrame(page);
|
||||||
|
await assertEdgelessShapeType(page, 'ellipse');
|
||||||
|
});
|
||||||
|
|||||||
@@ -86,9 +86,14 @@ test('toggle shapes shortcut', async ({ page }) => {
|
|||||||
'roundedRect',
|
'roundedRect',
|
||||||
] as ShapeName[];
|
] as ShapeName[];
|
||||||
for (const shape of shapesInOrder) {
|
for (const shape of shapesInOrder) {
|
||||||
await page.keyboard.press('Shift+s');
|
await page.keyboard.press('s');
|
||||||
await assertEdgelessShapeType(page, shape);
|
await assertEdgelessShapeType(page, shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const shape of shapesInOrder.reverse()) {
|
||||||
|
await assertEdgelessShapeType(page, shape);
|
||||||
|
await page.keyboard.press('Shift+s');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not switch shapes in editing', async ({ page }) => {
|
test('should not switch shapes in editing', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user