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

This commit is contained in:
2026-03-13 17:37:22 +08:00
11 changed files with 320 additions and 158 deletions

Binary file not shown.

View File

@@ -106,6 +106,7 @@
isReady.value = true // 准备就绪 isReady.value = true // 准备就绪
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer.value.disconnect()
canvasManager.dispose() canvasManager.dispose()
stateManager.dispose() stateManager.dispose()
layerManager.dispose() layerManager.dispose()

View File

@@ -153,6 +153,9 @@ export class CanvasManager {
getObjectById(id: string) { getObjectById(id: string) {
return this.getObjects().find((item: any) => item?.info?.id === id) return this.getObjects().find((item: any) => item?.info?.id === id)
} }
getActiveObject() {
return this.getObjectById(this.layerManager.activeID.value)
}
renderAll() { renderAll() {
this.canvas.renderAll() this.canvas.renderAll()
} }

View File

@@ -22,6 +22,9 @@ export class LayerManager {
this.stateManager.toolManager.setTool(OperationType.SELECT) this.stateManager.toolManager.setTool(OperationType.SELECT)
} }
} }
getActiveLayer() {
return this.getLayerById(this.activeID.value)
}
getLayerById(id) { getLayerById(id) {
return this.layers.value.find((item: any) => item.info.id === id) return this.layers.value.find((item: any) => item.info.id === id)
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="node" :class="{ center: posCenter, mask: mask }"> <div class="node-el" :class="{ center: posCenter, mask: mask }">
<Handle <Handle
v-for="handle in handles[type] || []" v-for="handle in handles[type] || []"
:key="handle.id" :key="handle.id"
@@ -19,8 +19,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core' import { Handle, Position } from '@vue-flow/core'
import { NODE_TYPE } from '../tools/index.d' import { NODE_DATATYPE, NODE_DATATIER, NODE_TYPE } from '../tools/index.d'
import { NODE_DATATYPE, NODE_DATATIER } from '../tools/index.d'
import { computed, ref, inject } from 'vue' import { computed, ref, inject } from 'vue'
const handles = ref({ const handles = ref({
[NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }], [NODE_TYPE.INPUT]: [{ id: 'Right', type: 'source', position: Position.Right }],
@@ -54,7 +53,10 @@
const isSubord = computed(() => nodes.value.some((v) => v.data.superiorID === props.node.id)) const isSubord = computed(() => nodes.value.some((v) => v.data.superiorID === props.node.id))
const tier = computed(() => Number(props.node?.data?.tier || 0)) const tier = computed(() => Number(props.node?.data?.tier || 0))
const isReturned = computed(() => { const isReturned = computed(() => {
return props.node.data.type == 'result-image' && props.node.data.data.imageProcessTasks[0].status == 'RETURNED' return (
props.node.data.type == NODE_DATATYPE.RESULT_IMAGE &&
props.node.data.data.imageProcessTasks[0].status == 'RETURNED'
)
}) })
const isAdd = computed( const isAdd = computed(
() => () =>
@@ -66,7 +68,9 @@
const onAdd = () => { const onAdd = () => {
const tier_ = tier.value + 1 const tier_ = tier.value + 1
// dataoriginalImage // dataoriginalImage
let nodeData = props.node?.data?.data.imageProcessTasks.filter((v) => v.taskId === props.node?.data?.data.selectTaskId) let nodeData = props.node?.data?.data.imageProcessTasks.filter(
(v) => v.taskId === props.node?.data?.data.selectTaskId
)
const originalImage = nodeData[0]?.url const originalImage = nodeData[0]?.url
if (!originalImage) console.log('originalImage 找不到原始图片') if (!originalImage) console.log('originalImage 找不到原始图片')
props.stateManager.nodeManager.createCardsSelect({ props.stateManager.nodeManager.createCardsSelect({
@@ -80,7 +84,7 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.node { .node-el {
position: relative; position: relative;
--top: 50px; --top: 50px;
&.mask *, &.mask *,

View File

@@ -4,7 +4,8 @@ import { useI18n } from 'vue-i18n'
import gsap from 'gsap'; import gsap from 'gsap';
import * as THREE from 'three'; import * as THREE from 'three';
import { initThree,clearModel,addModel } from './threeTool' import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } from './threeTool'
import threeGlb from '@/assets/images/three/sample.glb'
//const props = defineProps({ //const props = defineProps({
//}) //})
@@ -28,7 +29,6 @@ const load = ref({
progress:0 progress:0
}) })
// const textureLoader = ref(new THREE.TextureLoader())//材质 // const textureLoader = ref(new THREE.TextureLoader())//材质
//初始化
const init = () => { const init = () => {
//初始化threejs //初始化threejs
if (scene.value) return if (scene.value) return
@@ -44,10 +44,40 @@ const init = ()=>{
threeDom.value.ondblclick = (event:any)=>{ threeDom.value.ondblclick = (event:any)=>{
let intersects = openModel(event); let intersects = openModel(event);
if(!intersects || intersects.length<=0) return if(!intersects || intersects.length<=0) return
const bbox = new THREE.Box3().setFromObject(intersects[0].object);
const size = new THREE.Vector3(); const clickedObject = intersects[0].object;
let target2 = bbox.getCenter(size);//获取选中包围起来后的中心坐标 const modelInfo = getModelInfo(clickedObject);
animateCamera(camera.value.position,intersects[0].point,controls.value.target,target2) 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
);
} }
} }
@@ -61,39 +91,43 @@ let openModel = (event:any)=>{
return intersects return intersects
} }
let isTweening = false; let isTweening = false;
function animateCamera(current1:any, target1:any, current2:any, target2:any){
if (isTweening) return function animateCamera(
isTweening = true startCameraPos: THREE.Vector3,
endCameraPos: THREE.Vector3,
startTarget: THREE.Vector3,
endTarget: THREE.Vector3
) {
if (isTweening) return;
isTweening = true;
let options = { let options = {
x1: current1.x, // 相机当前位置x cx: startCameraPos.x,
y1: current1.y, // 相机当前位置y cy: startCameraPos.y,
z1: current1.z, // 相机当前位置z cz: startCameraPos.z,
x2: current2.x, // 控制当前的中心点x tx: startTarget.x,
y2: current2.y, // 控制当前的中心点y ty: startTarget.y,
// z2: current2.z // 控制当前的中心点z tz: startTarget.z
} };
gsap.to(options, { gsap.to(options, {
x1: 0, // 新的相机位置x cx: endCameraPos.x,
y1: target2.y, // 新的相机位置y cy: endCameraPos.y,
z1: 1000, // 新的相机位置z cz: endCameraPos.z,
x2: 0, // 新的控制中心点位置x tx: endTarget.x,
y2: target2.y, // 新的控制中心点位置x ty: endTarget.y,
tz: endTarget.z,
duration: 1, duration: 1,
ease:'linear', ease: 'power2.inOut', // 使用更自然的缓动
onUpdate: () => { onUpdate: () => {
camera.value.position.x = options.x1; camera.value.position.set(options.cx, options.cy, options.cz);
camera.value.position.y = options.y1; controls.value.target.set(options.tx, options.ty, options.tz);
camera.value.position.z = options.z1;
controls.value.target.x = options.x2;
controls.value.target.y = options.y2;
// controls.value.target.z = object.z2;
controls.value.update(); controls.value.update();
}, },
onComplete: () => { onComplete: () => {
isTweening = false isTweening = false;
} }
// z2: target2.z // 新的控制中心点位置x });
})
} }
const setModel = async (url:any)=>{ const setModel = async (url:any)=>{
clearModel(group,scene) clearModel(group,scene)
@@ -118,7 +152,7 @@ const open = async ()=>{
// composer.render(); // composer.render();
}; };
animate(); animate();
await setModel("https://www.minio-api.aida.com.hk/aida-threed/female/glb/1%E7%9F%AD%E8%A2%96T%E6%81%A4.glb?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260310%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260310T032933Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=184ec7f9ff3076dde5aca66e2d2e27f8d180add698a5b1040fe903a55cb2f85e") await setModel(threeGlb)
load.value.state = false load.value.state = false
} }

View File

@@ -57,21 +57,25 @@ export const initThree = (threeDom)=>{
DirectionalLight 平行光,比如太阳光 DirectionalLight 平行光,比如太阳光
SpotLight 聚光源 SpotLight 聚光源
*/ */
const pointLight = new THREE.DirectionalLight(0xffffff,.5); //设置环境光全亮
pointLight.intensity = 1.2 const pointLight = new THREE.AmbientLight(0xffffff,.2);
pointLight.castShadow = true//开启阴影 scene.add(pointLight);
pointLight.shadow.mapSize = new THREE.Vector2(width, height) // const pointLight = new THREE.AmbientLight(0xffffff,1.0);
scene.add(pointLight); //光源添加到场景中 // 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.set(400, 200, 300); //点光源位置
pointLight.position.y = 400; // pointLight.position.y = 100;
pointLight.position.z = 200; // pointLight.position.z = 50;
pointLight.position.x = 200; // pointLight.position.x = 100;
let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小
let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", }) // let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小
let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial); // let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", })
floorMesh.rotation.x = -0.5 * Math.PI; // let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial);
floorMesh.receiveShadow = true; // floorMesh.rotation.x = -0.5 * Math.PI;
floorMesh.position.y = -0.001; // floorMesh.receiveShadow = true;
// floorMesh.position.y = -0.001;
// scene.add(floorMesh); // scene.add(floorMesh);
const textureLoader = new THREE.TextureLoader(); const textureLoader = new THREE.TextureLoader();
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg'); // const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
@@ -85,41 +89,144 @@ export const clearModel = (group,scene)=>{
scene.value.remove(oldGroup); 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 ( export const addModel = async (
url: any, url: any,
controls: OrbitControls, controls: OrbitControls,
camera: THREE.PerspectiveCamera, camera: THREE.PerspectiveCamera,
pointLight: THREE.DirectionalLight, pointLight: THREE.DirectionalLight,
group: THREE.Group, group: THREE.Group,
load:any)=>{ load: any
) => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
var fbxLoader = new GLTFLoader(); var fbxLoader = new GLTFLoader();
let drac = new DRACOLoader() let drac = new DRACOLoader()
drac.setDecoderPath('/draco/') drac.setDecoderPath('/draco/')
fbxLoader.setDRACOLoader(drac) fbxLoader.setDRACOLoader(drac)
// fbxLoader.load('/3dModel/222/1111.glb',
fbxLoader.load(url,
fbxLoader.load(url,
(obj: any) => { (obj: any) => {
let scene = obj.scene; let scene = obj.scene;
var box = new THREE.Box3().setFromObject(scene); scene.traverse((child: any) => {
var center = box.getCenter(new THREE.Vector3()); if (child.isMesh) {
controls.value.target.copy(center); // 如果是基础材质,转换为标准材质
// controls.autoRotate = true if (child.material instanceof THREE.MeshBasicMaterial) {
camera.value.position.y = center.y; const oldMat = child.material;
camera.value.position.z = 1000; child.material = new THREE.MeshStandardMaterial({
pointLight.value.position.y = 250; map: oldMat.map,
pointLight.value.position.z = 1250; 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); group.value.add(scene);
// 可选:将模型信息存储在模型上,方便后续使用
(scene as any).userData.modelInfo = modelInfo;
resolve('') resolve('')
},(xhr:any) => { // 加载进度回调 },
(xhr: any) => { // 加载进度回调
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2); const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
load.value.progress = percent load.value.progress = percent
// updateProgressBar(Number(percent)); console.log('模型加载进度:', percent);
},(error:any) => { // 加载失败回调 },
(error: any) => { // 加载失败回调
console.error('模型加载失败:', error); console.error('模型加载失败:', error);
reject('') reject('')
}) }
)
}) })
} }

View File

@@ -17,7 +17,7 @@
:style="{ '--custom-cursor': stateManager.cursor.value }" :style="{ '--custom-cursor': stateManager.cursor.value }"
> >
<template v-for="v in nodeTypes" :key="v" #[`node-${v}`]="node"> <template v-for="v in nodeTypes" :key="v" #[`node-${v}`]="node">
<node <node-el
:type="v" :type="v"
:stateManager="stateManager" :stateManager="stateManager"
:node="node" :node="node"
@@ -41,7 +41,7 @@
@bring-to-font="bringToFont(node.id)" @bring-to-font="bringToFont(node.id)"
@send-to-back="sendToBack(node.id)" @send-to-back="sendToBack(node.id)"
/> />
</node> </node-el>
</template> </template>
</VueFlow> </VueFlow>
</div> </div>
@@ -73,13 +73,11 @@
// 工具 // 工具
import threeModel from './components/tools/threeModel/index.vue' import threeModel from './components/tools/threeModel/index.vue'
// 节点 // 节点
import node from './components/node.vue' import nodeEl from './components/node-el.vue'
import resultImage from './components/nodes/result-image.vue' import resultImage from './components/nodes/result-image.vue'
import card from './components/nodes/cards/index.vue' import card from './components/nodes/cards/index.vue'
import text from './components/nodes/text.vue' import text from './components/nodes/text.vue'
// 接口
import { getSketchFlowCanvas, putSketchFlowCanvas } from '@/api/flow-canvas'
const components = { const components = {
[NODE_COMPONENT.RESULT_IMAGE]: resultImage, [NODE_COMPONENT.RESULT_IMAGE]: resultImage,
[NODE_COMPONENT.CARD]: card, [NODE_COMPONENT.CARD]: card,
@@ -100,6 +98,7 @@
default: () => ({}) default: () => ({})
} }
}) })
const emit = defineEmits(['exportFlow'])
const vueFlow = ref<any>() const vueFlow = ref<any>()
const nodeTypes = ref([NODE_TYPE.INPUT, NODE_TYPE.SECONDARY, NODE_TYPE.OUTPUT, NODE_TYPE.ALONE]) const nodeTypes = ref([NODE_TYPE.INPUT, NODE_TYPE.SECONDARY, NODE_TYPE.OUTPUT, NODE_TYPE.ALONE])
@@ -136,8 +135,7 @@
const nodes = computed(() => stateManager.nodes_.value) const nodes = computed(() => stateManager.nodes_.value)
const edges = computed(() => stateManager.edges.value) const edges = computed(() => stateManager.edges.value)
const edges_ = computed(() => { const edges_ = computed(() => {
console.log(123) return edges.value.filter((v) => v?.visible)
return edges.value.filter((v) => v.visible)
}) })
const nodesDraggable = computed(() => stateManager.nodesDraggable.value) const nodesDraggable = computed(() => stateManager.nodesDraggable.value)
const panOnDrag = computed(() => stateManager.panOnDrag.value) const panOnDrag = computed(() => stateManager.panOnDrag.value)
@@ -146,6 +144,7 @@
const { layout } = useLayout() const { layout } = useLayout()
const index = ref(0) const index = ref(0)
async function layoutGraph(direction) { async function layoutGraph(direction) {
if (props.config.json > 0) return
if (index.value > 0) return if (index.value > 0) return
index.value++ index.value++
setTimeout(() => { setTimeout(() => {
@@ -190,15 +189,7 @@
// flowManager.exportFlow() // flowManager.exportFlow()
const str = JSON.stringify(stateManager.nodes.value) const str = JSON.stringify(stateManager.nodes.value)
const json = JSON.parse(str) emit('exportFlow', str)
putSketchFlowCanvas({
id: props.config.imgId,
canvasData: str
}).then((res) => {
if (res) {
console.log(res)
}
})
// localStorage.setItem('flow_json', str) // localStorage.setItem('flow_json', str)
} }
// 导入流程 // 导入流程
@@ -229,19 +220,9 @@
onMounted(async () => { onMounted(async () => {
// window['vueFlow'] = vueFlow // window['vueFlow'] = vueFlow
// window['nodes'] = nodes // window['nodes'] = nodes
let json = []
await new Promise((resolve) => { if (props.config.json.length > 0) {
getSketchFlowCanvas({ id: props.config.imgId }).then((res:any) => { importFlow(props.config.json)
if (res) {
json = JSON.parse(res)
}
resolve(true)
}).catch(() => {
resolve(true)
})
})
if(json.length > 0){
importFlow(json)
} else { } else {
const timestamp = Date.now() const timestamp = Date.now()
nodeManager.createResultNode({ nodeManager.createResultNode({
@@ -255,15 +236,14 @@
id: props.config.imgId, id: props.config.imgId,
url: props.config.url, url: props.config.url,
status: 'RETURNED', status: 'RETURNED',
taskId: timestamp + '', taskId: timestamp + ''
}, }
], ],
selectTaskId: timestamp + '', selectTaskId: timestamp + ''
} }
} }
}) })
} }
}) })
onBeforeMount(() => { onBeforeMount(() => {
stateManager.dispose() stateManager.dispose()

View File

@@ -1,6 +1,6 @@
<template> <template>
<fullscreen-dialog v-model="dialogVisible" hide-destroy> <fullscreen-dialog v-model="dialogVisible" hide-destroy>
<flow-canvas :config="config" /> <flow-canvas :config="config" @exportFlow="exportFlow" />
</fullscreen-dialog> </fullscreen-dialog>
</template> </template>
@@ -8,18 +8,46 @@
import FullscreenDialog from '../components/fullscreen-dialog.vue' import FullscreenDialog from '../components/fullscreen-dialog.vue'
import flowCanvas from './flow-canvas.vue' import flowCanvas from './flow-canvas.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { getSketchFlowCanvas, putSketchFlowCanvas } from '@/api/flow-canvas'
const dialogVisible = ref(false) const dialogVisible = ref(false)
const config = ref({}) const config = ref({}) as any
const open = (options) => { const open = async (options) => {
dialogVisible.value = true let json = []
config.value = options || {} await new Promise((resolve) => {
getSketchFlowCanvas({ id: options.imgId }).then((res:any) => {
if (res) {
json = JSON.parse(res)
} }
resolve(true)
}).catch(() => {
resolve(true)
})
})
config.value = options || {}
config.value.json = json
dialogVisible.value = true
}
const exportFlow = async (str) => {
if(!config.value.imgId)return
await new Promise((resolve) => {
putSketchFlowCanvas({
id: config.value.imgId,
canvasData: str }).then(() => {
resolve(true)
}).catch(() => {
resolve(true)
})
})
}
const close = () => { const close = () => {
dialogVisible.value = false dialogVisible.value = false
} }
defineExpose({ defineExpose({
open, open,
close close,
exportFlow
}) })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -97,6 +97,7 @@ export class StateManager {
return arr return arr
}) })
window.nodes = this.nodes window.nodes = this.nodes
window.aaa = this
} }
/** 设置激活节点 */ /** 设置激活节点 */

View File

@@ -36,6 +36,7 @@ export default defineConfig(({ mode }) => {
inject: 'body-last' // 注入位置优化 inject: 'body-last' // 注入位置优化
}) })
], ],
assetsInclude: ['**/*.glb'],
define: { define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
}, },