前言

3D 场景中的面不只有水平面这一个,空间是由无数个面组成的,所以我们有可能会在任意一个面上放置物体,而空间中的面如何确定呢?我们知道,空间中的面可以由一个点和一条法线组成。这个 Demo 左侧为面板,从面板中拖动物体到右侧的 3D 场景中,当然,我鼠标拖动到的位置就是物体放置的点,但是这次我们的重点是如何在斜面上放置模型。

效果图

http://www.hightopo.com/demo/Simple3DEditor/index.html

代码生成

创建场景

  1. dm = new ht.DataModel();//数据模型(http://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html)
  2. g3d = new ht.graph3d.Graph3dView(dm);//3D 场景组件(http://hightopo.com/guide/guide/core/3d/ht-3d-guide.html)
  3. palette = new ht.widget.Palette();//面板组件(http://hightopo.com/guide/guide/plugin/palette/ht-palette-guide.html)
  4. splitView = new ht.widget.SplitView(palette, g3d, 'h', 0.2);//分割组件,第三个参数为分割的方式 h 为左右分,v 为上下分;第四个参数为分割比例,大于 1 的值为绝对宽度,小于 1 则为比例
  5. splitView.addToDOM();//将分割组件添加进 body 体中

关于这些组件的定义可以到对应的链接里面查看,至于将分割组件添加进 body 体中的 addToDOM 函数有必要解释一下(我每次都提,这个真的很重要!)。

HT 的组件一般都会嵌入 BorderPane、SplitView 和 TabView 等容器中使用,而最外层的 HT 组件则需要用户手工将 getView() 返回的底层 div 元素添加到页面的 DOM 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 BorderPane 和 SplitView 等这些HT预定义的容器组件,则 HT 的容器会自动递归调用孩子组件 invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 HT 组件无法获知需要更新,因此最外层的 HT 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。

为了最外层组件加载填充满窗口的方便性,HT 的所有组件都有 addToDOM 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写:

  1. addToDOM = function(){
  2. var self = this,
  3. view = self.getView(),//获取组件的底层 div
  4. style = view.style;
  5. document.body.appendChild(view);//将组件底层div添加进body中
  6. style.left = '0';//ht 默认将所有的组件的position都设置为absolute绝对定位
  7. style.right = '0';
  8. style.top = '0';
  9. style.bottom = '0';
  10. window.addEventListener('resize', function () { self.iv(); }, false);//窗口大小改变事件,调用刷新函数
  11. }

大家可能注意到了,场景中我添加的斜面实际上就是一个 ht.Node 节点,作为与地平面的参照,在这样的对比下立体感会更强一点。下面是这个节点的定义:

  1. node = new ht.Node();
  2. node.s3(1000, 1, 1000);//设置节点的大小
  3. node.r3(0, 0, Math.PI/4);//设置节点旋转 这个旋转的角度是有学问的,跟下面我们要设置的拖拽放置的位置有关系
  4. node.s('3d.movable', false);//设置节点在3d上不可移动 因为这个节点只是一个参照物,建议是不允许移动
  5. dm.add(node);//将节点添加进数据容器中

左侧内容构建

