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

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

一、前情提要

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

之前弄了一个 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)

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

  • 思源笔记

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

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

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

相关帖子

欢迎来到这里!

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

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