基於 ThingJs OOP 編程嘗試

成功的含義不在於要獲得什麼,而在於你從那個奮鬥的起點走了多遠。 ---《心靈捕手》
複製代碼

前言

原本計劃是最近好好研究下 Vue ,但是項目一個接一個,需求開發有點吃力效率還通常。做爲一個又高又帥的全棧特別不爽啊,擠時間把關於 ThingJs 資料所有過了一遍,滿滿的乾貨拉回家啊。其實 ThingJs 文檔作的挺全,之前沒仔細看過,還吐槽了,🤦🤦,我錯了。html

早上6:30起牀,忙活到 16:22,終於搞定。順利寫完內心仍是挺開心的。😬前端

下一階段封裝一套採集工具出來。又是一個執念啊。☹️jquery

ThingJs 文檔中心es6

ThingJs Apiajax

Thingjs 在線開發 Demojson

面臨的問題

面向過程的編碼散落在業務邏輯中很差維護。嘗試了一下 OOP ,還滿意。後端

攝影機飛向物體,達不到滿意的效果。 基於物體自身座標系換算世界座標。api

先後端分離的狀況下,須要依賴後端服務才能進行操做。Mock 數據,攔截 ajax 請求。app

方案嘗試

攝影機,想飛哪,就飛那

蒙多,想去哪就去哪。😄😄前後端分離

座標系

飛以前,先了解下座標系,否則迷失茫茫宇宙沒法自拔啊。

  • 世界座標系

每一個物體都會有對應的世界座標系。攝影機控制主要基於世界座標。

thingjs 官網教程圖,勿怪啊

在 ThingjJs 中,座標系如圖所示。參照物爲場景中心點(猜想)。 app.query('.Car')[0].position 獲取世界座標。

  • 物體父級座標系

還沒來得及嘗試,先跳過,有經驗以後來補。

  • 物體自身座標系

物體自身座標系在攝影機飛行的時候頗有用。基於物體自身座標系,很容易獲得相對於物體的一個位置座標,而後利用 api 將物體自身座標系的座標,轉換爲世界座標(攝影機位置 position)。而後 target 爲物體的座標。

終於能夠飛了。

座標轉換
// 將輸入的物體自身座標系下座標轉換成世界座標
obj.selfToWorld(pos) 
// 將輸入的世界座標標轉換成物體自身座標系下座標
obj.worldToSelf(pos) 
// 將輸入的世界座標轉換成父物體座標
obj.worldToLocal(pos)
// 將輸入的父物體座標轉換成世界座標
obj.localToWorld(pos)  
複製代碼
app.camera.flyTo({
    // 物體在世界座標系座標
    position: position,
    // 基於物體的相對位置轉換的世界座標。obj.selfToWord([-2,2,2])。這樣攝影機就會飛向物體右前方(以本身做爲參考系)。
    target: target,
    time: 1000,
});
app.camera.flyTo({
    target: target,
    xAngle: 30, //繞物體自身X軸旋轉角度
    yAngle: 60, //繞物體自身Y軸旋轉角度
    radiusFactor: 3, //物體包圍盒半徑的倍數
    //radius: 3, //距離物體包圍盒中心的距離 單位米
    time: 1000,
});
複製代碼

MMP,原本想寫個簡單的 demo,寫着寫着又去看 ThingJs 代碼去了。😭😭😭

座標系 api 沒中不足的就是沒有所有能夠用 安培定則記憶。右手大拇指指向正方向,四根手指指向旋轉的正方向。

camera.rotateX(xAngle);符合安培定則

camera.rotateY(yAngle);符合安培定則

app.camera.flyTo({yAngle: 60});符合安培定則

app.camera.flyTo({xAngle: 60});不符合安培定則,強迫症感受特別不適啊。😄

Demo,能夠在 ThingJs 在線開發運行

window.uino=window.uino||{};
// 建立一個地球
const app = new THING.App({
    // 指定 3d 容器 div標籤 的id屬性值
    container: 'div3d',
    url: 'https://www.thingjs.com/static/models/storehouse',
    // url: `${uino.sceneUrl}public/thingjs/scene/jail`,
    // 模型庫 url
    // loaderResourceUrl: `${uino.modelsUrl}public/thingjs/models/jail`,
    // 天空盒
    // skyBox: './images/blueSky',
    // 加載模型庫的時候是否加載最高級別的,依照場景文件版本號加載
    enableUseHighestVersionResource: false
});
uino.app = app;
// 獲取特定小車
const getCar = function _getCar(name = 'car01') {
    if (uino.car) {
        return uino.car;
    }
    uino.car = uino.app.query(name)[0];
    return uino.car;
};
// 監聽場景加載完成
uino.app.on('load', ev => {
    const car = getCar();
    // 改變小車顏色
    car.style.color = 'red';
    // 顯示包圍盒
    car.style.boundingBox = true;
    // 設置包圍盒顏色
    car.style.boundingBoxColor = 'rgb(255,0,0)';
    app.level.change(ev.campus);
});
// 信息面板
const panel = new THING.widget.Panel({
    titleText: '攝影機屬性',
    hasTitle: true,
    position: [200, 5],
    width: 230
});
const camera = uino.app.camera;
const panelData = {
    distance: camera.distance,
    position: camera.position,
    target: camera.target,
    xAngle: camera.cameraObject.angleX,
    yAngle: camera.cameraObject.angleY,
    zAngle: camera.cameraObject.angleZ
};
panel.addString(panelData, 'distance').caption('攝影機距離物體距離');
panel.addString(panelData, 'position').caption('攝影機世界座標');
panel.addString(panelData, 'xAngle').caption('xAngle');
panel.addString(panelData, 'yAngle').caption('yAngle');
panel.addString(panelData, 'zAngle').caption('zAngle');
const updatePanelData = function _updatePanelData(camera) {
    panelData.distance = camera.distance;
    panelData.position = camera.position;
    panelData.target = camera.target;
    panelData.xAngle = camera.cameraObject.angleX;
    panelData.yAngle = camera.cameraObject.angleY;
    panelData.zAngle = camera.cameraObject.angleZ;
};
// 小車自身座標系,x 每次點擊改變
// 小車自身座標系,y 每次點擊改變
// 小車自身座標系,z 每次點擊改變
const [objSelfX, objSelfY, objSelfZ] = [1, 1, 1];
// 繞小車 x 軸,每次旋轉
// 繞小車 y 軸,每次旋轉
// 繞小車 z 軸,每次旋轉
const [xRotate, yRotate] = [10, 10];
// 攝影機位置 基於世界座標系
const cameraPosition = app.camera.tempComplete;
// 相對於紅色小車自身座標系的偏移
let [x, y, z] = [0, 0, 0];
// 旋轉角度的偏移量
let [xAngle, yAngle, zAngle] = [0, 0, 0];
// 攝影機基於小車自身座標系 x 軸移動
const addCameraPositionX = function _changeCameraPositionX() {
    x = x + objSelfX;
    updateCameraPosition();
};
// 攝影機基於小車自身座標系 x 軸移動
const reduceCameraPositionX = function _changeCameraPositionX() {
    x = x - objSelfX;

    updateCameraPosition();
};
// 攝影機基於小車自身座標系 y 軸移動
const addCameraPositionY = function _changeCameraPositionY() {
    y = y + objSelfY;
    updateCameraPosition();
};
// 攝影機基於小車自身座標系 y 軸移動
const reduceCameraPositionY = function _changeCameraPositionY() {
    y = y - objSelfY;
    updateCameraPosition();
};
// 攝影機基於小車自身座標系 z 軸移動
const addCameraPositionZ = function _changeCameraPositionZ() {
    z = z + objSelfZ;
    updateCameraPosition();
};
// 攝影機基於小車自身座標系 z 軸移動
const reduceCameraPositionZ = function _changeCameraPositionZ() {
    z = z - objSelfZ;
    updateCameraPosition();
};
// 更新攝影機飛向
const updateCameraPosition = function _updateCameraPosition({ targetObj = getCar(), xAngle = 0, yAngle = 0, radiusFactor = 1.5, radius } = {}) {
    let obj = null;
    if (arguments.length > 0) {
        obj = { target: targetObj, xAngle, yAngle, radiusFactor };
    } else {
        const nowPosition = targetObj.selfToWorld([x, y, z]);
        obj = {
            // 飛行時間
            time: 1000,
            // 攝影機位置
            position: nowPosition,
            // 小車位置
            target: targetObj.position
        };
    }
    obj.complete = function _complete(ev) {
        // 更新面板數據
        updatePanelData(uino.app.camera);
    };
    uino.app.camera.flyTo(obj);
};
// 預先找好的復原視角位置
const reset = function _reset() {
    const car = getCar();
    const position = [23.416928429425614, 10.920238566451447, 19.87585306710976];
    uino.app.camera.flyTo({
        time: 1000,
        // 攝影機位置
        position: position,
        // 小車位置
        target: car.position,
        complete() {
            updatePanelData(uino.app.camera);
        }
    });
};
// 攝影機繞 x 軸旋轉
const addRotateCameraPositionX = function _addRotateCameraPositionX() {
    // uino.app.camera.rotateX(xAngle);
    xAngle = xAngle + xRotate;
    updateCameraPosition({ xAngle, yAngle });
}; // 攝影機繞 x 軸旋轉
const reduceRotateCameraPositionX = function _reduceRotateCameraPositionX() {
    // uino.app.camera.rotateX(-xAngle);
    xAngle = xAngle - xRotate;
    updateCameraPosition({ xAngle, yAngle });
};
// 攝影機繞 y 軸旋轉
const addRotateCameraPositionY = function addRotateCameraPositionY() {
    // uino.app.camera.rotateY(yAngle);
    yAngle = yAngle + yRotate;
    updateCameraPosition({ xAngle, yAngle });
};
// 攝影機繞 y 軸旋轉
const reduceRotateCameraPositionY = function _reduceRotateCameraPositionY() {
    // uino.app.camera.rotateY(-yAngle);
    yAngle = yAngle - yRotate;
    updateCameraPosition({ xAngle, yAngle });
};

new THING.widget.Button(`復位`, reset);
new THING.widget.Button(`紅車自身 x 軸 + ${objSelfX}`, addCameraPositionX);
new THING.widget.Button(`紅車自身 x 軸 - ${objSelfX}`, reduceCameraPositionX);
new THING.widget.Button(`紅車自身 y 軸 + ${objSelfX}`, addCameraPositionY);
new THING.widget.Button(`紅車自身 y 軸 - ${objSelfX}`, reduceCameraPositionY);
new THING.widget.Button(`紅車自身 z 軸 + ${objSelfX}`, addCameraPositionZ);
new THING.widget.Button(`紅車自身 z 軸 - ${objSelfX}`, reduceCameraPositionZ);
new THING.widget.Button(`紅車 x 軸旋轉 + ${xRotate}`, addRotateCameraPositionX);
new THING.widget.Button(`紅車 x 軸旋轉 - ${xRotate}`, reduceRotateCameraPositionX);
new THING.widget.Button(`紅車 y 軸旋轉 + ${yRotate}`, addRotateCameraPositionY);
new THING.widget.Button(`紅車 y 軸旋轉 - ${yRotate}`, reduceRotateCameraPositionY);
複製代碼

Mock 排除依賴,獨立部署

mock.js 攔截 ajax (JQuery),排除後端依賴

研究半天,仍是沒有通,早上六點半到如今,牙都沒刷,飯也沒吃,執念啊。😭😭🙄🙄 幸虧,最後寫這個,MMP,要不晚上都睡不着了。

單獨 mock 數據沒有問題,可是加入到 thingjs 中就會報錯。難道攔截到 thingjs 權限驗證的接口了?

setInterval(() => {
    $.ajax({
        url: '/api/alarm',
        type: 'get',
        dataType: 'json',
        success(data) {
            if (data.code == 200) {
                console.log(data);
            }
        },
        error(error) {
            console.error(error);
        }
    });
}, 2000);
Mock.setup({ timeout: 4000 });
Mock.mock('/api/alarm', 'get', {
    'data|1-5': [
        {
            name: '測試建立攝像頭',
            'color|1': ['red', 'yellow', 'green']
        }
    ],
    code: 200
});

複製代碼

基於 ThingJs mock 方案

當須要部署的時候,直接將 js 文件去掉便可。開發或演示的時候,只需部署前端。

  • jquery.mockjax.js 結合 mock.js 排除後端接口依賴
let interval;
uino.thingjsUtil.createWidgetButton(
    '開啓查詢告警信息',
    ev => {
        interval = setInterval(() => {
            const video = createVideo();
            $.ajax({
                url: '/fly/alarm',
                type: 'get',
                dataType: 'json',
                success(data) {
                    const i = Math.round(Math.random() * 10);
                    data.data.forEach((item, index) => {
                        if (i != index) {
                            return;
                        }
                        if (video) {
                            video.triggerAlarm(item);
                        }
                    });
                },
                error(error) {
                    console.error(error);
                }
            });
        });
    },
    10000
);
uino.thingjsUtil.createWidgetButton('關閉查詢告警信息', ev => {
    if (interval) {
        clearInterval(interval);
    }
});
複製代碼
  • jquery.mockjax.js 結合 mock.js 攔截 ajax 而且模擬數據返回。
$.mockjax({
    url: '/fly/alarm',
    contentType: 'application/json',
    responseText: Mock.mock({
        'data|10': [
            {
                name: '測試建立攝像頭',
                'color|1': [
                    'red',
                    'yellow',
                    'green',
                    '#FF34B3',
                    '#F0F8FF',
                    '#ADFF2F',
                    '#9400D3',
                    '#1A1A1A',
                    '#008B8B',
                    '#00FF00'
                ]
            }
        ],
        code: 200
    })
});

複製代碼

ThingJs OOP 嘗試

面向對象三大特徵,封裝,繼承,多態。es6 以後,基於 class 實現的繼承能夠知足需求了。想實現多重繼承也能夠,本身再封裝一下。 多態呢就比較坑了,父類引用指向子類對象,運行時期動態執行代碼。js 呢作爲弱類型語言,想實現這個只能本身封裝了,不過之前看文章好像有個開源庫能幫忙作到了,ts 好像能夠(猜想,對 ts 不瞭解,只是有印象)。 可是 js 基於原型實現繼承,多態的特性也能夠用。

代碼驗證 OOP 的可行性

A,B,C,D,E,F

B,C 繼承 A

D 繼承 B

E 繼承 C

F 繼承 A

class A {
    run() {
        console.log('A');
    }
}
class B extends A {
    run() {
        console.log('B');
    }
}
class C extends A {
    run() {
        console.log('C');
    }
}
class D extends B {
    run() {
        console.log('D');
    }
}
class E extends C {}
class F extends A {}
const client = function _client(a) {
    if (a instanceof A) {
        a.run();
        return;
    }
    throw new Error('傳入變量類型有誤');
};
const a = new A();
const d = new D();
const b = new B();
const c = new C();
const e = new E();
const f = new F();
console.log(a instanceof A); // true
console.log(d instanceof A); // true
console.log(d instanceof B); // true
console.log(c instanceof A); // true
console.log(b instanceof A); // true
console.log(e instanceof A); // true
console.log(e instanceof C); // true
console.log(f instanceof A); // true
client(a); // A
client(b); // B
client(c); // C
client(d); // D
client(e); // C
client(f); // A

複製代碼

擴展 Campus,Building,Floor,Room

ThingJs 場景中咱們常常操做的是 Campus,Building,Floor,Room。這是類呢是 ThingJs 原生的,可是也是能夠擴展的。

class Building extends THING.Building {
    /** * @author: 張攀欽 * @description: 給建築描邊 */
    changOutLineColor() {
        this.style.color = '#00ff00';
    }
    enterBuilding() {
        this.app.level.change(this);
    }
    enterFloorFromBuilding(floorNum) {
        this.app.level.change(this.getFloor(floorNum));
    }
    getFloor(floorNum) {
        return this.floors[floorNum];
    }
}
THING.factory.registerClass('Building', Building);
複製代碼
使用的時候呢,就爽歪歪了,obj.enterBuilding();便可進入建築立面了;
app.query('.Building')[0].enterBuilding();
複製代碼

擴展搭建場景時填寫自定義屬性_TYPE_

// scene.json 場景文件
{
    "id": "1444",
    "userid": "Cabinet_01",
    "type": "Cabinet",
    "name": "Cab",
    "properties": {
        "Group": "Inside01"
    },
    "model": 17,
    "position": [-5.087, 0.01, 9.347],
    "rotation": [0.0, 0.707, 0.0, 0.707],
    "scale": [0.998, 1.0, 0.999]
}
class Cabinet extends THING.Thing {

    changOutLineColor() {
        this.style.color = '#00ff00';
    }
}
THING.factory.registerClass('Cabinet', Cabinet);
// 給對象描邊
app.query('.Cabinet')[0].changOutLineColor();
複製代碼

id 對應 thingjs 對象 uuid 屬性。

userid 對應搭建時候填寫的 [自定義 ID]。對應 thingjs 物體對象 id 屬性。

name 對應搭建時填寫的 [名稱]。對應 thingjs 物體對象 name 屬性。

model 爲查找當前物體所用模型,對應 scene.json 中 models 中的索引。

properties 爲自定義的屬性

type 爲自定義屬性添加TYPE(TYPE兩邊有_)。THING.factory.registerClass('Cabinet', Cabinet); app.query('.Cabinet');能夠查到這個物體。

加載場景以後新建立物體及監聽自定義事件

class Video extends THING.Thing {
    constructor(app = uino.app) {
        // 不傳 app 報錯。
        super(app);
        this._init();
    }
    // 調用建立對象須要綁定的事件之類的數據
    _init() {
        this.on('alarm', ev => {
            this.style.color = 'red';
        });
    }
    changOutLineColor() {
        this.style.color = '#00ff00';
    }
     // 觸發告警事件
    triggerAlarm(eventData, tag) {
        this.trigger(AlarmManage.eventType, eventData, tag);
    }
}
THING.factory.registerClass('Video', Video);

class VideoFactory {
    static createVideo(obj) {
        return VideoFactory.app.create(obj);
    }
    static getVideos() {
        return VideoFactory.app.query('.Video');
    }
}
VideoFactory.app = uino.app;

 const videoPro = {
        id: THING.Utils.generateUUID(),
        name: '測試建立攝像頭',
        type: 'Video',
        url: 'https://model.3dmomoda.com/models/335472acb6bb468ead21d1a8d9a2d24e/1/gltf',
        position: [-5, 0, 7],
        scale: [50, 50, 50],
        complete(ev) {
            console.log('建立物體:', ev.object);
        }
    };
    
    const video = VideoFactory.createVideo(videoPro);
    // 便可對物體進行描邊
    video.changOutLineColor();
複製代碼

一般呢,建立的物體有業務邏輯。好比攝像頭可能會有監測功能,當觸發告警規則以後呢,須要在 3D 中進行特殊展現。

當 ajax 請求後臺告警數據時,就能夠知道那個攝像頭告警了。
const video = uino.app.query('#id')[0];
// 對象觸發的只會當前對象監聽到
video.triggerAlarm({ name: '寧姚', native: '倒懸山' });
// 全局觸發的不會傳遞到對象上
AlarmManage.globalTriggerAlarm({ name: '陳平安', native: '落魄山' }, 'app-global');
});
uino.app.on('Alarm', ev => {
    console.log('全局事件', ev);
});
// 最近小說的男女主不停撒狗糧啊,心痛啊。幸虧對象還能夠 new。🤦🤦
複製代碼

基於物體類型爲 Thing 的對象進行設置原型,從而能夠進行 OOP 。

const videos=app.query(/.*video.*/);
videos.forEach(item=>THING.Utils.convertObjectClass(item, 'Video'));

// 根據物體類型進行查找物體,而後調用原型方法
app.query('.Video')[0].changOutLineColor();
複製代碼
相關文章
相關標籤/搜索