Palette 和 GraphView 类似,由 ht.DataModel 驱动,用 ht.Group 展示分组,ht.Node 展示按钮元素。我将加载 Palette 面板中的图元函数封装为 initPalette,定义如下:

  1. function initPalette() {//加载palette面板组件中的图元
  2. var arrNode = ['displayDevice', 'cabinetRelative', 'deskChair', 'temperature', 'indoors', 'monitor','others'];
  3. var nameArr = ['展示设施', '机柜相关', '桌椅储物', '温度控制', '室内', '视频监控', '其他'];//arrNode中的index与nameArr中的一一对应
  4.  
  5. for (var i = 0; i < arrNode.length; i++) {
  6. var name = nameArr[i];
  7. var vName = arrNode[i];
  8.  
  9. arrNode[i] = new ht.Group();//palette面板是将图元都分在“组”里面,然后向“组”中添加图元即可
  10. palette.dm().add(arrNode[i]);//向palette面板组件中添加group图元
  11. arrNode[i].setExpanded(true);//设置分组为打开的状态
  12. arrNode[i].setName(name);//设置组的名字 显示在分组上
  13.  
  14. var imageArr = [];
  15. switch(i){//根据不同的分组设置每个分组中不同的图元
  16. case 0:
  17. imageArr = ['models/机房/展示设施/大屏.png'];
  18. break;
  19. case 1:
  20. imageArr = ['models/机房/机柜相关/配电箱.png', 'models/机房/机柜相关/室外天线.png', 'models/机房/机柜相关/机柜1.png',
    'models/机房/机柜相关/机柜2.png', 'models/机房/机柜相关/机柜3.png', 'models/机房/机柜相关/机柜4.png',
    'models/机房/机柜相关/电池柜.png'];
  21. break;
  22. case 2:
  23. imageArr = ['models/机房/桌椅储物/储物柜.png', 'models/机房/桌椅储物/桌子.png', 'models/机房/桌椅储物/椅子.png'];
  24. break;
  25. case 3:
  26. imageArr = ['models/机房/温度控制/空调精简.png', 'models/机房/消防设施/消防设备.png'];
  27. break;
  28. case 4:
  29. imageArr = ['models/室内/办公桌简易.png', 'models/室内/书.png', 'models/室内/办公桌镜像.png', 'models/室内/办公椅.png'];
  30. break;
  31. case 5:
  32. imageArr = ['models/机房/视频监控/摄像头方.png', 'models/机房/视频监控/对讲维护摄像头.png', 'models/机房/视频监控/微型摄像头.png'];
  33. break;
  34. default:
  35. imageArr = ['models/其他/信号塔.png'];
  36. break;
  37. }
  38. setPalNode(imageArr, arrNode[i]);//创建palette上节点及设置名称、显示图片、父子关系
  39. }
  40. }

我在 setPalNode 函数中做了一些名称的设置,主要是想要根据上面 initPalette 函数中我传入的路径名称来设置模型的名称以及在不同文件在不同的文件夹下的路径:

  1. function setPalNode(imageArr, arr) {
  2. for (var j = 0; j < imageArr.length; j++) {
  3. var imageName = imageArr[j];
  4. var jsonUrl = imageName.slice(0, imageName.lastIndexOf('.')) + '.json';//shape3d中的 json 路径
  5. var name = imageName.slice(imageName.lastIndexOf('/')+1, imageName.lastIndexOf('.')); //取最后一个/和.之间的字符串用来设置节点名称
  6. var url = imageName.slice(imageName.indexOf('/')+1, imageName.lastIndexOf('.'));//取第一个/和最后一个.之间的字符串用来设置拖拽生成模型obj文件的路径
  7.  
  8. createNode(name, imageName, arr, url, jsonUrl);//创建节点,这个节点是显示在palette面板上
  9. }
  10. }

createNode 创建节点的函数比较简单:

  1. function createNode(name, image, parent, urlName, jsonUrl) {//创建palette面板组件上的节点
  2. var node = new ht.Node();
  3. palette.dm().add(node);
  4. node.setName(name);//设置节点名称 palette面板上显示的文字也是通过这个属性设置名称
  5. node.setImage(image);//设置节点的图片
  6. node.setParent(parent);//设置父亲节点
  7. node.s({
  8. 'draggable': true,//设置节点可拖拽
  9. 'image.stretch': 'centerUniform',//设置节点图片的绘制方式
  10. 'label': ''//设置节点的label为空,这样即使设置了name也不会显示在3d中的模型下方
  11. });
  12. node.a('urlName', urlName);//a设置用户自定义属性
  13. node.a('jsonUrl', jsonUrl);
  14. return node;
  15. }

虽然简单,但是还是要提一下,draggable: true 为设置节点可拖拽,否则节点不可拖拽;还有 node.s 是 HT 默认封装好的样式设置方法,如果用户需要自己添加方法,则可通过 node.a 方法来添加,参数一为用户自定义名称,参数二为用户自定义值,不仅能传常量,也能传变量、对象,还能传函数!又是一个非常强大的功能。

拖拽功能

拖拽基本上就是响应 windows 自带的 dragover 以及 drop 事件,要在放开鼠标的时候创建模型,就要在事件触发时生成模型:

  1. function dragAndDrop() {//拖拽功能
  2. g3d.getView().addEventListener("dragover", function(e) {//拖拽事件
  3. e.dataTransfer.dropEffect = "copy";
  4. handleOver(e);
  5. });
  6. g3d.getView().addEventListener("drop", function(e) {//放开鼠标事件
  7. handleDrop(e);
  8. });
  9. }
  10.  
  11. function handleOver(e) {
  12. e.preventDefault();//取消事件的默认动作。
  13. }
  14.  
  15. function handleDrop(e) {//鼠标放开时
  16. e.preventDefault();//取消事件的默认动作。
  17.  
  18. var paletteNode = palette.dm().sm().ld();//获取palette面板中最后选中的节点
  19. if (paletteNode) {
  20. loadObjFunc('assets/objs/' + paletteNode.a('urlName') + '.obj', 'assets/objs/' + paletteNode.a('urlName') + '.mtl',
    paletteNode.a('jsonUrl'), g3d.getHitPosition(e, [0, 0, 0], [-1, 1, 0]));//加载obj模型
  21. }
  22. }

