思源笔记折腾记录 - 整一个全景图浏览器 - 简单的全景图渲染

本贴最后更新于 501 天前,其中的信息可能已经物是人非

一、前情提要

其实我的本行是设计师嘛,所以总是经常需要浏览一下全景图什么的,但是网上的全景图网站总是有点限制,而且把图片全都交给在线服务总有种失控的感觉,所以这回我们来搞一个本地的全景图浏览器。

之前弄了一个 noob-service-vite,用来通过 vite 伺服文件夹方便写点小功能。

思源笔记折腾记录 - 做一个白板 - 最最最基础的卡片显示 - 链滴 (ld246.com)

但是它只写死了伺服一个文件夹(也就是那个白板),为了方便鼓捣,我们来尝试弄一个新活。

二、准备工作

1、自动伺服新挂件

我们对之前的 noob-service-vite 做一点改造。

首先是让他能够自动识别 data/viteWidgets 中的文件夹:

noob-service-vite\util\file.js

let fs = require('fs')
let path = require('path')
export let vite挂件目录 = window.siyuan.config.system.workspaceDir +'/data/viteWidgets'
export function 读取挂件列表(){
    let 挂件列表 =[]
    let 路径列表= require('fs').readdirSync(vite挂件目录)
    路径列表.forEach(
        路径名=>{
	    //如果目录下有vite.config.js
            if(fs.existsSync(path.join(vite挂件目录,路径名,'vite.config.js'))){
                挂件列表.push({
                    name:路径名,
                    path:path.join(vite挂件目录,路径名)
                })
            }
        }
    )
    return 挂件列表
}

2、自动伺服

这样之后,就可以改造最开始的 noob-service-vite

noob-service-vite\index.js

import noobApi from '../noobApi/index.js'
import { 读取挂件列表 } from './util/file.js'
import { 获取可用端口号 } from './util/port.js'
if (window.require) {
    const path = require('path')
    const vite = require('vite')
    let vite服务注册表 = {}
    读取挂件列表().forEach(
        挂件属性 => {
            创建vite服务(挂件属性.path)
        }
    )
    function 创建vite服务(文件夹路径) {
        return new Promise(async (resolve, reject) => {
            let vite配置文件路径 = path.join(文件夹路径, 'vite.config.js')
            if (!vite服务注册表.文件夹路径) {
                let server = await vite.createServer({
                    configFile: vite配置文件路径,
                    root: 文件夹路径,
                })
                !vite服务注册表[文件夹路径]?vite服务注册表[文件夹路径]={}:null
                vite服务注册表[文件夹路径]['server'] = server
                let port
                if (server.config.server.port) {
                    port = await 获取可用端口号(server.config.server.port)
                } else {
                    port =await 获取可用端口号(6806)
                }
                await server.listen(port,'127.0.0.1').then(
                    s => { 
                        console.log(文件夹路径,'的开发服务在:',port,'上启用') 
                        vite服务注册表[文件夹路径]['port'] = port
                    }
                )
                resolve(server)
            }
        })
    }
}

获取可用端口号的实现其实就是尝试能不能监听这个端口,如果不能就试个新的:

export async function 获取可用端口号(端口号){
    return new Promise((resolve, reject) => {
        let http = require('http')
        let 测试服务 = http.createServer()
        let 可用端口号 = 端口号||3000
        测试服务.on(
            'listening',()=>{
                console.log(端口号)

                测试服务.close(()=>{
                    resolve(可用端口号)
                })
      
            }
        )
        测试服务.on(
            'error',async(error)=>{
                console.log(error)
                if(error.code==='EADDRINUSE'){
                    resolve(await 获取可用端口号(可用端口号+1))
                }
                else{
                    reject(error)
                }
            }
        )
        测试服务.listen(端口号)
    })
  
}

二、实现简单的全景图浏览

1、安装 photo sphere viewer

这个很简单啦:

npm i photo sphere viewer --registry=https://registry.npmmirror.com

2、最简单的球面全景图查看器

欸,我就是不写 index.html 的内容,你们试着自己弄一下。

要实现一个最简单的全景图 i 浏览只需要几行代码啦:

import { Viewer } from 'photo-sphere-viewer';
//这里是引入了css,vite里面你这么干跟在html里面引入差不多
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'

const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  //这个是我之前上传到工作空间里的一张全景图
  panorama: 'assets/11111D5_全景图 1_20221211_061304-20221211182953-7i0dt0q.jpg',
});

就像这样:

识别一下本地文件夹 1.gif全景图云 park 客厅 120221211203533vtrxkcd.jpg

这张全景的原图长这样:

全景图云 park 客厅 120221211203533vtrxkcd.jpg

第一个全景图就渲染出来了~~

3、 加上画廊

photo sphere viewer 有很多插件,其中有一个 PhotoSphereViewer.GalleryPlugin 是用来生成一连串的全景图的。

PhotoSphereViewer.GalleryPlugin

这里我们约定文件名为:全景图-< 项目名 >-< 空间名 >-.jpg 的附件文件就是某一个系列的全景图。

所以这里可以搞一个 sql:

select * 
from assets
where name like '全景图-${项目名}-%'

然后我们来实验一下:

sphereViewer\src\index.js

import { Viewer } from 'photo-sphere-viewer';
import {GalleryPlugin} from 'photo-sphere-viewer/dist/plugins/gallery.js'
import  核心api from 'http://127.0.0.1:6806/snippets/noobApi/util/kernelApi.js'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
import 'photo-sphere-viewer/dist/plugins/gallery.css'

import { 获取地址参数 } from '../../whiteBoard/src/data';
let {project} =获取地址参数()
console.log(project)
let sql =`select * from assets where name like '全景图-${project}-%'`
let 全景图列表 = await 核心api.sql({stmt:sql})
console.log(全景图列表)
全景图列表.forEach(
  item=>{
    item.panorama=item.path
    item.thumbnail =item.path
    let 空间名 = item.name.split('-')[2]
    item.options ={
      caption: 空间名,

    }
  }
)
console.log(Viewer,GalleryPlugin)
const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: 全景图列表[0].path,
  plugins: [
    [GalleryPlugin, {
      visibleOnLoad: true,
    }],
  ],

});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)

image

嗯,好像渲染成功了~~~。

4、更方便的项目创建

啊,我们现在有一个全景图浏览器了,但是一张张图上传到思源的 assets 里面好像太麻烦了点,有没有更方便的办法咧?

首先来设想一个简单的全景图项目文件夹结构

  • 项目名称

    • 空间名 1

      • 全景图们
      • 其他数据
    • 空间名 2

    • 空间名 3

全部通过 assets 来管理好像有点麻烦,而且它的嵌套结构也不适合放到 assets 里面一起管理,全景图的数据也不大可能会被用到别的地方。

所以,我们来起一个后端吧,专门用来给这个网页提供服务(什么叫 BFF 啊~~)。

之前在 noob-service-vite 中,我们直接让 vite 来创建和监听服务器,这回改一下,使用中间件模式。

    function 创建vite服务(文件夹路径) {
        return new Promise(async (resolve, reject) => {
            let vite配置文件路径 = path.join(文件夹路径, 'vite.config.js')
            if (!vite服务注册表.文件夹路径) {

                let vite中间件 = await vite.createServer({
                    configFile: vite配置文件路径,
                    root: 文件夹路径,
                    server: {
                        middlewareMode: "html"
                    }
                })
                const app = express()
                !vite服务注册表[文件夹路径] ? vite服务注册表[文件夹路径] = {} : null
                vite服务注册表[文件夹路径]['server'] = app
                let port = vite中间件.config.server.port || 6807
                port = await 获取可用端口号(port)
                let server = await app.listen(port, '127.0.0.1', async () => {
                    vite服务注册表[文件夹路径]['port'] = port
                    vite服务注册表[文件夹路径]['server'] = server
                    vite服务注册表[文件夹路径]['app'] = app
                    vite服务注册表[文件夹路径]['vite'] = vite中间件
                    console.log(文件夹路径, '的开发服务在:', 'http://127.0.0.1:' + port, '上启用')
                    if (require('fs').existsSync(文件夹路径 + '/backend/index.js')) {
                            //因为某些蛋疼的原因,这里只能用require
			    let router = require(文件夹路径 +'/backend/index.js')
			    //主意router不要妨碍vite的工作,也就是别占了vite的坑
                            app.use(router)
                            app.use(vite中间件.middlewares)
                    }
                })/*.then(
                    s => { 
                        let {address,port} =  server.httpServer.address()
                        console.log(文件夹路径,'的开发服务在:','http://'+address+':'+port,'上启用') 
                        vite服务注册表[文件夹路径]['port'] = port
                        console.log(server)
                    }
                )*/
                resolve(server)
            }
        })
    }

这样之后,随便试一下,就把文件存在 data/widgetsData 里面吧。

按照上面约定的后端文件入口,先弄两个接口吧:

