需求分析
- 三维场景加载
- 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.Pannel
object.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();
// 如果是显示创建并显示 panel
if (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 UI
let 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();
}
}
}