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 库的功能入口,提供了如下功能:

  1. 负责 3D 的初始化,如上述例子所见;
  2. 园区的加载;
  3. 提供了通过 create 创建物体、创建基本形状等;

  4. 提供了 query 搜索功能;

  5. 一些全局对象访问入口,如 root ,如 camera ;

  6. 通过 level 提供场景层级的控制;

  7. 提供了全局事件绑定功能;

  8. 时间:

    • 通过 deltaTime 获取距离上一帧的流逝时间(毫秒);

    • 通过 elapsedTime 获取从启动到现在的流逝时间(毫秒)。

  9. 效果控制:

    • 通过 background 设置背景颜色或者图片;

    • 提供了 lighting 设置灯光参数;

    • 通过 postEffect 设置后期处理参数;

    • 通过 fog 设置雾参数;

    • 通过 skyBox 设置天空盒;

    • 通过 skyEffect 设置时间线效果。

  10. 键盘输入

    • 通过 isKeyPressed 判断某按键是否按下。
  11. 系统

    • 通过 isMobileDevice 判断是否为移动端设备;

    • 通过 pixelRatio 设置像素比例

    • 通过 pixelRatio 获取像素比例。
  12. 页面相关

    • 通过 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学习笔记相关推荐

  1. PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 call

    您的位置 首页 PyTorch 学习笔记系列 PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 发布: 2017年8月4日 7,195阅读 ...

  2. 容器云原生DevOps学习笔记——第三期:从零搭建CI/CD系统标准化交付流程

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  3. 容器云原生DevOps学习笔记——第二期:如何快速高质量的应用容器化迁移

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  4. 2020年Yann Lecun深度学习笔记(下)

    2020年Yann Lecun深度学习笔记(下)

  5. 2020年Yann Lecun深度学习笔记(上)

    2020年Yann Lecun深度学习笔记(上)

  6. 知识图谱学习笔记(1)

    知识图谱学习笔记第一部分,包含RDF介绍,以及Jena RDF API使用 知识图谱的基石:RDF RDF(Resource Description Framework),即资源描述框架,其本质是一个 ...

  7. 计算机基础知识第十讲,计算机文化基础(第十讲)学习笔记

    计算机文化基础(第十讲)学习笔记 采样和量化PictureElement Pixel(像素)(链接: 采样的实质就是要用多少点(这个点我们叫像素)来描述一张图像,比如,一幅420x570的图像,就表示 ...

  8. 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 ...

  9. MongoDB学习笔记(入门)

    MongoDB学习笔记(入门) 一.文档的注意事项: 1.  键值对是有序的,如:{ "name" : "stephen", "genda" ...

  10. NuGet学习笔记(3) 搭建属于自己的NuGet服务器

    文章导读 创建NuGetServer Web站点 发布站点到IIS 添加本地站点到包包数据源 在上一篇NuGet学习笔记(2) 使用图形化界面打包自己的类库 中讲解了如何打包自己的类库,接下来进行最重 ...

最新文章

  1. C# 创建控制台应用程序
  2. .NET弹出对话框小结
  3. ELS多种方式集群部署
  4. java责任链设计模式_Java中的责任链设计模式
  5. Java核心技术- Java内存分配原理
  6. 平面上两直线的夹角求法解析
  7. 通信原理 简易蒙特卡洛仿真法仿真无码间干扰基带系统误码率的matlab实现
  8. 亲密关系科学(05)男女大脑激素差异
  9. 【图像增强】基于matlab Frangi滤波器血管图像增强【含Matlab源码 2108期】
  10. 苹果支付成功后,JAVA服务端二次验证
  11. 微信小程序实现保存图片(唤起用户授权)
  12. python模块之signal信号
  13. 【Latex】如何用 latex 分双栏(分两列)
  14. 射灯安装方法图解_射灯怎么安装?射灯安装离墙距离多少合适?射灯安装图解介绍!...
  15. PMP项目管理过程实用表格与应用(实用表格推荐)
  16. python物性库能调用哪些物质_用于水和水蒸汽物性计算的Python模块——iapws
  17. 使用Synergy控制实现一套键盘鼠标控制多台电脑(windows+linux)
  18. SNA(社会网络分析)——三种中心度总结
  19. fiddler抓包工具安装,配置https、移动端抓包、弱网设置
  20. 运放电路的知识点(四)

热门文章

  1. 基于微信小程序 校园跑腿小程序毕业设计毕设开题报告参考功能
  2. Unity实现鼠标控制摄像机围绕中心点的旋转和缩放
  3. 中华传统文化网页作业报告
  4. ISIS metric
  5. PC电脑使用无线网卡连接上手机热点,为什么不能上网
  6. 单元词检索计算机,截词检索
  7. 计算机硬盘格式分类,fat32是什么?
  8. 语音识别工具Sphinx4
  9. gopher协议利用
  10. qgis比例尺级别设置