这里完全有必要说明一下,这个 Demo 的重点来了! loadObjFunc 函数中的最后一个参数为生成模型的 position3d 坐标,g3d.getHitPosition 这个方法总共有三个参数,第一个参数为事件类型,第二和第三个参数如果不设置,则默认为水平面的中心点也就是 [0, 0, 0] 以及法线为 y 轴,也就是 [0, 1, 0],一条法线和一个点就可以确定一个面,所以我们通过这个方法来设置这个节点所要放置的平面是在哪一个面上,我前面将 node 节点设置为绕 z 轴旋转 45° 角,所以这边的法线也就要好好想想如何设置了,这是数学上的问题,要自己思考了。

加载模型

HT 通过 ht.Default.loadObj 函数来加载模型,但是前提是要有一个节点,然后再在这个节点上加载模型:

  1. function loadObjFunc(objUrl, mtlUrl, jsonUrl, p3) {//加载obj模型
  2. var node = new ht.Node();
  3. var shape3d = jsonUrl.slice(jsonUrl.lastIndexOf('/')+1, jsonUrl.lastIndexOf('.'));
  4.  
  5. ht.Default.loadObj(objUrl, mtlUrl, {//HT 通过 loadObj 函数来加载 obj 模型
  6. cube: true,//是否将模型缩放到单位1的尺寸范围内,默认为false
  7. center: true,//模型是否居中,默认为false,设置为true则会移动模型位置使其内容居中
  8. shape3d: shape3d,//如果指定了shape3d名称,则HT将自动将加载解析后的所有材质模型构建成数组的方式,以该名称进行注册
  9. finishFunc: function(modelMap, array, rawS3) {//用于加载后的回调处理
  10. if (modelMap) {
  11. node.s({//设置节点样式
  12. 'shape3d': jsonUrl,//jsonUrl 为 obj 模型的 json 文件路径
  13. 'label': ''//设置label为空,label的优先级高于name,所以即使设置了name,节点的下方也不会显示name名称
  14. });
  15. g3d.dm().add(node);//将节点添加进数据容器中
  16.  
  17. node.s3(rawS3);//设置节点大小 rawS3 模型的原始尺寸
  18. node.p3(p3);//设置节点的三维坐标
  19. node.setName(shape3d);//设置节点名称
  20. node.setElevation(node.s3()[1]/2);//控制Node图元中心位置所在3D坐标系的y轴位置
  21. g3d.sm().ss(node);//设置选中当前节点
  22. g3d.setFocus(node);//将焦点设置在当前节点上
  23. return node;
  24. }
  25. }
  26. });
  27. }

代码结束!

总结

说实在的这个 Demo 真的是非常容易,难度可能在于空间思维能力了,先确认法线和点,然后根据法线和点找到那个面,这个面按照我的这种方式有个对照还比较能够理解,真幻想的话,可能容易串。这个 Demo 容易主要还是因为封装的 hitPosition 函数简单好用,这个真的是功不可没。

