【Three.js】创建CAD标注线

目录

📖前言

🐞创建箭头对象

🐬创建文字

👻箭头两端的线段

✈️封装方法


📖前言

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>

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值