Vue使用Three.js加载glb (gltf) 文件模型及实现简单的选中高亮、测距、测面积
安装:
# three.jsnpm install --save three
附中文网:
5. gltf不同文件形式(.glb) | Three.js中文网
附官网:
安装 – three.js docs
完整代码(简易demo):
<template><div class="siteInspection" ref="threeContainer"><div class="measurement-buttons"><button @click="startDistanceMeasurement">测距</button><button @click="startAreaMeasurement">测面积</button></div></div>
</template><script lang="ts" setup>
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";const lzModel = ref("/lz.glb"); // 此处为你的 模型文件 .glb或 .gltf
const threeContainer = ref(null);// 创建场景、相机和渲染器
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let model: THREE.Group | null = null;
let controls: OrbitControls;
let raycaster: THREE.Raycaster;
let mouse: THREE.Vector2;
let selectedObject: THREE.Mesh | null = null; // 用于跟踪当前选中的对象
let line: THREE.Line | null = null; // 用于存储高亮线
let label: THREE.Sprite | null = null; // 用于存储标签// 测量相关变量
let isDistanceMeasuring = false;
let isAreaMeasuring = false;
let distancePoints: THREE.Vector3[] = [];
let areaPoints: THREE.Vector3[] = [];
let distanceLine: THREE.Line | null = null;
let areaLines: THREE.Line[] = [];
let distanceLabel: THREE.Sprite | null = null;
let areaLabel: THREE.Sprite | null = null;
let distanceDots: THREE.Mesh[] = [];
let areaDots: THREE.Mesh[] = [];// 初始化
function initThree() {scene = new THREE.Scene();scene.background = new THREE.Color(0xffffff); // 设置背景颜色为亮色camera = new THREE.PerspectiveCamera(75,window.innerWidth / window.innerHeight,0.1,1000);renderer = new THREE.WebGLRenderer();renderer.setSize(window.innerWidth, window.innerHeight);threeContainer.value.appendChild(renderer.domElement);// 添加光源const ambientLight = new THREE.AmbientLight(0x404040); // 环境光scene.add(ambientLight);const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);directionalLight.position.set(1, 1, 1).normalize();scene.add(directionalLight);// 设置相机位置camera.position.z = 5;camera.lookAt(0, 0, 0);// 创建GLTF加载器对象const loader = new GLTFLoader();loader.load(lzModel.value, function (gltf: any) {model = gltf.scene;scene.add(model);});// 初始化OrbitControlscontrols = new OrbitControls(camera, renderer.domElement);controls.enableDamping = true; // 启用阻尼效果controls.dampingFactor = 0.25; // 阻尼系数controls.enableZoom = true; // 启用缩放controls.enablePan = true; // 启用平移// 初始化Raycaster和Vector2raycaster = new THREE.Raycaster();mouse = new THREE.Vector2();// 渲染循环function animate() {requestAnimationFrame(animate);controls.update(); // 更新OrbitControlsrenderer.render(scene, camera);}animate();// 清理事件监听器onUnmounted(() => {controls?.dispose();threeContainer.value?.removeEventListener("mousedown", onDocumentMouseDown);if (line) {scene.remove(line);}if (label) {scene.remove(label);}if (distanceLine) {scene.remove(distanceLine);}areaLines.forEach((areaLine) => {scene.remove(areaLine);});if (distanceLabel) {scene.remove(distanceLabel);}if (areaLabel) {scene.remove(areaLabel);}distanceDots.forEach((dot) => {scene.remove(dot);});areaDots.forEach((dot) => {scene.remove(dot);});});// 添加鼠标点击事件监听threeContainer.value.addEventListener("mousedown", onDocumentMouseDown);
}function onDocumentMouseDown(event: MouseEvent) {// 将鼠标位置归一化到-1到1之间mouse.x = (event.clientX / window.innerWidth) * 2 - 1;mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;// 更新射线投射器raycaster.setFromCamera(mouse, camera);// 计算交点const intersects = raycaster.intersectObjects(model ? model.children : [],true);if (intersects.length > 0) {const intersect = intersects[0];const point = intersect.point;if (isDistanceMeasuring) {if (event.button === 0) {// 左键点击distancePoints.push(point);addDot(point, distanceDots);if (distancePoints.length === 2) {calculateDistance();}} else if (event.button === 2) {// 右键点击cancelDistanceMeasurement();}} else if (isAreaMeasuring) {if (event.button === 0) {// 左键点击areaPoints.push(point);addDot(point, areaDots);if (areaPoints.length >= 3) {updateAreaLines();// calculateArea();}} else if (event.button === 2) {// 右键点击if (areaPoints.length >= 3) {calculateArea();} else {cancelAreaMeasurement();}}} else {// 原有的点击选中功能if (selectedObject) {(selectedObject.material as THREE.MeshStandardMaterial).emissive.set(0x000000); // 恢复 emissive 颜色为黑色if (line) {scene.remove(line);line = null;}if (label) {scene.remove(label);label = null;}}if (intersect.object instanceof THREE.Mesh) {(intersect.object.material as THREE.MeshStandardMaterial).emissive.set(0x16d46b); // 设置 emissive 颜色为绿色selectedObject = intersect.object; // 更新选中的对象// 创建线段const startPoint = intersect.point;const endPoint = startPoint.clone().add(new THREE.Vector3(0, 0, 0.5)); // 延伸到外部const geometry = new THREE.BufferGeometry().setFromPoints([startPoint,endPoint,]);const material = new THREE.LineBasicMaterial({color: 0x16d46b,});line = new THREE.Line(geometry, material);scene.add(line);// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const textWidth = context.measureText(intersect.object.name).width;canvas.width = textWidth + 20;canvas.height = 50;context.font = "16px";// context.fillStyle = "rgba(0,0,0,0.8)";context.fillRect(0, 0, canvas.width, canvas.height);context.fillStyle = "#0179d4";context.textAlign = "center";context.fillText(intersect.object.name, canvas.width / 2, 30);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });label = new THREE.Sprite(spriteMaterial);label.position.copy(endPoint);label.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(label);}}}} else {// 如果没有点击到任何对象,恢复当前选中的对象的颜色并移除线和标签if (selectedObject) {(selectedObject.material as THREE.MeshStandardMaterial).emissive.set(0x000000); // 恢复 emissive 颜色为黑色selectedObject = null; // 清除选中的对象if (line) {scene.remove(line);line = null;}if (label) {scene.remove(label);label = null;}}}
}function startDistanceMeasurement() {isDistanceMeasuring = true;isAreaMeasuring = false;distancePoints = [];distanceDots.forEach((dot) => {scene.remove(dot);});distanceDots = [];if (distanceLine) {scene.remove(distanceLine);distanceLine = null;}if (distanceLabel) {scene.remove(distanceLabel);distanceLabel = null;}
}function startAreaMeasurement() {isAreaMeasuring = true;isDistanceMeasuring = false;areaPoints = [];areaDots.forEach((dot) => {scene.remove(dot);});areaDots = [];areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];if (areaLabel) {scene.remove(areaLabel);areaLabel = null;}
}function calculateDistance() {const startPoint = distancePoints[0];const endPoint = distancePoints[1];const distance = startPoint.distanceTo(endPoint);// 创建线段const geometry = new THREE.BufferGeometry().setFromPoints([startPoint,endPoint,]);const material = new THREE.LineBasicMaterial({color: 0x00ff00,});distanceLine = new THREE.Line(geometry, material);scene.add(distanceLine);// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const text = `距离: ${distance.toFixed(2)}`;const textWidth = context.measureText(text).width;canvas.width = textWidth + 25;canvas.height = 35;context.font = "12px Arial";context.fillStyle = "#ffffff";context.textAlign = "center";context.fillText(text, canvas.width / 2, 15);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });distanceLabel = new THREE.Sprite(spriteMaterial);const midPoint = startPoint.clone().add(endPoint).divideScalar(2);distanceLabel.position.copy(midPoint);distanceLabel.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(distanceLabel);}isDistanceMeasuring = false;
}function calculateArea() {let area = 0;const numPoints = areaPoints.length;for (let i = 0; i < numPoints; i++) {const j = (i + 1) % numPoints;area +=areaPoints[i].x * areaPoints[j].y - areaPoints[j].x * areaPoints[i].y;}area = Math.abs(area) / 2;// 创建标签const canvas = document.createElement("canvas");const context = canvas.getContext("2d");if (context) {const text = `面积: ${area.toFixed(2)}`;const textWidth = context.measureText(text).width;canvas.width = textWidth + 25;canvas.height = 35;context.font = "12px Arial";context.fillStyle = "#ffffff";context.textAlign = "center";context.fillText(text, canvas.width / 2, 15);const texture = new THREE.CanvasTexture(canvas);texture.needsUpdate = true; // 确保纹理更新const spriteMaterial = new THREE.SpriteMaterial({ map: texture });areaLabel = new THREE.Sprite(spriteMaterial);const centroid = new THREE.Vector3();areaPoints.forEach((point) => {centroid.add(point);});centroid.divideScalar(numPoints);areaLabel.position.copy(centroid);areaLabel.scale.set(0.1, 0.1, 1); // 调整标签大小scene.add(areaLabel);}isAreaMeasuring = false;
}function cancelDistanceMeasurement() {isDistanceMeasuring = false;distancePoints = [];distanceDots.forEach((dot) => {scene.remove(dot);});distanceDots = [];if (distanceLine) {scene.remove(distanceLine);distanceLine = null;}if (distanceLabel) {scene.remove(distanceLabel);distanceLabel = null;}
}function cancelAreaMeasurement() {isAreaMeasuring = false;areaPoints = [];areaDots.forEach((dot) => {scene.remove(dot);});areaDots = [];areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];if (areaLabel) {scene.remove(areaLabel);areaLabel = null;}
}// 添加点击圆点
function addDot(point: THREE.Vector3, dots: THREE.Mesh[]) {const geometry = new THREE.SphereGeometry(0.01, 12, 12);const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });const dot = new THREE.Mesh(geometry, material);dot.position.copy(point);scene.add(dot);dots.push(dot);
}function updateAreaLines() {areaLines.forEach((areaLine) => {scene.remove(areaLine);});areaLines = [];const numPoints = areaPoints.length;for (let i = 0; i < numPoints; i++) {const j = (i + 1) % numPoints;const geometry = new THREE.BufferGeometry().setFromPoints([areaPoints[i],areaPoints[j],]);const material = new THREE.LineBasicMaterial({color: 0x00ff00,});const line = new THREE.Line(geometry, material);scene.add(line);areaLines.push(line);}
}onMounted(() => {initThree();
});
</script><style lang="scss" scoped>
.siteInspection {width: 100%;height: 100vh;position: relative;
}.measurement-buttons {position: absolute;bottom: 10px;left: 50%;transform: translateX(-50%);display: flex;gap: 10px;
}
</style>
相关文章:
Vue使用Three.js加载glb (gltf) 文件模型及实现简单的选中高亮、测距、测面积
安装: # three.jsnpm install --save three 附中文网: 5. gltf不同文件形式(.glb) | Three.js中文网 附官网: 安装 – three.js docs 完整代码(简易demo): <template><div class"siteInspe…...
<el-table>右侧有空白列解决办法
问题如图: 解决办法:.box 为本页面最外层的class名,保证各个页面样式不会互相污染。 .box::v-deep .el-table th.gutter {display: none;width: 0}.box ::v-deep.el-table colgroup col[namegutter] {display: none;width: 0;}.box::v-deep …...
Linux网络 网络层
IP 协议 协议头格式 4 位版本号(version): 指定 IP 协议的版本, 对于 IPv4 来说, 就是 4. 4 位头部长度(header length): IP 头部的长度是多少个 32bit, 也就是 4 字节,4bit 表示最大的数字是 15, 因此 IP 头部最大长度是 60 字节. 8 位服务类型(Type Of Service):…...
系统讨论Qt的并发编程——逻辑上下文的分类
目录 前言 首先,讨论Qt里常见的三种上下文 同一线程的串行执行 同一线程的异步执行 多线程的执行 moveToThread办法 前言 笔者最近看了一个具备一定启发性质的Qt教程,在这里,笔者打算整理一下自己的笔记。分享在这里. 首先,…...
《Linux Shell 脚本深度探索:原理与高效编程》
1. 基本结构 Shebang 行 #!/bin/bash # Shebang 行指定了脚本使用的解释器。 /bin/bash 表示使用 Bash 解释器执行脚本。 注释 # 这是注释,不会被执行 2. 变量 定义变量 variable_namevalue # 不需要加 $ 来定义变量。 # 变量名不能包含空格或特殊字符。 访…...
深入剖析:基于红黑树实现自定义 map 和 set 容器
🌟 快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。🌟 在 C 标准模板库(STL)的大家庭里,map和set可是超级重要的关联容器成员呢😎&#x…...
在大数据项目中如何设计和优化数据模型
在大数据项目中,设计和优化数据模型是一个涉及多个步骤和维度的复杂过程。以下是我通常采取的方法: 一、数据模型设计 明确业务需求: 深入了解项目的业务场景和目标,明确数据模型需要解决的具体问题。与业务团队紧密合作…...
JavaScript querySelector()、querySelectorAll() CSS选择器解析(DOM元素选择)
文章目录 基于querySelector系列方法的CSS选择器深度解析一、方法概述二、基础选择器类型1. 类型选择器2. ID选择器3. 类选择器4. 属性选择器 三、组合选择器1. 后代组合器2. 子元素组合器3. 相邻兄弟组合器4. 通用兄弟组合器 四、伪类与伪元素1. 结构伪类2. 状态伪类3. 内容伪…...
Linux系统中处理子进程的终止问题
1. 理解子进程终止的机制 在Unix/Linux系统中,当子进程终止时,会向父进程发送一个SIGCHLD信号。父进程需要捕捉这个信号,并通过调用wait()或waitpid()等函数来回收子进程的资源。这一过程被称为“回收僵尸进程”。 如果父进程没有及时调用w…...
Docker 不再难懂:快速掌握容器命令与架构原理
1. Docker 是容器技术的一种 容器(Container)概述 容器(Container)是一种轻量级的虚拟化技术,它将应用程序及其所有依赖环境打包在一个独立的、可移植的运行时环境中。容器通过操作系统级的虚拟化提供隔离࿰…...
取消票证会把指定的票证从数据库中删除,同时也会把票证和航班 等相关表中的关联关系一起删除。但在删除之前,它会先检查当前用户是否拥有这张票
在做航班智能客服问答系统时会遇到取消票证的场景,这里涉及数据库的操作时会把指定的票证从数据库中删除,同时也会把票证和航班等相关表中的关联关系一起删除。但在删除之前,需要先检查当前用户是否拥有这张票,只有票主才有权限取…...
力扣-贪心-763 划分字母区间
思路 先统计字符串中每一个字母出现的最后下标,然后从end初始化为第一个字母出现的最后下标,在i<end时,不断更新end,因为一旦囊括新的字母就最起码要遍历到新字母出现的最后下标,在i>end时,说明遍历…...
【Redis 原理】网络模型
文章目录 用户空间 && 内核空间阻塞IO非阻塞IO信号驱动IO异步IOIO多路复用selectpollepoll Web服务流程Redis 网络模型Redis单线程网络模型的整个流程Redis多线程网络模型的整个流程 用户空间 && 内核空间 为了避免用户应用导致冲突甚至内核崩溃,用…...
cpp中的继承
一、继承概念 在cpp中,封装、继承、多态是面向对象的三大特性。这里的继承就是允许已经存在的类(也就是基类)的基础上创建新类(派生类或者子类),从而实现代码的复用。 如上图所示,Person是基类&…...
DeepSeek全栈接入指南:从零到生产环境的深度实践
第一章:DeepSeek技术体系全景解析 1.1 认知DeepSeek技术生态 DeepSeek作为新一代人工智能技术平台,构建了覆盖算法开发、模型训练、服务部署的全链路技术栈。其核心能力体现在: 1.1.1 多模态智能引擎 自然语言处理:支持文本生成(NLG)、语义理解(NLU)、情感分析等计算…...
CSS 真的会阻塞文档解析吗?
在网页开发领域,一个常见的疑问是 CSS 是否会阻塞文档解析。理解这一问题对于优化网页性能、提升用户体验至关重要。要深入解答这个问题,需要从浏览器渲染网页的原理说起。 浏览器渲染网页的基本流程 浏览器在接收到 HTML 文档后,会依次进行…...
大模型的UI自动化:Cline 使用Playwright MCP Server完成测试
大模型的UI自动化:Cline 使用Playwright MCP Server完成测试 MCP MCP(Model Context Protocol),是一个开发的协议,标准化了应用程序如何为大模型提供上下文。MCP提供了一个标准的为LLM提供数据、工具的方式,使用MCP会更容易的构建Agent或者是基于LLM的复杂工作流。 最近…...
碰撞检测 | 图解凸多边形分离轴定理(附ROS C++可视化)
目录 0 专栏介绍1 凸多边形碰撞检测2 多边形判凸算法3 分离轴定理(SAT)4 算法仿真与可视化4.1 核心算法4.2 仿真实验 0 专栏介绍 🔥课设、毕设、创新竞赛必备!🔥本专栏涉及更高阶的运动规划算法轨迹优化实战,包括:曲线…...
Python 基本数据类型
目录 1. 字符串(String) 2. 列表(List) 3. 字典(Dictionary) 4. 集合(Set) 5. 数字(Number) 6. 布尔值(Boolean) 1. 字符串&…...
突破“第一崇拜“:五维心理重构之路
一、视频介绍 在这个崇尚"第一"的时代,我们如何找到自己的独特价值?本视频将带您踏上五维心理重构之旅,从诗意人生的角度探讨如何突破"圣人之下皆蝼蚁"的局限。我们将穿越人生的不同阶段,从青春的意气风发到…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
css3笔记 (1) 自用
outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size:0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格ÿ…...
Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
