三维装配可视化界面开发笔记

三维装配可视化界面开发笔记

项目概述

这是一个基于Vue.js和Three.js的三维装配可视化系统,用于展示机械零部件的装配和拆解过程。系统支持模型加载、拆解/装配路径生成、动画展示和工艺流程图生成等功能。

技术栈

  • 前端框架: Vue 3 (使用组合式API)
  • 构建工具: Vite 6.3.4
  • 3D引擎: Three.js r152
  • 状态管理: Pinia 2.1.0
  • 路由: Vue Router 4.2.0
  • 模型加载: GLTFLoader, OBJLoader
  • 控制器: OrbitControls

开发日志

2025-04-30

  • 初始化项目,使用npm create vite@latest创建Vue3项目
  • 安装Three.js及相关依赖:npm install three @types/three
  • 实现了基本的Three.js场景初始化,包括场景、相机、渲染器和控制器
  • 添加了模型加载功能,支持GLTF和OBJ格式
  • 遇到问题:模型加载后显示太小,调整了相机距离参数(从默认的5改为根据模型大小动态计算)
  • 踩坑:Three.js的OrbitControls需要从examples中导入,不是核心包的一部分

2025-05-01

  • 实现了部件选择和拖动功能,使用Raycaster进行射线检测
  • 添加了拆解步骤记录功能,记录部件ID、动作类型和移动路径
  • 实现了视图切换功能(正视图、俯视图、侧视图)
  • 遇到问题:视图切换后模型显示不正确,调整了相机参数和上方向设置
  • 踩坑:Three.js中相机的up向量设置对视图方向有重要影响,特别是在俯视图中需要设置为(0,0,-1)

2023-05-01

  • 修复了模型加载问题,现在可以正确加载zhuangpeitu_asm模型
  • 优化了相机控制,使模型显示更加合理(调整了fitCameraToObject函数中的边距系数)
  • 添加了模型加载失败时的备用方案(loadFallbackModel函数)
  • 遇到问题:某些复杂模型的部件层次结构难以正确解析
  • 踩坑:GLTF模型中的bin文件路径问题,需要确保bin文件和gltf文件在同一目录下

系统架构

整体架构

系统采用前端单页应用架构,使用Vue3作为框架,Three.js作为3D渲染引擎。数据流向如下:

  1. 用户交互 -> Vue组件 -> Pinia Store -> Three.js场景更新
  2. 模型加载 -> 部件提取 -> 存储到Store -> 渲染到场景
  3. 部件操作 -> 记录步骤 -> 生成工艺流程

模块划分

系统分为以下几个主要模块:

  1. 模型查看器模块:负责3D场景渲染、模型加载和交互
  2. 工艺步骤模块:记录和展示装配/拆解步骤
  3. 工艺流程图模块:可视化展示装配流程
  4. 工具栏模块:提供视图切换、模型加载等功能

项目结构

src/
├── assets/          # 静态资源
├── components/      # 组件
│   ├── ModelViewer/ # 3D模型查看器
│   │   └── ModelViewer.vue  # 核心3D渲染组件
│   ├── ProcessChart/# 工艺流程图
│   │   └── ProcessChart.vue # 流程图组件
│   ├── StepList/    # 工艺步骤列表
│   │   └── StepList.vue     # 步骤列表组件
│   └── ToolBar/     # 工具栏
│       └── ToolBar.vue      # 工具栏组件
├── router/          # 路由配置
│   └── index.js     # 路由定义
├── services/        # 服务
│   └── assemblyService.js # 装配相关服务,包含路径计算等
├── stores/          # 状态管理
│   ├── modelStore.js    # 模型状态,存储模型和部件信息
│   └── assemblyStore.js # 装配状态,存储装配步骤和播放状态
└── views/           # 页面视图
    ├── AssemblyDesignView.vue # 装配设计页面
    ├── ProcessDesignView.vue  # 工艺设计页面
    └── StepDesignView.vue     # 工步设计页面

核心文件说明

  • ModelViewer.vue: 系统核心组件,包含Three.js场景初始化、模型加载、部件交互等功能
  • assemblyStore.js: 存储装配步骤、播放状态等信息,提供步骤添加、播放控制等方法
  • modelStore.js: 存储模型信息、部件列表等,提供部件选择、信息更新等方法
  • assemblyService.js: 提供路径计算、碰撞检测等服务

关键功能实现与数据结构

模型加载

模型加载使用Three.js的GLTFLoader和OBJLoader实现。加载后会提取模型的部件信息,并存储在modelStore中。

