Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: crop ai backdrop image to project stage aspect ratio by default #885

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 4 deletions spx-gui/src/components/asset/library/AssetLibrary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,15 @@
<div class="sider">
<LibraryMenu @update:value="handleUserSelectCategory" />
<UIDivider />
<LibraryTree
:type="type"
style="flex: 1 1 0%; overflow: auto; scrollbar-width: thin"
/>
<LibraryTree :type="type" style="flex: 1 1 0%; overflow: auto; scrollbar-width: thin;" />
</div>
</section>
<Transition name="fade" mode="out-in" appear>
<section v-if="selectedAsset && isAiAsset in selectedAsset" class="body">
<AIPreviewModal
:asset="selectedAsset"
:ai-assets="currentAIAssetList"
:project="props.project"
class="asset-page"
:add-to-project-pending="handleAddToProject.isLoading.value"
@add-to-project="handleAddToProject.fn"
Expand Down
4 changes: 3 additions & 1 deletion spx-gui/src/components/asset/library/ai/AIBackdropEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ImageCrop
v-if="editMode === 'crop' && image !== null && stageConfig != null && !loading"
ref="imageCrop"
:initial-crop="props.defaultRatio"
:image="image"
:stage-config="stageConfig"
:width="mapWidth"
Expand Down Expand Up @@ -88,6 +89,7 @@ import ImageCrop from './ImageEditor/ImageCrop.vue'

const props = defineProps<{
asset: TaggedAIAssetData<AssetType.Backdrop>
defaultRatio: number
}>()

