Skip to content

Commit 264a71b

Browse files
committed
feat(spx-gui): complete sprite quick config UI
1 parent c77e0a1 commit 264a71b

File tree

17 files changed

+404
-55
lines changed

17 files changed

+404
-55
lines changed

spx-gui/src/components/editor/common/config/sprite/SpriteDirection.vue

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import { headingToLeftRight, LeftRight, leftRightToHeading, RotationStyle, type
77
import { wrapUpdateHandler } from '../utils'
88
99
import AnglePicker from '@/components/editor/common/AnglePicker.vue'
10-
import { UIButtonGroup, UIButtonGroupItem, UIDropdown, UINumberInput, UITooltip } from '@/components/ui'
11-
import rotateIcon from './rotate.svg?raw'
12-
import leftRightIcon from './left-right.svg?raw'
13-
import noRotateIcon from './no-rotate.svg?raw'
10+
import { UIButtonGroup, UIButtonGroupItem, UIDropdown, UIIcon, UINumberInput, UITooltip } from '@/components/ui'
1411
1512
const props = defineProps<{
1613
sprite: Sprite
@@ -67,23 +64,23 @@ const handleHeadingUpdate = wrapUpdateHandler((h: number | null) => props.sprite
6764
{{ $t(rotationStyleTips.normal) }}
6865
<template #trigger>
6966
<UIButtonGroupItem :value="RotationStyle.Normal">
70-
<i class="rotation-icon" v-html="rotateIcon"></i>
67+
<UIIcon type="rotateAround" />
7168
</UIButtonGroupItem>
7269
</template>
7370
</UITooltip>
7471
<UITooltip>
7572
{{ $t(rotationStyleTips.leftRight) }}
7673
<template #trigger>
7774
<UIButtonGroupItem :value="RotationStyle.LeftRight">
78-
<i class="rotation-icon" v-html="leftRightIcon"></i>
75+
<UIIcon type="leftRight" />
7976
</UIButtonGroupItem>
8077
</template>
8178
</UITooltip>
8279
<UITooltip>
8380
{{ $t(rotationStyleTips.none) }}
8481
<template #trigger>
8582
<UIButtonGroupItem :value="RotationStyle.None">
86-
<i class="rotation-icon" v-html="noRotateIcon"></i>
83+
<UIIcon type="notRotate" />
8784
</UIButtonGroupItem>
8885
</template>
8986
</UITooltip>
@@ -144,14 +141,4 @@ const handleHeadingUpdate = wrapUpdateHandler((h: number | null) => props.sprite
144141
align-items: center;
145142
gap: 12px;
146143
}
147-
148-
.rotation-icon {
149-
display: flex;
150-
width: 16px;
151-
height: 16px;
152-
:deep(svg) {
153-
width: 100%;
154-
height: 100%;
155-
}
156-
}
157144
</style>

spx-gui/src/components/editor/common/config/sprite/left-right.svg

Lines changed: 0 additions & 5 deletions
This file was deleted.

spx-gui/src/components/editor/common/config/sprite/no-rotate.svg

Lines changed: 0 additions & 3 deletions
This file was deleted.

spx-gui/src/components/editor/common/config/sprite/rotate.svg

Lines changed: 0 additions & 3 deletions
This file was deleted.

spx-gui/src/components/editor/common/viewer/quick-config/widgets/ConfigPanel.vue renamed to spx-gui/src/components/editor/common/viewer/quick-config/ConfigPanel.vue

File renamed without changes.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<script lang="ts" setup>
2+
import { UIDropdown, UIIcon, UIMenu, UIMenuItem, UITooltip, type IconType } from '@/components/ui'
3+
import ConfigPanel from './ConfigPanel.vue'
4+
import { headingToLeftRight, leftRightToHeading, RotationStyle, type Sprite } from '@/models/sprite'
5+
import type { Project } from '@/models/project'
6+
import { wrapUpdateHandler } from '@/components/editor/common/config/utils'
7+
import type { LocaleMessage } from '@/utils/i18n'
8+
9+
const props = defineProps<{
10+
sprite: Sprite
11+
project: Project
12+
}>()
13+
14+
const rotationStyleTips = {
15+
[RotationStyle.Normal]: {
16+
icon: 'rotateAround',
17+
tips: {
18+
en: 'Normal: the sprite can be rotated to any heading',
19+
zh: '正常旋转:精灵可以被旋转到任意方向'
20+
}
21+
},
22+
[RotationStyle.LeftRight]: {
23+
icon: 'leftRight',
24+
tips: {
25+
en: 'Left-Right: the sprite can only be flipped horizontally',
26+
zh: '左右翻转:精灵只可以在水平方向翻转'
27+
}
28+
},
29+
[RotationStyle.None]: {
30+
icon: 'notRotate',
31+
tips: {
32+
en: "Don't Rotate: the sprite will not be rotated",
33+
zh: '不旋转:精灵不会被旋转'
34+
}
35+
}
36+
} satisfies Record<RotationStyle, { icon: IconType; tips: LocaleMessage }>
37+
38+
const spriteContext = () => ({
39+
sprite: props.sprite,
40+
project: props.project
41+
})
42+
43+
const handleRotationStyleUpdate = wrapUpdateHandler(
44+
(style: RotationStyle) => {
45+
props.sprite.setRotationStyle(style)
46+
if (style === RotationStyle.None) props.sprite.setHeading(90)
47+
if (style === RotationStyle.LeftRight) {
48+
// normalize heading to 90 / -90
49+
const normalizedHeading = leftRightToHeading(headingToLeftRight(props.sprite.heading))
50+
props.sprite.setHeading(normalizedHeading)
51+
}
52+
},
53+
spriteContext,
54+
false
55+
)
56+
57+
const moveActionNames = {
58+
up: { en: 'Bring forward', zh: '向前移动' },
59+
top: { en: 'Bring to front', zh: '移到最前' },
60+
down: { en: 'Send backward', zh: '向后移动' },
61+
bottom: { en: 'Send to back', zh: '移到最后' }
62+
}
63+
64+
async function moveZorder(direction: 'up' | 'down' | 'top' | 'bottom') {
65+
await props.project.history.doAction({ name: moveActionNames[direction] }, () => {
66+
const { sprite, project } = props
67+
if (direction === 'up') {
68+
project.upSpriteZorder(sprite.id)
69+
} else if (direction === 'down') {
70+
project.downSpriteZorder(sprite.id)
71+
} else if (direction === 'top') {
72+
project.topSpriteZorder(sprite.id)
73+
} else if (direction === 'bottom') {
74+
project.bottomSpriteZorder(sprite.id)
75+
}
76+
})
77+
}
78+
</script>
79+
80+
<!-- eslint-disable vue/no-v-html -->
81+
<template>
82+
<ConfigPanel v-radar="{ name: 'Rotation style control', desc: 'Control to set sprite rotation style' }">
83+
<div class="default-config-wrapper">
84+
<UITooltip v-for="(value, key) in rotationStyleTips" :key="key">
85+
{{ $t(value.tips) }}
86+
<template #trigger>
87+
<div
88+
class="config-item"
89+
:class="{ active: props.sprite.rotationStyle === key }"
90+
@click="handleRotationStyleUpdate(key)"
91+
>
92+
<UIIcon class="icon" :type="value.icon" />
93+
</div>
94+
</template>
95+
</UITooltip>
96+
<UIDropdown trigger="click" placement="top">
97+
<template #trigger>
98+
<div class="config-item">
99+
<UIIcon class="icon" type="layer" />
100+
</div>
101+
</template>
102+
<UIMenu>
103+
<UIMenuItem
104+
v-radar="{ name: 'Move up', desc: 'Click to move sprite up in z-order' }"
105+
@click="moveZorder('up')"
106+
>{{ $t(moveActionNames.up) }}</UIMenuItem
107+
>
108+
<UIMenuItem
109+
v-radar="{ name: 'Move to top', desc: 'Click to move sprite to top in z-order' }"
110+
@click="moveZorder('top')"
111+
>{{ $t(moveActionNames.top) }}</UIMenuItem
112+
>
113+
<UIMenuItem
114+
v-radar="{ name: 'Move down', desc: 'Click to move sprite down in z-order' }"
115+
@click="moveZorder('down')"
116+
>{{ $t(moveActionNames.down) }}</UIMenuItem
117+
>
118+
<UIMenuItem
119+
v-radar="{ name: 'Move to bottom', desc: 'Click to move sprite to bottom in z-order' }"
120+
@click="moveZorder('bottom')"
121+
>{{ $t(moveActionNames.bottom) }}</UIMenuItem
122+
>
123+
</UIMenu>
124+
</UIDropdown>
125+
</div>
126+
</ConfigPanel>
127+
</template>
128+
129+
<style lang="scss" scoped>
130+
.default-config-wrapper {
131+
display: flex;
132+
gap: 4px;
133+
134+
.config-item {
135+
width: 32px;
136+
height: 32px;
137+
text-align: center;
138+
display: flex;
139+
align-items: center;
140+
justify-content: center;
141+
border-radius: 10px;
142+
cursor: pointer;
143+
144+
&:hover,
145+
&.active {
146+
background: var(--ui-color-turquoise-200);
147+
148+
.icon {
149+
color: var(--ui-color-turquoise-500);
150+
}
151+
}
152+
}
153+
}
154+
</style>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
import { UIDropdown, UINumberInput } from '@/components/ui'
4+
import ConfigPanel from './ConfigPanel.vue'
5+
import { RotationStyle, type Sprite } from '@/models/sprite'
6+
import type { Project } from '@/models/project'
7+
import { wrapUpdateHandler } from '@/components/editor/common/config/utils'
8+
import AnglePicker from '@/components/editor/common/AnglePicker.vue'
9+
10+
const props = defineProps<{
11+
sprite: Sprite
12+
project: Project
13+
}>()
14+
15+
const spriteContext = () => ({
16+
sprite: props.sprite,
17+
project: props.project
18+
})
19+
20+
const rotateDropdownVisible = ref(false)
21+
const handleHeadingUpdate = wrapUpdateHandler((h: number | null) => props.sprite.setHeading(h ?? 0), spriteContext)
22+
</script>
23+
24+
<template>
25+
<ConfigPanel>
26+
<UIDropdown
27+
v-if="sprite.rotationStyle !== RotationStyle.LeftRight"
28+
trigger="manual"
29+
placement="top"
30+
:visible="rotateDropdownVisible"
31+
:disabled="sprite.rotationStyle === RotationStyle.None"
32+
@click-outside="rotateDropdownVisible = false"
33+
>
34+
<template #trigger>
35+
<UINumberInput
36+
v-radar="{ name: 'Heading input', desc: 'Input to set sprite heading angle' }"
37+
class="heading-input"
38+
:disabled="sprite.rotationStyle === RotationStyle.None"
39+
:min="-180"
40+
:max="180"
41+
:value="sprite.heading"
42+
@update:value="handleHeadingUpdate"
43+
@focus="rotateDropdownVisible = true"
44+
>
45+
<template #prefix
46+
><span class="label">{{ $t({ en: 'Heading', zh: '朝向' }) }}</span>
47+
</template>
48+
</UINumberInput>
49+
</template>
50+
<div class="rotation-heading-container">
51+
<AnglePicker :model-value="sprite.heading" @update:model-value="handleHeadingUpdate" />
52+
</div>
53+
</UIDropdown>
54+
</ConfigPanel>
55+
</template>
56+
57+
<style lang="scss" scoped>
58+
.heading-input {
59+
width: 130px;
60+
61+
.label {
62+
margin-right: 8px;
63+
}
64+
}
65+
66+
.rotation-heading-container {
67+
padding: 12px;
68+
}
69+
</style>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts" setup>
2+
import { UINumberInput } from '@/components/ui'
3+
import ConfigPanel from './ConfigPanel.vue'
4+
import { type Sprite } from '@/models/sprite'
5+
import type { Project } from '@/models/project'
6+
import { wrapUpdateHandler } from '@/components/editor/common/config/utils'
7+
8+
const props = defineProps<{
9+
sprite: Sprite
10+
project: Project
11+
}>()
12+
13+
const spriteContext = () => ({
14+
sprite: props.sprite,
15+
project: props.project
16+
})
17+
18+
const handleXUpdate = wrapUpdateHandler((x: number | null) => props.sprite.setX(x ?? 0), spriteContext)
19+
const handleYUpdate = wrapUpdateHandler((y: number | null) => props.sprite.setY(y ?? 0), spriteContext)
20+
</script>
21+
22+
<template>
23+
<ConfigPanel>
24+
<div class="position-config-wrapper">
25+
<UINumberInput
26+
v-radar="{ name: 'X position input', desc: 'Input to set sprite X position' }"
27+
:value="sprite.x"
28+
@update:value="handleXUpdate"
29+
>
30+
<template #prefix>X</template>
31+
</UINumberInput>
32+
<UINumberInput
33+
v-radar="{ name: 'Y position input', desc: 'Input to set sprite Y position' }"
34+
:value="sprite.y"
35+
@update:value="handleYUpdate"
36+
>
37+
<template #prefix>Y</template>
38+
</UINumberInput>
39+
</div>
40+
</ConfigPanel>
41+
</template>
42+
43+
<style lang="scss" scoped>
44+
.position-config-wrapper {
45+
display: flex;
46+
gap: 4px;
47+
width: 158px;
48+
}
49+
</style>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts" setup>
2+
import { UINumberInput } from '@/components/ui'
3+
import ConfigPanel from './ConfigPanel.vue'
4+
import type { Sprite } from '@/models/sprite'
5+
import type { Project } from '@/models/project'
6+
import { computed } from 'vue'
7+
import { round } from '@/utils/utils'
8+
import { wrapUpdateHandler } from '@/components/editor/common/config/utils'
9+
10+
const props = defineProps<{
11+
sprite: Sprite
12+
project: Project
13+
}>()
14+
15+
const spriteContext = () => ({
16+
sprite: props.sprite,
17+
project: props.project
18+
})
19+
20+
const sizePercent = computed(() => round(props.sprite.size * 100))
21+
const handleSizePercentUpdate = wrapUpdateHandler((sizeInPercent: number | null) => {
22+
if (sizeInPercent == null) return
23+
props.sprite.setSize(round(sizeInPercent / 100, 2))
24+
}, spriteContext)
25+
</script>
26+
27+
<template>
28+
<ConfigPanel>
29+
<UINumberInput
30+
v-radar="{ name: 'Size input', desc: 'Input to set sprite size percentage' }"
31+
class="size-input"
32+
:min="0"
33+
:value="sizePercent"
34+
@update:value="handleSizePercentUpdate"
35+
>
36+
<template #prefix
37+
><span class="label">{{ $t({ en: 'Size', zh: '大小' }) }}</span></template
38+
>
39+
<template #suffix>%</template>
40+
</UINumberInput>
41+
</ConfigPanel>
42+
</template>
43+
44+
<style lang="scss" scoped>
45+
.size-input {
46+
width: 102px;
47+
48+
.label {
49+
margin-right: 8px;
50+
}
51+
}
52+
</style>

0 commit comments

Comments
 (0)