Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
|
||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
||||
|
||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="753" height="753" viewBox="0 0 753 753" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M634.536 365.274L634.824 364.392L697.737 132.952H631.864L613.396 200.848H363.698L360.43 201.55C272.317 220.226 254.644 306.834 253.94 310.508L253.362 313.497V419.411C180.124 419.159 122.772 418.871 121.382 418.835L121.039 482.264C126.671 482.3 182.651 482.606 253.38 482.84V632.625H316.96V483.038C345.627 483.11 374.98 483.128 403.051 483.128C467.299 483.128 461.451 482.912 489.865 482.264C493.114 482.192 496.255 482.012 499.378 481.76L608.071 632.625H686.364L562.128 460.166C613.089 427.209 633.434 368.678 634.572 365.274H634.536ZM488.403 418.871C448.579 419.753 411.121 419.789 316.942 419.573V320.251C319.668 310.255 332.142 274.11 370.702 264.277H596.175L574.042 345.752C571.028 353.928 545.845 417.574 488.403 418.889V418.871Z" fill="black"/>
|
||||
<path d="M91.0127 418.726H54.2764V482.498H91.0127V418.726Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 942 B |
@@ -205,11 +205,13 @@ export const sketchAddPrintApi = (data:sketchAddPrintData) => {
|
||||
* @param data 图片转3d的参数
|
||||
* @param data.sketchId sketch id
|
||||
* @param data.imageUrls 进行生成的图片。minio地址和正常地址都可以
|
||||
* @param data.mode 选择的模型
|
||||
* @returns 图片转3d
|
||||
*/
|
||||
export interface sketchToThreeData {
|
||||
sketchId?: string
|
||||
imageUrls?: Array<string>
|
||||
mode?: string
|
||||
}
|
||||
export const sketchToThreeApi = (data:sketchToThreeData) => {
|
||||
return request({
|
||||
@@ -218,6 +220,7 @@ export const sketchToThreeApi = (data:sketchToThreeData) => {
|
||||
data:{
|
||||
sketchId: data.sketchId,
|
||||
imageUrls: data.imageUrls,
|
||||
mode: data.mode,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
3
src/assets/icons/captureView.svg
Normal file
3
src/assets/icons/captureView.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.99997 1.15789C5.33447 1.15789 4.75255 1.50608 4.44041 2.02674C4.33317 2.20563 4.13523 2.31579 3.92105 2.31579H3.48C2.96605 2.31579 2.61668 2.31624 2.34662 2.33753C2.08357 2.35827 1.94905 2.39586 1.85521 2.44199C1.62942 2.553 1.44584 2.73014 1.33079 2.94801C1.28298 3.03856 1.24402 3.16836 1.22253 3.42218C1.20047 3.68276 1.2 4.01988 1.2 4.51579V7.64211C1.2 8.13802 1.20047 8.47514 1.22253 8.73571C1.24402 8.98953 1.28298 9.11934 1.33079 9.20988C1.44584 9.42776 1.62942 9.60489 1.85521 9.7159C1.94905 9.76204 2.08357 9.79963 2.34662 9.82036C2.61668 9.84165 2.96605 9.8421 3.48 9.8421H8.52C9.03395 9.8421 9.38332 9.84165 9.65338 9.82036C9.91643 9.79963 10.051 9.76204 10.1448 9.7159C10.3706 9.60489 10.5542 9.42776 10.6692 9.20988C10.717 9.11934 10.756 8.98953 10.7775 8.73571C10.7995 8.47514 10.8 8.13802 10.8 7.64211V4.51579C10.8 4.01988 10.7995 3.68276 10.7775 3.42218C10.756 3.16836 10.717 3.03856 10.6692 2.94801C10.5542 2.73014 10.3706 2.553 10.1448 2.44199C10.051 2.39586 9.91643 2.35827 9.65338 2.33753C9.38332 2.31624 9.03395 2.31579 8.52 2.31579H8.07889C7.86472 2.31579 7.66678 2.20563 7.55953 2.02674C7.2474 1.50608 6.66547 1.15789 5.99997 1.15789ZM3.59984 1.15789C4.1465 0.455643 5.01782 0 5.99997 0C6.98213 0 7.85344 0.455643 8.40011 1.15789L8.54478 1.15789C9.02776 1.15789 9.42638 1.15788 9.7511 1.18348C10.0884 1.21007 10.3984 1.26713 10.6896 1.4103C11.1412 1.63232 11.5083 1.98659 11.7384 2.42234C11.8868 2.70332 11.9459 3.00247 11.9735 3.32789C12 3.64121 12 4.02584 12 4.49187V7.66603C12 8.13206 12 8.51668 11.9735 8.83C11.9459 9.15543 11.8868 9.45457 11.7384 9.73556C11.5083 10.1713 11.1412 10.5256 10.6896 10.7476C10.3984 10.8908 10.0884 10.9478 9.7511 10.9744C9.42638 11 9.02777 11 8.5448 11H3.45521C2.97223 11 2.57362 11 2.2489 10.9744C1.91165 10.9478 1.60162 10.8908 1.31042 10.7476C0.858835 10.5256 0.491681 10.1713 0.261585 9.73556C0.11321 9.45457 0.0540718 9.15543 0.0265167 8.83C-1.36882e-05 8.51668 -7.39446e-06 8.13205 2.58775e-07 7.66602V4.49188C-7.39446e-06 4.02584 -1.36882e-05 3.64121 0.0265167 3.32789C0.0540718 3.00247 0.11321 2.70332 0.261585 2.42234C0.491681 1.98659 0.858834 1.63232 1.31042 1.4103C1.60162 1.26713 1.91165 1.21007 2.2489 1.18348C2.57362 1.15788 2.97224 1.15789 3.45522 1.15789L3.59984 1.15789ZM5.99997 4.63158C5.25439 4.63158 4.64997 5.21479 4.64997 5.93421C4.64997 6.65363 5.25439 7.23684 5.99997 7.23684C6.74556 7.23684 7.34997 6.65363 7.34997 5.93421C7.34997 5.21479 6.74556 4.63158 5.99997 4.63158ZM3.44997 5.93421C3.44997 4.5753 4.59165 3.47368 5.99997 3.47368C7.4083 3.47368 8.54997 4.5753 8.54997 5.93421C8.54997 7.29312 7.4083 8.39474 5.99997 8.39474C4.59165 8.39474 3.44997 7.29312 3.44997 5.93421Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/images/three/hdri.hdr
Normal file
BIN
src/assets/images/three/hdri.hdr
Normal file
Binary file not shown.
Binary file not shown.
@@ -176,6 +176,7 @@
|
||||
]
|
||||
let tier = (tritList.includes(currentComponent.value.tier) && typeList.includes(currentComponent.value.type))?currentComponent.value.tier - 1:currentComponent.value.tier
|
||||
if(NODE_DATATYPE.TO_REAL_STYLE == currentComponent.value.type && false){
|
||||
//一个结果节点里面多个子节点
|
||||
let imageProcessTasks = taskList
|
||||
nodeManager.createResultNode({
|
||||
data: {
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
<div class="image">
|
||||
<img :src="data.url" alt="">
|
||||
</div>
|
||||
<p class="label">Mode</p>
|
||||
<my-select v-model="data.mode" :list="modeList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, inject, useAttrs, computed } from 'vue'
|
||||
import { reactive, inject, useAttrs, computed,ref } from 'vue'
|
||||
import uploadFile from '../../tools/upload-file.vue'
|
||||
const attrs = useAttrs()
|
||||
const stateManager = inject('stateManager') as any
|
||||
const data = reactive({
|
||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||
mode: 'Advanced',
|
||||
})
|
||||
const modeList = ref([
|
||||
{ value: 'Advanced', label: 'Advanced' },
|
||||
{ value: 'Normal', label: 'Normal' }
|
||||
])
|
||||
const getApiData = ()=>{
|
||||
return {
|
||||
imageUrls: [data.url],
|
||||
mode: data.mode,
|
||||
}
|
||||
}
|
||||
defineExpose({ data,getApiData })
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
])
|
||||
const onPreview = (item: any) => {
|
||||
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){
|
||||
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj})
|
||||
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj,nodeId:props.node.id})
|
||||
}else{
|
||||
openImagePreview(item.url)
|
||||
}
|
||||
@@ -379,6 +379,7 @@
|
||||
width: 100%;
|
||||
// height: 140px;
|
||||
height: 100%;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -7,13 +7,18 @@ const props = defineProps({
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const emit = defineEmits([
|
||||
'captureView'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
const onDownload = () => {
|
||||
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
||||
}
|
||||
const captureView = ()=>{
|
||||
emit('captureView')
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
@@ -70,14 +75,22 @@ const {} = toRefs(data);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div>
|
||||
<div class="captureView" @click="captureView">
|
||||
<div class="icon">
|
||||
<svgIcon name="captureView" size="12" />
|
||||
</div>
|
||||
{{ $t('threeModel.captureView') }}
|
||||
</div>
|
||||
<!-- <div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.modalDetail{
|
||||
width: 100%;
|
||||
width: 22rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .title{
|
||||
margin: 2.4rem 0;
|
||||
font-weight: 500;
|
||||
@@ -85,9 +98,9 @@ const {} = toRefs(data);
|
||||
line-height: 2.7rem;
|
||||
color: #000;
|
||||
}
|
||||
> .captureView ,
|
||||
> .download{
|
||||
margin-left: 4.2rem;
|
||||
margin-top: 24.8rem;
|
||||
line-height: 3rem;
|
||||
width: 20rem;
|
||||
border-radius: 1.5rem;
|
||||
@@ -99,6 +112,15 @@ const {} = toRefs(data);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
> .captureView{
|
||||
margin-top: 6.4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: .7rem;
|
||||
}
|
||||
> .download{
|
||||
margin-top: 15.4rem;
|
||||
}
|
||||
> .detail{
|
||||
> .name{
|
||||
> .title{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import threeGlb from '@/assets/images/three/sample.glb'
|
||||
import { uploadImage } from '@/api/upload'
|
||||
import { base64Tofile } from '@/components/Canvas/tools/tools'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import model from './model.vue'
|
||||
import detail from './detail.vue'
|
||||
@@ -11,13 +14,29 @@ const props = defineProps({
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const emit = defineEmits([
|
||||
'captureView'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
|
||||
const modelRef = ref(null)
|
||||
const captureView = async ()=>{
|
||||
let url = modelRef.value.captureView()
|
||||
|
||||
const file = base64Tofile(url, 'canvas.png')
|
||||
const formData = new FormData()
|
||||
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
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
// modelRef.value.open(threeGlb)
|
||||
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
@@ -31,7 +50,7 @@ const {} = toRefs(data);
|
||||
<model ref="modelRef" />
|
||||
</div>
|
||||
<div class="detailBox">
|
||||
<detail ref="detailRef" :config="currentData" />
|
||||
<detail ref="detailRef" @captureView="captureView" :config="currentData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,8 +67,10 @@ const {} = toRefs(data);
|
||||
width: 65.5rem;
|
||||
}
|
||||
> .detailBox{
|
||||
width: 22rem;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import gsap from 'gsap';
|
||||
import * as THREE from 'three';
|
||||
import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } from './threeTool'
|
||||
import { ThreeManager } from './threeTool copy'
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
@@ -12,154 +12,37 @@ import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } fr
|
||||
//])
|
||||
|
||||
const threeDom = ref()//threeDom元素
|
||||
|
||||
let scene = shallowRef()//场景
|
||||
let group = shallowRef()//组
|
||||
let camera = shallowRef()//相机
|
||||
let renderer = shallowRef()//渲染器
|
||||
let pointLight = shallowRef();//光
|
||||
let ambient = shallowRef()//环境光
|
||||
let controls = shallowRef()//监听鼠标、键盘事件
|
||||
|
||||
const animationId = ref(null);
|
||||
//加载进度
|
||||
const load = ref({
|
||||
state:false,
|
||||
progress:0
|
||||
})
|
||||
// const textureLoader = ref(new THREE.TextureLoader())//材质
|
||||
const init = () => {
|
||||
//初始化threejs
|
||||
if (scene.value) return
|
||||
const initResult = initThree(threeDom.value)
|
||||
scene.value = initResult.scene
|
||||
group.value = initResult.group
|
||||
camera.value = initResult.camera
|
||||
renderer.value = initResult.renderer
|
||||
ambient.value = initResult.ambient
|
||||
controls.value = initResult.controls
|
||||
pointLight.value = initResult.pointLight
|
||||
let threeModel = null
|
||||
|
||||
threeDom.value.ondblclick = (event:any)=>{
|
||||
let intersects = openModel(event);
|
||||
if(!intersects || intersects.length<=0) return
|
||||
|
||||
const clickedObject = intersects[0].object;
|
||||
const modelInfo = getModelInfo(clickedObject);
|
||||
const { size } = modelInfo;
|
||||
const maxSize = Math.max(size.x, size.y, size.z);
|
||||
let distanceFactor = 1.2;
|
||||
let heightFactor = 0.3;
|
||||
let angle = 0;
|
||||
if (size.y > size.x * 2) {
|
||||
// 高瘦物体,拉远一点,稍微抬高视角
|
||||
distanceFactor = 1.5;
|
||||
heightFactor = 0.4;
|
||||
angle = Math.PI / 6; // 30度
|
||||
} else if (size.x > size.y * 2) {
|
||||
// 扁平物体,降低视角
|
||||
heightFactor = 0.2;
|
||||
angle = Math.PI / 8; // 22.5度
|
||||
}
|
||||
const { position: cameraPos, target } = calculateCameraPosition(
|
||||
modelInfo,
|
||||
camera.value,
|
||||
{
|
||||
distanceFactor,
|
||||
heightFactor,
|
||||
angle
|
||||
}
|
||||
);
|
||||
// 执行相机动画
|
||||
animateCamera(
|
||||
camera.value.position,
|
||||
cameraPos,
|
||||
controls.value.target,
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let openModel = (event:any)=>{
|
||||
let mouse = new THREE.Vector2();
|
||||
let raycaster = new THREE.Raycaster();
|
||||
mouse.x=(event.clientX/window.innerWidth)*2-1;
|
||||
mouse.y=-(event.clientY/window.innerHeight)*2+1;
|
||||
raycaster.setFromCamera(mouse, camera.value);
|
||||
let intersects = raycaster.intersectObjects(scene.value.children);
|
||||
return intersects
|
||||
}
|
||||
let isTweening = false;
|
||||
|
||||
function animateCamera(
|
||||
startCameraPos: THREE.Vector3,
|
||||
endCameraPos: THREE.Vector3,
|
||||
startTarget: THREE.Vector3,
|
||||
endTarget: THREE.Vector3
|
||||
) {
|
||||
if (isTweening) return;
|
||||
isTweening = true;
|
||||
|
||||
let options = {
|
||||
cx: startCameraPos.x,
|
||||
cy: startCameraPos.y,
|
||||
cz: startCameraPos.z,
|
||||
tx: startTarget.x,
|
||||
ty: startTarget.y,
|
||||
tz: startTarget.z
|
||||
};
|
||||
|
||||
gsap.to(options, {
|
||||
cx: endCameraPos.x,
|
||||
cy: endCameraPos.y,
|
||||
cz: endCameraPos.z,
|
||||
tx: endTarget.x,
|
||||
ty: endTarget.y,
|
||||
tz: endTarget.z,
|
||||
duration: 1,
|
||||
ease: 'power2.inOut', // 使用更自然的缓动
|
||||
onUpdate: () => {
|
||||
camera.value.position.set(options.cx, options.cy, options.cz);
|
||||
controls.value.target.set(options.tx, options.ty, options.tz);
|
||||
controls.value.update();
|
||||
},
|
||||
onComplete: () => {
|
||||
isTweening = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
const setModel = async (url:any)=>{
|
||||
clearModel(group,scene)
|
||||
await addModel(url,controls,camera,pointLight,group,load)
|
||||
// addMaterial()
|
||||
await threeModel.setModel(url,load)
|
||||
}
|
||||
const open = async (url)=>{
|
||||
const open = (url)=>{
|
||||
load.value.state = true
|
||||
await nextTick(()=>{
|
||||
init()
|
||||
nextTick(async ()=>{
|
||||
threeModel = new ThreeManager(threeDom.value)
|
||||
await threeModel.setHDRI()
|
||||
await setModel(url)
|
||||
load.value.state = false
|
||||
threeModel.operation()
|
||||
})
|
||||
controls.value.enableDamping = true;
|
||||
let animate = ()=>{
|
||||
animationId.value = requestAnimationFrame(animate);
|
||||
// renderer.value.render(scene.value, camera.value);
|
||||
// model.rotation.x += 0.01; //旋转物体
|
||||
var vector = camera.value.position.clone()
|
||||
controls.value.update();
|
||||
renderer.value.render(scene.value, camera.value);
|
||||
// point.position.set(vector.x,vector.y,vector.z);
|
||||
// group.rotation.y += 0.01;
|
||||
// composer.render();
|
||||
};
|
||||
animate();
|
||||
await setModel(url)
|
||||
load.value.state = false
|
||||
|
||||
}
|
||||
|
||||
const captureView = ()=>{
|
||||
return threeModel.exportAsImage()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({open})
|
||||
defineExpose({open,captureView})
|
||||
</script>
|
||||
<template>
|
||||
<div class="modelBox">
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,28 @@ 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 hdri from '@/assets/images/three/hdri.hdr'
|
||||
|
||||
export const initThree = (threeDom)=>{
|
||||
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, 10000);
|
||||
camera.position.set(0, 90, 6); //设置相机位置
|
||||
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;
|
||||
|
||||
/**
|
||||
* 创建渲染器对象
|
||||
*/
|
||||
const width = threeDom.offsetWidth; //窗口宽度
|
||||
|
||||
|
||||
const width = threeDom.offsetWidth; //窗口宽度
|
||||
const height = threeDom.offsetHeight; //窗口高度
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
@@ -34,9 +41,8 @@ export const initThree = (threeDom)=>{
|
||||
threeDom.appendChild(renderer.domElement);
|
||||
|
||||
// 设置渲染器大小
|
||||
//环境光
|
||||
const ambient = new THREE.AmbientLight(0xffffff,.8);
|
||||
scene.add(ambient);
|
||||
|
||||
|
||||
const controls = new OrbitControls(camera,renderer.domElement)//监听鼠标、键盘事件;
|
||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||
@@ -47,6 +53,27 @@ export const initThree = (threeDom)=>{
|
||||
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
|
||||
|
||||
// 设置环境贴图(影响材质反射)
|
||||
scene.environment = hdrTexture
|
||||
// 可选:同时设置为背景
|
||||
scene.background = hdrTexture
|
||||
|
||||
console.log('HDR 环境贴图加载成功:', hdri)
|
||||
} catch (error) {
|
||||
console.error('HDR 加载失败:', error)
|
||||
// 降级方案:使用环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
||||
scene.add(ambientLight)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 光源设置
|
||||
*/
|
||||
@@ -57,9 +84,33 @@ export const initThree = (threeDom)=>{
|
||||
DirectionalLight 平行光,比如太阳光
|
||||
SpotLight 聚光源
|
||||
*/
|
||||
|
||||
|
||||
//设置环境光全亮
|
||||
const pointLight = new THREE.AmbientLight(0xffffff,.2);
|
||||
scene.add(pointLight);
|
||||
//环境光
|
||||
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
||||
// scene.add(pointLight);
|
||||
|
||||
function 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);
|
||||
// 存储 offset 向量副本,避免后续重复创建
|
||||
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//开启阴影
|
||||
@@ -80,7 +131,7 @@ export const initThree = (threeDom)=>{
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
|
||||
scene.background = new THREE.Color("#fff");
|
||||
return {scene,group,camera,renderer,ambient,controls,pointLight}
|
||||
return {scene,group,camera,renderer,controls,pointLight,studioLights}
|
||||
}
|
||||
export const clearModel = (group,scene)=>{
|
||||
const oldGroup:any = group.value;
|
||||
@@ -229,4 +280,14 @@ export const addModel = async (
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
// 导出当前视图为图片
|
||||
export const exportAsImage = (renderer, camera, scene, filename = 'model.png')=>{
|
||||
// 渲染当前场景
|
||||
renderer.render(scene, camera)
|
||||
|
||||
// 获取 canvas 数据
|
||||
const canvas = renderer.domElement
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
return dataURL
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
<image-preview ref="imagePreviewRef" />
|
||||
<baseModal ref="threeModelRef">
|
||||
<template v-slot="{ currentData }">
|
||||
<threeModel :currentData="currentData" />
|
||||
<threeModel :currentData="currentData" @captureView="captureView" />
|
||||
</template>
|
||||
</baseModal>
|
||||
<Assistant />
|
||||
@@ -253,6 +253,27 @@
|
||||
const openThreeModelPreview = (currentData) => {
|
||||
threeModelRef.value.open(currentData)
|
||||
}
|
||||
const captureView = (captureData)=>{
|
||||
const timestamp = Date.now()
|
||||
nodeManager.createResultNode({
|
||||
data: {
|
||||
superiorID_: captureData.nodeId,
|
||||
data: {
|
||||
selectable: false,
|
||||
imageProcessTasks: [
|
||||
{
|
||||
id: timestamp + '',
|
||||
url: captureData.minioUrl,
|
||||
status: 'RETURNED',
|
||||
taskId: timestamp + ''
|
||||
}
|
||||
],
|
||||
selectTaskId: timestamp + ''
|
||||
}
|
||||
}
|
||||
})
|
||||
// threeModelRef.value.close()
|
||||
}
|
||||
provide('openImagePreview', openImagePreview)
|
||||
provide('openThreeModelPreview', openThreeModelPreview)
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ interface NodeData {
|
||||
data?: object// 节点数据
|
||||
tier?: string// 节点层级
|
||||
isHeader?: boolean// 是否显示头
|
||||
superiorID?: string// 上级节点ID
|
||||
superiorID?: string// 上级节点ID,有连接线
|
||||
superiorID_?: string// 上级节点ID,没有连接线
|
||||
superiorNodeType?: string// 上级节点类型
|
||||
disableDelete?: boolean// 是否禁用删除
|
||||
disableCopy?: boolean// 是否禁用复制
|
||||
@@ -45,23 +46,31 @@ export class NodeManager {
|
||||
/** 创建节点 */
|
||||
createNode(options: NodeOptions) {
|
||||
const superiorID = options?.data?.superiorID
|
||||
const superiorID_ = options?.data?.superiorID_// 上级节点ID,用于创建子节点时使用
|
||||
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
||||
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
|
||||
const superiorGenerateNodes = superiorID?this.stateManager.getSubordNodes(superiorID):[]
|
||||
const currentNode = superiorGenerateNodes.find((node) => {
|
||||
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
||||
})
|
||||
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
||||
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
||||
}, superiorGenerateNodes[0])
|
||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
||||
let snode = null as any
|
||||
if(superiorID){
|
||||
snode = this.stateManager.flowManager.getNodeById(superiorID)
|
||||
}else if(superiorID_){
|
||||
snode = this.stateManager.flowManager.getNodeById(superiorID_)
|
||||
}else{
|
||||
snode = this.stateManager.flowManager.getLastNode()
|
||||
}
|
||||
const id = options.id || createId()
|
||||
const positionX = options.positionX || 0
|
||||
const positionY = options.positionY || 0
|
||||
const position = options.position ||
|
||||
(
|
||||
currentNode ?
|
||||
currentNode ?//当前节点位置,覆盖操作
|
||||
currentNode.position :
|
||||
endGenerateNode ?
|
||||
endGenerateNode ?//最大子级位置
|
||||
{
|
||||
x: endGenerateNode.position.x + positionX,
|
||||
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
||||
|
||||
@@ -211,7 +211,7 @@ export class StateManager {
|
||||
}
|
||||
if(!deletePromise) return console.log('删除操作被取消')
|
||||
|
||||
if(node.data.data.imageProcessTasks.length > 1){
|
||||
if(node.data.data?.imageProcessTasks?.length > 1){
|
||||
node.data.data.imageProcessTasks = node.data.data.imageProcessTasks.filter((item) => item.taskId !== node.data.data.selectTaskId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||
const props = defineProps({
|
||||
modalWidth: {
|
||||
type: String,
|
||||
@@ -29,7 +29,7 @@
|
||||
})
|
||||
const showDialog = ref(false)
|
||||
let currentData = ref(null)
|
||||
const open = (data: any,) => {
|
||||
const open = (data: any) => {
|
||||
currentData.value = data
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
@@ -222,7 +222,8 @@ export default {
|
||||
//3d面板
|
||||
threeModel: {
|
||||
loading: 'Loading',
|
||||
download: 'Download'
|
||||
download: 'Download',
|
||||
captureView: 'Capture View',
|
||||
},
|
||||
DepthCanvas: {
|
||||
layer: 'Layer',
|
||||
|
||||
@@ -217,7 +217,8 @@ export default {
|
||||
//3d面板
|
||||
threeModel: {
|
||||
loading: '加载中',
|
||||
download: '下载'
|
||||
download: '下载',
|
||||
captureView: '捕获视图',
|
||||
},
|
||||
DepthCanvas: {
|
||||
layer: '图层',
|
||||
|
||||
@@ -21,7 +21,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
onUnmounted,
|
||||
onMounted,
|
||||
nextTick,
|
||||
watch,
|
||||
onActivated
|
||||
} from 'vue'
|
||||
import List from './List.vue'
|
||||
import Input from '../../components/Input.vue'
|
||||
import { chatUrl } from '@/api/agent'
|
||||
@@ -29,8 +38,10 @@
|
||||
import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores'
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const userStore = useUserInfoStore()
|
||||
const agentStore = useAgentStore()
|
||||
@@ -56,7 +67,7 @@
|
||||
const isGenerating = ref(false)
|
||||
const isPaused = ref(false) // 标记是否为主动暂停
|
||||
const params = reactive<AgentParamsType>({
|
||||
projectID: null,
|
||||
projectID: route.params.id as string,
|
||||
message: '',
|
||||
token: userStore.state.token,
|
||||
versionID: '',
|
||||
@@ -116,14 +127,13 @@
|
||||
onMounted(() => {
|
||||
MyEvent.add('resetAgent', handleReset)
|
||||
// 检查 store 中是否有初始项目数据
|
||||
// projectStore.setId('1') // 临时设置项目ID为1,实际应用中应根据上下文动态设置
|
||||
const initialData = agentStore.getInitialProjectData
|
||||
if (initialData) {
|
||||
// 等待页面渲染完成后自动发送初始消息
|
||||
params.configParams = {
|
||||
type: initialData.type,
|
||||
region: initialData.area,
|
||||
style: initialData.style,
|
||||
type: initialData.type || '',
|
||||
region: initialData.area || '',
|
||||
style: initialData.style || '',
|
||||
temperature: 0.7
|
||||
}
|
||||
params.needSuggestion = initialData.needSuggestion || false
|
||||
@@ -142,6 +152,28 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 用于存储不在页面显示但是仍进行中的对话信息
|
||||
const newQueue = ref<{
|
||||
nodeId?: string
|
||||
name?: string
|
||||
}>({})
|
||||
onActivated(() => {
|
||||
if (newQueue.value.nodeId) {
|
||||
projectStore.setProject({ nodeId: newQueue.value.nodeId })
|
||||
}
|
||||
if (newQueue.value.name) {
|
||||
MyEvent.emit('newTitle', {
|
||||
title: newQueue.value.name,
|
||||
id: params.projectID
|
||||
})
|
||||
}
|
||||
if (newQueue.value.newSketch) {
|
||||
mergeUniqueKeys(sketchList.value, newQueue.value.newSketch)
|
||||
MyEvent.emit('OpenSketch', params.projectID)
|
||||
}
|
||||
newQueue.value = {}
|
||||
})
|
||||
|
||||
const handleSendMessage = async (
|
||||
message: {
|
||||
text: string
|
||||
@@ -176,7 +208,7 @@
|
||||
id: messageList.value.length + 1,
|
||||
text: '',
|
||||
isUser: false,
|
||||
sessionId: projectStore.state.id,
|
||||
sessionId: route.params.id as string,
|
||||
loading: true,
|
||||
thinking: false,
|
||||
thinkingText: '',
|
||||
@@ -188,16 +220,20 @@
|
||||
const abortController = createAbortController()
|
||||
|
||||
// console.log('token---', params.token, '参数---', params)
|
||||
params.projectID = projectStore.state.id
|
||||
params.projectID = route.params.id as string
|
||||
try {
|
||||
const urlParams = new URLSearchParams<AgentParamsType>({
|
||||
...params,
|
||||
configParams: JSON.stringify(params.configParams)
|
||||
// configParams: JSON.stringify(params.configParams)
|
||||
configParams: JSON.stringify({
|
||||
type: params.configParams.type||'',
|
||||
region: params.configParams.region||'',
|
||||
style: params.configParams.style||'',
|
||||
temperature: params.configParams.temperature
|
||||
})
|
||||
})
|
||||
const BASEURL = import.meta.env.VITE_APP_URL
|
||||
// console.log('params', params)
|
||||
|
||||
// debugger
|
||||
const response = await fetch(`${BASEURL}${chatUrl}?${urlParams.toString()}`, {
|
||||
method: 'GET',
|
||||
signal: abortController.signal
|
||||
@@ -300,10 +336,7 @@
|
||||
reportsContent.value
|
||||
) {
|
||||
isGeneratingReport.value = false
|
||||
sessionStorage.setItem(
|
||||
'reportsContent_' + projectStore.state.id,
|
||||
reportsContent.value
|
||||
)
|
||||
aiMessage.report = reportsContent.value
|
||||
}
|
||||
|
||||
previousEventName = eventName
|
||||
@@ -339,7 +372,11 @@
|
||||
.filter((line) => line.startsWith('data:'))
|
||||
.map((line) => line.replace(/^data:\s*/, ''))[0]
|
||||
params.versionID = versionID
|
||||
projectStore.setProject({ nodeId: versionID })
|
||||
if (String(aiMessage.sessionId) === String(projectStore.state.id)) {
|
||||
projectStore.setProject({ nodeId: versionID })
|
||||
} else {
|
||||
newQueue.value.nodeId = versionID
|
||||
}
|
||||
}
|
||||
|
||||
if (eventName === 'tool') {
|
||||
@@ -357,11 +394,12 @@
|
||||
contentBody += `<slot slot-name="url"></slot>`
|
||||
}
|
||||
if (jsonData.title) {
|
||||
emits('setTitle', jsonData.title)
|
||||
console.log('发送title', {
|
||||
title: jsonData.title,
|
||||
id: params.projectID
|
||||
})
|
||||
if (aiMessage.sessionId === projectStore.state.id) {
|
||||
emits('setTitle', jsonData.title)
|
||||
} else {
|
||||
newQueue.value.name = jsonData.title
|
||||
}
|
||||
|
||||
MyEvent.emit('newTitle', {
|
||||
title: jsonData.title,
|
||||
id: params.projectID
|
||||
@@ -370,15 +408,15 @@
|
||||
|
||||
if (hasSketch) {
|
||||
hasSketchEvent = true
|
||||
let tempArr = []
|
||||
|
||||
Object.keys(jsonData).forEach((key) => {
|
||||
if (!sketchList.value.some((item) => item[key])) {
|
||||
sketchList.value.push({
|
||||
[key]: jsonData[key]
|
||||
})
|
||||
}
|
||||
})
|
||||
MyEvent.emit('OpenSketch')
|
||||
if (String(params.projectID) === String(projectStore.state.id)) {
|
||||
mergeUniqueKeys(sketchList.value, jsonData)
|
||||
MyEvent.emit('OpenSketch', params.projectID)
|
||||
} else {
|
||||
mergeUniqueKeys(tempArr, jsonData)
|
||||
newQueue.value.newSketch = tempArr
|
||||
}
|
||||
}
|
||||
if (eventName === 'reportName' || eventName === 'reportTitle') {
|
||||
aiMessage.reportName = jsonData.reportName || jsonData.reportTitle
|
||||
@@ -598,11 +636,14 @@
|
||||
|
||||
params.versionID = ''
|
||||
sketchList.value = []
|
||||
params.useReport = false
|
||||
|
||||
if (project) {
|
||||
params.configParams.type = project.type
|
||||
params.configParams.region = project.area
|
||||
params.configParams.style = project.style
|
||||
params.configParams.type = project.type || ''
|
||||
params.configParams.region = project.area || ''
|
||||
params.configParams.style = project.style || ''
|
||||
params.configParams.temperature = project.temperature
|
||||
params.projectID = project.id
|
||||
}
|
||||
if (!data) {
|
||||
messageList.value = []
|
||||
@@ -633,6 +674,18 @@
|
||||
})
|
||||
}
|
||||
|
||||
// 防止插入重复图片
|
||||
const mergeUniqueKeys = (targetArr, newData) => {
|
||||
const existingKeys = new Set(targetArr.flatMap((item) => Object.keys(item)))
|
||||
|
||||
Object.entries(newData).forEach(([key, value]) => {
|
||||
if (!existingKeys.has(key)) {
|
||||
targetArr.unshift({ [key]: value })
|
||||
existingKeys.add(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
setChatInfo
|
||||
})
|
||||
|
||||
@@ -125,9 +125,10 @@
|
||||
import type { CustomAttrs } from '@crazydos/vue-markdown'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
import { useUserInfoStore } from '@/stores'
|
||||
import { useUserInfoStore, useProjectStore } from '@/stores'
|
||||
|
||||
const userStore = useUserInfoStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
@@ -285,7 +286,7 @@
|
||||
// 点击显示来源
|
||||
}
|
||||
const handleClickSketch = () => {
|
||||
MyEvent.emit('openSketch')
|
||||
MyEvent.emit('openSketch', projectStore.state.id)
|
||||
}
|
||||
|
||||
const showStop = ref(false)
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
&-header {
|
||||
font-family: 'Medium';
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.3rem;
|
||||
// margin-bottom: 1.3rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const previewRef = ref(null)
|
||||
|
||||
const proJectId = computed(() => route.params.id)
|
||||
const agentTitle = ref('Conversation')
|
||||
const previewType = ref<'sketch' | 'report'>('sketch')
|
||||
const VersionTreeIndexRef = ref()
|
||||
@@ -96,13 +96,14 @@
|
||||
}
|
||||
|
||||
const handleGetProjectInfoAndHistory = () => {
|
||||
handleOpenSketch()
|
||||
sketchList.value = []
|
||||
getProjectInfo({ id: route.params.id }).then((res) => {
|
||||
if (!res) {
|
||||
router.push({ name: 'mainInput' })
|
||||
ElMessage.warning(t('Home.notFound'))
|
||||
return
|
||||
}
|
||||
handleOpenSketch()
|
||||
if (res) agentRef.value.setChatInfo(res)
|
||||
let data = res?.project || res
|
||||
if (data?.latestNodeId) data.nodeId = data.latestNodeId
|
||||
@@ -115,8 +116,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
const proJectId = computed(() => route.params.id)
|
||||
|
||||
const handleOpenReport = (data) => {
|
||||
previewRef.value.setSessionId(data.sessionId)
|
||||
previewRef.value.setReport(data.reportName, data.report)
|
||||
@@ -128,7 +127,7 @@
|
||||
previewType.value = 'url'
|
||||
}
|
||||
|
||||
const handleOpenSketch = () => {
|
||||
const handleOpenSketch = (id) => {
|
||||
previewType.value = 'sketch'
|
||||
}
|
||||
|
||||
@@ -144,11 +143,17 @@
|
||||
}
|
||||
)
|
||||
|
||||
const handleRenameConversation = (item) => {
|
||||
if (String(item.id) === String(proJectId.value)) {
|
||||
handleSetTitle(item.name)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('11111agentindex')
|
||||
MyEvent.add('openReport', handleOpenReport)
|
||||
MyEvent.add('openUrls', handleOpenUrls)
|
||||
MyEvent.add('openSketch', handleOpenSketch)
|
||||
MyEvent.add('renameConversation', handleRenameConversation)
|
||||
projectStore.clearProject()
|
||||
if (proJectId.value) {
|
||||
handleGetProjectInfoAndHistory()
|
||||
@@ -157,7 +162,8 @@
|
||||
onUnmounted(() => {
|
||||
MyEvent.remove('openReport', handleOpenReport)
|
||||
MyEvent.remove('openUrls', handleOpenUrls)
|
||||
MyEvent.remove('OpenSketch')
|
||||
MyEvent.remove('OpenSketch', handleOpenSketch)
|
||||
MyEvent.remove('renameConversation', handleRenameConversation)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -499,7 +499,7 @@
|
||||
// 清理掉已被删除的标签引用(从 DOM 中移除的元素)
|
||||
reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null)
|
||||
|
||||
if (reportTags.value.length > 0 ) {
|
||||
if (reportTags.value.length > 0) {
|
||||
// 移除所有标签及其关联的零宽空格
|
||||
reportTags.value.forEach((tag) => {
|
||||
if (
|
||||
@@ -783,9 +783,9 @@
|
||||
}
|
||||
|
||||
const params = {
|
||||
type: typeValue.value,
|
||||
area: areaValue.value,
|
||||
style: styleValue.value,
|
||||
type: typeValue.value || '',
|
||||
area: areaValue.value || '',
|
||||
style: styleValue.value || '',
|
||||
useReport: reportTags.value.length > 0,
|
||||
temperature: 0.7
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
const name = e.target.value
|
||||
if (!name) return console.warn('未输入名称,不允许重命名')
|
||||
item.name = name
|
||||
MyEvent.emit('renameConversation', item)
|
||||
updateProject(item.id, { name }).then(() => {
|
||||
GetProjectList()
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ export default defineConfig(({ mode }) => {
|
||||
inject: 'body-last' // 注入位置优化
|
||||
})
|
||||
],
|
||||
assetsInclude: ['**/*.glb'],
|
||||
assetsInclude: ['**/*.glb','**/*.hdr'],
|
||||
define: {
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user