Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
2026-04-15 11:24:11 +08:00
33 changed files with 834 additions and 785 deletions

View File

@@ -107,6 +107,39 @@ export const toRealStyleApi = (data:toRealStyleData) => {
}
})
}
/**
* 图片转真是风格-变体
* @param data 图片转真是风格-变体的参数
* @param data.sketchId sketch id
* @param data.imageUrl 进行生成的图片。minio地址和正常地址都可以
* @param data.mode 选择的模型
* @param data.size 生成图片的大小
* @param data.userPrompt 生成图片的提示词
* @param data.original3dUrl 原始3d模型的url
* @returns 图片转真是风格
*/
export interface toRealStyleData {
sketchId?: string
imageUrl?: string
mode?: string
size?: string
userPrompt?: string
original3dUrl?: string
}
export const toRealVariantsApi = (data:toRealStyleData) => {
return request({
url: `/api/image/to-real-variants`,
method: 'post',
data:{
sketchId: data.sketchId,
imageUrl: data.imageUrl,
mode: data.mode,
size: data.size,
userPrompt: data.userPrompt,
original3dUrl: data.original3dUrl,
}
})
}
/**
* 线稿图上色
* @param data 线稿图上色的参数

View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1959 2.6173C12.4159 1.8333 11.1479 1.8333 10.3679 2.6173L3.01592 9.9693C2.23192 10.7493 2.23192 12.0173 3.01592 12.7973C3.79592 13.5813 5.06392 13.5813 5.84392 12.7973L13.1959 5.4453C13.9799 4.6653 13.9799 3.3973 13.1959 2.6173ZM12.5319 4.5213L11.1159 5.9333C11.0399 6.0133 10.9119 6.0133 10.8359 5.9333L10.2679 5.3693C10.1919 5.2893 10.1919 5.1613 10.2679 5.0853L11.6839 3.6693C11.7599 3.5933 11.8879 3.5933 11.9679 3.6693L12.5319 4.2373C12.6079 4.3133 12.6079 4.4413 12.5319 4.5213Z" fill="black"/>
<path d="M7.59998 3.6C8.04181 3.6 8.39998 3.24183 8.39998 2.8C8.39998 2.35817 8.04181 2 7.59998 2C7.15815 2 6.79998 2.35817 6.79998 2.8C6.79998 3.24183 7.15815 3.6 7.59998 3.6Z" fill="#FF7A51"/>
<path d="M11.624 9.412C11.752 9.064 12.244 9.064 12.376 9.412L12.8 10.56C12.84 10.668 12.928 10.756 13.036 10.796L14.184 11.22C14.532 11.348 14.532 11.84 14.184 11.972L13.036 12.396C12.928 12.436 12.84 12.524 12.8 12.632L12.376 13.78C12.248 14.128 11.756 14.128 11.624 13.78L11.2 12.632C11.16 12.524 11.072 12.436 10.964 12.396L9.81598 11.972C9.46798 11.844 9.46798 11.352 9.81598 11.22L10.964 10.796C11.072 10.756 11.16 10.668 11.2 10.56L11.624 9.412Z" fill="#FF7A51"/>
<path d="M3.62398 3.012C3.75198 2.664 4.24398 2.664 4.37598 3.012L4.58398 3.576C4.62398 3.684 4.71198 3.772 4.81998 3.812L5.38398 4.02C5.73198 4.148 5.73198 4.64 5.38398 4.772L4.81998 4.98C4.71198 5.02 4.62398 5.108 4.58398 5.216L4.37598 5.78C4.24798 6.128 3.75598 6.128 3.62398 5.78L3.41598 5.216C3.37598 5.108 3.28798 5.02 3.17998 4.98L2.61598 4.772C2.26798 4.644 2.26798 4.152 2.61598 4.02L3.17998 3.812C3.28798 3.772 3.37598 3.684 3.41598 3.576L3.62398 3.012Z" fill="#FF7A51"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4956 3.27113C15.5206 2.29113 13.9356 2.29113 12.9606 3.27113L3.77064 12.4611C2.79064 13.4361 2.79064 15.0211 3.77064 15.9961C4.74564 16.9761 6.33064 16.9761 7.30564 15.9961L16.4956 6.80613C17.4756 5.83113 17.4756 4.24613 16.4956 3.27113ZM15.6656 5.65113L13.8956 7.41613C13.8006 7.51613 13.6406 7.51613 13.5456 7.41613L12.8356 6.71113C12.7406 6.61113 12.7406 6.45113 12.8356 6.35613L14.6056 4.58613C14.7006 4.49113 14.8606 4.49113 14.9606 4.58613L15.6656 5.29613C15.7606 5.39113 15.7606 5.55113 15.6656 5.65113Z" fill="white"/>
<path d="M9.50059 4.5C10.0529 4.5 10.5006 4.05228 10.5006 3.5C10.5006 2.94772 10.0529 2.5 9.50059 2.5C8.9483 2.5 8.50059 2.94772 8.50059 3.5C8.50059 4.05228 8.9483 4.5 9.50059 4.5Z" fill="white"/>
<path d="M14.5306 11.765C14.6906 11.33 15.3056 11.33 15.4706 11.765L16.0006 13.2C16.0506 13.335 16.1606 13.445 16.2956 13.495L17.7306 14.025C18.1656 14.185 18.1656 14.8 17.7306 14.965L16.2956 15.495C16.1606 15.545 16.0506 15.655 16.0006 15.79L15.4706 17.225C15.3106 17.66 14.6956 17.66 14.5306 17.225L14.0006 15.79C13.9506 15.655 13.8406 15.545 13.7056 15.495L12.2706 14.965C11.8356 14.805 11.8356 14.19 12.2706 14.025L13.7056 13.495C13.8406 13.445 13.9506 13.335 14.0006 13.2L14.5306 11.765Z" fill="white"/>
<path d="M4.53059 3.765C4.69059 3.33 5.30559 3.33 5.47059 3.765L5.73059 4.47C5.78059 4.605 5.89059 4.715 6.02559 4.765L6.73059 5.025C7.16559 5.185 7.16559 5.8 6.73059 5.965L6.02559 6.225C5.89059 6.275 5.78059 6.385 5.73059 6.52L5.47059 7.225C5.31059 7.66 4.69559 7.66 4.53059 7.225L4.27059 6.52C4.22059 6.385 4.11059 6.275 3.97559 6.225L3.27059 5.965C2.83559 5.805 2.83559 5.19 3.27059 5.025L3.97559 4.765C4.11059 4.715 4.22059 4.605 4.27059 4.47L4.53059 3.765Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -14,7 +14,7 @@
<div>
<button class="export" @click="emit('export')">
<span class="icon"><svg-icon name="export" size="11" /></span>
<span class="text">Export</span>
<span class="text">{{ $t('FlowCanvas.export') }}</span>
</button>
<div v-loading="true" class="mask" v-if="downloadData.status == 'loading'"></div>
</div>

View File

@@ -1,12 +1,12 @@
<template>
<!-- 添加印花 -->
<div class="add-print">
<p class="label">Print</p>
<p class="label">{{ $t('FlowCanvas.print') }}</p>
<upload-file v-model="data.file" />
<p class="label">Settings</p>
<p class="label">{{ $t('FlowCanvas.settings') }}</p>
<div class="settings">
<div>
<p class="label">Angle</p>
<p class="label">{{ $t('FlowCanvas.angle') }}</p>
<my-input
v-model="data.setting.angle"
type="number"
@@ -18,7 +18,7 @@
/>
</div>
<div>
<span class="label">Scale</span>
<span class="label">{{ $t('FlowCanvas.scale') }}</span>
<slider
:min="1"
:max="1000"
@@ -27,7 +27,7 @@
/>
</div>
<div>
<span class="label">Gap X</span>
<span class="label">{{ $t('FlowCanvas.gapX') }}</span>
<slider
:min="0"
:max="1000"
@@ -36,7 +36,7 @@
/>
</div>
<div>
<span class="label">Gap Y</span>
<span class="label">{{ $t('FlowCanvas.gapY') }}</span>
<slider
:min="0"
:max="1000"
@@ -45,14 +45,14 @@
/>
</div>
<div>
<span class="label">Offset</span>
<span class="label">{{ $t('FlowCanvas.offset') }}</span>
<offset-tool v-model="data.setting.offset" :show-dish="false" />
</div>
<div class="offset">
<offset-tool v-model="data.setting.offset" :show-input="false" />
</div>
</div>
<p class="label">Prompt</p>
<p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" />
</div>
</template>

View File

@@ -1,7 +1,16 @@
<template>
<!-- 高级工具选择 -->
<div class="cards-select">
<div v-for="v in (node.data?.secondaryMenu?.selectList || list)" :key="v.type" @click="onClickItem(v)" v-show="v.tier === tier">
<div
v-for="v in (node.data?.secondaryMenu?.selectList || list)"
:key="v.type"
@click="onClickItem(v)"
v-show="
(v.tier === tier) &&
((v.type == NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL && props.tier == NODE_DATATIER.TO_REAL_VARIANTS))||
(v.type != NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType != NODE_DATATYPE.TO_3D_MODEL || props.tier != NODE_DATATIER.TO_REAL_VARIANTS)))
"
>
<span class="icon">
<svg-icon :name="v.type + '-2'" size="15" size-unit="px" />
</span>
@@ -13,34 +22,44 @@
<script setup lang="ts">
import { ref, inject, computed } from 'vue'
import { NODE_DATATYPE, NODE_DATATIER } from '../../../tools/index.d'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const nodeManager = inject('nodeManager') as any
const stateManager = inject('stateManager') as any
const props = defineProps({
node: { required: true, type: Object },
tier: { default: 1, type: Number }
})
const superiorNode = computed(() => {
return stateManager.getNodeById(props.node.data.superiorID)
})
const list = ref([
{
tier: NODE_DATATIER.TO_REAL_STYLE,
type: NODE_DATATYPE.TO_REAL_STYLE,
title: 'To Real Style'
title: t('FlowCanvas.toRealStyleTitle')
},
{
tier: NODE_DATATIER.TO_REAL_VARIANTS,
type: NODE_DATATYPE.TO_REAL_VARIANTS,
title: t('FlowCanvas.toRealVariantsTitle')
},
{
tier: NODE_DATATIER.SURFACE_EDIT,
type: NODE_DATATYPE.SURFACE_EDIT,
title: 'Surface Edit',
title: t('FlowCanvas.surfaceEditTitle'),
secondaryMenu: {
title: 'Surface Edit',
title: t('FlowCanvas.surfaceEditTitle'),
icon: NODE_DATATYPE.SURFACE_EDIT,
selectList: [
{
tier: NODE_DATATIER.CANVAS_MODE,
type: NODE_DATATYPE.CANVAS_MODE,
title: 'Surface Edit (Canvas)',
title: t('FlowCanvas.surfaceEditCanvasTitle'),
},{
tier: NODE_DATATIER.Fast_MODE,
type: NODE_DATATYPE.Fast_MODE,
title: 'Surface Edit',
title: t('FlowCanvas.surfaceEditTitle'),
},
]
}
@@ -48,22 +67,22 @@
{
tier: NODE_DATATIER.SCENE_COMPOSITION,
type: NODE_DATATYPE.SCENE_COMPOSITION,
title: 'Scene Composition'
title: t('FlowCanvas.sceneCompositionTitle')
},
{
tier: NODE_DATATIER.COLOR_PALETTE,
type: NODE_DATATYPE.COLOR_PALETTE,
title: 'Color Palette'
title: t('FlowCanvas.colorPaletteTitle')
},
{
tier: NODE_DATATIER.TO_3D_MODEL,
type: NODE_DATATYPE.TO_3D_MODEL,
title: 'To 3D Model'
title: t('FlowCanvas.to3DModelTitle')
},
{
tier: NODE_DATATIER.TO_3VIEW,
type: NODE_DATATYPE.TO_3VIEW,
title: 'To 3-View'
title: t('FlowCanvas.to3ViewTitle')
}
])
const onClickItem = (v) => {

View File

@@ -1,9 +1,9 @@
<template>
<!-- 颜色调色板 -->
<div class="color-palette">
<p class="label">Mode</p>
<p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" />
<p class="label">Choose Color</p>
<p class="label">{{ $t('FlowCanvas.chooseColor') }}</p>
<div class="color-list">
<div
class="color-item"
@@ -25,6 +25,8 @@
<script setup lang="ts">
import { reactive, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const data = reactive({
colors: [],
mode: 'Advanced',
@@ -45,8 +47,8 @@
data.colors.push(target.value)
}
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: t('FlowCanvas.normalMode') }
])
const getApiData = ()=>{
return {

View File

@@ -1,7 +1,7 @@
<template>
<!-- 画布编辑印花 -->
<div class="fast-mode">
<p class="label">Output</p>
<p class="label">{{ $t('FlowCanvas.output') }}</p>
<div class="imgBox">
<img :src="data.url" alt="">
</div>

View File

@@ -19,13 +19,13 @@
<div class="footer" @mousedown.stop v-if="!currentComponent?.hideFooter">
<button @click="onGenerateClick">
<svg-icon name="xingxing" size="16" size-unit="px" />
<span>Generate</span>
<span>{{ $t('FlowCanvas.generate') }}</span>
</button>
</div>
<div class="footer canvasEdit" @mousedown.stop v-if="currentComponent?.showCanvasEdit">
<button @click="currentComponent?.on()">
<svg-icon name="xingxing" size="16" size-unit="px" />
<span>Edit</span>
<span>{{ $t('FlowCanvas.edit') }}</span>
</button>
</div>
</div>
@@ -36,6 +36,7 @@
import { computed, ref, useAttrs, onMounted, inject, watch } from 'vue'
import CardsSelect from './cards-select.vue'
import ToRealStyle from './to-real-style.vue'
import ToRealVariants from './to-real-variants.vue'
import SurfaceEdit from './surface-edit.vue'
import FastMode from './fast-mode.vue'
import SceneComposition from './scene-composition.vue'
@@ -45,7 +46,7 @@
import { useI18n } from 'vue-i18n'
import { ElMessageBox } from 'element-plus'
import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi } from '@/api/flow-canvas'
import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi, toRealVariantsApi } from '@/api/flow-canvas'
// import ToVideo from './to-video.vue'
// import AddPrint from './add-print.vue'
@@ -57,7 +58,7 @@
{
tier: NODE_DATATIER.CARDS_SELECT,
type: NODE_DATATYPE.CARDS_SELECT,
title: 'Advanced Tools',
title: t('FlowCanvas.selectCardsTitle'),
component: CardsSelect,
hideFooter: true,
hideIcon: true,
@@ -65,21 +66,28 @@
{
tier: NODE_DATATIER.TO_REAL_STYLE,
type: NODE_DATATYPE.TO_REAL_STYLE,
title: 'To Real Style',
title: t('FlowCanvas.toRealStyleTitle'),
component: ToRealStyle,
api: toRealStyleApi
},
{
tier: NODE_DATATIER.TO_REAL_VARIANTS,
type: NODE_DATATYPE.TO_REAL_VARIANTS,
title: 'To Real Variants',
component: ToRealVariants,
api: toRealVariantsApi
},
{
tier: NODE_DATATIER.Fast_MODE,
type: NODE_DATATYPE.Fast_MODE,
title: 'Surface Edit',
title: t('FlowCanvas.surfaceEditTitle'),
component: SurfaceEdit,
api: sketchAddPrintApi
},
{
tier: NODE_DATATIER.CANVAS_MODE,
type: NODE_DATATYPE.CANVAS_MODE,
title: 'Surface Edit (Canvas)',
title: t('FlowCanvas.surfaceEditCanvasTitle'),
component: FastMode,
hideFooter: true,
showCanvasEdit: true,
@@ -90,28 +98,28 @@
{
tier: NODE_DATATIER.SCENE_COMPOSITION,
type: NODE_DATATYPE.SCENE_COMPOSITION,
title: 'Scene Composition',
title: t('FlowCanvas.sceneCompositionTitle'),
component: SceneComposition,
api: toSceneCompositionApi
},
{
tier: NODE_DATATIER.COLOR_PALETTE,
type: NODE_DATATYPE.COLOR_PALETTE,
title: 'Color Palette',
title: t('FlowCanvas.colorPaletteTitle'),
component: ColorPalette,
api: toColorPaletteApi
},
{
tier: NODE_DATATIER.TO_3D_MODEL,
type: NODE_DATATYPE.TO_3D_MODEL,
title: 'To 3D Model',
title: t('FlowCanvas.to3DModelTitle'),
component: To3DModel,
api:sketchToThreeApi
},
{
tier: NODE_DATATIER.TO_3VIEW,
type: NODE_DATATYPE.TO_3VIEW,
title: 'To 3-View',
title: t('FlowCanvas.to3ViewTitle'),
component: To3View,
api:threeToThreeViewsApi
}
@@ -197,6 +205,7 @@
data: {
superiorID: attrs.node.id,
superiorNodeType: attrs.node?.data?.type,
superiorGenerateImg: superiorNodeUrl || null,
createIndexPosition: index + subordNodes.length,
tier: tier,
isActive: index == 0 && subordNodes.length == 0,

View File

@@ -1,11 +1,11 @@
<template>
<!-- 场景构图 -->
<div class="scene-composition">
<p class="label">Prompt</p>
<p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" />
<p class="label">Mode</p>
<p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" />
<p class="label">Choose Style</p>
<p class="label">{{ $t('FlowCanvas.chooseStyle') }}</p>
<div class="style-list">
<div
class="item"
@@ -26,26 +26,28 @@
<script setup lang="ts">
import { computed, ref, reactive, onMounted } from 'vue'
import myTextarea from '../../tools/my-textarea.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const styleList = ref([
{ label: 'Colorful', value: 'Colorful' },
{ label: 'Minimalist', value: 'Minimalist' },
{ label: 'Modernist', value: 'Modernist' },
{ label: 'Bauhaus', value: 'Bauhaus' },
{ label: 'Mintage', value: 'Mintage' },
{ label: 'Industrial', value: 'Industrial' },
{ label: 'Futuristic', value: 'Futuristic' },
{ label: 'Elegant', value: 'Elegant' },
{ label: 'Organic', value: 'Organic' },
{ label: 'Calm', value: 'Calm' },
{ label: 'Abstract', value: 'Abstract' },
{ label: 'Kitsch-core', value: 'Kitsch-core' },
{ label: 'Sophisticated', value: 'Sophisticated' },
{ label: 'Maximalism', value: 'Maximalism' },
{ label: 'Clean', value: 'Clean' },
{ label: 'Bright Colors', value: 'Bright Colors' },
{ label: 'Luxurious', value: 'Luxurious' },
{ label: 'Bold Colors', value: 'Bold Colors' },
{ label: 'Brutalism', value: 'Brutalism' }
{ label: t('FlowCanvas.colorful'), value: 'Colorful' },
{ label: t('FlowCanvas.minimalist'), value: 'Minimalist' },
{ label: t('FlowCanvas.modernist'), value: 'Modernist' },
{ label: t('FlowCanvas.bauhaus'), value: 'Bauhaus' },
{ label: t('FlowCanvas.mintage'), value: 'Mintage' },
{ label: t('FlowCanvas.industrial'), value: 'Industrial' },
{ label: t('FlowCanvas.futuristic'), value: 'Futuristic' },
{ label: t('FlowCanvas.elegant'), value: 'Elegant' },
{ label: t('FlowCanvas.organic'), value: 'Organic' },
{ label: t('FlowCanvas.calm'), value: 'Calm' },
{ label: t('FlowCanvas.abstract'), value: 'Abstract' },
{ label: t('FlowCanvas.kitschCore'), value: 'Kitsch-core' },
{ label: t('FlowCanvas.sophisticated'), value: 'Sophisticated' },
{ label: t('FlowCanvas.maximalism'), value: 'Maximalism' },
{ label: t('FlowCanvas.clean'), value: 'Clean' },
{ label: t('FlowCanvas.brightColors'), value: 'Bright Colors' },
{ label: t('FlowCanvas.luxurious'), value: 'Luxurious' },
{ label: t('FlowCanvas.boldColors'), value: 'Bold Colors' },
{ label: t('FlowCanvas.brutalism'), value: 'Brutalism' }
])
const data = reactive({
prompt: '',
@@ -53,8 +55,8 @@
mode: 'Advanced',
})
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: t('FlowCanvas.normalMode') },
])
const onClickStyle = (value: string) => {
if (data.styles.includes(value)) {

View File

@@ -1,11 +1,11 @@
<template>
<!-- 编辑素材 -->
<div class="surface-edit">
<p class="label">Image</p>
<p class="label">{{ $t('FlowCanvas.image') }}</p>
<upload-file v-model="data.file" />
<p class="label">Mode</p>
<p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" />
<p class="label">Prompt</p>
<p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" />
</div>
</template>
@@ -15,14 +15,16 @@
import myTextarea from '../../tools/my-textarea.vue'
import uploadFile from '../../tools/upload-file.vue'
import mySelect from '../../tools/my-select.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const data = reactive({
prompt: '',
file: null,
mode: 'Advanced',
})
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: t('FlowCanvas.normalMode') }
])
const getApiData = ()=>{
return {

View File

@@ -1,11 +1,11 @@
<template>
<!-- 转3D模型 -->
<div class="to-3d-model">
<p class="label">Image</p>
<p class="label">{{ $t('FlowCanvas.image') }}</p>
<div class="image">
<img :src="data.url" alt="">
</div>
<p class="label">Mode</p>
<p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" />
</div>
</template>
@@ -13,15 +13,18 @@
<script setup lang="ts">
import { reactive, inject, useAttrs, computed,ref } from 'vue'
import uploadFile from '../../tools/upload-file.vue'
import mySelect from '../../tools/my-select.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const attrs = useAttrs()
const stateManager = inject('stateManager') as any
const data = reactive({
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
url: stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID),
mode: 'Advanced',
})
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: t('FlowCanvas.normalMode') }
])
const getApiData = ()=>{
return {

View File

@@ -1,7 +1,7 @@
<template>
<!-- 转3-View -->
<div class="to-3view">
<p class="label">3D Model</p>
<p class="label">{{ $t('FlowCanvas._3DModel') }}</p>
<div class="image">
<img :src="data.url" alt="">
</div>

View File

@@ -1,9 +1,9 @@
<template>
<!-- 转CAD -->
<div class="to-cad">
<p class="label">3D Model</p>
<p class="label">{{ $t('FlowCanvas._3DModel') }}</p>
<upload-file v-model="data.file" />
<p class="label">Prompt</p>
<p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" />
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<!-- 转换为真实图 -->
<div class="to-real-style">
<p class="label">Prompt</p>
<p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" />
<div class="shortcut-list">
<div
@@ -13,9 +13,9 @@
{{ v.label }}
</div>
</div>
<p class="label">Mode</p>
<p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" />
<p class="label">Size</p>
<p class="label">{{ $t('FlowCanvas.size') }}</p>
<pixel-ratio-selection v-model="data.pixelRatio" />
</div>
</template>
@@ -25,43 +25,30 @@
import myTextarea from '../../tools/my-textarea.vue'
import mySelect from '../../tools/my-select.vue'
import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue'
const shortcutList = ref([
{
label: 'Change the...',
value: 'Change the style to a realistic design. '
},
{
label: 'Bright Colors...',
value: 'Bright colors with modern patterns, change the style to a realistic furniture design. '
},
{
label: 'Make the...',
value: 'Make the structure more refined and balanced, change the style to a realistic furniture style. '
},
{
label: 'Imagine...',
value: 'Imagine this furniture with detailed fabric textures, change the style to a realistic design. '
},
{
label: 'Wood Materials with...',
value: 'Wood materials with natural oak texture and soft fabric, change the style to a realistic furniture design.'
}
])
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const shortcutList = ref([])
for (let i = 0; i < 5; i++) {
shortcutList.value.push({
label: t(`FlowCanvas.toRealStyleShortcut${i + 1}Label`),
value: t(`FlowCanvas.toRealStyleShortcut${i + 1}Value`)
})
}
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: t('FlowCanvas.normalMode') }
])
const data = reactive({
prompt: '',
pixelRatio: '1:1',
mode: 'Advanced',
mode: 'Advanced'
})
const getApiData = ()=>{
const getApiData = () => {
return {
mode: data.mode,
size: data.pixelRatio,
userPrompt: data.prompt,
userPrompt: data.prompt
}
}

View File

@@ -0,0 +1,98 @@
<template>
<!-- 转换为真实图 -->
<div class="to-real-style">
<p class="label">{{ t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" :placeholder="$t('FlowCanvas.toRealVariantsPlaceholder')" />
<div class="shortcut-list">
<div
class="item"
v-for="v in shortcutList"
:key="v.value"
@click="data.prompt = v.value"
>
{{ v.label }}
</div>
</div>
<!-- <p class="label">{{ t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> -->
<p class="label">{{ t('FlowCanvas.size') }}</p>
<pixel-ratio-selection v-model="data.pixelRatio" />
</div>
</template>
<script setup lang="ts">
import { inject, ref, reactive, useAttrs } from 'vue'
import myTextarea from '../../tools/my-textarea.vue'
import mySelect from '../../tools/my-select.vue'
import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const shortcutList = ref([
{
label: t('FlowCanvas.toRealVariantsShortcut1Label'),
value: t('FlowCanvas.toRealVariantsShortcut1Value')
},
{
label: t('FlowCanvas.toRealVariantsShortcut2Label'),
value: t('FlowCanvas.toRealVariantsShortcut2Value')
},
{
label: t('FlowCanvas.toRealVariantsShortcut3Label'),
value: t('FlowCanvas.toRealVariantsShortcut3Value')
},
{
label: t('FlowCanvas.toRealVariantsShortcut4Label'),
value: t('FlowCanvas.toRealVariantsShortcut4Value')
},
{
label: t('FlowCanvas.toRealVariantsShortcut5Label'),
value: t('FlowCanvas.toRealVariantsShortcut5Value')
}
])
const stateManager = inject('stateManager') as any
const attrs = useAttrs()
const modeList = ref([
{ value: 'Advanced', label: 'Advanced' },
{ value: 'Normal', label: 'Normal' }
])
const data = reactive({
prompt: '',
pixelRatio: '1:1',
mode: 'Advanced',
})
const getApiData = ()=>{
let superior = stateManager.getNodeById(attrs.node?.data?.superiorID)
// let {superiorGenerateImg} = attrs
return {
mode: data.mode,
size: data.pixelRatio,
userPrompt: data.prompt,
original3dUrl: superior?.data?.superiorGenerateImg,
}
}
defineExpose({ data, getApiData })
</script>
<style lang="less" scoped>
.to-real-style {
> .shortcut-list {
display: flex;
flex-wrap: wrap;
gap: 10px 3px;
user-select: none;
> .item {
display: flex;
align-items: center;
padding: 5px 3px;
font-family: Medium;
border-radius: 3px;
font-size: 10px;
border: 1px solid #e4e4e7;
background: #f0f0f0;
cursor: pointer;
}
}
}
</style>

View File

@@ -17,9 +17,9 @@
<span class="icon" @click="onDownload(item)">
<svg-icon name="download" size="20" size-unit="px" />
</span>
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL">
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL || node.data.tier == 0">
<span class="icon"><svg-icon name="edit" size="13" /></span>
<span class="text">Edit</span>
<span class="text">{{ $t('FlowCanvas.edit') }}</span>
</button>
</div>
<img
@@ -65,8 +65,10 @@
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue'
import HighlightAdmin from '@/components/highlightAdmin.vue'
import { NODE_DATATIER, NODE_DATATYPE } from '../../tools/index.d'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const openImagePreview = inject('openImagePreview') as (url: string) => void
const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void
const openThreeModelPreview = inject('openThreeModelPreview') as (obj: any) => void
const props = defineProps({
node: {
type: Object,
@@ -125,9 +127,9 @@
{ immediate: true }
)
const menus = ref([
{ label: 'Copy', tip: 'Ctrl+C', on: () => emit('copy-node') },
{ label: t('FlowCanvas.copy'), tip: 'Ctrl+C', on: () => emit('copy-node') },
{
label: 'Delete',
label: t('FlowCanvas.delete'),
tip: 'Del',
on: () => {
emit('delete-node', props.node.id)
@@ -135,23 +137,8 @@
disabled: !!props.config?.disableDelete
},
{ isDivide: true },
// {
// label: 'Bring to font',
// tip: '',
// on: () => {
// emit('bring-to-font')
// }
// },
// {
// label: 'Send to back',
// tip: '',
// on: () => {
// emit('send-to-back')
// }
// },
// { isDivide: true },
{
label: 'Flip horizontal',
label: t('FlowCanvas.flipHorizontal'),
tip: '',
on: () => {
data.imageProcessTasks.forEach((item) => {
@@ -167,7 +154,7 @@
}
},
{
label: 'Flip vertical',
label: t('FlowCanvas.flipVertical'),
tip: '',
on: () => {
data.imageProcessTasks.forEach((item) => {
@@ -184,8 +171,14 @@
}
])
const onPreview = (item: any) => {
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj,nodeId:props.node.id})
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL && tier.value != 0){
openThreeModelPreview({
glbPath:item?.glbPath,
glbInfoObj:item?.glbInfoObj,
nodeId:props.node?.id,
nodeType:props.node.data?.superiorNodeType,
superiorGenerateImg:props.node.data?.superiorGenerateImg,
})
}else{
openImagePreview(item.url)
}

View File

@@ -24,6 +24,8 @@
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick, watch } from 'vue'
import myTextTools from '@/components/Canvas/FlowCanvas/components/tools/my-textTools.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
active: {
type: Boolean,
@@ -36,7 +38,7 @@
})
const emit = defineEmits(['update-data', 'delete-node'])
const data = reactive({
text: props.data?.text || '点击编辑文本',
text: props.data?.text || t('FlowCanvas.clickEditText'),
textStyle:{
'--font-size':props.data?.textStyle?.['--font-size'] || '16px',
'--font-color':props.data?.textStyle?.['--font-color'] || '#000',

View File

@@ -38,7 +38,7 @@
import { reactive, ref, markRaw, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const emit = defineEmits(['update:textStyle'])
const { locale } = useI18n()
const { locale, t } = useI18n()
const fontFamilyList = ref({
ENGLISH: [
{ value:'Medium',label:'Medium' },

View File

@@ -1,7 +1,7 @@
<template>
<div class="my-textarea">
<textarea
:placeholder="placeholder"
:placeholder="placeholder || $t('FlowCanvas.promptDefaultPlaceholder')"
:value="modelValue"
@input="onInput"
@change="onChange"
@@ -16,12 +16,15 @@
<script setup lang="ts">
import { computed, ref, markRaw, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const placeholder = t('FlowCanvas.promptDefaultPlaceholder')
const emit = defineEmits(['update:modelValue', 'input', 'change'])
const props = defineProps({
modelValue: { type: String },
placeholder: {
type: String,
default: 'Enter the scene you want to describe...'
default: ''
}
})
const onInput = (e) => {

View File

@@ -28,21 +28,13 @@ const {} = toRefs(data);
</script>
<template>
<div class="modalDetail">
<div class="title">
Properties Information
</div>
<div class="title">{{ $t('threeModel.propertiesInformation') }}</div>
<div class="detail">
<div class="name">
<div class="title fs18">
Sofa
</div>
<div class="fs14 c66">
Model Name
</div>
</div>
<div class="fs14 c18">
Transform
<div class="title fs18">{{ $t('threeModel._3DAsset') }}</div>
<div class="fs14 c66">{{ $t('threeModel.fileFormat') }}</div>
</div>
<div class="fs14 c18">{{ $t('threeModel.transform') }}</div>
<div class="flex">
<div>
<div class="fs14 c18">X</div>
@@ -57,20 +49,18 @@ const {} = toRefs(data);
<div class="fs12 c66">{{ config?.glbInfoObj?.centroid?.[2].toFixed(2) || 0 }}</div>
</div>
</div>
<div class="fs14 c18">
Dimensions
</div>
<div class="fs14 c18">{{ $t('threeModel.dimensions') }}</div>
<div class="flex">
<div>
<div class="fs14 c18">Height</div>
<div class="fs14 c18">{{ $t('threeModel.height') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[0].toFixed(2) || 0 }}</div>
</div>
<div>
<div class="fs14 c18">Width</div>
<div class="fs14 c18">{{ $t('threeModel.width') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[1].toFixed(2) || 0 }}</div>
</div>
<div>
<div class="fs14 c18">Depth</div>
<div class="fs14 c18">{{ $t('threeModel.depth') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[2].toFixed(2) || 0 }}</div>
</div>
</div>
@@ -100,7 +90,7 @@ const {} = toRefs(data);
}
> .captureView ,
> .download{
margin-left: 4.2rem;
transform: translateX(calc(13rem / 2));
line-height: 3rem;
width: 20rem;
border-radius: 1.5rem;

View File

@@ -29,10 +29,11 @@ const captureView = async ()=>{
formData.append('file', file)
const minioUrl = await uploadImage(formData, true)
ElMessage.warning('Your new view has been captured.')
emit('captureView', {
minioUrl,
nodeId: props?.currentData?.nodeId
nodeId: props?.currentData?.nodeId,
nodeType: props?.currentData?.nodeType,
superiorGenerateImg: props?.currentData?.superiorGenerateImg,
})
}
onMounted(()=>{

View File

@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
import gsap from 'gsap';
import * as THREE from 'three';
import { ThreeManager } from './threeTool copy'
import { ThreeManager } from './threeTool'
//const props = defineProps({
//})

View File

@@ -1,351 +0,0 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
import hdri from '@/assets/images/three/hdri.hdr'
interface ModelInfo {
box: THREE.Box3;
center: THREE.Vector3;
size: THREE.Vector3;
maxSize: number;
}
const CONFIG = {
hdriIntensity: 7.4,
exposureBase: 0.92,
backlightBoost: { min: 2.1, max: 4.8 }, // 背光增强系数
hdriUrl: hdri,
};
export class ThreeManager {
threeDom: HTMLElement;
scene: THREE.Scene;//场景对象
camera: THREE.PerspectiveCamera;//相机对象
renderer: THREE.WebGLRenderer;//渲染器对象
controls: OrbitControls;//轨道控制器对象
pointLight: THREE.AmbientLight;//环境光对象
studioLights: any;//工作室光对象数组
v1: THREE.Vector3;//相机前向向量
camDir: THREE.Vector3;//相机前向向量
camForward: THREE.Vector3;//相机前向向量
camToTarget: THREE.Vector3;//相机目标向量
currentModel: any;//当前模型对象
animate: any;//动画对象
modelInfo: ModelInfo;//模型信息对象
defaultSoftboxPositions: Array<{ x: number; y: number; z: number; intensity: number; w: number; h: number }> = [
{ x: 0, y: 5.8, z: 3.5, intensity: 3.5, w: 6, h: 4.8 }, // 主光
{ x: 0, y: 2.8, z: 7.5, intensity: 2.2, w: 5, h: 4 }, // 前光
{ x: 0, y: 2.8, z: -7.5, intensity: 6.5, w: 5, h: 4 }, // 背光
{ x: -7.5, y: 2.8, z: 0, intensity: 10.8, w: 5, h: 4 }, // 侧光
{ x: 7.5, y: 2.8, z: 0, intensity: 2.0, w: 5, h: 4 }, // 侧光
{ x: 0, y: -2.2, z: 3, intensity: 0.8, w: 9, h: 4 } // 地面反光板
];
constructor(threeDom: HTMLElement,config:any = {}){
this.threeDom = threeDom;
this.studioLights = []
//创建场景
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xffffff);
//创建相机
this.camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
this.camera.position.set(0, 1.5, 6); //设置相机位置
this.v1 = new THREE.Vector3();
this.camDir = new THREE.Vector3();
this.camForward = new THREE.Vector3();
this.camToTarget = new THREE.Vector3();
//设置渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance", preserveDrawingBuffer: true });
//设置环境光
this.scene.add(new THREE.AmbientLight(0xffffff, 0.15));
// 关键优化:物理光照与色彩管理
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = CONFIG.exposureBase;
this.renderer.physicallyCorrectLights = true; // 开启物理光照模式,光衰减更真实
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight); //设置渲染区域尺寸
this.renderer.setClearColor(0xffffff, 1); //设置背景颜色
threeDom.innerHTML = '';
threeDom.appendChild(this.renderer.domElement);
RectAreaLightUniformsLib.init();
//设置轨道控制器
this.controls = new OrbitControls(this.camera,this.renderer.domElement)//监听鼠标、键盘事件;
// controls.minDistance = 500; // 设置相机与焦点的最小距离
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
this.controls.mouseButtons = {
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转ROTATE)
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转ROTATE)
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
RIGHT:THREE.MOUSE.PAN // 右键 旋转默认拖动PAN
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转默认拖动PAN
}
this.controls.enableDamping = true;
}
/**
* 根据模型大小计算自适应配置
*/
calculateAdaptiveConfig(modelInfo: ModelInfo) {
const { size, maxSize, center } = modelInfo;
// 基础距离系数(可根据需要调整)
let distanceFactor = 1.5;
// 根据模型形状调整距离系数
const aspectRatio = size.x / size.y;
if (aspectRatio > 2) {
// 扁平模型,拉远一点
distanceFactor = 3.0;
} else if (aspectRatio < 0.5) {
// 高瘦模型,稍微拉近
distanceFactor = 2.0;
}
// 计算相机距离
const fov = this.camera.fov * (Math.PI / 180);
const cameraDistance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
// 灯光缩放系数
const lightScale = Math.max(0.5, Math.min(2.0, maxSize / 2.5));
// 强度缩放系数(模型越大,灯光需要越强)
const intensityScale = Math.max(0.6, Math.min(2.5, maxSize / 2));
console.log('自适应配置:', {
相机距离: cameraDistance,
灯光缩放: lightScale,
强度缩放: intensityScale,
目标中心: center
});
return {
cameraDistance,
lightScale,
intensityScale,
targetCenter: center.clone()
};
}
/**
* 获取模型信息(包围盒、中心点、尺寸等)
*/
getModelInfo(model: THREE.Object3D): ModelInfo {
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
console.log('模型信息:', {
中心点: center,
: { x: size.x, y: size.y, z: size.z },
最大尺寸: maxSize
});
return { box, center, size, maxSize };
}
/**
* 根据模型大小自适应创建柔光箱灯光
*/
createAdaptiveSoftboxes(modelInfo: ModelInfo) {
const { lightScale, intensityScale, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
// 清除现有灯光
this.studioLights.forEach(item => {
this.scene.remove(item.light);
});
this.studioLights = [];
// 根据模型大小创建自适应灯光
this.defaultSoftboxPositions.forEach(pos => {
const scaledX = pos.x * lightScale;
const scaledY = pos.y * lightScale;
const scaledZ = pos.z * lightScale;
const scaledW = pos.w * lightScale;
const scaledH = pos.h * lightScale;
const scaledIntensity = pos.intensity * intensityScale;
const light = new THREE.RectAreaLight(0xffffff, scaledIntensity, scaledW, scaledH);
light.position.set(
targetCenter.x + scaledX,
targetCenter.y + scaledY,
targetCenter.z + scaledZ
);
light.lookAt(targetCenter);
this.scene.add(light);
this.studioLights.push({
light,
offset: new THREE.Vector3(scaledX, scaledY, scaledZ),
baseIntensity: scaledIntensity
});
});
console.log('自适应灯光已创建,缩放系数:', lightScale, '强度系数:', intensityScale);
}
/**
* 根据模型大小调整相机位置
*/
adjustCameraToModel(modelInfo: ModelInfo) {
const { cameraDistance, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
// 正面俯视角度:相机在模型正前方,稍高于模型中心
// X 轴:模型中心(正面视角,不左右偏移)
// Y 轴:相机高度 = 模型中心高度 + 模型高度的 0.3 倍(俯视效果)
// Z 轴:相机距离 = 根据模型大小计算的距离
const x = targetCenter.x;
const y = targetCenter.y + modelInfo.size.y * 0.3; // 相机高度为模型中心的 0.3 倍
const z = targetCenter.z + cameraDistance;
this.camera.position.set(x, y, z);
this.controls.target.copy(targetCenter);
this.controls.update();
}
createSoftbox(x, y, z, intensity, w = 5, h = 4) {
const light = new THREE.RectAreaLight(0xffffff, intensity, w, h);
light.position.set(x, y, z);
light.lookAt(0, 0, 0);
this.scene.add(light);
// 存储 offset 向量副本,避免后续重复创建
this.studioLights.push({
light,
offset: new THREE.Vector3(x, y, z),
baseIntensity: intensity
});
}
async setHDRI(){
const rgbeLoader = new RGBELoader()
await rgbeLoader.load(CONFIG.hdriUrl, (texture)=>{
texture.mapping = THREE.EquirectangularReflectionMapping;
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
this.scene.environment = envMap;
this.scene.environmentIntensity = CONFIG.hdriIntensity;
pmremGenerator.dispose();
texture.dispose();
console.log('✅ HDRI Loaded');
});
this.createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
this.createSoftbox(0, 2.8, 7.5, 2.2); // 前光
this.createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
this.createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
this.createSoftbox(7.5, 2.8, 0, 2.0);
this.createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
}
// 更新工作室光位置
updateStudioLighting(){
this.camera.getWorldDirection(this.camDir);
this.studioLights.forEach(item => {
// 使用 applyQuaternion 同步灯光位置
this.v1.copy(item.offset).applyQuaternion(this.camera.quaternion);
item.light.position.copy(this.controls.target).add(this.v1);
item.light.lookAt(this.controls.target);
// 动态背光逻辑
if (item.offset.z < -2) {
this.v1.copy(item.light.position).sub(this.controls.target).normalize();
const dot = this.v1.dot(this.camDir);
const factor = THREE.MathUtils.lerp(CONFIG.backlightBoost.min, CONFIG.backlightBoost.max, Math.max(0, 1 - dot));
item.light.intensity = item.baseIntensity * factor;
}
});
}
//动态设置曝光
updateImagePipeline() {
let exposure = 1.0;
this.camera.getWorldDirection(this.camForward);
this.camToTarget.copy(this.controls.target).sub(this.camera.position).normalize();
const facing = Math.abs(this.camForward.dot(this.camToTarget));
const targetExposure = THREE.MathUtils.lerp(1.25, 0.90, facing);
exposure = THREE.MathUtils.lerp(exposure, targetExposure, 0.08);
this.renderer.toneMappingExposure = CONFIG.exposureBase * exposure;
}
async setModel(modelUrl,load){
await new Promise((resolve, reject) => {
const drac = new DRACOLoader()
drac.setDecoderPath('/draco/')
const loader = new GLTFLoader().setDRACOLoader(drac);
if (this.currentModel) {
this.scene.remove(this.currentModel);
this.dispose(this.currentModel); // 核心优化:释放显存
}
loader.load(modelUrl,
(gltf) => {
this.currentModel = gltf.scene;
// 遍历模型:增强材质表现
this.currentModel.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
if (child.material) {
child.material.envMapIntensity = 1.2; // 增强 HDRI 反射强度
}
}
});
const box = new THREE.Box3().setFromObject(this.currentModel);
const center = box.getCenter(new THREE.Vector3());
// this.currentModel.position.sub(center);
this.scene.add(this.currentModel);
this.modelInfo = this.getModelInfo(this.currentModel)
// 根据模型大小调整相机位置
this.adjustCameraToModel(this.modelInfo);
// 根据模型大小创建自适应灯光
this.createAdaptiveSoftboxes(this.modelInfo);
resolve('')
},
(xhr: any) => { // 加载进度回调
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
load.value.progress = percent
console.log('模型加载进度:', percent);
},
(error: any) => { // 加载失败回调
console.error('模型加载失败:', error);
resolve('')
}
);
})
}
operation(){
let this_ = this
const animate = () => {
requestAnimationFrame(animate);
this.controls.update();
this.updateStudioLighting();
this.updateImagePipeline();
this.renderer.render(this.scene, this.camera);
}
animate();
}
// 释放模型资源
dispose(obj){
if (!obj) return;
obj.traverse(node => {
if (node.isMesh) {
if (node.geometry) node.geometry.dispose();
if (node.material) {
if (Array.isArray(node.material)) {
node.material.forEach(m => m.dispose());
} else {
node.material.dispose();
}
}
}
});
}
exportAsImage(){
return this.renderer.domElement.toDataURL('image/png');
}
}

