Add opacity keyboard shortcuts matching Figma: pressing Digit1-Digit9 sets the selected layers to 10%-90% opacity, and Digit0 sets 100%. - Add 'selection.setOpacity' EditorCommandId with registry metadata (shortcut display '1-9, 0') so it appears in the command cheatsheet - Implement the command in selection.ts using editor.undo.runBatch + updateNodeWithUndo, following the toggleVisibility/setBlendMode pattern - Add setOpacityTarget closure in useEditorCommands so the external keyboard handler can inject the digit value before runCommand - Add canSetOpacity capability (enabled when there is a selection) - Add bindOpacityKeys handler in src/app/shell/keyboard/opacity.ts, following the nudging.ts external-handler pattern since the opacity value comes from event.code and cannot be expressed as a fixed keybinding in the registry - Guard against text editing, number field focus, and modifier keys - Add i18n label 'setOpacity' across all 8 locales - Add unit tests for single/multi selection, undo batch, and no-op
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
|
|
import { createEditor } from '@open-pencil/core/editor'
|
|
|
|
import { getNodeOrThrow } from '#tests/helpers/assert'
|
|
|
|
describe('opacity via updateNodeWithUndo + runBatch', () => {
|
|
function setup() {
|
|
const editor = createEditor()
|
|
const pageId = editor.graph.getPages()[0].id
|
|
const rect = editor.graph.createNode('RECTANGLE', pageId, {
|
|
name: 'Rect',
|
|
x: 0,
|
|
y: 0,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
editor.select([rect.id])
|
|
return { editor, rect }
|
|
}
|
|
|
|
function setOpacity(editor: ReturnType<typeof createEditor>, opacity: number) {
|
|
const targets = editor.getSelectedNodes()
|
|
if (targets.length === 0) return
|
|
editor.undo.runBatch('Set opacity', () => {
|
|
for (const target of targets) {
|
|
if (target.opacity === opacity) continue
|
|
editor.updateNodeWithUndo(target.id, { opacity }, 'Set opacity')
|
|
}
|
|
})
|
|
}
|
|
|
|
test('sets opacity to 50%', () => {
|
|
const { editor, rect } = setup()
|
|
|
|
setOpacity(editor, 0.5)
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.5)
|
|
})
|
|
|
|
test('0 digit sets opacity to 100%', () => {
|
|
const { editor, rect } = setup()
|
|
|
|
setOpacity(editor, 0.5)
|
|
setOpacity(editor, 1)
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
|
|
})
|
|
|
|
test('opacity change is undoable as a single batch entry', () => {
|
|
const { editor, rect } = setup()
|
|
|
|
setOpacity(editor, 0.3)
|
|
expect(editor.undo.canUndo).toBe(true)
|
|
|
|
editor.undo.undo()
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
|
|
|
|
editor.undo.redo()
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.3)
|
|
})
|
|
|
|
test('opacity batch for multiple selections collapses to one undo entry', () => {
|
|
const { editor, rect } = setup()
|
|
const pageId = editor.graph.getPages()[0].id
|
|
const rect2 = editor.graph.createNode('RECTANGLE', pageId, {
|
|
name: 'Rect2',
|
|
x: 100,
|
|
y: 0,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
editor.select([rect.id, rect2.id])
|
|
|
|
setOpacity(editor, 0.7)
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.7)
|
|
expect(getNodeOrThrow(editor.graph, rect2.id).opacity).toBe(0.7)
|
|
|
|
editor.undo.undo()
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
|
|
expect(getNodeOrThrow(editor.graph, rect2.id).opacity).toBe(1)
|
|
})
|
|
|
|
test('no-op when opacity already matches', () => {
|
|
const { editor, rect } = setup()
|
|
|
|
setOpacity(editor, 1)
|
|
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
|
|
expect(editor.undo.canUndo).toBe(false)
|
|
})
|
|
})
|