vue vite+three在线编辑模型导入导出

文章目录

  • 一、1.0.0版本
    • 1.新增
    • 2.编辑
    • 3.导出
    • 4.导入
  • 二、2.0.0版本
    • 1. 修复模型垂直方向放置时 模型会重合
    • 4. 修复了导出导入功能 现在是1:1导出导入
    • 5. 新增一个地面 视角看不到地下 设置了禁止编辑地面 地面设置为圆形
    • 6. 新增功能 可选择基本圆形 方形 圆柱形等模型以及可放置自己的模型文件
    • 7. 优化面板样式
  • 总结

要实现一个类似于数字孪生的场景 可以在线、新增、删除模型 、以及编辑模型的颜色、长宽高
然后还要实现 编辑完后 保存为json数据 记录模型数据 既可以导入也可以导出

一、1.0.0版本

1.新增

先拿建议的立方体来代替模型
点击新增按钮就新增一个立方体
在这里插入图片描述

2.编辑

点击编辑按钮可以修改坐标 长宽高 颜色等等信息
在这里插入图片描述

3.导出

点击导出按钮 可以导出为json数据格式
在这里插入图片描述

在这里插入图片描述

4.导入

选择导入刚才的json文件
在这里插入图片描述
有一个bug 就是导入后颜色丢失了 点击模型 信息面板的颜色显示正常 渲染颜色丢失
在这里插入图片描述


源码

<template>
  <div id="app" @click="onAppClick">
    <div id="info">
      <button @click.stop="addBuilding">新增</button>
      <button @click.stop="showEditor">编辑</button>
      <button @click.stop="exportModelData">导出</button>
      <input type="file" @change="importModelData" ref="fileInput" />
    </div>
    <div id="editor" v-if="editorVisible" @click.stop>
      <h3>Edit Building</h3>
      <label for="color">Color:</label>
      <input type="color" id="color" v-model="selectedObjectProps.color" /><br />
      <label for="posX">Position X:</label>
      <input
        type="number"
        id="posX"
        v-model="selectedObjectProps.posX"
        step="0.1"
      /><br />
      <label for="posY">Position Y:</label>
      <input
        type="number"
        id="posY"
        v-model="selectedObjectProps.posY"
        step="0.1"
      /><br />
      <label for="posZ">Position Z:</label>
      <input
        type="number"
        id="posZ"
        v-model="selectedObjectProps.posZ"
        step="0.1"
      /><br />
      <label for="scaleX">Scale X:</label>
      <input
        type="number"
        id="scaleX"
        v-model="selectedObjectProps.scaleX"
        step="0.1"
      /><br />
      <label for="scaleY">Scale Y:</label>
      <input
        type="number"
        id="scaleY"
        v-model="selectedObjectProps.scaleY"
        step="0.1"
      /><br />
      <label for="scaleZ">Scale Z:</label>
      <input
        type="number"
        id="scaleZ"
        v-model="selectedObjectProps.scaleZ"
        step="0.1"
      /><br />
      <label for="rotX">Rotation X:</label>
      <input
        type="number"
        id="rotX"
        v-model="selectedObjectProps.rotX"
        step="0.1"
      /><br />
      <label for="rotY">Rotation Y:</label>
      <input
        type="number"
        id="rotY"
        v-model="selectedObjectProps.rotY"
        step="0.1"
      /><br />
      <label for="rotZ">Rotation Z:</label>
      <input
        type="number"
        id="rotZ"
        v-model="selectedObjectProps.rotZ"
        step="0.1"
      /><br />
      <button @click="applyEdit">保存</button>
      <button @click="deleteBuilding">删除</button>
    </div>
    <div ref="canvasContainer" style="width: 100vw; height: 100vh"></div>
  </div>
</template>

<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

