ThingJS学习笔记
1.现在模摸搭上建模这个需要专业人员操作
切记需要操作的物品或者建筑一定要设置属性,要不然无法操作
2,建模完毕后要对模型进行2次开发需要到ThingJS控制台上操作
3.进入thisgjs控制台
1.控制台会自动生成代码
//加载场景代码 var app = new THING.App({ // 场景地址"url": "/api/scene/08c75551cf95cdeec73e09da",//背景设置"skyBox" : "BlueSky" });
2,加载完成事件
// 加载完成事件 app.on('load', function (ev) {/* 参数:ev.campus 园区,类型:Campusev.buildings 园区建筑物,类型:Selector*/ var campus=ev.campus;console.log('after load '+campus.id); // 切换层级到园区app.level.change(campus);});
通过 CamBuilder 可搭建并输出一个园区,该园区可在 ThingJS 场景中加载
ThingJS 场景中可以加载园区,加载后系统自动创建了园区、建筑、楼层、房间等物体对象,这些对象也自然把场景分成了不同的层级
场景和园区
当我们使用 App 启动了 ThingJS,ThingJS 就会创建一个三维空间,整个三维空间我们称之为“场景”(scene),在场景内我们可以创建对象,比如园区,建筑,车辆,传感器等等。
通过 CamBuilder 可编辑并输出一个园区,该园区可在 ThingJS 场景中加载。创建 App 时,我们传入的 url,就是被创建园区的地址。
CamBuilder 对象和ThingJS对象
在 CamBuilder 中创建的物体,只有在编辑了 UserID、Name 或者 自定义属性 后,导入到 ThingJS 中才能成为独立的管理对象,被程序读取或修改。并且 CamBuilder 中 UserID 和 Name 与 ThingJS 中的对象有对应关系。
在场景里,是可以添加多个独立园区的,每一个园区是一个 THING.Campus 类的对象,我们通过“app.create”接口来实现。
var app = new THING.App(); var campus1 = app.create({type: "Campus",url: "models/storehouse",complete: function (ev) {console.log("Campus created: " + ev.object.id);} }); var campus2 = app.create({type: "Campus",url: "models/chinesehouse",position: [50, 0, 0],complete: function (ev) {console.log("Campus created: " + ev.object.id);} });
场景和层级
ThingJS 场景中加载了园区后,场景中自动创建了 campus,building,floor,room 和一些在 CamBuilder 中添加的物体对象。这些对象不是独立散落在场景中的,他们会相互关联,形成一棵树的结构,从而构建了场景的层级。
ThingJS 提供了两套层级体系:父子树、分类对象属性树。
如您所见,场景会有一个根物体,可通过 app.root 访问到,所有对象都是他的子子孙孙。
创建一个物体对象时,可指定该对象的父物体。
一个物体对象也可以通过 add ,添加子物体。
在 ThingJS 场景中,每个对象,都可以通过 children 访问到下层子对象物体,通过 parent 访问到对应的父物体。
/*** 说明:通过 “父子树” 访问场景内的对象* 操作:无,查看log信息* 教程:ThingJS 教程——>园区与层级——>场景层级* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 加载场景后执行 app.on('load', function (ev) {// 获取园区对象var campus = ev.campus;// 通过场景的 父子树 访问对象var children = campus.children;for (var i = 0; i < children.length; i++) {var child = children[i];var id = child.id;var name = child.name;var type = child.type;console.log('id: ' + id + ' name: ' + name + ' type: ' + type);}// id 107 为白色厂区建筑, // parent: app.query('107')[0] 为在厂区内创建物体// 厂区内创建的物体,只有在进入厂区后才会能显示,点击厂区进入,则看到绿色小车// 当推出厂区后,绿色小车则隐藏var obj = app.create({type: 'Thing',id: 'No1234567',name: 'truck',parent: app.query('107')[0],url: 'https://model.3dmomoda.com/models/8CF6171F7EE046968B16E10181E8D941/0/gltf/', // 模型地址 position: [0, 0, 0], // 世界坐标系下的位置complete: function (ev) {//物体创建成功以后执行函数console.log('thing created: ' + ev.object.id);}});var campus = ev.campus;console.log('after load ' + campus.id);// 切换层级到园区app.level.change(campus); });
每个对象可以有多个孩子,为了方便分类查找物体,ThingJS 又针对每类对象提供了一些内置属性。
campus 提供了三个分类内置属性:
- buildings:可以访问到该园区下所有的建筑对象。
- ground:可以访问到园区的地面对象。
- things:其他所有 Thing 类型的物体。
如果属性的英文拼写是复数,说明该属性管理了多个物体对象,使用的是 Selector 数据结构。
如果是单数,说明管理的只能是一个物体对象,属性返回就是该对象本身。
层级切换
场景提供了层级结构,我们可以通过 “父子树” 和 “分类对象属性树” 来批量控制子物体,比如移动、显示或者透明控制等。
借用此能力,系统在园区加载完成后仅显示建筑外立面、隐藏楼层;当双击进入建筑时,再把该建筑的所有楼层都显示出来,以提高场景显示的性能。
我们把从园区进入到建筑内,定义为一次 “层级切换” 。
为了方便 “层级切换” 操作, ThingJS 提供了 SceneLevel 模块,通过
app.level
可以访问到。
提供如下接口,方便控制当前物体层级:
- app.level.change(object):将场景设置到指定物体的层级
- app.level.back():返回当前层级的父物体层级
系统启动后,只要调用了一次 app.level.change(无论是将层级切换到了园区还是切换到了某个Thing),ThingJS 就启动了内置的 园区<—>建筑<—>楼层<—>物体…… 的逐级进入和退出的交互操作流程和对应的响应。
ThingJS 中设定左键双击可进入到所拾取的物体层级,右键单击可返回到上一层级。
当进入层级时会触发 EnterLevel 事件。
当退出层级时会触发 LeaveLevel 事件。
/*** 说明:以建筑(Building)层级为例,说明进出层级事件 及其 方向性* 操作:* 左键双击建筑 进入建筑层级;此时触发了进入建筑事件* 进入建筑后再左键双击 进入楼层;此时触发了退出建筑事件* 进入楼层后右键单击 返回建筑;此时触发了进入建筑事件* 返回建筑后 右键单击 返回园区;此时触发了退出建筑事件* 教程:ThingJS教程——>园区与层级——>【进阶】场景层级事件* 难度:★★★☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' // 场景地址 });app.on('load', function (ev) {// 场景加载完成后 进入园区层级app.level.change(ev.campus); });// 监听建筑层级的 EnterLevel 事件 app.on(THING.EventType.EnterLevel, ".Building", function (ev) {// 当前进入的层级对象var current = ev.current;// 上一层级对象var preObject = ev.previous;// 如果当前层级对象的父亲是上一层级对象(即正向进入)if (current.parent === preObject) {console.log("从 " + preObject.type + " 进入了 " + current.type);}else {console.log("进入 " + current.type + "(从 " + preObject.type + " 退出)");} });// 监听建筑层级的 LeaveLevel 事件 app.on(THING.EventType.LeaveLevel, ".Building", function (ev) {// 要进入的层级对象var current = ev.current;// 上一层级对象(退出的层级)var preObject = ev.previous;if (current.parent === preObject) {console.log("退出 " + preObject.type + " 进入 " + current.type);}else {console.log("退出 " + preObject.type + " ,返回 " + current.type);} })
我们可通过暂停系统内置的 LevelEnterOperation 来屏蔽掉默认的左键双击进入层级操作。
暂停系统内置的 LevelBackOperation 来屏蔽掉系统默认的右键单击退出层级的操作。
// 修改进入层级操作// 单击进入app.on(THING.EventType.SingleClick, function (ev) {var object = ev.object;if (object) {object.app.level.change(object);}}, 'customLevelEnterMethod');// 暂停双击进入app.pauseEvent(THING.EventType.DBLClick, '*', THING.EventTag.LevelEnterOperation);// 修改退出层级操作// 双击右键回到上一层级app.on(THING.EventType.DBLClick, function (ev) {if (ev.button != 2) {return;}app.level.back();}, 'customLevelBackMethod');// 暂停单击返回上一层级功能app.pauseEvent(THING.EventType.Click, null, THING.EventTag.LevelBackMethod)
当默认的层级切换飞行结束后,会触发 THING.EventType.LevelFlyEnd 事件。
可在该事件的回调函数中,进行层级切换飞行结束后的行为控制。
/*** 说明:层级飞行回调* 操作:* 当摄像机切换层级完成后,会打印完成回调日志* 教程:* ThingJS教程——>园区与层级——>【进阶】场景层级事件* ThingJS教程——>事件* 难度:★★★☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' // 场景地址 });app.on('load', function (ev) {var campus = ev.campus;app.level.change(campus); });// 层级切换飞行结束回调 app.on(THING.EventType.LevelFlyEnd, '*', function (ev) {console.clear();if (ev.previous) {console.log('上一层级:' + ev.previous.name)}console.log('[' + ev.object.name + '] 物体层级飞行结束'); });
切换场景层级响应
当层级发生变化后,会触发进入层级事件(EnterLevel)的四个内置响应和退出层级事件(LeaveLevel)的一个内置响应,他们分别是:
- 进入层级时的场景控制(THING.EventTag.LevelSceneOperations)
如进入建筑时显示所有楼层;进入物体时,设置兄弟物体半透明
- 进入层级时的飞行控制(THING.EventTag.LevelFly)
如进入各个层级时的飞行控制(飞行时间、视角等)
- 进入层级时背景控制(THING.EventTag.LevelSetBackground)
如进入建筑后隐藏天空盒
- 进入层级时的 Pick 设置(THING.EventTag.LevelPickedResultFunc)
如进入建筑后是只能 Pick 楼层还是也能 Pick 楼层下的物体
- 退出层级时的场景控制(THING.EventTag.LevelSceneOperations)
如从园区进入建筑层级(即退出园区)后,园区隐藏
如果想修改默认设置,可以暂停掉内置响应后再重新注册 EnterLevel 、 LeaveLevel 事件来进行修改。
可使用代码块快捷完成修改:
/*** 说明:* 自定义层级切换效果 例如* 进入建筑层级摊开楼层* 进入楼层层级更换背景图 等** 操作:* 关闭自定义层级控制时 层级切换执行系统内置的响应* 开启自定义层级控制时 层级切换执行自定义的效果** 难度:★★★★☆* 预备知识:场景层级、层级切换、事件(注册、暂停、恢复、卸载)*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse',skyBox: 'Night' });// 初始化完成后开启场景层级 var campus; app.on('load', function (ev) {campus = ev.campus;// 将层级切换到园区 开启场景层级app.level.change(ev.campus);createWidget(); });function createWidget() {// 界面组件var panel = new THING.widget.Panel();var customLevelControl = panel.addBoolean({ 'isEnabled': false }, 'isEnabled').caption('自定义层级控制');customLevelControl.on("change", function (ev) {app.level.change(campus);var isEnabled = ev;if (isEnabled) {console.log('启用自定义层级控制');enableCustomLevelChange();}else {console.log('恢复默认层级控制');disableCustomLevelChange();}}); }function enableCustomLevelChange() {// 暂停默认退出园区行为app.pauseEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);// 进入建筑摊开楼层app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var previous = ev.previous;console.log('从' + previous.type + '进入建筑');ev.current.expandFloors({'time': 1000,'complete': function () {console.log('ExpandFloor complete ');}});}, 'customEnterBuildingOperations');// 进入建筑保留天空盒app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);// 修改进入建筑层级选择设置app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var curBuilding = ev.current;app.picker.pickedResultFunc = function (object) {var parents = object.parents;for (var i = 0; i < parents.length; i++) {var parent = parents[i];// 如果被Pick物体的父亲是当前层级(Building)就返回被Pick的物体if (parent == curBuilding) {return object;}if (curBuilding.children.includes(parent)) {// return parent;return object;}}}}, 'customLevelPickedResultFunc');// 暂停建筑层级的默认选择行为app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelPickedResultFunc);// 退出建筑关闭摊开的楼层app.on(THING.EventType.LeaveLevel, '.Building', function (ev) {var current = ev.current;console.log('退出建筑,进入' + current.type);ev.object.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});}, 'customLeaveBuildingOperations');// 进入楼层设置背景app.on(THING.EventType.EnterLevel, '.Floor', function (ev) {var previous = ev.previous;console.log('从' + previous.type + '进入楼层');if (previous instanceof THING.Building) {// 从建筑进入楼层时app.background = '/uploads/wechat/emhhbmd4aWFuZw==/file/img/bg_grid.png';}}, 'setFloorBackground');app.pauseEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);// 退出楼层设置背景app.on(THING.EventType.LeaveLevel, '.Floor', function (ev) {var current = ev.current;console.log('退出楼层,进入' + current.type);if (current instanceof THING.Building) {// 从楼层退出到建筑时app.background = null;app.skyBox = "Night";}}, 'customLeaveFloorOperations');// 修改进入层级场景响应// * @property {Object} ev 进入物体层级的辅助数据// * @property {THING.BaseObject} ev.object 当前层级// * @property {THING.BaseObject} ev.current 当前层级// * @property {THING.BaseObject} ev.previous 上一层级app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐隐var things = object.brothers.query('.Thing');things.fadeOut();// 尝试播放动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],});}}, 'customEnterThingOperations');// 停止进入物体层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐现var things = object.brothers.query('.Thing');things.fadeIn();// 反播动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],reverse: true});}}, 'customLeaveThingOperations'); }function disableCustomLevelChange() {app.resumeEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelPickedResultFunc);app.resumeEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);app.off(THING.EventType.EnterLevel, '.Building', 'customEnterBuildingOperations');app.off(THING.EventType.EnterLevel, '.Building', 'customLevelPickedResultFunc');app.off(THING.EventType.LeaveLevel, '.Building', 'customLeaveBuildingOperations');app.off(THING.EventType.EnterLevel, '.Floor', 'setFloorBackground');app.off(THING.EventType.LeaveLevel, '.Floor', 'customLeaveFloorOperations');app.off(THING.EventType.EnterLevel, '.Thing', 'customEnterThingOperations');app.off(THING.EventType.LeaveLevel, '.Thing', 'customLeaveThingOperations');var curLevel = app.level.current;app.background = 'rgb(144,144,144)';if (curLevel instanceof THING.Building) {curLevel.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});} }
场景和地图
场景和地图
通过 CamBuilder 搭建一个园区后,我们可以用插件设置场景在地图上面的位置。
场景同步过去之后,我们可以通过代码获取场景在地图中摆放的经纬度数据。
app.on('load', function () {let tjsLnglat = app.root.defaultCampus.extraData;console.log(tjsLnglat); })
在ThingJS中,可以把园区摆放在地球对应位置上,上文提到的获取到的经纬度数据使用示例如下:
var app = new THING.App({url : 'https://www.thingjs.com/./client/ThingJS/13628/20191010182917578932750' }); var sceneLonlat = null; app.on('load', function(ev){app.background = [0, 0, 0];var map;let tjsLnt = app.root.defaultCampus.extraData.coordinates;tjsLnt = tjsLnt.split(',')sceneLonlat = tjsLnt;createMap(); })function createMap(){var map;THING.Utils.dynamicLoadJS(["https://www.thingjs.com/uearth/uearth.min.js"], function () {// 新建一个地图map = app.create({type: 'Map',style: {night: false},attribution: 'Google'});// 新建一个瓦片图层var tileLayer = app.create({type: 'TileLayer',name: 'tileLayer1',url: 'https://mt{0,1,2,3}.google.cn/vt/lyrs=s&hl=zh-CN&gl=cn&x={x}&y={y}&z={z}',});// 将瓦片图层添加到map中map.addLayer(tileLayer);app.root.defaultCampus.position = CMAP.Util.convertLonlatToWorld(sceneLonlat, 0);app.root.defaultCampus.angles = CMAP.Util.getAnglesFromLonlat(sceneLonlat, 90);app.camera.flyToGeoPosition({lonlat: sceneLonlat,height: 200,time: 3000,complete: function () { }});}) }
大型场景上述问题解决办法
- 在 CamBuilder 中我们可以分成多个工程进行搭建,比如园区和所有建筑的外立面使用一个独立的工程进行搭建,每栋建筑的室内可分别使用其他独立工程进行搭建。在搭建过程中有一条重要的规则需要遵守:
每个工程里的物体命名需要保证唯一
为了保证物体对象不重名,每个工程里的命名(工程文件的名称就是园区的名字),和每个工程里建筑的命名都要唯一。因为建筑的外立面和室内是在两个工程里分开搭建的,两个工程里本应有同一个名字的建筑,但为了后期可以加载到一起,就不能用同一个建筑名字了。
比如,建模需求是一个园区内有一个建筑,我们分成两个工程进行搭建,分别是“XX工业园区”、“XX工业园区-办公楼室内”,工程内物体命名如下:
XX工业园区(工程文件名,代表园区名),包括如下物体:
办公楼(建筑)
办公楼外立面(建筑外立面)
XX工业园区-办公楼室内(此工程和上个工程文件名不能一样),包括如下物体:
办公楼楼层一(楼层)
桌子。。。。。(物体)
办公楼楼层二(楼层)
桌子。。。。。(物体)
- 分别导出各个工程,并上传到 ThingJS 网站;
- 在 ThingJS 先加载"XX工业园区",该园区中包含建筑,但该建筑只有外立面。
- 使用事件,可重新注册进入建筑的响应函数,事件回调内使用 app.create ,动态加载“XX工业园区-办公楼室内”这个园区工程。
- 再使用代码,获取“办公楼TMP”这个园区物体的建筑,将其下的“办公楼楼层一”,“办公楼楼层二”,添加到本来只有外立面的“办公楼”对象身上。再将“XX工业园区-办公楼室内”和“办公楼TMP”这些临时对象删掉。此时,我们就动态加载了一个完整的“办公楼”。
/*** 说明:通过动态加载场景 动态加载建筑里的楼层* 说明:双击建筑 动态加载场景* 教程:ThingJS教程——>示例讲解——>动态加载场景* 难度:★★★★★* 预备知识:场景层级 事件 等*/ var app = new THING.App({"url": "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2","skyBox": "Universal", });// 场景加载进度条数据对象 var dataObj = {progress: 0 }; // 进度条界面组件 var loadingPanel; // 场景卸载开关 var switchWidget; // 创建界面 createWidgets();// 主场景加载完后 删掉楼层 app.on('load', function (ev) {// 进入层级切换app.level.change(ev.campus);console.log('卸载园区建筑下的默认楼层');// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < ev.buildings.length; i++) {ev.buildings[i].floors.destroy();} });// 配置相应建筑的园区场景url var buildingConfig = {'商业A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AA%E6%A5%BC%E5%B1%82%E7%BA%A7','商业B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AB%E6%A5%BC%E5%B1%82%E7%BA%A7','商业C楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7','商业D楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AD%E6%A5%BC%E5%B1%82%E7%BA%A7','商业E楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AE%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7', }// 进入建筑时 动态加载园区 app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var buildingMain = ev.object;var buildingName = buildingMain.name;// 上一层级的物体var preObject = ev.previous;// 如果是从楼层退出 进入Building的 则不做操作if (preObject instanceof THING.Floor) {console.log('从楼层退回到Building');return;}// 判断楼层是否加载if (buildingMain._isAlreadyBuildedFloors) {console.log('=== 建筑已加载!=== ' + buildingName);return;}else {console.log('=== 我要加载!=== ' + buildingName);}loadingPanel.visible = true;// 暂停进入建筑时的默认飞行操作,等待楼层创建完成app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 暂停单击右键返回上一层级功能app.pauseEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 动态创建园区var campusTmp = app.create({type: 'Campus',// 根据不同的建筑,传入园区相应的urlurl: buildingConfig[buildingName],// 在回调中,将动态创建的园区和园区下的建筑删除 只保留楼层 并添加到相应的建筑中complete: function () {var buildingTmp = campusTmp.buildings[0];buildingTmp.floors.forEach(function (floor) {buildingMain.add({object: floor,// 设置相对坐标,楼层相对于建筑的位置保持一致localPosition: floor.localPosition});})// 楼层添加后,删除园区以及内部的园区建筑buildingTmp.destroy();campusTmp.destroy();loadingPanel.visible = false;if (switchWidget.getValue() === false) {// 如果退出不卸载 则标记建筑已加载楼层buildingMain._isAlreadyBuildedFloors = true;}// 恢复默认的进入建筑飞行操作app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 恢复单击右键返回上一层级功能app.resumeEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 这一帧内 暂停自定义的 “进入建筑创建楼层” 响应app.pauseEventInFrame(THING.EventType.EnterLevel, '.Building', '进入建筑创建楼层');// 触发进入建筑的层级切换事件 从而触发内置响应buildingMain.trigger(THING.EventType.EnterLevel, ev);console.log('=== 加载完成!===');}}); }, '进入建筑创建楼层', 51);app.on(THING.EventType.LoadCampusProgress, function (ev) {var value = ev.progress;dataObj.progress = value; }, '加载场景进度');function createWidgets() {// 进度条界面组件loadingPanel = new THING.widget.Panel({titleText: '场景加载进度',opacity: 0.9, // 透明度hasTitle: true});// 设置进度条界面位置loadingPanel.positionOrigin = 'TR'// 基于界面右上角定位loadingPanel.position = ['100%', 0];loadingPanel.visible = false;loadingPanel.addNumberSlider(dataObj, 'progress').step(0.01).min(0).max(1).isPercentage(true);// 场景卸载界面组件var switchPanel = new THING.widget.Panel({titleText: '退出建筑时卸载',opacity: 0.9, // 透明度hasTitle: true});switchWidget = switchPanel.addBoolean({ 'open': false }, "open").caption("卸载场景");switchWidget.on('change', function (ev) {var value = ev;if (value) {// 退出建筑 进入到园区时 卸载建筑下动态创建的楼层app.on(THING.EventType.EnterLevel, '.Campus', function (ev) {var building = ev.previous;building._isAlreadyBuildedFloors = false;building.floors.destroy();console.log(building.name + '的楼层已被卸载!');}, '退出建筑时卸载建筑下的楼层');}else {app.off(THING.EventType.EnterLevel, '.Campus', '退出建筑时卸载建筑下的楼层');if (app.level.current instanceof THING.Building) {app.level.current._isAlreadyBuildedFloors = true;}}}) }
App 对象
创建 App 对象
当启动 ThingJS 系统的时候。我们需要创建 App 对象。
var app = new THING.App({url: "models/storehouse" });
上述代码中 url: "models/storehouse" 指园区场景数据的地址,此处为选填,该地址可写绝对路径也可写相对路径。
当然也可以不输入路径,在你需要的时候通过
app.create
创建园区物体,从而加载园区,如下例:var app = new THING.App(); var obj = app.create({type: "Campus",url: "models/storehouse/",complete: function() {console.log("Campus created: " + this.id);} });
App 提供的功能
App 作为 ThingJS 库的功能入口,提供了如下功能:
- 负责 3D 的初始化,如上述例子所见;
- 园区的加载;
提供了通过 create 创建物体、创建基本形状等;
提供了 query 搜索功能;
一些全局对象访问入口,如 root ,如 camera ;
通过 level 提供场景层级的控制;
提供了全局事件绑定功能;
时间:
通过 deltaTime 获取距离上一帧的流逝时间(毫秒);
通过 elapsedTime 获取从启动到现在的流逝时间(毫秒)。
效果控制:
通过 background 设置背景颜色或者图片;
提供了 lighting 设置灯光参数;
通过 postEffect 设置后期处理参数;
通过 fog 设置雾参数;
通过 skyBox 设置天空盒;
通过 skyEffect 设置时间线效果。
键盘输入
- 通过 isKeyPressed 判断某按键是否按下。
系统
通过 isMobileDevice 判断是否为移动端设备;
通过 pixelRatio 设置像素比例
- 通过 pixelRatio 获取像素比例。
页面相关
- 通过 app.domElement 获取包裹 3D 场景的 div
创建对象
有了场景,我们就可以添加物体对象了。
创建物体
在ThingJS中,可以动态创建或删除 Thing、Marker、Box等常见物体,他们大多继承自 BaseObject 。本章先以创建 Thing 物体为例,讲解创建对象时所需要的参数,其他各类对象会在相应章节中进行具体讲解。
var truck = app.create({ type: "Thing", name: "truck", position: [-5, 0, 0], url: "https://www.thingjs.com/static/models/truck/", complete: function() {console.log("truck created!"); } });
删除物体
truck.destroy();
/*** 说明:创建、删除卡车* 操作:点击按钮* 教程:ThingJS教程——>对象创建* 难度:★☆☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });app.on('load', function () {new THING.widget.Button('创建物体', create_truck);new THING.widget.Button('删除物体', destroy_truck); });// 卡车 var truck = null;// 创建卡车 function create_truck() {if (truck) { return; }truck = app.create({type: 'Thing',url: 'models/truck/',name: '我的卡车',id: '31415926',position: [-5, 0, 7],complete: function(ev) {console.log(ev.object.name + ' created!');}}); }// 删除卡车 function destroy_truck() {if (truck) {truck.destroy();truck = null;} }
创建物体参数
创建的物体参数可以分为以下三种:通用参数、特定物体类型(type)的专属参数、系统其他功能。
通用参数:
- type:该物体用什么物体类来创建
- id:该物体的编号
- name:物体的名字
- position:设置世界位置
- localPosition:设置在父物体下的相对位置,和 position 只能输入一个
- angles:设置世界坐标系下三轴旋转角度,例如:angles:[90,45,90] ,代表在世界坐标系下物体沿X轴旋转90度,沿Y轴旋转45度,沿Z轴旋转90度
- scale:设置相对自身坐标系下的缩放比例
- parent:设置父物体是谁
注意事项
为了更清晰明确的对用户动态创建的物体对象进行管理,建议创建物体对象时,显式指明该物体对象的 parent。如果没有显式填写parent时:
- 如果没有开启系统层级,则该物体的父亲默认是 root (不会是园区 Campus )
- 注册层级后创建物体不再默认指定父物体,若需要添加到父物体上,通过设置parent参数指定父物体,不指定默认添加到root下。
获取对象
通过 parent,children 属性找到要控制的对象。
/*** 说明:通过 “父子树” 访问场景内的对象* 操作:无,查看log信息* 教程:ThingJS 教程——>园区与层级——>场景层级* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 加载场景后执行 app.on('load', function (ev) {// 获取园区对象var campus = ev.campus;// 通过场景的 父子树 访问对象var children = campus.children;for (var i = 0; i < children.length; i++) {var child = children[i];var id = child.id;var name = child.name;var type = child.type;console.log('id: ' + id + ' name: ' + name + ' type: ' + type);}// id 107 为白色厂区建筑, // parent: app.query('107')[0] 为在厂区内创建物体// 厂区内创建的物体,只有在进入厂区后才会能显示,点击厂区进入,则看到绿色小车// 当推出厂区后,绿色小车则隐藏var obj = app.create({type: 'Thing',id: 'No1234567',name: 'truck',parent: app.query('107')[0],url: 'https://model.3dmomoda.com/models/8CF6171F7EE046968B16E10181E8D941/0/gltf/', // 模型地址 position: [0, 0, 0], // 世界坐标系下的位置complete: function (ev) {//物体创建成功以后执行函数console.log('thing created: ' + ev.object.id);}});var campus = ev.campus;console.log('after load ' + campus.id);// 切换层级到园区app.level.change(campus); });
通过类身上分类属性找到要控制的对象。
/*** 说明:通过 “分类对象属性树” 访问场景内的对象* 操作:无,查看log信息* 教程:ThingJS 教程——>园区与层级——>场景层级* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 加载场景后执行 app.on('load', function (ev) {// 获取园区对象var campus = ev.campus;// 打印园区内的 Thing 物体campus.things.forEach(function (thing) {console.log('Thing: ' + thing.id);});// 获取园区下的建筑对象var buildings = campus.buildings;buildings.forEach(function (building) {console.log('Building: ' + building.id);});// 打印第一个建筑中所有的楼层buildings[0].floors.forEach(function (floor) {console.log('Floor: ' + floor.id);});});
使用 query 方法
ThingJS 的 query 方法,包括
全局
和局部
。全局查询是对所有场景内的对象进行查询;
局部查询 是在一个对象的子对象中进行查询,如在一个楼层内查询某个设备;如果还需要更精确的缩小查询范围,还可以对查询结果进行继续查询;
由于场景加载是异步的 所以要查询场景内的物体时,需要在场景加载完成后查询才生效。
// 查询id是100的对象 app.query("#100")[0];// 查询名称(name)是 car01 的对象 app.query("car01")[0];// 查询物体类是Thing的对象 app.query(".Thing");//有物体类型属性的,无论值是什么 app.query("[alarm]");//查询物体类型属性是粮仓的对象 app.query("[报警=normal]"); app.query('["userData/物体类型"="粮仓"]');// 查询levelNum属性大于2的对象,目前支持 <= , < , = , > , >= app.query("[levelNum>2]");// 正则表达式(RegExp)对象,目前只是对名称(name)属性值进行正则匹配 app.query(/car/); // 上例等同于 var reg=new RegExp('car'); app.query(reg);
//在查询结果中再进行查询,可实现多个条件的“与操作” var sel = app.query('.Thing').query( '[品牌=IBM]' );//实现多个条件的“或操作” var sel = app.query( '[品牌=IBM]' ); app.query('[品牌=HP]').add( sel );//实现“非操作”,not 操作支持标准的条件 building.query('.Thing').not( 'cabinetB0' );//add操作除了上例中可添加 Selector 对象,还可以物体对象 app.query('.Thing').add( obj1 ); app.query('.Thing').add( [obj1,obj2.....] );//not 操作除了上例中可通过添加条件实现,也可以直接输入物体对象或 Selector 对象 app.query('.Thing').not( obj1 ); app.query('.Thing').not( [obj1,obj2.....] ); app.query('.Thing').not( sel );// 获取第一个元素 var obj = app.query('.Thing')[0];// 循环选择器对象,数组方式 var objs = app.query('.Thing'); for (var i = 0; i < objs.length; i ++) { console.log(objs[i]); }// 循环选择器对象(return false将不再循环) app.query('.Thing').forEach(function(obj) {...... });//可批量进行操作,具体查看 [Selector] app.query('.Thing').visible = false; app.query('.Thing').style.color = "#ff0000";//对查询到的结果每个物体进行绑定事件 app.query('.Thing').on('click', function(event) {console.log(event.object); });
/*** 说明:全局查询,根据 id 、name 、类型、属性、正则 等方式查询* 操作:点击按钮* 教程:ThingJS教程——>获取对象* 难度:★☆☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });app.on('load', function () {new THING.widget.Button('按id查询', queryById);new THING.widget.Button('按name查询', queryByName);new THING.widget.Button('按name正则查询', queryByRegExp);new THING.widget.Button('按类型查询', queryByClass);new THING.widget.Button('按属性查询', queryByProperty); });// 搜索 id 为 2271 的物体 function queryById() {var car = app.query('#2271')[0];car.style.color = '#ff0000'; }// 搜索 name 为'car01'的物体 function queryByName() {var car = app.query('car01')[0];car.style.outlineColor = '#ff0000'; } // 根据正则表达式匹配 name 中包含'car'的物体 function queryByRegExp() {var cars = app.query(/car/);// 上行代码等同于// var reg = new RegExp('car');// var cars=app.query(reg);cars.forEach(function (obj) {obj.style.color = '#00ff00';}) } // 搜索类型是'Building'的物体 function queryByClass() {var things = app.query('.Building');for (var i = 0; i < things.length; i++) {things[i].style.outlineColor = '#0000ff';} }// 搜索名字中包含'car'、并且属性字段userData中马力大于50的物体 function queryByProperty() {app.query(/car/).query('[userData/power>50]').forEach(function (obj) {obj.style.outlineColor = '#ffff00';}); }
控制对象
连接
在讲解园区层级的章节时,提到了父子树的概念,除了在创建物体对象时(app.create)可以指定一个对象的父物体外,还可以使用 add 接口让一个物体 B 作为孩子添加到另一个物体 A 的子物体集合中,物体 A 即为物体 B 的父物体。
因为子物体会跟随父物体一同移动、旋转和缩放,所以我们把绑定父物体的操作定义为 “连接操作” 。
可以直接使用 add(object) 方法进行连接操作:
car.add(box);
此时 “连接” 上的一刻,子物体的世界位置不发生变化,并保持那一刻与父物体的相对位置关系进行移动
如果我们要删除 “连接” 关系,那么就需要将该物体指定另一个父物体进行 “连接”。
// 创建箱子 var box = app.create({ type: 'Box', center: 'Bottom', position: [5, 0, 2] });// 创建Thing 叉车 var car = app.create({type: 'Thing',name: '叉车',url: 'https://model.3dmomoda.cn/models/7fb3a14c34cc42bd81a39bdf075d5d85/0/gltf/',// 模型地址 position: [-12, 0, 0],// 世界坐标下的位置 complete: function (ev) {new THING.widget.Button('连接到叉车', function () {// 将物 box 作为孩子添加到 car 上car.add(box);});new THING.widget.Button('从叉车移除', function () {// 将 box 从叉车移除 ,重新连接到园区上var campus = app.query('.Campus')[0];campus.add(box);});// 设置物体沿路径移动 car.moveTo({'position': [27, 0, 0], // 路径点数组 'time': 10 * 1000, // 路径总时间,2秒 'orientToPath': true, // 物体移动时沿向路径方向 'loopType': THING.LoopType.PingPong,'lerpType': null});} });
如果我们 “连接” 时,想设置子物体与父物体的相对位置关系,示例如下:
car.add({object: box, // 作为孩子的对象localPosition: [0, 2, 0] // 相对于父物体的坐标 });
// 创建箱子 var box = app.create({ type: 'Box', center: 'Bottom', position: [5, 0, 2] });// 创建Thing 叉车 var car = app.create({ type: 'Thing', name: '叉车', url: 'https://model.3dmomoda.cn/models/7fb3a14c34cc42bd81a39bdf075d5d85/0/gltf/',// 模型地址 position: [-12, 0, 0],// 世界坐标下的位置 complete: function (ev) {new THING.widget.Button('连接到叉车', function () {// 将物 box 作为孩子添加到 car 上car.add({object: box, // 作为孩子的对象localPosition: [0, 2, 0] // 相对于父物体的坐标});});new THING.widget.Button('从叉车移除', function () {// 将 box 从叉车移除 ,重新连接到园区上var campus = app.query('Campus')[0];campus.add(box);});// 设置物体沿路径移动 car.moveTo({'position': [27, 0, 0], // 路径点数组 'time': 10 * 1000, // 路径总时间,2秒 'orientToPath': true, // 物体移动时沿向路径方向 'loopType': THING.LoopType.PingPong,'lerpType': null}); } });
以子节点作为基准连接
如果一个物体的模型是由多个“子节点”组合而成的
效果如下:
那么我们也可以基于某个“子节点”设置该模型与所连接的父物体的相对位置关系,如下:
car.add({object: box,// 作为孩子的对象basePoint: "chazi", // 作为“基准”的“子节点”名称offset: [0, 0.2,0] // 相对于参考点位的自身偏移量 });
// 创建箱子 var box = app.create({ type: 'Box', center: 'Bottom', position: [5, 0, 5] }); box.style.color = 'rgb(255,0,0)';// 创建Thing var car = app.create({type: 'Thing',name: '叉车',url: 'https://model.3dmomoda.cn/models/7fb3a14c34cc42bd81a39bdf075d5d85/0/gltf/',// 模型地址 position: [10, 0, 5],complete: function (ev) {var radio = createUI();radio.on('change', function (ev) {// console.clear();var subNodeName = ev;console.log(car.subNodes)console.log('将 ' + subNodeName + ' 节点 作为基准');car.add({object: box,basePoint: subNodeName,offset: [0, 0.1, 0]});})} });function createUI() {// 界面组件var panel = new THING.widget.Panel({titleText: '各个子节点',width: '200px',hasTitle: true, // 是否有标题});// 创建数据对象 var dataObj = {'subNodes': 'chazi',};// 界面绑定对象 var radio = panel.addRadio(dataObj, 'subNodes', ['chazi', 'qianlun', 'houlun', 'SubModelNode001']);return radio; }car.moveTo({'position': [12, 0, 0], // 路径点数组 'time': 10 * 1000, // 路径总时间,2秒 'orientToPath': true, // 物体移动时沿向路径方向 'loopType': THING.LoopType.PingPong,'lerpType': null});
对象的拾取和选择
通过事件获取鼠标拾取的物体
可以通过 MouseEnter 和 MouseLeave 来实现 。
// 鼠标拾取物体显示边框 app.on(THING.EventType.MouseEnter, '.Thing' ,function(ev) {ev.object.style.outlineColor = '#FF0000'; }); // 鼠标离开物体边框取消 app.on(THING.EventType.MouseLeave,'.Thing', function(ev) {ev.object.style.outlineColor = null; });
/*** 说明: 拾取物体*/var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });app.on('load', function () {// 鼠标拾取物体显示边框app.on(THING.EventType.MouseEnter, '.Thing', function(ev) {ev.object.style.outlineColor = '#FF0000';});// 鼠标离开物体边框取消app.on(THING.EventType.MouseLeave, '.Thing', function(ev) {ev.object.style.outlineColor = null;});app.on(THING.EventType.MouseEnter, '.Building', function(ev) {ev.object.style.outlineColor = '#FF0000';});// 鼠标离开物体边框取消app.on(THING.EventType.MouseLeave, '.Building', function(ev) {ev.object.style.outlineColor = null;});// 每一帧判断拾取的物体是否发生变化app.on('update', function () {if (app.picker.isChanged()) {console.clear();// 打印当前被pick的物体if (app.picker.objects[0]) {console.log('当前拾取的物体 ' + app.picker.objects[0].name);}// 打印之前被pick的物体if (app.picker.previousObjects[0]) {console.log('之前拾取的物体 ' + app.picker.previousObjects[0].name);}}}); });
选择物体
鼠标悬停到物体上,但不代表我选择它了,比如是我们点击后才表明我们选择了它。选择物体,我们通过 Selection 模块实现,可通过 app.selection 的接口实现该功能,见下例:
//将物体加入到选择集中 app.selection.select(obj); // 判断对象是否在选择集中 app.selection.has(obj); //将物体从选择集中删除 app.selection.deselect(obj); //清空选择集 app.selection.clear();
摄影机
界面
标记物
Marker物体可以添加一个图片放置到你希望的位置,也可以将这个图片作为孩子添加到物体身上,进行对象一同移动。
app.create({type: "Marker",offset: [0, 2, 0],size: [4, 4],url: "https://thingjs.com/static/images/warning1.png",parent: app.query("car01")[0] });
参数:
- 类型:通知系统创建Marker物体;
- offset:设置自身坐标系下偏移量为[0,2,0];
- size:设置Marker物体大小,也可以添一个数字如4,等于于[4,4],大小依据米计算的;
- 网址:图片的网址;
- parent:指定Marker的父实体;
- keepSize:控制是否受距离远近影响,呈现近大远小的3D效果。如果设置为true,表示保持大小,不随距离近大远小,则size的单位是屏幕的后果点;
标记默认为受距离远近影响,呈现近大远小的3D效果,也会在3D空间中实现前后遮挡。
我们还可以使用h5的canvas手动创建动态图。
function createTextCanvas(text, canvas) {if (!canvas) {canvas = document.createElement("canvas");canvas.width = 64;canvas.height = 64;}const ctx = canvas.getContext("2d");ctx.fillStyle = "rgb(32, 32, 256)";ctx.beginPath();ctx.arc(32, 32, 30, 0, Math.PI * 2);ctx.fill();ctx.strokeStyle = "rgb(255, 255, 255)";ctx.lineWidth = 4;ctx.beginPath();ctx.arc(32, 32, 30, 0, Math.PI * 2);ctx.stroke();ctx.fillStyle = "rgb(255, 255, 255)";ctx.font = "32px sans-serif";ctx.textAlign = "center";ctx.textBaseline = "middle";ctx.fillText(text, 32, 32);return canvas; }app.on('load', function (ev) {var marker = app.create({type: "Marker",offset: [0, 2, 0],size: 3,canvas: createTextCanvas('100'),parent: app.query('car02')[0]}).on('click', function (ev) {var txt = Math.floor(Math.random() * 100);ev.object.canvas = createTextCanvas(txt, ev.object.canvas)}) })
WebView对象
我们可以使用WebView物体,将其他网站或页面的内容嵌入到3D中。
/*** 说明:WebView页面* 文档:ThingJS教程——>界面——>3D界面* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 加载场景后执行 app.on('load', function () {// 设置摄像机位置和目标点app.camera.position = [20.325589006298948, 25.47555854790737, 23.598673245623264];app.camera.target = [2.3860871693835133, -0.2973609127471111, -5.171065071269126];var webView01 = app.create({type: 'WebView',url: 'https://cn.bing.com/',position: [10, 13, -5],width: 1920 * 0.01, // 3D 中实际宽度 单位 米height: 1080 * 0.01, // 3D 中实际高度 单位 米domWidth: 1920, // 页面宽度 单位 pxdomHeight: 1080// 页面高度 单位 px});var webView02 = app.create({type: 'WebView',url: 'https://www.thingjs.com',position: [10, 0.5, 5],width: 1920 * 0.01, // 3D 中实际宽度 单位 米height: 1080 * 0.01, // 3D 中实际高度 单位 米domWidth: 1920, // 页面高度 单位 pxdomHeight: 1080 // 页面高度 单位 px});webView02.rotateX(-90);// 设置页面不可拾取交互webView02.pickable = false;// 以小车为父物体创建 WebViewvar car01 = app.query('car01')[0];var webView03 = app.create({type: 'WebView',url: 'https://www.thingjs.com/static/pages/page02/index.html?name=' + car01.name,parent: car01, // 父物体localPosition: [0, 3, -1], // 父物体坐标系下相对坐标位置width: 462 * 0.008, // 3D 中实际宽度 单位 米height: 296 * 0.008, // 3D 中实际高度 单位 米domWidth: 462, // 页面宽度 单位 pxdomHeight: 296 // 页面高度 单位 px});webView03.rotateX(-30);// 设置页面不可拾取交互webView03.pickable = false;new THING.widget.Button('切换页面', function () {webView01.url = 'https://www.thingjs.com/guide/cn/tutorial_Introduce/index.html'}) });
UIAnchor
还有一个神奇的功能,即使是2D html界面,我们照样可以把它连接到3D物体上,跟随3D物体移动,我们使用UIAnchor`物体来实现这个功能。
var uiAnchor = app.create({type: "UIAnchor",parent: app.query("car02")[0],element: document.getElementById("XXXX"),localPosition: [0, 2, 0],pivotPixel: [0.5, 1] });
参数:
- element :要绑定的页面的 element 对象
- pivotPixel :指定页面的哪个点放到 localPosition 位置上,0.5 相当于 50%
删除UIAnchor方法为:
uiAnchor.destroy();
删除后,其对应的 panel 也会被删除
显示和隐藏UIAnchor方法为:
uiAnchor.visible = true / false;
可以利用 UIAnchor 连接到 3D 物体上。
/*** 说明:创建界面元素,作为UIAnchor连接到物体上,使其能跟随物体* 操作:点击按钮 创建、删除 UIAnchor* 教程:ThingJS教程——>界面——>2D html界面* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 创建按钮 function createUI() {new THING.widget.Button('物体界面', test_create_ui);new THING.widget.Button('位置界面', test_create_ui_at_point);new THING.widget.Button('删除界面', test_destroy_ui); } createUI();// 添加html function create_html() {var sign =`<div class="sign" id="board" style="font-size: 12px;width: 120px;text-align: center;background-color: rgba(0, 0, 0, .6);border: 3px solid #eeeeee;border-radius: 8px;color: #eee;position: absolute;top: 0;left: 0;z-index: 10;display: none;"><div class="s1" style="margin: 5px 0px 5px 0px;line-height: 32px;overflow: hidden;"><span class="span-l icon" style="float: left;width: 30px;height: 30px;background:url(https://www.thingjs.com/static/images/example/hydrant.png) no-repeat center;margin: 1px 1px 1px 5px;"></span><span class="span-l font" style="float: left;margin: 0px 0px 0px 3px;">物体</span><span class="span-r point" style="float: right;width: 12px;height: 12px;background-color: #18EB20;border-radius: 50%;margin: 10px 5px 10px 0px;"></span></div><div class="s2" style="margin: 5px 0px 10px 0px;line-height: 18px;font-size: 10px;overflow: hidden;"><span class="span-l font1" style="float: left;margin: 0px 10px 0px 10px;">数值</span><span class="span-l font2" style="float: left;width: 70px;background-color: #2480E3;">0.14MPa</span></div><div class="point-top" style="position: absolute;top: -7px;right: -7px;background-color: #3F6781;width: 10px;height: 10px;border: 3px solid #eee;border-radius: 50%;"></div></div>`$('#div3d').append($(sign)); } create_html();// 生成一个新面板 function create_element() {var srcElem = document.getElementById('board');var newElem = srcElem.cloneNode(true);newElem.style.display = "block";app.domElement.insertBefore(newElem, srcElem);return newElem; }// 物体顶界面 var ui = null; function test_create_ui() {if(ui==null){ui = app.create({type: 'UIAnchor',parent: app.query('car02')[0],element: create_element(),localPosition: [0, 2, 0],pivot: [0.5, 1] // [0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});}}// 位置界面 var ui2 = null; function test_create_ui_at_point() {if(ui2==null){ui2 = app.create({type: 'UIAnchor',element: create_element(),position: [0, 1, 0]});}}// 删除界面 function test_destroy_ui() {if (ui) {ui.destroy();ui = null;}if (ui2) {ui2.destroy();ui2 = null;} }
也可以通过快捷界面库,创建 Panel 以 UIAnchor 的方式连接到物体上。
/*** 说明:用快捷界面库 给物体添加UIAnchor* 教程:ThingJS教程——>界面——>2D html界面 及 快捷界面库* 难度:★★☆☆☆*/ const app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' // 场景地址 });app.on('load', function () {const car = app.query('car01')[0];car.style.color = 'rgb(255,0,0)';// 用快捷界面库 给物体添加UIAnchorconst uiAnchor = createUIAnchor(car);new THING.widget.Button('显示/隐藏', function () {// 显示/隐藏 uiAnchoruiAnchor.visible = !uiAnchor.visible;}) })// 创建UIAnchor function createUIAnchor(obj) {// 创建widget (绑定数据用)const panel = new THING.widget.Panel({// 设置面板宽度width: '150px',// cornerType 角标样式// 没有角标 none ,没有线的角标 noline ,折线角标 polylinecornerType: 'polyline'})// 绑定物体身上相应的属性数据panel.addString(obj, 'name').caption('名称');panel.addString(obj.userData, 'power').caption('马力');// 创建UIAnchor面板const uiAnchor = app.create({// 类型type: 'UIAnchor',// 父节点设置parent: obj,// 要绑定的页面的 element 对象element: panel.domElement,// 设置 localPosition 为 [0, 0, 0]localPosition: [0, 0, 0],// 相对于Element左上角的偏移像素值pivotPixel: [-16, 109] // 当前用值是角标的中心点});return uiAnchor; }
快捷界面库
THING.widget是一个支持动态数据绑定的轻量级界面库。
可通过界面库中的Panel组件创建一个面板,依次向该面板中添加文本,数字,单选框,复选框等其他组件。
var panel = new THING.widget.Panel({// 设置面板样式template: 'default',// 角标样式cornerType: "none",// 设置面板宽度width: "300px",// 是否有标题hasTitle: true,// 设置标题名称titleText: "我是标题",// 面板是否允许有关闭按钮closeIcon: true,// 面板是否支持拖拽功能dragable: true,// 面板是否支持收起功能retractable: true,// 设置透明度opacity: 0.9,// 设置层级zIndex: 99 });
- width:如果写百分比长度则表示相对宽度(相对于3D容器的宽度)
- template:目前,模板样式提供两个样式default和default2
- cornerType:cornerType是指角标样式,依次是:没有角标none,没有线的角标noline,折线角标polyline;
角标样式都不区分大小写
如果panel面板设置了关闭按钮则单击关闭按钮时替换面板设置为隐藏,如需再次打开该面板则调用panel.visible = true; 显示面板即可。
/*** 说明:创建Widget面板,可动态双向绑定数据* THING.widget是一个支持动态绑定的轻量界面库,可配合ThingJS使用* 需要引入文件 https://www.thingjs.com/static/release/thing.widget.min.js(在线开发环境已内置)* 教程:ThingJS教程——>界面——>快捷界面库* 难度:★★☆☆☆*/ var app = new THING.App({url: 'https://www.thingjs.com/static/models/storehouse' });// 界面组件 var panel = new THING.widget.Panel({titleText: '我是标题', // 可通过font标签设置标题颜色 例如:'<font color="red">我是红色标题</font>'closeIcon: true, // 是否有关闭按钮dragable: true, // 是否可拖拽retractable: true, // 是否可收缩opacity: 0.9, // 设置透明度hasTitle: true, // 设置标题zIndex: 999 // 设置层级 }); // 设置panel位置 panel.position = [10, 10]; // 可设置panel.visible属性(true/false)来控制panel的显示/隐藏 // 如果panel面板设置了关闭按钮 则点击关闭按钮时 会将面板设置为隐藏 // 如需再次打开该面板 则调用 panel.visible = true; 显示面板即可// 创建数据对象 var dataObj = {pressure: '0.14MPa',temperature: '21°C',checkbox: { '设备1': false, '设备2': false, '设备3': true, '设备4': true },radio: '摄像头01',open: true,height: 10,maxSize: 1.0,iframe: 'https://www.thingjs.com/guide/' };// 动态绑定数据// 加载字符型组件 var press = panel.addString(dataObj, 'pressure').caption('水压').isChangeValue(true);// 可通过font标签设置 组件caption颜色 var water = panel.addString(dataObj, 'temperature').caption('<font color="red">水温</font>').isChangeValue(true); // 加载复选框组件 var check = panel.addCheckbox(dataObj, 'checkbox').caption({ "设备2": "设备2(rename)" }); // 复选框需逐个添加change事件 check[0].on('change', function(ev) {console.log(ev); }); check[1].on('change', function(ev) {console.log(ev); }) // 加载单选框组件 var radio = panel.addRadio(dataObj, 'radio', ['摄像头01', '摄像头02']); radio.on('change', function(ev) {console.log(ev); }) // 加载开关组件(适用于Boolean类型数据) var open1 = panel.addBoolean(dataObj, 'open').caption('开关01'); open1.on('change', function(ev) {console.log(ev); }) // 加载数字组件 var height = panel.addNumber(dataObj, 'height').caption('高度'); // 加载数字型进度条组件 var maxSize = panel.addNumberSlider(dataObj, 'maxSize').step(0.25).min(1).max(10); // 加载iframe组件 var iframe = panel.addIframe(dataObj, 'iframe').caption('视频'); // 设置iframe高度 iframe.setHeight("250px");// 每秒更新组件数据 setInterval(function () {if (dataObj.maxSize >= 10) { dataObj.maxSize = 0; }dataObj.height += 1;dataObj.maxSize += 1; }, 1000);
// 获取面板标签 panel.domElement;// 修改面板标题 panel.titleText='修改标题';// 设置/获取面板相关属性 panel.visible = true / false; panel.position = [10, 10];//设置panel面板的位置 panel.zIndex = 9; panel.opacity = 0.5;// 删除面板 panel.destroy();
面板事件
// 常用事件类型均支持 panel.on("click", callback); // 'close'事件为面板关闭时触发 panel.on("close", callback);
面板中的数据 可通过各组件实现双向绑定
var dataObj = {pressure: "0.14MPa",temperature: "21°C",checkbox: { 设备1: false, 设备2: false, 设备3: true, 设备4: true },radio: "摄像头01",open1: true,height: 10,maxSize: 1.0,iframe: "https://www.3dmomoda.com",progress: 1,img: "https://www.thingjs.com/guide/image/new/logo2x.png",button: false };
逐条添加组件
var press = panel.addString(dataObj, 'pressure').caption('水压').isChangeValue(true); var height = panel.addNumber(dataObj, 'height').caption('高度'); var maxSize = panel.addNumberSlider(dataObj, 'maxSize').step(0.25).min(1).max(10); var open1 = panel.addBoolean(dataObj, 'open1').caption('开关01'); var radio = panel.addRadio(dataObj, 'radio', ['摄像头01', '摄像头02']); var check = panel.addCheckbox(dataObj, 'checkbox').caption({ "设备2": "设备2(rename)" }); var iframe = panel.addIframe(dataObj, 'iframe').caption('视屏'); var img = panel.addIframe(dataObj, 'img').caption('图片'); var button = panel.addImageBoolean(dataObj, 'button').caption('仓库编号').url('https://www.thingjs.com/static/images/example/icon.png');
删除组件
panel.remove(press); panel.remove(radio); ......
组件方法介绍
组件通用方法:
ThingJS学习笔记相关推荐
- PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 call
您的位置 首页 PyTorch 学习笔记系列 PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 发布: 2017年8月4日 7,195阅读 ...
- 容器云原生DevOps学习笔记——第三期:从零搭建CI/CD系统标准化交付流程
暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...
- 容器云原生DevOps学习笔记——第二期:如何快速高质量的应用容器化迁移
暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...
- 2020年Yann Lecun深度学习笔记(下)
2020年Yann Lecun深度学习笔记(下)
- 2020年Yann Lecun深度学习笔记(上)
2020年Yann Lecun深度学习笔记(上)
- 知识图谱学习笔记(1)
知识图谱学习笔记第一部分,包含RDF介绍,以及Jena RDF API使用 知识图谱的基石:RDF RDF(Resource Description Framework),即资源描述框架,其本质是一个 ...
- 计算机基础知识第十讲,计算机文化基础(第十讲)学习笔记
计算机文化基础(第十讲)学习笔记 采样和量化PictureElement Pixel(像素)(链接: 采样的实质就是要用多少点(这个点我们叫像素)来描述一张图像,比如,一幅420x570的图像,就表示 ...
- Go 学习推荐 —(Go by example 中文版、Go 构建 Web 应用、Go 学习笔记、Golang常见错误、Go 语言四十二章经、Go 语言高级编程)
Go by example 中文版 Go 构建 Web 应用 Go 学习笔记:无痕 Go 标准库中文文档 Golang开发新手常犯的50个错误 50 Shades of Go: Traps, Gotc ...
- MongoDB学习笔记(入门)
MongoDB学习笔记(入门) 一.文档的注意事项: 1. 键值对是有序的,如:{ "name" : "stephen", "genda" ...
- NuGet学习笔记(3) 搭建属于自己的NuGet服务器
文章导读 创建NuGetServer Web站点 发布站点到IIS 添加本地站点到包包数据源 在上一篇NuGet学习笔记(2) 使用图形化界面打包自己的类库 中讲解了如何打包自己的类库,接下来进行最重 ...
最新文章
- C# 创建控制台应用程序
- .NET弹出对话框小结
- ELS多种方式集群部署
- java责任链设计模式_Java中的责任链设计模式
- Java核心技术- Java内存分配原理
- 平面上两直线的夹角求法解析
- 通信原理 简易蒙特卡洛仿真法仿真无码间干扰基带系统误码率的matlab实现
- 亲密关系科学(05)男女大脑激素差异
- 【图像增强】基于matlab Frangi滤波器血管图像增强【含Matlab源码 2108期】
- 苹果支付成功后,JAVA服务端二次验证
- 微信小程序实现保存图片(唤起用户授权)
- python模块之signal信号
- 【Latex】如何用 latex 分双栏(分两列)
- 射灯安装方法图解_射灯怎么安装?射灯安装离墙距离多少合适?射灯安装图解介绍!...
- PMP项目管理过程实用表格与应用(实用表格推荐)
- python物性库能调用哪些物质_用于水和水蒸汽物性计算的Python模块——iapws
- 使用Synergy控制实现一套键盘鼠标控制多台电脑(windows+linux)
- SNA(社会网络分析)——三种中心度总结
- fiddler抓包工具安装,配置https、移动端抓包、弱网设置
- 运放电路的知识点(四)