const express = require('express')
let router = express.Router()
const fs = require('fs')
const path = require('path')
let 数据路径 = path.join(window.siyuan.config.system.workspaceDir,'data','widgetsData','sphereViewer')
if(fs.existsSync(数据路径)){
    //为了避免跟思源的接口重合,挂件后端的接口全都加上widget前缀好了
    router.use('/widgetData',express.static(数据路径))
    router.use('/widgetApi/projects/getAll',(req,res)=>{
        res.json(fs.readdirSync(数据路径)
    })
}
//上面已经做了自动导入,回自动把这个接口混入vite的开发服务器当中
module.exports=router

这样之后,往这个文件夹里面放一点文件:

image

这个时候访问一下 127.0.0.1:6809/widgetApi/projects/listAllProjects(这个端口是在 vite.config.js 里设置的)。

image

可以看到我们刚刚写的后端已经返回了项目列表。

我们之前是从思源的 sql 获取数据的,所以上面的查看器的获取方式也要改一下。

但是因为之后可能还需要其他来源的全景图数据的渲染,所以这里也使用一个跟之前做简单白板的时候类似的适配器。

sphereViewer\src\adapters\sql.js

import { 核心api } from "../../../whiteBoard/src/data"
export class sql全景图适配器{
    获取项目列表(){
  
    }
    async 获取全景图列表(项目名){
        let sql =`select * from assets where name like '全景图-${项目名}-%'`
        let 全景图列表 = await 核心api.sql({stmt:sql})
        全景图列表.forEach(
            item=>{
              item.panorama=item.path
              item.thumbnail =item.path
              let 空间名 = item.name.split('-')[2]
              item.options ={
                caption: 空间名,
  
              }
            }
          )
        return 全景图列表
    }
}
import { Viewer } from 'photo-sphere-viewer';
import {GalleryPlugin} from 'photo-sphere-viewer/dist/plugins/gallery.js'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'
import 'photo-sphere-viewer/dist/plugins/gallery.css'
import { 获取地址参数 } from '../../whiteBoard/src/data';
import {sql全景图适配器} from "./adapters/sql.js"
let {project} =获取地址参数()
console.log(project)
let 当前适配器 = new sql全景图适配器()
let 全景图列表=  await 当前适配器.获取全景图列表(project) 
const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: 全景图列表[0].path,
  plugins: [
    [GalleryPlugin, {
      visibleOnLoad: true,
    }],
  ],

});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)

这个时候访问 127.0.0.1:6809/?project=云 park,应该可以看到跟之前一样的结果。

然后来试一下写一个后端适配器:

export class sql全景图适配器{
    获取项目列表(){
  
    }
    async 获取全景图列表(项目名){
        let res = await fetch('widgetApi/projects/getScenesByName',{
            method:"POST",
            body:{
                name:项目名
            }
        })
        let 全景图列表 = await res.json()
        return 全景图列表
    }
}

啊,是不是跟从思源获取数据挺像的,这个时候 getScenesByName 这个接口还不在,所以我们需要实现一下它。

    router.post('widgetApi/projects/getScenesByName',(req,res)=>{
        let {name} =  req.body
        let 场景列表 = []
        let 项目文件列表 = fs.readdirSync(path.join(数据路径,name))
        项目文件列表.forEach(
            路径名=>{
                let 缩略图路径 
                if( fs.existsSync(path.join(数据路径,name,路径名,'thumbnail.jpg'))){
                    缩略图路径 = path.join('/widgetData',name,路径名,'thumbnail.jpg').replace(/\\/g,'/')
                }else if(  fs.existsSync(path.join(数据路径,name,路径名,'_f.jpg'))  ){
                    缩略图路径=path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/')
                }else if(  fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))  ){
                    缩略图路径=path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/')
                }
                if(fs.existsSync(path.join(数据路径,name,路径名,'_b.jpg'))){
                    场景列表.push(
                        {
                            id:路径名,
                            panorama:{
                                left:   path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
                                front:  path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
                                right:  path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
                                back:   path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
                                top:    path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
                                bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
                              },
                            thumbnail:缩略图路径,
                            options:{
                                caption:name,
                            }
                        }
                    )

                }

                else if(fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))){
                    场景列表.push(
                        {
                            id:路径名,
                            panorama:path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/'),
                            options:{
                                caption:name,
                            },
                            thumbnail:缩略图路径,

                        }
                    )
                }
            }
        )
        res.json(场景列表)
    })

看起来应该跟上面的这个差不多对吧,就是识别文件夹里的文件,然后匹配下路径然后返回。

不过这里有个地方需要注意:

 panorama:{
	//这里的文件名是按照酷家乐的下载包内部名称弄的
	//但是除了上下不变以外,需要把左边映射到右边,右边弄到左边,其他的依此类推
	left:   path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
	front:  path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
	right:  path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
	back:   path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
	top:    path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
	bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
},