View File

@@ -3,291 +3,349 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
import hdri from '@/assets/images/three/hdri.hdr'
export const initThree = async (threeDom)=>{
const scene = new THREE.Scene();
const group = new THREE.Group()
scene.add(group)
const studioLights = []
//创建相机对象
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
const camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 1000);
camera.position.set(0, 1.5, 5);
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
/**
* 创建渲染器对象
*/
interface ModelInfo {
box: THREE.Box3;
center: THREE.Vector3;
size: THREE.Vector3;
maxSize: number;
}
const CONFIG = {
hdriIntensity: 7.4,
exposureBase: 0.92,
backlightBoost: { min: 2.1, max: 4.8 }, // 背光增强系数
hdriUrl: hdri,
};
export class ThreeManager {
threeDom: HTMLElement;
scene: THREE.Scene;//场景对象
camera: THREE.PerspectiveCamera;//相机对象
renderer: THREE.WebGLRenderer;//渲染器对象
controls: OrbitControls;//轨道控制器对象
const width = threeDom.offsetWidth; //窗口宽度
const height = threeDom.offsetHeight; //窗口高度
const renderer = new THREE.WebGLRenderer({
antialias: true,
logarithmicDepthBuffer: true,//深度缓存 防止模型闪烁重影
});
pointLight: THREE.AmbientLight;//环境光对象
studioLights: any;//工作室光对象数组
v1: THREE.Vector3;//相机前向向量
camDir: THREE.Vector3;//相机前向向量
camForward: THREE.Vector3;//相机前向向量
camToTarget: THREE.Vector3;//相机目标向量
renderer.toneMapping = THREE.ACESFilmicToneMapping;//设置色调
renderer.toneMappingExposure = 1.3;
currentModel: any;//当前模型对象
animate: any;//动画对象
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height); //设置渲染区域尺寸
renderer.setClearColor(0xffffff, 1); //设置背景颜色
threeDom.innerHTML = '';
threeDom.appendChild(renderer.domElement);
modelInfo: ModelInfo;//模型信息对象
defaultSoftboxPositions: Array<{ x: number; y: number; z: number; intensity: number; w: number; h: number }> = [
{ x: 0, y: 5.8, z: 3.5, intensity: 3.5, w: 6, h: 4.8 }, // 主光
{ x: 0, y: 2.8, z: 7.5, intensity: 2.2, w: 5, h: 4 }, // 前光
{ x: 0, y: 2.8, z: -7.5, intensity: 6.5, w: 5, h: 4 }, // 背光
{ x: -7.5, y: 2.8, z: 0, intensity: 10.8, w: 5, h: 4 }, // 侧光
{ x: 7.5, y: 2.8, z: 0, intensity: 2.0, w: 5, h: 4 }, // 侧光
{ x: 0, y: -2.2, z: 3, intensity: 0.8, w: 9, h: 4 } // 地面反光板
];
constructor(threeDom: HTMLElement,config:any = {}){
// 设置渲染器大小
this.threeDom = threeDom;
this.studioLights = []
//创建场景
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xffffff);
//创建相机
this.camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
this.camera.position.set(0, 1.5, 6); //设置相机位置
this.v1 = new THREE.Vector3();
this.camDir = new THREE.Vector3();
this.camForward = new THREE.Vector3();
this.camToTarget = new THREE.Vector3();
//设置渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance", preserveDrawingBuffer: true });
//设置环境光
this.scene.add(new THREE.AmbientLight(0xffffff, 0.15));
// 关键优化:物理光照与色彩管理
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = CONFIG.exposureBase;
this.renderer.physicallyCorrectLights = true; // 开启物理光照模式,光衰减更真实
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight); //设置渲染区域尺寸
this.renderer.setClearColor(0xffffff, 1); //设置背景颜色
threeDom.innerHTML = '';
threeDom.appendChild(this.renderer.domElement);
RectAreaLightUniformsLib.init();
//设置轨道控制器
this.controls = new OrbitControls(this.camera,this.renderer.domElement)//监听鼠标、键盘事件;
// controls.minDistance = 500; // 设置相机与焦点的最小距离
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
this.controls.mouseButtons = {
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转ROTATE)
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转ROTATE)
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
RIGHT:THREE.MOUSE.PAN // 右键 旋转默认拖动PAN
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转默认拖动PAN
}
this.controls.enableDamping = true;
const controls = new OrbitControls(camera,renderer.domElement)//监听鼠标、键盘事件;
// controls.minDistance = 500; // 设置相机与焦点的最小距离
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
controls.mouseButtons = {
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转ROTATE)
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转ROTATE)
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
RIGHT:THREE.MOUSE.PAN // 右键 旋转默认拖动PAN
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转默认拖动PAN
}
//使用hdri文件
try {
const rgbeLoader = new RGBELoader()
const hdrTexture = await rgbeLoader.loadAsync(hdri)
hdrTexture.mapping = THREE.EquirectangularMapping
/**
* 根据模型大小计算自适应配置
*/
calculateAdaptiveConfig(modelInfo: ModelInfo) {
const { size, maxSize, center } = modelInfo;
// 基础距离系数(可根据需要调整)
let distanceFactor = 1.5;
// 根据模型形状调整距离系数
const aspectRatio = size.x / size.y;
if (aspectRatio > 2) {
// 扁平模型,拉远一点
distanceFactor = 3.0;
} else if (aspectRatio < 0.5) {
// 高瘦模型,稍微拉近
distanceFactor = 2.0;
}
// 计算相机距离
const fov = this.camera.fov * (Math.PI / 180);
const cameraDistance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
// 灯光缩放系数
const lightScale = Math.max(0.5, Math.min(2.0, maxSize / 2.5));
// 强度缩放系数(模型越大,灯光需要越强)
const intensityScale = Math.max(0.6, Math.min(2.5, maxSize / 2));
console.log('自适应配置:', {
相机距离: cameraDistance,
灯光缩放: lightScale,
强度缩放: intensityScale,
目标中心: center
});
return {
cameraDistance,
lightScale,
intensityScale,
targetCenter: center.clone()
};
}
/**
* 获取模型信息(包围盒、中心点、尺寸等)
*/
getModelInfo(model: THREE.Object3D): ModelInfo {
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
console.log('模型信息:', {
中心点: center,
: { x: size.x, y: size.y, z: size.z },
最大尺寸: maxSize
});
return { box, center, size, maxSize };
}
/**
* 根据模型大小自适应创建柔光箱灯光
*/
createAdaptiveSoftboxes(modelInfo: ModelInfo) {
const { lightScale, intensityScale, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
// 清除现有灯光
this.studioLights.forEach(item => {
this.scene.remove(item.light);
});
this.studioLights = [];
// 设置环境贴图(影响材质反射)
scene.environment = hdrTexture
// 可选:同时设置为背景
scene.background = hdrTexture
// 根据模型大小创建自适应灯光
this.defaultSoftboxPositions.forEach(pos => {
const scaledX = pos.x * lightScale;
const scaledY = pos.y * lightScale;
const scaledZ = pos.z * lightScale;
const scaledW = pos.w * lightScale;
const scaledH = pos.h * lightScale;
const scaledIntensity = pos.intensity * intensityScale;
const light = new THREE.RectAreaLight(0xffffff, scaledIntensity, scaledW, scaledH);
light.position.set(
targetCenter.x + scaledX,
targetCenter.y + scaledY,
targetCenter.z + scaledZ
);
light.lookAt(targetCenter);
this.scene.add(light);
this.studioLights.push({
light,
offset: new THREE.Vector3(scaledX, scaledY, scaledZ),
baseIntensity: scaledIntensity
});
});
console.log('自适应灯光已创建,缩放系数:', lightScale, '强度系数:', intensityScale);
}
/**
* 根据模型大小调整相机位置
*/
adjustCameraToModel(modelInfo: ModelInfo) {
const { cameraDistance, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
// 正面俯视角度:相机在模型正前方,稍高于模型中心
// X 轴:模型中心(正面视角,不左右偏移)
// Y 轴:相机高度 = 模型中心高度 + 模型高度的 0.3 倍(俯视效果)
// Z 轴:相机距离 = 根据模型大小计算的距离
const x = targetCenter.x;
const y = targetCenter.y + modelInfo.size.y * 0.3; // 相机高度为模型中心的 0.3 倍
const z = targetCenter.z + cameraDistance;
console.log('HDR 环境贴图加载成功:', hdri)
} catch (error) {
console.error('HDR 加载失败:', error)
// 降级方案:使用环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
scene.add(ambientLight)
this.camera.position.set(x, y, z);
this.controls.target.copy(targetCenter);
this.controls.update();
}
/**
* 光源设置
*/
//点光源
/**
* AmbientLight 环境光
PointLight 点光源
DirectionalLight 平行光,比如太阳光
SpotLight 聚光源
*/
//设置环境光全亮
//环境光
const pointLight = new THREE.AmbientLight(0xffffff,.8);
// scene.add(pointLight);
function createSoftbox(x, y, z, intensity, w = 5, h = 4) {
createSoftbox(x, y, z, intensity, w = 5, h = 4) {
const light = new THREE.RectAreaLight(0xffffff, intensity, w, h);
light.position.set(x, y, z);
light.lookAt(0, 0, 0);
scene.add(light);
this.scene.add(light);
// 存储 offset 向量副本,避免后续重复创建
studioLights.push({
this.studioLights.push({
light,
offset: new THREE.Vector3(x, y, z),
baseIntensity: intensity
});
}
// 灯光布局优化
createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
createSoftbox(0, 2.8, 7.5, 2.2); // 前光
createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
createSoftbox(7.5, 2.8, 0, 2.0);
createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
// const pointLight = new THREE.AmbientLight(0xffffff,1.0);
// pointLight.intensity = 1.2//光源强度
// pointLight.castShadow = true//开启阴影
// pointLight.shadow.mapSize = new THREE.Vector2(width, height)
// scene.add(pointLight); //点光源添加到场景中
// pointLight.position.set(400, 200, 300); //点光源位置
// pointLight.position.y = 100;
// pointLight.position.z = 50;
// pointLight.position.x = 100;
// let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小
// let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", })
// let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial);
// floorMesh.rotation.x = -0.5 * Math.PI;
// floorMesh.receiveShadow = true;
// floorMesh.position.y = -0.001;
// scene.add(floorMesh);
const textureLoader = new THREE.TextureLoader();
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
scene.background = new THREE.Color("#fff");
return {scene,group,camera,renderer,controls,pointLight,studioLights}
}
export const clearModel = (group,scene)=>{
const oldGroup:any = group.value;
group.value = new THREE.Group();
scene.value.add(group.value);
scene.value.remove(oldGroup);
}
// 计算模型包围盒
export const getModelInfo = (model: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
return {
box,
center,
size,
maxSize
};
};
// 根据模型信息计算相机位置
export const calculateCameraPosition = (
modelInfo: ReturnType<typeof getModelInfo>,
camera: THREE.PerspectiveCamera,
options?: {
distanceFactor?: number; // 距离系数默认1.5
heightFactor?: number; // 高度偏移系数默认0.3
angle?: number; // 观察角度,默认正前方
}
) => {
const { center, size, maxSize } = modelInfo;
const fov = camera.fov * (Math.PI / 180);
const distanceFactor = options?.distanceFactor ?? 1.5;
const heightFactor = options?.heightFactor ?? 0.3;
// 计算合适的相机距离
const distance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
// 根据角度计算相机位置
const angle = options?.angle ?? 0; // 0表示正前方
return {
position: new THREE.Vector3(
center.x + distance * Math.sin(angle),
center.y + size.y * heightFactor,
center.z + distance * Math.cos(angle)
),
target: center.clone(),
distance,
center,
size
};
};
// 根据模型信息计算光源位置
export const calculateLightPosition = (
modelInfo: ReturnType<typeof getModelInfo>,
options?: {
xFactor?: number; // X轴偏移系数默认0.5
yFactor?: number; // Y轴偏移系数默认0.8
zFactor?: number; // Z轴偏移系数默认0.5
}
) => {
const { center, size } = modelInfo;
const xFactor = options?.xFactor ?? 0.5;
const yFactor = options?.yFactor ?? 0.8;
const zFactor = options?.zFactor ?? 0.5;
return new THREE.Vector3(
center.x + size.x * xFactor,
center.y + size.y * yFactor,
center.z + size.z * zFactor
);
};
export const addModel = async (
url: any,
controls: OrbitControls,
camera: THREE.PerspectiveCamera,
pointLight: THREE.DirectionalLight,
group: THREE.Group,
load: any
) => {
await new Promise((resolve, reject) => {
const fbxLoader = new GLTFLoader();
const drac = new DRACOLoader()
drac.setDecoderPath('/draco/')
fbxLoader.setDRACOLoader(drac)
fbxLoader.load(url,
(obj: any) => {
const scene = obj.scene;
scene.traverse((child: any) => {
if (child.isMesh) {
// 如果是基础材质,转换为标准材质
if (child.material instanceof THREE.MeshBasicMaterial) {
const oldMat = child.material;
child.material = new THREE.MeshStandardMaterial({
map: oldMat.map,
color: oldMat.color,
roughness: 0.4,
metalness: 0
});
}
// 如果是标准材质,调整粗糙度
else if (child.material instanceof THREE.MeshStandardMaterial) {
child.material.roughness = 0.4;
child.material.metalness = 0;
}
}
});
// 获取模型信息
const modelInfo = getModelInfo(scene);
const { position: cameraPos, target } = calculateCameraPosition(
modelInfo,
camera.value,
{
distanceFactor: 1.5,
heightFactor: 0.3,
angle: 0 // 正前方
}
);
// 设置相机位置
camera.value.position.copy(cameraPos);
// 设置控制器目标点
controls.value.target.copy(target);
// 计算并设置光源位置
const lightPos = calculateLightPosition(modelInfo);
pointLight.value.position.copy(lightPos);
// 将模型添加到场景
group.value.add(scene);
// 可选:将模型信息存储在模型上,方便后续使用
(scene as any).userData.modelInfo = modelInfo;
resolve('')
},
(xhr: any) => { // 加载进度回调
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
load.value.progress = percent
console.log('模型加载进度:', percent);
},
(error: any) => { // 加载失败回调
console.error('模型加载失败:', error);
reject('')
}
)
})
}
// 导出当前视图为图片
export const exportAsImage = (renderer, camera, scene, filename = 'model.png')=>{
// 渲染当前场景
renderer.render(scene, camera)
async setHDRI(){
const rgbeLoader = new RGBELoader()
await rgbeLoader.load(CONFIG.hdriUrl, (texture)=>{
texture.mapping = THREE.EquirectangularReflectionMapping;
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
// 获取 canvas 数据
const canvas = renderer.domElement
const dataURL = canvas.toDataURL('image/png')
return dataURL
this.scene.environment = envMap;
this.scene.environmentIntensity = CONFIG.hdriIntensity;
pmremGenerator.dispose();
texture.dispose();
console.log('✅ HDRI Loaded');
});
this.createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
this.createSoftbox(0, 2.8, 7.5, 2.2); // 前光
this.createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
this.createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
this.createSoftbox(7.5, 2.8, 0, 2.0);
this.createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
}
// 更新工作室光位置
updateStudioLighting(){
this.camera.getWorldDirection(this.camDir);
this.studioLights.forEach(item => {
// 使用 applyQuaternion 同步灯光位置
this.v1.copy(item.offset).applyQuaternion(this.camera.quaternion);
item.light.position.copy(this.controls.target).add(this.v1);
item.light.lookAt(this.controls.target);
// 动态背光逻辑
if (item.offset.z < -2) {
this.v1.copy(item.light.position).sub(this.controls.target).normalize();
const dot = this.v1.dot(this.camDir);
const factor = THREE.MathUtils.lerp(CONFIG.backlightBoost.min, CONFIG.backlightBoost.max, Math.max(0, 1 - dot));
item.light.intensity = item.baseIntensity * factor;
}
});
}
//动态设置曝光
updateImagePipeline() {
let exposure = 1.0;
this.camera.getWorldDirection(this.camForward);
this.camToTarget.copy(this.controls.target).sub(this.camera.position).normalize();
const facing = Math.abs(this.camForward.dot(this.camToTarget));
const targetExposure = THREE.MathUtils.lerp(1.25, 0.90, facing);
exposure = THREE.MathUtils.lerp(exposure, targetExposure, 0.08);
this.renderer.toneMappingExposure = CONFIG.exposureBase * exposure;
}
async setModel(modelUrl,load){
await new Promise((resolve, reject) => {
const drac = new DRACOLoader()
drac.setDecoderPath('/draco/')
const loader = new GLTFLoader().setDRACOLoader(drac);
if (this.currentModel) {
this.scene.remove(this.currentModel);
this.dispose(this.currentModel); // 核心优化:释放显存
}
loader.load(modelUrl,
(gltf) => {
this.currentModel = gltf.scene;
// 遍历模型:增强材质表现
this.currentModel.traverse(child => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
if (child.material) {
child.material.envMapIntensity = 1.2; // 增强 HDRI 反射强度
}
}
});
const box = new THREE.Box3().setFromObject(this.currentModel);
const center = box.getCenter(new THREE.Vector3());
// this.currentModel.position.sub(center);
this.scene.add(this.currentModel);
this.modelInfo = this.getModelInfo(this.currentModel)
// 根据模型大小调整相机位置
this.adjustCameraToModel(this.modelInfo);
// 根据模型大小创建自适应灯光
this.createAdaptiveSoftboxes(this.modelInfo);
resolve('')
},
(xhr: any) => { // 加载进度回调
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
load.value.progress = percent
console.log('模型加载进度:', percent);
},
(error: any) => { // 加载失败回调
console.error('模型加载失败:', error);
resolve('')
}
);
})
}
operation(){
let this_ = this
const animate = () => {
requestAnimationFrame(animate);
this.controls.update();
this.updateStudioLighting();
this.updateImagePipeline();
this.renderer.render(this.scene, this.camera);
}
animate();
}
// 释放模型资源
dispose(obj){
if (!obj) return;
obj.traverse(node => {
if (node.isMesh) {
if (node.geometry) node.geometry.dispose();
if (node.material) {
if (Array.isArray(node.material)) {
node.material.forEach(m => m.dispose());
} else {
node.material.dispose();
}
}
}
});
}
exportAsImage(){
return this.renderer.domElement.toDataURL('image/png');
}
}

View File

@@ -8,8 +8,8 @@
</div>
<div class="control" v-else>
<div class="icon"><svg-icon name="upload" size="17" size-unit="px" /></div>
<div class="txt">{{ tip }}</div>
<div class="btn" @click="onSelectFile">Select File</div>
<div class="txt">{{ tip || $t('FlowCanvas.uploadFiles') }}</div>
<div class="btn" @click="onSelectFile">{{ $t('FlowCanvas.selectFile') }}</div>
</div>
</div>
</template>
@@ -20,7 +20,7 @@
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: { type: [File, Object, String, null] },
tip: { type: String, default: 'Upload your files' }
tip: { type: String, default: '' }
})
const data = reactive({
file: null

View File

@@ -258,6 +258,8 @@
nodeManager.createResultNode({
data: {
superiorID_: captureData.nodeId,
superiorNodeType: captureData.nodeType,
superiorGenerateImg: captureData.superiorGenerateImg,
data: {
selectable: false,
imageProcessTasks: [

View File

@@ -9,6 +9,7 @@ interface NodeData {
superiorID?: string// 上级节点ID有连接线
superiorID_?: string// 上级节点ID没有连接线
superiorNodeType?: string// 上级节点类型
superiorGenerateImg?: string// 上级生成节点图片
disableDelete?: boolean// 是否禁用删除
disableCopy?: boolean// 是否禁用复制
createIndexPosition?: number// 创建索引位置
@@ -97,6 +98,7 @@ export class NodeManager {
}
/** 创建结果节点 */
createResultNode(options?: NodeOptions) {
console.log(options)
const options_ = {
...(options ? options : {}),
component: NODE_COMPONENT.RESULT_IMAGE,

View File

@@ -10,31 +10,31 @@ const t = i18n.global.t
//推送到对话框的助手
const chatAssistant = {
[NODE_DATATYPE.TO_REAL_STYLE]:{
content: t('flowCanvas.toRealStyleDesignAssistant'),
content: t('FlowCanvas.toRealStyleDesignAssistant'),
nodeType:NODE_DATATYPE.TO_REAL_STYLE,
},
[NODE_DATATYPE.CANVAS_MODE]:{
content: t('flowCanvas.surfaceEditCanvasDesignAssistant'),
content: t('FlowCanvas.surfaceEditCanvasDesignAssistant'),
nodeType:NODE_DATATYPE.CANVAS_MODE,
},
[NODE_DATATYPE.Fast_MODE]:{
content: t('flowCanvas.surfaceEditAIDesignAssistant'),
content: t('FlowCanvas.surfaceEditAIDesignAssistant'),
nodeType:NODE_DATATYPE.Fast_MODE,
},
[NODE_DATATYPE.COLOR_PALETTE]:{
content: t('flowCanvas.colorPaletteDesignAssistant'),
content: t('FlowCanvas.colorPaletteDesignAssistant'),
nodeType:NODE_DATATYPE.COLOR_PALETTE,
},
[NODE_DATATYPE.SCENE_COMPOSITION]:{
content: t('flowCanvas.threeModelDesignAssistant'),
content: t('FlowCanvas.threeModelDesignAssistant'),
nodeType:NODE_DATATYPE.SCENE_COMPOSITION,
},
[NODE_DATATYPE.TO_3D_MODEL]:{
content: t('flowCanvas.threeModelDesignAssistant'),
content: t('FlowCanvas.threeModelDesignAssistant'),
nodeType:NODE_DATATYPE.TO_3D_MODEL,
},
[NODE_DATATYPE.TO_3VIEW]:{
content: t('flowCanvas.threeModelDesignAssistant'),
content: t('FlowCanvas.threeModelDesignAssistant'),
nodeType:NODE_DATATYPE.TO_3VIEW,
},
}
@@ -167,7 +167,7 @@ export class StateManager {
getSuperiorNodeImage(superiorID: string) {
const superiorNode = this.getNodeById(superiorID)
if(!superiorNode){
// ElMessage.error(t('flowCanvas.cannotFindSuperiorImage'))
// ElMessage.error(t('FlowCanvas.cannotFindSuperiorImage'))
return null
}
const superiorNodeUrl = superiorNode.data.data.imageProcessTasks.filter((item)=>{
@@ -179,7 +179,7 @@ export class StateManager {
async deleteSubordinateAllNodes(id: string,{ isElMessageBox } = { isElMessageBox: false }) {
const node = this.getNodeById(id)
if (!node) return console.warn(`没有找到指定id:${id}`)
if (node.data.disableDelete) return ElMessage.error(t('flowCanvas.initialNodeProhibited'))
if (node.data.disableDelete) return ElMessage.error(t('FlowCanvas.initialNodeProhibited'))
const result = [node]
@@ -197,11 +197,11 @@ export class StateManager {
if (isElMessageBox) {
deletePromise = await new Promise<void>((resolve, reject) => {
ElMessageBox.confirm(
result.length > 1 ? t('flowCanvas.deleteSubordinateCard') : t('flowCanvas.deleteCardConfirm'),
result.length > 1 ? t('FlowCanvas.deleteSubordinateCard') : t('FlowCanvas.deleteCardConfirm'),
'',
{
confirmButtonText: t('flowCanvas.confirm'),
cancelButtonText: t('flowCanvas.cancel'),
confirmButtonText: t('FlowCanvas.confirm'),
cancelButtonText: t('FlowCanvas.cancel'),
}
).then(() => {resolve(true)
}).catch(() => {

View File

@@ -24,6 +24,7 @@ export const NODE_DATATYPE = {
RESULT_IMAGE: 'result-image',
CARDS_SELECT: 'cards-select',
TO_REAL_STYLE: 'to-real-style',
TO_REAL_VARIANTS: 'to-real-variants',
SURFACE_EDIT: 'surface-edit',
CANVAS_MODE: 'canvas-mode',
Fast_MODE: 'fast-mode',
@@ -39,6 +40,7 @@ export const NODE_DATATIER = {
RESULT_IMAGE: 0,
CARDS_SELECT: 0,
TO_REAL_STYLE: 1,
TO_REAL_VARIANTS: 1,
SURFACE_EDIT: 1,
CANVAS_MODE: 1,
Fast_MODE: 1,

View File

@@ -193,7 +193,7 @@ export default {
Confirm: 'Confirm',
export: 'Export'
},
flowCanvas: {
FlowCanvas: {
deleteCardConfirm: 'Are you sure you want to delete this function card?',
confirm: 'Confirm',
cancel: 'Cancel',
@@ -214,7 +214,87 @@ export default {
threeModelDesignAssistant:
"🔄 I'll turn your render into a 3D model you can rotate and look at from any angle. I'd recommend paying close attention to the corner joints, leg proportions, and seat depth — these are the spots that are easy to miss in a sketch but tend to cause the most trouble during prototyping. Better to catch them now while it's easy to fix.",
to3DViewDesignAssistant:
"📐 We're at the final step! I'll export your 3D model as front, side, and top view!"
"📐 We're at the final step! I'll export your 3D model as front, side, and top view!",
toRealVariantsShortcut1Label: 'Change the...',
toRealVariantsShortcut1Value: 'Change the sofa backrest shape.',
toRealVariantsShortcut2Label: 'The chair legs...',
toRealVariantsShortcut2Value: 'The chair legs were modified.',
toRealVariantsShortcut3Label: 'Adjust the...',
toRealVariantsShortcut3Value: "Adjust the chair's armrest design.",
toRealVariantsShortcut4Label: 'Replace...',
toRealVariantsShortcut4Value: 'Replace tabletop shape with drawn shape. ',
toRealVariantsShortcut5Label: 'Modified the outer...',
toRealVariantsShortcut5Value: 'Modified the outer contour of the chair back.',
toRealVariantsPlaceholder: 'Enter the furniture details you modified...',
export: 'Export',
edit: 'Edit',
generate: 'Generate',
copy: 'Copy',
delete: 'Delete',
flipHorizontal: 'Flip horizontal',
flipVertical: 'Flip vertical',
clickEditText: 'Click to edit text',
// 卡片工具标题
selectCardsTitle: 'Advanced Tools',
toRealStyleTitle: 'To Real Style',
toRealVariantsTitle: 'To Real Variants',
surfaceEditTitle: 'Surface Edit',
surfaceEditCanvasTitle: 'Surface Edit (Canvas)',
sceneCompositionTitle: 'Scene Composition',
colorPaletteTitle: 'Color Palette',
to3DModelTitle: 'To 3D Model',
to3ViewTitle: 'To 3D View',
// 卡片工具
print: 'Print',
settings: 'Settings',
angle: 'Angle',
scale: 'Scale',
gapX: 'Gap X',
gapY: 'Gap Y',
offset: 'Offset',
size: 'Size',
prompt: 'Prompt',
promptDefaultPlaceholder: 'Enter the scene you want to describe...',
mode: 'Mode',
advancedMode: 'Advanced',
normalMode: 'Normal',
chooseColor: 'Choose Color',
output: 'Output',
chooseStyle: 'Choose Style',
colorful: 'Colorful',
minimalist: 'Minimalist',
modernist: 'Modernist',
bauhaus: 'Bauhaus',
mintage: 'Mintage',
industrial: 'Industrial',
futuristic: 'Futuristic',
elegant: 'Elegant',
organic: 'Organic',
calm: 'Calm',
abstract: 'Abstract',
kitschCore: 'Kitsch-core',
sophisticated: 'Sophisticated',
maximalism: 'Maximalism',
clean: 'Clean',
brightColors: 'Bright Colors',
luxurious: 'Luxurious',
boldColors: 'Bold Colors',
brutalism: 'Brutalism',
image: 'Image',
_3DModel: '3D Model',
toRealStyleShortcut1Label: 'Change the...',
toRealStyleShortcut1Value: 'Change the style to a realistic design. ',
toRealStyleShortcut2Label: 'Bright Colors...',
toRealStyleShortcut2Value: 'Bright colors with modern patterns, change the style to a realistic furniture design. ',
toRealStyleShortcut3Label: 'Make the...',
toRealStyleShortcut3Value: "RMake the structure more refined and balanced, change the style to a realistic furniture style. ",
toRealStyleShortcut4Label: 'Imagine...',
toRealStyleShortcut4Value: 'Imagine this furniture with detailed fabric textures, change the style to a realistic design. ',
toRealStyleShortcut5Label: 'Wood Materials with...',
toRealStyleShortcut5Value: 'Wood materials with natural oak texture and soft fabric, change the style to a realistic furniture design.',
// 上传文件组件
selectFile: 'Select File',
uploadFiles: 'Upload your files',
},
assistant: {
inputPlaceholder: 'Ask anything'
@@ -224,6 +304,14 @@ export default {
loading: 'Loading',
download: 'Download',
captureView: 'Capture View',
propertiesInformation: 'Properties Information',
_3DAsset: '3D Asset',
fileFormat: 'File Format:glb.',
transform: 'Transform',
dimensions: 'Dimensions',
width: 'Width',
height: 'Height',
depth: 'Depth',
},
DepthCanvas: {
layer: 'Layer',

View File

@@ -189,7 +189,7 @@ export default {
delete: '删除',
edit: '编辑'
},
flowCanvas: {
FlowCanvas: {
deleteCardConfirm: '确定要删除该功能卡片吗?',
confirm: '确认',
cancel: '取消',
@@ -209,7 +209,87 @@ export default {
threeModelDesignAssistant:
'🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。',
to3DViewDesignAssistant:
'📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!'
'📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!',
toRealVariantsShortcut1Label: '修改...',
toRealVariantsShortcut1Value: '修改了沙发靠背的形状.',
toRealVariantsShortcut2Label: '椅子腿...',
toRealVariantsShortcut2Value: '修改了椅子腿的形状.',
toRealVariantsShortcut3Label: '更换...',
toRealVariantsShortcut3Value: "更换了椅子的扶手设计.",
toRealVariantsShortcut4Label: '替换...',
toRealVariantsShortcut4Value: '桌面形状完全换成画成的形状.',
toRealVariantsShortcut5Label: '修改...',
toRealVariantsShortcut5Value: '修改了椅背的外轮廓.',
toRealVariantsPlaceholder: '请输入修改后的家具详情...',
export: '导出',
edit: '编辑',
generate: '生成',
copy: '复制',
delete: '删除',
flipHorizontal: '水平翻转',
flipVertical: '垂直翻转',
clickEditText: '点击编辑文本',
// 卡片工具标题
selectCardsTitle: '高级工具',
toRealStyleTitle: '真实风格',
toRealVariantsTitle: '真实风格变体',
surfaceEditTitle: '表面编辑',
surfaceEditCanvasTitle: '表面编辑Canvas',
sceneCompositionTitle: '场景构图',
colorPaletteTitle: '颜色调色板',
to3DModelTitle: '3D模型',
to3ViewTitle: '3视图',
// 卡片工具
print: '印花',
settings: '设置',
angle: '角度',
scale: '缩放',
gapX: '水平间距',
gapY: '垂直间距',
offset: '偏移量',
size: '大小',
prompt: '提示',
promptDefaultPlaceholder: '请输入你想要描述的场景…',
mode: '模式',
advancedMode: '高级',
normalMode: '普通',
chooseColor: '选择颜色',
output: '输出',
chooseStyle: '选择风格',
colorful: '多彩',
minimalist: '极简',
modernist: '现代',
bauhaus: '包豪斯',
mintage: '复古',
industrial: '工业',
futuristic: '未来',
elegant: '优雅',
organic: '有机',
calm: '平静',
abstract: '抽象',
kitschCore: '媚俗核心',
sophisticated: '精致',
maximalism: '极繁',
clean: '干净',
brightColors: '明亮色彩',
luxurious: '豪华',
boldColors: '大胆色彩',
brutalism: '粗野主义',
image: '图片',
_3DModel: '3D模型',
toRealStyleShortcut1Label: '将图像风格...',
toRealStyleShortcut1Value: '将图像风格转换为真实画风。.',
toRealStyleShortcut2Label: '采用明亮色彩...',
toRealStyleShortcut2Value: '采用明亮色彩与现代图案,将家具图像风格转换为真实画风。',
toRealStyleShortcut3Label: '使结构更加...',
toRealStyleShortcut3Value: "使结构更加精致与平衡,将家具图像风格转换为真实画风。",
toRealStyleShortcut4Label: '想象该家具具有...',
toRealStyleShortcut4Value: '想象该家具具有细腻的织物纹理,将家具图像风格转换为真实画风。',
toRealStyleShortcut5Label: '使用天然橡木纹理...',
toRealStyleShortcut5Value: '使用天然橡木纹理的木质材料与柔软织物,将家具图像风格转换为真实画风。',
// 上传文件组件
selectFile: '选择文件',
uploadFiles: '上传文件',
},
assistant: {
inputPlaceholder: '请输入'
@@ -219,6 +299,14 @@ export default {
loading: '加载中',
download: '下载',
captureView: '捕获视图',
propertiesInformation: '属性信息',
_3DAsset: '3D资产',
fileFormat: '文件格式glb',
transform: '变换',
dimensions: '尺寸',
width: '宽度',
height: '高度',
depth: '深度',
},
DepthCanvas: {
layer: '图层',

View File

@@ -13,7 +13,11 @@
const url =
'https://www.minio-api.aida.com.hk/fida-user/2/d8512e53-f016-4ad6-8245-2f304d89e7b2.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T032733Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=25e5ec227a0ca22942e71eff3a4f07a23f8812ff3db5522e1466b3a77288be70'
const openCanvas = () => {
myEvent.emit('openFlowCanvas', { url })
myEvent.emit('openFlowCanvas', {
url,
imgId: '69bcaae11e0cee430b750050',
nodeId: '69cde574a510db41350b404c'
})
}
const openDepthCanvas = () => {
myEvent.emit('openDepthCanvas', {