快速开发 HTML5 WebGL 的 3D 斜面拖拽生成模型的更多相关文章

  1. 基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用

    基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用 前言 在目前大数据时代背景之下,数据可视化的需求也变得越来越庞大,在数据可视化的背景之下,通过智能机器间的链接并最终将人机链接 ...

  2. iOS开发拓展篇—xib中关于拖拽手势的潜在错误

    iOS开发拓展篇—xib中关于拖拽手势的潜在错误 一.错误说明 自定义一个用来封装工具条的类 搭建xib,并添加一个拖拽的手势. 主控制器的代码:加载工具条 封装工具条以及手势拖拽的监听事件 此时运行 ...

  3. wordpress用Elementor拖拽生成酷炫页面

    很多朋友看到wordpress网站做得很高大上,想知道是怎么做到的,其实很简单,用Elementor就能拖拽生成酷炫页面,ytkah就直接上干货了. 1.安装Elementor,到wordpress后 ...

  4. 基于 HTML5 WebGL 的 3D 网络拓扑图

    在数据量很大的2D 场景下,要找到具体的模型比较困难,并且只能显示出模型的的某一部分,显示也不够直观,这种时候能快速搭建出 3D 场景就有很大需求了.但是搭建 3D 应用场景又依赖于通过 3ds Ma ...

  5. 基于 HTML5 WebGL 的 3D SCADA 主站系统

    这个例子的初衷是模拟服务器与客户端的通信,我把整个需求简化变成了今天的这个例子.3D 的模拟一般需要鹰眼来辅助的,这样找产品以及整个空间的概括会比较明确,在这个例子中我也加了,这篇文章就算是我对这次项 ...

  6. 基于 HTML5 WebGL 的 3D 科幻风机

    前言 许多世纪以来,风力机同水力机械一样,作为动力源替代人力.畜力,对生产力的发展发挥过重要作用.近代机电动力的广泛应用以及二十世纪50年代中东油田的发现,使风机发电机的发展缓慢下来. 70年代初期, ...

  7. 基于 HTML5 WebGL 的 3D 仓储管理系统

    仓储管理系统(WMS)是一个实时的计算机软件系统,它能够按照运作的业务规则和运算法则,对信息.资源.行为.存货和分销运作进行更完美地管理,使其最大化满足有效产出和精确性的要求.从财务软件.进销存软件C ...

  8. 基于 HTML5 + WebGL 的 3D 可视化挖掘机

    前言 在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求.如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因 ...

  9. 基于 HTML5 + WebGL 实现 3D 可视化地铁系统

    前言 工业互联网,物联网,可视化等名词在我们现在信息化的大背景下已经是耳熟能详,日常生活的交通,出行,吃穿等可能都可以用信息化的方式来为我们表达,在传统的可视化监控领域,一般都是基于 Web SCAD ...

随机推荐

  1. Zabbix Agent active主动模式监控

    zabbix_server端当主机数量过多的时候,由Server端去收集数据,Zabbix会出现严重的性能问题,主要表现如下: 1.当被监控端到达一个量级的时候,Web操作很卡,容易出现502 2.图 ...

  2. dedecms在任意页面调用任意栏目文章

    dedecms在任意页面调用任意栏目文章,我们用arclist标签即可实现.如果是调用多个栏目文章可以给typeid多个值. 我们以调用ID为1和ID为30的两个栏目下5篇文章为例: {dede:ar ...

  3. TensorFlow实战之实现AlexNet经典卷积神经网络

    本文根据最近学习TensorFlow书籍网络文章的情况,特将一些学习心得做了总结,详情如下.如有不当之处,请各位大拿多多指点,在此谢过. 一.AlexNet模型及其基本原理阐述 1.关于AlexNet ...

  4. 危化品速查APP--Android Project

    开发环境 Android studio 2.3.1 功能描述 集成多种查询方式,查看本地数据库中危险化学品的信息: 按照中文拼音和英文首字母,对化学品进行查询: 按照UN号或者CAS号查询相应的化学品 ...

  5. hihoCoder 1033 : 交错和 数位dp

    思路:数位dp,dp(i, j, k)表示考虑i位数,每位数可以任意取[0~9],并且这i位数的交错和为j,k=1表示前缀全是0(如000456),k=0表示前缀不为0.注意,前缀是否为0是这道题的一 ...

  6. Luogu P1522 牛的旅行 Cow Tours

    题目描述 农民 John的农场里有很多牧区.有的路径连接一些特定的牧区.一片所有连通的牧区称为一个牧场.但是就目前而言,你能看到至少有两个牧区通过任何路径都不连通.这样,Farmer John就有多个 ...

  7. javascript form表单常用的正则表达式

    form验证时常用的几个正则表达式 座机: \d{3,4}-\d{7,8} 手机号: /^1[34578][0-9]{9}$/ (\86)?\s+1[34578]\d{0-9} (\+86)?\s*1 ...

  8. PCI和PCIE插槽有什么区别?

    PCI是Peripheral Component Interconnect(外设部件互连标准)的缩写,它是目前个人电脑中使用最为广泛的接口,几乎所有的主板产品上都带有这种插槽.PCI插槽也是主板带有最 ...

  9. mxnet:结合R与GPU加速深度学习

    转载于统计之都,http://cos.name/tag/dmlc/,作者陈天奇 ------------------------------------------------------------ ...

  10. Excel 2010高级应用-圆环图(七)

    Excel 2010高级应用-圆环图(七) 基本操作如下: 1.新建空白Excel文档,并命名为圆环图 2.单击"插入",并找到圆环图图样 3.单击圆环图图样,并在空白文档上生成图 ...