export default {
  data() {
    return {
      editorVisible: false,
      selectedObject: null,
      selectedObjectProps: {
        color: "#00ff00",
        posX: 0,
        posY: 0,
        posZ: 0,
        scaleX: 1,
        scaleY: 1,
        scaleZ: 1,
        rotX: 0,
        rotY: 0,
        rotZ: 0,
      },
      raycaster: null,
    };
  },
  mounted() {
    this.init();
    this.animate();
    window.addEventListener("resize", this.onWindowResize, false);
    this.loadModelData(); // Load saved model data on page load
  },
  methods: {
    init() {
      console.log("Initializing Three.js");

      this.scene = new THREE.Scene();
      this.scene.background = new THREE.Color(0xcccccc);
      this.camera = new THREE.PerspectiveCamera(
        60,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      this.camera.position.set(0, 10, 20);
      this.renderer = new THREE.WebGLRenderer({ antialias: true });
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.$refs.canvasContainer.appendChild(this.renderer.domElement);
      this.controls = new OrbitControls(this.camera, this.renderer.domElement);

      const light = new THREE.DirectionalLight(0xffffff, 1);
      light.position.set(5, 10, 7.5);
      this.scene.add(light);

      this.raycaster = new THREE.Raycaster();

      const geometry = new THREE.BoxGeometry(1, 1, 1);
      const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
      this.cube = new THREE.Mesh(geometry, material);
      this.scene.add(this.cube);
    },
    onWindowResize() {
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
    },
    onAppClick(event) {
      const mouse = new THREE.Vector2();
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
      this.raycaster.setFromCamera(mouse, this.camera);
      const intersects = this.raycaster.intersectObjects(this.scene.children, true);
      if (intersects.length > 0) {
        this.selectedObject = intersects[0].object;
        console.log("Object selected:", this.selectedObject);
        this.showEditor();
      }
    },
    addBuilding() {
      const geometry = new THREE.BoxGeometry(1, 1, 1);
      const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
      const building = new THREE.Mesh(geometry, material);
      building.position.set(Math.random() * 10 - 5, 0.5, Math.random() * 10 - 5);
      this.scene.add(building);
    },
    showEditor() {
      if (this.selectedObject) {
        this.editorVisible = true;
        this.updateEditor(this.selectedObject);
      }
    },
    updateEditor(object) {
      this.selectedObjectProps.color = `#${object.material.color.getHexString()}`;
      this.selectedObjectProps.posX = object.position.x;
      this.selectedObjectProps.posY = object.position.y;
      this.selectedObjectProps.posZ = object.position.z;
      this.selectedObjectProps.scaleX = object.scale.x;
      this.selectedObjectProps.scaleY = object.scale.y;
      this.selectedObjectProps.scaleZ = object.scale.z;
      this.selectedObjectProps.rotX = object.rotation.x;
      this.selectedObjectProps.rotY = object.rotation.y;
      this.selectedObjectProps.rotZ = object.rotation.z;
    },
    applyEdit() {
      if (this.selectedObject) {
        const color = this.selectedObjectProps.color;
        this.selectedObject.material.color.set(color);
        this.selectedObject.position.set(
          parseFloat(this.selectedObjectProps.posX),
          parseFloat(this.selectedObjectProps.posY),
          parseFloat(this.selectedObjectProps.posZ)
        );
        this.selectedObject.scale.set(
          parseFloat(this.selectedObjectProps.scaleX),
          parseFloat(this.selectedObjectProps.scaleY),
          parseFloat(this.selectedObjectProps.scaleZ)
        );
        this.selectedObject.rotation.set(
          parseFloat(this.selectedObjectProps.rotX),
          parseFloat(this.selectedObjectProps.rotY),
          parseFloat(this.selectedObjectProps.rotZ)
        );
      }
    },
    deleteBuilding() {
      if (this.selectedObject) {
        this.scene.remove(this.selectedObject);
        this.selectedObject = null;
        this.editorVisible = false;
      }
    },
    animate() {
      requestAnimationFrame(this.animate);
      this.renderer.render(this.scene, this.camera);
      this.controls.update();
    },
    exportModelData() {
      const modelData = {
        objects: this.scene.children
          .filter((obj) => obj instanceof THREE.Mesh) // 过滤出是 Mesh 对象的物体
          .map((obj) => ({
            position: obj.position.toArray(),
            scale: obj.scale.toArray(),
            rotation: obj.rotation.toArray(),
            color: `#${obj.material.color.getHexString()}`,
          })),
      };
      const jsonData = JSON.stringify(modelData);
      const blob = new Blob([jsonData], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.style.display = "none";
      a.href = url;
      a.download = "model_data.json";
      document.body.appendChild(a);
      a.click();
      URL.revokeObjectURL(url);
      document.body.removeChild(a);
    },
    importModelData(event) {
      const file = event.target.files[0];
      if (file) {
        const reader = new FileReader();
        reader.onload = () => {
          try {
            const data = JSON.parse(reader.result);
            console.log("Imported data:", data); // 输出导入的完整数据,确保格式和内容正确

            this.clearScene();
            data.objects.forEach((objData, index) => {
              const geometry = new THREE.BoxGeometry();

              // 设置默认颜色为红色
              const color = new THREE.Color(0xff0000); // 红色

              // 如果数据中有颜色字段并且是合法的颜色值,则使用数据中的颜色
              if (objData.color && typeof objData.color === "string") {
                try {
                  color.set(objData.color);
                } catch (error) {
                  console.error(`Error parsing color for object ${index}:`, error);
                }
              } else {
                console.warn(`Invalid color value for object ${index}:`, objData.color);
              }

              const material = new THREE.MeshStandardMaterial({
                color: color,
                metalness: 0.5, // 示例中的金属度设置为0.5,可以根据需求调整
                roughness: 0.8, // 示例中的粗糙度设置为0.8,可以根据需求调整
              });
              const object = new THREE.Mesh(geometry, material);
              object.position.fromArray(objData.position);
              object.scale.fromArray(objData.scale);
              object.rotation.fromArray(objData.rotation);
              this.scene.add(object);
            });
          } catch (error) {
            console.error("Error importing model data:", error);
          }
        };
        reader.readAsText(file);
      }
    },
    clearScene() {
      while (this.scene.children.length > 0) {
        this.scene.remove(this.scene.children[0]);
      }
    },
    saveModelData() {
      const modelData = {
        objects: this.scene.children.map((obj) => ({
          position: obj.position.toArray(),
          scale: obj.scale.toArray(),
          rotation: obj.rotation.toArray(),
          color: `#${obj.material.color.getHexString()}`,
        })),
      };
      localStorage.setItem("modelData", JSON.stringify(modelData));
    },
    loadModelData() {
      const savedData = localStorage.getItem("modelData");
      if (savedData) {
        try {
          const data = JSON.parse(savedData);
          this.clearScene();
          data.objects.forEach((objData) => {
            const geometry = new THREE.BoxGeometry();
            const material = new THREE.MeshStandardMaterial({
              color: parseInt(objData.color.replace("#", "0x"), 16),
            });
            const object = new THREE.Mesh(geometry, material);
            object.position.fromArray(objData.position);
            object.scale.fromArray(objData.scale);
            object.rotation.fromArray(objData.rotation);
            this.scene.add(object);
          });
        } catch (error) {
          console.error("Error loading model data from localStorage:", error);
        }
      }
    },
  },
};
</script>

<style>
body {
  margin: 0;
  overflow: hidden;
}

canvas {
  display: block;
}

#info {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(255, 255, 255, 0.8);
  padding: 10px;
}

#editor {
  position: absolute;
  top: 100px;
  left: 10px;
  background: rgba(255, 255, 255, 0.8);
  padding: 10px;
}
</style>