const backdrop = useAsyncComputed<Backdrop | undefined>(() => {
Expand All @@ -107,7 +109,7 @@ const layer = ref<Konva.Layer>()
const nodeRef = ref<KonvaNode<Konva.Image>>()
const node = computed(() => nodeRef.value?.getNode())

const editMode = ref<'resize' | 'crop' | 'preview'>('preview')
const editMode = ref<'resize' | 'crop' | 'preview'>('crop')

const imageResize = ref<InstanceType<typeof ImageResize> | null>(null)
const imageCrop = ref<InstanceType<typeof ImageCrop> | null>(null)
Expand Down
120 changes: 87 additions & 33 deletions spx-gui/src/components/asset/library/ai/AIPreviewModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
<div class="head head-actions">
<Transition name="slide-fade" mode="out-in" appear>
<div v-if="contentReady" class="head-left">
<UIButton v-for="action in currentActions" :key="action.name" size="large" :type="action.type"
:disabled="action.disabled" @click="action.action">
<UIButton
v-for="action in currentActions"
:key="action.name"
size="large"
:type="action.type"
:disabled="action.disabled"
@click="action.action"
>
<NIcon v-if="action.icon">
<component :is="action.icon" />
</NIcon>
Expand All @@ -16,8 +22,12 @@
</div>
</Transition>
<div class="head-right">
<UIButton size="large" class="insert-button" :disabled="!contentReady || addToProjectPending || exportPending"
@click="handleAddButton">
<UIButton
size="large"
class="insert-button"
:disabled="!contentReady || addToProjectPending || exportPending"
@click="handleAddButton"
>
<span style="white-space: nowrap">
{{
addToProjectPending || exportPending
Expand Down Expand Up @@ -51,7 +61,10 @@
<span v-else-if="status === AIGCStatus.Generating" class="generating-text">
{{ $t({ en: `Generating...`, zh: `生成中...` }) }}
</span>
<span v-else-if="status === AIGCStatus.Finished && !contentReady" class="generating-text">
<span
v-else-if="status === AIGCStatus.Finished && !contentReady"
class="generating-text"
>
{{ $t({ en: `Loading...`, zh: `加载中...` }) }}
</span>
</Transition>
Expand All @@ -73,25 +86,48 @@
</div>
</template>
<template v-else>
<AISpriteEditor v-if="asset.assetType === AssetType.Sprite" ref="spriteEditor"
:asset="asset as TaggedAIAssetData<AssetType.Sprite>" class="asset-editor sprite-editor" />
<AIBackdropEditor v-else-if="asset.assetType === AssetType.Backdrop" ref="backdropEditor"
:asset="asset as TaggedAIAssetData<AssetType.Backdrop>" class="asset-editor backdrop-editor" />
<AISoundEditor v-else-if="asset.assetType === AssetType.Sound" ref="soundEditor" :asset="asset" />
<AISpriteEditor
v-if="asset.assetType === AssetType.Sprite"
ref="spriteEditor"
:asset="asset as TaggedAIAssetData<AssetType.Sprite>"
class="asset-editor sprite-editor"
/>
<AIBackdropEditor
v-else-if="asset.assetType === AssetType.Backdrop"
ref="backdropEditor"
:default-ratio="props.project.stage.mapWidth / props.project.stage.mapHeight"
:asset="asset as TaggedAIAssetData<AssetType.Backdrop>"
class="asset-editor backdrop-editor"
/>
<AISoundEditor
v-else-if="asset.assetType === AssetType.Sound"
ref="soundEditor"
:asset="asset"
/>
</template>
</Transition>
</main>
<aside>
<NScrollbar :content-style="{
paddingRight: '15px',
display: 'flex',
flexDirection: 'column',
gap: '10px'
}">
<div v-for="aiAsset in aiAssets" :key="aiAsset.taskId" class="ai-asset-wrapper"
:class="{ selected: aiAsset.result?.id === asset.id }">
<AIAssetItem :task="aiAsset" :show-ai-asset-tip="false" @ready="(aiAsset as any)[isPreviewReady] = true"
@click="(aiAsset as any)[isPreviewReady] && emit('selectAi', aiAsset.result!)" />
<NScrollbar
:content-style="{
paddingRight: '15px',
display: 'flex',
flexDirection: 'column',
gap: '10px'
}"
>
<div
v-for="aiAsset in aiAssets"
:key="aiAsset.taskId"
class="ai-asset-wrapper"
:class="{ selected: aiAsset.result?.id === asset.id }"
>
<AIAssetItem
:task="aiAsset"
:show-ai-asset-tip="false"
@ready="(aiAsset as any)[isPreviewReady] = true"
@click="(aiAsset as any)[isPreviewReady] && emit('selectAi', aiAsset.result!)"
/>
</div>
</NScrollbar>
</aside>
Expand All @@ -111,7 +147,14 @@ export interface EditorAction {
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { NIcon, NSpin } from 'naive-ui'
import { addAsset, AssetType, getAsset, IsPublic, type AddAssetParams, type AssetData } from '@/apis/asset'
import {
addAsset,
AssetType,
getAsset,
IsPublic,
type AddAssetParams,
type AssetData
} from '@/apis/asset'
import UIButton, { type ButtonType } from '@/components/ui/UIButton.vue'
import AIAssetItem from '../AIAssetItem.vue'
import { NScrollbar } from 'naive-ui'
Expand All @@ -122,7 +165,7 @@ import {
isPreviewReady,
type TaggedAIAssetData,
exportedId,
type RequiredAIGCFiles,
type RequiredAIGCFiles
} from '@/apis/aigc'
import { debounce } from '@/utils/utils'
import { getFiles } from '@/models/common/cloud'
Expand All @@ -137,12 +180,14 @@ import type { LocaleMessage } from '@/utils/i18n'
import { AIGCTask, AISpriteTask } from '@/models/aigc'
import { useRenameAsset } from '../..'
import { useMessageHandle } from '@/utils/exception'
import type { Project } from '@/models/project'

// Define component props
const props = defineProps<{
asset: TaggedAIAssetData
aiAssets: AIGCTask[]
addToProjectPending: boolean
project: Project
}>()

const emit = defineEmits<{
Expand All @@ -155,13 +200,25 @@ const backdropEditor = ref<InstanceType<typeof AIBackdropEditor> | null>(null)
const soundEditor = ref<InstanceType<typeof AISoundEditor> | null>(null)

const currentActions = computed<EditorAction[]>(() => {
if (props.asset.assetType === AssetType.Sprite && spriteEditor.value && 'actions' in spriteEditor.value) {
if (
props.asset.assetType === AssetType.Sprite &&
spriteEditor.value &&
'actions' in spriteEditor.value
) {
return spriteEditor?.value?.actions as EditorAction[]
}
if (props.asset.assetType === AssetType.Backdrop && backdropEditor.value && 'actions' in backdropEditor.value) {
if (
props.asset.assetType === AssetType.Backdrop &&
backdropEditor.value &&
'actions' in backdropEditor.value
) {
return backdropEditor?.value?.actions as EditorAction[]
}
if (props.asset.assetType === AssetType.Sound && soundEditor.value && 'actions' in soundEditor.value) {
if (
props.asset.assetType === AssetType.Sound &&
soundEditor.value &&
'actions' in soundEditor.value
) {
return soundEditor?.value?.actions as EditorAction[]
}
return []
Expand Down Expand Up @@ -245,7 +302,7 @@ const publicAsset = ref<AssetData | null>(null)
* Get the public asset data from the asset data
* If the asset data is not exported, export it first
*/
const exportAssetDataToPublic = async () => {
const exportAssetDataToPublic = async () => {
if (!props.asset[isContentReady]) {
throw new Error('Could not export an incomplete asset')
}
Expand All @@ -256,8 +313,8 @@ const publicAsset = ref<AssetData | null>(null)
files: props.asset.files!,
displayName: props.asset.displayName ?? props.asset.id,
filesHash: props.asset.filesHash!,
preview: "TODO",
category: '*',
preview: 'TODO',
category: '*'
}
exportPending.value = true
const assetId = props.asset[exportedId] ?? (await addAsset(addAssetParam)).id
Expand All @@ -266,21 +323,18 @@ const publicAsset = ref<AssetData | null>(null)
return publicAsset
}


const handleAddButton = async () => {
if (props.addToProjectPending) {
return
}

if (!publicAsset.value) {
const exportedAsset = await exportAssetDataToPublic()
publicAsset.value = exportedAsset
}
emit('addToProject', publicAsset.value)
}



const renameAsset = useRenameAsset()
const handleRename = useMessageHandle(
async () => {
Expand Down Expand Up @@ -411,4 +465,4 @@ aside {
.slide-fade-leave-to {
transform: translateY(-10px);
}
</style>
</style>
35 changes: 29 additions & 6 deletions spx-gui/src/components/asset/library/ai/ImageEditor/ImageCrop.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import type { KonvaNode } from '../AIBackdropEditor.vue'
import type { TransformerConfig } from 'konva/lib/shapes/Transformer'
import { useRenderScale } from './useRenderScale'
import { useCenterPosition } from './useCenterPosition'
import { c } from 'naive-ui'

const nodeRef = ref<KonvaNode<Konva.Image>>()
const rectRef = ref<KonvaNode<Konva.Rect>>()
Expand All @@ -43,6 +42,11 @@ const props = defineProps<{
width: number
height: number
fillPercent: number
/**
* Initial crop position
* or a number representing the ratio of the cropped area
*/
initialCrop?: { x: number; y: number; width: number; height: number } | number
}>()

const { renderScale, updateRenderScale } = useRenderScale(props, false)
Expand All @@ -65,14 +69,33 @@ const crop = ref({

watch(
imageSize,
(newSize) => {
(newSize, oldSize) => {
const { width, height } = newSize ?? { width: 0, height: 0 }
updateRenderScale(width, height)
updateCenterPosition(width * renderScale.value, height * renderScale.value)
crop.value.width = width
crop.value.height = height
crop.value.x = centerPos.value.x
crop.value.y = centerPos.value.y
if (oldSize || !props.initialCrop) {
crop.value.width = width
crop.value.height = height
crop.value.x = centerPos.value.x
crop.value.y = centerPos.value.y
}
else if (typeof props.initialCrop === 'number') {
const expectRatio = props.initialCrop
const ratio = width / height
if (ratio > expectRatio) {
crop.value.width = height * expectRatio
crop.value.height = height
crop.value.x = centerPos.value.x + (width - crop.value.width) / 2
crop.value.y = centerPos.value.y
} else {
crop.value.width = width
crop.value.height = width / expectRatio
crop.value.x = centerPos.value.x
crop.value.y = centerPos.value.y + (height - crop.value.height) / 2
}
} else {
crop.value = props.initialCrop
}
},
{ immediate: true }
)
Expand Down
Loading