ThingJS学习笔记

1.现在模摸搭上建模这个需要专业人员操作

切记需要操作的物品或者建筑一定要设置属性,要不然无法操作

2,建模完毕后要对模型进行2次开发需要到ThingJS控制台上操作

ThingJS学习笔记_第1张图片

 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 场景中可以加载园区,加载后系统自动创建了园区、建筑、楼层、房间等物体对象,这些对象也自然把场景分成了不同的层级

ThingJS学习笔记_第2张图片

场景和园区

当我们使用 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 提供了两套层级体系:父子树、分类对象属性树。

ThingJS学习笔记_第3张图片

如您所见,场景会有一个根物体,可通过 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 又针对每类对象提供了一些内置属性。

ThingJS学习笔记_第4张图片

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 搭建一个园区后,我们可以用插件设置场景在地图上面的位置。

ThingJS学习笔记_第5张图片

场景同步过去之后,我们可以通过代码获取场景在地图中摆放的经纬度数据。

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;
			}
		}
	})
}

 

 

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
    });
}
}); 

以子节点作为基准连接

如果一个物体的模型是由多个“子节点”组合而成的

效果如下:

ThingJS学习笔记_第6张图片

那么我们也可以基于某个“子节点”设置该模型与所连接的父物体的相对位置关系,如下:

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);
...... 

组件方法介绍

组件通用方法:

 

 

你可能感兴趣的:(thingjs)