// 加载GLTF模型
const loadGLTF = (url) => {
  const loader = new GLTFLoader()

  loader.load(
    url,
    (gltf) => {
      // 清除现有模型
      clearScene()

      // 添加新模型到场景
      scene.add(gltf.scene)

      // 调整相机位置以适应模型
      fitCameraToObject(gltf.scene)

      // 提取部件信息
      const parts = extractParts(gltf.scene)
      modelStore.setParts(parts)

      // 设置动画混合器
      if (gltf.animations && gltf.animations.length > 0) {
        animationMixer = new THREE.AnimationMixer(gltf.scene)
        gltf.animations.forEach((clip) => {
          animationMixer.clipAction(clip).play()
        })
      }
    },
    // ...错误处理
  )
}

模型加载中遇到的主要问题是bin文件路径问题。GLTF文件通常引用外部的bin文件,需要确保这些文件在正确的相对路径上。我们通过将所有模型文件放在public目录下解决了这个问题。

部件提取与数据结构

部件提取是从加载的3D模型中识别和分离各个组件的过程。我们使用以下数据结构来表示部件:

// 部件数据结构
{
  id: String,         // 部件唯一标识符
  name: String,       // 部件名称
  mesh: THREE.Mesh,   // 部件的3D网格对象
  parentId: String    // 父部件ID,用于构建层次结构
}

提取过程中,遍历模型的所有网格对象,为每个网格创建一个部件对象:

// 从模型中提取部件信息
const extractParts = (object) => {
  const parts = []

  object.traverse((child) => {
    if (child.isMesh) {
      // 为每个网格创建一个唯一ID
      const id = `part_${parts.length}`

      // 获取部件名称
      const name = child.name || `部件 ${parts.length + 1}`

      // 确定父部件ID
      let parentId = null
      if (child.parent && child.parent !== object) {
        parentId = child.parent.uuid
      }

      // 添加到部件列表
      parts.push({
        id,
        name,
        mesh: child,
        parentId
      })

      // 存储原始位置
      child.userData.originalPosition = child.position.clone()
      child.userData.originalRotation = child.rotation.clone()

      // 添加点击事件
      child.userData.partId = id
    }
  })

  return parts
}

部件拖拽与交互

实现了基于射线检测的部件选择和拖拽功能。当用户拖动部件时,会记录拆解步骤。

// 鼠标按下事件处理
const onMouseDown = (event) => {
  // 计算鼠标位置
  const rect = renderer.domElement.getBoundingClientRect()
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1

  // 设置射线
  raycaster.setFromCamera(mouse, camera)

  // 获取与射线相交的对象
  const intersects = raycaster.intersectObjects(scene.children, true)

  if (intersects.length > 0) {
    // 找到第一个有partId的对象
    const intersectedObject = intersects.find(intersect =>
      intersect.object.userData && intersect.object.userData.partId
    )

    if (intersectedObject) {
      // 禁用轨道控制器
      controls.enabled = false

      // 设置拖拽状态
      isDragging = true

      // 获取选中的部件
      const partId = intersectedObject.object.userData.partId
      selectedPart = modelStore.parts.find(part => part.id === partId)

      // 记录起始位置
      dragStartPosition.copy(selectedPart.mesh.position)

      // 设置拖拽平面
      planeNormal.copy(camera.position).sub(controls.target).normalize()
      planePoint.copy(selectedPart.mesh.position)
      plane.setFromNormalAndCoplanarPoint(planeNormal, planePoint)
    }
  }
}

拖拽过程中,使用射线与平面的交点来确定部件的新位置:

// 鼠标移动事件处理
const onMouseMove = (event) => {
  if (!isDragging || !selectedPart) return

  // 计算鼠标位置
  const rect = renderer.domElement.getBoundingClientRect()
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1

  // 设置射线
  raycaster.setFromCamera(mouse, camera)

  // 计算射线与平面的交点
  const ray = raycaster.ray
  if (ray.intersectPlane(plane, intersectionPoint)) {
    // 移动部件
    selectedPart.mesh.position.copy(intersectionPoint)

    // 更新当前位置
    dragCurrentPosition.copy(intersectionPoint)
  }
}

工艺步骤记录

当用户拖动部件完成拆解操作时,系统会记录这个步骤。步骤数据结构如下:

// 步骤数据结构
{
  partId: String,     // 部件ID
  action: String,     // 动作类型(拆解/装配)
  path: Array         // 移动路径,包含一系列位置点
}

步骤记录过程:

// 鼠标释放事件处理
const onMouseUp = () => {
  if (!isDragging || !selectedPart) return

  // 启用轨道控制器
  controls.enabled = true

  // 计算移动距离
  const distance = dragStartPosition.distanceTo(dragCurrentPosition)

  // 如果移动距离足够大,则记录拆解步骤
  if (distance > 0.5) {
    // 计算移动路径
    const path = calculateLinearPath(dragStartPosition, dragCurrentPosition, 20)

    // 记录拆解步骤
    assemblyStore.addStep({
      partId: selectedPart.id,
      action: '拆解',
      path: path
    })
  } else {
    // 如果移动距离不够,则恢复原位
    selectedPart.mesh.position.copy(dragStartPosition)
  }

  // 重置拖拽状态
  isDragging = false
  selectedPart = null
}

视图切换

实现了正视图、俯视图和侧视图的切换功能。关键是设置相机位置和上方向向量:

// 改变视角
const changeView = (viewType) => {
  // 获取模型的边界框
  const box = new THREE.Box3().setFromObject(scene)
  const size = box.getSize(new THREE.Vector3())
  const center = box.getCenter(new THREE.Vector3())

  // 计算合适的距离
  const maxDim = Math.max(size.x, size.y, size.z)
  const distance = maxDim * 1.2

  // 根据视角类型设置相机位置
  switch (viewType) {
    case 'front':
      camera.position.set(center.x, center.y, center.z + distance)
      camera.up.set(0, 1, 0) // Y轴向上
      break
    case 'top':
      camera.position.set(center.x, center.y + distance, center.z)
      camera.up.set(0, 0, -1) // Z轴向下
      break
    case 'side':
      camera.position.set(center.x + distance, center.y, center.z)
      camera.up.set(0, 1, 0) // Y轴向上
      break
  }

  // 更新相机
  camera.lookAt(center)
  camera.updateProjectionMatrix()

  // 更新控制器
  controls.update()
}

工艺流程图生成

工艺流程图基于记录的拆解步骤生成,使用简单的节点和连线表示装配关系:

// 生成工艺流程图
const generateProcessChart = () => {
  const steps = assemblyStore.steps
  if (steps.length === 0) return

  // 清除现有图表
  chartContainer.innerHTML = ''

  // 创建SVG元素
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  svg.setAttribute('width', '100%')
  svg.setAttribute('height', '100%')

  // 为每个步骤创建节点
  steps.forEach((step, index) => {
    const part = modelStore.parts.find(p => p.id === step.partId)
    if (!part) return

    // 创建节点
    const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
    node.setAttribute('cx', 50 + index * 100)
    node.setAttribute('cy', 50)
    node.setAttribute('r', 20)
    node.setAttribute('fill', '#42b883')

    // 创建标签
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
    text.setAttribute('x', 50 + index * 100)
    text.setAttribute('y', 90)
    text.setAttribute('text-anchor', 'middle')
    text.textContent = part.name

    // 添加到SVG
    svg.appendChild(node)
    svg.appendChild(text)

    // 添加连线
    if (index > 0) {
      const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
      line.setAttribute('x1', 50 + (index - 1) * 100)
      line.setAttribute('y1', 50)
      line.setAttribute('x2', 50 + index * 100)
      line.setAttribute('y2', 50)
      line.setAttribute('stroke', '#666')
      line.setAttribute('stroke-width', 2)
      svg.appendChild(line)
    }
  })

  // 添加到容器
  chartContainer.appendChild(svg)
}

开发流程与工作方式

开发流程

  1. 需求分析:确定系统功能和用户交互方式
  2. 技术选型:选择Vue3和Three.js作为主要技术栈
  3. 架构设计:设计系统模块和数据流
  4. 组件开发
    • 先开发核心的ModelViewer组件
    • 实现基本的模型加载和显示
    • 添加部件选择和拖动功能
    • 实现工艺步骤记录
    • 开发工艺流程图生成功能
  5. 集成测试:测试各模块之间的交互
  6. 优化改进:根据测试结果进行优化

工作方式

  • 使用Git进行版本控制
  • 采用组件化开发方式,每个功能模块独立开发
  • 使用Pinia进行状态管理,确保数据流的清晰性
  • 定期进行代码审查和重构,保持代码质量

参考资料

  • Three.js文档: https://threejs.org/docs/
  • Vue 3文档: https://v3.vuejs.org/
  • GLTF格式规范: https://github.com/KhronosGroup/glTF
  • Pinia状态管理: https://pinia.vuejs.org/
  • 《3D Game Engine Design》 - David H. Eberly
  • 《Learning Three.js》 - Jos Dirksen
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值