需求分析
- 三维场景加载
- 3D DOM 容器;
- 加载场景;
- 摄像机飞行(角度,视角)
- 创建 2D UI 通栏
- 仓库对象:顶牌信息显隐、面板信息显隐、仓库盖板控制
- 监控对象:顶牌显隐、顶牌事件、面板信息显隐(播放)
- 车辆对象:路径移动、面板信息显隐(跟随移动)
功能清单
| 需求 | 功能 | API | | —- | —- | —- | | 三维场景加载 |
- 创建 3D DOM 容器节点
- 创建并加载园区场景
- 初始化事件
|new THING.App()app.on("load", () => {})| | 2D UI 通栏 |
- 创建通栏 UI
|new THING.widget.Banner| | 仓库对象 |
- 顶牌信息
- 面板信息
- 盖板控制
|app.create({type: "UI"})new THING.widget.Pannelobject.visiable/object.position/object.moveTo| | 监控对象 |
- 顶牌信息
- 顶牌事件
- 面板信息
|app.create({type: "Maker"})maker.on("click", () => {})new THING.widget.Pannel| | 车辆对象 |
- 车辆路径移动
- 面板信息(跟随移动)
|object.movePath([])new THING.widget.Pannel|
实战:
一、三维场景加载
// 1.1 加载场景const app = new THING.App({el: "div3d",url: "models/silohouse",});// 1.2 初始化事件app.on("load", (e) => {// 1.3 调整摄像机初始位置及视角app.camera.flyTo({position: [-182.16900300883736, 53.24677728392183, 72.21965470775368],target: [-68.1412926741533, -18.16319203074775, -23.30416731768694],time: 1500,});});
二、创建 2D UI 通栏
/**2.1 通过 toolbar = new THING.widget.Banner(param) 创建通栏其中 param = {template: "default", // 模板column: "top" | "left", // 位置opaticy: 1, // 透明度visiable: true, // 可见性}2.2 通过 toolbar.addImageBoolean(buttonCollection, buttonKey) 初始化图标按钮2.3 通过初始化图标按钮的 .caption(title) 设置标题2.4 通过初始化图标按钮的 .url(url) 设置图片路径2.5 通过初始化图标按钮的 .on(eventName, callback) 设置事件监听*/const toolbar = new THING.widget.Banner({column: "left"});const iconBaseUrl = "https://www.thingjs.com/static/images/sliohouse/";toolbar.btns = {status: {number: false,},info: {number: {caption: "仓库编号",url: `${btnIconBaseUrl}warehouse_code.png`},}}for (const btnKey in toolbar.btns.status) {toolbar.addImageBoolean(toolbar.btns.status, btnKey).caption(toolbar.btns.info[btnKey]["caption"]).url(toolbar.btns.info[btnKey]["url"]).on("change", function (toolbarBtnStatus) {console.log(btnKey, toolbarBtnStatus);gui_btn_onchange(btnKey, toolbar.btns.info[btnKey]["caption"], toolbarBtnStatus);});}}function gui_btn_onchange(btnKey, status) {// TODO}
三、创建仓库控制类
1. 控制粮仓顶牌显隐
// 3.1 设置粮仓控制类class SiloHouseCtrl {constructor(siloHouse) {// 保存粮仓对象this.siloHouse = siloHouse;// 保存控制对象siloHouse.ctrl = this;// 初始化(模拟)数据this.mockData();// UI 面板对象this.panel = null;this.ui = null;}// 模拟数据mockData() {const that = this;this.data = {number: this.siloHouse.name,temper: "26℃",humi: "35%",power: "15kWh",store: "70%",info: {基本信息: {品种: Math.ceil(Math.random() * 2) == 1 ? "小麦" : "玉米",库存数量: Math.ceil(Math.random() * 9000) + "",报关员: Math.ceil(Math.random() * 2) == 1 ? "张三" : "李四",入库时间: Math.ceil(Math.random() * 2) == 1 ? "11:24" : "19:02",用电量: Math.ceil(Math.random() * 100) + "",单仓核算: "无",},粮情信息: {仓房温度: Math.ceil(Math.random() * 27 + 25) + "",粮食温度: Math.ceil(Math.random() * 25 + 20) + "",},报警信息: {火灾: "无",虫害: "无",},}}setInterval(() => {that.data.temper = `${(20 + Math.random() * 10).toFixed(2)}℃`;that.data.humi = `${(30 + Math.random() * 10).toFixed(2)}%`;that.data.power = `${(Math.random() * 20).toFixed(2)}kWh`;that.data.store = `${(Math.random() * 100).toFixed(2)}%`;}, 1000);}// 3.1.1 创建顶牌界面createUI(width = 140) {// 创建一个面板,并可向该面板中添加组件。this.panel = new THING.widget.Panel({template: "default",// 设置面板角标cornerType: "s2c5",width: `${width}px`,isClose: false,opacity: 0.8,media: true,});// 创建顶牌,顶牌内容为前面的面板this.ui = app.create({type: "UI",parent: this.siloHouse,el: this.panel.domElement,});// 返回面板实例,方便后续设置内容return this.panel;}// 3.1.2 隐藏顶牌界面hideUI() {if (this.panel) {this.panel.destroy()this.panel = null;}if (this.ui) {this.ui.destroy()this.ui = null;}}// 3.1.3 显示顶牌toggleUI(dataKey, title, show = false) {// 先隐藏前一种this.hideUI();// 如果是显示创建并显示 panelif (show) {// console.log(this, dataKey, title);// ⭐此处调用了 panel 的 add 方法创建文本控件,传入数据对象和数据键值,然后设置 panel 的标题this.createUI().addString(this.data, dataKey).name(title);}}}
2. 修改初始化事件
在场景加载完成后,先创建 2D 界面,接着获取操作物体对象集合,并将 2D UI 按钮的事件回调设置为控制物体对象。
// 1.2 初始化事件app.on("load", (e) => {init_gui();init();});// 2.1 初始化 2D UIlet toolbar;function init_gui() {// ...}// 2.2 通栏按钮响应回调function gui_btn_onchange(btnKey, caption, status = false) {// 关闭其他按钮for (const k in toolbar.btns.status) {if (["cloud", "location", btnKey].indexOf(k) > -1) {continue;}toolbar.btns.status[k] = false;}// 3.2.2 粮仓面板信息展示切换if (["number", "temper", "humi", "power", "store"].indexOf(btnKey) > -1) {siloHouseCtrlList.forEach((siloHouseCtrl) => {siloHouseCtrl.toggleUI(btnKey, caption, status);// 隐藏粮仓模型展示粮食储量模型// ❗不能直接设置不可见,visible 的隐藏会使子物体一起隐藏// 应使用 opacity 透明度,模拟“隐藏”// siloHouseCtrl.siloHouse.visible = !status;let opacity = 1;if (btnKey === "store") {opacity = status ? 0 : 1;}siloHouseCtrl.siloHouse.style.opacity = opacity;});}}// 3.2.1 初始化事件const siloHouseCtrlList = []; // 粮仓控制对象集合function init() {// 调整摄像机初始位置及视角app.camera.flyTo({position: [-182.16900300883736, 53.24677728392183, 72.21965470775368],target: [-68.1412926741533, -18.16319203074775, -23.30416731768694],time: 1500,});// 获取场景中粮仓对象集合,并为每一个粮仓对象创建控制类app.query("[物体类型=粮仓]").forEach(function (siloHouse) {siloHouseCtrlList.push(new SiloHouseCtrl(siloHouse));});}
3. 单击粮仓模型事件
单击粮仓模型时,模型显示选中效果(橙色勾边)并展示详细信息。修改粮仓控制类:
// 3.1 设置粮仓控制类class SiloHouseCtrl {constructor(siloHouse) {// ...// 3.3 粮仓模型单击事件this.selected = false;// this.infoPanel = null; 设置为全局单例,绑定到粮仓控制类上SiloHouseCtrl.infoPanel = null;this.initClickEvent();}// ...// 3.3.1 初始化监听单击事件initClickEvent() {this.siloHouse.on("click", (e) => {const { button } = e;// 左键单击if (button === 0) {// 根据当前是否选中然后切换选中状态this.toggleSelected();}});}// 3.3.2 切换选中状态toggleSelected() {// 切换勾边this.siloHouse.style.outlineColor = this.selected ? null : "orange";// console.log(SiloHouseCtrl.infoPanel, this.selected);// 先销毁全局粮仓信息面板if (SiloHouseCtrl.infoPanel) {SiloHouseCtrl.infoPanel.destroy();SiloHouseCtrl.infoPanel = null;}// 如果是未选中改为选中,新创建全局粮仓信息面板if (!this.selected) {SiloHouseCtrl.infoPanel = new THING.widget.Panel({width: '350px',isClose: true,isDrag: true,hasTitle: true,name: this.siloHouse.name,// 设置面板坐标位置(相对世界的绝对坐标)position: [300, 50, 9999999],});SiloHouseCtrl.infoPanel.addTab(this.data.info);// TODO: 设置 z-index 无效// SiloHouseCtrl.infoPanel.setZIndex(999999);}// 记录新选中状态this.selected = !this.selected;}}
4. 双击粮仓模型事件
双击粮仓模型时,模型屋顶切换打开/关闭(本质是移动模型位置),修改粮仓控制类:
// 3.1 设置粮仓控制类class SiloHouseCtrl {constructor(siloHouse) {// ...// 3.3 粮仓模型点击事件this.initClickEvent();// 单击this.selected = false;// this.infoPanel = null; 设置为全局单例,绑定到粮仓控制类上,保证只打开一个面板SiloHouseCtrl.infoPanel = null;// 双击// 全局单例,保证只打开一个屋顶SiloHouseCtrl.opendRoof = null;// 保存屋顶方便后续操作this.roof = this.siloHouse.query("/gaizi")[0];// 记录屋顶原始位置this.roof.originPos = [].concat(this.roof.position);}// 模拟数据mockData() {// ...}// 3.1.1 创建顶牌界面createUI(width = 140) {// ...}// 3.1.2 隐藏顶牌界面hideUI() {// ...}// 3.1.3 显示顶牌toggleUI(dataKey, title, show = false) {// ...}// 3.3.1 初始化监听事件initClickEvent() {/*** ⭐如果用到双击事件是需要严格区分 click 与 singleClick* 否则 click 会响应 dblclick*/// 单击事件this.siloHouse.on("singleClick", (e) => {const { button } = e;// 左键单击if (button === 0) {// 根据当前是否选中然后切换选中状态this.toggleSelected();}});// 双击事件this.siloHouse.on("dblclick", (e) => {const { button, object } = e;// 左键双击if (button === 0) {// 切换屋顶打开状态this.toggleRoof();}})}// 3.3.2 单击切换选中状态toggleSelected() {// ...}// 3.4.1 双击切换屋顶打开/关闭toggleRoof() {// 如果有已经打开的先关闭if (SiloHouseCtrl.opendRoof) {SiloHouseCtrl.opendRoof.moveTo({position: SiloHouseCtrl.opendRoof.originPos,time: 400,});// 如果本来就是当前双击的已打开,关闭后不再打开新的if (SiloHouseCtrl.opendRoof === this.roof) {SiloHouseCtrl.opendRoof = null;return;}}// 记录新打开的屋顶SiloHouseCtrl.opendRoof = this.roof;// 获取原始位置const [x, y, z] = SiloHouseCtrl.opendRoof.originPos;// 移动到新位置SiloHouseCtrl.opendRoof.moveTo({position: [x, y + 20, z],time: 400,});}}
四、监控摄像头控制类
需求:
- 点击 2D UI 通栏的按钮,所有摄像头展示顶牌,再次点击可隐藏;
- 点击摄像头的顶牌,显示播放面板; ```javascript
// 2.2 通栏按钮响应回调 function gui_btn_onchange(btnKey, caption, status = false) { // … // 4.2 切换云台监控 Maker 展示 if (btnKey === “video”) { videoCtrlList.forEach((videoCtrl) => { videoCtrl.toggleMaker(status); }); } }
// 3.2.1 初始化事件 // 粮仓控制对象集合 const siloHouseCtrlList = []; const videoCtrlList = []; function init() { // … // 获取场景中云台监控对象集合,并为每一个云台监控对像创建控制类 app.query(“[物体类型=摄像头]”).forEach(function (video) { videoCtrlList.push(new VideoCtrl(video)); }); }
// 4.1 创建云台监控控制类
class VideoCtrl {
constructor(video) {
// 保存云台监控对象
this.video = video;
// 保存云台控制对象
this.video.ctrl = this;
// 创建顶牌
this.marker = app.create({
type: “Marker”,
offset: [0, 3.5, 0],
size: 10,
url: “https://www.thingjs.com/static/images/sliohouse/videocamera3.png“,
parent: this.video,
});
// 初始化记录播放视频面板
VideoCtrl.videoPanelList = [];
// 点击顶牌时,切换监控视频面板
this.marker.on(“singleClick”, () => {
this.togglePanel();
});
// 默认隐藏顶牌
this.toggleMaker();
}
// 切换顶牌显隐
toggleMaker(vis = false) {
this.marker.visible = vis;
this.video.style.color = vis ? “orange” : null;
this.video.style.glow = vis ? true : false;
// 隐藏摄像头时顶牌时,销毁所有视频播放
if (!vis) {
VideoCtrl.videoPanelList.forEach((videoPanel) => {
videoPanel.close();
});
}
}
// 销毁监控视频播放面板
destroyPanel() {
const videoPanelList = [].concat(VideoCtrl.videoPanelList || []);
const index = videoPanelList.indexOf(this.videoPanel);
// 已经创建,销毁
if (this.videoPanel && index > -1) {
videoPanelList.splice(index, 1);
this.videoPanel.destroy();
this.videoPanel = null;
}
VideoCtrl.videoPanelList = videoPanelList;
}
// 创建监控视频播放面板
createPanel() {
const videoPanelList = [].concat(VideoCtrl.videoPanelList || []);
// 创建一个新空 Panel
const panel = new THING.widget.Panel({
template: “default2”,
// width: “450px”,
isClose: true,
isDrag: true,
media: true,
hasTitle: true,
name: this.video.name,
});
// 给新面板创建视频播放
panel
.addIframe({ iframe: true }, ‘iframe’)
.caption(‘’) // Panel 已经有标题了, iframe 无需重新设置标题
.iframeUrl(“https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4“);
// ⭐position 的更改必须设置在 panel 创建之后
panel.setPosition({
top: 0,
left: app.domElement.offsetWidth - panel.domElement.offsetWidth,
});
// ⭐绑定面板关闭事件,关闭时销毁播放器面板
panel.on(“close”, () => {
this.destroyPanel();
});
// 保存已创建的新面板
this.videoPanel = panel;
videoPanelList.push(panel);
VideoCtrl.videoPanelList = videoPanelList;
}
// 切换监控视频播放面板
togglePanel() {
// 已存在,销毁
if (this.videoPanel) {
this.destroyPanel();
} else {
// 每超出线路数,创建
if (VideoCtrl.videoPanelList.length === 9) {
// 创建提示
alert(性能不足!最多支持9路摄像头播放。);
return;
}
this.createPanel();
}
}
}
<a name="qe5aT"></a>### 五、人车对象控制类```javascript// 5.1 创建人车定位控制类class LocationCtrl {constructor(type) {this.ui = null;this.panel = null;this.obj = null;this.createObject(type);}createObject(type) {if (type === "worker") {// 创建工人this.obj = app.create({type: 'Thing',name: '工人',url: '/api/models/3a1f327991084d25a1dd362917f0b347/0/gltf/',position: [0, 0, 0],angle: 180,scale: [0.1, 0.1, 0.1],userData: {物体类型: type,info: {编号: 9527,名称: '张三',职位: '巡检员',年龄: 25,}},complete: function () {console.log(`${type} thing created: ${this.id}`);}});}if (type === "truck") {// 创建卡车this.obj = app.create({type: 'Thing',name: 'truck',url: 'https://www.thingjs.com/static/models/truck',position: [0, 0, 0],angle: 0,userData: {物体类型: type,info: {编号: 89757,车牌: '渝N.B74110',种类: '货车',使用年限: 2,}},complete: function () {console.log(`${type} thing created: ${this.id}`);}});}if (this.obj) {this.obj.ctrl = this;this.moveObj();}}moveObj() {const path = ["L109","L110","L104","L103","L102","L108","L109","L118","L119","L112","L111","L117","L118",].map((point) => {const pObj = app.query(point)[0];return pObj && pObj.position;}).filter((pos) => pos && pos.length === 3);this.obj.movePath({path,orientToPath: true,orientToPathDegree: this.obj.name === "truck" ? 180 : 0,speed: this.obj.name === "truck" ? 20 : 5,delayTime: 500,lerp: false,loop: true,});}createUI() {this.panel = new THING.widget.Panel({template: 'default',width: '180px',// cornerType: "polyline",offset: [0, 0, 0],pivot: [0, 0],isClose: false,opacity: 0.8,});for (const dataKey in this.obj.userData["info"]) {this.panel.addString(this.obj.userData["info"], dataKey);}this.ui = app.create({type: "UI",el: this.panel.domElement,parent: this.obj,});}destroyUI() {if (this.panel) {this.panel.destroy();this.panel = null;}if (this.ui) {this.ui.destroy();this.ui = null;}}toggleUI() {if (this.panel || this.ui) {this.destroyUI();} else {this.createUI();}}}