二、2.0.0版本

在这里插入图片描述

1. 修复模型垂直方向放置时 模型会重合

4. 修复了导出导入功能 现在是1:1导出导入

5. 新增一个地面 视角看不到地下 设置了禁止编辑地面 地面设置为圆形

6. 新增功能 可选择基本圆形 方形 圆柱形等模型以及可放置自己的模型文件

7. 优化面板样式

总结

未完待续

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/792333.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

判断非radio\checkbox 勾选框是否被勾选

1、通常如果是标准的勾选框我们可以使用使用isSelected()方法无法判断其勾选状态&#xff0c;如下代码&#xff1a; Boolean bldriver.findElement(By.xpath("//*[contains(class,el-icon-success)]")).isSelected(); 2、如图所示&#xff0c;该勾选框并不是一个…

51单片机STC89C52RC——16.1 五线四相步进电机

目录 目的/效果 一&#xff0c;STC单片机模块 二&#xff0c;步进电机 2.2 什么是步进电机&#xff1f; 2.2.1 步进电机驱动板 静态参数 动态参数 2.2.2 五线四相 单相激励步进 双相激励步进 混合激励驱动 2.3 细分驱动 2.4 通过数字信号控制旋转位置和转速。 2…

深入理解计算机系统 CSAPP 练习题9.9

