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 园区,类型:Campus ev.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', // 根据不同的建筑,传入园区相应的url url: 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; } } }) }
当启动 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, // 页面宽度 单位 px domHeight: 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, // 页面高度 单位 px domHeight: 1080 // 页面高度 单位 px }); webView02.rotateX(-90); // 设置页面不可拾取交互 webView02.pickable = false; // 以小车为父物体创建 WebView var 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, // 页面宽度 单位 px domHeight: 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 = `
` $('#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)'; // 用快捷界面库 给物体添加UIAnchor const uiAnchor = createUIAnchor(car); new THING.widget.Button('显示/隐藏', function () { // 显示/隐藏 uiAnchor uiAnchor.visible = !uiAnchor.visible; }) }) // 创建UIAnchor function createUIAnchor(obj) { // 创建widget (绑定数据用) const panel = new THING.widget.Panel({ // 设置面板宽度 width: '150px', // cornerType 角标样式 // 没有角标 none ,没有线的角标 noline ,折线角标 polyline cornerType: '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标签设置标题颜色 例如:'我是红色标题' 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('水温').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); ......
组件方法介绍
组件通用方法: