This commit is contained in:
lzp
2026-04-13 17:09:35 +08:00
27 changed files with 665 additions and 214 deletions

View File

@@ -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: {

View File

@@ -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 })

View File

@@ -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;

View File

@@ -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{

View File

@@ -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>

View File

@@ -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">

View File

@@ -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');
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}