Skip to content

Commit ae4ffa3

Browse files
committed
refactor(spx-gui): refine sprite/widget transformation logic and fix jittering
1 parent 424e912 commit ae4ffa3

File tree

7 files changed

+207
-124
lines changed

7 files changed

+207
-124
lines changed

spx-gui/src/components/editor/common/viewer/NodeTransformer.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ function setupKeyboardMovement(stage: Konva.Stage, selectedNode: Node) {
4444
stage.container().tabIndex = 1
4545
stage.container().focus()
4646
stage.container().style.outline = 'none'
47-
const keyboardMovementEnd = debounce(() => selectedNode.fire('transformend'), 500)
47+
const keyboardMovementEnd = debounce(() => selectedNode.fire('nodeupdated', { evt: { op: 'move' } }), 500)
4848
const handler = (e: KeyboardEvent) => {
4949
const idx = keyboardMovementCodes.indexOf(e.code)
5050
if (idx === -1) return
5151
selectedNode.x(selectedNode.x() + keyboardMovementOffset[idx][0])
5252
selectedNode.y(selectedNode.y() + keyboardMovementOffset[idx][1])
53-
selectedNode.fire('transform')
53+
selectedNode.fire('nodeupdating', { evt: { op: 'move' } })
5454
e.preventDefault()
5555
keyboardMovementEnd()
5656
}

spx-gui/src/components/editor/common/viewer/SpriteNode.vue

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { normalizeDegree, round, useAsyncComputedLegacy } from '@/utils/utils'
1111
import { useFileImg } from '@/utils/file'
1212
import { cancelBubble, getNodeId } from './common'
1313
import type { SpriteLocalConfig } from './quick-config/utils'
14+
import type { NodeUpdateEvent, TransformOp } from './custom-transformer'
1415
1516
const props = defineProps<{
1617
sprite: Sprite
@@ -58,101 +59,112 @@ onMounted(() => {
5859
}
5960
})
6061
61-
function updateLocalConfig({
62-
oldX,
63-
x,
64-
oldY,
65-
y,
66-
oldSize,
67-
size,
68-
oldHeading,
69-
heading
70-
}: {
71-
oldX: number
72-
x: number
73-
oldY: number
74-
y: number
75-
oldSize: number
76-
size: number
77-
oldHeading: number
78-
heading: number
79-
}) {
62+
function updateLocalConfig(
63+
{
64+
oldX,
65+
x,
66+
oldY,
67+
y,
68+
oldSize,
69+
size,
70+
oldHeading,
71+
heading
72+
}: {
73+
oldX: number
74+
x: number
75+
oldY: number
76+
y: number
77+
oldSize: number
78+
size: number
79+
oldHeading: number
80+
heading: number
81+
},
82+
op: TransformOp
83+
) {
8084
const spriteLocalConfig = props.localConfig
8185
if (spriteLocalConfig == null) return
8286
if (size !== oldSize) {
83-
spriteLocalConfig.setSize(size)
84-
return
87+
spriteLocalConfig.setSize(size, op === 'scale')
8588
}
86-
if (heading !== oldHeading && spriteLocalConfig.rotationStyle === RotationStyle.Normal) {
87-
spriteLocalConfig.setHeading(heading)
88-
return
89+
if (heading !== oldHeading) {
90+
spriteLocalConfig.setHeading(heading, op === 'rotate')
8991
}
9092
if (x !== oldX || y !== oldY) {
91-
spriteLocalConfig.setX(x)
92-
spriteLocalConfig.setY(y)
93+
spriteLocalConfig.setX(x, op === 'move')
94+
spriteLocalConfig.setY(y, op === 'move')
9395
}
9496
}
95-
function updateLocalConfigByShape(node: Shape | Stage) {
97+
function updateLocalConfigByShape(node: Shape | Stage, op: TransformOp) {
9698
if (!props.selected || props.localConfig == null) return
9799
const { x, y } = toPosition(node)
98-
updateLocalConfig({
99-
oldX: props.sprite.x,
100-
x,
101-
oldY: props.sprite.y,
102-
y,
103-
oldSize: props.sprite.size,
104-
size: toSize(node),
105-
oldHeading: props.sprite.heading,
106-
heading: toHeading(node)
107-
})
100+
updateLocalConfig(
101+
{
102+
oldX: props.sprite.x,
103+
x,
104+
oldY: props.sprite.y,
105+
y,
106+
oldSize: props.sprite.size,
107+
size: toSize(node),
108+
oldHeading: props.sprite.heading,
109+
heading: toHeading(node)
110+
},
111+
op
112+
)
108113
}
109114
110-
function syncLocalConfig({ size, x, y, heading }: { size: number; x: number; y: number; heading: number }) {
115+
function syncLocalConfig(
116+
{ size, x, y, heading }: { size: number; x: number; y: number; heading: number },
117+
op: TransformOp
118+
) {
111119
const spriteLocalConfig = props.localConfig
112120
if (spriteLocalConfig == null) return
121+
113122
if (size != null && props.sprite.size !== size) {
114-
spriteLocalConfig.setSize(size, false)
115-
spriteLocalConfig.syncSize()
116-
return
123+
spriteLocalConfig.setSize(size, op === 'scale')
117124
}
118125
if (props.sprite.heading !== heading) {
119-
spriteLocalConfig.setHeading(heading, false)
120-
spriteLocalConfig.syncHeading()
121-
return
126+
spriteLocalConfig.setHeading(heading, op === 'rotate')
122127
}
123128
if (props.sprite.x !== x || props.sprite.y !== y) {
124-
spriteLocalConfig.setX(x, false)
125-
spriteLocalConfig.setX(x, false)
126-
spriteLocalConfig.syncPos()
129+
spriteLocalConfig.setX(x, op === 'move')
130+
spriteLocalConfig.setY(y, op === 'move')
127131
}
132+
133+
spriteLocalConfig.syncAll()
128134
}
129-
function syncLocalConfigByShape(node: Shape | Stage) {
130-
syncLocalConfig({
131-
size: toSize(node),
132-
x: toPosition(node).x,
133-
y: toPosition(node).y,
134-
heading: toHeading(node)
135-
})
135+
function syncLocalConfigByShape(node: Shape | Stage, op: TransformOp) {
136+
syncLocalConfig(
137+
{
138+
size: toSize(node),
139+
x: toPosition(node).x,
140+
y: toPosition(node).y,
141+
heading: toHeading(node)
142+
},
143+
op
144+
)
136145
}
137146
138147
function handleDragMove(e: KonvaEventObject<unknown>) {
139148
cancelBubble(e)
140-
updateLocalConfigByShape(e.target)
149+
updateLocalConfigByShape(e.target, 'move')
141150
emit('dragMove', (delta) => {
142151
// Adjust position if camera scrolled during dragging to keep the sprite visually unmoved
143152
e.target.x(e.target.x() - delta.x)
144153
e.target.y(e.target.y() - delta.y)
145154
})
146155
}
147156
148-
function handleDragEnd(e: KonvaEventObject<unknown>) {
157+
function handleDragEnd(e: KonvaEventObject<TransformOp>) {
149158
cancelBubble(e)
150-
syncLocalConfigByShape(e.target)
159+
syncLocalConfigByShape(e.target, 'move')
151160
emit('dragEnd')
152161
}
153162
154-
function handleTransformed(e: KonvaEventObject<unknown>) {
155-
syncLocalConfigByShape(e.target)
163+
function handleNodeUpdating(e: KonvaEventObject<NodeUpdateEvent>) {
164+
updateLocalConfigByShape(e.target, e.evt.op)
165+
}
166+
function handleNodeUpdated(e: KonvaEventObject<NodeUpdateEvent>) {
167+
syncLocalConfigByShape(e.target, e.evt.op)
156168
}
157169
158170
const config = computed<ImageConfig>(() => {
@@ -214,8 +226,8 @@ function handleClick() {
214226
:config="config"
215227
@dragmove="handleDragMove"
216228
@dragend="handleDragEnd"
217-
@transform="updateLocalConfigByShape($event.target)"
218-
@transformend="handleTransformed"
229+
@nodeupdating="handleNodeUpdating"
230+
@nodeupdated="handleNodeUpdated"
219231
@click="handleClick"
220232
/>
221233
</template>

spx-gui/src/components/editor/common/viewer/custom-transformer/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,34 @@ transformerFlipArrowDisabledImg.src = transformerFlipArrowDisabledPng
1919
const rotatorCircleImg = new Image()
2020
rotatorCircleImg.src = rotatorCirclePng
2121

22+
export type TransformOp = 'rotate' | 'scale' | 'move' | 'flip'
23+
24+
export type NodeUpdateEvent = {
25+
op: TransformOp
26+
}
27+
2228
export type CustomTransformerConfig = {
2329
rotationStyle?: 'none' | 'normal' | 'left-right'
2430
} & Pick<TransformerConfig, 'centeredScaling'>
2531

32+
const scaleAnchors = [
33+
'top-left',
34+
'top-center',
35+
'top-right',
36+
'middle-left',
37+
'middle-right',
38+
'bottom-left',
39+
'bottom-center',
40+
'bottom-right'
41+
]
42+
function getTransformOp(transformer: Konva.Transformer): TransformOp | null {
43+
const activeAnchor = transformer.getActiveAnchor()
44+
if (activeAnchor == null) return null
45+
if (scaleAnchors.includes(activeAnchor)) return 'scale'
46+
if (activeAnchor === 'rotater') return 'rotate'
47+
return null
48+
}
49+
2650
class RotatorTag extends Konva.Group {
2751
text: Konva.Text
2852
constructor() {
@@ -200,7 +224,7 @@ export class CustomTransformer extends Konva.Transformer {
200224
// as it decides the flip based on the rotation only.
201225
node.scaleY(node.scaleY() * -1)
202226
node.rotation(node.rotation() - 180)
203-
node._fire('transformend', { target: node })
227+
node._fire('nodeupdated', { target: node, evt: { op: 'flip' } })
204228
})
205229

206230
const right = new FlipButton('right')
@@ -237,10 +261,17 @@ export class CustomTransformer extends Konva.Transformer {
237261
// Konva.Transformer resets the pointer to '', and we need to override that.
238262
setCursor('grabbing')
239263
})
264+
this.on('transform', () => {
265+
const node = this._nodes[0]
266+
node._fire('nodeupdating', { target: node, evt: { op: getTransformOp(this) } })
267+
})
240268
this.on('transformend', () => {
241269
this.rotatorTag.visible(false)
242270
setCursor('')
243271
dragging = false
272+
273+
const node = this._nodes[0]
274+
node._fire('nodeupdated', { target: node, evt: { op: getTransformOp(this) } })
244275
})
245276
this.on('rotationStyleChange', () => {
246277
this.update()

spx-gui/src/components/editor/common/viewer/quick-config/common/ZorderConfigItem.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function moveZorder(action: MoveAction) {
5959

6060
<style lang="scss" scoped>
6161
.item {
62-
width: max-content;
62+
width: 100%;
6363
white-space: nowrap;
6464
}
6565
</style>

spx-gui/src/components/editor/common/viewer/quick-config/sprite/DefaultConfigPanel.vue

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,16 @@ const rotationStyleTips = {
3939
4040
function handleRotationStyleUpdate(style: RotationStyle) {
4141
const localConfig = props.localConfig
42-
localConfig.syncToSprite((sprite) => {
43-
sprite.setRotationStyle(style)
44-
if (style === RotationStyle.None) {
45-
sprite.setHeading(90)
46-
}
47-
if (style === RotationStyle.LeftRight) {
48-
// normalize heading to 90 / -90
49-
const normalizedHeading = leftRightToHeading(headingToLeftRight(sprite.heading))
50-
sprite.setHeading(normalizedHeading)
51-
}
52-
})
42+
localConfig.setRotationStyle(style, false)
43+
if (style === RotationStyle.None) {
44+
localConfig.setHeading(90, false)
45+
}
46+
if (style === RotationStyle.LeftRight) {
47+
// normalize heading to 90 / -90
48+
const normalizedHeading = leftRightToHeading(headingToLeftRight(localConfig.heading))
49+
localConfig.setHeading(normalizedHeading, false)
50+
}
51+
localConfig.syncAll()
5352
}
5453
5554
async function moveZorder(direction: MoveAction) {

spx-gui/src/components/editor/common/viewer/quick-config/utils.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ export class LocalConfig extends Disposable {
7070
}
7171
}
7272

73+
// TODO: Temporary workaround: when rotating or scaling a Sprite/Widget, jittering may occur and cause position changes, which might trigger the 'pos' panel.
74+
// This fix has a limitation: if the current panel is 'rotate', it might not switch to the expected 'pos' panel after updating the Sprite/Widget.
75+
function shouldUpdatePosConfigType(configType: ConfigType | undefined) {
76+
return configType != null && !['size', 'rotate'].includes(configType)
77+
}
78+
7379
export class SpriteLocalConfig extends LocalConfig {
7480
constructor(
7581
private sprite: Sprite,
@@ -86,14 +92,14 @@ export class SpriteLocalConfig extends LocalConfig {
8692
this.addDisposer(
8793
watch(
8894
() => sprite.x,
89-
(x, old) => this.setX(x, old != null),
95+
(x, old) => this.setX(x, old != null && shouldUpdatePosConfigType(this.configTypes.at(-1))),
9096
{ immediate: true }
9197
)
9298
)
9399
this.addDisposer(
94100
watch(
95101
() => sprite.y,
96-
(y, old) => this.setY(y, old != null),
102+
(y, old) => this.setY(y, old != null && shouldUpdatePosConfigType(this.configTypes.at(-1))),
97103
{ immediate: true }
98104
)
99105
)
@@ -133,9 +139,13 @@ export class SpriteLocalConfig extends LocalConfig {
133139
}
134140
syncRotationStyle = this.doAction(() => this.sprite.setRotationStyle(this.rotationStyle))
135141

136-
syncToSprite(updater: (sprite: Sprite) => void) {
137-
return this.doAction(() => updater(this.sprite), false)()
138-
}
142+
syncAll = this.doAction(() => {
143+
this.sprite.setSize(this.size)
144+
this.sprite.setX(this.x)
145+
this.sprite.setY(this.y)
146+
this.sprite.setHeading(this.heading)
147+
this.sprite.setRotationStyle(this.rotationStyle)
148+
})
139149
}
140150

141151
export class WidgetLocalConfig extends LocalConfig {
@@ -148,14 +158,14 @@ export class WidgetLocalConfig extends LocalConfig {
148158
this.addDisposer(
149159
watch(
150160
() => widget.x,
151-
(x, old) => this.setX(x, old != null),
161+
(x, old) => this.setX(x, old != null && shouldUpdatePosConfigType(this.configTypes.at(-1))),
152162
{ immediate: true }
153163
)
154164
)
155165
this.addDisposer(
156166
watch(
157167
() => widget.y,
158-
(y, old) => this.setY(y, old != null),
168+
(y, old) => this.setY(y, old != null && shouldUpdatePosConfigType(this.configTypes.at(-1))),
159169
{ immediate: true }
160170
)
161171
)
@@ -167,4 +177,10 @@ export class WidgetLocalConfig extends LocalConfig {
167177
)
168178
)
169179
}
180+
181+
syncAll = this.doAction(() => {
182+
this.widget.setSize(this.size)
183+
this.widget.setX(this.x)
184+
this.widget.setY(this.y)
185+
})
170186
}

0 commit comments

Comments
 (0)