From 24d3c484198eb92ae5948c3776f2aa6b16b997f7 Mon Sep 17 00:00:00 2001 From: Overu Date: Fri, 5 Dec 2025 19:45:23 +0800 Subject: [PATCH 01/33] feat(spx-gui): add a configurable button to the custom transformer --- .../common/viewer/custom-transformer/index.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts b/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts index eae8743ca..417b4a39a 100644 --- a/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts +++ b/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts @@ -19,6 +19,9 @@ transformerFlipArrowDisabledImg.src = transformerFlipArrowDisabledPng const rotatorCircleImg = new Image() rotatorCircleImg.src = rotatorCirclePng +const configorTagImg = new Image() +configorTagImg.src = rotatorCirclePng + export type CustomTransformerConfig = { rotationStyle?: 'none' | 'normal' | 'left-right' } & Pick @@ -55,6 +58,40 @@ class RotatorTag extends Konva.Group { } } +class ConfigorButton extends Konva.Group { + rect: Konva.Rect + image: Konva.Image + + constructor() { + super() + + this.rect = new Konva.Rect({ + width: 20, + height: 20, + cornerRadius: 10, + + shadowEnabled: true, + shadowColor: 'rgba(51, 51, 51, 0.2)', + shadowBlur: 4, + shadowOffsetY: 2, + + stroke: 'rgba(217, 223, 229, 1)', + strokeWidth: 0.5, + fill: '#fff' + }) + + this.image = new Konva.Image({ + width: 20, + height: 20, + cornerRadius: 10, + image: configorTagImg, + rotation: 0 + }) + + this.add(this.rect, this.image) + } +} + class FlipButton extends Konva.Group { orientation: 'left' | 'right' rect: Konva.Rect @@ -136,6 +173,7 @@ export class CustomTransformer extends Konva.Transformer { right: FlipButton } rotatorTag: RotatorTag + configorButton: ConfigorButton rotationStyle(attr?: CustomTransformerConfig['rotationStyle']): CustomTransformerConfig['rotationStyle'] { if (!attr) return this.getAttr('rotationStyle') @@ -216,6 +254,10 @@ export class CustomTransformer extends Konva.Transformer { this.rotatorTag.visible(false) this.add(this.rotatorTag) + this.configorButton = new ConfigorButton() + this.configorButton.visible(true) + this.add(this.configorButton) + const rotator = this.children.find((n) => n.name().match(/rotater/)) if (!(rotator instanceof Konva.Rect)) { throw new Error('rotator rect not found') @@ -265,6 +307,9 @@ export class CustomTransformer extends Konva.Transformer { } } + this.configorButton.x(this.width() / 2 - 10) + this.configorButton.y(this.height() + 4) + if (this.rotationStyle() === 'normal') { this.rotateEnabled(true) } else { From 9236530e10466f4fb63046ccdaafeb6d8b6e7659 Mon Sep 17 00:00:00 2001 From: Overu Date: Sun, 7 Dec 2025 22:27:37 +0800 Subject: [PATCH 02/33] feat(spx-gui): add SpriteItem title visible icon --- .../src/components/editor/sprite/SpriteItem.vue | 15 ++++++++++++++- .../ui/block-items/UIBlockItemTitle.vue | 14 ++++++++++---- .../ui/block-items/UIEditorSpriteItem.vue | 8 +++++++- spx-gui/src/components/ui/icons/UIIcon.vue | 2 ++ spx-gui/src/components/ui/icons/eye-off.svg | 3 +++ 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 spx-gui/src/components/ui/icons/eye-off.svg diff --git a/spx-gui/src/components/editor/sprite/SpriteItem.vue b/spx-gui/src/components/editor/sprite/SpriteItem.vue index e9ccc76fd..c0e2c8a4e 100644 --- a/spx-gui/src/components/editor/sprite/SpriteItem.vue +++ b/spx-gui/src/components/editor/sprite/SpriteItem.vue @@ -7,7 +7,7 @@ import { useDragDroppable } from '@/utils/drag-and-drop' import { Sprite } from '@/models/sprite' import { Costume } from '@/models/costume' import { Animation } from '@/models/animation' -import { UIImg, UIEditorSpriteItem } from '@/components/ui' +import { UIImg, UIEditorSpriteItem, UIMenuItem } from '@/components/ui' import CostumesAutoPlayer from '@/components/common/CostumesAutoPlayer.vue' import { useEditorCtx } from '../EditorContextProvider.vue' import { @@ -52,6 +52,12 @@ const radarNodeMeta = computed(() => { return { name, desc } }) +function toggleSpriteVisible() { + const name = props.sprite.name + const action = { name: { en: `Configure sprite ${name}`, zh: `修改精灵 ${name} 配置` } } + editorCtx.project.history.doAction(action, () => props.sprite.setVisible(!props.sprite.visible)) +} + const { fn: handleDuplicate } = useMessageHandle( async () => { const sprite = props.sprite @@ -148,6 +154,7 @@ useDragDroppable(() => (props.droppable ? wrapperRef.value?.$el : null), { :name="sprite.name" :selectable="selectable" :color="color" + :visible="sprite.visible" > + + {{ $t({ en: `${sprite.visible ? 'Hide' : 'Show'} Sprite`, zh: `${sprite.visible ? '隐藏' : '显示'}精灵` }) }} + +
+ +
diff --git a/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue b/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue index c1991ea05..553aa8100 100644 --- a/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue +++ b/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue @@ -3,6 +3,9 @@ {{ name }} + @@ -11,6 +14,7 @@ import { computed, type CSSProperties } from 'vue' import UIBlockItem, { type DroppableState } from './UIBlockItem.vue' import UIBlockItemTitle from './UIBlockItemTitle.vue' +import UIIcon from '../icons/UIIcon.vue' const props = withDefaults( defineProps<{ @@ -18,11 +22,13 @@ const props = withDefaults( selectable?: false | { selected: boolean } color?: 'sprite' | 'primary' droppable?: DroppableState | false + visible?: boolean | null }>(), { selectable: false, color: 'sprite', - droppable: false + droppable: false, + visible: null } ) diff --git a/spx-gui/src/components/ui/icons/UIIcon.vue b/spx-gui/src/components/ui/icons/UIIcon.vue index 3c35bf60e..d477600b1 100644 --- a/spx-gui/src/components/ui/icons/UIIcon.vue +++ b/spx-gui/src/components/ui/icons/UIIcon.vue @@ -21,6 +21,7 @@ import trash from './trash.svg?raw' import edit from './edit.svg?raw' import eye from './eye.svg?raw' import eyeSlash from './eye-slash.svg?raw' +import eyeOff from './eye-off.svg?raw' import more from './more.svg?raw' import exchange from './exchange.svg?raw' import search from './search.svg?raw' @@ -82,6 +83,7 @@ const typeIconMap = { edit, eye, eyeSlash, + eyeOff, more, exchange, search, diff --git a/spx-gui/src/components/ui/icons/eye-off.svg b/spx-gui/src/components/ui/icons/eye-off.svg new file mode 100644 index 000000000..a61ad6ab8 --- /dev/null +++ b/spx-gui/src/components/ui/icons/eye-off.svg @@ -0,0 +1,3 @@ + + + From 8633a54184065be8d2f3c68ddcfaf4fd31ea2691 Mon Sep 17 00:00:00 2001 From: Overu Date: Mon, 8 Dec 2025 19:44:52 +0800 Subject: [PATCH 03/33] feat(spx-gui): add sprite quick configuration UI --- .../editor/common/viewer/SpriteNode.vue | 48 +++++++++- .../common/viewer/custom-transformer/index.ts | 23 ++++- .../viewer/quick-config/SpriteQuickConfig.vue | 88 +++++++++++++++++++ .../quick-config/widgets/ConfigPanel.vue | 18 ++++ .../quick-config/widgets/DefaultConfig.vue | 14 +++ .../preview/stage-viewer/StageViewer.vue | 10 +++ .../components/editor/sprite/SpriteItem.vue | 2 +- .../ui/block-items/UIEditorSpriteItem.vue | 2 +- 8 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 spx-gui/src/components/editor/common/viewer/quick-config/SpriteQuickConfig.vue create mode 100644 spx-gui/src/components/editor/common/viewer/quick-config/widgets/ConfigPanel.vue create mode 100644 spx-gui/src/components/editor/common/viewer/quick-config/widgets/DefaultConfig.vue diff --git a/spx-gui/src/components/editor/common/viewer/SpriteNode.vue b/spx-gui/src/components/editor/common/viewer/SpriteNode.vue index d65e0fae2..aaa9a0d83 100644 --- a/spx-gui/src/components/editor/common/viewer/SpriteNode.vue +++ b/spx-gui/src/components/editor/common/viewer/SpriteNode.vue @@ -8,6 +8,8 @@ import type { Size } from '@/models/common' import { nomalizeDegree, round, useAsyncComputedLegacy } from '@/utils/utils' import { useFileImg } from '@/utils/file' import { cancelBubble, getNodeId } from './common' +import type { ConfigType } from './quick-config/SpriteQuickConfig.vue' +import { throttle } from 'lodash' const props = defineProps<{ sprite: Sprite @@ -26,6 +28,7 @@ const emit = defineEmits<{ selected: [] dragMove: [notifyCameraScroll: CameraScrollNotifyFn] dragEnd: [] + openConfigor: [type: ConfigType] }>() const nodeRef = ref>() @@ -56,6 +59,7 @@ onMounted(() => { function handleDragMove(e: KonvaEventObject) { cancelBubble(e) + notifyConfigorOnSpriteChange(e) emit('dragMove', (delta) => { // Adjust position if camera scrolled during dragging to keep the sprite visually unmoved e.target.x(e.target.x() - delta.x) @@ -72,8 +76,30 @@ function handleDragEnd(e: KonvaEventObject) { emit('dragEnd') } +const notifyConfigorOnSpriteChange = throttle((e: KonvaEventObject) => { + if (!props.selected) return + + const { sprite } = props + + const size = toSize(e) + if (size != sprite.size) { + emit('openConfigor', 'size') + } + + const { x, y } = toPosition(e) + if (sprite.x !== x || sprite.y !== y) { + emit('openConfigor', 'pos') + } + + const heading = toHeading(e) + if (sprite.heading !== heading) { + emit('openConfigor', 'heading') + } +}, 200) + function handleTransformed(e: KonvaEventObject) { const sname = props.sprite.name + notifyConfigorOnSpriteChange(e) handleChange(e, { name: { en: `Transform sprite ${sname}`, zh: `调整精灵 ${sname}` } }) @@ -108,16 +134,30 @@ const config = computed(() => { return config }) -/** Handler for position-change (drag) or transform */ -function handleChange(e: KonvaEventObject, action: Action) { - const { sprite, mapSize } = props +function toPosition(e: KonvaEventObject) { + const { mapSize } = props const x = round(e.target.x() - mapSize.width / 2) const y = round(mapSize.height / 2 - e.target.y()) + return { x, y } +} +function toHeading(e: KonvaEventObject) { + const { sprite } = props let heading = sprite.heading if (sprite.rotationStyle === RotationStyle.Normal || sprite.rotationStyle === RotationStyle.LeftRight) { heading = nomalizeDegree(round(e.target.rotation() + 90)) } + return heading +} +function toSize(e: KonvaEventObject) { const size = round(Math.abs(e.target.scaleX()) * bitmapResolution.value, 2) + return size +} +/** Handler for position-change (drag) or transform */ +function handleChange(e: KonvaEventObject, action: Action) { + const { sprite } = props + const { x, y } = toPosition(e) + const heading = toHeading(e) + const size = toSize(e) props.project.history.doAction(action, () => { sprite.setX(x) sprite.setY(y) @@ -137,7 +177,9 @@ function handleClick() { :config="config" @dragmove="handleDragMove" @dragend="handleDragEnd" + @transform="notifyConfigorOnSpriteChange" @transformend="handleTransformed" @click="handleClick" + @open-configor="emit('openConfigor', 'default')" /> diff --git a/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts b/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts index 417b4a39a..070e143b1 100644 --- a/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts +++ b/spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts @@ -80,12 +80,25 @@ class ConfigorButton extends Konva.Group { fill: '#fff' }) + const setCursor = (cursor: string) => { + const content = this.getStage()?.content + if (content) { + content.style.cursor = cursor + } + } + this.on('mouseenter', () => { + setCursor('pointer') + }) + this.on('mouseout', () => { + setCursor('') + }) + this.image = new Konva.Image({ width: 20, height: 20, cornerRadius: 10, - image: configorTagImg, - rotation: 0 + + image: configorTagImg }) this.add(this.rect, this.image) @@ -255,8 +268,11 @@ export class CustomTransformer extends Konva.Transformer { this.add(this.rotatorTag) this.configorButton = new ConfigorButton() - this.configorButton.visible(true) this.add(this.configorButton) + this.configorButton.on('click', () => { + const node = this.getNode() + node.fire('openconfigor', { bubbles: true }) + }) const rotator = this.children.find((n) => n.name().match(/rotater/)) if (!(rotator instanceof Konva.Rect)) { @@ -309,6 +325,7 @@ export class CustomTransformer extends Konva.Transformer { this.configorButton.x(this.width() / 2 - 10) this.configorButton.y(this.height() + 4) + this.configorButton.visible(true) if (this.rotationStyle() === 'normal') { this.rotateEnabled(true) diff --git a/spx-gui/src/components/editor/common/viewer/quick-config/SpriteQuickConfig.vue b/spx-gui/src/components/editor/common/viewer/quick-config/SpriteQuickConfig.vue new file mode 100644 index 000000000..6af5c7ac8 --- /dev/null +++ b/spx-gui/src/components/editor/common/viewer/quick-config/SpriteQuickConfig.vue @@ -0,0 +1,88 @@ + + + + + + + diff --git a/spx-gui/src/components/editor/common/viewer/quick-config/widgets/ConfigPanel.vue b/spx-gui/src/components/editor/common/viewer/quick-config/widgets/ConfigPanel.vue new file mode 100644 index 000000000..44fb0eb9a --- /dev/null +++ b/spx-gui/src/components/editor/common/viewer/quick-config/widgets/ConfigPanel.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/spx-gui/src/components/editor/common/viewer/quick-config/widgets/DefaultConfig.vue b/spx-gui/src/components/editor/common/viewer/quick-config/widgets/DefaultConfig.vue new file mode 100644 index 000000000..c9eaa3fa8 --- /dev/null +++ b/spx-gui/src/components/editor/common/viewer/quick-config/widgets/DefaultConfig.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue b/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue index c69b2e463..7876e05d7 100644 --- a/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue +++ b/spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue @@ -46,6 +46,7 @@ @drag-move="handleSpriteDragMove" @drag-end="handleSpriteDragEnd" @selected="handleSpriteSelected(sprite)" + @open-configor="handleOpenConfigor" /> @@ -66,6 +67,7 @@ /> + | null>(null) +function handleOpenConfigor(e: ConfigType) { + spriteQuickConfigRef.value?.open(e) +} + const menuVisible = ref(false) const menuPos = ref({ x: 0, y: 0 }) diff --git a/spx-gui/src/components/editor/sprite/SpriteItem.vue b/spx-gui/src/components/editor/sprite/SpriteItem.vue index c0e2c8a4e..e6dde6b12 100644 --- a/spx-gui/src/components/editor/sprite/SpriteItem.vue +++ b/spx-gui/src/components/editor/sprite/SpriteItem.vue @@ -54,7 +54,7 @@ const radarNodeMeta = computed(() => { function toggleSpriteVisible() { const name = props.sprite.name - const action = { name: { en: `Configure sprite ${name}`, zh: `修改精灵 ${name} 配置` } } + const action = { name: { en: `Toggle visibility for sprite ${name}`, zh: `切换精灵 ${name} 的可见性` } } editorCtx.project.history.doAction(action, () => props.sprite.setVisible(!props.sprite.visible)) } diff --git a/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue b/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue index 553aa8100..3030deae0 100644 --- a/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue +++ b/spx-gui/src/components/ui/block-items/UIEditorSpriteItem.vue @@ -3,7 +3,7 @@ {{ name }} -