因为渲染器给出的六面全景图一般是按照“人在方盒子里面”来适配六个面的,而 PhotoSphereViewer 是按照“人在方盒子外面”来读取的,所以读取文件的时候,除了上下不用反之外,其他前后左右都要反一下~~~

我们试着使用一下这个适配器:

import {后端适配器} from './adapters/internal.js'
let {project} =获取地址参数()
console.log(project)
let 当前适配器 = new 后端适配器()
let 全景图列表=  await 当前适配器.获取全景图列表(project) 
const viewer = new Viewer({
  container: document.querySelector('#viewer'),
  panorama: 全景图列表[0].path,
  plugins: [
    [GalleryPlugin, {
      visibleOnLoad: true,
    }],
  ],

});
const gallery = viewer.getPlugin(GalleryPlugin);
gallery.setItems(全景图列表)

然后它就报错了:

image

因为我们使用的 express 后端并没有自带解析 post 请求的 body 的能力,这个时候需要一个 body-parser

一样的,首先需要安装它:

npm i --registry=https://registry.npmmirror.com body-parser

然后引入并使用:

const bodyParser = require('body-parser')
    router.post('/widgetApi/projects/getScenesByName',
    (req,res,next)=>{
	//强制按json解析,这样前端就不用设置content-type了
        req.headers['content-type']='application/json'
	//记得调用next()
        next()
    },
    bodyParser.json(),
        let {name} =  req.body
        let 场景列表 = []
        let 项目文件列表 = fs.readdirSync(path.join(数据路径,name))
        项目文件列表.forEach(
            路径名=>{
                let 缩略图路径 
                if( fs.existsSync(path.join(数据路径,name,路径名,'thumbnail.jpg'))){
                    缩略图路径 = path.join('/widgetData',name,路径名,'thumbnail.jpg').replace(/\\/g,'/')
                }else if(  fs.existsSync(path.join(数据路径,name,路径名,'_f.jpg'))  ){
                    缩略图路径=path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/')
                }else if(  fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))  ){
                    缩略图路径=path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/')
                }
                if(fs.existsSync(path.join(数据路径,name,路径名,'_b.jpg'))){
                    场景列表.push(
                        {
                            id:路径名,
                            panorama:{
                                left:   path.join('/widgetData',name,路径名,'_r.jpg').replace(/\\/g,'/'),
                                front:  path.join('/widgetData',name,路径名,'_b.jpg').replace(/\\/g,'/'),
                                right:  path.join('/widgetData',name,路径名,'_l.jpg').replace(/\\/g,'/'),
                                back:   path.join('/widgetData',name,路径名,'_f.jpg').replace(/\\/g,'/'),
                                top:    path.join('/widgetData',name,路径名,'_u.jpg').replace(/\\/g,'/'),
                                bottom: path.join('/widgetData',name,路径名,'_d.jpg').replace(/\\/g,'/'),
                              },
                            thumbnail:缩略图路径,
                            options:{
                                caption:name,
                            }
                        }
                    )

                }

                else if(fs.existsSync(path.join(数据路径,name,路径名,'sphere.jpg'))){
                    场景列表.push(
                        {
                            id:路径名,
                            panorama:path.join('/widgetData',name,路径名,'sphere.jpg').replace(/\\/g,'/'),
                            options:{
                                caption:name,
                            },
                            thumbnail:缩略图路径,

                        }
                    )
                }
            }
        )
        res.json(场景列表)
}

然后再看一下 127.0.0.1:6809/?project=云 park48

这个时候它显示:

image

这是因为 PhotoSphereViewer 默认使用的是球面图适配器,而我们优先返回的是六面图,有关它的适配器可以参考文档:

Adapters | Photo Sphere Viewer (photo-sphere-viewer.js.org)

先不纠结,按照文档使用六面图适配器看看:

sphereViewer\src\index.js

let 全景图选项 = {
  container: document.querySelector('#viewer'),
  panorama: 全景图列表[0].panorama,
  plugins: [
    [GalleryPlugin, {
      visibleOnLoad: true,
    }],
  ],
}
//如果有六面图文件,就使用六面图适配器
if(全景图列表[0].panorama.left){
  全景图选项.adapter = CubemapAdapter
}
let viewer= new Viewer(全景图选项)

这回显示对了,效果就像这样:

识别一下本地文件夹.gif

这回好像终于干了点跟设计师沾边的活儿了~~~


目前的代码片段的地址位于:

leolee9086/snippets (github.com)

viteWidgets 的地址位于

leolee9086/viteWidgets (github.com)

啊 全景图文件我就不提供了,自己试试吧。

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    18708 引用 • 69844 回帖
1 操作
leolee 在 2022-12-12 15:03:22 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...