修改3d光线效果,增加3d模型选择,增加3d弹窗按钮创建新的卡片逻辑
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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"> -->
|
||||||
<!-- <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"> -->
|
||||||
<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 |
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 |
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
|
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){
|
if(NODE_DATATYPE.TO_REAL_STYLE == currentComponent.value.type && false){
|
||||||
|
//一个结果节点里面多个子节点
|
||||||
let imageProcessTasks = taskList
|
let imageProcessTasks = taskList
|
||||||
nodeManager.createResultNode({
|
nodeManager.createResultNode({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -5,20 +5,28 @@
|
|||||||
<div class="image">
|
<div class="image">
|
||||||
<img :src="data.url" alt="">
|
<img :src="data.url" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="label">Mode</p>
|
||||||
|
<my-select v-model="data.mode" :list="modeList" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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'
|
import uploadFile from '../../tools/upload-file.vue'
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
const stateManager = inject('stateManager') as any
|
const stateManager = inject('stateManager') as any
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||||
|
mode: 'Advanced',
|
||||||
})
|
})
|
||||||
|
const modeList = ref([
|
||||||
|
{ value: 'Advanced', label: 'Advanced' },
|
||||||
|
{ value: 'Normal', label: 'Normal' }
|
||||||
|
])
|
||||||
const getApiData = ()=>{
|
const getApiData = ()=>{
|
||||||
return {
|
return {
|
||||||
imageUrls: [data.url],
|
imageUrls: [data.url],
|
||||||
|
mode: data.mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defineExpose({ data,getApiData })
|
defineExpose({ data,getApiData })
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
])
|
])
|
||||||
const onPreview = (item: any) => {
|
const onPreview = (item: any) => {
|
||||||
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){
|
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{
|
}else{
|
||||||
openImagePreview(item.url)
|
openImagePreview(item.url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
//])
|
'captureView'
|
||||||
|
])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
||||||
}
|
}
|
||||||
|
const captureView = ()=>{
|
||||||
|
emit('captureView')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
@@ -70,7 +75,13 @@ const {} = toRefs(data);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -85,9 +96,9 @@ const {} = toRefs(data);
|
|||||||
line-height: 2.7rem;
|
line-height: 2.7rem;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
> .captureView ,
|
||||||
> .download{
|
> .download{
|
||||||
margin-left: 4.2rem;
|
margin-left: 4.2rem;
|
||||||
margin-top: 24.8rem;
|
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
@@ -99,6 +110,15 @@ const {} = toRefs(data);
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
> .captureView{
|
||||||
|
margin-top: 6.4rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .7rem;
|
||||||
|
}
|
||||||
|
> .download{
|
||||||
|
margin-top: 15.4rem;
|
||||||
|
}
|
||||||
> .detail{
|
> .detail{
|
||||||
> .name{
|
> .name{
|
||||||
> .title{
|
> .title{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
import threeGlb from '@/assets/images/three/sample.glb'
|
import threeGlb from '@/assets/images/three/sample.glb'
|
||||||
|
import { uploadImage } from '@/api/upload'
|
||||||
|
import { base64Tofile } from '@/components/Canvas/tools/tools'
|
||||||
|
|
||||||
import model from './model.vue'
|
import model from './model.vue'
|
||||||
import detail from './detail.vue'
|
import detail from './detail.vue'
|
||||||
@@ -11,12 +13,25 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
//])
|
'captureView'
|
||||||
|
])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const modelRef = ref(null)
|
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)
|
||||||
|
emit('captureView', {
|
||||||
|
minioUrl,
|
||||||
|
nodeId: props?.currentData?.nodeId
|
||||||
|
})
|
||||||
|
}
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
// modelRef.value.open(threeGlb)
|
// modelRef.value.open(threeGlb)
|
||||||
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
||||||
@@ -32,7 +47,7 @@ const {} = toRefs(data);
|
|||||||
<model ref="modelRef" />
|
<model ref="modelRef" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detailBox">
|
<div class="detailBox">
|
||||||
<detail ref="detailRef" :config="currentData" />
|
<detail ref="detailRef" @captureView="captureView" :config="currentData" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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,getModelInfo,calculateCameraPosition } from './threeTool'
|
import { ThreeManager } from './threeTool copy'
|
||||||
|
|
||||||
//const props = defineProps({
|
//const props = defineProps({
|
||||||
//})
|
//})
|
||||||
@@ -12,154 +12,37 @@ import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } fr
|
|||||||
//])
|
//])
|
||||||
|
|
||||||
const threeDom = ref()//threeDom元素
|
const threeDom = ref()//threeDom元素
|
||||||
|
|
||||||
let scene = shallowRef()//场景
|
|
||||||
let group = shallowRef()//组
|
|
||||||
let camera = shallowRef()//相机
|
|
||||||
let renderer = shallowRef()//渲染器
|
|
||||||
let pointLight = shallowRef();//光
|
|
||||||
let controls = shallowRef()//监听鼠标、键盘事件
|
|
||||||
|
|
||||||
const animationId = ref(null);
|
|
||||||
//加载进度
|
//加载进度
|
||||||
const load = ref({
|
const load = ref({
|
||||||
state:false,
|
state:false,
|
||||||
progress:0
|
progress:0
|
||||||
})
|
})
|
||||||
// const textureLoader = ref(new THREE.TextureLoader())//材质
|
let threeModel = null
|
||||||
const init = async () => {
|
|
||||||
//初始化threejs
|
|
||||||
if (scene.value) return
|
|
||||||
const initResult = await initThree(threeDom.value)
|
|
||||||
scene.value = initResult.scene
|
|
||||||
group.value = initResult.group
|
|
||||||
camera.value = initResult.camera
|
|
||||||
renderer.value = initResult.renderer
|
|
||||||
pointLight.value = initResult.pointLight
|
|
||||||
controls.value = initResult.controls
|
|
||||||
// pointLight.value = initResult.pointLight
|
|
||||||
|
|
||||||
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)=>{
|
const setModel = async (url:any)=>{
|
||||||
clearModel(group,scene)
|
await threeModel.setModel(url,load)
|
||||||
await addModel(url,controls,camera,pointLight,group,load)
|
|
||||||
// addMaterial()
|
|
||||||
}
|
}
|
||||||
const open = (url)=>{
|
const open = (url)=>{
|
||||||
load.value.state = true
|
load.value.state = true
|
||||||
nextTick(async ()=>{
|
nextTick(async ()=>{
|
||||||
await init()
|
threeModel = new ThreeManager(threeDom.value)
|
||||||
controls.value.enableDamping = true;
|
await threeModel.setHDRI()
|
||||||
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)
|
await setModel(url)
|
||||||
load.value.state = false
|
load.value.state = false
|
||||||
|
threeModel.operation()
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const captureView = ()=>{
|
||||||
|
return threeModel.exportAsImage()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
})
|
})
|
||||||
defineExpose({open})
|
defineExpose({open,captureView})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="modelBox">
|
<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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,18 +9,21 @@ export const initThree = async (threeDom)=>{
|
|||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
scene.add(group)
|
scene.add(group)
|
||||||
|
const studioLights = []
|
||||||
//创建相机对象
|
//创建相机对象
|
||||||
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
|
// 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);
|
const camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 1000);
|
||||||
camera.position.set(0, 90, 6); //设置相机位置
|
camera.position.set(0, 1.5, 5);
|
||||||
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
|
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 height = threeDom.offsetHeight; //窗口高度
|
||||||
const renderer = new THREE.WebGLRenderer({
|
const renderer = new THREE.WebGLRenderer({
|
||||||
antialias: true,
|
antialias: true,
|
||||||
@@ -50,6 +53,8 @@ export const initThree = async (threeDom)=>{
|
|||||||
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
||||||
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//使用hdri文件
|
||||||
try {
|
try {
|
||||||
const rgbeLoader = new RGBELoader()
|
const rgbeLoader = new RGBELoader()
|
||||||
const hdrTexture = await rgbeLoader.loadAsync(hdri)
|
const hdrTexture = await rgbeLoader.loadAsync(hdri)
|
||||||
@@ -67,6 +72,8 @@ export const initThree = async (threeDom)=>{
|
|||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 光源设置
|
* 光源设置
|
||||||
*/
|
*/
|
||||||
@@ -78,16 +85,32 @@ export const initThree = async (threeDom)=>{
|
|||||||
SpotLight 聚光源
|
SpotLight 聚光源
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 2. 定向光(主光源)
|
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
||||||
// 设置定向光的目标点(指向原点)
|
|
||||||
directionalLight.target.position.set(0, 0, 0);
|
|
||||||
scene.add(directionalLight);
|
|
||||||
|
|
||||||
//设置环境光全亮
|
//设置环境光全亮
|
||||||
//环境光
|
//环境光
|
||||||
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
||||||
scene.add(pointLight);
|
// 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);
|
// const pointLight = new THREE.AmbientLight(0xffffff,1.0);
|
||||||
// pointLight.intensity = 1.2//光源强度
|
// pointLight.intensity = 1.2//光源强度
|
||||||
// pointLight.castShadow = true//开启阴影
|
// pointLight.castShadow = true//开启阴影
|
||||||
@@ -108,7 +131,7 @@ export const initThree = async (threeDom)=>{
|
|||||||
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');
|
||||||
scene.background = new THREE.Color("#fff");
|
scene.background = new THREE.Color("#fff");
|
||||||
return {scene,group,camera,renderer,controls,pointLight}
|
return {scene,group,camera,renderer,controls,pointLight,studioLights}
|
||||||
}
|
}
|
||||||
export const clearModel = (group,scene)=>{
|
export const clearModel = (group,scene)=>{
|
||||||
const oldGroup:any = group.value;
|
const oldGroup:any = group.value;
|
||||||
@@ -258,3 +281,13 @@ 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" />
|
<image-preview ref="imagePreviewRef" />
|
||||||
<baseModal ref="threeModelRef">
|
<baseModal ref="threeModelRef">
|
||||||
<template v-slot="{ currentData }">
|
<template v-slot="{ currentData }">
|
||||||
<threeModel :currentData="currentData" />
|
<threeModel :currentData="currentData" @captureView="captureView" />
|
||||||
</template>
|
</template>
|
||||||
</baseModal>
|
</baseModal>
|
||||||
<Assistant />
|
<Assistant />
|
||||||
@@ -253,6 +253,27 @@
|
|||||||
const openThreeModelPreview = (currentData) => {
|
const openThreeModelPreview = (currentData) => {
|
||||||
threeModelRef.value.open(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('openImagePreview', openImagePreview)
|
||||||
provide('openThreeModelPreview', openThreeModelPreview)
|
provide('openThreeModelPreview', openThreeModelPreview)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ interface NodeData {
|
|||||||
data?: object// 节点数据
|
data?: object// 节点数据
|
||||||
tier?: string// 节点层级
|
tier?: string// 节点层级
|
||||||
isHeader?: boolean// 是否显示头
|
isHeader?: boolean// 是否显示头
|
||||||
superiorID?: string// 上级节点ID
|
superiorID?: string// 上级节点ID,有连接线
|
||||||
|
superiorID_?: string// 上级节点ID,没有连接线
|
||||||
superiorNodeType?: string// 上级节点类型
|
superiorNodeType?: string// 上级节点类型
|
||||||
disableDelete?: boolean// 是否禁用删除
|
disableDelete?: boolean// 是否禁用删除
|
||||||
disableCopy?: boolean// 是否禁用复制
|
disableCopy?: boolean// 是否禁用复制
|
||||||
@@ -45,23 +46,31 @@ export class NodeManager {
|
|||||||
/** 创建节点 */
|
/** 创建节点 */
|
||||||
createNode(options: NodeOptions) {
|
createNode(options: NodeOptions) {
|
||||||
const superiorID = options?.data?.superiorID
|
const superiorID = options?.data?.superiorID
|
||||||
|
const superiorID_ = options?.data?.superiorID_// 上级节点ID,用于创建子节点时使用
|
||||||
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
||||||
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
|
const superiorGenerateNodes = superiorID?this.stateManager.getSubordNodes(superiorID):[]
|
||||||
const currentNode = superiorGenerateNodes.find((node) => {
|
const currentNode = superiorGenerateNodes.find((node) => {
|
||||||
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
||||||
})
|
})
|
||||||
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
||||||
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
||||||
}, superiorGenerateNodes[0])
|
}, 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 id = options.id || createId()
|
||||||
const positionX = options.positionX || 0
|
const positionX = options.positionX || 0
|
||||||
const positionY = options.positionY || 0
|
const positionY = options.positionY || 0
|
||||||
const position = options.position ||
|
const position = options.position ||
|
||||||
(
|
(
|
||||||
currentNode ?
|
currentNode ?//当前节点位置,覆盖操作
|
||||||
currentNode.position :
|
currentNode.position :
|
||||||
endGenerateNode ?
|
endGenerateNode ?//最大子级位置
|
||||||
{
|
{
|
||||||
x: endGenerateNode.position.x + positionX,
|
x: endGenerateNode.position.x + positionX,
|
||||||
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modalWidth: {
|
modalWidth: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
})
|
})
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
let currentData = ref(null)
|
let currentData = ref(null)
|
||||||
const open = (data: any,) => {
|
const open = (data: any) => {
|
||||||
currentData.value = data
|
currentData.value = data
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ export default {
|
|||||||
//3d面板
|
//3d面板
|
||||||
threeModel: {
|
threeModel: {
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
download: 'Download'
|
download: 'Download',
|
||||||
|
captureView: 'Capture View',
|
||||||
},
|
},
|
||||||
DepthCanvas: {
|
DepthCanvas: {
|
||||||
layer: 'Layer',
|
layer: 'Layer',
|
||||||
|
|||||||
@@ -217,7 +217,8 @@ export default {
|
|||||||
//3d面板
|
//3d面板
|
||||||
threeModel: {
|
threeModel: {
|
||||||
loading: '加载中',
|
loading: '加载中',
|
||||||
download: '下载'
|
download: '下载',
|
||||||
|
captureView: '捕获视图',
|
||||||
},
|
},
|
||||||
DepthCanvas: {
|
DepthCanvas: {
|
||||||
layer: '图层',
|
layer: '图层',
|
||||||
|
|||||||
Reference in New Issue
Block a user