Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b033fa9
feat(spx-gui): add a configurable button to the custom transformer
Overu Dec 5, 2025
c772a3d
feat(spx-gui): add SpriteItem title visible icon
Overu Dec 7, 2025
7b0524f
feat(spx-gui): add sprite quick configuration UI
Overu Dec 8, 2025
50ab652
feat(spx-gui): complete sprite quick config UI
Overu Dec 9, 2025
43885ce
refactor(spx-gui): implement a new quick configuration system for wid…
Overu Dec 10, 2025
58ba5d4
refactor(spx-gui): move update handling logic into SpriteNode and Mon…
Overu Dec 10, 2025
47f7d75
add keyboard movement sprite
Overu Dec 10, 2025
f11c745
feat(spx-gui): the movement of a graphic element (sprite) controlled …
Overu Dec 11, 2025
803910b
refactor(spx-gui): remove sprite basic config from editor panel
Overu Dec 11, 2025
4e0e975
style(spx-gui): replace transformer icon
Overu Dec 11, 2025
d494815
refactor(spx-gui): extract common config items and rename panels for …
Overu Dec 11, 2025
120d039
feat(spx-gui): add quick config to MapViwer
Overu Dec 12, 2025
ec4b8d7
refactor(spx-gui): modify QuickConfig style
Overu Dec 12, 2025
7c8c914
refactor(spx-gui): replace custom-transformer icons
Overu Dec 15, 2025
09cc994
feat(spx-gui): make QuickConfig follow sprites
Overu Dec 15, 2025
275527b
refactor(spx-gui): fix QuickConfig centering
Overu Dec 15, 2025
5a47f4d
refactor(spx-gui): refactor config type handling to pass specific data
Overu Dec 16, 2025
75932de
refactor(spx-gui): fix config UI positioning for transformed nodes
Overu Dec 16, 2025
dfae37b
refactor(spx-gui): refine quick config initialization and interaction
Overu Dec 16, 2025
483d989
refactor(spx-gui): remove transformer config
Overu Dec 17, 2025
fdb7a5f
refactor(spx-gui): enhance quick config stability with cleanup logic
Overu Dec 17, 2025
4138675
refactor(spx-gui): clean up code and add comments
Overu Dec 18, 2025
ec51326
refactor(spx-gui): improve data flow in QuickConfig
Overu Dec 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions spx-gui/src/components/editor/common/AnglePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { computed, ref, watch } from 'vue'
import { useDraggableAngleForElement } from '@/utils/dom'
import { makeArcPathString } from '@/utils/svg'
import { nomalizeDegree, useDebouncedModel } from '@/utils/utils'
import { normalizeDegree, useDebouncedModel } from '@/utils/utils'
import { specialDirections } from '@/utils/spx'
import { UITag } from '@/components/ui'