这个函数和练习题9.8的find_fit函数相关,asize是我们实际需要的大小,但是find_fit函数返回的bp有可能是比我们需要的还大的块,此时我们需要对bp进行分割.

kind kubernetes(k8s虚拟环境)使用本地docker的镜像

kubernetes中&#xff0c;虽然下载镜像使用docker&#xff0c;但是存储在docker image里的镜像是不能被k8s直接使用的&#xff0c;但是kind不同&#xff0c;可以使用下面的方法&#xff0c;让kind kubernetes环境使用docker image里的镜像。 kind – Quick Start 例如&#x…

AGI 之 【Hugging Face】 的【文本摘要】的 [评估PEGASUS ] / [ 微调PEGASUS ] / [生成对话摘要] 的简单整理

AGI 之 【Hugging Face】 的【文本摘要】的 [评估PEGASUS ] / [ 微调PEGASUS ] / [生成对话摘要] 的简单整理 目录 AGI 之 【Hugging Face】 的【文本摘要】的 [评估PEGASUS ] / [ 微调PEGASUS ] / [生成对话摘要] 的简单整理 一、简单介绍 二、文本摘要 三、在CNN/Daily…

【python算法学习1】用递归和循环分别写下 fibonacci 斐波拉契数列,比较差异

问题&#xff1a; fibonacci 斐波拉契数列&#xff0c;用递归和循环的方法分别写,比较递归和循环的思路和写法的差别 最直接的思路&#xff0c;是写递归方法 循环方法的稍微有点绕&#xff0c;我觉得问题主要是出在&#xff0c;总结循环的通项公式更麻烦&#xff0c;难在数学…

【YOLOv8】 用YOLOv8实现数字式工业仪表智能读数(三)

上一篇圆形表盘指针式仪表的项目受到很多人的关注&#xff0c;咱们一鼓作气&#xff0c;把数字式工业仪表的智能读数也研究一下。本篇主要讲如何用YOLOV8实现数字式工业仪表的自动读数&#xff0c;并将读数结果进行输出&#xff0c;若需要完整数据集和源代码可以私信。 目录 &a…

2008年下半年软件设计师【下午题】真题及答案

文章目录 2008年下半年软件设计师下午题--真题2008年下半年软件设计师下午题--答案 2008年下半年软件设计师下午题–真题 2008年下半年软件设计师下午题–答案

OV证书适合什么样的网站?

随着互联网的发展&#xff0c;网站安全问题备受关注。为了保护用户数据和建立信任关系&#xff0c;网站拥有一个安全可靠的SSL证书至关重要。而OV证书作为一种高级SSL证书&#xff0c;适合于要求更高安全性和可信度的网站使用。那么&#xff0c;OV证书适合什么样的网站呢&#…

融合CDN是什么?为什么需要融合CDN?其应用方法与原理是什么?

