You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

932 lines
27 KiB

<template>
<div class="model3d-view">
<el-space :gutter="10" class="toolbar">
<el-upload :on-change="handleFileChange"
:show-file-list="false" accept=".fbx,.obj,.mtl,.3ds" action="" :auto-upload="false">
<el-button type="primary">打开模型</el-button>
</el-upload>
<el-upload :on-change="handleTextureUpload"
:show-file-list="false" accept=".png,.jpg,.jpeg" action="" :auto-upload="false"
list-type="picture">
<el-button>打开贴图</el-button>
</el-upload>
<el-upload :on-change="handleMtlUpload"
:show-file-list="false" accept=".mtl" action="" :auto-upload="false"
list-type="picture">
<el-button>打开材质</el-button>
</el-upload>
<el-button @click="addConveyor">添加输送线</el-button>
<el-button @click="createShelf">添加货架</el-button>
<el-button @click="createGroundStore">添加地堆</el-button>
<div class="demo-color-block">
<span class="demonstration">材质颜色</span>
<el-color-picker v-model="restate.targetColor" />
</div>
<!-- 按 1:1 / 1:0.001 比例 -->
<div class="demo-color-block">
<span class="demonstration">默认比例</span>
<el-radio-group v-model="restate.loadScale">
<el-radio-button label="1" :value="1" />
<el-radio-button label="0.01" :value="0.01" />
<el-radio-button label="0.001" :value="0.001" />
</el-radio-group>
</div>
<div class="demo-color-block">
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span>
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span>
<span class="demonstration"> 三角形数:<el-text type="danger">{{ restate.faces }}</el-text></span>
</div>
</el-space>
<div class="main-content">
<Split class="model3d-content" :direction="'horizontal'">
<SplitArea class="model3d-canvas" :size="70">
<!-- Three.js 渲染画布 -->
<div class="canvas-container" ref="canvasContainer">
<div class="canvas-left-toolbar">
<button class="Button" :class="{selected:(restate.mode==='translate')}" title="平移"
@mousedown="restate.mode='translate'">
<component :is="renderIcon('element Rank')" />
</button>
<button class="Button" :class="{selected:(restate.mode==='rotate')}" title="旋转"
@mousedown="restate.mode='rotate'">
<component :is="renderIcon('element RefreshLeft')" />
</button>
<button class="Button" :class="{selected:(restate.mode==='scale')}" title="缩放"
@mousedown="restate.mode='scale'">
<component :is="renderIcon('element ScaleToOriginal')" />
</button>
</div>
</div>
</SplitArea>
<SplitArea class="model3d-gui" :size="30">
<div class="model3d-gui-wrap">
<!-- 右侧面板 -->
<div class="gui-toolbar">
<TransformEdit ref="transformEditCtl" />
</div>
<div class="gui-panel" ref="guiPanel"></div>
</div>
</SplitArea>
</Split>
</div>
</div>
</template>
<script setup>
import TransformEdit from '@/components/propertyEdit/TransformEdit.vue'
import { ref, onMounted, nextTick, reactive, watch, getCurrentInstance, onUnmounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
import { TDSLoader } from 'three/examples/jsm/loaders/TDSLoader'
import * as dat from 'three/examples/jsm/libs/lil-gui.module.min.js'
import Stats from 'three/examples/jsm/libs/stats.module'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'
import Split from '@/components/split/split.vue'
import SplitArea from '@/components/split/split-area.vue'
import { renderIcon } from '@/utils/webutils.js'
import textureUrl from '@/assets/images/conveyor/shapes/RibSideSkirtThumbnail.jpg'
import moveUrl from '@/assets/images/conveyor/shapes/move.svg'
import arrowRightUrl from '@/assets/images/conveyor/shapes/arrow-right.svg'
import rackUrl from '@/assets/images/conveyor/shapes/Rack.png'
// DOM refs
const canvasContainer = ref(null)
const guiPanel = ref(null)
const transformEditCtl = ref(null)
// Three.js 场景相关
let scene, camera, renderer, controls
let statsControls, axesHelper, gridHelper, animationFrameId
let gui, tcontrols, modelGroup, resizeObserver
const restate = reactive({
targetColor: '#ff0000',
loadScale: 1,
mode: 'translate',
objects: 0,
vertices: 0,
faces: 0
})
// 状态变量
const state = {
showAxesHelper: true,
showGridHelper: true,
camera: {
position: { x: 0, y: 5, z: 10 },
rotation: { x: 0, y: 0, z: 0 }
}
}
onMounted(() => {
nextTick(() => {
initThree()
initGUI()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(viewerDom)
window['model3dView'] = getCurrentInstance()
window['model3dViewRenderer'] = renderer
})
})
onBeforeUnmount(() => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
cleanupThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
window['model3dView'] = null
window['model3dViewRenderer'] = null
})
watch(() => restate.targetColor, (newVal) => {
if (modelGroup) {
modelGroup.traverse((child) => {
if (child.isMesh && child.material?.color) {
child.material.color.set(newVal)
child.material.needsUpdate = true
}
})
}
})
watch(() => restate.mode, (newVal) => {
if (tcontrols) {
tcontrols.setMode(newVal)
}
})
function addConveyor(){//输送线
// 1. 创建几何体:长 10 米,宽 0.8 米
const length = 20; // 长度 (X轴)
const width = 0.8; // 宽度 (Y轴)
const height = 0.2; // 厚度/高度 (Z轴)
const geometry = new THREE.BoxGeometry(length, width, height);
// 2. 加载纹理
const textureLoader = new THREE.TextureLoader();
textureLoader.load(textureUrl, (texture)=>{
// 3. 设置纹理参数
texture.wrapS = THREE.RepeatWrapping; // X方向重复(实际会拉伸)
texture.wrapT = THREE.RepeatWrapping; // Y方向重复
texture.center.set(0.5,0.5)
texture.rotation = Math.PI / 2
texture.repeat.set(1, 1); // 初始不缩放,后面根据需要调整
// 4. 根据输送线长度自动调整Y方向的重复次数
// 假设你的纹理高度是 256px,想要在 0.8m 宽度上显示完整的一行
// 所以我们计算 repeat.y = 0.8 / (texture.height / texture.width * length)
// 或者你可以手动设置 repeat.y 来控制重复频率
texture.repeat.set(1, 20); // 调整这个值来控制Y方向的重复频率
// 5. 创建材质并应用纹理
const material = new THREE.MeshBasicMaterial(
{ map: texture,color: 0x9f9f9f});
// 6. 创建网格对象
const conveyorBelt = new THREE.Mesh(geometry, material);
// 7. 可选:旋转为X轴方向(默认PlaneGeometry是X/Y平面)
conveyorBelt.rotation.x = -Math.PI / 2; // 让其平放在地面上(X/Z 平面)
// 放置到合适的位置(如果需要)
conveyorBelt.position.y = height / 2; // 让底部贴地
// // 示例:在输送线不同位置添加多个标记
addMarkerAt(-9.5, height,0,moveUrl); // 起点
addMarkerAt(0, height,0,arrowRightUrl); // 中间
addMarkerAt(9.5, height,0,moveUrl); // 终点
scene.add(conveyorBelt)
// 动画函数
let offsetSpeed = 0.01; // 控制滚动速度
function animate() {
requestAnimationFrame(animate);
// 沿Y轴滚动
if (texture) {
let currentOffset = texture.offset.y;
currentOffset += offsetSpeed;
// 当偏移量超过1时重置,以实现无缝循环
if (currentOffset >= 1) {
currentOffset -= 1;
}
texture.offset.set(texture.offset.x,currentOffset);
}
renderer.render(scene, camera);
}
animate();
});
}
function addMarkerAt(x, y,z,textUrl, scale = 1) {
const loader = new THREE.TextureLoader();
loader.load(textUrl, (texture) => {
// 创建一个平面作为标记(大小可以调整)
const markerGeometry = new THREE.PlaneGeometry(.6 * scale, 0.5 * scale); // 宽高比例保持一致
const markerMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 1,
depthWrite: false,
side: THREE.DoubleSide
});
const marker = new THREE.Mesh(markerGeometry, markerMaterial);
let yHeight=y+0.01
// 设置位置:位于输送线表面(Y轴略高于输送线一点)
marker.position.set(x, yHeight, z); // Y = 输送线高度 + 一点间距
// 旋转为 X/Z 平面方向(与输送线一致)
marker.rotation.x = -Math.PI / 2; // 和 conveyorBelt 一样旋转角度
marker.rotation.z = Math.PI / 2; // 在平面上旋转 90 度
scene.add(marker);
});
}
function createShelf(){//创建货架
const textureLoader = new THREE.TextureLoader();
const metalTexture = textureLoader.load('path/to/your/metal/texture.jpg'); // 替换为实际路径
// 材质设置
const material = new THREE.MeshStandardMaterial({
map: metalTexture,
metalness: 1.0,
roughness: 0.5
});
// 货架参数
const shelfLength = 10; // 总长
const shelfWidth = 0.8; // 宽度(深度)
const shelfThickness = 0.1; // 层板厚度
const rows = 3; // 层数
const columns = 4; // 列数
const spacingY = 1; // 层间距
const unitLength = shelfLength / columns; // 每个格子长度:2.5m
// ✅ 整体上浮高度:让模型位于水平面之上
const baseHeight = 2; // 单位:米
// ---------------------
// 创建层板(3层 × 4列)
// ---------------------
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
const geometry = new THREE.BoxGeometry(unitLength, shelfThickness, shelfWidth);
const mesh = new THREE.Mesh(geometry, material);
const x = -shelfLength / 2 + unitLength * col + unitLength / 2;
const y = -(shelfThickness + spacingY) * row - shelfThickness / 2 + baseHeight; // 加上偏移
const z = 0;
mesh.position.set(x, y, z);
scene.add(mesh);
}
}
// ---------------------
// 计算第三层层板底部位置(用于支撑柱对齐)
// ---------------------
const thirdShelfCenterY = -(shelfThickness + spacingY) * 2 - shelfThickness / 2 + baseHeight;
const thirdShelfBottomY = thirdShelfCenterY - shelfThickness / 2;
// ---------------------
// 创建支撑柱 + 添加贴纸
// ---------------------
const postWidth = 0.1;
const postDepth = 0.1;
const postHeight = 3.3;
const postPositionsX = [0, 2.5, 5, 7.5, 10];
const postPositionsZ = [-shelfWidth / 2, shelfWidth / 2];
// 加载贴纸纹理(可以是透明PNG)
const loader = new THREE.TextureLoader();
loader.load(rackUrl, (texture) => {
// 创建贴纸材质(支持透明)
const stickerMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide // 双面可见
});
postPositionsX.forEach(x => {
postPositionsZ.forEach(z => {
// 创建柱子
const postGeometry = new THREE.BoxGeometry(postWidth, postHeight, postDepth);
const post = new THREE.Mesh(postGeometry, material);
// 设置柱子中心 Y 坐标,使其底部对齐第三层层板底部
const postCenterY = thirdShelfBottomY + postHeight / 2;
post.position.set(
x - shelfLength / 2,
postCenterY,
z
);
scene.add(post);
// ---------------------
// 创建贴纸(附加在外侧面上)
// ---------------------
const stickerWidth = 0.08; // 贴纸宽度
const stickerHeight = 0.2; // 贴纸高度
const stickerGeometry = new THREE.PlaneGeometry(stickerWidth, stickerHeight);
const sticker = new THREE.Mesh(stickerGeometry, stickerMaterial);
// 将贴纸放在柱子外侧表面中间位置
sticker.position.set(
post.position.x + postWidth / 2, // 柱子外侧
post.position.y, // Y 中心
post.position.z // Z 对齐
);
// 让贴纸始终面向相机(可选)
// 使用 dcm.orientation.lookAt(camera.position) 或手动设置朝向
// 这里简单设置固定朝向正面(可根据需求扩展为自动朝向相机)
sticker.lookAt(new THREE.Vector3(1, 0, 0)); // 面向 X 正方向
// 微调:让贴纸略微浮出柱子表面,避免深度冲突
sticker.position.x += 0.01;
scene.add(sticker);
});
});
})
}
function createGroundStore() {
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
side: THREE.DoubleSide // 双面渲染:ml-citation{ref="5,8" data="citationList"}
});
const planeMesh = new THREE.Mesh(planeGeometry, material);
planeMesh.rotateX(Math.PI / 2)
scene.add(planeMesh);
}
function initThree() {
const viewerDom = canvasContainer.value
// 场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xeeeeee)
// 渲染器
renderer = new THREE.WebGLRenderer({
logarithmicDepthBuffer: true,
antialias: true,
physicallyCorrectLights: true,
outputEncoding: THREE.SRGBColorSpace,
alpha: true,
precision: 'mediump',
premultipliedAlpha: true,
preserveDrawingBuffer: false,
powerPreference: 'high-performance'
})
renderer.clearDepth()
renderer.shadowMap.enabled = true
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height)
viewerDom.appendChild(renderer.domElement)
// 摄像机
initMode3DCamera()
// 辅助线
axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
gridHelper = new THREE.GridHelper(40, 40)
scene.add(gridHelper)
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
directionalLight.position.set(5, 5, 5).multiplyScalar(3)
directionalLight.castShadow = true
scene.add(directionalLight)
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1)
scene.add(hemisphereLight)
// 性能监控
statsControls = new Stats()
statsControls.showPanel(0)
statsControls.dom.style.position = 'absolute'
statsControls.dom.style.bottom = '5px'
statsControls.dom.style.left = '5px'
statsControls.dom.style.top = 'unset'
viewerDom.appendChild(statsControls.dom)
// // 创建几何体和材质做测试用
// const geometry = new THREE.BoxGeometry(1, 1, 1)
// const material = new THREE.MeshStandardMaterial({
// color: 0xcccccc,
// metalness: 0.9,
// roughness: 0.1
// })
//
// const cube = new THREE.Mesh(geometry, material)
// scene.add(cube)
// camera.position.z = 5
animate()
// 转换控制器 TransformControls
tcontrols = new TransformControls(camera, renderer.domElement)
tcontrols.addEventListener('change', () => {
renderer.render(scene, camera)
transformEditCtl.value?.refreshData()
})
tcontrols.addEventListener('dragging-changed', function(event) {
controls.enabled = !event.value
})
tcontrols.setMode('translate')
scene.add(tcontrols.getHelper())
}
// 动画循环
function animate() {
animationFrameId = requestAnimationFrame(animate)
renderView()
}
function initGUI() {
const guiDom = guiPanel.value
gui = new dat.GUI({ autoPlace: false })
guiDom.appendChild(gui.domElement)
// 显示辅助线
gui.add(state, 'showAxesHelper').name('显示坐标轴').onChange(val => {
axesHelper.visible = val
})
gui.add(state, 'showGridHelper').name('显示网格').onChange(val => {
gridHelper.visible = val
})
// Position
const cameraPosFolder = gui.addFolder('摄像机位置')
cameraPosFolder.add(state.camera.position, 'x', -100, 100).step(0.1).listen().onChange(val => {
camera.position.x = val
})
cameraPosFolder.add(state.camera.position, 'y', -100, 100).step(0.1).listen().onChange(val => {
camera.position.y = val
})
cameraPosFolder.add(state.camera.position, 'z', -100, 100).step(0.1).listen().onChange(val => {
camera.position.z = val
})
cameraPosFolder.close()
// Rotation (in radians)
const cameraRotationFolder = gui.addFolder('摄像机旋转角')
cameraRotationFolder.add(state.camera.rotation, 'x', -Math.PI, Math.PI).listen().onChange(val => {
camera.rotation.x = val
})
cameraRotationFolder.add(state.camera.rotation, 'y', -Math.PI, Math.PI).listen().onChange(val => {
camera.rotation.y = val
})
cameraRotationFolder.add(state.camera.rotation, 'z', -Math.PI, Math.PI).listen().onChange(val => {
camera.rotation.z = val
})
cameraRotationFolder.close()
}
function handleResize(entries) {
for (let entry of entries) {
// entry.contentRect包含了元素的尺寸信息
console.log('Element size changed:', entry.contentRect)
const width = entry.contentRect.width
const height = entry.contentRect.height
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = width / height
camera.updateProjectionMatrix()
} else if (camera instanceof THREE.OrthographicCamera) {
camera.left = width / -2
camera.right = width / 2
camera.top = height / 2
camera.bottom = height / -2
camera.updateProjectionMatrix()
}
renderer.setSize(width, height)
break
}
}
function initMode3DCamera() {
if (camera) {
scene.remove(camera)
}
const viewerDom = canvasContainer.value
// ============================ 创建透视相机
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000)
cameraNew.position.set(5, 5, 5)
cameraNew.lookAt(0, 0, 0)
camera = cameraNew
scene.add(camera)
const controlsNew = new OrbitControls(camera, viewerDom)
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移
controlsNew.enableDamping = false
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
controlsNew.minDistance = 2
controlsNew.addEventListener('change', syncCameraState)
controls = controlsNew
controls.update()
camera.updateProjectionMatrix()
syncCameraState()
}
/**
* 重新加载相机状态到全局状态
*/
function syncCameraState() {
if (camera) {
state.camera.position.x = camera.position.x
state.camera.position.y = camera.position.y
state.camera.position.z = camera.position.z
state.camera.rotation.x = camera.rotation.x
state.camera.rotation.y = camera.rotation.y
state.camera.rotation.z = camera.rotation.z
}
}
function renderView() {
statsControls?.update()
renderer?.render(scene, camera)
}
function handleTextureUpload(file) {
console.log('file', file)
if (!file) return
file = file.raw
// 移除旧模型
// cleaupModel()
const fileName = file.name.toLowerCase()
const reader = new FileReader()
reader.onerror = (error) => {
system.showErrorDialog('加载文件失败', error.toString())
system.clearLoading()
}
if (fileName.endsWith('.png') || fileName.endsWith('.jpg') || fileName.endsWith('.jpeg')) {
reader.onload = () => {
// const textureLoader = new THREE.TextureLoader()
// const texture = textureLoader.load(reader.result)
// tcontrols.setTexture(texture)
// system.clearLoading()
const texture = new THREE.TextureLoader().load(reader.result)
// 假设模型的所有网格都应用相同的材质
modelGroup.traverse(function(child) {
if (child.isMesh) {
child.material.map = texture
child.material.needsUpdate = true
}
})
}
reader.readAsDataURL(file)
} else {
alert('不支持的文件类型!')
}
}
let lastObjfile = undefined
function handleMtlUpload(file) {
console.log('file', file)
if (!file) return
file = file.raw
// 移除旧模型
cleaupModel()
const fileName = file.name.toLowerCase()
const reader = new FileReader()
reader.onerror = (error) => {
system.showErrorDialog('加载文件失败', error.toString())
system.clearLoading()
}
if (fileName.endsWith('.mtl')) {
reader.onload = () => {
const mtlLoader = new MTLLoader()
mtlLoader.load(reader.result, (materials) => {
materials.preload()
const objLoader = new OBJLoader()
objLoader.setMaterials(materials)
const content = objLoader.parse(lastObjfile)
addGroupToScene(content)
})
}
reader.readAsText(file)
} else {
alert('不支持的文件类型!')
}
}
function addGroupToScene(group) {
modelGroup = group
if (restate.loadScale) {
// 设置默认模型缩放
modelGroup.scale.set(restate.loadScale, restate.loadScale, restate.loadScale)
}
scene.add(modelGroup)
tcontrols.attach(modelGroup)
transformEditCtl.value?.attachObject3D(modelGroup)
controls.target.copy(modelGroup.position)
controls.update()
// 遍历场景中的所有对象
let totalObjects = 0
let totalVertices = 0
let totalFaces = 0
modelGroup.traverse(function(child) {
if (child.isMesh) {
totalObjects++
// 获取几何体
const geometry = child.geometry
// 如果几何体是 BufferGeometry 类型
if (geometry.isBufferGeometry) {
// 计算顶点数
if (geometry.attributes.position) {
totalVertices += geometry.attributes.position.count
}
// 计算面数(假设每个面都是由三个顶点组成的三角形)
if (geometry.index) {
totalFaces += geometry.index.count / 3
} else if (geometry.attributes.position) {
// 如果没有索引,计算非索引几何体的面数
totalFaces += geometry.attributes.position.count / 3
}
}
// 如果几何体是 Geometry 类型(较旧的版本使用)
else if (geometry.isGeometry) {
// 计算顶点数
totalVertices += geometry.vertices.length
// 计算面数
totalFaces += geometry.faces.length
}
}
})
restate.objects = totalObjects
restate.vertices = totalVertices
restate.faces = totalFaces
}
function handleFileChange(file) {
console.log('file', file)
if (!file) return
file = file.raw
// 移除旧模型
cleaupModel()
const fileName = file.name.toLowerCase()
const reader = new FileReader()
reader.onerror = (error) => {
system.showErrorDialog('加载文件失败', error.toString())
system.clearLoading()
}
if (fileName.endsWith('.fbx')) {
system.showLoading()
reader.onload = () => {
const loader = new FBXLoader()
const arrayBuffer = reader.result
const content = loader.parse(arrayBuffer, '')
addGroupToScene(content)
system.clearLoading()
}
reader.readAsArrayBuffer(file)
} else if (fileName.endsWith('.obj')) {
reader.readAsText(file)
reader.onload = () => {
const loader = new OBJLoader()
lastObjfile = reader.result
//@ts-ignore
const content = loader.parse(reader.result)
addGroupToScene(content)
system.clearLoading()
}
} else if (fileName.endsWith('.3ds')) {
reader.onload = () => {
const loader = new TDSLoader()
const arrayBuffer = reader.result
//@ts-ignore
const content = loader.parse(arrayBuffer, '')
addGroupToScene(content)
system.clearLoading()
}
reader.readAsArrayBuffer(file)
} else {
alert('不支持的文件类型!')
}
}
function cleaupModel() {
if (modelGroup) {
scene.remove(modelGroup)
}
tcontrols.detach()
transformEditCtl.value.detach()
}
function cleanupThree() {
// 移除旧模型
if (scene) {
scene.traverse((obj) => {
// 释放几何体
if (obj.geometry) {
obj.geometry.dispose()
}
// 释放材质
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose())
} else {
obj.material.dispose()
}
}
// 释放纹理
if (obj.texture) {
obj.texture.dispose()
}
// 释放渲染目标
if (obj.renderTarget) {
obj.renderTarget.dispose()
}
// 移除事件监听(如 OrbitControls)
if (obj.dispose) {
obj.dispose()
}
})
if (modelGroup) {
scene.remove(modelGroup)
}
// 清空场景
scene.children = []
modelGroup = null
}
if (gui) {
gui.destroy()
}
if (tcontrols) {
tcontrols.dispose()
}
if (statsControls) {
statsControls.dom.remove()
}
if (renderer) {
renderer.dispose()
renderer.forceContextLoss()
console.log('WebGL disposed, memory:', renderer.info.memory)
renderer.domElement = null
}
}
</script>
<style scoped lang="less">
.model3d-view {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.canvas-left-toolbar {
position: absolute;
left: 5px;
top: 5px;
width: 32px;
background: #eee;
text-align: center;
button {
color: #555;
background-color: #ddd;
border: 0;
margin: 0;
padding: 5px 8px;
font-size: 12px;
text-transform: uppercase;
cursor: pointer;
outline: none;
}
button.selected {
background-color: #fff;
}
}
.toolbar {
padding: 10px;
background: #f5f5f5;
border-bottom: 1px solid #ccc;
}
.dialog-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
.model3d-content {
height: 100%;
display: flex;
flex-direction: row;
}
}
.canvas-container {
width: 100%;
height: 100%;
position: relative;
}
.model3d-gui-wrap {
border-left: 1px solid #ccc;
background: #111;
width: 100%;
height: 100%;
overflow-y: auto;
}
.gui-panel {
:deep(.lil-gui.root) {
width: 100%;
}
}
}
</style>