Expand All @@ -16,7 +16,7 @@ const emit = defineEmits<{

const [modelValue] = useDebouncedModel<number>(
() => props.modelValue,
(v) => emit('update:modelValue', nomalizeDegree(Math.floor(v)))
(v) => emit('update:modelValue', normalizeDegree(Math.floor(v)))
)

const svgEl = ref<HTMLElement | null>(null)
Expand All @@ -26,7 +26,7 @@ const arcPath = computed(() => {
return makeArcPathString({ x: 70, y: 70, r: 63, start, end })
})
const angle = useDraggableAngleForElement(svgEl, { initialValue: props.modelValue, snap: 15 })
watch(angle, (v) => (modelValue.value = nomalizeDegree(v)))
watch(angle, (v) => (modelValue.value = normalizeDegree(v)))
</script>

<template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import { headingToLeftRight, LeftRight, leftRightToHeading, RotationStyle, type
import { wrapUpdateHandler } from '../utils'

import AnglePicker from '@/components/editor/common/AnglePicker.vue'
import { UIButtonGroup, UIButtonGroupItem, UIDropdown, UINumberInput, UITooltip } from '@/components/ui'
import rotateIcon from './rotate.svg?raw'
import leftRightIcon from './left-right.svg?raw'
import noRotateIcon from './no-rotate.svg?raw'
import { UIButtonGroup, UIButtonGroupItem, UIDropdown, UIIcon, UINumberInput, UITooltip } from '@/components/ui'

const props = defineProps<{
sprite: Sprite
Expand Down Expand Up @@ -71,23 +68,23 @@ const handleHeadingUpdate = wrapUpdateHandler(
{{ $t(rotationStyleTips.normal) }}
<template #trigger>
<UIButtonGroupItem :value="RotationStyle.Normal">
<i class="rotation-icon" v-html="rotateIcon"></i>
<UIIcon type="rotateAround" />
</UIButtonGroupItem>
</template>
</UITooltip>
<UITooltip>
{{ $t(rotationStyleTips.leftRight) }}
<template #trigger>
<UIButtonGroupItem :value="RotationStyle.LeftRight">
<i class="rotation-icon" v-html="leftRightIcon"></i>
<UIIcon type="leftRight" />
</UIButtonGroupItem>
</template>
</UITooltip>
<UITooltip>
{{ $t(rotationStyleTips.none) }}
<template #trigger>
<UIButtonGroupItem :value="RotationStyle.None">
<i class="rotation-icon" v-html="noRotateIcon"></i>
<UIIcon type="notRotate" />
</UIButtonGroupItem>
</template>
</UITooltip>
Expand Down Expand Up @@ -148,14 +145,4 @@ const handleHeadingUpdate = wrapUpdateHandler(
align-items: center;
gap: 12px;
}

.rotation-icon {
display: flex;
width: 16px;
height: 16px;
:deep(svg) {
width: 100%;
height: 100%;
}
}
</style>

This file was deleted.

This file was deleted.

3 changes: 0 additions & 3 deletions spx-gui/src/components/editor/common/config/sprite/rotate.svg

This file was deleted.

4 changes: 2 additions & 2 deletions spx-gui/src/components/editor/common/viewer/DecoratorNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed } from 'vue'
import type { ImageConfig } from 'konva/lib/shapes/Image'
import type { Size } from '@/models/common'
import type { Decorator } from '@/models/tilemap'
import { nomalizeDegree } from '@/utils/utils'
import { normalizeDegree } from '@/utils/utils'
import { useFileImg } from '@/utils/file'

const props = defineProps<{
Expand All @@ -24,7 +24,7 @@ const config = computed<ImageConfig>(() => {
offsetY: pivot.y,
x: props.mapSize.width / 2 + position.x,
y: props.mapSize.height / 2 - position.y,
rotation: nomalizeDegree(rotation - 90),
rotation: normalizeDegree(rotation - 90),
scaleX: scale.x,
scaleY: scale.y
} satisfies ImageConfig
Expand Down
32 changes: 30 additions & 2 deletions spx-gui/src/components/editor/common/viewer/NodeTransformer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
</template>

<script setup lang="ts">
import { computed, effect, nextTick, ref } from 'vue'
import { computed, nextTick, ref, watchEffect } from 'vue'
import type { Node } from 'konva/lib/Node'
import { Sprite } from '@/models/sprite'
import type { Widget } from '@/models/widget'
import type { CustomTransformer, CustomTransformerConfig } from './custom-transformer'
import { getNodeId } from './common'
import { debounce } from 'lodash'

const props = defineProps<{
target: Sprite | Widget | null
Expand All @@ -30,7 +31,15 @@ const config = computed<CustomTransformerConfig>(() => {
}
})

effect(async () => {
const keyboardMovementCodes = ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowLeft']
const keyboardMovementOffset = [
[0, -1],
[1, 0],
[0, 1],
[-1, 0]
]

watchEffect(async (onCleanup) => {
if (transformer.value == null) return
const transformerNode = transformer.value.getNode()
transformerNode.nodes([])
Expand All @@ -44,6 +53,25 @@ effect(async () => {
if (selectedNode == null || selectedNode === (transformerNode as any).node()) return
await nextTick() // Wait to ensure the selected node updated by Konva
transformerNode.nodes([selectedNode])

// keyboard
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: 这部分逻辑可以考虑定义为一个单独的函数,然后在这里调用?

stage.container().tabIndex = 1
stage.container().focus()
const keyboardMovementEnd = debounce(() => selectedNode.fire('transformend'), 500)
const handler = (e: KeyboardEvent) => {
const idx = keyboardMovementCodes.indexOf(e.code)
if (idx === -1) return
selectedNode.x(selectedNode.x() + keyboardMovementOffset[idx][0])
selectedNode.y(selectedNode.y() + keyboardMovementOffset[idx][1])
selectedNode.fire('transform')
e.preventDefault()
keyboardMovementEnd()
}
stage.container().addEventListener('keydown', handler)
onCleanup(() => {
keyboardMovementEnd.cancel()
stage.container().removeEventListener('keydown', handler)
})
})

defineExpose({
Expand Down
109 changes: 100 additions & 9 deletions spx-gui/src/components/editor/common/viewer/SpriteNode.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { computed, onMounted, ref, watchEffect } from 'vue'
import type { Stage } from 'konva/lib/Stage'
import type { Shape } from 'konva/lib/Shape'
import type { KonvaEventObject } from 'konva/lib/Node'
import type { Image, ImageConfig } from 'konva/lib/shapes/Image'
import type { Action, Project } from '@/models/project'
import { LeftRight, RotationStyle, headingToLeftRight, leftRightToHeading, type Sprite } from '@/models/sprite'
import type { Size } from '@/models/common'
import { nomalizeDegree, round, useAsyncComputedLegacy } from '@/utils/utils'
import { normalizeDegree, round, useAsyncComputedLegacy } from '@/utils/utils'
import { useFileImg } from '@/utils/file'
import { cancelBubble, getNodeId } from './common'

Expand All @@ -26,6 +28,9 @@ const emit = defineEmits<{
selected: []
dragMove: [notifyCameraScroll: CameraScrollNotifyFn]
dragEnd: []
updatePos: [{ x: number; y: number }]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mark

updateHeading: [{ heading: number; leftRight?: LeftRight }]
updateSize: [{ size: number }]
}>()

const nodeRef = ref<KonvaNodeInstance<Image>>()
Expand Down Expand Up @@ -54,8 +59,60 @@ onMounted(() => {
}
})

function updateSprite({
oldSize,
size,
oldHeading,
heading,
oldX,
x,
oldY,
y
}: {
oldSize: number
size: number
oldHeading: number
heading: number
oldX: number
x: number
oldY: number
y: number
}) {
if (oldSize !== size) {
emit('updateSize', { size })
return
}
if (oldHeading !== heading && props.sprite.rotationStyle !== RotationStyle.None) {
let leftRight: LeftRight | undefined = undefined
if (props.sprite.rotationStyle === RotationStyle.LeftRight) {
leftRight = headingToLeftRight(heading)
}
emit('updateHeading', { heading, leftRight })
return
}
if (oldX !== x || oldY !== y) {
emit('updatePos', { x, y })
}
}

const notifyUpdateSprite = (node: Shape | Stage) => {
if (!props.selected) return
const { x, y } = toPosition(node)
updateSprite({
oldSize: props.sprite.size,
size: toSize(node),
oldHeading: props.sprite.heading,
heading: toHeading(node),
oldX: props.sprite.x,
x,
oldY: props.sprite.y,
y
})
}

function handleDragMove(e: KonvaEventObject<unknown>) {
cancelBubble(e)
notifyUpdateSprite(e.target)
emit('dragMove', (delta) => {
// Adjust position if camera scrolled during dragging to keep the sprite visually unmoved
e.target.x(e.target.x() - delta.x)
Expand All @@ -69,6 +126,7 @@ function handleDragEnd(e: KonvaEventObject<unknown>) {
handleChange(e, {
name: { en: `Move sprite ${sname}`, zh: `移动精灵 ${sname}` }
})
notifyUpdateSprite(e.target)
emit('dragEnd')
}

Expand All @@ -77,6 +135,7 @@ function handleTransformed(e: KonvaEventObject<unknown>) {
handleChange(e, {
name: { en: `Transform sprite ${sname}`, zh: `调整精灵 ${sname}` }
})
notifyUpdateSprite(e.target)
}

const config = computed<ImageConfig>(() => {
Expand All @@ -94,7 +153,7 @@ const config = computed<ImageConfig>(() => {
visible: visible,
x: props.mapSize.width / 2 + x,
y: props.mapSize.height / 2 - y,
rotation: nomalizeDegree(heading - 90),
rotation: normalizeDegree(heading - 90),
scaleX: scale,
scaleY: scale
} satisfies ImageConfig
Expand All @@ -105,19 +164,50 @@ const config = computed<ImageConfig>(() => {
// Note that you can get the same result with `ratation: 0, scaleX: -scaleX` here, but there will be problem
// if the user then do transform with transformer. Konva transformer prefers to make `scaleX` positive.
}
// After Sprite changes, updateXXX events also need to be triggered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Side effects in computed property

This updateSprite() call inside the computed property violates Vue's computed property best practices (should be pure functions). The updates are already handled by event handlers (@dragmove, @dragend, @transform, @transformend), making this redundant.

Recommendation: Remove lines 167-182. The event handlers already provide proper update notifications.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里说的没毛病,computed 里不应该产生副作用

const node = nodeRef.value?.getNode()
if (node != null) {
const { x: oldX, y: oldY } = toPosition(node)
const oldHeading = toHeading(node)
const oldSize = toSize(node)
updateSprite({
oldSize,
size,
oldHeading,
heading,
oldX,
x,
oldY,
y
})
}
return config
})

/** Handler for position-change (drag) or transform */
function handleChange(e: KonvaEventObject<unknown>, action: Action) {
const { sprite, mapSize } = props
const x = round(e.target.x() - mapSize.width / 2)
const y = round(mapSize.height / 2 - e.target.y())
function toPosition(node: Shape | Stage) {
const { mapSize } = props
const x = round(node.x() - mapSize.width / 2)
const y = round(mapSize.height / 2 - node.y())
return { x, y }
}
function toHeading(node: Shape | Stage) {
const { sprite } = props
let heading = sprite.heading
if (sprite.rotationStyle === RotationStyle.Normal || sprite.rotationStyle === RotationStyle.LeftRight) {
heading = nomalizeDegree(round(e.target.rotation() + 90))
heading = normalizeDegree(round(node.rotation() + 90))
}
const size = round(Math.abs(e.target.scaleX()) * bitmapResolution.value, 2)
return heading
}
function toSize(node: Shape | Stage) {
const size = round(Math.abs(node.scaleX()) * bitmapResolution.value, 2)
return size
}
/** Handler for position-change (drag) or transform */
function handleChange(e: KonvaEventObject<unknown>, action: Action) {
const { sprite } = props
const { x, y } = toPosition(e.target)
const heading = toHeading(e.target)
const size = toSize(e.target)
props.project.history.doAction(action, () => {
sprite.setX(x)
sprite.setY(y)
Expand All @@ -137,6 +227,7 @@ function handleClick() {
:config="config"
@dragmove="handleDragMove"
@dragend="handleDragEnd"
@transform="notifyUpdateSprite($event.target)"
@transformend="handleTransformed"
@click="handleClick"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import transformerFlipArrowDisabledPng from './transformer-flip-arrow-disabled.p
import rotatorCirclePng from './rotate-circle.png'
import type { RectConfig } from 'konva/lib/shapes/Rect'
import type { ImageConfig } from 'konva/lib/shapes/Image'
import { nomalizeDegree, round } from '@/utils/utils'
import { normalizeDegree, round } from '@/utils/utils'

// There seems to be an issue rendering svg Image.
// We are using 2x png here.
Expand Down Expand Up @@ -51,7 +51,7 @@ class RotatorTag extends Konva.Group {
}

updateRotationNumber(rotationNumber: number) {
this.text.text(`${nomalizeDegree(round(rotationNumber + 90))}°`)
this.text.text(`${normalizeDegree(round(rotationNumber + 90))}°`)
}
}

Expand Down Expand Up @@ -88,7 +88,7 @@ class FlipButton extends Konva.Group {
strokeWidth: 0.5
}
const imageStyle: Partial<ImageConfig> = {
width: 4,
width: 6,
height: 8
}
this.rect = new Konva.Rect({
Expand All @@ -113,7 +113,7 @@ class FlipButton extends Konva.Group {
...imageStyle,
image: enabled ? transformerFlipArrowImg : transformerFlipArrowDisabledImg,
rotation: enabled ? 180 : 0,
x: enabled ? 11 : 9,
x: enabled ? 12 : 9,
y: enabled ? 14 : 6
})
this.image.on('mouseenter', () => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading