Potree 002 Desktop开发环境搭建

1、工程创建

我们使用Visual Studio 2022开发,把下载好后的PotreeDesktop源码添加到Visual Studio中。

打开Visual Studio 2022,新建Asp.Net Core空项目,如下图所示。

Potree 002 Desktop开发环境搭建插图

点击下一步按钮,设置项目的名称、存储路径以及解决方案名称等。如下图所示。

Potree 002 Desktop开发环境搭建插图1

点击下一步按钮,最后点击创建按钮,完成创建工作。创建后的根目录如下图所示。

Potree 002 Desktop开发环境搭建插图2

打开WOBM.Potree.Web目录,里面的内容如下图所示。

Potree 002 Desktop开发环境搭建插图3

创建的工程在Visual Studio中的工程树如下图所示。

Potree 002 Desktop开发环境搭建插图4

接下来,我们把PotreeDesktop下的源码,拷贝到WOBM.Potree.Web目录下,拷贝后,文件夹组织如下图所示。

Potree 002 Desktop开发环境搭建插图5

从Visual Studio的工程树上,通过添加、删除和项目中排除等方法,最终工程树的组织如下图所示。

Potree 002 Desktop开发环境搭建插图6

目录上不需要的内容,也可以删除,最终保留的内容如下图所示。

Potree 002 Desktop开发环境搭建插图7

此时,双击PotreeDesktop.bat,如果系统能够成功启动,那么开发环境就算搭建成功了,接下来就可以在Visual Studio写我们自定义的html和js代码了。

2、系统启动过程

系统是从PotreeDesktop.bat开始启动的,里面的内容如下所示。

start ./node_modules/electron/dist/electron.exe ./main

系统从electron.exe启动,后面跟了一个参数,参数启动了main.js文件。在main.js中有下面一句代码。

mainWindow.loadFile(path.join(__dirname, 'index.html'));

该代码的意思,是在mainWindow中加载index.html文件,所以index.html页面是我们展示的主页面。打开index.html页面,我们可以看到,该页面就是我们平常开发的Web页面,把这个页面单独放到Web服务器,然后通过浏览器访问也是可以的。

在index.html页面中,我们看到了熟悉的代码,例如对其他js库的引用,html元素的定义和一些js代码。js代码中,定义了Potree.Viewer,也就是显示点云的主UI。

整个执行流程如下,通过PotreeDesktop.bat启动electron.exe,electron.exe加载解析main.js文件,在main.js文件中设置加载index.html页面,至此完成了我们开发的主页面的加载。

3、Main.js文件内容介绍

在该文件的开始,先定义了一些变量,代码如下。

const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const Menu = electron.Menu;
const MenuItem = electron.MenuItem;
const remote = electron.remote;
const path = require('path')
const url = require('url')
let mainWindow

通过这段代码,获取了对electron的引用,app为系统的主App,BrowserWindow是主对话框,Menu是主对话框中的菜单。接下来对app进行操作。

app.on('ready', createWindow)
app.on('window-all-closed', function () {
    if(process.platform !== 'darwin') {
         app.quit()
    }
})
app.on('activate', function () {
    if(mainWindow === null) {
         createWindow()
    }
})

当app准备好之后,系统会调用createWindow函数。当所有的窗口关闭后,调用app的quit函数。当app被激活的时候,如果当前的mainWindow为null的话,调用createWindow函数。

下面我们顺藤摸瓜,看下定义在main.js中的createWindow函数。代码如下。

function createWindow () {
    mainWindow= new BrowserWindow({
         width:1600,
         height:1200,
         webPreferences:{
             nodeIntegration:true,
             backgroundThrottling:false,
         }
    })
    mainWindow.loadFile(path.join(__dirname,'index.html'));
    lettemplate = [
         {
             label:"Window",
             submenu:[
                  {label:"Reload", click() {mainWindow.webContents.reloadIgnoringCache() }},
                  {label:"Toggle Developer Tools", click() {mainWindow.webContents.toggleDevTools() }},
             ]
         }
    ];
    letmenu = Menu.buildFromTemplate(template);
    mainWindow.setMenu(menu);
    {
         const{ ipcMain } = require('electron');
         ipcMain.on('asynchronous-message',(event, arg) => {
             console.log(arg)// prints "ping"
             event.reply('asynchronous-reply','pong')
         })
         ipcMain.on('synchronous-message',(event, arg) => {
             console.log(arg)// prints "ping"
             event.returnValue= 'pong'
         })
    }
    mainWindow.on('closed',function () {
         mainWindow= null
    })
}

在该代码中,系统初始化了一个BrowserWindow对象,也就是初始化一个主对话框,设置了宽度为1600,宽度为1200。调用mainWindow.loadFile函数,加载Index.html页面。下面的代码,就是组织主菜单上的菜单,我们开发的时候,一般不用electron上定义的菜单,而直接在Index.html中的菜单。所以实际开发的时候,会把这段代码去掉。

最后是监听mainWindow的closed事件,当窗体关闭后,把主变量mainWindow设置为null。

4、Index.html文件内容介绍

在该文件的Head部分,系统引用了一些css,代码如下所示。






接下来引用外部的js文件。













该页面的html元素定义如下。

其中potree_render_area用来放主渲染UI对象Viewer,potree_sidebar_container用来放左侧的功能面板。

let elRenderArea = document.getElementById("potree_render_area");
let viewerArgs = {
     noDragAndDrop: true
};
window.viewer = new Potree.Viewer(elRenderArea, viewerArgs);
viewer.setEDLEnabled(true);
viewer.setFOV(60);
viewer.setPointBudget(3 * 1000 * 1000);
viewer.setMinNodeSize(0);
viewer.loadSettingsFromURL();
viewer.setDescription("");
viewer.loadGUI(() => {
     viewer.setLanguage('en');
     $("#menu_appearance").next().show();
     $("#menu_tools").next().show();
     $("#menu_scene").next().show();
     $("#menu_filters").next().show();
     viewer.toggleSidebar();
});

系统启动后,整个系统的主界面就出来了,如下图所示。

Potree 002 Desktop开发环境搭建插图8

左侧为功能面板区,是potree_sidebar_container对应的区域,右侧为点云主显示区,是potree_render_area对应的区域。系统启动后,我们会发现,可以通过把las文件拖到界面上的方式,处理和加载点云数据。这个功能是在desktop.js中实现的。

5、desktop.js文件内容介绍

在index.html中定义了和拖动las文件相关的代码,如下所示。

import {
     loadDroppedPointcloud,
     createPlaceholder,
     convert_17,
     convert_20,
     doConversion,
     dragEnter, dragOver, dragLeave, dropHandler,
} from "./src/desktop.js";
const shell = require('electron').shell;
$(document).on('click', 'a[href^="http"]', function (event) {
     event.preventDefault();
     shell.openExternal(this.href);
});
let elBody = document.body;
elBody.addEventListener("dragenter", dragEnter, false);
elBody.addEventListener("dragover", dragOver, false);
elBody.addEventListener("dragleave", dragLeave, false);
elBody.addEventListener("drop", dropHandler, false);

从desktop.js文件引用 dragEnter,dragOver, dragLeave, dropHandler等模块。然后在document.body上注册事件,在dragenter的时候,调用desktop.js定义的dragEnter模块,dragover的时候,调用dragOver函数,dragleave的时候,调用dragleave函数,触发drop事件的时候,调用dropHandler函数。

下面我们再看下desktop.js文件中,关于这几个函数和模块的定义。

export function dragEnter(e) {
     e.dataTransfer.dropEffect = 'copy';
     e.preventDefault();
     e.stopPropagation();
     console.log("enter");
     showDropzones();
     return false;
}
export function dragOver(e){
     e.preventDefault();
     e.stopPropagation();
     showDropzones();
     return false;
}
export function dragLeave(e){
     e.preventDefault();
     e.stopPropagation();
     hideDropzones();
     return false;
}

这三个函数定义的比较简单,当拖到主区域的时候,显示拖拽区域,当移动到主区域的时候,也显示拖拽区域,当离开的时候,则隐藏该区域。显示拖拽区域的效果如下图所示。

Potree 002 Desktop开发环境搭建插图9

当鼠标放下之后,系统会调用dropHandler函数,该函数的定义如下。

export async function dropHandler(event) {
    event.preventDefault();
    event.stopPropagation();
    hideDropzones();
    let u = event.clientX / document.body.clientWidth;
    console.log(u);

    const cloudJsFiles = [];
    const lasLazFiles = [];
    let suggestedDirectory = null;
    let suggestedName = null;

    for (let i = 0; i ) {
        let item = event.dataTransfer.items[i];
        if (item.kind !== "file") {
            continue;
        }
        let file = item.getAsFile();
        let path = file.path;
        const fs = require("fs");
        const fsp = fs.promises;
        const np = require('path');
        const whitelist = [".las", ".laz"];
        let isFile = fs.lstatSync(path).isFile();

        if (isFile && path.indexOf("cloud.js") >= 0) {
            cloudJsFiles.push(file.path);
        } else if (isFile && path.indexOf("metadata.json") >= 0) {
            cloudJsFiles.push(file.path);
        } else if (isFile) {
            const extension = np.extname(path).toLowerCase();

            if (whitelist.includes(extension)) {
                lasLazFiles.push(file.path);

                if (suggestedDirectory == null) {
                    suggestedDirectory = np.normalize(${path}/..);
                    suggestedName = np.basename(path, np.extname(path)) + "_converted";
                }
            }
        } else if (fs.lstatSync(path).isDirectory()) {
            console.log("start readdir!");
            const files = await fsp.readdir(path);
            console.log("readdir done!");
            for (const file of files) {
                const extension = np.extname(file).toLowerCase();
                if (whitelist.includes(extension)) {
                    lasLazFiles.push(${path}</span>/${file});
                    if (suggestedDirectory == null) {
                        suggestedDirectory = np.normalize(${path}/..);
                        suggestedName = np.basename(path, np.extname(path)) + "_converted";
                    }
                } else if (file.toLowerCase().endsWith("cloud.js")) {
                    cloudJsFiles.push(${path}</span>/${file});
                } else if (file.toLowerCase().endsWith("metadata.json")) {
                    cloudJsFiles.push(${path}</span>/${file});
                }
            };
        }
    }
    if (lasLazFiles.length > 0) {
        doConversion(lasLazFiles, suggestedDirectory, suggestedName);
    }
    for (const cloudjs of cloudJsFiles) {
        loadDroppedPointcloud(cloudjs);
    }
    return false;
};

该代码中,判断拖入系统的内容,可能是文件或者目录,系统需要取出拖入内容包含的cloud.js文件、metadata.json文件或者.las和.laz文件。如果是cloud.js和metadata.json文件,则不需要进行转换,直接调用loadDroppedPointcloud函数加载即可。如果文件是.las和.laz文件,则调用doConversion函数。

export function loadDroppedPointcloud(cloudjsPath) {
    const folderName = cloudjsPath.replace(/\/g, "/").split("/").reverse()[1];
    Potree.loadPointCloud(cloudjsPath).then(e => {
    let pointcloud = e.pointcloud;
    let material = pointcloud.material;
    pointcloud.name = folderName;
    viewer.scene.addPointCloud(pointcloud);
    let hasRGBA = pointcloud.getAttributes().attributes.find(a =>     a.name === "rgba") !== undefined
    if (hasRGBA) {
        pointcloud.material.activeAttributeName = "rgba";
    } else {
        pointcloud.material.activeAttributeName = "color";
    }
    material.size = 1;
    material.pointSizeType = Potree.PointSizeType.ADAPTIVE;
    viewer.zoomTo(e.pointcloud);
    });
}; 

获取点云文件路径后,调用Potree.loadPointCloud函数,加载点云数据。该函数执行后,获取pointcloud,系统调用viewer.scene.addPointCloud函数,把打开的点云数据加载到场景中。后面的代码是判断该点云是否包含RGBA属性,如果包含,则使用rgba方式显示点云,如果不包含,则使用单颜色模式显示。

代码最后,设置点显示大小,大小模式并把场景缩放至该点云数据。

当文件是.las和.laz时候,调用doConversion函数,该函数会判断当前用户选择的是使用1.7版本的转换程序,还是使用2.0版本的转换程序,选择不同,调用desktop.js中定义的不同转换函数,以2.0为例,其定义代码如下所示。

export function convert_20(inputPaths, chosenPath, pointcloudName) {
    viewer.postMessage(message, { duration: 15000 });
    const { spawn, fork, execFile } = require('child_process');
    let exe = './libs/PotreeConverter2/PotreeConverter.exe';
    let parameters = [
...inputPaths,
"-o", chosenPath
              ];
    const converter = spawn(exe, parameters);
        let placeholder = null;
    let outputBuffer = "";
    converter.stdout.on('data', (data) => {
        const string = new TextDecoder("utf-8").decode(data);
        console.log("stdout", string);
     });
    converter.stderr.on('data', (data) => {
        console.log("==");
        console.error(stderr: ${data});
        });
    converter.on('exit', (code) => {
        console.log(child process exited </span><span style="color: rgba(0, 0, 255, 1)">with</span><span style="color: rgba(0, 0, 0, 1)"> code ${code});
        const cloudJS = ${chosenPath}/metadata.json;
        console.log("now loading point cloud: " + cloudJS);
        let message = conversion finished, now loading ${cloudJS};
        viewer.postMessage(message, { duration: 15000 });
        Potree.loadPointCloud(cloudJS).then(e => {
        let pointcloud = e.pointcloud;
        let material = pointcloud.material;
        pointcloud.name = pointcloudName;
        let hasRGBA = pointcloud.getAttributes().attributes.find(a => a.name === "rgba") !== undefined
        if (hasRGBA) {
            pointcloud.material.activeAttributeName = "rgba";
        } else {
            pointcloud.material.activeAttributeName = "color";
        }
        material.size = 1;
        material.pointSizeType = Potree.PointSizeType.ADAPTIVE;
        viewer.scene.addPointCloud(pointcloud);
        viewer.zoomTo(e.pointcloud);
        });
    });
}

系统通过spawn调用PotreeConverter.exe,并传入组织好的参数,参数包括输入文件和输出文件路径等。在spawn执行的过程中,可以通过converter.stdout.on('data', (data)捕捉进度信息,通过converter.stderr.on('data', (data)捕捉错误信息,通过converter.on('exit', (code)捕捉执行结束事件。当执行结束后,转换后的结果会包含metadata.json文件,然后调用Potree.loadPointCloud函数加载该文件即可,加载的过程和上面提到的直接加载点云数据一致。

文章来源于互联网:Potree 002 Desktop开发环境搭建

THE END
分享
二维码