目录
📖前言
🐞创建箭头对象
🐬创建文字
👻箭头两端的线段
✈️封装方法
📖前言
CAD标注线在工程和制造领域中被广泛用于标记零部件、装配体和机械系统的尺寸、距离、角度等信息。它们帮助工程师和设计师更好地理解设计要求,并确保制造的准确性。在三维场景中添加标注线使得设计更加直观。人们可以在一个真实的三维环境中看到物体的形状、大小和相互关系,相比于传统的二维图纸,更容易理解和把握设计意图。
下面是一个简单的效果图:
要创建上图所示的标注线,我们可以把标注线拆分成三个部分:箭头线、箭头两端的线段 、文本信息。
🐞创建箭头对象
threejs中可以使用ArrowHelper创建模拟方向的三维箭头对象,但是这个只能创建一个单向的箭头,我们需要创建两个相反方向的箭头对象。
ArrowHelper由一个三维坐标点和一个单位向量来确定其位置和方向。
startPoint和endPoint分别是标注线的起点和终点坐标,通过起点和终点可以得出方向向量和长度,取两点的中点作为箭头的起点坐标,就可以绘制两个方向相反的箭头。
const startPoint = new THREE.Vector3(8, 8, 0)
const endPoint= new THREE.Vector3(8, -8, 0)
const color = 0x666666
const headLength = 1
const headWidth = 0.6
// 中点位置
const centerPoint = startPoint.clone().add(endPoint).multiplyScalar(0.5)
// 标注线长度(总长的一半)
const halfLength = startPoint.distanceTo(endPoint) / 2
// 方向向量
const dir0 = startPoint.clone().sub(endPoint).normalize() //normalize() 归一化操作,使其成为单位向量
const dir1 = dir0.clone().negate() //取反
const arrowHelper0 = new THREE.ArrowHelper(dir0, centerPoint, halfLength, color, headLength, headWidth)
const arrowHelper1 = new THREE.ArrowHelper(dir1, centerPoint, halfLength, color, headLength, headWidth)
scene.add(arrowHelper0, arrowHelper1)
🐬创建文字
threejs中可以使用TextGeometry类将文本生成为单一的几何体。
这里需要导入字体文件才能加载文本几何体,threejs自带了一些字体文件,存放在 /examples/fonts/ 路径下,可以直接导入使用。但是threejs自带的字体文件不支持中文,如果需要显示中文字体,需要另外寻找字体文件,然后可以通过 typeface.json 将字体文件转成json格式的文件。
🐬导入字体、FontLoader加载器、TextGeometry类
import gentilisRegular from 'three/examples/fonts/gentilis_regular.typeface.json'
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'
🐬使用FontLoader加载器加载字体,并调用parse方法解析JSON格式的对象:
const font = new FontLoader().parse(gentilisRegular)
🐬使用TextGeometry生成字体:
const geometry = new TextGeometry('16m', {
font: font,
size: 1,
height: 0.5,
})
//添加材质
const material = new THREE.MeshBasicMaterial({
color: 0x666666,
})
const textLabel = new THREE.Mesh(geometry, material)
scene.add(textLabel)
🐬最后就是要把字体放到合适的位置,上面我们已经计算得出了两个箭头对象的中点位置,把文字位置放到centerPoint位置:
textLabel.position.copy(centerPoint)
🐬但是文字默认是沿x轴正方向排列的,我们需要让文字沿线的方向排列,让文字和线平行:
// 创建四元数 从x轴正方向旋转到方向向量 dir0 所需的旋转角度
const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), dir0)
// 将字体的旋转设为四元数的旋转
textLabel.setRotationFromQuaternion(quaternion)
🐬但是,此时会发现字体并没有完全居中,字体的初始位置处在了中点位置,而我们需要的是整个文本的中心处在线的中心,因此需要计算文本长度,并将其平移:
geometry.computeBoundingBox()
textLabel.position.sub(
dir0.clone().multiplyScalar(geometry.boundingBox.getSize(new THREE.Vector3()).x / 2)
)
🐬如果不想字体和线过于贴合,可以计算线段的法向量,然后沿法向量方向平移:
// 法向量
const normalVector = new THREE.Vector3().crossVectors(dir0, new THREE.Vector3(0, 0, 1)).normalize()
textLabel.position.sub(normalVector.clone().multiplyScalar(0.5))
👻箭头两端的线段
我们已经知道了箭头线的方向向量,那我们可以在这个方向上创建两条线段,然后将它们旋转90°,最后再分别将它们放到startPoint和endPoint位置上即可。
// 创建线段的材质
const material = new THREE.LineBasicMaterial({
color: 0x666666 ,
linewidth: 1
})
// 创建线段的几何体
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0).clone().add(dir0.clone().multiplyScalar(2)),
new THREE.Vector3(0, 0, 0).clone().sub(dir1.clone().multiplyScalar(2))
])
const line = new THREE.LineSegments(geometry, material)
const line0 = line.clone()
const line1 = line.clone()
line0.rotateZ(Math.PI / 2)
line1.rotateZ(Math.PI / 2)
line0.position.copy(item.startPoint)
line1.position.copy(item.endPoint)
scene.add(line0, line1)
上面创建出来的线段可能会和物体连接不上,一个简单的方法就是创建一条长一点的线段,但是这样可能会使线段的另一端长出很多,显得不美观,所以我们可以通过平移线段的方法,让线段和物体能够连接上:
// 深度克隆
const line0 = new THREE.Line(line.geometry.clone(), line.material.clone())
const line1 = new THREE.Line(line.geometry.clone(), line.material.clone())
// 获取线段的顶点属性
const startPositions = line0.geometry.attributes.position.array
// 分别将线段的两个端点沿着方向向量平移
startPositions[0] += direction.x * 1.5
startPositions[1] += direction.y * 1.5
startPositions[2] += direction.z * 1.5
startPositions[3] += direction.x * 1.5
startPositions[4] += direction.y * 1.5
startPositions[5] += direction.z * 1.5
// 更新线段的几何体
line0.geometry.attributes.position.needsUpdate = true
🎯这里使用深度克隆,可以避免其中一条线修改影响到另一条线。
✈️封装方法
dimLine.js
import * as THREE from 'three'
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'
import gentilisRegular from 'three/examples/fonts/gentilis_regular.typeface.json'
export const useDimLine = () => {
// 创建实线
const createSoildLine = (data) => {
const {startPoint, endPoint, color, linewidth} = {...data}
// 创建线段的材质
const material = new THREE.LineBasicMaterial({
color: color ,
linewidth: linewidth || 1
})
// 创建线段的几何体
const geometry = new THREE.BufferGeometry().setFromPoints([startPoint, endPoint])
const line = new THREE.LineSegments(geometry, material)
return {
line,
material,
geometry
}
}
// 创建文字
const createText = (data) => {
// data = {
// text,
// position,
// size,
// height,
// color
// direction
// }
const font = new FontLoader().parse(gentilisRegular)
let geometrys = []
let materials = []
let textLabels = []
data.map(item => {
if(item.text === ''){
return
}
const geometry = new TextGeometry(item.text, {
font: font,
size: item.size,
height: item.height,
})
const material = new THREE.MeshBasicMaterial({
color: item.color,
})
const textLabel = new THREE.Mesh(geometry, material)
textLabel.position.copy(item.position)
item.direction = item.direction ? item.direction : new THREE.Vector3(1, 0, 0)
// 使用线的方向向量创建四元数 从方向向量 new THREE.Vector3(1, 0, 0) 旋转到方向向量 dir1 所需的旋转
const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1, 0, 0), item.direction)
// 将字体的旋转设为四元数的旋转
textLabel.setRotationFromQuaternion(quaternion)
// 将字体完全居中
geometry.computeBoundingBox()
textLabel.position.sub(
item.direction.clone().multiplyScalar(geometry.boundingBox.getSize(new THREE.Vector3()).x / 2)
)
// 方向向量法向量
const normalVector = new THREE.Vector3().crossVectors(item.direction, new THREE.Vector3(0, 0, 1)).normalize()
textLabel.position.sub(normalVector.clone().multiplyScalar(item.paddingBottom || 0.1))
geometrys.push(geometry)
textLabels.push(textLabel)
materials.push(material)
})
return {
geometrys,
textLabels,
materials
}
}
// 创建标注线
const createDimLine = (data) => {
// data = [
// {
// type: 'arrow', //'type'
// startPoint: new THREE.Vector3(8, 8, 0),
// endPoint: new THREE.Vector3(8, -8, 0),
// color: "#bd2fa7",
// headLength: 1, //箭头长
// headWidth: 0.6, //箭头宽
// textObj: {
// text: '57m',
// size: 1,
// height: 0.5,
// color: '#bd2fa7',
// paddingBottom: 0.1, //文字和线的距离
// },
// // 两端的线段长度
// closeLine: {
// startLength: 10, //distance+2 = 8+2 = 10 线总长
// endLength: 4,
// translateStart: -3,// startLength/2-distance = 5-8 = -3 平移距离
// translateEnd: 0,
// }
// }
// ]
const group = new THREE.Group()
let geometrys = []
let materials = []
data.map(item => {
// 中点位置
const centerPoint = item.startPoint.clone().add(item.endPoint).multiplyScalar(0.5)
// 标注线长度(一半)
const halfLength = item.startPoint.distanceTo(item.endPoint) / 2
// 方向向量 2个
const dir0 = item.startPoint.clone().sub(item.endPoint).normalize() //normalize() 归一化操作,使其成为单位向量
const dir1 = dir0.clone().negate() //取反
const arrowHelper0 = new THREE.ArrowHelper(dir0, centerPoint, halfLength, item.color, item.headLength, item.headWidth)
const arrowHelper1 = new THREE.ArrowHelper(dir1, centerPoint, halfLength, item.color, item.headLength, item.headWidth)
const direction = item.startPoint.x <= item.endPoint.x ? dir1 : dir0
const textLabelObj = createText([
{
...item.textObj,
position: centerPoint,
direction
}
])
// 标注线两端的线段
if(item.closeLine.startLength > 0){
// 创建两端线段
const lineObj0 = createSoildLine({
startPoint: new THREE.Vector3(0, 0, 0).clone().add(direction.clone().multiplyScalar(item.closeLine.startLength/2)),
endPoint: new THREE.Vector3(0, 0, 0).clone().sub(direction.clone().multiplyScalar(item.closeLine.startLength/2)),
color: item.color,
linewidth: 1
})
const line0 = lineObj0.line
// 获取线段的顶点属性
const startPositions = line0.geometry.attributes.position.array
// 分别将线段的两个端点沿着方向向量平移
startPositions[0] += direction.x * item.closeLine.translateStart
startPositions[1] += direction.y * item.closeLine.translateStart
startPositions[2] += direction.z * item.closeLine.translateStart
startPositions[3] += direction.x * item.closeLine.translateStart
startPositions[4] += direction.y * item.closeLine.translateStart
startPositions[5] += direction.z * item.closeLine.translateStart
// 更新线段的几何体
line0.geometry.attributes.position.needsUpdate = true
line0.rotateZ(Math.PI / 2)
line0.position.copy(item.startPoint)
group.add(line0)
geometrys.push(lineObj0.geometry)
materials.push(lineObj0.material)
}
if(item.closeLine.endLength > 0){
// 创建两端线段
const lineObj1 = createSoildLine({
startPoint: new THREE.Vector3(0, 0, 0).clone().add(direction.clone().multiplyScalar(item.closeLine.endLength/2)),
endPoint: new THREE.Vector3(0, 0, 0).clone().sub(direction.clone().multiplyScalar(item.closeLine.endLength/2)),
color: item.color,
linewidth: 1
})
const line1 = lineObj1.line
// 获取线段的顶点属性
const endPositions = line1.geometry.attributes.position.array
// 分别将线段的两个端点沿着方向向量平移
endPositions[0] += direction.x * item.closeLine.translateEnd
endPositions[1] += direction.y * item.closeLine.translateEnd
endPositions[2] += direction.z * item.closeLine.translateEnd
endPositions[3] += direction.x * item.closeLine.translateEnd
endPositions[4] += direction.y * item.closeLine.translateEnd
endPositions[5] += direction.z * item.closeLine.translateEnd
// 更新线段的几何体
line1.geometry.attributes.position.needsUpdate = true
line1.rotateZ(Math.PI / 2)
line1.position.copy(item.endPoint)
group.add(line1)
geometrys.push(lineObj1.geometry)
materials.push(lineObj1.material)
}
group.add(arrowHelper0, arrowHelper1, textLabelObj.textLabels[0])
geometrys.push(textLabelObj.geometrys[0])
materials.push(textLabelObj.materials[0])
})
return {
group,
geometrys,
materials
}
}
return {
createSoildLine,
createText,
createDimLine,
}
}
使用:
//导入
import { useDimLine } from './dimLine.js'
const { createDimLine } = useDimLine()
//使用
const wireDim = createDimLine([
{
startPoint: new THREE.Vector3(8, 8, 0),//起点
endPoint: new THREE.Vector3(8, -8, 0),//终点
color: "#666666",//线条颜色
headLength: 1,//箭头长度
headWidth: 0.6,//箭头宽度
textObj: {
text: '16m',//标注文本
size: 1,//文本大小
height: 0.5,//文本厚度
color: '#666666',//文本颜色
paddingBottom: 0.5,//文本和线的距离
},
// 两端的线段长度
closeLine: {
startLength: 10, //起点端的线段长度
endLength: 4,//终点端的线段长度
translateStart: -3,// 平移距离startLength/2-distance = 5-8 = -3
translateEnd: 0,
}
},
{
startPoint: new THREE.Vector3(-6, -12, 0),
endPoint: new THREE.Vector3(6, -12, 0),
color: "#666666",
headLength: 1,
headWidth: 0.6,
textObj: {
text: '12m',
size: 1,
height: 0.5,
color: '#666666',
paddingBottom: 0.5,
},
// 两端的线段长度
closeLine: {
startLength: 5,
endLength: 5,
translateStart: 1.5,
translateEnd: 1.5,
}
},
])
scene.add(wireDim.group)
参数解释:
完整代码:
<template>
<div id="three"></div>
</template>
<script setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted } from 'vue'
import { useDimLine } from './dimLine.js'
const { createDimLine } = useDimLine()
const initThree = (domContainer, cameraPos) => {
const renderer = new THREE.WebGLRenderer({
antialias: true, //抗锯齿
alpha: true
})
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, domContainer?.offsetWidth / domContainer?.offsetHeight, 0.01, 10000) //透视视角
const controls = new OrbitControls(camera, renderer.domElement) //创建控件对象
camera.position.copy(cameraPos)
camera.lookAt(new THREE.Vector3(0, 0, 0))
scene.add(camera)
// 设置背景颜色 0表示透明
renderer.setClearColor(0xffffff, 0)
renderer.setSize(domContainer?.offsetWidth, domContainer?.offsetHeight)
controls.update()
// 将webgl渲染的canvas内容添加到domContainer
domContainer?.appendChild(renderer.domElement)
const animate = () => {
renderer.render(scene, camera)
// 请求下一帧
requestAnimationFrame(animate)
}
animate()
return scene
}
onMounted(() => {
const scene = initThree(document.getElementById('three'), new THREE.Vector3(0, 0, 50))
const geometry = new THREE.ConeGeometry( 6, 16, 32 )
const material = new THREE.MeshBasicMaterial( {color: 0xe3ab9a} )
const cone = new THREE.Mesh( geometry, material )
const wireDim = createDimLine([
{
startPoint: new THREE.Vector3(8, 8, 0),
endPoint: new THREE.Vector3(8, -8, 0),
color: "#666666",
headLength: 1,
headWidth: 0.6,
textObj: {
text: '16m',
size: 1,
height: 0.5,
color: '#666666',
paddingBottom: 0.5,
},
// 两端的线段长度
closeLine: {
startLength: 10, //distance+2 = 8+2 = 10
endLength: 4,
translateStart: -3,// startLength/2-distance = 5-8 = -3
translateEnd: 0,
}
},
{
startPoint: new THREE.Vector3(-6, -12, 0),
endPoint: new THREE.Vector3(6, -12, 0),
color: "#666666",
headLength: 1,
headWidth: 0.6,
textObj: {
text: '12m',
size: 1,
height: 0.5,
color: '#666666',
paddingBottom: 0.5,
},
// 两端的线段长度
closeLine: {
startLength: 5,
endLength: 5,
translateStart: 1.5,
translateEnd: 1.5,
}
},
])
scene.add(cone, wireDim.group)
})
</script>
<style lang="scss" scoped>
#three{
width: 100vw;
height: 100vh;
}
</style>