你了解融合CDN是什么吗&#xff1f;为什么需要融合CDN&#xff1f;你可能有听过融合CDN&#xff0c;但你知道它的应用方法与原理吗&#xff1f;本文将带你一次了解什么是融合CDN&#xff0c;详细介绍融合CDN的应用方法与运用原理&#xff0c;立刻替您解开心中疑惑&#xff01; …

便携式气象参数检测仪:智能气象监测

随着科技的飞速发展&#xff0c;气象监测已不再是传统意义上的固定站点观测&#xff0c;而是逐渐向智能化、便携化、高精度化方向演进。在这一背景下&#xff0c;便携式气象参数检测仪应运而生&#xff0c;以其轻便、高效、多功能的特性&#xff0c;成为气象监测领域的得力助手…

Linux C语言基础 day8

目录 思维导图&#xff1a; 学习目标&#xff1a; 学习内容&#xff1a; 1. 字符数组 1.1 二维字符数组 1.1.1 格式 1.1.2 初始化 1.1.3 二维字符数组输入输出、求最值、排序 2. 函数 2.1 概念 关于函数的相关概念 2.2 函数的定义及调用 2.2.1 定义函数的格式 2.3…

Android高级——Logger日志系统

Logger日志系统 Logger日志系统是基于内核中的Logger日志驱动程序实现将日志记录保存在内核空间中使用一个环形缓冲区来保存日志&#xff0c;满了之后&#xff0c;新的日志就会覆盖旧的日志 日志类型 main&#xff0c;记录应用程序级别system&#xff0c;记录系统级别radio&…

Vue3打包发布,刷新出现的空白页面和错误

Vue3打包发布出现的错误&#xff1a;Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of text/html. Strict MIME type checking is enforced for module scripts per HTML spec. 第一次点击访问到这个路径&…

北斗GPS天线使用技巧与性能对比

北斗GPS天线使用中注意的问题 多系统兼容性&#xff1a;确保天线不仅能接收北斗信号&#xff0c;还能同时接收其他GNSS系统&#xff08;如GPS、GLONASS、Galileo&#xff09;的信号&#xff0c;以提高定位精度和可靠性。 信号频率选择&#xff1a;根据应用需求选择合适的信号…

MFC Ribbon菜单 - 中英文实时切换方法

简介 最近在搞一个老外的项目&#xff0c;本来谈的好好的&#xff0c;纯英文界面。项目接近尾声了&#xff0c;又提出了中英文实时切换的新需求&#xff0c;没办法就只能想办法&#xff0c;毕竟客户最大嘛。 实现方法 还好本来的ribbon英文菜单不复杂&#xff0c;就用纯C编码…

谷歌插件之一键关闭同域名页面

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 谷歌插件之一键关闭同域名页面 前言项目结构mainfest.jsonbackgroud.js 项目实现效果展示展望 前…

【手把手教你使用cgroup配置,十分钟就会】

手把手教你使用cgroup配置&#xff0c;十分钟就会 什么是cgroupcgroup中的参数概念及原理 以 memory为例看下如何配置配置内存限制写一个内存申请脚本执行脚本测试结束语 什么是cgroup cgroups 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制&#xff0c…

论文降痕降重全攻略:从技巧到工具,助你轻松应对学术挑战

AIGC降重工具&#xff1a;快速降低论文查重率 高查重率是许多毕业生的困扰。通常&#xff0c;高查重率源于过度引用未经修改的参考资料和格式错误。传统的降重方法&#xff0c;如修改文本和增添原创内容&#xff0c;虽必要但耗时且成效不一。 鉴于此&#xff0c;应用AI工具进…

未来互联网的新篇章:深度解析Web3技术

随着技术的不断演进&#xff0c;Web3正逐渐成为引领未来互联网发展的关键驱动力。本文将深入探讨Web3技术的核心概念、关键特征以及其对未来互联网生态的深远影响&#xff0c;旨在帮助读者全面理解和把握这一新兴技术的发展方向和潜力。 1. Web3的基本概念和演进 Web3并非简单…