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.
1219 lines
36 KiB
1219 lines
36 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,.glb,.gltf" action="" :auto-upload="false">
|
|
<el-button type="primary">打开模型</el-button>
|
|
</el-upload>
|
|
<el-upload :on-change="handleCadFileChange"
|
|
:show-file-list="false" accept=".dxf" action="" :auto-upload="false">
|
|
<el-button type="primary">打开CAD</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">添加货架2</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 { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
|
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 rackPlatUrl from '@/assets/images/conveyor/shapes/RackPlatform.png'
|
|
import rackBlue from '@/assets/images/conveyor/shapes/Rack-blue.png'
|
|
import triangleUrl from '@/assets/images/conveyor/shapes/triangle.png'
|
|
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'
|
|
import Plastic_Rough_JPG from "@/assets/Models/Plastic_Rough.jpg";
|
|
import storageBar_PNG from "@/assets/Models/storageBar.png";
|
|
// import {DXFViewer} from "three-dxf-viewer";
|
|
import cadFont from "@/assets/fonts/helvetiker_regular.typeface.json?url";
|
|
|
|
// 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() {
|
|
const conveyorLength = 10 // 长度
|
|
const conveyorWidth = 0.8 // 宽度
|
|
const conveyorHeight = 0.08 // 厚度
|
|
const wallHeight = conveyorHeight + 0.15 // 挡板高出输送线 0.2 米
|
|
|
|
const legWidth = 0.1 // 柱子宽度(X)
|
|
const legDepth = 0.1 // 柱子深度(Z)
|
|
const legHeight = 0.8 // 柱子高度(Y)
|
|
|
|
// 创建输送线底部板子(灰色)
|
|
const conveyorGeometry = new THREE.BoxGeometry(conveyorWidth, conveyorHeight, conveyorLength)
|
|
const conveyorMaterial = new THREE.MeshBasicMaterial({ color: 0x6a6a6a })
|
|
const conveyor = new THREE.Mesh(conveyorGeometry, conveyorMaterial)
|
|
conveyor.position.y = conveyorHeight / 2
|
|
|
|
// 添加两侧挡板(灰色)
|
|
const wallWidth = 0.05
|
|
const wallGeometry = new THREE.BoxGeometry(wallWidth, wallHeight, conveyorLength)
|
|
const wallMaterial = new THREE.MeshBasicMaterial({ color: 0x6a6a6a })
|
|
|
|
const leftWall = new THREE.Mesh(wallGeometry, wallMaterial)
|
|
leftWall.position.set(-(conveyorWidth / 2 + wallWidth / 2), wallHeight / 2, 0)
|
|
|
|
const rightWall = new THREE.Mesh(wallGeometry, wallMaterial)
|
|
rightWall.position.set((conveyorWidth / 2 + wallWidth / 2), wallHeight / 2, 0)
|
|
|
|
// 创建柱子(灰色,类似桌腿)
|
|
const legGeometry = new THREE.BoxGeometry(legWidth, legHeight, legDepth)
|
|
const legMaterial = new THREE.MeshBasicMaterial({ color: 0x6a6a6a }) // 灰色
|
|
|
|
const legPositions = [
|
|
[conveyorWidth / 2 - legWidth / 2, -(conveyorLength / 2 - legDepth / 2)], // 右前
|
|
[-conveyorWidth / 2 + legWidth / 2, -(conveyorLength / 2 - legDepth / 2)], // 左前
|
|
[conveyorWidth / 2 - legWidth / 2, +(conveyorLength / 2 - legDepth / 2)], // 右后
|
|
[-conveyorWidth / 2 + legWidth / 2, +(conveyorLength / 2 - legDepth / 2)] // 左后
|
|
]
|
|
|
|
const legs = []
|
|
|
|
legPositions.forEach(pos => {
|
|
const leg = new THREE.Mesh(legGeometry, legMaterial)
|
|
leg.position.set(
|
|
pos[0],
|
|
-legHeight / 2,
|
|
pos[1]
|
|
)
|
|
legs.push(leg)
|
|
})
|
|
|
|
// ====== 新增部分:顶部带圆角的长方体(胶囊型)======
|
|
const beamWidth = conveyorWidth // 和输送线一样宽(0.8米)
|
|
const beamLength = conveyorLength - conveyorHeight / 2// 和输送线一样长(10米)
|
|
const beamHeight = 0.08 // 高度(0.1米)
|
|
|
|
// 创建 Shape(二维路径)
|
|
const shape = new THREE.Shape()
|
|
|
|
const radius = beamHeight / 2 // 圆角半径 = 一半宽度
|
|
|
|
// 左侧半圆(从顶部开始,顺时针)
|
|
shape.absarc(-beamLength / 2, 0, radius, Math.PI / 2, -Math.PI / 2, false)
|
|
|
|
// 右侧半圆(从底部开始,顺时针)
|
|
shape.absarc(beamLength / 2, 0, radius, -Math.PI / 2, Math.PI / 2, false)
|
|
|
|
shape.closePath() // 封闭路径
|
|
|
|
// 拉伸成三维几何体
|
|
const extrudeSettings = {
|
|
depth: beamWidth,
|
|
steps: 16,
|
|
bevelEnabled: false
|
|
}
|
|
|
|
const beamGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
|
|
const beamMaterial = new THREE.MeshBasicMaterial({ color: 0x032702 }) // 粉色
|
|
|
|
const beam = new THREE.Mesh(beamGeometry, beamMaterial)
|
|
beam.rotation.x = Math.PI / 2
|
|
beam.rotation.z = Math.PI / 2
|
|
beam.rotation.y = Math.PI / 2
|
|
beam.position.y = beamHeight + conveyorHeight / 2 // 底部贴着输送线顶部
|
|
beam.position.x = -beamWidth / 2
|
|
addMarkerAt(0, legHeight + beamHeight + conveyorHeight, 0, triangleUrl, 1, false)
|
|
addMarkerAt(beamWidth / 2 + wallWidth + 0.01, legHeight + wallHeight / 2, 0, triangleUrl, .5, true)
|
|
addMarkerAt(beamWidth / 2 + wallWidth + 0.01, legHeight + wallHeight / 2, conveyorLength / 4, triangleUrl, .5, true)
|
|
addMarkerAt(beamWidth / 2 + wallWidth + 0.01, legHeight + wallHeight / 2, -conveyorLength / 4, triangleUrl, .5, true)
|
|
|
|
addMarkerAt(-(beamWidth / 2 + wallWidth + 0.01), legHeight + wallHeight / 2, 0, triangleUrl, .5, true)
|
|
addMarkerAt(-(beamWidth / 2 + wallWidth + 0.01), legHeight + wallHeight / 2, conveyorLength / 4, triangleUrl, .5, true)
|
|
addMarkerAt(-(beamWidth / 2 + wallWidth + 0.01), legHeight + wallHeight / 2, -conveyorLength / 4, triangleUrl, .5, true)
|
|
// 将所有元素组合成一个组
|
|
const group = new THREE.Group()
|
|
group.add(conveyor)
|
|
group.add(leftWall)
|
|
group.add(rightWall)
|
|
legs.forEach(leg => group.add(leg))
|
|
group.add(beam) // 添加新长方体
|
|
// 设置组沿Z轴向上移动的距离,例如 1 米
|
|
group.position.y = legHeight // 修改这里的值来改变提升的高度
|
|
scene.add(group)
|
|
}
|
|
|
|
function addMarkerAt(x, y, z, textUrl, scale = 1, lip) {
|
|
const loader = new THREE.TextureLoader()
|
|
loader.load(textUrl, (texture) => {
|
|
// 创建一个平面作为标记(大小可以调整)
|
|
let markerGeometry
|
|
if (lip) {
|
|
markerGeometry = new THREE.PlaneGeometry(.25 * scale, .25 * scale)
|
|
} else {
|
|
markerGeometry = new THREE.PlaneGeometry(.45 * scale, .3 * 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 度
|
|
if (lip) {
|
|
marker.rotation.y = Math.PI / 2 // 在平面上旋转 90 度
|
|
} else {
|
|
marker.rotation.y = Math.PI
|
|
}
|
|
scene.add(marker)
|
|
})
|
|
}
|
|
|
|
function createShelf() {//创建货架
|
|
const shelfLength = 5
|
|
const shelfWidth = 0.8
|
|
const shelfThickness = 0.02
|
|
const rows = 3
|
|
const columns = 4
|
|
const spacingY = 1 // 每行之间的间距
|
|
const gapBetweenPlanks = 0.1 // 层板之间的间隙宽度
|
|
const baseHeight = 0 // 地面高度设为0
|
|
|
|
const unitLength = (shelfLength - (columns - 1) * gapBetweenPlanks) / columns
|
|
|
|
const textureLoader = new THREE.TextureLoader()
|
|
const shelfTexture = textureLoader.load(rackPlatUrl, () => {
|
|
renderer.render(scene, camera)
|
|
})
|
|
const redPoleTexture = textureLoader.load(rackBlue, () => {
|
|
renderer.render(scene, camera) // 确保贴图加载后重新渲染
|
|
})
|
|
|
|
shelfTexture.wrapS = THREE.RepeatWrapping
|
|
shelfTexture.wrapT = THREE.RepeatWrapping
|
|
shelfTexture.repeat.set(1, 20)
|
|
shelfTexture.rotation = Math.PI / 2
|
|
|
|
const shelfMaterial = new THREE.MeshPhongMaterial({
|
|
map: shelfTexture,
|
|
color: 0x999999,
|
|
shininess: 60
|
|
})
|
|
|
|
// 垂直柱子材质
|
|
const poleMaterial = new THREE.MeshPhongMaterial({
|
|
map: shelfTexture,
|
|
color: 0x333333,
|
|
shininess: 60
|
|
})
|
|
|
|
// 柱子尺寸
|
|
const poleSize = gapBetweenPlanks // 柱子的宽度和深度都等于缝隙宽度
|
|
const totalHeight = rows * spacingY // 柱子总高度
|
|
|
|
for (let row = 0; row < rows; row++) {
|
|
let currentXOffset = -shelfLength / 2
|
|
|
|
for (let col = 0; col < columns; col++) {
|
|
const geometry = new THREE.BoxGeometry(unitLength, shelfThickness, shelfWidth)
|
|
const mesh = new THREE.Mesh(geometry, shelfMaterial)
|
|
|
|
const x = currentXOffset + unitLength / 2
|
|
// 计算层板的 Y 坐标,使其基于地面高度
|
|
const y = baseHeight + row * spacingY + shelfThickness / 2
|
|
const z = 0
|
|
|
|
mesh.position.set(x, y, z)
|
|
scene.add(mesh)
|
|
|
|
if (row === 0) { // 只在第一层添加垂直柱子
|
|
// 最左侧柱子
|
|
if (col === 0) {
|
|
const leftPoleX = currentXOffset - poleSize / 2
|
|
const leftPoleY = baseHeight + totalHeight / 2
|
|
const leftPoleZ = z - shelfWidth / 2 + poleSize / 2
|
|
|
|
// 黑色柱子
|
|
const leftPole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
poleMaterial
|
|
)
|
|
leftPole.position.set(leftPoleX, leftPoleY, leftPoleZ)
|
|
scene.add(leftPole)
|
|
|
|
// // 红色柱子
|
|
const redLeftPoleZ = z + shelfWidth / 2 - poleSize / 2
|
|
const redLeftPole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
new THREE.MeshPhongMaterial({
|
|
map: redPoleTexture,
|
|
color: 0xffffff, // 颜色可保留作为叠加色
|
|
shininess: 60,
|
|
transparent: true
|
|
})
|
|
// new THREE.MeshPhongMaterial({ color: 0xff0000, shininess: 60 })
|
|
)
|
|
redLeftPole.position.set(leftPoleX, leftPoleY, redLeftPoleZ)
|
|
scene.add(redLeftPole)
|
|
}
|
|
|
|
// 层板间隙中的柱子
|
|
if (col < columns - 1) {
|
|
const gapStartX = currentXOffset + unitLength
|
|
|
|
// 原有灰色柱子(居中)
|
|
const poleX = gapStartX + poleSize / 2
|
|
const poleY = baseHeight + totalHeight / 2
|
|
const poleZ = z - shelfWidth / 2 + poleSize / 2
|
|
|
|
const pole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
poleMaterial
|
|
)
|
|
pole.position.set(poleX, poleY, poleZ)
|
|
scene.add(pole)
|
|
|
|
// 新增红色柱子,在原有柱子的前面或后面并排
|
|
const redPoleX = poleX // 和灰柱 X 相同
|
|
const redPoleY = poleY // 和灰柱 Y 相同
|
|
const redPoleZ = z + shelfWidth / 2 - poleSize / 2 // 在前方
|
|
|
|
const redPoleMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0xff0000,
|
|
shininess: 60
|
|
})
|
|
|
|
const redPole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
redPoleMaterial
|
|
)
|
|
redPole.position.set(redPoleX, redPoleY, redPoleZ)
|
|
scene.add(redPole)
|
|
}
|
|
|
|
// 最右侧柱子
|
|
if (col === columns - 1) {
|
|
const rightPoleX = currentXOffset + unitLength + gapBetweenPlanks - poleSize / 2
|
|
const rightPoleY = baseHeight + totalHeight / 2
|
|
const rightPoleZ = z - shelfWidth / 2 + poleSize / 2
|
|
|
|
// 黑色柱子
|
|
const rightPole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
poleMaterial
|
|
)
|
|
rightPole.position.set(rightPoleX, rightPoleY, rightPoleZ)
|
|
scene.add(rightPole)
|
|
|
|
// 红色柱子
|
|
const redRightPoleZ = z + shelfWidth / 2 - poleSize / 2
|
|
const redRightPole = new THREE.Mesh(
|
|
new THREE.BoxGeometry(poleSize, totalHeight, poleSize),
|
|
new THREE.MeshPhongMaterial({ color: 0xff0000, shininess: 60 })
|
|
)
|
|
redRightPole.position.set(rightPoleX, rightPoleY, redRightPoleZ)
|
|
scene.add(redRightPole)
|
|
}
|
|
}
|
|
|
|
currentXOffset += unitLength + gapBetweenPlanks
|
|
}
|
|
}
|
|
|
|
// 光源
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
|
scene.add(ambientLight)
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
|
directionalLight.position.set(5, 10, 7)
|
|
scene.add(directionalLight)
|
|
}
|
|
|
|
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)
|
|
|
|
let textureLoader = new THREE.TextureLoader()
|
|
|
|
const texture1 = textureLoader.load(storageBar_PNG);
|
|
const texture2 = textureLoader.load(Plastic_Rough_JPG);
|
|
|
|
|
|
const curve = new THREE.CatmullRomCurve3(
|
|
[new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 2, 0)],
|
|
false, // 闭合曲线
|
|
'catmullrom',
|
|
0
|
|
);
|
|
// const points = curve.getPoints(10);
|
|
// const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
// const material = new THREE.LineBasicMaterial({ color: 0x00ff00 });
|
|
// const lineS = new THREE.Line(geometry, material);
|
|
// group.add(lineS as THREE.Object3D)
|
|
|
|
|
|
const splits = [
|
|
[0, 0],
|
|
[0.025, 0],
|
|
[0.04, 0.005],
|
|
[0.06, 0.005],
|
|
[0.075, 0],
|
|
[0.1, 0],
|
|
[0.1, 0.092],
|
|
[0.092, 0.1],
|
|
[0.075, 0.1],
|
|
[0.075, 0.092],
|
|
[0.092, 0.092],
|
|
[0.092, 0.008],
|
|
[0.008, 0.008],
|
|
[0.008, 0.092],
|
|
[0.025, 0.092],
|
|
[0.025, 0.1],
|
|
[0.008, 0.1],
|
|
[0, 0.092],
|
|
[0, 0]
|
|
]
|
|
|
|
|
|
|
|
let pointsShape = [];
|
|
for (let i = 0; i < splits.length ; i ++) {
|
|
let _x = splits[i][0] //* 10;
|
|
let _y = splits[i][1] //* 10;
|
|
pointsShape.push({
|
|
x: _x,
|
|
y: _y,
|
|
});
|
|
}
|
|
|
|
const shape = new THREE.Shape();
|
|
shape.moveTo(pointsShape[0].x, pointsShape[0].y);
|
|
for (let i = 1; i < pointsShape.length; i++) {
|
|
shape.lineTo(pointsShape[i].x , pointsShape[i].y);
|
|
}
|
|
|
|
const options = {
|
|
steps: 1,
|
|
bevelEnabled: false,
|
|
extrudePath: curve,
|
|
};
|
|
|
|
const geometry1 = new THREE.ExtrudeGeometry(shape, options);
|
|
|
|
resetUVs(geometry1);
|
|
|
|
|
|
// 设置贴图在 U 和 V 方向上的重复次数
|
|
texture1.repeat.set(10, 18); // X轴重复2次,Y轴重复3次(相当于缩小纹理)
|
|
texture2.repeat.set(2, 2); // X轴重复2次,Y轴重复3次(相当于缩小纹理)
|
|
// texture1.offset.set(0.5, 0)
|
|
texture1.center.set(0.5, 0)
|
|
// 必须设置包裹模式为重复
|
|
texture1.wrapS = THREE.RepeatWrapping;
|
|
texture1.wrapT = THREE.RepeatWrapping;
|
|
|
|
texture2.wrapS = THREE.RepeatWrapping;
|
|
texture2.wrapT = THREE.RepeatWrapping;
|
|
|
|
const material1 = new THREE.MeshPhongMaterial({
|
|
// color: '#FF3549',
|
|
alphaMap: texture1, // 应用纹理
|
|
normalMap: texture2, // 应用纹理
|
|
// side: THREE.DoubleSide // 如果你的几何体有双面,确保这一面也被贴图
|
|
// metalness: 0.6,
|
|
// roughness: 0.8,
|
|
// specular: 0x6d6d6d,
|
|
transparent: true,
|
|
needsUpdate: true,
|
|
});
|
|
material1.color.setHex(0xFF35499C, "srgb");
|
|
|
|
geometry1.scale(0.8, 1, 1)
|
|
// const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
|
// const mesh1 = new THREE.Mesh(geometry1, material1);
|
|
|
|
let mesh = new THREE.InstancedMesh(geometry1, material1, 1600);
|
|
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
|
|
let dummy = new THREE.Object3D();
|
|
|
|
|
|
for (let i = 0; i < 40; i++) {
|
|
for (let j = 0; j < 40; j++) {
|
|
dummy.position.set(i * 0.5, 0, j * 0.5);
|
|
dummy.updateMatrix();
|
|
mesh.setMatrixAt(i*40 + j, dummy.matrix);
|
|
}
|
|
}
|
|
|
|
scene.add(mesh);
|
|
}
|
|
|
|
function resetUVs(geometry) {
|
|
if (geometry == undefined) return;
|
|
var pos = geometry.getAttribute("position"),
|
|
nor = geometry.getAttribute("normal"),
|
|
uvs = geometry.getAttribute("uv");
|
|
|
|
for (var i = 0; i < pos.count; i++) {
|
|
var x = 0,
|
|
y = 0;
|
|
|
|
var nx = Math.abs(nor.getX(i)),
|
|
ny = Math.abs(nor.getY(i)),
|
|
nz = Math.abs(nor.getZ(i));
|
|
|
|
// if facing X
|
|
if (nx >= ny && nx >= nz) {
|
|
x = pos.getZ(i);
|
|
y = pos.getY(i);
|
|
}
|
|
|
|
// if facing Y
|
|
if (ny >= nx && ny >= nz) {
|
|
x = pos.getX(i);
|
|
y = pos.getZ(i);
|
|
}
|
|
|
|
// if facing Z
|
|
if (nz >= nx && nz >= ny) {
|
|
x = pos.getX(i);
|
|
y = pos.getY(i);
|
|
}
|
|
|
|
uvs.setXY(i, x, y);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
console.log('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()
|
|
}
|
|
|
|
} 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()
|
|
}
|
|
|
|
} else if (fileName.endsWith('.glb') || fileName.endsWith('.gltf')) {
|
|
reader.onload = () => {
|
|
const loader = new GLTFLoader()
|
|
const arrayBuffer = reader.result
|
|
//@ts-ignore
|
|
loader.parseAsync(arrayBuffer, '').then((content) => {
|
|
addGroupToScene(content.scene)
|
|
})
|
|
|
|
system.clearLoading()
|
|
}
|
|
|
|
} else {
|
|
alert('不支持的文件类型!')
|
|
return
|
|
}
|
|
|
|
reader.readAsArrayBuffer(file)
|
|
}
|
|
|
|
async function handleCadFileChange(file) {
|
|
console.log('file', file)
|
|
if (!file) return
|
|
|
|
file = file.raw
|
|
const viewer = new DXFViewer();
|
|
let dxf = await viewer.getFromFile(file, cadFont);
|
|
dxf.scale.set(0.001, 0.001, 0.001)
|
|
dxf.position.x = -3
|
|
dxf.position.y = 1
|
|
dxf.rotation.x = -Math.PI / 2
|
|
// Add the geometry to the scene
|
|
scene.add(dxf);
|
|
}
|
|
|
|
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;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.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>
|
|
|