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

项目概述

这是一个基于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) {// 为每个网格创建一个唯一IDconst id = `part_${parts.length}`// 获取部件名称const name = child.name || `部件 ${parts.length + 1}`// 确定父部件IDlet parentId = nullif (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 - 1mouse.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.partIdselectedPart = 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 - 1mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1// 设置射线raycaster.setFromCamera(mouse, camera)// 计算射线与平面的交点const ray = raycaster.rayif (ray.intersectPlane(plane, intersectionPoint)) {// 移动部件selectedPart.mesh.position.copy(intersectionPoint)// 更新当前位置dragCurrentPosition.copy(intersectionPoint)}
}

工艺步骤记录

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

// 步骤数据结构
{partId: String,     // 部件IDaction: 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 = falseselectedPart = 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轴向上breakcase 'top':camera.position.set(center.x, center.y + distance, center.z)camera.up.set(0, 0, -1) // Z轴向下breakcase '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.stepsif (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// 添加到SVGsvg.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/903729.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/903729.shtml
英文地址,请注明出处:http://en.pswp.cn/news/903729.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深⼊理解指针(8)

1.对上一篇的补充内容 typedef int* ptr_t #define PTR_T int* 这两种写法都是可以的 ptr_t p1, p2; //p1, p2 都是指针变量 PTR_T p3, p4; //p3 是指针变量, p4是整型变量 为什么p3 是指针变量, p4是整型变量呢? 因为PTR_T 真的被改为了 int* 在编译器中…

neo4j暴露公网ip接口——给大模型联通知识图谱

特别鸣谢 我的领导,我的脑子,我的学习能力,感动了 1. 搭建知识图谱数据库(见上一章博客) 这里不加赘述了,请参考上一篇博客搭建 2. FastApi包装接口 这里注意:NEO4J_URI不得写http:,只能写…

AI编程新选择!VSCode + RooCode,超越Cursor​

在当今快节奏的开发环境中,AI编程助手已经成为提升开发效率的关键工具。然而,面对众多选择,开发者往往陷入纠结:如何在众多AI编程工具中找到最适合自己的方案?尤其是当VSCode搭配RooCode时,相比Cursor&…

电子病历高质量语料库构建方法与架构项目(环境聆听与自动化文档生成篇)

电子病历高质量语料库的构建是一个复杂而系统的工程,涉及数据收集、清洗、标注、验证等多个环节。在项目实施过程中,"环境聆听"和"自动化文档生成"是两个关键支撑要素,前者确保项目能够适应不断变化的技术和业务环境,后者则保障项目过程的可追溯性和知…

Python协程入门指北

一、什么是协程? 协程(Coroutine)就像可以暂停执行的函数,能够在执行过程中主动让出控制权,等准备好后再继续执行。 生活小例子 想象你在咖啡店排队: 普通函数:必须一直排到取餐&#xff08…

mysql-5.7.24-linux-glibc2.12-x86_64.tar.gz的下载安装和使用

资源获取链接: mysql-5.7.24-linux-glibc2.12-x86-64.tar.gz和使用说明资源-CSDN文库 详细作用 数据库服务器的核心文件: 这是一个压缩包,解压后包含 MySQL 数据库服务器的可执行文件、库文件、配置文件模板等。 它用于在 Linux 系统上安装…

C++笔记-继承(下)(包含派生类的默认成员函数,菱形继承等)

一.派生类的默认成员函数 1.14个常见默认成员函数 默认成员函数,默认的意思就是指我们不写,编译器会自动为我们生成一个,那么在派生类中,这几个成员函数是如何生成的呢? 1.派生类的构造函数必须调用基类的构造函数初…

C++中指针使用详解(3)数组、指针和函数参数传递的底层 ABI实现

要深入理解 数组、指针和函数参数传递 的底层 ABI(Application Binary Interface)实现,需要从以下几个维度出发进行学习: 一、什么是 ABI? ABI 是编译器和操作系统之间的协定,规定了: 函数如何…

【RustDesk 】中继1:压力测试 Python 版 RustDesk 中继服务器

测试 Python 版 RustDesk 中继服务器 测试我们实现的中继服务器有几种方法,从简单到复杂依次如下: 1. 基本连接测试客户端 创建一个简单的测试客户端来验证中继服务器的基本功能: 2. 用两个测试客户端测试中继功能 要测试完整的中继功能,你需要运行两个客户端实例来模拟…

Spring Boot集成Spring Cloud 2024(不使用Feign)

本文介绍Spring Boot集成Spring Cloud 2024,且不使用Feign,而是采用Spring 6自带的HttpExchange方式进行服务调用的详细步骤: 环境准备 Spring Boot版本:推荐使用Spring Boot 3.4.1及以上版本,以更好地与Spring Clou…

vue中$set原理

Vue 中的 $set 方法(Vue.set)主要用于 向响应式对象中添加一个新的属性,并确保这个新属性是响应式的,能够触发视图更新。 📌 背景问题:为什么需要 $set? 在 Vue 2 中,直接给对象新增…

Superset二次开发之深度解读系列:1.概述

Apache Superset 是一款现代化的企业级商业智能 Web 应用程序,专为数据探索和可视化而设计。本概述介绍了 Superset 的架构、核心组件和主要功能,以帮助开发人员了解该系统的工作原理。 What is Apache Superset? Apache Superset 是一个开源数据探索…

Linux系统之elfedit详解

elfedit 是一个用于修改 ELF(可执行与可链接格式)文件头的工具。它允许用户根据指定的条件(如机器类型、文件类型、操作系统/ABI)匹配并更新 ELF 文件的头部信息。支持 32 位和 64 位 ELF 文件,以及包含 ELF 文件的归档…

前端HTML基础知识

1.HTML介绍 HTML(HyperText Markup Language,超文本标记语言)是构成网页的基本元素,是一种用于创建网页的标准化标记语言。HTML不是一种编程语言,而是一种标记语言,通过标签来描述网页的结构和内容。 超文本:超文本是…

【IP101】图像滤波技术详解:从均值滤波到高斯滤波的完整指南

🌟 图像滤波魔法指南 🎨 在图像处理的世界里,滤波就像是给图片"美颜"的魔法工具。让我们一起来探索这些神奇的滤波术吧! 📑 目录 1. 均值滤波:图像的"磨皮"大法2. 中值滤波&#xff1…

LINE FRIENDS 正式与 Walrus 合作,全新 AI 驱动的游戏即将上线

风靡全球的 LINE FRIENDS 角色即将以“minini”迷你造型登陆 Walrus,虽然尺寸更小,但承诺带来“大”动作。IPX(LINE FRIENDS 背后的公司)打造了《minini universe: ROOM》游戏,这是一款基于其 minini 系列角色的多链游…

2025年信息素养大赛C++算法创意实践挑战赛初赛样题及答案解析(小学组)

一、选择题 1、下列代码&#xff0c;能够输出 hello world 的是_____ A. cout (hello world) B. cout << hello world C. cout:hello world D. cout << "hello world"; 答案&#xff1a;D 解析&#xff1a;cout输出的文本内容要用双引号引起来 2、…

[c语言日寄]检查环形链表

【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋&#xff1a;这是一个专注于C语言刷题的专栏&#xff0c;精选题目&#xff0c;搭配详细题解、拓展算法。从基础语法到复杂算法&#xff0c;题目涉及的知识点全面覆盖&#xff0c;助力你系统提升。无论你是初学者&#xff0c;还是…

黄雀在后:外卖大战新变局,淘宝+饿了么开启电商大零售时代

当所有人以为美团和京东的“口水战”硝烟渐散&#xff0c;外卖大战告一段落时&#xff0c;“螳螂捕蝉&#xff0c;黄雀在后”&#xff0c;淘宝闪购联合饿了么“闪现”外卖战场&#xff0c;外卖烽火再度燃起。 4 月30日&#xff0c;淘宝天猫旗下即时零售业务“小时达”正式升级…

如何在uni-app中自定义输入框placeholder的样式

在开发uni-app应用时&#xff0c;我们经常需要自定义输入框&#xff08;<input>&#xff09;的样式以匹配应用的整体设计。默认情况下&#xff0c;uni-app的输入框提供了一些基本的样式选项&#xff0c;但有时候我们需要更细致地控制输入框的每个部分&#xff0c;例如pla…