This commit is contained in:
2025-12-26 16:45:58 +08:00
commit 1a20560753
190 changed files with 37841 additions and 0 deletions

6
.env.development Normal file
View File

@ -0,0 +1,6 @@
ENV = 'production'
VUE_APP_MODE = 'production'
VITE_BASE_URL = 'https://wly-demo.wlyun.co'
VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
VITE_PUBLIC_URL=/

6
.env.production Normal file
View File

@ -0,0 +1,6 @@
ENV = 'production'
VUE_APP_MODE = 'production'
VITE_BASE_URL = 'https://wly-demo.wlyun.co'
VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
VITE_PUBLIC_URL=./

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

14
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,14 @@
# This file is a template, and might need editing before it works on your project.
# Full project: https://gitlab.com/pages/plain-html
image: busybox
pages:
stage: deploy
script:
- echo "The site will be deployed to $CI_PAGES_URL"
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

106
README.md Normal file
View File

@ -0,0 +1,106 @@
# Vue 3 + TypeScript + Vite + BabylonJS 项目模板
## 项目简介
这是一个基于 Vue 3、TypeScript 和 Vite 构建的 3D 可视化项目。本项目使用 BabylonJS 作为 3D 引擎,结合 Vue 3 的 `<script setup>` 语法糖,提供了现代化的 Web3D 开发环境。
## 技术栈
### 核心框架
- Vue 3 - 渐进式 JavaScript 框架
- TypeScript - JavaScript 的超集,添加了类型系统
- Vite - 下一代前端构建工具
### 3D 引擎与工具
- BabylonJS 7.32 - 强大的 Web3D 引擎
- BabylonJS Loaders - 用于加载 3D 模型和资源
- Stats.js - 性能监控工具
### UI 框架
- Element Plus - 基于 Vue 3 的组件库
- Vant - 移动端组件库
### 开发工具
- GLSL 着色器支持 (vite-plugin-glsl)
- Monaco Editor - 代码编辑器集成
- Sass - CSS 预处理器
## 项目功能
- 3D 场景渲染与交互
- 自定义着色器支持shader 目录)
- 性能监控和优化
- 响应式界面设计
- 代码编辑器集成
## 开发环境设置
### 环境要求
- Node.js 版本 >= 14.0.0
- npm 版本 >= 6.0.0
### 推荐的 IDE 配置
- [VSCode](https://code.visualstudio.com/) - 轻量级但功能强大的编辑器
- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue 3 的官方 VSCode 插件
## 项目启动步骤
1. 安装依赖
```bash
npm install
```
2. 启动开发服务器
```bash
npm run dev
```
3. 构建生产版本
```bash
npm run build
```
## 项目结构
```
src/
├── api/ # API 接口定义
├── assets/ # 静态资源文件
├── components/ # Vue 组件
├── global/ # 全局配置和工具
├── shader/ # GLSL 着色器文件
├── types/ # TypeScript 类型定义
├── view/ # 页面视图组件
└── App.vue # 根组件
```
## 开发规范
- 组件文件使用 PascalCase 命名
- TypeScript 文件使用 camelCase 命名
- 样式文件使用 kebab-case 命名
- 着色器文件使用 .glsl 或 .shader 扩展名
## 性能优化
- 使用 vite-plugin-compression 进行资源压缩
- 通过 Stats.js 监控性能指标
- 支持代码分割和懒加载
- 针对移动端的优化配置
## 注意事项
- 开发时请确保启用了 Volar 并禁用了 Vetur
- 建议使用 Vue 3 的组合式 API 进行开发
- 3D 模型推荐使用 glTF/glb 格式
- 确保在提交代码前运行类型检查和代码格式化
## 相关文档
- [Vue 3 文档](https://v3.cn.vuejs.org/)
- [BabylonJS 文档](https://doc.babylonjs.com/)
- [TypeScript 文档](https://www.typescriptlang.org/)
- [Element Plus 文档](https://element-plus.org/zh-CN/)
- [Vite 文档](https://cn.vitejs.dev/)

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

9570
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@babylonjs/core": "^7.54.3",
"@babylonjs/loaders": "^7.54.3",
"@openai/codex": "^0.1.2505291658",
"axios": "^1.2.1",
"earcut": "^3.0.1",
"editor": "^1.0.0",
"element-plus": "^2.9.9",
"mitt": "^3.0.1",
"pinia": "^3.0.2",
"popmotion": "^11.0.5",
"stats.js": "^0.17.0",
"uuid": "^10.0.0",
"vant": "^4.0.2",
"vite-plugin-compression": "^0.5.1",
"vue": "^3.2.25"
},
"devDependencies": {
"@types/node": "^18.19.67",
"@types/stats.js": "^0.17.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-legacy": "^2.0.0",
"@vitejs/plugin-vue": "^3.0.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.54.9",
"sass-loader": "^13.0.2",
"sass-resources-loader": "^2.2.5",
"terser": "^5.14.2",
"typescript": "^4.9.5",
"vite": "^3.2.11",
"vue-tsc": "^0.34.17",
"webpack-bundle-analyzer": "^4.10.2"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,7 @@
{
"publicURL": "",
"freeHiglight": true,
"panningSensibility": 2000,
"clickReturnOrigin": false
}

View File

@ -0,0 +1,33 @@
[
{
"id": "2e0f716c-1d3b-47f8-9374-780d97a6b7f7",
"code": "dt-01",
"name": "直梯",
"modelUrl": "src/model/car3.glb",
"thumbnailUrl": "src/直梯10.png",
"paths": [
"config/x1/Component.json"
],
"vr": true,
"contrast":true,
"cameraPosition":[7.52,1.88,-5.24],
"cameraTarget":[0,0.5,0.2],
"angularSensibility":1000
},
{
"id": "2e0f716c-155b-47f8-9374-5a0d97a6b7f7",
"code": "dt-02",
"name": "扶梯",
"modelUrl": "src/model/日立扶梯.glb",
"thumbnailUrl": "src/扶梯10.png",
"paths": [
"config/扶梯/Component.json"
],
"vr": false,
"contrast":true,
"cameraPosition":[0,0,70],
"cameraTarget":[0,0.2,0],
"angularSensibility":1500
}
]

View File

@ -0,0 +1,149 @@
[
{
"name": "座椅白_N_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "src/texture/座椅白_N_T.jpg",
"aoTexture": ""
},
{
"name": "后尾灯红色内部_N_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "src/texture/车门黄灯_N_T.ktx2",
"aoTexture": ""
},
{
"name": "后尾灯黄色内部_N_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "src/texture/车门黄灯_N_T.ktx2",
"aoTexture": ""
},
{
"name": "挡泥板标_T",
"albedoTexture": "src/texture/挡泥板标_T.png",
"alphaTexture": "src/texture/挡泥板标_T.png",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "方向盘磨砂_N_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "src/texture/座椅白_N_T.ktx2",
"aoTexture": ""
},
{
"name": "红条_T",
"albedoTexture": "src/texture/红条2_T.ktx2",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "蓝色-前脸_AO_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": "src/texture/蓝色前脸_Mixed_AO2.ktx2"
},
{
"name": "藍色--车顶侧_AO_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": "src/texture/车侧顶AO.ktx2"
},
{
"name": "车厢_AO_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": "src/texture/车厢_Mixed_AO2.ktx2"
},
{
"name": "车厢2_AO_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": "src/texture/车厢.001_Mixed_AO2.ktx2"
},
{
"name": "蓝色-门_AO_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": "src/texture/蓝色-门_Mixed_AO2.ktx2"
},
{
"name": "右门标_T",
"albedoTexture": "src/texture/车门标.ktx2",
"alphaTexture": "src/texture/车门标.ktx2",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "右门标2_T",
"albedoTexture": "src/texture/右门标_T.ktx2",
"alphaTexture": "src/texture/右门标_T.ktx2",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "车前脸标_T",
"albedoTexture": "",
"alphaTexture": "src/texture/车前脸标_T.ktx2",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "红条_T.001",
"albedoTexture": "src/texture/红条2_T.ktx2",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "车后标2_T",
"albedoTexture": "",
"alphaTexture": "src/texture/车后标2_T.ktx2",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "车后标_T",
"albedoTexture": "",
"alphaTexture": "src/texture/车后标_T.ktx2",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "藍色.005",
"albedoTexture": "src/texture/车牌_T.png",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "内饰磨砂_N_T",
"albedoTexture": "",
"alphaTexture": "",
"bumpTexture": "src/texture/座椅白_N_T.jpg",
"aoTexture": ""
},
{
"name": "仪表盘_D_T",
"albedoTexture": "src/texture/仪表盘_D_T.ktx2",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": ""
},
{
"name": "内饰显示屏_D_T",
"albedoTexture": "src/texture/内饰显示屏_D_T.ktx2",
"alphaTexture": "",
"bumpTexture": "",
"aoTexture": ""
}
]

View File

@ -0,0 +1,193 @@
[
{
"id": "2beebcaf-8851-4690-b156-252124ff788",
"name": "10成新1",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2beeb3af-8851-4690-b166-2526a1sff788",
"albedoTexture": "src/texture1/外观/扶手10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/扶手10_RoughnessM.ktx2",
"transparent": -1
},
{
"id": "2beebcaf-8851-4690-b156-552124f6788",
"name": "10成新1",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2beeb3af-8851-4690-b166-56564543f788",
"albedoTexture": "src/texture1/外观/栏板10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/栏板10_RoughnessM.ktx2",
"transparent": -1
},
{
"id": "34eebcaf-8851-4690-b156-34343455667",
"name": "10成新18",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1235b3af-8871-2690-b166-453456111f788",
"albedoTexture": "src/texture1/外观/楼层板10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/楼层板10_RoughnessM.ktx2",
"transparent": -1
},
{
"id": "11eebcaf-8821-4690-b136-252641sff788",
"name": "10成新2",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1beeb3af-8821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/外观/玻璃10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/玻璃10_RoughnessM.ktx2",
"transparent": 0.5
},
{
"id": "11eebcaf-8821-4690-b136-252641sf1111",
"name": "10成新2",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1235b3af-8871-2690-b166-2323231111f788",
"albedoTexture": "src/texture1/外观/群板盖板10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/群板盖板10_RoughnessM.ktx2",
"transparent": -1
},
{
"id": "12eebcaf-8871-4690-b166-2586a1sff788",
"name": "10成新3",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "25eeb3af-8871-4690-b166-2586a1sff788",
"albedoTexture": "src/texture1/外观/梯级/梯级10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/外观/梯级/梯级10_RoughnessM.ktx2",
"transparent": -1
},
{
"id": "13113caf-8871-2690-b166-2486a1sff788",
"name": "10成新4",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2323233-8821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/扶梯主机/扶梯主机10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/扶梯主机/扶梯主机10_RoughnessMetallic.ktx2",
"transparent": -1
},
{
"id": "1422bcaf-8851-4690-b166-2526a1sff788",
"name": "10成新5",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1b33b3af-8821-4690-b136-252641s11111",
"albedoTexture": "src/texture1/单双链/单链10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/单双链/单链10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "1533bcaf-8821-4690-b136-252641sff788",
"name": "10成新6",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2544b3af-8871-4690-b166-2586a1sff788",
"albedoTexture": "src/texture1/附加制动器/附加制动器10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/附加制动器/附加制动器10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "2333bcaf-8821-4690-b136-252641sff788",
"name": "10成新6",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2555b3af-8871-2690-b166-2486a1sff788",
"albedoTexture": "src/texture1/桁架/桁架10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/桁架/桁架10_RoughnessMetallic.ktx2",
"transparent": -1
},
{
"id": "1755bcaf-8871-2690-b166-2486a1sff788",
"name": "10成新7",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1235b3af-8871-2690-b166-2486a1111f788",
"albedoTexture": "src/texture1/单双链/双链10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/单双链/双链10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "1755bcaf-8871-2690-b166-24823132a1sff788",
"name": "10成新8",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2b66b3af-8851-4690-b166-2526a1sff788",
"albedoTexture": "src/texture1/轴承/轴承10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/轴承/轴承10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "34eebcaf-8851-4690-b156-2526a1412128",
"name": "10成新18",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1b77b3af-8821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/其他/制动器/制动器10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/其他/制动器/制动器10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "1866bcaf-8851-4690-b166-2526a1sff788",
"name": "10成新9",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2588b3af-8871-4690-b166-2586a1sff788",
"albedoTexture": "其他/其他支架/其他支架10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "其他/其他支架/其他支架10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "1977bcaf-8851-4690-b166-2526a1sff788",
"name": "10成新10",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "2599b3af-8871-2690-b166-22345566ff788",
"albedoTexture": "src/texture1/其他/其他下/下10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/其他/其他下/下10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "2199bcaf-8851-4690-b166-2526a1sff788",
"name": "10成新12",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1b12b3af-8821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/其他/其他上/上10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/其他/其他上/上10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "676766663-8871-4690-b166-2586a1sff788",
"name": "5成新3",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "56666555-8821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/控制系统/控制柜/控制柜10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/控制系统/控制柜/控制柜10_RoughnessMF.ktx2",
"transparent": -1
},
{
"id": "67844434445-8871-4690-b166-2586a1sff788",
"name": "5成新3",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "1234567438821-4690-b136-252641sff788",
"albedoTexture": "src/texture1/控制系统/油泵/油泵10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/控制系统/油泵/油泵10_RoughnessMF.ktx2",
"transparent": -1
}
,
{
"id": "tgdrfg45435-8871-4690-b166-2586a1sff788",
"name": "5成新3",
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
"materialId": "6785543333-4690-b136-252641sff788",
"albedoTexture": "src/texture1/控制系统/油泵/透明10_BaseColor.ktx2",
"bumpTexture": "",
"roughnessTexture": "src/texture1/控制系统/油泵/透明10_RoughnessMF.ktx2",
"transparent": 0.6
}
]

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,116 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports?(exports.DracoDecoderModule=DracoDecoderModule):globalThis["DracoDecoderModule"] = DracoDecoderModule;

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,116 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/src/hdr/bg03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
public/src/hdr/bg2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

BIN
public/src/hdr/bg3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Binary file not shown.

BIN
public/src/ui/背景.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

21
src/App.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<Editor />
</template>
<script setup lang="ts">
import MainApp from './view/MainApp.vue'
import Editor from './view/Editor.vue'
</script>
<style>
#app {
width: 100vw;
height: 100vh;
}
* {
margin: 0;
padding: 0;
}
</style>

288
src/api/README.md Normal file
View File

@ -0,0 +1,288 @@
# API 架构重构说明
## 概述
本次重构将原有的网络请求代码进行了全面的重新设计和组织提供了更加模块化、类型安全和易于扩展的API架构。
## 目录结构
```
src/api/
├── types/ # 类型定义
│ └── index.ts
├── config/ # 配置文件
│ └── index.ts
├── utils/ # 工具函数
│ └── index.ts
├── core/ # 核心模块
│ ├── request.ts # HTTP请求客户端
│ ├── upload.ts # 文件上传模块
│ └── download.ts # 文件下载模块
├── modules/ # 业务API模块
│ ├── editor.ts # 编辑器相关API
│ └── resource.ts # 资源管理API
├── examples/ # 使用示例
│ └── usage.ts
├── index.ts # 统一入口
└── README.md # 说明文档
```
## 主要特性
### 1. 类型安全
- 完整的TypeScript类型定义
- 泛型支持,确保请求和响应数据的类型安全
- 严格的类型检查
### 2. 模块化设计
- 核心功能与业务逻辑分离
- 可插拔的模块设计
- 易于扩展和维护
### 3. 文件上传功能
- 普通文件上传
- 图片上传(支持压缩)
- 大文件分片上传
- 批量文件上传
- Base64上传
- 上传进度监控
### 4. 文件下载功能
- 普通文件下载
- 分片下载(大文件)
- 批量下载
- 下载为不同格式Blob、Base64、ArrayBuffer、Text
- 下载进度监控
### 5. 错误处理
- 统一的错误处理机制
- 自动重试功能
- 详细的错误信息
### 6. 请求管理
- 请求拦截器
- 响应拦截器
- 请求取消功能
- 认证token管理
## 快速开始
### 基本使用
```typescript
import { editorApi, resourceApi, ApiManager } from '@/api'
// 设置认证token
ApiManager.setAuthToken('your-token')
// 创建订单
const order = await editorApi.createOrder({
projectId: 'project-123',
type: 'render'
})
// 上传资源
const resource = await resourceApi.uploadResource(file, {
type: 'model',
tags: ['建筑', '现代']
})
```
### 文件上传
```typescript
import { fileUploader } from '@/api'
// 普通文件上传
const result = await fileUploader.uploadFile({
file,
onProgress: (progress) => {
console.log(`上传进度: ${progress}%`)
}
})
// 图片上传(带压缩)
const imageResult = await fileUploader.uploadImage(file, {
compress: true,
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080
})
// 大文件分片上传
const largeFileResult = await fileUploader.uploadLargeFile(file, {
chunkSize: 2 * 1024 * 1024, // 2MB per chunk
onProgress: (progress) => {
console.log(`上传进度: ${progress}%`)
}
})
```
### 文件下载
```typescript
import { fileDownloader } from '@/api'
// 下载并保存文件
await fileDownloader.downloadAndSave({
url: 'https://example.com/file.zip',
filename: 'download.zip'
})
// 下载为Base64
const base64 = await fileDownloader.downloadAsBase64({
url: 'https://example.com/image.jpg'
})
// 批量下载
const result = await fileDownloader.downloadBatch([
'https://example.com/file1.zip',
'https://example.com/file2.zip'
], {
concurrent: 3,
onProgress: (progress) => {
console.log(`批量下载进度: ${progress}%`)
}
})
```
## API 模块
### 编辑器API (EditorApi)
提供编辑器相关的功能:
- `createOrder(params)` - 创建订单
- `saveProject(projectData)` - 保存项目
- `loadProject(projectId)` - 加载项目
- `deleteProject(projectId)` - 删除项目
- `getProjectList(params)` - 获取项目列表
- `uploadThumbnail(file, projectId)` - 上传缩略图
- `exportProject(projectId, format)` - 导出项目
- `importProject(file)` - 导入项目
### 资源API (ResourceApi)
提供资源管理功能:
- `getResourceList(params)` - 获取资源列表
- `getResourceDetail(resourceId)` - 获取资源详情
- `uploadResource(file, metadata)` - 上传资源
- `uploadMultipleResources(files, metadata)` - 批量上传资源
- `deleteResource(resourceId)` - 删除资源
- `downloadResource(resource)` - 下载资源
- `searchResources(params)` - 搜索资源
## 配置
### 环境变量
```bash
# .env.development
VITE_BASE_URL=http://localhost:3000/api
# .env.production
VITE_BASE_URL=https://api.example.com
```
### API配置
```typescript
// src/api/config/index.ts
export const API_CONFIG = {
BASE_URL: import.meta.env.VITE_BASE_URL,
TIMEOUT: 15000,
RETRY_COUNT: 3,
RETRY_DELAY: 1000,
// ...其他配置
}
```
## 错误处理
API会自动处理常见的错误情况
- 网络连接失败
- 请求超时
- 认证失败
- 权限不足
- 服务器错误
所有API方法都返回统一的响应格式
```typescript
interface ApiResponse<T> {
code: number
message: string
data: T
success: boolean
}
```
## 扩展性
### 添加新的API模块
1.`src/api/modules/` 目录下创建新的模块文件
2. 定义相关的类型接口
3. 实现API类
4.`src/api/index.ts` 中导出
### 添加新的请求拦截器
```typescript
import { httpClient } from '@/api'
// 添加请求拦截器
httpClient.instance.interceptors.request.use(
(config) => {
// 自定义请求处理
return config
},
(error) => {
return Promise.reject(error)
}
)
```
## 迁移指南
### 从旧API迁移
旧的API调用
```typescript
// 旧方式
const request = new Request()
request.post(new ParamsReq('/api/endpoint', data))
```
新的API调用
```typescript
// 新方式
import { httpClient } from '@/api'
const response = await httpClient.post('/api/endpoint', data)
```
### 兼容性
新的API架构与旧代码完全分离可以逐步迁移不会影响现有功能。
## 最佳实践
1. **使用TypeScript类型**:充分利用类型定义,确保代码的类型安全
2. **错误处理**始终处理API调用可能出现的错误
3. **进度监控**:对于文件上传下载,使用进度回调提升用户体验
4. **请求取消**:在组件卸载时取消未完成的请求
5. **缓存策略**:对于不经常变化的数据,考虑实现缓存机制
## 性能优化
1. **并发控制**:批量操作时控制并发数量
2. **分片上传**:大文件使用分片上传
3. **压缩优化**:图片上传时自动压缩
4. **请求去重**:避免重复请求
5. **缓存机制**:合理使用缓存减少网络请求
## 示例代码
详细的使用示例请参考 `src/api/examples/usage.ts` 文件。

83
src/api/config/index.ts Normal file
View File

@ -0,0 +1,83 @@
// API 基础配置
export const API_CONFIG = {
// 基础URL
BASE_URL: import.meta.env.VITE_BASE_URL || 'https://script.jiaweijia.cn/script',
// 超时时间
TIMEOUT: 15000,
// 重试次数
RETRY_COUNT: 3,
// 重试延迟(毫秒)
RETRY_DELAY: 1000,
// 默认请求头
DEFAULT_HEADERS: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
// 文件上传请求头
UPLOAD_HEADERS: {
'Content-Type': 'multipart/form-data',
},
// 响应状态码
STATUS_CODES: {
SUCCESS: 200,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500,
},
// 业务状态码
BUSINESS_CODES: {
SUCCESS: 0,
FAIL: -1,
TOKEN_EXPIRED: 401,
PERMISSION_DENIED: 403,
}
}
// 环境配置
export const ENV_CONFIG = {
isDev: import.meta.env.DEV,
isProd: import.meta.env.PROD,
mode: import.meta.env.MODE,
}
// API 端点配置
export const API_ENDPOINTS = {
// 编辑器相关
EDITOR: {
CREATE_ORDER: '/cpq/3d/create/order',
SAVE_PROJECT: '/editor/project/save',
LOAD_PROJECT: '/editor/project/load',
DELETE_PROJECT: '/editor/project/delete',
},
// 文件相关
FILE: {
UPLOAD: '/file/upload',
DOWNLOAD: '/file/download',
DELETE: '/file/delete',
LIST: '/file/list',
},
// 用户相关
USER: {
LOGIN: '/user/login',
LOGOUT: '/user/logout',
INFO: '/user/info',
UPDATE: '/user/update',
},
// 资源相关
RESOURCE: {
LIST: '/resource/list',
UPLOAD: '/resource/upload',
DELETE: '/resource/delete',
}
}

377
src/api/core/download.ts Normal file
View File

@ -0,0 +1,377 @@
import { httpClient } from './request'
import { DownloadConfig, ApiResponse } from '../types'
import { API_ENDPOINTS } from '../config'
import { downloadFile, generateId } from '../utils'
/**
* 下载响应类型
*/
export interface DownloadResponse {
blob: Blob
filename: string
size: number
type: string
}
/**
* 批量下载响应类型
*/
export interface BatchDownloadResponse {
success: DownloadResponse[]
failed: Array<{
url: string
error: string
}>
total: number
successCount: number
failedCount: number
}
/**
* 文件下载类
*/
export class FileDownloader {
private downloadQueue: Map<string, AbortController> = new Map()
/**
* 下载文件
*/
public async downloadFile(config: DownloadConfig): Promise<DownloadResponse> {
const {
url,
filename,
params,
onProgress
} = config
// 生成下载ID
const downloadId = generateId()
const controller = new AbortController()
this.downloadQueue.set(downloadId, controller)
try {
const response = await httpClient.request<Blob>({
url,
method: 'GET',
params,
responseType: 'blob',
timeout: 0, // 下载不设置超时
})
if (!response.success) {
throw new Error(response.message)
}
const blob = response.data
const contentType = blob.type || 'application/octet-stream'
// 从响应头或URL中获取文件名
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
return {
blob,
filename: finalFilename,
size: blob.size,
type: contentType
}
} finally {
this.downloadQueue.delete(downloadId)
}
}
/**
* 下载并保存文件
*/
public async downloadAndSave(config: DownloadConfig): Promise<void> {
const result = await this.downloadFile(config)
downloadFile(result.blob, result.filename)
}
/**
* 下载文件为Base64
*/
public async downloadAsBase64(config: DownloadConfig): Promise<string> {
const result = await this.downloadFile(config)
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const base64 = reader.result as string
resolve(base64.split(',')[1]) // 移除data:xxx;base64,前缀
}
reader.onerror = reject
reader.readAsDataURL(result.blob)
})
}
/**
* 下载文件为ArrayBuffer
*/
public async downloadAsArrayBuffer(config: DownloadConfig): Promise<ArrayBuffer> {
const result = await this.downloadFile(config)
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = reject
reader.readAsArrayBuffer(result.blob)
})
}
/**
* 下载文件为文本
*/
public async downloadAsText(config: DownloadConfig): Promise<string> {
const result = await this.downloadFile(config)
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsText(result.blob)
})
}
/**
* 分片下载(大文件)
*/
public async downloadLargeFile(
config: DownloadConfig & {
chunkSize?: number
onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
}
): Promise<DownloadResponse> {
const {
url,
filename,
params,
chunkSize = 2 * 1024 * 1024, // 2MB per chunk
onProgress,
onChunkProgress
} = config
try {
// 获取文件信息
const headResponse = await httpClient.request({
url,
method: 'HEAD',
params
})
if (!headResponse.success) {
throw new Error('无法获取文件信息')
}
// 这里需要根据实际的响应头来获取文件大小
// const fileSize = parseInt(headResponse.headers['content-length'] || '0')
const fileSize = 0 // 临时设置,实际应该从响应头获取
if (fileSize === 0) {
// 如果无法获取文件大小,回退到普通下载
return this.downloadFile(config)
}
const totalChunks = Math.ceil(fileSize / chunkSize)
const chunks: Blob[] = []
// 下载分片
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize - 1, fileSize - 1)
const chunkResponse = await httpClient.request<Blob>({
url,
method: 'GET',
params,
headers: {
'Range': `bytes=${start}-${end}`
},
responseType: 'blob'
})
if (chunkResponse.success) {
chunks.push(chunkResponse.data)
// 分片进度回调
onChunkProgress?.(i, 100)
// 总进度回调
const progress = ((i + 1) / totalChunks) * 100
onProgress?.(progress)
} else {
throw new Error(`分片 ${i} 下载失败: ${chunkResponse.message}`)
}
}
// 合并分片
const blob = new Blob(chunks)
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
return {
blob,
filename: finalFilename,
size: blob.size,
type: blob.type || 'application/octet-stream'
}
} catch (error) {
throw new Error(`分片下载失败: ${error}`)
}
}
/**
* 批量下载
*/
public async downloadBatch(
urls: string[],
options: {
concurrent?: number
onProgress?: (progress: number) => void
onFileProgress?: (url: string, progress: number) => void
onFileComplete?: (url: string, result: DownloadResponse | Error) => void
} = {}
): Promise<BatchDownloadResponse> {
const {
concurrent = 3,
onProgress,
onFileProgress,
onFileComplete
} = options
const results: BatchDownloadResponse = {
success: [],
failed: [],
total: urls.length,
successCount: 0,
failedCount: 0
}
const downloadUrl = async (url: string): Promise<void> => {
try {
const response = await this.downloadFile({
url,
onProgress: (progress) => onFileProgress?.(url, progress)
})
results.success.push(response)
results.successCount++
onFileComplete?.(url, response)
} catch (error) {
results.failed.push({
url,
error: error instanceof Error ? error.message : '下载失败'
})
results.failedCount++
onFileComplete?.(url, error as Error)
}
// 更新总进度
const completed = results.successCount + results.failedCount
const progress = (completed / results.total) * 100
onProgress?.(progress)
}
// 并发下载
const chunks = []
for (let i = 0; i < urls.length; i += concurrent) {
chunks.push(urls.slice(i, i + concurrent))
}
for (const chunk of chunks) {
await Promise.all(chunk.map(downloadUrl))
}
return results
}
/**
* 批量下载并打包为ZIP
*/
public async downloadAsZip(
files: Array<{
url: string
filename?: string
}>,
zipFilename: string = 'download.zip'
): Promise<void> {
try {
// 下载所有文件
const downloadPromises = files.map(async (file) => {
const result = await this.downloadFile({
url: file.url,
filename: file.filename
})
return {
filename: result.filename,
blob: result.blob
}
})
const downloadedFiles = await Promise.all(downloadPromises)
// 这里需要使用JSZip库来创建ZIP文件
// 由于没有导入JSZip这里只是示例代码
console.log('需要集成JSZip库来实现ZIP打包功能')
console.log('下载的文件:', downloadedFiles)
// 示例如果有JSZip
// const JSZip = (await import('jszip')).default
// const zip = new JSZip()
//
// downloadedFiles.forEach(file => {
// zip.file(file.filename, file.blob)
// })
//
// const zipBlob = await zip.generateAsync({ type: 'blob' })
// downloadFile(zipBlob, zipFilename)
} catch (error) {
throw new Error(`ZIP下载失败: ${error}`)
}
}
/**
* 从URL中提取文件名
*/
private extractFilenameFromUrl(url: string): string {
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const filename = pathname.split('/').pop()
return filename || 'download'
} catch {
return 'download'
}
}
/**
* 取消下载
*/
public cancelDownload(downloadId: string): void {
const controller = this.downloadQueue.get(downloadId)
if (controller) {
controller.abort()
this.downloadQueue.delete(downloadId)
}
}
/**
* 取消所有下载
*/
public cancelAllDownloads(): void {
this.downloadQueue.forEach((controller) => {
controller.abort()
})
this.downloadQueue.clear()
}
/**
* 获取下载进度
*/
public getDownloadProgress(downloadId: string): number {
// 这里可以实现获取具体下载进度的逻辑
return 0
}
}
// 创建默认实例
export const fileDownloader = new FileDownloader()

344
src/api/core/request.ts Normal file
View File

@ -0,0 +1,344 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ApiResponse, RequestConfig, ApiError } from '../types'
import { API_CONFIG, ENV_CONFIG } from '../config'
import { createApiError, retry } from '../utils'
/**
* HTTP请求客户端类
*/
export class HttpClient {
private instance: AxiosInstance
private requestQueue: Map<string, AbortController> = new Map()
constructor() {
this.instance = this.createInstance()
this.setupInterceptors()
}
/**
* 创建axios实例
*/
private createInstance(): AxiosInstance {
return axios.create({
baseURL: API_CONFIG.BASE_URL,
timeout: API_CONFIG.TIMEOUT,
headers: API_CONFIG.DEFAULT_HEADERS,
})
}
/**
* 设置拦截器
*/
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
// 添加请求ID用于取消请求
const requestId = this.generateRequestId(config)
config.metadata = { requestId }
// 添加认证token
const token = this.getAuthToken()
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
// 开发环境下打印请求信息
if (ENV_CONFIG.isDev) {
console.log('🚀 Request:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
})
}
return config
},
(error) => {
return Promise.reject(this.handleError(error))
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
// 清理请求队列
const requestId = response.config.metadata?.requestId
if (requestId) {
this.requestQueue.delete(requestId)
}
// 开发环境下打印响应信息
if (ENV_CONFIG.isDev) {
console.log('✅ Response:', {
url: response.config.url,
status: response.status,
data: response.data,
})
}
// 直接返回response在具体的请求方法中处理数据格式
return response
},
(error) => {
// 清理请求队列
const requestId = error.config?.metadata?.requestId
if (requestId) {
this.requestQueue.delete(requestId)
}
return Promise.reject(this.handleError(error))
}
)
}
/**
* 处理响应数据
*/
private handleResponse<T>(response: AxiosResponse): ApiResponse<T> {
const { data } = response
// 如果后端返回的数据已经是标准格式
if (data && typeof data.code !== 'undefined') {
return {
code: data.code,
message: data.message || 'Success',
data: data.data,
success: data.code === API_CONFIG.BUSINESS_CODES.SUCCESS,
}
}
// 否则包装成标准格式
return {
code: API_CONFIG.BUSINESS_CODES.SUCCESS,
message: 'Success',
data: data,
success: true,
}
}
/**
* 处理错误
*/
private handleError(error: any): ApiError {
if (ENV_CONFIG.isDev) {
console.error('❌ Request Error:', error)
}
// 请求被取消
if (axios.isCancel(error)) {
return createApiError(-1, '请求已取消')
}
// 网络错误
if (!error.response) {
return createApiError(-1, '网络连接失败,请检查网络设置')
}
const { status, data } = error.response as AxiosResponse
// 根据HTTP状态码处理
switch (status) {
case API_CONFIG.STATUS_CODES.UNAUTHORIZED:
this.handleUnauthorized()
return createApiError(status, '登录已过期,请重新登录')
case API_CONFIG.STATUS_CODES.FORBIDDEN:
return createApiError(status, '没有权限访问该资源')
case API_CONFIG.STATUS_CODES.NOT_FOUND:
return createApiError(status, '请求的资源不存在')
case API_CONFIG.STATUS_CODES.SERVER_ERROR:
return createApiError(status, '服务器内部错误')
default:
return createApiError(
status,
data?.message || `请求失败 (${status})`,
data
)
}
}
/**
* 处理未授权
*/
private handleUnauthorized(): void {
// 清除token
this.clearAuthToken()
// 可以在这里添加跳转到登录页的逻辑
// router.push('/login')
}
/**
* 生成请求ID
*/
private generateRequestId(config: AxiosRequestConfig): string {
return `${config.method}_${config.url}_${Date.now()}`
}
/**
* 获取认证token
*/
private getAuthToken(): string | null {
return localStorage.getItem('auth_token')
}
/**
* 清除认证token
*/
private clearAuthToken(): void {
localStorage.removeItem('auth_token')
}
/**
* 设置认证token
*/
public setAuthToken(token: string): void {
localStorage.setItem('auth_token', token)
}
/**
* 通用请求方法
*/
public async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
const axiosConfig: AxiosRequestConfig = {
url: config.url,
method: config.method || 'GET',
params: config.params,
data: config.data,
headers: config.headers,
timeout: config.timeout,
responseType: config.responseType || 'json',
}
const response = await this.instance.request(axiosConfig)
return this.handleResponse<T>(response)
}
/**
* 带重试的请求方法
*/
public async requestWithRetry<T = any>(
config: RequestConfig,
retries?: number
): Promise<ApiResponse<T>> {
return retry(() => this.request<T>(config), retries)
}
/**
* GET请求
*/
public get<T = any>(
url: string,
params?: Record<string, any>,
config?: Partial<RequestConfig>
): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'GET',
params,
...config,
})
}
/**
* POST请求
*/
public post<T = any>(
url: string,
data?: any,
config?: Partial<RequestConfig>
): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'POST',
data,
...config,
})
}
/**
* PUT请求
*/
public put<T = any>(
url: string,
data?: any,
config?: Partial<RequestConfig>
): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'PUT',
data,
...config,
})
}
/**
* DELETE请求
*/
public delete<T = any>(
url: string,
params?: Record<string, any>,
config?: Partial<RequestConfig>
): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'DELETE',
params,
...config,
})
}
/**
* PATCH请求
*/
public patch<T = any>(
url: string,
data?: any,
config?: Partial<RequestConfig>
): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'PATCH',
data,
...config,
})
}
/**
* 取消请求
*/
public cancelRequest(requestId: string): void {
const controller = this.requestQueue.get(requestId)
if (controller) {
controller.abort()
this.requestQueue.delete(requestId)
}
}
/**
* 取消所有请求
*/
public cancelAllRequests(): void {
this.requestQueue.forEach((controller) => {
controller.abort()
})
this.requestQueue.clear()
}
}
// 创建默认实例
export const httpClient = new HttpClient()
// 扩展axios配置类型
declare module 'axios' {
interface AxiosRequestConfig {
metadata?: {
requestId: string
}
}
}

362
src/api/core/upload.ts Normal file
View File

@ -0,0 +1,362 @@
import { AxiosRequestConfig } from 'axios'
import { httpClient } from './request'
import { UploadConfig, ApiResponse } from '../types'
import { API_CONFIG, API_ENDPOINTS } from '../config'
import {
validateFileType,
formatFileSize,
compressImage,
readFileAsBase64,
generateId
} from '../utils'
/**
* 文件上传响应类型
*/
export interface UploadResponse {
fileId: string
filename: string
url: string
size: number
type: string
uploadTime: string
}
/**
* 批量上传响应类型
*/
export interface BatchUploadResponse {
success: UploadResponse[]
failed: Array<{
file: File
error: string
}>
total: number
successCount: number
failedCount: number
}
/**
* 文件上传类
*/
export class FileUploader {
private uploadQueue: Map<string, AbortController> = new Map()
/**
* 单文件上传
*/
public async uploadFile(config: UploadConfig): Promise<ApiResponse<UploadResponse>> {
const {
file,
filename = file instanceof File ? file.name : 'blob',
fieldName = 'file',
data = {},
onProgress
} = config
// 创建FormData
const formData = new FormData()
formData.append(fieldName, file, filename)
// 添加额外数据
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
// 生成上传ID
const uploadId = generateId()
const controller = new AbortController()
this.uploadQueue.set(uploadId, controller)
try {
const response = await httpClient.request<UploadResponse>({
url: API_ENDPOINTS.FILE.UPLOAD,
method: 'POST',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 0, // 上传不设置超时
responseType: 'json',
})
return response
} finally {
this.uploadQueue.delete(uploadId)
}
}
/**
* 图片上传(带压缩)
*/
public async uploadImage(
file: File,
options: {
compress?: boolean
quality?: number
maxWidth?: number
maxHeight?: number
allowedTypes?: string[]
maxSize?: number
onProgress?: (progress: number) => void
} = {}
): Promise<ApiResponse<UploadResponse>> {
const {
compress = true,
quality = 0.8,
maxWidth = 1920,
maxHeight = 1080,
allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'],
maxSize = 10 * 1024 * 1024, // 10MB
onProgress
} = options
// 验证文件类型
if (!validateFileType(file, allowedTypes)) {
throw new Error(`不支持的文件类型,仅支持: ${allowedTypes.join(', ')}`)
}
// 验证文件大小
if (file.size > maxSize) {
throw new Error(`文件大小超出限制,最大支持: ${formatFileSize(maxSize)}`)
}
let uploadFile = file
// 压缩图片
if (compress && file.type.startsWith('image/')) {
try {
const compressedBlob = await compressImage(file, quality, maxWidth, maxHeight)
uploadFile = new File([compressedBlob], file.name, { type: file.type })
} catch (error) {
console.warn('图片压缩失败,使用原文件上传:', error)
}
}
return this.uploadFile({
file: uploadFile,
onProgress
})
}
/**
* Base64上传
*/
public async uploadBase64(
file: File,
options: {
onProgress?: (progress: number) => void
} = {}
): Promise<ApiResponse<UploadResponse>> {
const { onProgress } = options
try {
const base64 = await readFileAsBase64(file)
const response = await httpClient.post<UploadResponse>(
API_ENDPOINTS.FILE.UPLOAD,
{
filename: file.name,
content: base64,
type: file.type,
size: file.size,
encoding: 'base64'
}
)
return response
} catch (error) {
throw new Error(`Base64上传失败: ${error}`)
}
}
/**
* 分片上传(大文件)
*/
public async uploadLargeFile(
file: File,
options: {
chunkSize?: number
onProgress?: (progress: number) => void
onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
} = {}
): Promise<ApiResponse<UploadResponse>> {
const {
chunkSize = 2 * 1024 * 1024, // 2MB per chunk
onProgress,
onChunkProgress
} = options
const totalChunks = Math.ceil(file.size / chunkSize)
const uploadId = generateId()
const uploadedChunks: string[] = []
try {
// 初始化分片上传
const initResponse = await httpClient.post('/file/upload/init', {
filename: file.name,
fileSize: file.size,
totalChunks,
uploadId
})
if (!initResponse.success) {
throw new Error(initResponse.message)
}
// 上传分片
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', i.toString())
formData.append('uploadId', uploadId)
const chunkResponse = await httpClient.post('/file/upload/chunk', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
if (chunkResponse.success) {
uploadedChunks.push(chunkResponse.data.chunkId)
// 分片进度回调
onChunkProgress?.(i, 100)
// 总进度回调
const progress = ((i + 1) / totalChunks) * 100
onProgress?.(progress)
} else {
throw new Error(`分片 ${i} 上传失败: ${chunkResponse.message}`)
}
}
// 合并分片
const mergeResponse = await httpClient.post<UploadResponse>('/file/upload/merge', {
uploadId,
filename: file.name,
chunks: uploadedChunks
})
return mergeResponse
} catch (error) {
// 清理失败的上传
try {
await httpClient.post('/file/upload/cleanup', { uploadId })
} catch (cleanupError) {
console.warn('清理上传失败:', cleanupError)
}
throw error
}
}
/**
* 批量上传
*/
public async uploadBatch(
files: File[],
options: {
concurrent?: number
onProgress?: (progress: number) => void
onFileProgress?: (file: File, progress: number) => void
onFileComplete?: (file: File, result: UploadResponse | Error) => void
} = {}
): Promise<BatchUploadResponse> {
const {
concurrent = 3,
onProgress,
onFileProgress,
onFileComplete
} = options
const results: BatchUploadResponse = {
success: [],
failed: [],
total: files.length,
successCount: 0,
failedCount: 0
}
const uploadFile = async (file: File): Promise<void> => {
try {
const response = await this.uploadFile({
file,
onProgress: (progress) => onFileProgress?.(file, progress)
})
if (response.success) {
results.success.push(response.data)
results.successCount++
} else {
results.failed.push({
file,
error: response.message
})
results.failedCount++
}
onFileComplete?.(file, response.data)
} catch (error) {
results.failed.push({
file,
error: error instanceof Error ? error.message : '上传失败'
})
results.failedCount++
onFileComplete?.(file, error as Error)
}
// 更新总进度
const completed = results.successCount + results.failedCount
const progress = (completed / results.total) * 100
onProgress?.(progress)
}
// 并发上传
const chunks = []
for (let i = 0; i < files.length; i += concurrent) {
chunks.push(files.slice(i, i + concurrent))
}
for (const chunk of chunks) {
await Promise.all(chunk.map(uploadFile))
}
return results
}
/**
* 取消上传
*/
public cancelUpload(uploadId: string): void {
const controller = this.uploadQueue.get(uploadId)
if (controller) {
controller.abort()
this.uploadQueue.delete(uploadId)
}
}
/**
* 取消所有上传
*/
public cancelAllUploads(): void {
this.uploadQueue.forEach((controller) => {
controller.abort()
})
this.uploadQueue.clear()
}
/**
* 获取上传进度
*/
public getUploadProgress(uploadId: string): number {
// 这里可以实现获取具体上传进度的逻辑
return 0
}
}
// 创建默认实例
export const fileUploader = new FileUploader()

417
src/api/examples/usage.ts Normal file
View File

@ -0,0 +1,417 @@
/**
* API使用示例
* 展示如何使用重构后的API架构
*/
import {
editorApi,
resourceApi,
fileUploader,
fileDownloader,
ApiManager,
type ProjectData,
type ResourceData
} from '../index'
/**
* 编辑器API使用示例
*/
export class EditorApiExamples {
/**
* 创建订单示例
*/
static async createOrderExample() {
try {
const response = await editorApi.createOrder({
projectId: 'project-123',
type: 'render',
options: {
quality: 'high',
format: 'png'
}
})
if (response.success) {
console.log('订单创建成功:', response.data)
return response.data
} else {
console.error('订单创建失败:', response.message)
}
} catch (error) {
console.error('创建订单时发生错误:', error)
}
}
/**
* 保存项目示例
*/
static async saveProjectExample() {
try {
const projectData: Partial<ProjectData> = {
name: '我的3D项目',
description: '这是一个示例项目',
sceneData: {
objects: [],
lights: [],
camera: {}
}
}
const response = await editorApi.saveProject(projectData)
if (response.success) {
console.log('项目保存成功:', response.data)
return response.data
} else {
console.error('项目保存失败:', response.message)
}
} catch (error) {
console.error('保存项目时发生错误:', error)
}
}
/**
* 上传缩略图示例
*/
static async uploadThumbnailExample(file: File, projectId: string) {
try {
const response = await editorApi.uploadThumbnail(file, projectId)
if (response.success) {
console.log('缩略图上传成功:', response.data)
return response.data
} else {
console.error('缩略图上传失败:', response.message)
}
} catch (error) {
console.error('上传缩略图时发生错误:', error)
}
}
/**
* 导出项目示例
*/
static async exportProjectExample(projectId: string) {
try {
await editorApi.exportProject(projectId, 'glb')
console.log('项目导出完成')
} catch (error) {
console.error('导出项目时发生错误:', error)
}
}
}
/**
* 资源API使用示例
*/
export class ResourceApiExamples {
/**
* 上传资源示例
*/
static async uploadResourceExample(file: File) {
try {
const response = await resourceApi.uploadResource(file, {
type: 'model',
tags: ['建筑', '现代'],
description: '现代建筑模型'
})
if (response.success) {
console.log('资源上传成功:', response.data)
return response.data
} else {
console.error('资源上传失败:', response.message)
}
} catch (error) {
console.error('上传资源时发生错误:', error)
}
}
/**
* 批量上传资源示例
*/
static async uploadMultipleResourcesExample(files: File[]) {
try {
const response = await resourceApi.uploadMultipleResources(files, {
type: 'texture',
tags: ['材质', '金属']
})
if (response.success) {
console.log('批量上传成功:', response.data)
return response.data
} else {
console.error('批量上传失败:', response.message)
}
} catch (error) {
console.error('批量上传时发生错误:', error)
}
}
/**
* 搜索资源示例
*/
static async searchResourcesExample() {
try {
const response = await resourceApi.searchResources({
keyword: '建筑',
type: 'model',
tags: ['现代'],
page: 1,
pageSize: 20
})
if (response.success) {
console.log('搜索结果:', response.data)
return response.data
} else {
console.error('搜索失败:', response.message)
}
} catch (error) {
console.error('搜索资源时发生错误:', error)
}
}
/**
* 下载资源示例
*/
static async downloadResourceExample(resource: ResourceData) {
try {
await resourceApi.downloadResource(resource)
console.log('资源下载完成')
} catch (error) {
console.error('下载资源时发生错误:', error)
}
}
/**
* 批量下载资源示例
*/
static async downloadMultipleResourcesExample(resources: ResourceData[]) {
try {
await resourceApi.downloadMultipleResources(resources)
console.log('批量下载完成')
} catch (error) {
console.error('批量下载时发生错误:', error)
}
}
}
/**
* 文件上传使用示例
*/
export class FileUploadExamples {
/**
* 普通文件上传示例
*/
static async uploadFileExample(file: File) {
try {
const response = await fileUploader.uploadFile({
file,
onProgress: (progress) => {
console.log(`上传进度: ${progress}%`)
}
})
if (response.success) {
console.log('文件上传成功:', response.data)
return response.data
} else {
console.error('文件上传失败:', response.message)
}
} catch (error) {
console.error('上传文件时发生错误:', error)
}
}
/**
* 图片上传示例(带压缩)
*/
static async uploadImageExample(file: File) {
try {
const response = await fileUploader.uploadImage(file, {
compress: true,
quality: 0.8,
maxWidth: 1920,
maxHeight: 1080,
onProgress: (progress) => {
console.log(`图片上传进度: ${progress}%`)
}
})
if (response.success) {
console.log('图片上传成功:', response.data)
return response.data
} else {
console.error('图片上传失败:', response.message)
}
} catch (error) {
console.error('上传图片时发生错误:', error)
}
}
/**
* 大文件分片上传示例
*/
static async uploadLargeFileExample(file: File) {
try {
const response = await fileUploader.uploadLargeFile(file, {
chunkSize: 2 * 1024 * 1024, // 2MB per chunk
onProgress: (progress) => {
console.log(`大文件上传进度: ${progress}%`)
},
onChunkProgress: (chunkIndex, chunkProgress) => {
console.log(`分片 ${chunkIndex} 上传进度: ${chunkProgress}%`)
}
})
if (response.success) {
console.log('大文件上传成功:', response.data)
return response.data
} else {
console.error('大文件上传失败:', response.message)
}
} catch (error) {
console.error('上传大文件时发生错误:', error)
}
}
/**
* 批量文件上传示例
*/
static async uploadBatchExample(files: File[]) {
try {
const result = await fileUploader.uploadBatch(files, {
concurrent: 3,
onProgress: (progress) => {
console.log(`批量上传总进度: ${progress}%`)
},
onFileProgress: (file, progress) => {
console.log(`文件 ${file.name} 上传进度: ${progress}%`)
},
onFileComplete: (file, result) => {
if (result instanceof Error) {
console.error(`文件 ${file.name} 上传失败:`, result)
} else {
console.log(`文件 ${file.name} 上传成功`)
}
}
})
console.log('批量上传结果:', result)
return result
} catch (error) {
console.error('批量上传时发生错误:', error)
}
}
}
/**
* 文件下载使用示例
*/
export class FileDownloadExamples {
/**
* 普通文件下载示例
*/
static async downloadFileExample(url: string, filename?: string) {
try {
await fileDownloader.downloadAndSave({
url,
filename,
onProgress: (progress) => {
console.log(`下载进度: ${progress}%`)
}
})
console.log('文件下载完成')
} catch (error) {
console.error('下载文件时发生错误:', error)
}
}
/**
* 下载为Base64示例
*/
static async downloadAsBase64Example(url: string) {
try {
const base64 = await fileDownloader.downloadAsBase64({ url })
console.log('文件下载为Base64完成:', base64.substring(0, 100) + '...')
return base64
} catch (error) {
console.error('下载为Base64时发生错误:', error)
}
}
/**
* 批量下载示例
*/
static async downloadBatchExample(urls: string[]) {
try {
const result = await fileDownloader.downloadBatch(urls, {
concurrent: 3,
onProgress: (progress) => {
console.log(`批量下载总进度: ${progress}%`)
},
onFileProgress: (url, progress) => {
console.log(`文件 ${url} 下载进度: ${progress}%`)
},
onFileComplete: (url, result) => {
if (result instanceof Error) {
console.error(`文件 ${url} 下载失败:`, result)
} else {
console.log(`文件 ${url} 下载成功`)
}
}
})
console.log('批量下载结果:', result)
return result
} catch (error) {
console.error('批量下载时发生错误:', error)
}
}
}
/**
* API管理器使用示例
*/
export class ApiManagerExamples {
/**
* 设置认证token示例
*/
static setAuthTokenExample(token: string) {
ApiManager.setAuthToken(token)
console.log('认证token已设置')
}
/**
* 检查网络连接示例
*/
static async checkConnectionExample() {
try {
const isConnected = await ApiManager.checkConnection()
console.log('网络连接状态:', isConnected ? '已连接' : '未连接')
return isConnected
} catch (error) {
console.error('检查网络连接时发生错误:', error)
return false
}
}
/**
* 获取API配置示例
*/
static getConfigExample() {
const config = ApiManager.getConfig()
console.log('API配置:', config)
return config
}
/**
* 取消所有请求示例
*/
static cancelAllRequestsExample() {
ApiManager.cancelAllRequests()
console.log('所有请求已取消')
}
}

116
src/api/index.ts Normal file
View File

@ -0,0 +1,116 @@
// 核心模块
export { httpClient, HttpClient } from './core/request'
export { fileUploader, FileUploader } from './core/upload'
export { fileDownloader, FileDownloader } from './core/download'
// 业务API模块
export { editorApi, EditorApi } from './modules/editor'
export { resourceApi, ResourceApi } from './modules/resource'
// 类型定义
export type {
ApiResponse,
RequestConfig,
UploadConfig,
DownloadConfig,
PaginationParams,
PaginationResponse,
ApiError
} from './types'
// 配置
export { API_CONFIG, API_ENDPOINTS, ENV_CONFIG } from './config'
// 工具函数
export {
delay,
generateId,
formatFileSize,
getFileExtension,
validateFileType,
createApiError,
isApiError,
retry,
downloadFile,
readFileAsBase64,
readFileAsArrayBuffer,
compressImage
} from './utils'
// 业务类型
export type {
ProjectData,
OrderData
} from './modules/editor'
export type {
ResourceData,
ResourceCategory,
ResourceSearchParams
} from './modules/resource'
// 上传下载相关类型
export type {
UploadResponse,
BatchUploadResponse
} from './core/upload'
export type {
DownloadResponse,
BatchDownloadResponse
} from './core/download'
// 导入实例用于ApiManager
import { httpClient } from './core/request'
import { fileUploader } from './core/upload'
import { fileDownloader } from './core/download'
import { API_CONFIG, ENV_CONFIG } from './config'
/**
* API管理器类
* 提供统一的API管理和配置
*/
export class ApiManager {
/**
* 设置认证token
*/
public static setAuthToken(token: string): void {
httpClient.setAuthToken(token)
}
/**
* 取消所有请求
*/
public static cancelAllRequests(): void {
httpClient.cancelAllRequests()
fileUploader.cancelAllUploads()
fileDownloader.cancelAllDownloads()
}
/**
* 获取API配置
*/
public static getConfig() {
return {
baseUrl: API_CONFIG.BASE_URL,
timeout: API_CONFIG.TIMEOUT,
retryCount: API_CONFIG.RETRY_COUNT,
env: ENV_CONFIG
}
}
/**
* 检查网络连接
*/
public static async checkConnection(): Promise<boolean> {
try {
const response = await httpClient.get('/health')
return response.success
} catch {
return false
}
}
}
// 默认导出API管理器
export default ApiManager

143
src/api/modules/editor.ts Normal file
View File

@ -0,0 +1,143 @@
import { httpClient } from '../core/request'
import { fileUploader } from '../core/upload'
import { fileDownloader } from '../core/download'
import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
import { API_ENDPOINTS } from '../config'
/**
* 项目数据类型
*/
export interface ProjectData {
id: string
name: string
description?: string
thumbnail?: string
sceneData: any
createdAt: string
updatedAt: string
userId: string
}
/**
* 订单数据类型
*/
export interface OrderData {
id: string
projectId: string
status: 'pending' | 'processing' | 'completed' | 'failed'
result?: any
createdAt: string
updatedAt: string
}
/**
* 编辑器API类
*/
export class EditorApi {
/**
* 创建订单
*/
public async createOrder(params: any): Promise<ApiResponse<OrderData>> {
return httpClient.post<OrderData>(API_ENDPOINTS.EDITOR.CREATE_ORDER, params)
}
/**
* 保存项目
*/
public async saveProject(projectData: Partial<ProjectData>): Promise<ApiResponse<ProjectData>> {
return httpClient.post<ProjectData>(API_ENDPOINTS.EDITOR.SAVE_PROJECT, projectData)
}
/**
* 加载项目
*/
public async loadProject(projectId: string): Promise<ApiResponse<ProjectData>> {
return httpClient.get<ProjectData>(`${API_ENDPOINTS.EDITOR.LOAD_PROJECT}/${projectId}`)
}
/**
* 删除项目
*/
public async deleteProject(projectId: string): Promise<ApiResponse<void>> {
return httpClient.delete<void>(`${API_ENDPOINTS.EDITOR.DELETE_PROJECT}/${projectId}`)
}
/**
* 获取项目列表
*/
public async getProjectList(params: PaginationParams): Promise<ApiResponse<PaginationResponse<ProjectData>>> {
return httpClient.get<PaginationResponse<ProjectData>>(API_ENDPOINTS.EDITOR.LOAD_PROJECT, params)
}
/**
* 上传项目缩略图
*/
public async uploadThumbnail(file: File, projectId: string): Promise<ApiResponse<{ url: string }>> {
return fileUploader.uploadImage(file, {
compress: true,
quality: 0.8,
maxWidth: 400,
maxHeight: 300,
onProgress: (progress) => {
console.log(`缩略图上传进度: ${progress}%`)
}
})
}
/**
* 导出项目
*/
public async exportProject(projectId: string, format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
await fileDownloader.downloadAndSave({
url: `/editor/project/export/${projectId}`,
filename: `project_${projectId}.${format}`,
params: { format }
})
}
/**
* 导入项目
*/
public async importProject(file: File): Promise<ApiResponse<ProjectData>> {
const uploadResponse = await fileUploader.uploadFile({
file,
fieldName: 'projectFile',
data: {
action: 'import'
}
})
if (!uploadResponse.success) {
throw new Error(uploadResponse.message)
}
// 上传成功后调用导入API
return httpClient.post<ProjectData>('/editor/project/import', {
fileUrl: uploadResponse.data.url
})
}
/**
* 批量导出项目
*/
public async exportMultipleProjects(projectIds: string[], format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
const downloadUrls = projectIds.map(id => `/editor/project/export/${id}?format=${format}`)
await fileDownloader.downloadBatch(downloadUrls, {
concurrent: 2,
onProgress: (progress) => {
console.log(`批量导出进度: ${progress}%`)
},
onFileComplete: (url, result) => {
if (result instanceof Error) {
console.error(`导出失败: ${url}`, result)
} else {
console.log(`导出完成: ${url}`)
}
}
})
}
}
// 创建默认实例
export const editorApi = new EditorApi()

281
src/api/modules/resource.ts Normal file
View File

@ -0,0 +1,281 @@
import { httpClient } from '../core/request'
import { fileUploader } from '../core/upload'
import { fileDownloader } from '../core/download'
import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
import { API_ENDPOINTS } from '../config'
/**
* 资源数据类型
*/
export interface ResourceData {
id: string
name: string
type: 'model' | 'texture' | 'material' | 'audio' | 'video' | 'other'
url: string
thumbnailUrl?: string
size: number
format: string
tags: string[]
description?: string
createdAt: string
updatedAt: string
userId: string
}
/**
* 资源分类类型
*/
export interface ResourceCategory {
id: string
name: string
description?: string
parentId?: string
children?: ResourceCategory[]
}
/**
* 资源搜索参数
*/
export interface ResourceSearchParams extends PaginationParams {
keyword?: string
type?: ResourceData['type']
tags?: string[]
categoryId?: string
format?: string
sizeMin?: number
sizeMax?: number
dateFrom?: string
dateTo?: string
}
/**
* 资源API类
*/
export class ResourceApi {
/**
* 获取资源列表
*/
public async getResourceList(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
return httpClient.get<PaginationResponse<ResourceData>>(API_ENDPOINTS.RESOURCE.LIST, params)
}
/**
* 获取资源详情
*/
public async getResourceDetail(resourceId: string): Promise<ApiResponse<ResourceData>> {
return httpClient.get<ResourceData>(`${API_ENDPOINTS.RESOURCE.LIST}/${resourceId}`)
}
/**
* 上传资源
*/
public async uploadResource(
file: File,
metadata: {
name?: string
type: ResourceData['type']
tags?: string[]
description?: string
categoryId?: string
}
): Promise<ApiResponse<ResourceData>> {
// 根据文件类型选择上传方式
let uploadResponse
if (metadata.type === 'texture' && file.type.startsWith('image/')) {
// 图片资源,使用图片上传(带压缩)
uploadResponse = await fileUploader.uploadImage(file, {
compress: true,
quality: 0.9,
maxWidth: 2048,
maxHeight: 2048,
onProgress: (progress) => {
console.log(`资源上传进度: ${progress}%`)
}
})
} else if (file.size > 50 * 1024 * 1024) {
// 大文件,使用分片上传
uploadResponse = await fileUploader.uploadLargeFile(file, {
onProgress: (progress) => {
console.log(`大文件上传进度: ${progress}%`)
}
})
} else {
// 普通文件上传
uploadResponse = await fileUploader.uploadFile({
file,
data: metadata,
onProgress: (progress) => {
console.log(`文件上传进度: ${progress}%`)
}
})
}
if (!uploadResponse.success) {
throw new Error(uploadResponse.message)
}
// 创建资源记录
return httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
...metadata,
name: metadata.name || file.name,
url: uploadResponse.data.url,
size: file.size,
format: file.name.split('.').pop()?.toLowerCase() || 'unknown'
})
}
/**
* 批量上传资源
*/
public async uploadMultipleResources(
files: File[],
metadata: {
type: ResourceData['type']
tags?: string[]
categoryId?: string
}
): Promise<ApiResponse<ResourceData[]>> {
const results = await fileUploader.uploadBatch(files, {
concurrent: 3,
onProgress: (progress) => {
console.log(`批量上传进度: ${progress}%`)
},
onFileComplete: (file, result) => {
if (result instanceof Error) {
console.error(`文件上传失败: ${file.name}`, result)
} else {
console.log(`文件上传完成: ${file.name}`)
}
}
})
// 创建资源记录
const resourcePromises = results.success.map(uploadResult =>
httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
...metadata,
name: uploadResult.filename,
url: uploadResult.url,
size: uploadResult.size,
format: uploadResult.filename.split('.').pop()?.toLowerCase() || 'unknown'
})
)
const resourceResults = await Promise.all(resourcePromises)
const successResources = resourceResults
.filter(result => result.success)
.map(result => result.data)
return {
code: 0,
message: 'Success',
data: successResources,
success: true
}
}
/**
* 删除资源
*/
public async deleteResource(resourceId: string): Promise<ApiResponse<void>> {
return httpClient.delete<void>(`${API_ENDPOINTS.RESOURCE.DELETE}/${resourceId}`)
}
/**
* 批量删除资源
*/
public async deleteMultipleResources(resourceIds: string[]): Promise<ApiResponse<void>> {
return httpClient.post<void>('/resource/batch-delete', { resourceIds })
}
/**
* 更新资源信息
*/
public async updateResource(
resourceId: string,
data: Partial<Pick<ResourceData, 'name' | 'tags' | 'description'>>
): Promise<ApiResponse<ResourceData>> {
return httpClient.put<ResourceData>(`/resource/${resourceId}`, data)
}
/**
* 下载资源
*/
public async downloadResource(resource: ResourceData): Promise<void> {
await fileDownloader.downloadAndSave({
url: resource.url,
filename: resource.name
})
}
/**
* 批量下载资源
*/
public async downloadMultipleResources(resources: ResourceData[]): Promise<void> {
const downloadFiles = resources.map(resource => ({
url: resource.url,
filename: resource.name
}))
await fileDownloader.downloadAsZip(downloadFiles, 'resources.zip')
}
/**
* 获取资源分类
*/
public async getResourceCategories(): Promise<ApiResponse<ResourceCategory[]>> {
return httpClient.get<ResourceCategory[]>('/resource/categories')
}
/**
* 创建资源分类
*/
public async createResourceCategory(
data: Pick<ResourceCategory, 'name' | 'description' | 'parentId'>
): Promise<ApiResponse<ResourceCategory>> {
return httpClient.post<ResourceCategory>('/resource/categories', data)
}
/**
* 搜索资源
*/
public async searchResources(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
return httpClient.get<PaginationResponse<ResourceData>>('/resource/search', params)
}
/**
* 获取热门标签
*/
public async getPopularTags(limit: number = 20): Promise<ApiResponse<string[]>> {
return httpClient.get<string[]>('/resource/tags/popular', { limit })
}
/**
* 获取资源统计信息
*/
public async getResourceStats(): Promise<ApiResponse<{
total: number
byType: Record<ResourceData['type'], number>
totalSize: number
recentUploads: number
}>> {
return httpClient.get('/resource/stats')
}
/**
* 预览资源获取预览URL
*/
public async getResourcePreview(resourceId: string): Promise<ApiResponse<{ previewUrl: string }>> {
return httpClient.get<{ previewUrl: string }>(`/resource/${resourceId}/preview`)
}
/**
* 生成资源缩略图
*/
public async generateThumbnail(resourceId: string): Promise<ApiResponse<{ thumbnailUrl: string }>> {
return httpClient.post<{ thumbnailUrl: string }>(`/resource/${resourceId}/thumbnail`)
}
}
// 创建默认实例
export const resourceApi = new ResourceApi()

58
src/api/types/index.ts Normal file
View File

@ -0,0 +1,58 @@
// 通用响应类型
export interface ApiResponse<T = any> {
code: number
message: string
data: T
success: boolean
}
// 请求配置类型
export interface RequestConfig {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
params?: Record<string, any>
data?: any
headers?: Record<string, string>
timeout?: number
responseType?: 'json' | 'blob' | 'text' | 'arraybuffer'
}
// 文件上传配置
export interface UploadConfig {
file: File | Blob
filename?: string
fieldName?: string
data?: Record<string, any>
onProgress?: (progress: number) => void
}
// 文件下载配置
export interface DownloadConfig {
url: string
filename?: string
params?: Record<string, any>
onProgress?: (progress: number) => void
}
// 分页请求参数
export interface PaginationParams {
page: number
pageSize: number
[key: string]: any
}
// 分页响应数据
export interface PaginationResponse<T> {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
// 错误类型
export interface ApiError {
code: number
message: string
details?: any
}

184
src/api/utils/index.ts Normal file
View File

@ -0,0 +1,184 @@
import { ApiError } from '../types'
import { API_CONFIG } from '../config'
/**
* 延迟函数
*/
export const delay = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 生成唯一ID
*/
export const generateId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
/**
* 格式化文件大小
*/
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
/**
* 获取文件扩展名
*/
export const getFileExtension = (filename: string): string => {
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2)
}
/**
* 验证文件类型
*/
export const validateFileType = (file: File, allowedTypes: string[]): boolean => {
const fileType = file.type
const fileExtension = getFileExtension(file.name).toLowerCase()
return allowedTypes.some(type => {
if (type.includes('/')) {
return fileType === type
}
return fileExtension === type.toLowerCase()
})
}
/**
* 创建API错误对象
*/
export const createApiError = (
code: number,
message: string,
details?: any
): ApiError => {
return {
code,
message,
details
}
}
/**
* 判断是否为API错误
*/
export const isApiError = (error: any): error is ApiError => {
return error && typeof error.code === 'number' && typeof error.message === 'string'
}
/**
* 重试函数
*/
export const retry = async <T>(
fn: () => Promise<T>,
retries: number = API_CONFIG.RETRY_COUNT,
delayMs: number = API_CONFIG.RETRY_DELAY
): Promise<T> => {
try {
return await fn()
} catch (error) {
if (retries > 0) {
await delay(delayMs)
return retry(fn, retries - 1, delayMs * 2) // 指数退避
}
throw error
}
}
/**
* 下载文件
*/
export const downloadFile = (blob: Blob, filename: string): void => {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
}
/**
* 读取文件为Base64
*/
export const readFileAsBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const result = reader.result as string
resolve(result.split(',')[1]) // 移除data:xxx;base64,前缀
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}
/**
* 读取文件为ArrayBuffer
*/
export const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
/**
* 压缩图片
*/
export const compressImage = (
file: File,
quality: number = 0.8,
maxWidth: number = 1920,
maxHeight: number = 1080
): Promise<Blob> => {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = () => {
// 计算压缩后的尺寸
let { width, height } = img
if (width > maxWidth) {
height = (height * maxWidth) / width
width = maxWidth
}
if (height > maxHeight) {
width = (width * maxHeight) / height
height = maxHeight
}
canvas.width = width
canvas.height = height
// 绘制并压缩
ctx?.drawImage(img, 0, 0, width, height)
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('图片压缩失败'))
}
},
file.type,
quality
)
}
img.onerror = reject
img.src = URL.createObjectURL(file)
})
}

Binary file not shown.

BIN
src/assets/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,46 @@
.inspectortotip {
margin-top: 5px;
color: rgba(250, 92, 0, 0.712);
width: 200px;
margin-left: 20px;
font-size: 12px;
margin-bottom: 5px;
}
.container {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.slider-them {
position: absolute;
pointer-events: auto;
cursor: ew-resize;
width: 20px;
height: 40px;
background-color: #006eff;
opacity: 0.7;
border-radius: 1rem;
top: calc(50% - 20px);
left: calc(50% - 10px);
}
.slider {
position: absolute;
pointer-events: auto;
cursor: ew-resize;
width: 40px;
height: 60px;
background-color: #7777773a;
opacity: 0.7;
border-radius: 1rem;
top: calc(50% - 20px);
left: calc(50% - 10px);
}

View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,391 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967FF, #B500FE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe660;</span>
<div class="name">全屏</div>
<div class="code-name">&amp;#xe660;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe620;</span>
<div class="name">帮助</div>
<div class="code-name">&amp;#xe620;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xeb72;</span>
<div class="name">帮助_o</div>
<div class="code-name">&amp;#xeb72;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60b;</span>
<div class="name">半透明2</div>
<div class="code-name">&amp;#xe60b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe71a;</span>
<div class="name">持续时间</div>
<div class="code-name">&amp;#xe71a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60c;</span>
<div class="name">对比</div>
<div class="code-name">&amp;#xe60c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe604;</span>
<div class="name">还原</div>
<div class="code-name">&amp;#xe604;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe70a;</span>
<div class="name">vr</div>
<div class="code-name">&amp;#xe70a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe51b;</span>
<div class="name">持续时间</div>
<div class="code-name">&amp;#xe51b;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持两种方式引用多色图标SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.ttf?t=1733881413198') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-quanping"></span>
<div class="name">
全屏
</div>
<div class="code-name">.icon-quanping
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-bangzhu"></span>
<div class="name">
帮助
</div>
<div class="code-name">.icon-bangzhu
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-bangzhu_o"></span>
<div class="name">
帮助_o
</div>
<div class="code-name">.icon-bangzhu_o
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-bantouming"></span>
<div class="name">
半透明2
</div>
<div class="code-name">.icon-bantouming
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-chixushijian"></span>
<div class="name">
持续时间
</div>
<div class="code-name">.icon-chixushijian
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-duibi"></span>
<div class="name">
对比
</div>
<div class="code-name">.icon-duibi
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-huanyuan"></span>
<div class="name">
还原
</div>
<div class="code-name">.icon-huanyuan
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-vr"></span>
<div class="name">
vr
</div>
<div class="code-name">.icon-vr
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-chixushijian1"></span>
<div class="name">
持续时间
</div>
<div class="code-name">.icon-chixushijian1
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-quanping"></use>
</svg>
<div class="name">全屏</div>
<div class="code-name">#icon-quanping</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-bangzhu"></use>
</svg>
<div class="name">帮助</div>
<div class="code-name">#icon-bangzhu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-bangzhu_o"></use>
</svg>
<div class="name">帮助_o</div>
<div class="code-name">#icon-bangzhu_o</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-bantouming"></use>
</svg>
<div class="name">半透明2</div>
<div class="code-name">#icon-bantouming</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-chixushijian"></use>
</svg>
<div class="name">持续时间</div>
<div class="code-name">#icon-chixushijian</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-duibi"></use>
</svg>
<div class="name">对比</div>
<div class="code-name">#icon-duibi</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-huanyuan"></use>
</svg>
<div class="name">还原</div>
<div class="code-name">#icon-huanyuan</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-vr"></use>
</svg>
<div class="name">vr</div>
<div class="code-name">#icon-vr</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-chixushijian1"></use>
</svg>
<div class="name">持续时间</div>
<div class="code-name">#icon-chixushijian1</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,49 @@
@font-face {
font-family: "iconfont"; /* Project id */
src: url('iconfont.ttf?t=1733881413198') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-quanping:before {
content: "\e660";
}
.icon-bangzhu:before {
content: "\e620";
}
.icon-bangzhu_o:before {
content: "\eb72";
}
.icon-bantouming:before {
content: "\e60b";
}
.icon-chixushijian:before {
content: "\e71a";
}
.icon-duibi:before {
content: "\e60c";
}
.icon-huanyuan:before {
content: "\e604";
}
.icon-vr:before {
content: "\e70a";
}
.icon-chixushijian1:before {
content: "\e51b";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,72 @@
{
"id": "",
"name": "",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "807275",
"name": "全屏",
"font_class": "quanping",
"unicode": "e660",
"unicode_decimal": 58976
},
{
"icon_id": "1304879",
"name": "帮助",
"font_class": "bangzhu",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "5387852",
"name": "帮助_o",
"font_class": "bangzhu_o",
"unicode": "eb72",
"unicode_decimal": 60274
},
{
"icon_id": "8883075",
"name": "半透明2",
"font_class": "bantouming",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "11768105",
"name": "持续时间",
"font_class": "chixushijian",
"unicode": "e71a",
"unicode_decimal": 59162
},
{
"icon_id": "15726777",
"name": "对比",
"font_class": "duibi",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "16731254",
"name": "还原",
"font_class": "huanyuan",
"unicode": "e604",
"unicode_decimal": 58884
},
{
"icon_id": "22273778",
"name": "vr",
"font_class": "vr",
"unicode": "e70a",
"unicode_decimal": 59146
},
{
"icon_id": "42514699",
"name": "持续时间",
"font_class": "chixushijian1",
"unicode": "e51b",
"unicode_decimal": 58651
}
]
}

Binary file not shown.

104
src/assets/style/global.css Normal file
View File

@ -0,0 +1,104 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #1e1e1e;
color: #cccccc;
overflow: hidden;
}
#app {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
/* 按钮基础样式 */
button {
border: none;
background: none;
cursor: pointer;
font-family: inherit;
}
/* 输入框基础样式 */
input, textarea, select {
font-family: inherit;
background: #2d2d2d;
border: 1px solid #4a4a4a;
color: #cccccc;
border-radius: 4px;
padding: 8px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #007acc;
}
/* 加载动画 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.loading::after {
content: '';
width: 32px;
height: 32px;
border: 3px solid #4a4a4a;
border-top: 3px solid #007acc;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 文本选择 */
::selection {
background: #007acc;
color: white;
}
/* 禁用文本选择的元素 */
.no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

View File

@ -0,0 +1,90 @@
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
background: url('/loading/加载页背景图.jpg') no-repeat center center;
background-size: 100% 100%;
}
.loading-content {
width: 230px;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
margin-top: -70px;
}
.logo {
width: 152px;
font-size: 32px;
font-weight: bold;
margin-bottom: 50px;
color: #333;
position: relative;
}
.logo img{
width: 100%;
}
.truck-container {
position: relative;
width: 100%;
height: 40px;
margin-top: -40px; /* 向上移动,与进度条重叠 */
}
.progress-container {
width: 100%;
height: 6px;
background-color: rgba(255, 255, 255, 0.37);
border-radius: 6px;
overflow: hidden;
position: relative;
margin-bottom: 20px;
padding: 2px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center; /* 保持垂直居中 */
}
.progress-bar {
height: 4px; /* 保持这个高度 */
background-color: #1087d6;
transition: width 0.3s;
border-radius: 3px;
/* 移除这两行,它们导致了中间扩散效果 */
/* margin: 0 auto; */
/* align-self: center; */
/* 添加这行确保从左边开始 */
margin-left: 0;
}
.truck {
position: absolute;
top: -16px; /* 相对于truck-container定位 */
transform: translateX(-90%); /* 改为-90%,让小车更靠左一些 */
transition: left 0.3s;
z-index: 10;
}
.truck img {
display: block;
width: 80px;
height: auto;
}
.loading-text {
font-size: 14px;
color: #333;
}

286
src/assets/style/media.css Normal file
View File

@ -0,0 +1,286 @@
@charset "UTF-8";
/* 移动设备 */
@media screen and (max-width: 350px) {
.title {
font-size: 16px !important;
}
}
/* 移动设备 */
@media screen and (max-width: 640px) {
.loaderbox img {
width: 200px !important;
}
.jiudu {
min-width: 70px;
height: 36px;
}
/* 横屏*/
@media screen and (max-height: 766px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 5rem;
height: 5rem;
}
.dialog-item .Bar-Line {
width: calc(5rem + 6px);
height: calc(5rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 5px 5px
}
.TopInfo-content .title {
font-size: 24px;
}
}
/* 竖屏*/
@media screen and (min-height: 768px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 5rem;
height: 5rem;
}
.dialog-item .Bar-Line {
width: calc(5rem + 6px);
height: calc(5rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 5px 5px
}
.TopInfo-content .title {
font-size: 20px;
}
.ColorBar1-container {
top: 20px;
}
/* .ColorBar-container {
left: 0;
top: 20px;
flex-direction: column;
justify-content: space-evenly;
} */
}
.return {
width: 40px;
height: 40px;
}
}
/* 移动设备 */
@media screen and (max-width: 767px) and (min-width: 640px) {
/* 横屏*/
@media screen and (max-height: 400px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 7rem;
height: 7rem;
}
.dialog-item .Bar-Line {
width: calc(7rem + 6px);
height: calc(7rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 30px 10px !important;
}
.dialog-bottom {
height: 30px;
}
}
/* 横屏*/
@media screen and (max-height: 766px) and (min-height: 400px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 9rem;
height: 9rem;
}
.dialog-item .Bar-Line {
width: calc(9rem + 6px);
height: calc(9rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 5px 5px
}
}
/* 竖屏*/
@media screen and (min-height: 768px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 10rem;
height: 10rem;
}
.dialog-item .Bar-Line {
width: calc(10rem + 6px);
height: calc(10rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 5px 5px
}
.TopInfo-content .title {
font-size: 24px;
}
}
.return {
width: 50px;
height: 50px;
}
}
/* 电脑设备 */
@media screen and (min-width: 768px) {
/* 横屏*/
@media screen and (max-height: 400px) {
.ui-bottom {
bottom: 20px;
right: 10px;
/* 通过偏移实现居中 */
}
.dialog-item {
width: 7rem;
height: 7rem;
}
.dialog-item .Bar-Line {
width: calc(7rem + 6px);
height: calc(7rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.dialog-body {
padding: 30px 10px !important;
}
}
/* 横屏*/
@media screen and (min-height: 400px) {
.ui-bottom {
right: 20px;
bottom: 0px;
}
.dialog-item {
width: 12.5rem;
height: 12.5rem;
}
.dialog-item .Bar-Line {
width: calc(12.5rem + 6px);
height: calc(12.5rem + 6px);
box-shadow: 0 0 0 1px #fff;
}
.ColorBar1-container {
top: 40px;
}
.jiudu {
min-width: 5rem;
height: 2rem;
}
}
.return {
width: 50px;
height: 50px;
}
}

View File

@ -0,0 +1,6 @@
html,
body {
overflow: hidden;
margin : 0;
padding : 0;
}

View File

@ -0,0 +1,8 @@
.property-label {
width: 60px;
color: #cccccc;
font-size: 11px;
flex-shrink: 0;
text-align: left;
cursor: help;
}

View File

@ -0,0 +1,414 @@
<template>
<div class="field-container" :class="{
'field-disabled': disabled,
'field-drag-over': isDragOver,
'field-has-value': hasValue
}" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
<!-- 字段标签 -->
<div class="field-label" v-if="label">
{{ label }}
</div>
<!-- 字段内容区域 -->
<div class="field-content">
<!-- 有值时显示的内容 -->
<div v-if="hasValue" class="field-value">
<!-- 图标 -->
<div class="field-icon">
<component :is="getIconComponent()" />
</div>
<!-- 名称 -->
<div class="field-name">
{{ modelValue?.name || 'Unnamed' }}
</div>
<!-- 清除按钮 -->
<button class="field-clear" @click="clearValue" :disabled="disabled">
×
</button>
</div>
<!-- 空值时显示的占位符 -->
<div v-else class="field-placeholder">
<div class="field-icon-placeholder">
<component :is="getIconComponent()" />
</div>
<div class="field-text">
None ({{ acceptedType }})
</div>
</div>
<!-- 拖拽提示 -->
<div v-if="isDragOver" class="field-drag-hint">
<div class="drag-hint-text">
{{ getDragHintText() }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import type { FieldProps, FieldEmits, FieldValue, AcceptedType } from './types'
import { userDragStore } from 'stores/Filed'
import { storeToRefs } from 'pinia'
import { useCursorStore } from '@/stores/Cursor'
const dragStore = userDragStore()
const cursorStore = useCursorStore()
const { dragType, dragData } = storeToRefs(dragStore)
// 图标组件
const TextureIcon = () => h('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'currentColor'
}, [
h('path', {
d: 'M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z'
})
])
const MeshIcon = () => h('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'currentColor'
}, [
h('path', {
d: 'M12,2L13.09,8.26L22,9L17.74,15.74L19,22L12,19L5,22L6.26,15.74L2,9L10.91,8.26L12,2Z'
})
])
const MaterialIcon = () => h('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'currentColor'
}, [
h('path', {
d: 'M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z'
})
])
const props = withDefaults(defineProps<FieldProps>(), {
disabled: false,
acceptedType: 'Texture',
allowClear: true
})
const emit = defineEmits<FieldEmits>()
// 状态
const isDragOver = ref(false)
// 计算属性
const hasValue = computed(() => props.modelValue !== null && props.modelValue !== undefined)
// 获取图标组件
const getIconComponent = () => {
switch (props.acceptedType) {
case 'Texture':
return TextureIcon
case 'Mesh':
return MeshIcon
case 'Material':
return MaterialIcon
default:
return TextureIcon
}
}
// 获取拖拽提示文本
const getDragHintText = () => {
return `Drop ${props.acceptedType} here`
}
// 检查拖拽的元素是否为支持的类型
const isValidDragItem = (dragData: any): boolean => {
if (!dragData) return false
// 检查类型是否匹配
if (dragData.type !== props.acceptedType) {
return false
}
// 可以添加更多的验证逻辑
// 例如检查文件扩展名、MIME类型等
if (props.acceptedType === 'Texture') {
const validExtensions = ['.png', '.jpg', '.jpeg', '.tga', '.bmp', '.gif']
const fileName = dragData.name || ''
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
}
if (props.acceptedType === 'Mesh') {
const validExtensions = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
const fileName = dragData.name || ''
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
}
if (props.acceptedType === 'Material') {
const validExtensions = ['.mat', '.material']
const fileName = dragData.name || ''
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
}
return true
}
// 拖拽事件处理
const handleDragOver = (event: DragEvent) => {
if (props.disabled) return
try {
const match = dragType.value == props.acceptedType
if (match) {
isDragOver.value = true
cursorStore.setCursor('default')
} else {
cursorStore.setCursor('not-allowed')
}
} catch {
// 如果无法解析数据,则拒绝拖拽
cursorStore.setCursor('not-allowed')
}
}
const handleDragLeave = () => {
isDragOver.value = false
}
const handleDrop = (event: DragEvent) => {
if (props.disabled) return
isDragOver.value = false
try {
const match = dragType.value == props.acceptedType
if (match) {
if (isValidDragItem(dragData.value)) {
const fieldValue: FieldValue = {
id: dragData.value.id || Date.now().toString(),
name: dragData.value.name || 'Unnamed',
type: dragData.value.type,
path: dragData.value.path || '',
metadata: dragData.value.metadata || {}
}
emit('update:modelValue', fieldValue)
emit('change', fieldValue)
} else {
// 发出错误事件
emit('error', {
type: 'invalid_type',
message: `Cannot assign ${dragData.value.type || 'unknown'} to ${props.acceptedType} field`,
expectedType: props.acceptedType,
actualType: dragData.value.type
})
}
} else {
cursorStore.setCursor('not-allowed')
}
} catch {
// 如果无法解析数据,则拒绝拖拽
cursorStore.setCursor('not-allowed')
}
// try {
// const dragData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}')
// if (isValidDragItem(dragData)) {
// const fieldValue: FieldValue = {
// id: dragData.id || Date.now().toString(),
// name: dragData.name || 'Unnamed',
// type: dragData.type,
// path: dragData.path || '',
// metadata: dragData.metadata || {}
// }
// emit('update:modelValue', fieldValue)
// emit('change', fieldValue)
// } else {
// // 发出错误事件
// emit('error', {
// type: 'invalid_type',
// message: `Cannot assign ${dragData.type || 'unknown'} to ${props.acceptedType} field`,
// expectedType: props.acceptedType,
// actualType: dragData.type
// })
// }
// } catch (error) {
// emit('error', {
// type: 'parse_error',
// message: 'Failed to parse drag data',
// error
// })
// }
}
// 清除值
const clearValue = () => {
if (props.disabled || !props.allowClear) return
emit('update:modelValue', null)
emit('change', null)
emit('clear')
}
</script>
<style scoped>
.field-container {
display: flex;
flex-direction: column;
gap: 2px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 11px;
}
.field-label {
color: #cccccc;
font-size: 10px;
font-weight: 500;
margin-bottom: 2px;
}
.field-content {
position: relative;
min-height: 20px;
background: #2d2d2d;
border: 1px solid #4a4a4a;
border-radius: 2px;
transition: all 0.2s ease;
}
.field-container.field-drag-over .field-content {
border-color: #409eff;
background: #353535;
box-shadow: 0 0 0 1px rgba(64, 158, 255, 0.2);
}
.field-container.field-disabled .field-content {
opacity: 0.6;
cursor: not-allowed;
}
.field-container.field-has-value .field-content {
border-color: #5a5a5a;
}
.field-value {
display: flex;
align-items: center;
padding: 3px 6px;
gap: 6px;
height: 100%;
}
.field-placeholder {
display: flex;
align-items: center;
padding: 3px 6px;
gap: 6px;
height: 100%;
color: #888888;
}
.field-icon,
.field-icon-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
color: #888888;
}
.field-has-value .field-icon {
color: #cccccc;
}
.field-name {
flex: 1;
color: #cccccc;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field-text {
flex: 1;
font-size: 10px;
font-style: italic;
}
.field-clear {
width: 12px;
height: 12px;
background: none;
border: none;
color: #888888;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1px;
font-size: 12px;
line-height: 1;
transition: all 0.2s ease;
flex-shrink: 0;
}
.field-clear:hover {
background: #4a4a4a;
color: #cccccc;
}
.field-clear:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.field-drag-hint {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(64, 158, 255, 0.1);
border: 2px dashed #409eff;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.drag-hint-text {
color: #409eff;
font-size: 9px;
font-weight: 500;
}
/* 悬停效果 */
.field-content:hover {
border-color: #5a5a5a;
}
.field-container.field-disabled .field-content:hover {
border-color: #4a4a4a;
}
/* 焦点效果 */
.field-content:focus-within {
border-color: #409eff;
box-shadow: 0 0 0 1px rgba(64, 158, 255, 0.2);
}
</style>

View File

@ -0,0 +1,34 @@
// Field 组件类型定义
export type AcceptedType = 'Texture' | 'Mesh' | 'Material'
export interface FieldValue {
id: string
name: string
type: AcceptedType
path: string
metadata?: Record<string, any>
}
export interface FieldError {
type: 'invalid_type' | 'parse_error'
message: string
expectedType?: AcceptedType
actualType?: string
error?: any
}
export interface FieldProps {
modelValue?: FieldValue | null
label?: string
acceptedType?: AcceptedType
disabled?: boolean
allowClear?: boolean
}
export interface FieldEmits {
'update:modelValue': [value: FieldValue | null]
'change': [value: FieldValue | null]
'clear': []
'error': [error: FieldError]
}

View File

@ -0,0 +1,163 @@
/* FileNode 组件样式 - 文件夹形式 */
.file-node {
display: flex;
flex-direction: column;
align-items: center;
padding: 2px 2px;
cursor: pointer;
user-select: none;
border-radius: 6px;
position: relative;
/* background: #353535; */
border: 2px solid transparent;
}
.file-node:hover {
background-color: #404040;
}
.file-node.dragging {
opacity: 0.5;
transform: scale(0.95);
}
.file-node.drag-over {
background-color: rgba(64, 158, 255, 0.2);
border: 2px dashed #409eff;
}
/* 拖拽指示器 */
.file-node.drag-over-before::before {
content: '';
position: absolute;
top: -2px;
left: 0;
right: 0;
height: 2px;
background-color: #409eff;
border-radius: 1px;
}
.file-node.drag-over-after::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background-color: #409eff;
border-radius: 1px;
}
/* 文件图标 */
.file-node-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
margin-bottom: 4px;
flex-shrink: 0;
}
/* 文件名 */
.file-node-name {
font-size: 11px;
color: #cccccc;
text-align: center;
word-break: break-word;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 2px 4px;
border-radius: 3px;
}
/* 选中效果只应用到文件名 */
.file-node.selected .file-node-name {
background-color: #2e70b3b7;
color: #ffffff;
}
.file-node-name.editing {
/* background-color: #2d2d2d;
border: 1px solid #409eff; */
border-radius: 2px;
padding: 2px 4px;
outline: none;
width: 90%;
text-align: center;
}
/* 文件大小 */
.file-node-size {
font-size: 10px;
color: #888;
margin-top: 4px;
text-align: center;
}
/* 拖拽鼠标样式 */
.drag-cursor-grab {
cursor: grab !important;
}
.drag-cursor-grabbing {
cursor: grabbing !important;
}
.drag-cursor-copy {
cursor: copy !important;
}
.drag-cursor-move {
cursor: move !important;
}
.drag-cursor-not-allowed {
cursor: not-allowed !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.file-node {
padding: 8px 6px;
min-height: 70px;
width: 80px;
}
.file-node-icon {
font-size: 28px;
margin-bottom: 6px;
}
.file-node-name {
font-size: 10px;
}
}
/* 滚动条样式 */
.file-node-container::-webkit-scrollbar {
width: 8px;
}
.file-node-container::-webkit-scrollbar-track {
background: #2d2d2d;
}
.file-node-container::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
.file-node-container::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}

View File

@ -0,0 +1,5 @@
// 导出类型和常量
export * from './types'
// 导出组件
export { default } from './index.vue'

View File

@ -0,0 +1,258 @@
<template>
<div
:class="nodeClasses"
:style="nodeStyles"
@click="handleClick"
@dblclick="handleDoubleClick"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
:draggable="draggable"
>
<!-- 文件图标 -->
<div class="file-node-icon">
{{ FILE_ICONS[node.type] }}
</div>
<!-- 文件名 -->
<div
v-if="!isEditing"
class="file-node-name"
@dblclick.stop="startEdit"
>
{{ node.name }}
</div>
<input
v-else
ref="editInput"
v-model="editName"
class="file-node-name editing"
@blur="finishEdit"
@keyup.enter="finishEdit"
@keyup.esc="cancelEdit"
@click.stop
/>
<!-- 文件大小可选显示 -->
<div
v-if="showSize && node.size !== undefined"
class="file-node-size"
>
{{ formatFileSize(node.size) }}
</div>
<!-- 修改时间 -->
<div
v-if="showDetails && node.lastModified"
class="file-node-date"
>
{{ formatDate(node.lastModified) }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch } from 'vue'
import { FileNode, FileNodeType, FILE_ICONS, DRAG_CURSORS } from './types'
import { formatFileSize, formatDate } from '../../../utils/Tools'
import './index.css'
interface Props {
node: FileNode
depth?: number
draggable?: boolean
showDetails?: boolean
selectedId?: string
dragState?: {
isDragging: boolean
dragNode: FileNode | null
dragOverNode: FileNode | null
dropPosition: 'before' | 'after' | 'inside' | null
}
showSize?: boolean
}
interface Emits {
(e: 'click', node: FileNode): void
(e: 'double-click', node: FileNode): void
(e: 'expand', node: FileNode): void
(e: 'rename', node: FileNode, newName: string): void
(e: 'drag-start', node: FileNode, event: DragEvent): void
(e: 'drag-end', node: FileNode, event: DragEvent): void
(e: 'drag-over', node: FileNode, event: DragEvent): void
(e: 'drag-leave', node: FileNode, event: DragEvent): void
(e: 'drop', targetNode: FileNode, dragNode: FileNode, position: string, event: DragEvent): void
}
const props = withDefaults(defineProps<Props>(), {
depth: 0,
draggable: true,
showDetails: false,
showSize: false
})
const emit = defineEmits<Emits>()
// 编辑状态
const isEditing = ref(false)
const editName = ref('')
const editInput = ref<HTMLInputElement>()
// 计算属性
const hasChildren = computed(() => {
return props.node.children && props.node.children.length > 0
})
const nodeClasses = computed(() => {
const classes = [
'file-node',
`type-${props.node.type.toLowerCase()}`
]
if (props.node.isSelected || props.selectedId === props.node.id) {
classes.push('selected')
}
if (props.node.isDragging) {
classes.push('dragging')
}
if (props.dragState?.dragOverNode?.id === props.node.id) {
classes.push('drag-over')
if (props.dragState.dropPosition === 'before') {
classes.push('drag-over-before')
} else if (props.dragState.dropPosition === 'after') {
classes.push('drag-over-after')
}
}
return classes
})
const nodeStyles = computed(() => {
const styles: Record<string, string> = {}
if (props.dragState?.isDragging) {
if (props.dragState.dragNode?.id === props.node.id) {
styles.cursor = DRAG_CURSORS.GRABBING
} else {
styles.cursor = DRAG_CURSORS.GRAB
}
}
return styles
})
// 事件处理
const handleClick = (event: MouseEvent) => {
if (!isEditing.value) {
emit('click', props.node)
}
}
const handleDoubleClick = (event: MouseEvent) => {
if (!isEditing.value) {
emit('double-click', props.node)
}
}
const toggleExpand = () => {
if (props.node.type === FileNodeType.Folder) {
emit('expand', props.node)
}
}
// 编辑功能
const startEdit = () => {
if (props.node.type !== FileNodeType.Folder) {
isEditing.value = true
editName.value = props.node.name
nextTick(() => {
editInput.value?.focus()
editInput.value?.select()
})
}
}
const finishEdit = () => {
if (editName.value.trim() && editName.value !== props.node.name) {
emit('rename', props.node, editName.value.trim())
}
isEditing.value = false
}
const cancelEdit = () => {
editName.value = props.node.name
isEditing.value = false
}
// 拖拽功能
const handleDragStart = (event: DragEvent) => {
if (!props.draggable) {
event.preventDefault()
return
}
event.dataTransfer!.effectAllowed = 'move'
event.dataTransfer!.setData('text/plain', props.node.id)
// 设置拖拽图像
const dragImage = event.currentTarget as HTMLElement
event.dataTransfer!.setDragImage(dragImage, 0, 0)
emit('drag-start', props.node, event)
}
const handleDragEnd = (event: DragEvent) => {
emit('drag-end', props.node, event)
}
const handleDragOver = (event: DragEvent) => {
event.preventDefault()
event.dataTransfer!.dropEffect = 'move'
// 计算拖拽位置
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const y = event.clientY - rect.top
const height = rect.height
let position: 'before' | 'after' | 'inside' = 'inside'
if (props.node.type === FileNodeType.Folder) {
if (y < height * 0.25) {
position = 'before'
} else if (y > height * 0.75) {
position = 'after'
} else {
position = 'inside'
}
} else {
position = y < height * 0.5 ? 'before' : 'after'
}
emit('drag-over', props.node, event)
}
const handleDragLeave = (event: DragEvent) => {
emit('drag-leave', props.node, event)
}
const handleDrop = (event: DragEvent) => {
event.preventDefault()
const dragNodeId = event.dataTransfer!.getData('text/plain')
if (dragNodeId && props.dragState?.dragNode) {
const position = props.dragState.dropPosition || 'inside'
emit('drop', props.node, props.dragState.dragNode, position, event)
}
}
// 监听选中状态变化
watch(() => props.selectedId, (newId) => {
if (newId !== props.node.id && isEditing.value) {
cancelEdit()
}
})
</script>

View File

@ -0,0 +1,82 @@
// 文件节点类型枚举
export enum FileNodeType {
Text = 'Text',
Folder = 'Folder',
Model = 'Model',
Audio = 'Audio',
Video = 'Video',
Material = 'Material',
Texture = 'Texture',
Image = 'Image',
Script = 'Script',
Scene = 'Scene'
}
// 文件节点接口
export interface FileNode {
id: string
name: string
type: FileNodeType
path: string
size?: number
lastModified?: Date
children?: FileNode[]
parent?: string
isExpanded?: boolean
isSelected?: boolean
isDragging?: boolean
}
// 拖拽状态
export interface DragState {
isDragging: boolean
dragNode: FileNode | null
dragOverNode: FileNode | null
dropPosition: 'before' | 'after' | 'inside' | null
}
// 文件图标映射
export const FILE_ICONS: Record<FileNodeType, string> = {
[FileNodeType.Text]: '📄',
[FileNodeType.Folder]: '📁',
[FileNodeType.Model]: '🎯',
[FileNodeType.Audio]: '🎵',
[FileNodeType.Video]: '🎬',
[FileNodeType.Material]: '🎨',
[FileNodeType.Texture]: '🖼️',
[FileNodeType.Image]: '🖼️',
[FileNodeType.Script]: '📜',
[FileNodeType.Scene]: '🌍'
}
// 文件扩展名映射
export const FILE_EXTENSIONS: Record<string, FileNodeType> = {
'.txt': FileNodeType.Text,
'.md': FileNodeType.Text,
'.js': FileNodeType.Script,
'.ts': FileNodeType.Script,
'.vue': FileNodeType.Script,
'.fbx': FileNodeType.Model,
'.obj': FileNodeType.Model,
'.gltf': FileNodeType.Model,
'.glb': FileNodeType.Model,
'.mp3': FileNodeType.Audio,
'.wav': FileNodeType.Audio,
'.mp4': FileNodeType.Video,
'.avi': FileNodeType.Video,
'.png': FileNodeType.Image,
'.jpg': FileNodeType.Image,
'.jpeg': FileNodeType.Image,
'.gif': FileNodeType.Image,
'.mat': FileNodeType.Material,
'.material': FileNodeType.Material
}
// 拖拽鼠标样式
export const DRAG_CURSORS = {
GRAB: 'grab',
GRABBING: 'grabbing',
COPY: 'copy',
MOVE: 'move',
NOT_ALLOWED: 'not-allowed'
}

View File

@ -0,0 +1,104 @@
<template>
<div class="color-input">
<div class="color-preview" :style="{ backgroundColor: modelValue }" @click="openColorPicker"></div>
<input
ref="colorInput"
type="color"
:value="modelValue"
@input="updateColor"
class="color-picker"
/>
<input
type="text"
:value="modelValue"
@input="updateColorText"
@blur="validateColor"
class="color-text"
placeholder="#ffffff"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 本地类型定义
interface ColorInputProps {
modelValue: string
}
interface ColorInputEmits {
(e: 'update:modelValue', value: string): void
}
const props = defineProps<ColorInputProps>()
const emit = defineEmits<ColorInputEmits>()
const colorInput = ref<HTMLInputElement>()
const openColorPicker = () => {
colorInput.value?.click()
}
const updateColor = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const updateColorText = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const validateColor = (event: Event) => {
const target = event.target as HTMLInputElement
const value = target.value
// 验证颜色格式
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
target.value = props.modelValue
}
}
</script>
<style scoped>
.color-input {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.color-preview {
width: 20px;
height: 20px;
border: 1px solid #333333;
border-radius: 2px;
cursor: pointer;
flex-shrink: 0;
}
.color-picker {
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
}
.color-text {
flex: 1;
min-width: 0;
padding: 2px 4px;
background: #2a2a2a;
border: 1px solid #333333;
border-radius: 2px;
color: #ffffff;
font-size: 11px;
font-family: 'Consolas', 'Monaco', monospace;
}
.color-text:focus {
outline: none;
border-color: #007acc;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<input
type="text"
:value="modelValue"
@input="handleInput"
@focus="onFocus"
:placeholder="placeholder"
:class="['text-input', { 'center': textAlign === 'center', 'uppercase': uppercase }]"
/>
</template>
<script setup lang="ts">
import type { TextInputProps, TextInputEmits } from './types'
const props = withDefaults(defineProps<TextInputProps>(), {
textAlign: 'left',
uppercase: false
})
const emit = defineEmits<TextInputEmits>()
const handleInput = (e: Event) => {
const target = e.target as HTMLInputElement
emit('update:modelValue', target.value)
}
const onFocus = (e: Event) => {
(e.target as HTMLInputElement).select()
}
</script>
<style scoped>
.text-input {
width: 100%;
height: 16px;
background: #2d2d2d;
border: 1px solid #4a4a4a;
border-radius: 2px;
color: #cccccc;
font-size: 11px;
padding: 0 3px;
text-align: left;
outline: none;
font-family: inherit;
box-sizing: border-box;
}
.text-input.center {
text-align: center;
}
.text-input.uppercase {
text-transform: uppercase;
}
.text-input:focus {
border-color: #409eff;
background: #353535;
}
.text-input:hover {
border-color: #5a5a5a;
}
/* 容器自适应 */
@container (max-width: 200px) {
.text-input {
font-size: 10px;
}
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<input
type="number"
:value="modelValue"
@input="handleInput"
@focus="onFocus"
:step="step"
:min="min"
:max="max"
:placeholder="placeholder"
:class="['number-input', { 'full-width': fullWidth }]"
/>
</template>
<script setup lang="ts">
import type { NumberInputProps, NumberInputEmits } from './types'
const props = withDefaults(defineProps<NumberInputProps>(), {
step: 0.1,
fullWidth: false
})
const emit = defineEmits<NumberInputEmits>()
const handleInput = (e: Event) => {
const target = e.target as HTMLInputElement
const value = parseFloat(target.value)
if (!isNaN(value)) {
emit('update:modelValue', value)
}
}
const onFocus = (e: Event) => {
(e.target as HTMLInputElement).select()
}
</script>
<style scoped>
.number-input {
width: 100%;
min-width: 30px;
height: 16px;
background: #2d2d2d;
border: 1px solid #4a4a4a;
border-radius: 2px;
color: #cccccc;
font-size: 11px;
padding: 0 3px;
text-align: right;
outline: none;
font-family: inherit;
box-sizing: border-box;
}
.number-input.full-width {
max-width: none;
}
.number-input:focus {
border-color: #409eff;
background: #353535;
}
.number-input:hover {
border-color: #5a5a5a;
}
/* 隐藏数字输入框的箭头 */
.number-input::-webkit-outer-spin-button,
.number-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.number-input[type="number"] {
-moz-appearance: textfield;
}
/* 容器自适应 */
@container (max-width: 200px) {
.number-input {
font-size: 10px;
}
}
@media (max-width: 250px) {
.number-input {
max-width: none;
}
}
</style>

View File

@ -0,0 +1,34 @@
// NumberInput 组件类型
export interface NumberInputProps {
modelValue: number
step?: number
min?: number
max?: number
placeholder?: string
fullWidth?: boolean
}
export interface NumberInputEmits {
(e: 'update:modelValue', value: number): void
}
// TextInput 组件类型
export interface TextInputProps {
modelValue: string
placeholder?: string
textAlign?: 'left' | 'center' | 'right'
uppercase?: boolean
}
export interface TextInputEmits {
(e: 'update:modelValue', value: string): void
}
// ColorInput 组件类型
export interface ColorInputProps {
modelValue: string
}
export interface ColorInputEmits {
(e: 'update:modelValue', value: string): void
}

View File

@ -0,0 +1,69 @@
<template>
<div class="property-row">
<Tooltip v-if="tooltip" :content="tooltip" placement="top">
<div class="property-label">{{ label }}</div>
</Tooltip>
<div v-else class="property-label">{{ label }}</div>
<div class="property-content">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import Tooltip from '../../public/Tooltip.vue'
import type { PropertyRowProps } from './types'
defineProps<PropertyRowProps>()
</script>
<style scoped>
.property-row {
display: flex;
align-items: center;
margin-bottom: 2px;
min-height: 18px;
gap: 8px;
}
.property-label {
width: 80px;
color: #cccccc;
font-size: 11px;
flex-shrink: 0;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.property-content {
flex: 1;
min-width: 0;
}
/* 响应式设计 */
@media (max-width: 250px) {
.property-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.property-label {
width: 100%;
}
.property-content {
width: 100%;
}
}
/* 容器自适应 */
@container (max-width: 200px) {
.property-label {
width: 60px;
font-size: 10px;
}
}
</style>

View File

@ -0,0 +1,5 @@
// PropertyRow 组件类型定义
export interface PropertyRowProps {
label: string
tooltip?: string
}

View File

@ -0,0 +1,66 @@
<template>
<select
:value="modelValue"
@change="handleChange"
class="select-input"
>
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</template>
<script setup lang="ts">
import type { SelectProps, SelectEmits } from './types'
defineProps<SelectProps>()
const emit = defineEmits<SelectEmits>()
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement
const value = target.value
emit('update:modelValue', value)
emit('change', value)
}
</script>
<style scoped>
.select-input {
width: 100%;
height: 18px;
background: #2d2d2d;
border: 1px solid #4a4a4a;
border-radius: 2px;
color: #cccccc;
font-size: 11px;
padding: 0 3px;
outline: none;
font-family: inherit;
box-sizing: border-box;
}
.select-input option {
background: #2d2d2d;
color: #cccccc;
}
.select-input:focus {
border-color: #409eff;
background: #353535;
}
.select-input:hover {
border-color: #5a5a5a;
}
/* 容器自适应 */
@container (max-width: 200px) {
.select-input {
font-size: 10px;
}
}
</style>

View File

@ -0,0 +1,15 @@
// Select 组件类型定义
export interface SelectOption {
value: string | number
label: string
}
export interface SelectProps {
modelValue: string | number
options: SelectOption[]
}
export interface SelectEmits {
(e: 'update:modelValue', value: string | number): void
(e: 'change', value: string | number): void
}

View File

@ -0,0 +1,326 @@
<template>
<div :class="['slider-wrapper', { 'slider-vertical': vertical }]">
<div
ref="sliderTrack"
:class="['slider-track', { 'slider-track-vertical': vertical }]"
@mousedown="handleMouseDown"
>
<div
:class="['slider-fill', { 'slider-fill-vertical': vertical, 'no-transition': isDragging }]"
:style="fillStyle"
></div>
<div
ref="sliderThumb"
:class="['slider-thumb', { 'slider-thumb-vertical': vertical, 'dragging': isDragging }]"
:style="thumbStyle"
@mousedown="handleThumbMouseDown"
></div>
</div>
<!-- 数值显示 -->
<div v-if="showValue && !showInput" class="slider-value">{{ formatValue(modelValue) }}</div>
<!-- 输入框 -->
<NumberInput
v-if="showInput"
:model-value="modelValue"
@update:model-value="handleInputChange"
:min="min"
:max="max"
:step="step"
class="slider-input"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import NumberInput from '../Input/index.vue'
import type { SliderProps, SliderEmits } from './types'
const props = withDefaults(defineProps<SliderProps>(), {
min: 0,
max: 100,
step: 1,
showValue: false,
showInput: true,
vertical: false
})
const emit = defineEmits<SliderEmits>()
const sliderTrack = ref<HTMLElement>()
const sliderThumb = ref<HTMLElement>()
const isDragging = ref(false)
// 计算百分比
const percentage = computed(() => {
const range = props.max - props.min
if (range === 0) return 0
return ((props.modelValue - props.min) / range) * 100
})
// 填充样式
const fillStyle = computed(() => {
if (props.vertical) {
return { height: `${percentage.value}%` }
} else {
return { width: `${percentage.value}%` }
}
})
// 滑块样式
const thumbStyle = computed(() => {
if (props.vertical) {
return { bottom: `calc(${percentage.value}% - 6px)` }
} else {
return { left: `calc(${percentage.value}% - 6px)` }
}
})
// 格式化显示值
const formatValue = (value: number) => {
if (props.step < 1) {
return value.toFixed(2)
} else if (props.step < 0.1) {
return value.toFixed(3)
} else {
return Math.round(value).toString()
}
}
// 处理输入框数值变化
const handleInputChange = (value: number) => {
// 确保值在范围内
const clampedValue = Math.max(props.min, Math.min(props.max, value))
if (clampedValue !== props.modelValue) {
emit('update:modelValue', clampedValue)
emit('change', clampedValue)
}
}
// 处理轨道鼠标按下
const handleMouseDown = (e: MouseEvent) => {
// 如果点击的是滑块,不处理
if (e.target === sliderThumb.value) return
isDragging.value = true
updateValue(e)
// 使用 requestAnimationFrame 优化性能
const handleMove = (e: MouseEvent) => {
if (!isDragging.value) return
requestAnimationFrame(() => updateValue(e))
}
const handleUp = (e: MouseEvent) => {
isDragging.value = false
document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleUp)
e.preventDefault()
}
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleUp)
e.preventDefault()
e.stopPropagation()
}
// 处理滑块鼠标按下
const handleThumbMouseDown = (e: MouseEvent) => {
isDragging.value = true
// 记录初始偏移量
const rect = sliderTrack.value!.getBoundingClientRect()
const thumbRect = sliderThumb.value!.getBoundingClientRect()
let offset = 0
if (props.vertical) {
offset = e.clientY - (thumbRect.top + thumbRect.height / 2)
} else {
offset = e.clientX - (thumbRect.left + thumbRect.width / 2)
}
const handleMove = (e: MouseEvent) => {
if (!isDragging.value) return
// 调整鼠标位置以考虑初始偏移
const adjustedEvent = {
...e,
clientX: props.vertical ? e.clientX : e.clientX - offset,
clientY: props.vertical ? e.clientY - offset : e.clientY
}
requestAnimationFrame(() => updateValue(adjustedEvent as MouseEvent))
}
const handleUp = (e: MouseEvent) => {
isDragging.value = false
document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleUp)
e.preventDefault()
}
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleUp)
e.preventDefault()
e.stopPropagation()
}
// 更新值
const updateValue = (e: MouseEvent) => {
if (!sliderTrack.value) return
const rect = sliderTrack.value.getBoundingClientRect()
let percentage: number
if (props.vertical) {
percentage = (rect.bottom - e.clientY) / rect.height
} else {
percentage = (e.clientX - rect.left) / rect.width
}
// 限制百分比在 0-1 之间
percentage = Math.max(0, Math.min(1, percentage))
// 计算新值
const range = props.max - props.min
let newValue = props.min + percentage * range
// 应用步长
if (props.step > 0) {
newValue = Math.round(newValue / props.step) * props.step
}
// 确保值在范围内
newValue = Math.max(props.min, Math.min(props.max, newValue))
// 修复浮点数精度问题
if (props.step < 1) {
const decimals = props.step.toString().split('.')[1]?.length || 0
newValue = parseFloat(newValue.toFixed(decimals))
}
// 减少不必要的更新
if (Math.abs(newValue - props.modelValue) > 0.0001) {
emit('update:modelValue', newValue)
emit('change', newValue)
}
}
</script>
<style scoped>
.slider-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.slider-wrapper.slider-vertical {
flex-direction: column;
height: 100px;
width: auto;
}
.slider-track {
position: relative;
height: 4px;
background: #4a4a4a;
border-radius: 2px;
cursor: pointer;
flex: 1;
}
.slider-track.slider-track-vertical {
width: 4px;
height: 100%;
flex: none;
}
.slider-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: #409eff;
border-radius: 2px;
transition: width 0.1s ease;
pointer-events: none;
}
.slider-fill.no-transition {
transition: none;
}
.slider-fill.slider-fill-vertical {
bottom: 0;
top: auto;
width: 100%;
transition: height 0.1s ease;
}
.slider-fill.slider-fill-vertical.no-transition {
transition: none;
}
.slider-thumb {
position: absolute;
top: 50%;
width: 12px;
height: 12px;
background: #ffffff;
border: 2px solid #409eff;
border-radius: 50%;
transform: translateY(-50%);
cursor: grab;
transition: all 0.1s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
z-index: 1;
}
.slider-thumb.dragging {
transition: none;
cursor: grabbing;
transform: translateY(-50%) scale(1.1);
}
.slider-thumb.slider-thumb-vertical {
left: 50%;
top: auto;
transform: translateX(-50%);
}
.slider-thumb.slider-thumb-vertical.dragging {
transform: translateX(-50%) scale(1.1);
}
.slider-thumb:hover:not(.dragging) {
transform: translateY(-50%) scale(1.1);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
}
.slider-thumb.slider-thumb-vertical:hover:not(.dragging) {
transform: translateX(-50%) scale(1.1);
}
.slider-thumb:active {
cursor: grabbing;
}
.slider-value {
min-width: 30px;
color: #cccccc;
font-size: 11px;
text-align: center;
user-select: none;
}
.slider-input {
width: 60px;
flex-shrink: 0;
}
</style>

View File

@ -0,0 +1,16 @@
// Slider 组件类型定义
export interface SliderProps {
modelValue: number
min?: number
max?: number
step?: number
showValue?: boolean
showInput?: boolean
vertical?: boolean
a?:number
}
export interface SliderEmits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}

View File

@ -0,0 +1,155 @@
<template>
<div class="switch-wrapper" :class="{ 'switch-disabled': disabled }">
<input
type="checkbox"
:id="switchId"
:checked="modelValue"
:disabled="disabled"
@change="handleChange"
class="switch-checkbox"
/>
<label :for="switchId" class="switch-label" :class="[`switch-${size}`]"></label>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { SwitchProps, SwitchEmits } from './types'
const props = withDefaults(defineProps<SwitchProps>(), {
disabled: false,
size: 'medium'
})
const emit = defineEmits<SwitchEmits>()
// 生成唯一的 ID
const switchId = computed(() => `switch-${Math.random().toString(36).substr(2, 9)}`)
const handleChange = (event: Event) => {
if (props.disabled) return
const target = event.target as HTMLInputElement
const newValue = target.checked
emit('update:modelValue', newValue)
emit('change', newValue)
}
</script>
<style scoped>
.switch-wrapper {
height: 16px;
width: 16px;
display: inline-block;
position: relative;
}
.switch-wrapper.switch-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.switch-checkbox {
opacity: 0;
position: absolute;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.switch-checkbox:disabled {
cursor: not-allowed;
}
.switch-label {
display: inline-block;
background: #2d2d2d;
border: 1px solid #5a5a5a;
border-radius: 2px;
cursor: pointer;
position: relative;
transition: all 0.15s ease;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
}
.switch-label:hover {
border-color: #6a6a6a;
background: #353535;
}
/* 选中状态的勾选标记 */
.switch-checkbox:checked + .switch-label::after {
content: '';
position: absolute;
border: solid #ffffff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
opacity: 1;
}
/* Small size */
.switch-small {
width: 12px;
height: 12px;
}
.switch-checkbox:checked + .switch-small::after {
left: 2px;
top: -1px;
width: 6px;
height: 4px;
border-width: 0 1px 1px 0;
}
/* Medium size (default) */
.switch-medium {
width: 14px;
height: 14px;
}
.switch-checkbox:checked + .switch-medium::after {
left: 2px;
top: 0px;
width: 8px;
height: 6px;
border-width: 0 1.5px 1.5px 0;
}
/* Large size */
.switch-large {
width: 16px;
height: 16px;
}
.switch-checkbox:checked + .switch-large::after {
left: 3px;
top: 1px;
width: 9px;
height: 7px;
border-width: 0 2px 2px 0;
}
/* Focus styles */
.switch-checkbox:focus + .switch-label {
outline: 2px solid #409eff;
outline-offset: 1px;
}
/* Disabled state */
.switch-disabled .switch-checkbox,
.switch-disabled .switch-label {
cursor: not-allowed;
}
.switch-disabled .switch-label:hover {
border-color: #5a5a5a;
background: #2d2d2d;
}
.switch-disabled .switch-checkbox:checked + .switch-label:hover {
background: #409eff;
border-color: #409eff;
}
</style>

View File

@ -0,0 +1,11 @@
// Switch 组件类型定义
export interface SwitchProps {
modelValue: boolean
disabled?: boolean
size?: 'small' | 'medium' | 'large'
}
export interface SwitchEmits {
(e: 'update:modelValue', value: boolean): void
(e: 'change', value: boolean): void
}

View File

@ -0,0 +1,19 @@
// 基础组件导出
export { default as PropertyRow } from './PropertyRow/index.vue'
export { default as NumberInput } from './Input/index.vue'
export { default as TextInput } from './Input/TextInput.vue'
export { default as ColorInput } from './Input/ColorInput.vue'
export { default as Select } from './Select/index.vue'
export { default as Switch } from './Switch/index.vue'
export { default as Slider } from './Slider/index.vue'
export { default as Field } from './Field/index.vue'
// 类型导出
export type { Vector3, RangeValue, SelectOption } from './types'
export type { PropertyRowProps } from './PropertyRow/types'
export type { NumberInputProps, NumberInputEmits, TextInputProps, TextInputEmits } from './Input/types'
export type { SelectProps, SelectEmits } from './Select/types'
export type { SwitchProps, SwitchEmits } from './Switch/types'
export type { SliderProps, SliderEmits } from './Slider/types'
export type { FieldProps, FieldEmits, FieldValue, FieldError, AcceptedType } from './Field/types'

View File

@ -0,0 +1,17 @@
// 基础组件通用类型定义
export interface Vector3 {
x: number
y: number
z: number
}
export interface RangeValue {
min: number
max: number
}
export interface SelectOption {
value: string | number
label: string
}

View File

@ -0,0 +1,747 @@
<template>
<div class="console">
<!-- 控制台头部 -->
<div class="console-header">
<div class="console-title">
<h3>控制台</h3>
<span class="log-count">{{ filteredLogs.length }} 条日志</span>
</div>
<div class="console-actions">
<!-- 日志级别过滤 -->
<div class="log-filters">
<button
v-for="level in logLevels"
:key="level.type"
:class="['filter-btn', level.type, { active: activeFilters.includes(level.type) }]"
@click="toggleFilter(level.type)"
:title="`${level.label} (${getLogCountByType(level.type)})`"
>
<span class="filter-icon">{{ level.icon }}</span>
<span class="filter-count">{{ getLogCountByType(level.type) }}</span>
</button>
</div>
<!-- 控制按钮 -->
<div class="control-buttons">
<button @click="toggleAutoScroll" :class="{ active: autoScroll }" title="自动滚动">
📜
</button>
<button @click="toggleTimestamp" :class="{ active: showTimestamp }" title="显示时间戳">
🕐
</button>
<button @click="exportLogs" title="导出日志">
💾
</button>
<button @click="clearLogs" title="清空日志">
🗑
</button>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<input
v-model="searchQuery"
type="text"
placeholder="搜索日志内容..."
class="search-input"
>
<button v-if="searchQuery" @click="clearSearch" class="clear-search">
</button>
</div>
<!-- 日志内容区域 -->
<div
ref="logContainer"
class="log-container"
@scroll="handleScroll"
>
<div class="log-list">
<div
v-for="(log, index) in filteredLogs"
:key="log.id"
:class="['log-item', log.type]"
@click="selectLog(log)"
:data-index="index"
>
<!-- 时间戳 -->
<span v-if="showTimestamp" class="log-timestamp">
{{ formatTime(log.timestamp) }}
</span>
<!-- 日志级别图标 -->
<span class="log-icon">
{{ getLogIcon(log.type) }}
</span>
<!-- 日志内容 -->
<div class="log-content">
<div class="log-message" v-html="highlightSearch(log.message)"></div>
<!-- 堆栈信息 -->
<div v-if="log.stack && selectedLogId === log.id" class="log-stack">
<pre>{{ log.stack }}</pre>
</div>
<!-- 额外数据 -->
<div v-if="log.data && selectedLogId === log.id" class="log-data">
<pre>{{ JSON.stringify(log.data, null, 2) }}</pre>
</div>
</div>
<!-- 日志来源 -->
<span v-if="log.source" class="log-source">
{{ log.source }}
</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredLogs.length === 0" class="empty-state">
<div v-if="logs.length === 0">
<p>暂无日志</p>
<button @click="addTestLogs">添加测试日志</button>
</div>
<div v-else>
<p>没有匹配的日志</p>
<button @click="clearFilters">清除过滤器</button>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="console-footer">
<div class="status-info">
<span>总计: {{ logs.length }} </span>
<span>显示: {{ filteredLogs.length }} </span>
<span v-if="selectedLogId">已选择: 1 </span>
</div>
<div class="scroll-info" v-if="!isAtBottom && filteredLogs.length > 0">
<button @click="scrollToBottom" class="scroll-to-bottom">
滚动到底部
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
// 日志类型定义
interface LogEntry {
id: string
type: 'info' | 'warn' | 'error' | 'debug' | 'success'
message: string
timestamp: Date
source?: string
stack?: string
data?: any
}
// 日志级别配置
const logLevels = [
{ type: 'info', label: '信息', icon: '' },
{ type: 'warn', label: '警告', icon: '⚠️' },
{ type: 'error', label: '错误', icon: '❌' },
{ type: 'debug', label: '调试', icon: '🐛' },
{ type: 'success', label: '成功', icon: '✅' }
] as const
// 响应式数据
const logs = ref<LogEntry[]>([])
const activeFilters = ref<string[]>(['info', 'warn', 'error', 'debug', 'success'])
const searchQuery = ref('')
const selectedLogId = ref<string>('')
const autoScroll = ref(true)
const showTimestamp = ref(true)
const isAtBottom = ref(true)
// DOM 引用
const logContainer = ref<HTMLElement>()
// 计算属性
const filteredLogs = computed(() => {
let filtered = logs.value.filter(log => activeFilters.value.includes(log.type))
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(query) ||
log.source?.toLowerCase().includes(query)
)
}
return filtered
})
// 工具函数
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
})
}
function getLogIcon(type: string): string {
const level = logLevels.find(l => l.type === type)
return level?.icon || ''
}
function getLogCountByType(type: string): number {
return logs.value.filter(log => log.type === type).length
}
function highlightSearch(text: string): string {
if (!searchQuery.value) return text
const regex = new RegExp(`(${searchQuery.value})`, 'gi')
return text.replace(regex, '<mark>$1</mark>')
}
// 日志操作
function addLog(type: LogEntry['type'], message: string, options: Partial<LogEntry> = {}) {
const log: LogEntry = {
id: generateId(),
type,
message,
timestamp: new Date(),
...options
}
logs.value.push(log)
// 自动滚动到底部
if (autoScroll.value) {
nextTick(() => {
scrollToBottom()
})
}
}
function clearLogs() {
logs.value = []
selectedLogId.value = ''
}
function addTestLogs() {
const testLogs = [
{ type: 'info' as const, message: '应用程序启动成功', source: 'App.vue' },
{ type: 'success' as const, message: '场景加载完成', source: 'SceneManager.ts' },
{ type: 'warn' as const, message: '纹理分辨率过大,可能影响性能', source: 'TextureLoader.ts' },
{ type: 'error' as const, message: '无法加载模型文件: model.fbx', source: 'ModelLoader.ts', stack: 'Error: File not found\n at ModelLoader.load (ModelLoader.ts:45)\n at Scene.addModel (Scene.ts:123)' },
{ type: 'debug' as const, message: '相机位置更新', source: 'Camera.ts', data: { position: { x: 0, y: 5, z: 10 }, rotation: { x: 0, y: 0, z: 0 } } },
{ type: 'info' as const, message: '用户交互事件: 鼠标点击', source: 'InputManager.ts' },
{ type: 'warn' as const, message: '内存使用率较高: 85%', source: 'PerformanceMonitor.ts' }
]
testLogs.forEach(log => {
addLog(log.type, log.message, { source: log.source, stack: log.stack, data: log.data })
})
}
// 过滤器操作
function toggleFilter(type: string) {
const index = activeFilters.value.indexOf(type)
if (index > -1) {
activeFilters.value.splice(index, 1)
} else {
activeFilters.value.push(type)
}
}
function clearFilters() {
activeFilters.value = ['info', 'warn', 'error', 'debug', 'success']
searchQuery.value = ''
}
// 搜索操作
function clearSearch() {
searchQuery.value = ''
}
// 日志选择
function selectLog(log: LogEntry) {
selectedLogId.value = selectedLogId.value === log.id ? '' : log.id
}
// 滚动控制
function handleScroll() {
if (!logContainer.value) return
const { scrollTop, scrollHeight, clientHeight } = logContainer.value
isAtBottom.value = scrollTop + clientHeight >= scrollHeight - 10
}
function scrollToBottom() {
if (!logContainer.value) return
logContainer.value.scrollTop = logContainer.value.scrollHeight
isAtBottom.value = true
}
function toggleAutoScroll() {
autoScroll.value = !autoScroll.value
if (autoScroll.value) {
scrollToBottom()
}
}
function toggleTimestamp() {
showTimestamp.value = !showTimestamp.value
}
// 导出功能
function exportLogs() {
const logText = filteredLogs.value.map(log => {
const timestamp = formatTime(log.timestamp)
const level = log.type.toUpperCase()
const source = log.source ? ` [${log.source}]` : ''
return `[${timestamp}] ${level}${source}: ${log.message}`
}).join('\n')
const blob = new Blob([logText], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `console-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`
a.click()
URL.revokeObjectURL(url)
}
// 监听过滤器变化,自动滚动到底部
watch(filteredLogs, () => {
if (autoScroll.value) {
nextTick(() => {
scrollToBottom()
})
}
})
// 暴露方法给父组件
defineExpose({
addLog,
clearLogs,
info: (message: string, options?: Partial<LogEntry>) => addLog('info', message, options),
warn: (message: string, options?: Partial<LogEntry>) => addLog('warn', message, options),
error: (message: string, options?: Partial<LogEntry>) => addLog('error', message, options),
debug: (message: string, options?: Partial<LogEntry>) => addLog('debug', message, options),
success: (message: string, options?: Partial<LogEntry>) => addLog('success', message, options)
})
// 生命周期
onMounted(() => {
// 监听全局错误
window.addEventListener('error', (event) => {
addLog('error', event.message, {
source: event.filename,
stack: event.error?.stack
})
})
// 监听未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
addLog('error', `未处理的 Promise 拒绝: ${event.reason}`, {
source: 'Promise'
})
})
// 重写 console 方法
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info,
debug: console.debug
}
console.log = (...args) => {
originalConsole.log(...args)
addLog('info', args.join(' '), { source: 'Console' })
}
console.warn = (...args) => {
originalConsole.warn(...args)
addLog('warn', args.join(' '), { source: 'Console' })
}
console.error = (...args) => {
originalConsole.error(...args)
addLog('error', args.join(' '), { source: 'Console' })
}
console.info = (...args) => {
originalConsole.info(...args)
addLog('info', args.join(' '), { source: 'Console' })
}
console.debug = (...args) => {
originalConsole.debug(...args)
addLog('debug', args.join(' '), { source: 'Console' })
}
})
onUnmounted(() => {
// 恢复原始 console 方法
// 这里可以根据需要决定是否恢复
})
</script>
<style scoped>
.console {
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
}
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
}
.console-title {
display: flex;
align-items: center;
gap: 12px;
}
.console-title h3 {
margin: 0;
font-size: 14px;
font-weight: 500;
}
.log-count {
font-size: 12px;
color: #888;
}
.console-actions {
display: flex;
align-items: center;
gap: 12px;
}
.log-filters {
display: flex;
gap: 4px;
}
.filter-btn {
display: flex;
align-items: center;
gap: 4px;
background: transparent;
border: 1px solid #3e3e3e;
color: #888;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
transition: all 0.2s;
}
.filter-btn:hover {
border-color: #007acc;
}
.filter-btn.active {
background: #007acc;
border-color: #007acc;
color: white;
}
.filter-btn.info.active {
background: #007acc;
}
.filter-btn.warn.active {
background: #ff8c00;
}
.filter-btn.error.active {
background: #f14c4c;
}
.filter-btn.debug.active {
background: #9c27b0;
}
.filter-btn.success.active {
background: #4caf50;
}
.filter-icon {
font-size: 12px;
}
.filter-count {
font-size: 10px;
background: rgba(255, 255, 255, 0.2);
padding: 1px 4px;
border-radius: 2px;
min-width: 16px;
text-align: center;
}
.control-buttons {
display: flex;
gap: 4px;
}
.control-buttons button {
background: transparent;
border: 1px solid #3e3e3e;
color: #888;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.control-buttons button:hover {
border-color: #007acc;
color: #d4d4d4;
}
.control-buttons button.active {
background: #007acc;
border-color: #007acc;
color: white;
}
.search-bar {
position: relative;
padding: 8px 12px;
background: #252526;
border-bottom: 1px solid #3e3e3e;
}
.search-input {
width: 100%;
background: #3c3c3c;
border: 1px solid #3e3e3e;
color: #d4d4d4;
padding: 6px 12px;
border-radius: 3px;
font-size: 12px;
outline: none;
}
.search-input:focus {
border-color: #007acc;
}
.clear-search {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: #888;
cursor: pointer;
padding: 4px;
border-radius: 2px;
}
.clear-search:hover {
background: #3e3e3e;
color: #d4d4d4;
}
.log-container {
flex: 1;
overflow-y: auto;
background: #1e1e1e;
}
.log-list {
padding: 4px;
}
.log-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
border-left: 3px solid transparent;
}
.log-item:hover {
background: #2d2d2d;
}
.log-item.info {
border-left-color: #007acc;
}
.log-item.warn {
border-left-color: #ff8c00;
}
.log-item.error {
border-left-color: #f14c4c;
}
.log-item.debug {
border-left-color: #9c27b0;
}
.log-item.success {
border-left-color: #4caf50;
}
.log-timestamp {
color: #888;
font-size: 11px;
white-space: nowrap;
min-width: 80px;
}
.log-icon {
font-size: 12px;
margin-top: 1px;
}
.log-content {
flex: 1;
min-width: 0;
}
.log-message {
word-break: break-word;
line-height: 1.4;
}
.log-message :deep(mark) {
background: #ff8c00;
color: #1e1e1e;
padding: 1px 2px;
border-radius: 2px;
}
.log-stack,
.log-data {
margin-top: 8px;
padding: 8px;
background: #2d2d2d;
border-radius: 3px;
border-left: 3px solid #3e3e3e;
}
.log-stack pre,
.log-data pre {
margin: 0;
font-size: 11px;
color: #888;
white-space: pre-wrap;
word-break: break-word;
}
.log-source {
color: #888;
font-size: 11px;
white-space: nowrap;
margin-left: auto;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-state p {
margin: 0 0 16px 0;
font-size: 14px;
}
.empty-state button {
background: #007acc;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
}
.empty-state button:hover {
background: #005a9e;
}
.console-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 12px;
background: #2d2d2d;
border-top: 1px solid #3e3e3e;
font-size: 11px;
color: #888;
}
.status-info {
display: flex;
gap: 16px;
}
.scroll-to-bottom {
background: #007acc;
border: none;
color: white;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
transition: background-color 0.2s;
}
.scroll-to-bottom:hover {
background: #005a9e;
}
/* 滚动条样式 */
.log-container::-webkit-scrollbar {
width: 8px;
}
.log-container::-webkit-scrollbar-track {
background: #1e1e1e;
}
.log-container::-webkit-scrollbar-thumb {
background: #3e3e3e;
border-radius: 4px;
}
.log-container::-webkit-scrollbar-thumb:hover {
background: #4e4e4e;
}
</style>

View File

@ -0,0 +1,959 @@
<template>
<div class="file-system">
<!-- 左侧文件夹树 -->
<div
class="file-tree-panel"
:style="{ width: leftPanelWidth + 'px' }"
>
<div class="tree-content">
<el-tree
:data="folderTree"
:props="treeProps"
:expand-on-click-node="false"
node-key="id"
:current-node-key="selectedFolderId"
@node-click="handleFolderClick"
@node-expand="handleFolderExpand"
@node-collapse="handleFolderCollapse"
>
<template #default="{ node, data }">
<span class="tree-node-content">
<i class="tree-node-icon">📁</i>
<span class="tree-node-label">{{ node.label }}</span>
</span>
</template>
</el-tree>
<div v-if="folderTree.length === 0" class="empty-state">
<p>暂无文件夹</p>
<button @click="createFolder">创建文件夹</button>
</div>
</div>
</div>
<!-- 拖拽分隔条 -->
<div
class="resize-handle"
@mousedown="startResize"
></div>
<!-- 右侧文件列表 -->
<div class="file-list-panel">
<div class="list-content" :class="viewMode">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span @click="navigateToRoot" class="breadcrumb-item">根目录</span>
<span
v-for="(folder, index) in currentPath"
:key="folder.id"
@click="navigateToFolder(folder, index)"
class="breadcrumb-item"
>
/ {{ folder.name }}
</span>
</div>
<!-- 文件网格视图 -->
<div class="file-grid" v-if="viewMode === 'grid'">
<FileNode
v-for="node in currentFolderFiles"
:key="node.id"
:node="node"
:selected-id="selectedFileId"
:show-size="true"
@click="handleFileClick"
@double-click="handleFileDoubleClick"
@rename="handleFileRename"
@drag-start="handleDragStart"
@drag-end="handleDragEnd"
@drag-over="handleDragOver"
@drag-leave="handleDragLeave"
@drop="handleDrop"
/>
</div>
<!-- 文件列表视图 -->
<div class="file-list" v-else>
<div class="list-header">
<div class="col-name">名称</div>
<div class="col-type">类型</div>
<div class="col-size">大小</div>
<div class="col-date">修改时间</div>
</div>
<div
v-for="node in currentFolderFiles"
:key="node.id"
:class="['file-row', node.type.toLowerCase(), { selected: selectedFileId === node.id }]"
@click="handleFileClick(node)"
@dblclick="handleFileDoubleClick(node)"
:draggable="true"
@dragstart="handleDragStart(node, $event)"
@dragend="handleDragEnd(node, $event)"
@dragover="handleDragOver(node, $event)"
@dragleave="handleDragLeave(node, $event)"
@drop="handleDrop(node, dragState.dragNode!, 'inside', $event)"
>
<div class="col-name">
<span class="file-icon">{{ getFileIcon(node.type) }}</span>
<span class="file-name" :title="node.name">{{ node.name }}</span>
</div>
<div class="col-type">{{ getFileTypeLabel(node.type) }}</div>
<div class="col-size">{{ node.size !== undefined ? formatFileSize(node.size) : '-' }}</div>
<div class="col-date">{{ node.lastModified ? formatDate(node.lastModified) : '-' }}</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="currentFolderFiles.length === 0" class="empty-state">
<p>此文件夹为空</p>
<div class="empty-actions">
<button @click="createFile">创建文件</button>
<button @click="uploadFile">上传文件</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { FileNode as FileNodeType, FileNodeType as NodeType, DragState, FILE_ICONS } from '../../BasicControls/FileNode/types'
import { generateId, formatFileSize, formatDate } from '../../../utils/Tools'
import FileNode from '../../BasicControls/FileNode/index.vue'
// 响应式数据
const leftPanelWidth = ref(250)
const minPanelWidth = 150
const maxPanelWidth = 500
const viewMode = ref<'list' | 'grid'>('grid')
const selectedFolderId = ref<string>('')
const selectedFileId = ref<string>('')
const currentPath = ref<FileNodeType[]>([])
// Element Plus 树组件配置
const treeProps = {
children: 'children',
label: 'name'
}
// 拖拽状态
const dragState = ref<DragState>({
isDragging: false,
dragNode: null,
dragOverNode: null,
dropPosition: null
})
// 模拟文件数据
const fileTree = ref<FileNodeType[]>([
{
id: 'root',
name: '项目文件',
type: NodeType.Folder,
path: '/',
isExpanded: true,
children: [
{
id: 'assets',
name: 'Assets',
type: NodeType.Folder,
path: '/Assets',
isExpanded: false,
children: [
{
id: 'models',
name: 'Models',
type: NodeType.Folder,
path: '/Assets/Models',
children: [
{
id: 'model1',
name: 'character.fbx',
type: NodeType.Model,
path: '/Assets/Models/character.fbx',
size: 2048576,
lastModified: new Date('2024-01-15')
},
{
id: 'model2',
name: 'environment.glb',
type: NodeType.Model,
path: '/Assets/Models/environment.glb',
size: 5242880,
lastModified: new Date('2024-01-18')
}
]
},
{
id: 'textures',
name: 'Textures',
type: NodeType.Folder,
path: '/Assets/Textures',
children: [
{
id: 'texture1',
name: 'wall_diffuse.png',
type: NodeType.Texture,
path: '/Assets/Textures/wall_diffuse.png',
size: 1024000,
lastModified: new Date('2024-01-10')
},
{
id: 'texture2',
name: 'floor_normal.jpg',
type: NodeType.Image,
path: '/Assets/Textures/floor_normal.jpg',
size: 512000,
lastModified: new Date('2024-01-12')
}
]
},
{
id: 'materials',
name: 'Materials',
type: NodeType.Folder,
path: '/Assets/Materials',
children: [
{
id: 'material1',
name: 'wood.material',
type: NodeType.Material,
path: '/Assets/Materials/wood.material',
size: 2048,
lastModified: new Date('2024-01-14')
}
]
},
{
id: 'audio',
name: 'Audio',
type: NodeType.Folder,
path: '/Assets/Audio',
children: [
{
id: 'audio1',
name: 'background.mp3',
type: NodeType.Audio,
path: '/Assets/Audio/background.mp3',
size: 3145728,
lastModified: new Date('2024-01-16')
},
{
id: 'audio2',
name: 'effect.wav',
type: NodeType.Audio,
path: '/Assets/Audio/effect.wav',
size: 1048576,
lastModified: new Date('2024-01-17')
}
]
}
]
},
{
id: 'scripts',
name: 'Scripts',
type: NodeType.Folder,
path: '/Scripts',
children: [
{
id: 'script1',
name: 'main.js',
type: NodeType.Script,
path: '/Scripts/main.js',
size: 4096,
lastModified: new Date('2024-01-20')
},
{
id: 'script2',
name: 'utils.ts',
type: NodeType.Script,
path: '/Scripts/utils.ts',
size: 2048,
lastModified: new Date('2024-01-19')
}
]
},
{
id: 'scenes',
name: 'Scenes',
type: NodeType.Folder,
path: '/Scenes',
children: [
{
id: 'scene1',
name: 'main.scene',
type: NodeType.Scene,
path: '/Scenes/main.scene',
size: 8192,
lastModified: new Date('2024-01-21')
}
]
},
{
id: 'docs',
name: 'Documentation',
type: NodeType.Folder,
path: '/Documentation',
children: [
{
id: 'readme',
name: 'README.md',
type: NodeType.Text,
path: '/Documentation/README.md',
size: 1024,
lastModified: new Date('2024-01-22')
},
{
id: 'guide',
name: 'user-guide.txt',
type: NodeType.Text,
path: '/Documentation/user-guide.txt',
size: 2048,
lastModified: new Date('2024-01-23')
}
]
}
]
}
])
// 计算属性
const folderTree = computed(() => {
return buildFolderTree(fileTree.value)
})
const currentFolderFiles = computed(() => {
let currentFolder = fileTree.value[0] // 根目录
for (const pathItem of currentPath.value) {
const found = currentFolder.children?.find(child => child.id === pathItem.id)
if (found) {
currentFolder = found
}
}
return currentFolder.children || []
})
// 工具函数
function buildFolderTree(nodes: FileNodeType[]): FileNodeType[] {
return nodes.map(node => {
if (node.type === NodeType.Folder) {
const folderNode: FileNodeType = {
...node,
children: node.children ? buildFolderTree(node.children.filter(child => child.type === NodeType.Folder)) : []
}
return folderNode
}
return node
}).filter(node => node.type === NodeType.Folder)
}
function findNodeById(nodes: FileNodeType[], id: string): FileNodeType | null {
for (const node of nodes) {
if (node.id === id) {
return node
}
if (node.children) {
const found = findNodeById(node.children, id)
if (found) return found
}
}
return null
}
function getFileIcon(type: NodeType): string {
return FILE_ICONS[type] || '📄'
}
function getFileTypeLabel(type: NodeType): string {
const typeLabels: Record<NodeType, string> = {
[NodeType.Text]: '文本',
[NodeType.Folder]: '文件夹',
[NodeType.Model]: '模型',
[NodeType.Audio]: '音频',
[NodeType.Video]: '视频',
[NodeType.Material]: '材质',
[NodeType.Texture]: '纹理',
[NodeType.Image]: '图片',
[NodeType.Script]: '脚本',
[NodeType.Scene]: '场景'
}
return typeLabels[type] || '未知'
}
// 事件处理
const handleFolderClick = (data: FileNodeType) => {
selectedFolderId.value = data.id
// 导航到选中的文件夹
const pathToNode = getPathToNode(fileTree.value, data.id)
currentPath.value = pathToNode.slice(1) // 移除根目录
}
const handleFolderExpand = (data: FileNodeType) => {
const targetNode = findNodeById(fileTree.value, data.id)
if (targetNode) {
targetNode.isExpanded = true
}
}
const handleFolderCollapse = (data: FileNodeType) => {
const targetNode = findNodeById(fileTree.value, data.id)
if (targetNode) {
targetNode.isExpanded = false
}
}
const handleFileClick = (node: FileNodeType) => {
selectedFileId.value = node.id
}
const handleFileRename = (node: FileNodeType, newName: string) => {
const targetNode = findNodeById(fileTree.value, node.id)
if (targetNode) {
targetNode.name = newName
}
}
const handleFileDoubleClick = (node: FileNodeType) => {
if (node.type === NodeType.Folder) {
// 进入文件夹
currentPath.value.push(node)
selectedFolderId.value = node.id
selectedFileId.value = ''
} else {
// 打开文件
console.log('打开文件:', node.name)
}
}
// 拖拽处理
const handleDragStart = (node: FileNodeType, event: DragEvent) => {
dragState.value.isDragging = true
dragState.value.dragNode = node
node.isDragging = true
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', node.id)
}
}
const handleDragEnd = (node: FileNodeType, event: DragEvent) => {
dragState.value.isDragging = false
dragState.value.dragNode = null
dragState.value.dragOverNode = null
dragState.value.dropPosition = null
node.isDragging = false
}
const handleDragOver = (node: FileNodeType, event: DragEvent) => {
if (dragState.value.dragNode && dragState.value.dragNode.id !== node.id) {
event.preventDefault()
dragState.value.dragOverNode = node
dragState.value.dropPosition = 'inside'
}
}
const handleDragLeave = (node: FileNodeType, event: DragEvent) => {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
if (dragState.value.dragOverNode?.id === node.id) {
dragState.value.dragOverNode = null
dragState.value.dropPosition = null
}
}
}
const handleDrop = (targetNode: FileNodeType, dragNode: FileNodeType, position: string, event: DragEvent) => {
event.preventDefault()
console.log('拖拽操作:', {
target: targetNode.name,
drag: dragNode.name,
position
})
// 这里实现具体的拖拽逻辑
}
// 导航功能
const navigateToRoot = () => {
currentPath.value = []
selectedFolderId.value = fileTree.value[0].id
selectedFileId.value = ''
}
const navigateToFolder = (folder: FileNodeType, index: number) => {
currentPath.value = currentPath.value.slice(0, index + 1)
selectedFolderId.value = folder.id
selectedFileId.value = ''
}
function getPathToNode(nodes: FileNodeType[], targetId: string, currentPath: FileNodeType[] = []): FileNodeType[] {
for (const node of nodes) {
const newPath = [...currentPath, node]
if (node.id === targetId) {
return newPath
}
if (node.children) {
const found = getPathToNode(node.children, targetId, newPath)
if (found.length > 0) return found
}
}
return []
}
// 操作功能
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
}
const createFolder = () => {
const newFolder: FileNodeType = {
id: generateId(),
name: '新建文件夹',
type: NodeType.Folder,
path: '/新建文件夹',
children: []
}
// 添加到当前位置
if (currentPath.value.length === 0) {
fileTree.value[0].children = fileTree.value[0].children || []
fileTree.value[0].children.push(newFolder)
} else {
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
if (currentFolder) {
currentFolder.children = currentFolder.children || []
currentFolder.children.push(newFolder)
}
}
}
const createFile = () => {
const newFile: FileNodeType = {
id: generateId(),
name: '新建文件.txt',
type: NodeType.Text,
path: '/新建文件.txt',
size: 0,
lastModified: new Date()
}
// 添加到当前文件夹
if (currentPath.value.length === 0) {
fileTree.value[0].children = fileTree.value[0].children || []
fileTree.value[0].children.push(newFile)
} else {
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
if (currentFolder) {
currentFolder.children = currentFolder.children || []
currentFolder.children.push(newFile)
}
}
}
const uploadFile = () => {
// 创建文件输入元素
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files) {
for (let i = 0; i < files.length; i++) {
const file = files[i]
console.log('上传文件:', file.name)
}
}
}
input.click()
}
const refreshTree = () => {
console.log('刷新文件树')
}
// 拖拽调整面板大小
let isResizing = false
const startResize = (event: MouseEvent) => {
isResizing = true
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
event.preventDefault()
}
const handleResize = (event: MouseEvent) => {
if (!isResizing) return
const container = document.querySelector('.file-system') as HTMLElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const newWidth = event.clientX - containerRect.left
if (newWidth >= minPanelWidth && newWidth <= maxPanelWidth) {
leftPanelWidth.value = newWidth
}
}
const stopResize = () => {
isResizing = false
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
}
// 生命周期
onMounted(() => {
// 初始化选中根目录
selectedFolderId.value = fileTree.value[0].id
})
onUnmounted(() => {
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
})
</script>
<style scoped>
.file-system {
display: flex;
height: 100%;
background: #2d2d2d;
color: #cccccc;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.file-tree-panel {
background: #393939;
border-right: 1px solid #4a4a4a;
display: flex;
flex-direction: column;
min-width: 150px;
}
.tree-content {
flex: 1;
overflow-y: auto;
padding: 8px 4px;
}
.tree-node-content {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}
.tree-node-icon {
font-size: 12px;
}
.tree-node-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resize-handle {
width: 4px;
background: #4a4a4a;
cursor: col-resize;
transition: background-color 0.2s;
}
.resize-handle:hover {
background: #409eff;
}
.file-list-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.list-content {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.breadcrumb {
display: flex;
align-items: center;
padding: 8px 12px;
background: #353535;
border-bottom: 1px solid #4a4a4a;
font-size: 12px;
margin-bottom: 4px;
}
.breadcrumb-item {
cursor: pointer;
color: #409eff;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: #66b1ff;
}
/* 网格视图样式 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 1px;
padding: 8px;
align-items: start;
}
/* 文件列表视图样式 */
.file-list {
display: flex;
flex-direction: column;
}
.list-header {
display: flex;
padding: 8px 12px;
background: #353535;
border-bottom: 1px solid #4a4a4a;
font-size: 12px;
font-weight: 500;
color: #888;
}
.file-row {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
transition: background-color 0.2s;
border-radius: 3px;
margin: 1px 4px;
}
.file-row:hover {
background: #404040;
}
.file-row.selected {
background: #2d4a6b;
}
.col-name {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
}
.file-icon {
font-size: 14px;
flex-shrink: 0;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-type {
width: 80px;
text-align: center;
font-size: 11px;
color: #888;
}
.col-size {
width: 80px;
text-align: right;
font-size: 11px;
color: #888;
}
.col-date {
width: 120px;
text-align: right;
font-size: 11px;
color: #888;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-state p {
margin: 0 0 16px 0;
font-size: 14px;
}
.empty-actions {
display: flex;
gap: 8px;
}
.empty-actions button {
background: #409eff;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
}
.empty-actions button:hover {
background: #66b1ff;
}
/* 文件类型特定样式 */
.file-item.folder .file-item-name,
.file-row.folder .file-name {
color: #e6a23c;
font-weight: 500;
}
.file-item.script .file-item-name,
.file-row.script .file-name {
color: #67c23a;
}
.file-item.model .file-item-name,
.file-row.model .file-name {
color: #409eff;
}
.file-item.image .file-item-name,
.file-item.texture .file-item-name,
.file-row.image .file-name,
.file-row.texture .file-name {
color: #f56c6c;
}
.file-item.material .file-item-name,
.file-row.material .file-name {
color: #e6a23c;
}
/* 滚动条样式 */
.tree-content::-webkit-scrollbar,
.list-content::-webkit-scrollbar {
width: 8px;
}
.tree-content::-webkit-scrollbar-track,
.list-content::-webkit-scrollbar-track {
background: #2d2d2d;
}
.tree-content::-webkit-scrollbar-thumb,
.list-content::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
.tree-content::-webkit-scrollbar-thumb:hover,
.list-content::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
/* Element Plus 树组件样式覆盖 */
/* :deep(.el-tree) {
background: transparent;
color: #cccccc;
}
.tree-content :deep(.el-tree-node__content) {
background: transparent;
border-radius: 4px;
margin: 2px 0;
padding: 4px 8px;
transition: background-color 0.2s;
}
.tree-content :deep(.el-tree-node__content:hover) {
background: #404040;
}
.tree-content :deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #409eff;
color: white;
}
.tree-content :deep(.el-tree-node__expand-icon) {
color: #888;
font-size: 12px;
}
.tree-content :deep(.el-tree-node__expand-icon.is-leaf) {
color: transparent;
}
*/
.tree-node-label{
color: #ffffff;
}
.el-tree {
background: #1c00be00;
}
/* 设置节点的默认文字颜色 */
::v-deep .el-tree-node__label {
color: #ffffff;
/* 自定义你想要的颜色 */
}
/* 设置鼠标悬停时的背景颜色 */
::v-deep .el-tree-node:hover>.el-tree-node__content {
background-color: #f0f9eb00;
/* 自定义悬停背景色 */
}
/* 设置鼠标悬停时的文字颜色 */
::v-deep .el-tree-node:hover .el-tree-node__label {
color: #f8f8f8;
/* 自定义悬停文字颜色 */
}
::v-deep .el-tree-node__content {
background-color: rgba(255, 255, 255, 0);
/* 鼠标离开后恢复的背景色 */
color: #333;
/* 默认文字颜色 */
}
::v-deep .el-tree-node.is-current>.el-tree-node__content {
background-color: #ffffff28;
}
</style>

View File

@ -0,0 +1,922 @@
<template>
<div class="file-system">
<!-- 左侧文件夹树 -->
<div class="file-tree-panel" :style="{ width: leftPanelWidth + 'px' }">
<div class="tree-content">
<el-tree :data="folderTree" :props="treeProps" :expand-on-click-node="false" node-key="id"
:current-node-key="selectedFolderId" @node-click="handleFolderClick" @node-expand="handleFolderExpand"
@node-collapse="handleFolderCollapse">
<template #default="{ node, data }">
<span class="tree-node-content">
<i class="tree-node-icon">📁</i>
<span class="tree-node-label">{{ node.label }}</span>
</span>
</template>
</el-tree>
<div v-if="folderTree.length === 0" class="empty-state">
<p>暂无文件夹</p>
<button @click="createFolder">创建文件夹</button>
</div>
</div>
</div>
<!-- 拖拽分隔条 -->
<div class="resize-handle" @mousedown="startResize"></div>
<!-- 右侧文件列表 -->
<div class="file-list-panel">
<div class="list-content" :class="viewMode">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span @click="navigateToRoot" class="breadcrumb-item">根目录</span>
<span v-for="(folder, index) in currentPath" :key="folder.id" @click="navigateToFolder(folder, index)"
class="breadcrumb-item">
/ {{ folder.name }}
</span>
</div>
<!-- 文件网格视图 -->
<div class="file-grid" v-if="viewMode === 'grid'">
<FileNode v-for="node in currentFolderFiles" :key="node.id" :node="node" :selected-id="selectedFileId"
:show-size="true" @click="handleFileClick" @double-click="handleFileDoubleClick" @rename="handleFileRename"
@drag-start="handleDragStart" @drag-end="handleDragEnd" @drag-over="handleDragOver"
@drag-leave="handleDragLeave" @drop="handleDrop" />
</div>
<!-- 文件列表视图 -->
<div class="file-list" v-else>
<div class="list-header">
<div class="col-name">名称</div>
<div class="col-type">类型</div>
<div class="col-size">大小</div>
<div class="col-date">修改时间</div>
</div>
<div v-for="node in currentFolderFiles" :key="node.id"
:class="['file-row', node.type.toLowerCase(), { selected: selectedFileId === node.id }]"
@click="handleFileClick(node)" @dblclick="handleFileDoubleClick(node)" :draggable="true"
@dragstart="handleDragStart(node, $event)" @dragend="handleDragEnd(node, $event)"
@dragover="handleDragOver(node, $event)" @dragleave="handleDragLeave(node, $event)"
@drop="handleDrop(node, dragState.dragNode!, 'inside', $event)">
<div class="col-name">
<span class="file-icon">{{ getFileIcon(node.type) }}</span>
<span class="file-name" :title="node.name">{{ node.name }}</span>
</div>
<div class="col-type">{{ getFileTypeLabel(node.type) }}</div>
<div class="col-size">{{ node.size !== undefined ? formatFileSize(node.size) : '-' }}</div>
<div class="col-date">{{ node.lastModified ? formatDate(node.lastModified) : '-' }}</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="currentFolderFiles.length === 0" class="empty-state">
<p>此文件夹为空</p>
<div class="empty-actions">
<button @click="createFile">创建文件</button>
<button @click="uploadFile">上传文件</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { FileNode as FileNodeType, FileNodeType as NodeType, DragState, FILE_ICONS } from '../../BasicControls/FileNode/types'
import { generateId, formatFileSize, formatDate } from '../../../utils/Tools'
import FileNode from '../../BasicControls/FileNode/index.vue'
import { userDragStore } from 'stores/Filed'
import { useCursorStore } from 'stores/Cursor'
const dragStore = userDragStore()
const cursorStore = useCursorStore()
// 响应式数据
const leftPanelWidth = ref(250)
const minPanelWidth = 150
const maxPanelWidth = 500
const viewMode = ref<'list' | 'grid'>('grid')
const selectedFolderId = ref<string>('')
const selectedFileId = ref<string>('')
const currentPath = ref<FileNodeType[]>([])
// Element Plus 树组件配置
const treeProps = {
children: 'children',
label: 'name'
}
// 拖拽状态
const dragState = ref<DragState>({
isDragging: false,
dragNode: null,
dragOverNode: null,
dropPosition: null
})
// 模拟文件数据
const fileTree = ref<FileNodeType[]>([
{
id: 'root',
name: '项目文件',
type: NodeType.Folder,
path: '/',
isExpanded: true,
children: [
{
id: 'assets',
name: 'Assets',
type: NodeType.Folder,
path: '/Assets',
isExpanded: false,
children: [
{
id: 'models',
name: 'Models',
type: NodeType.Folder,
path: '/Assets/Models',
children: [
{
id: 'model1',
name: 'character.fbx',
type: NodeType.Model,
path: '/Assets/Models/character.fbx',
size: 2048576,
lastModified: new Date('2024-01-15')
},
{
id: 'model2',
name: 'environment.glb',
type: NodeType.Model,
path: '/Assets/Models/environment.glb',
size: 5242880,
lastModified: new Date('2024-01-18')
}
]
},
{
id: 'textures',
name: 'Textures',
type: NodeType.Folder,
path: '/Assets/Textures',
children: [
{
id: 'texture1',
name: 'wall_diffuse.png',
type: NodeType.Texture,
path: '/Assets/Textures/wall_diffuse.png',
size: 1024000,
lastModified: new Date('2024-01-10')
},
{
id: 'texture2',
name: 'floor_normal.jpg',
type: NodeType.Image,
path: '/Assets/Textures/floor_normal.jpg',
size: 512000,
lastModified: new Date('2024-01-12')
}
]
},
{
id: 'materials',
name: 'Materials',
type: NodeType.Folder,
path: '/Assets/Materials',
children: [
{
id: 'material1',
name: 'wood.material',
type: NodeType.Material,
path: '/Assets/Materials/wood.material',
size: 2048,
lastModified: new Date('2024-01-14')
}
]
},
{
id: 'audio',
name: 'Audio',
type: NodeType.Folder,
path: '/Assets/Audio',
children: [
{
id: 'audio1',
name: 'background.mp3',
type: NodeType.Audio,
path: '/Assets/Audio/background.mp3',
size: 3145728,
lastModified: new Date('2024-01-16')
},
{
id: 'audio2',
name: 'effect.wav',
type: NodeType.Audio,
path: '/Assets/Audio/effect.wav',
size: 1048576,
lastModified: new Date('2024-01-17')
}
]
}
]
},
{
id: 'scripts',
name: 'Scripts',
type: NodeType.Folder,
path: '/Scripts',
children: [
{
id: 'script1',
name: 'main.js',
type: NodeType.Script,
path: '/Scripts/main.js',
size: 4096,
lastModified: new Date('2024-01-20')
},
{
id: 'script2',
name: 'utils.ts',
type: NodeType.Script,
path: '/Scripts/utils.ts',
size: 2048,
lastModified: new Date('2024-01-19')
}
]
},
{
id: 'scenes',
name: 'Scenes',
type: NodeType.Folder,
path: '/Scenes',
children: [
{
id: 'scene1',
name: 'main.scene',
type: NodeType.Scene,
path: '/Scenes/main.scene',
size: 8192,
lastModified: new Date('2024-01-21')
}
]
},
{
id: 'docs',
name: 'Documentation',
type: NodeType.Folder,
path: '/Documentation',
children: [
{
id: 'readme',
name: 'README.md',
type: NodeType.Text,
path: '/Documentation/README.md',
size: 1024,
lastModified: new Date('2024-01-22')
},
{
id: 'guide',
name: 'user-guide.txt',
type: NodeType.Text,
path: '/Documentation/user-guide.txt',
size: 2048,
lastModified: new Date('2024-01-23')
}
]
}
]
}
])
// 计算属性
const folderTree = computed(() => {
return buildFolderTree(fileTree.value)
})
const currentFolderFiles = computed(() => {
let currentFolder = fileTree.value[0] // 根目录
for (const pathItem of currentPath.value) {
const found = currentFolder.children?.find(child => child.id === pathItem.id)
if (found) {
currentFolder = found
}
}
return currentFolder.children || []
})
// 工具函数
function buildFolderTree(nodes: FileNodeType[]): FileNodeType[] {
return nodes.map(node => {
if (node.type === NodeType.Folder) {
const folderNode: FileNodeType = {
...node,
children: node.children ? buildFolderTree(node.children.filter(child => child.type === NodeType.Folder)) : []
}
return folderNode
}
return node
}).filter(node => node.type === NodeType.Folder)
}
function findNodeById(nodes: FileNodeType[], id: string): FileNodeType | null {
for (const node of nodes) {
if (node.id === id) {
return node
}
if (node.children) {
const found = findNodeById(node.children, id)
if (found) return found
}
}
return null
}
function getFileIcon(type: NodeType): string {
return FILE_ICONS[type] || '📄'
}
function getFileTypeLabel(type: NodeType): string {
const typeLabels: Record<NodeType, string> = {
[NodeType.Text]: '文本',
[NodeType.Folder]: '文件夹',
[NodeType.Model]: '模型',
[NodeType.Audio]: '音频',
[NodeType.Video]: '视频',
[NodeType.Material]: '材质',
[NodeType.Texture]: '纹理',
[NodeType.Image]: '图片',
[NodeType.Script]: '脚本',
[NodeType.Scene]: '场景'
}
return typeLabels[type] || '未知'
}
// 事件处理
const handleFolderClick = (data: FileNodeType) => {
selectedFolderId.value = data.id
// 导航到选中的文件夹
const pathToNode = getPathToNode(fileTree.value, data.id)
currentPath.value = pathToNode.slice(1) // 移除根目录
}
const handleFolderExpand = (data: FileNodeType) => {
const targetNode = findNodeById(fileTree.value, data.id)
if (targetNode) {
targetNode.isExpanded = true
}
}
const handleFolderCollapse = (data: FileNodeType) => {
const targetNode = findNodeById(fileTree.value, data.id)
if (targetNode) {
targetNode.isExpanded = false
}
}
const handleFileClick = (node: FileNodeType) => {
selectedFileId.value = node.id
}
const handleFileRename = (node: FileNodeType, newName: string) => {
const targetNode = findNodeById(fileTree.value, node.id)
if (targetNode) {
targetNode.name = newName
}
}
const handleFileDoubleClick = (node: FileNodeType) => {
if (node.type === NodeType.Folder) {
// 进入文件夹
currentPath.value.push(node)
selectedFolderId.value = node.id
selectedFileId.value = ''
} else {
// 打开文件
}
}
// 拖拽处理
const handleDragStart = (node: FileNodeType, event: DragEvent) => {
dragState.value.isDragging = true
dragState.value.dragNode = node
node.isDragging = true
if (event.dataTransfer) {
cursorStore.setCursor('move')
// event.dataTransfer.effectAllowed = 'move'
// event.dataTransfer.setData('application/json', JSON.stringify(node))
// event.dataTransfer.setData('text/plain', node.id)
dragStore.setDragType(node.type)
dragStore.setDragData(node)
}
}
const handleDragEnd = (node: FileNodeType, event: DragEvent) => {
cursorStore.setCursor('default')
dragState.value.isDragging = false
dragState.value.dragNode = null
dragState.value.dragOverNode = null
dragState.value.dropPosition = null
node.isDragging = false
}
const handleDragOver = (node: FileNodeType, event: DragEvent) => {
if (dragState.value.dragNode && dragState.value.dragNode.id !== node.id) {
event.preventDefault()
dragState.value.dragOverNode = node
dragState.value.dropPosition = 'inside'
}
}
const handleDragLeave = (node: FileNodeType, event: DragEvent) => {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const x = event.clientX
const y = event.clientY
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
if (dragState.value.dragOverNode?.id === node.id) {
dragState.value.dragOverNode = null
dragState.value.dropPosition = null
}
}
}
const handleDrop = (targetNode: FileNodeType, dragNode: FileNodeType, position: string, event: DragEvent) => {
event.preventDefault()
// 这里实现具体的拖拽逻辑
}
// 导航功能
const navigateToRoot = () => {
currentPath.value = []
selectedFolderId.value = fileTree.value[0].id
selectedFileId.value = ''
}
const navigateToFolder = (folder: FileNodeType, index: number) => {
currentPath.value = currentPath.value.slice(0, index + 1)
selectedFolderId.value = folder.id
selectedFileId.value = ''
}
function getPathToNode(nodes: FileNodeType[], targetId: string, currentPath: FileNodeType[] = []): FileNodeType[] {
for (const node of nodes) {
const newPath = [...currentPath, node]
if (node.id === targetId) {
return newPath
}
if (node.children) {
const found = getPathToNode(node.children, targetId, newPath)
if (found.length > 0) return found
}
}
return []
}
// 操作功能
const toggleViewMode = () => {
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
}
const createFolder = () => {
const newFolder: FileNodeType = {
id: generateId(),
name: '新建文件夹',
type: NodeType.Folder,
path: '/新建文件夹',
children: []
}
// 添加到当前位置
if (currentPath.value.length === 0) {
fileTree.value[0].children = fileTree.value[0].children || []
fileTree.value[0].children.push(newFolder)
} else {
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
if (currentFolder) {
currentFolder.children = currentFolder.children || []
currentFolder.children.push(newFolder)
}
}
}
const createFile = () => {
const newFile: FileNodeType = {
id: generateId(),
name: '新建文件.txt',
type: NodeType.Text,
path: '/新建文件.txt',
size: 0,
lastModified: new Date()
}
// 添加到当前文件夹
if (currentPath.value.length === 0) {
fileTree.value[0].children = fileTree.value[0].children || []
fileTree.value[0].children.push(newFile)
} else {
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
if (currentFolder) {
currentFolder.children = currentFolder.children || []
currentFolder.children.push(newFile)
}
}
}
const uploadFile = () => {
// 创建文件输入元素
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files) {
for (let i = 0; i < files.length; i++) {
const file = files[i]
}
}
}
input.click()
}
const refreshTree = () => {
}
// 拖拽调整面板大小
let isResizing = false
const startResize = (event: MouseEvent) => {
isResizing = true
document.addEventListener('mousemove', handleResize)
document.addEventListener('mouseup', stopResize)
event.preventDefault()
}
const handleResize = (event: MouseEvent) => {
if (!isResizing) return
const container = document.querySelector('.file-system') as HTMLElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const newWidth = event.clientX - containerRect.left
if (newWidth >= minPanelWidth && newWidth <= maxPanelWidth) {
leftPanelWidth.value = newWidth
}
}
const stopResize = () => {
isResizing = false
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
}
// 生命周期
onMounted(() => {
// 初始化选中根目录
selectedFolderId.value = fileTree.value[0].id
})
onUnmounted(() => {
document.removeEventListener('mousemove', handleResize)
document.removeEventListener('mouseup', stopResize)
})
</script>
<style scoped>
.file-system {
display: flex;
height: 100%;
background: #2d2d2d;
color: #cccccc;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.file-tree-panel {
background: #393939;
border-right: 1px solid #4a4a4a;
display: flex;
flex-direction: column;
min-width: 150px;
}
.tree-content {
flex: 1;
overflow-y: auto;
padding: 8px 4px;
}
.tree-node-content {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
}
.tree-node-icon {
font-size: 12px;
}
.tree-node-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resize-handle {
width: 4px;
background: #4a4a4a;
cursor: col-resize;
transition: background-color 0.2s;
}
.resize-handle:hover {
background: #409eff;
}
.file-list-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.list-content {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.breadcrumb {
display: flex;
align-items: center;
padding: 8px 12px;
background: #353535;
border-bottom: 1px solid #4a4a4a;
font-size: 12px;
margin-bottom: 4px;
}
.breadcrumb-item {
cursor: pointer;
color: #409eff;
transition: color 0.2s;
}
.breadcrumb-item:hover {
color: #66b1ff;
}
/* 网格视图样式 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 1px;
padding: 8px;
align-items: start;
}
/* 文件列表视图样式 */
.file-list {
display: flex;
flex-direction: column;
}
.list-header {
display: flex;
padding: 8px 12px;
background: #353535;
border-bottom: 1px solid #4a4a4a;
font-size: 12px;
font-weight: 500;
color: #888;
}
.file-row {
display: flex;
align-items: center;
padding: 6px 12px;
cursor: pointer;
transition: background-color 0.2s;
border-radius: 3px;
margin: 1px 4px;
}
.file-row:hover {
background: #404040;
}
.file-row.selected {
background: #2d4a6b;
}
.col-name {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
}
.file-icon {
font-size: 14px;
flex-shrink: 0;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-type {
width: 80px;
text-align: center;
font-size: 11px;
color: #888;
}
.col-size {
width: 80px;
text-align: right;
font-size: 11px;
color: #888;
}
.col-date {
width: 120px;
text-align: right;
font-size: 11px;
color: #888;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-state p {
margin: 0 0 16px 0;
font-size: 14px;
}
.empty-actions {
display: flex;
gap: 8px;
}
.empty-actions button {
background: #409eff;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
}
.empty-actions button:hover {
background: #66b1ff;
}
/* 文件类型特定样式 */
.file-item.folder .file-item-name,
.file-row.folder .file-name {
color: #e6a23c;
font-weight: 500;
}
.file-item.script .file-item-name,
.file-row.script .file-name {
color: #67c23a;
}
.file-item.model .file-item-name,
.file-row.model .file-name {
color: #409eff;
}
.file-item.image .file-item-name,
.file-item.texture .file-item-name,
.file-row.image .file-name,
.file-row.texture .file-name {
color: #f56c6c;
}
.file-item.material .file-item-name,
.file-row.material .file-name {
color: #e6a23c;
}
/* 滚动条样式 */
.tree-content::-webkit-scrollbar,
.list-content::-webkit-scrollbar {
width: 8px;
}
.tree-content::-webkit-scrollbar-track,
.list-content::-webkit-scrollbar-track {
background: #2d2d2d;
}
.tree-content::-webkit-scrollbar-thumb,
.list-content::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
.tree-content::-webkit-scrollbar-thumb:hover,
.list-content::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}
/* Element Plus 树组件样式覆盖 */
/* :deep(.el-tree) {
background: transparent;
color: #cccccc;
}
.tree-content :deep(.el-tree-node__content) {
background: transparent;
border-radius: 4px;
margin: 2px 0;
padding: 4px 8px;
transition: background-color 0.2s;
}
.tree-content :deep(.el-tree-node__content:hover) {
background: #404040;
}
.tree-content :deep(.el-tree-node.is-current > .el-tree-node__content) {
background: #409eff;
color: white;
}
.tree-content :deep(.el-tree-node__expand-icon) {
color: #888;
font-size: 12px;
}
.tree-content :deep(.el-tree-node__expand-icon.is-leaf) {
color: transparent;
}
*/
.tree-node-label {
color: #ffffff;
}
.el-tree {
background: #1c00be00;
}
/* 设置节点的默认文字颜色 */
::v-deep .el-tree-node__label {
color: #ffffff;
/* 自定义你想要的颜色 */
}
/* 设置鼠标悬停时的背景颜色 */
::v-deep .el-tree-node:hover>.el-tree-node__content {
background-color: #f0f9eb00;
/* 自定义悬停背景色 */
}
/* 设置鼠标悬停时的文字颜色 */
::v-deep .el-tree-node:hover .el-tree-node__label {
color: #f8f8f8;
/* 自定义悬停文字颜色 */
}
::v-deep .el-tree-node__content {
background-color: rgba(255, 255, 255, 0);
/* 鼠标离开后恢复的背景色 */
color: #333;
/* 默认文字颜色 */
}
::v-deep .el-tree-node.is-current>.el-tree-node__content {
background-color: #ffffff28;
}
</style>

View File

@ -0,0 +1,428 @@
<template>
<div class="bottom-panel" :style="{ height: layoutStore.bottomPanelHeight + 'px' }">
<!-- 拖拽调整高度的手柄 -->
<div
class="resize-handle"
@mousedown="startResize"
@dblclick="resetHeight"
>
<div class="resize-indicator"></div>
</div>
<div class="panel-tabs">
<div class="tab-list">
<button
v-for="tab in tabs"
:key="tab.key"
:class="['tab-button', { active: activeTab === tab.key }]"
@click="setActiveTab(tab.key)"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-label">{{ tab.label }}</span>
<span v-if="tab.badge" class="tab-badge">{{ tab.badge }}</span>
</button>
</div>
<div class="tab-actions">
<button @click="togglePanel" :title="layoutStore.bottomPanelCollapsed ? '展开面板' : '收起面板'">
{{ layoutStore.bottomPanelCollapsed ? '⬆️' : '⬇️' }}
</button>
<button @click="closePanel" title="关闭面板">
</button>
</div>
</div>
<div v-show="!layoutStore.bottomPanelCollapsed" class="panel-content">
<!-- 文件系统 -->
<div v-show="activeTab === 'files'" class="tab-content">
<FileSystem ref="fileSystemRef" />
</div>
<!-- 控制台 -->
<div v-show="activeTab === 'console'" class="tab-content">
<Console ref="consoleRef" />
</div>
<!-- 终端 -->
<div v-show="activeTab === 'terminal'" class="tab-content">
<div class="terminal-placeholder">
<div class="placeholder-content">
<h3>终端</h3>
<p>终端功能正在开发中...</p>
<button @click="addTestTerminalOutput">添加测试输出</button>
</div>
</div>
</div>
<!-- 输出 -->
<div v-show="activeTab === 'output'" class="tab-content">
<div class="output-placeholder">
<div class="placeholder-content">
<h3>输出</h3>
<p>构建输出将显示在这里</p>
<div class="output-example">
<pre>{{ sampleOutput }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useLayoutStore } from '@/stores/Layout'
import FileSystem from './FileSystem/index.vue'
import Console from './Console/index.vue'
// 使用布局store
const layoutStore = useLayoutStore()
// 标签页配置
const tabs = [
{ key: 'files', label: '文件', icon: '📁', badge: null },
{ key: 'console', label: '控制台', icon: '🖥️', badge: null },
{ key: 'terminal', label: '终端', icon: '⚡', badge: null },
{ key: 'output', label: '输出', icon: '📄', badge: null }
]
// 响应式数据
const activeTab = ref('files')
// 组件引用
const fileSystemRef = ref()
const consoleRef = ref()
// 示例输出内容
const sampleOutput = ref(`> 正在构建项目...
✓ 编译 TypeScript 文件
✓ 打包资源文件
✓ 优化代码
✓ 生成 source map
✅ 构建完成!
输出目录: dist/
构建时间: 2.3s
文件大小: 1.2MB`)
// 计算属性
const consoleBadge = computed(() => {
if (consoleRef.value?.logs?.length > 0) {
return consoleRef.value.logs.length.toString()
}
return null
})
// 方法
function setActiveTab(tabKey: string) {
activeTab.value = tabKey
}
function togglePanel() {
layoutStore.toggleBottomPanelCollapse()
}
function closePanel() {
layoutStore.hideBottomPanel()
}
function addTestTerminalOutput() {
if (consoleRef.value) {
consoleRef.value.info('终端命令执行: npm run build', { source: 'Terminal' })
consoleRef.value.success('构建完成', { source: 'Terminal' })
}
}
// 拖拽调整高度功能
function startResize(event: MouseEvent) {
layoutStore.setResizing(true, 'bottom')
const startY = event.clientY
const startHeight = layoutStore.bottomPanelHeight
const handleMouseMove = (e: MouseEvent) => {
if (!layoutStore.isResizing) return
const deltaY = startY - e.clientY // 向上拖拽为正值
layoutStore.setBottomPanelHeight(startHeight + deltaY)
}
const handleMouseUp = () => {
layoutStore.setResizing(false, null)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
// 保存布局配置
layoutStore.saveLayoutConfig()
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.body.style.cursor = 'ns-resize'
document.body.style.userSelect = 'none'
event.preventDefault()
}
function resetHeight() {
layoutStore.setBottomPanelHeight(300) // 重置到默认高度
layoutStore.saveLayoutConfig()
}
// 暴露方法给父组件
defineExpose({
showPanel: () => {
layoutStore.showBottomPanel()
},
hidePanel: () => {
layoutStore.hideBottomPanel()
},
togglePanel,
setActiveTab,
addLog: (type: string, message: string, options?: any) => {
if (consoleRef.value) {
consoleRef.value.addLog(type, message, options)
// 自动切换到控制台标签
if (type === 'error' || type === 'warn') {
activeTab.value = 'console'
}
}
},
console: computed(() => consoleRef.value),
fileSystem: computed(() => fileSystemRef.value)
})
// 生命周期
onMounted(() => {
// 更新控制台徽章
if (consoleRef.value) {
const consoleTab = tabs.find(tab => tab.key === 'console')
if (consoleTab) {
consoleTab.badge = consoleBadge.value
}
}
})
</script>
<style scoped>
.bottom-panel {
display: flex;
flex-direction: column;
background: #2d2d2d;
color: #cccccc;
border-top: 1px solid #3e3e3e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
}
/* 拖拽调整高度的手柄 */
.resize-handle {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 6px;
background: transparent;
cursor: ns-resize;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.resize-handle:hover {
background: rgba(0, 122, 204, 0.2);
}
.resize-handle:active {
background: rgba(0, 122, 204, 0.4);
}
.resize-indicator {
width: 40px;
height: 3px;
background: #4a4a4a;
border-radius: 2px;
transition: all 0.2s;
}
.resize-handle:hover .resize-indicator {
background: #007acc;
width: 60px;
}
.resize-handle:active .resize-indicator {
background: #005a9e;
width: 80px;
}
.panel-tabs {
display: flex;
align-items: center;
justify-content: space-between;
background: #3c3c3c;
border-bottom: 1px solid #4a4a4a;
}
.tab-list {
display: flex;
align-items: center;
}
.tab-button {
display: flex;
align-items: center;
gap: 6px;
background: transparent;
border: none;
color: #888;
padding: 2px 16px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
border-top: 2px solid transparent;
}
.tab-button:hover {
background: #4a4a4a;
color: #cccccc;
}
.tab-button.active {
background: #2d2d2d;
color: #ffffff;
border-top-color: #007acc;
}
.tab-icon {
font-size: 14px;
}
.tab-badge {
background: #f14c4c;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
min-width: 16px;
text-align: center;
line-height: 1;
}
.tab-actions {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px;
}
.tab-actions button {
background: transparent;
border: 1px solid #4a4a4a;
color: #888;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.tab-actions button:hover {
background: #4a4a4a;
border-color: #007acc;
color: #cccccc;
}
.panel-content {
flex: 1;
min-height: 0;
background: #2d2d2d;
}
.tab-content {
height: 100%;
overflow: hidden;
}
.terminal-placeholder,
.output-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #1e1e1e;
}
.placeholder-content {
text-align: center;
color: #888;
}
.placeholder-content h3 {
margin: 0 0 12px 0;
color: #cccccc;
font-size: 18px;
font-weight: 500;
}
.placeholder-content p {
margin: 0 0 16px 0;
font-size: 14px;
}
.placeholder-content button {
background: #007acc;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
}
.placeholder-content button:hover {
background: #005a9e;
}
.output-example {
margin-top: 16px;
text-align: left;
background: #2d2d2d;
border: 1px solid #3e3e3e;
border-radius: 4px;
padding: 12px;
max-width: 400px;
}
.output-example pre {
margin: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
color: #d4d4d4;
white-space: pre-wrap;
}
/* 响应式设计 */
@media (max-width: 768px) {
.tab-button {
padding: 6px 12px;
font-size: 12px;
}
.tab-label {
display: none;
}
.tab-icon {
font-size: 16px;
}
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="hierarchy-panel" @click="handlePanelClick">
<el-tree ref="HierarchytreeRef" style="max-width: 600px" :data="HierarchytreeData" node-key="id"
:props="defaultProps" @node-click="handleNodeClick">
<template #default="{ node, data }">
</template>
</el-tree>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useHierarchyPanelStore } from 'stores/HierarchyPanel'
import { storeToRefs } from 'pinia'
import { useInspectorPanelStore } from '@/stores/InspectorPanel'
const hierarchyPanelStore = useHierarchyPanelStore() // 获取HierarchyPanelStore 树结构pinia
const inspectorPanelStore = useInspectorPanelStore() // 获取InspectorPanelStore 面板pinia
const { selectedHierarchytreeData, HierarchytreeData } = storeToRefs(hierarchyPanelStore)
const HierarchytreeRef = ref()
const defaultProps = ref({
children: 'children',
label: 'label'
})
watch(selectedHierarchytreeData, (node: any) => {
// data.value = newVal
if (node) {
HierarchytreeRef.value.setCurrentKey(node.id)
}
else {
HierarchytreeRef.value.setCurrentKey(null)
}
})
const handleNodeClick = (node: any) => {
console.log(node);
hierarchyPanelStore.setSelectedData(node)
inspectorPanelStore.setSelecteInspectorData(node)
}
const handlePanelClick = () => {
hierarchyPanelStore.setSelectedData(null)
inspectorPanelStore.setSelecteInspectorData(null)
}
</script>
<style scoped>
.hierarchy-panel {
height: 100%;
border-right: 1px solid #dcdfe6;
}
.el-tree {
background: #1c00be00;
}
/* 设置节点的默认文字颜色 */
::v-deep .el-tree-node__label {
color: #ffffff;
/* 自定义你想要的颜色 */
}
/* 设置鼠标悬停时的背景颜色 */
::v-deep .el-tree-node:hover>.el-tree-node__content {
background-color: #f0f9eb00;
/* 自定义悬停背景色 */
}
/* 设置鼠标悬停时的文字颜色 */
::v-deep .el-tree-node:hover .el-tree-node__label {
color: #f8f8f8;
/* 自定义悬停文字颜色 */
}
::v-deep .el-tree-node__content {
background-color: rgba(255, 255, 255, 0);
/* 鼠标离开后恢复的背景色 */
color: #333;
/* 默认文字颜色 */
}
::v-deep .el-tree-node.is-current>.el-tree-node__content {
background-color: #ffffff28;
}
</style>

View File

@ -0,0 +1 @@
export const contain=["Camera"]

View File

@ -0,0 +1,480 @@
<template>
<div class="camera-panel" v-show="visible">
<!-- Camera 标题栏 -->
<CollapsibleHeader title="相机" :expanded="isExpanded" :enabled="isEnabled" :checkable="false" @update:expanded="isExpanded = $event"
@update:enabled="handleEnabledChange" @action-click="handleActionClick" />
<!-- Camera 内容 -->
<div class="camera-content" v-show="isExpanded">
<!-- 相机类型选择 -->
<PropertyRow label="类型" tooltip="选择场景中使用的相机类型">
<Select v-model="cameraData.type" :options="cameraTypeOptions" @change="onCameraTypeChange" />
</PropertyRow>
<!-- ArcRotateCamera 属性 -->
<template v-if="cameraData.type === 'ArcRotateCamera'">
<!-- Target -->
<PropertyRow label="目标位置" tooltip="相机观察的目标位置 (Vector3)">
<div class="vector3-input">
<div v-for="axis in vector3Axes" :key="axis.key" class="value-group">
<span class="axis-label">{{ axis.label }}</span>
<NumberInput :model-value="cameraData.arcRotate.target[axis.key]"
@update:model-value="updateTargetAxis(axis.key, $event)" :step="0.1" />
</div>
</div>
</PropertyRow>
<!-- 范围输入组件 -->
<PropertyRow v-for="range in arcRotateRanges" :key="range.key" :label="range.label" :tooltip="range.tooltip">
<div class="range-input">
<div class="value-group">
<span class="axis-label">最小</span>
<NumberInput :model-value="cameraData.arcRotate[range.minKey]" @update:model-value="range.updateMin"
:step="range.step" :min="range.min" />
</div>
<div class="value-group">
<span class="axis-label">最大</span>
<NumberInput :model-value="cameraData.arcRotate[range.maxKey]" @update:model-value="range.updateMax"
:step="range.step" :min="range.min" />
</div>
</div>
</PropertyRow>
<!-- 单值属性 -->
<!-- Wheel Delta Percentage -->
<PropertyRow label="滚轮灵敏度" tooltip="鼠标滚轮缩放灵敏度百分比">
<NumberInput v-model="cameraData.arcRotate.wheelDeltaPercentage" @update:model-value="updateArcRotateCamera"
:step="0.01" :min="0" :max="1" :full-width="true" />
</PropertyRow>
<!-- Panning Sensibility -->
<PropertyRow label="平移灵敏度" tooltip="平移灵敏度 - 数值越小越灵敏">
<NumberInput v-model="cameraData.arcRotate.panningSensibility" @update:model-value="updateArcRotateCamera"
:step="1" :min="1" :full-width="true" />
</PropertyRow>
<!-- Inertia -->
<PropertyRow label="惯性" tooltip="移动惯性 (0-1),默认 0.9">
<Slider v-model="cameraData.arcRotate.inertia" @change="updateArcRotateCamera" :min="0" :max="1"
:step="0.01" />
</PropertyRow>
<!-- Auto Rotation -->
<PropertyRow label="自动旋转" tooltip="启用相机自动旋转">
<Switch v-model="cameraData.arcRotate.autoRotation" @change="updateArcRotateCamera" size="small" />
</PropertyRow>
</template>
<!-- UniversalCamera 属性 -->
<template v-if="cameraData.type === 'UniversalCamera'">
<!-- 数值属性 -->
<PropertyRow v-for="prop in universalNumericProps" :key="prop.key" :label="prop.label" :tooltip="prop.tooltip">
<NumberInput v-model="cameraData.universal[prop.key]" @update:model-value="updateUniversalCamera"
v-bind="prop.props" />
</PropertyRow>
<!-- 键位绑定 -->
<PropertyRow v-for="keyProp in universalKeyProps" :key="keyProp.key" :label="keyProp.label"
:tooltip="keyProp.tooltip">
<TextInput v-model="cameraData.universal[keyProp.key]" @update:model-value="updateUniversalCamera"
:placeholder="keyProp.placeholder" text-align="center" :uppercase="true" />
</PropertyRow>
</template>
<!-- VR Camera 属性 -->
<template v-if="cameraData.type === 'vrCamera'">
<!-- 数值属性 -->
<PropertyRow v-for="prop in vrNumericProps" :key="prop.key" :label="prop.label" :tooltip="prop.tooltip">
<NumberInput v-model="cameraData.vr[prop.key]" @update:model-value="updateVrCamera" v-bind="prop.props" />
</PropertyRow>
<!-- VR 范围输入 -->
<PropertyRow v-for="range in vrRanges" :key="range.key" :label="range.label" :tooltip="range.tooltip">
<div class="range-input">
<div class="value-group">
<span class="axis-label">最小</span>
<NumberInput :model-value="cameraData.vr[range.minKey]" @update:model-value="range.updateMin"
:step="range.step" :min="range.min" />
</div>
<div class="value-group">
<span class="axis-label">最大</span>
<NumberInput :model-value="cameraData.vr[range.maxKey]" @update:model-value="range.updateMax"
:step="range.step" :min="range.min" />
</div>
</div>
</PropertyRow>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, reactive, computed } from 'vue'
import { useInspectorPanelStore } from 'stores/InspectorPanel'
import { storeToRefs } from 'pinia'
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
import Select from 'components/BasicControls/Select/index.vue'
import NumberInput from 'components/BasicControls/Input/index.vue'
import Slider from 'components/BasicControls/Slider/index.vue'
import Switch from 'components/BasicControls/Switch/index.vue'
import TextInput from 'components/BasicControls/Input/TextInput.vue'
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS } from '../../../composables/useInspectorPanel'
import type { SelectOption } from 'components/BasicControls/Select/types'
import type {
Vector3,
CameraData,
} from './types'
import { contain } from "./index"
const inspectorPanelStore = useInspectorPanelStore()
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
// 使用通用的 Inspector Panel 逻辑
const {
isExpanded,
isEnabled,
createEnabledChangeHandler,
createActionClickHandler,
createShowMoreHandler
} = useInspectorPanel()
// 相机类型选项
const cameraTypeOptions: SelectOption[] = [
{ value: 'ArcRotateCamera', label: '弧形旋转相机' },
{ value: 'UniversalCamera', label: '通用相机' },
{ value: 'vrCamera', label: 'VR相机' }
]
// 相机数据
const cameraData = reactive<CameraData>({
type: 'ArcRotateCamera',
arcRotate: { ...DEFAULT_COMPONENT_CONFIGS.camera.arcRotate },
universal: { ...DEFAULT_COMPONENT_CONFIGS.camera.universal },
vr: { ...DEFAULT_COMPONENT_CONFIGS.camera.vr }
})
// Vector3 轴配置
const vector3Axes = [
{ key: 'x' as keyof Vector3, label: 'X' },
{ key: 'y' as keyof Vector3, label: 'Y' },
{ key: 'z' as keyof Vector3, label: 'Z' }
]
const visible = ref<any>({})
const inspectorPanelData = ref<any>(
{
id: "",
label: '',
type: "",
}
)
watch(selecteInspectorData, (node: any) => {
if (!node) {
visible.value = false
return;
}
if (contain.includes(node.type)) {
visible.value = true
} else {
visible.value = false
}
inspectorPanelData.value = node
})
// 更新函数
const updateArcRotateCamera = () => {
// console.log('弧形旋转相机已更新:', cameraData.arcRotate)
// 这里可以添加实际的更新逻辑
}
const updateUniversalCamera = () => {
//console.log('通用相机已更新:', cameraData.universal)
// 这里可以添加实际的更新逻辑
}
const updateVrCamera = () => {
//console.log('VR相机已更新:', cameraData.vr)
// 这里可以添加实际的更新逻辑
}
// 范围更新函数
const updateBetaMin = (value: number) => {
cameraData.arcRotate.lowerBetaLimit = value
updateArcRotateCamera()
}
const updateBetaMax = (value: number) => {
cameraData.arcRotate.upperBetaLimit = value
updateArcRotateCamera()
}
const updateRadiusMin = (value: number) => {
cameraData.arcRotate.lowerRadiusLimit = value
updateArcRotateCamera()
}
const updateRadiusMax = (value: number) => {
cameraData.arcRotate.upperRadiusLimit = value
updateArcRotateCamera()
}
const updateVrBetaMin = (value: number) => {
cameraData.vr.lowerBetaLimit = value
updateVrCamera()
}
const updateVrBetaMax = (value: number) => {
cameraData.vr.upperBetaLimit = value
updateVrCamera()
}
const updateVrRadiusMin = (value: number) => {
cameraData.vr.lowerRadiusLimit = value
updateVrCamera()
}
const updateVrRadiusMax = (value: number) => {
cameraData.vr.upperRadiusLimit = value
updateVrCamera()
}
const updateTargetAxis = (axis: keyof Vector3, value: number) => {
cameraData.arcRotate.target[axis] = value
updateArcRotateCamera()
}
// 弧形旋转相机范围配置
const arcRotateRanges = [
{
key: 'beta',
label: 'Beta角度限制',
tooltip: '垂直角度限制 (弧度)',
minKey: 'lowerBetaLimit' as const,
maxKey: 'upperBetaLimit' as const,
step: 0.1,
updateMin: updateBetaMin,
updateMax: updateBetaMax
},
{
key: 'radius',
label: '半径限制',
tooltip: '距离目标的缩放距离限制',
minKey: 'lowerRadiusLimit' as const,
maxKey: 'upperRadiusLimit' as const,
step: 0.1,
min: 0,
updateMin: updateRadiusMin,
updateMax: updateRadiusMax
}
]
// 通用相机数值属性配置
const universalNumericProps = [
{
key: 'speed' as const,
label: '移动速度',
tooltip: '相机的移动速度',
props: { step: 0.1, min: 0, 'full-width': true }
},
{
key: 'angularSensibility' as const,
label: '旋转灵敏度',
tooltip: '鼠标旋转灵敏度',
props: { step: 1, min: 1, 'full-width': true }
}
]
// 通用相机键位属性配置
const universalKeyProps = [
{
key: 'keysUp' as const,
label: '前进键',
tooltip: '相机移动的键位绑定 (默认WASD)',
placeholder: 'W'
},
{
key: 'keysDown' as const,
label: '后退键',
tooltip: '相机移动的键位绑定 (默认WASD)',
placeholder: 'S'
},
{
key: 'keysLeft' as const,
label: '左移键',
tooltip: '相机移动的键位绑定 (默认WASD)',
placeholder: 'A'
},
{
key: 'keysRight' as const,
label: '右移键',
tooltip: '相机移动的键位绑定 (默认WASD)',
placeholder: 'D'
}
]
// VR相机数值属性配置
const vrNumericProps = [
{
key: 'angularSensibility' as const,
label: '旋转灵敏度',
tooltip: 'VR相机的鼠标旋转灵敏度',
props: { step: 1, min: 1, 'full-width': true }
},
{
key: 'wheelDeltaPercentage' as const,
label: '滚轮灵敏度',
tooltip: 'VR相机的鼠标滚轮缩放灵敏度',
props: { step: 0.01, min: 0, max: 1, 'full-width': true }
}
]
// VR相机范围配置
const vrRanges = [
{
key: 'beta',
label: 'Beta角度限制',
tooltip: 'VR相机的垂直角度限制 (弧度)',
minKey: 'lowerBetaLimit' as const,
maxKey: 'upperBetaLimit' as const,
step: 0.1,
updateMin: updateVrBetaMin,
updateMax: updateVrBetaMax
},
{
key: 'radius',
label: '半径限制',
tooltip: 'VR相机的缩放距离限制',
minKey: 'lowerRadiusLimit' as const,
maxKey: 'upperRadiusLimit' as const,
step: 0.1,
min: 0,
updateMin: updateVrRadiusMin,
updateMax: updateVrRadiusMax
}
]
// 相机类型变化处理
const onCameraTypeChange = () => {
console.log('相机类型已更改为:', cameraData.type)
// 这里可以添加相机类型切换的逻辑
}
// 重置相机
const resetCamera = () => {
switch (cameraData.type) {
case 'ArcRotateCamera':
Object.assign(cameraData.arcRotate, DEFAULT_COMPONENT_CONFIGS.camera.arcRotate)
updateArcRotateCamera()
break
case 'UniversalCamera':
Object.assign(cameraData.universal, DEFAULT_COMPONENT_CONFIGS.camera.universal)
updateUniversalCamera()
break
case 'vrCamera':
Object.assign(cameraData.vr, DEFAULT_COMPONENT_CONFIGS.camera.vr)
updateVrCamera()
break
}
}
// 处理启用状态变化
const handleEnabledChange = createEnabledChangeHandler('Camera')
// 处理操作按钮点击
const handleActionClick = createActionClickHandler(
resetCamera,
createShowMoreHandler('Camera')
)
// 监听选中对象变化
watch(selecteInspectorData, (node: any) => {
if (node && node.camera) {
// 更新相机数据
const camera = node.camera
if (camera.type) {
cameraData.type = camera.type
}
// 根据相机类型更新对应数据
if (camera.type === 'ArcRotateCamera' && camera.arcRotate) {
Object.assign(cameraData.arcRotate, camera.arcRotate)
} else if (camera.type === 'UniversalCamera' && camera.universal) {
Object.assign(cameraData.universal, camera.universal)
} else if (camera.type === 'vrCamera' && camera.vr) {
Object.assign(cameraData.vr, camera.vr)
}
isEnabled.value = camera.isEnabled !== false
}
}, { immediate: true })
</script>
<style scoped>
.camera-panel {
background: #393939;
border-bottom: 1px solid #2d2d2d;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 12px;
width: 100%;
}
.camera-content {
padding: 4px 8px 8px 8px;
}
.vector3-input {
display: flex;
gap: 4px;
width: 100%;
}
.vector3-input .value-group {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 2px;
}
.vector3-input .axis-label {
width: 20px;
color: #888888;
font-size: 10px;
text-align: center;
flex-shrink: 0;
}
.range-input {
display: flex;
gap: 4px;
width: 100%;
}
.range-input .value-group {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 2px;
}
.range-input .axis-label {
width: 20px;
color: #888888;
font-size: 10px;
text-align: center;
flex-shrink: 0;
}
/* 容器自适应 */
@container (max-width: 200px) {
.vector3-input .axis-label,
.range-input .axis-label {
width: 15px;
font-size: 9px;
}
}
</style>

View File

@ -0,0 +1,57 @@
// Camera 组件类型定义
export interface Vector3 {
x: number
y: number
z: number
}
export interface ArcRotateCameraData {
target: Vector3
lowerBetaLimit: number
upperBetaLimit: number
lowerRadiusLimit: number
upperRadiusLimit: number
wheelDeltaPercentage: number
panningSensibility: number
inertia: number
autoRotation: boolean
}
export interface UniversalCameraData {
speed: number
angularSensibility: number
keysUp: string
keysDown: string
keysLeft: string
keysRight: string
}
export interface VrCameraData {
angularSensibility: number
lowerBetaLimit: number
upperBetaLimit: number
lowerRadiusLimit: number
upperRadiusLimit: number
wheelDeltaPercentage: number
}
export interface CameraData {
type: 'ArcRotateCamera' | 'UniversalCamera' | 'vrCamera'
arcRotate: ArcRotateCameraData
universal: UniversalCameraData
vr: VrCameraData
}
export interface CameraTypeOption {
label: string
value: 'ArcRotateCamera' | 'UniversalCamera' | 'vrCamera'
}
export interface CameraProps {
// 如果有 props 的话可以在这里定义
}
export interface CameraEmits {
// 如果有 emits 的话可以在这里定义
}

View File

@ -0,0 +1,255 @@
<template>
<div class="inspector-header-panel">
<div class="header-content">
<!-- 左侧图标和激活状态 -->
<div class="header-left">
<Switch v-model="isActive" @change="toggleActive" size="small" />
<!-- 对象名称输入框 -->
<div class="object-name">
<TextInput v-model="objectName" placeholder="GameObject" @update:modelValue="updateObjectName"
class="name-input-wrapper" />
</div>
</div>
<!-- 右侧内容 -->
<div class="header-right">
<!-- 透明模式 -->
<PropertyRow label="图层" tooltip="图层">
<Select v-model="selectedLayer" :options="layerOptions" @change="updateLayer" />
</PropertyRow>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { useInspectorPanelStore } from 'stores/InspectorPanel'
import { storeToRefs } from 'pinia'
import Switch from 'components/BasicControls/Switch/index.vue'
import TextInput from 'components/BasicControls/Input/TextInput.vue'
import Select from 'components/BasicControls/Select/index.vue'
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
import type { SelectOption } from 'components/BasicControls/Select/types'
const inspectorPanelStore = useInspectorPanelStore()
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
// 对象属性
const isActive = ref(true)
const objectName = ref('Directional Light')
const isStatic = ref(false)
const selectedTag = ref('Untagged')
const selectedLayer = ref('Default')
// 图层选项
const layerOptions: SelectOption[] = [
{ value: 'Default', label: '默认' },
{ value: 'TransparentFX', label: '透明特效' },
{ value: 'Ignore Raycast', label: '忽略射线' },
{ value: 'Water', label: '水面' },
{ value: 'UI', label: '用户界面' },
{ value: 'PostProcessing', label: '后期处理' }
]
// 切换激活状态
const toggleActive = () => {
if (selecteInspectorData.value) {
(selecteInspectorData.value as any).setEnabled(isActive.value)
console.log(`对象${isActive.value ? '已激活' : '已停用'}`)
}
}
// 更新对象名称
const updateObjectName = () => {
if (selecteInspectorData.value && objectName.value.trim()) {
(selecteInspectorData.value as any).name = objectName.value.trim()
console.log('对象名称已更新:', objectName.value)
}
}
// 更新Static状态
const updateStatic = () => {
if (selecteInspectorData.value) {
console.log(`静态${isStatic.value ? '已启用' : '已禁用'}`)
}
}
// 更新Tag
const updateTag = () => {
if (selecteInspectorData.value) {
console.log(`标签已更改为 ${selectedTag.value}`)
}
}
// 更新Layer
const updateLayer = () => {
if (selecteInspectorData.value) {
console.log(`图层已更改为 ${selectedLayer.value}`)
}
}
// 监听选中对象变化
watch(selecteInspectorData, (node: any) => {
if (node) {
// 更新界面数据
objectName.value = node.label || 'GameObject'
isActive.value = node.isEnabled !== false
console.log(node);
// 根据对象类型设置不同的默认值
if (node.label?.includes('Light')) {
selectedTag.value = 'Untagged'
selectedLayer.value = 'Default'
} else if (node.label?.includes('Camera')) {
selectedTag.value = 'MainCamera'
selectedLayer.value = 'Default'
} else {
selectedTag.value = 'Untagged'
selectedLayer.value = 'Default'
}
} else {
// 重置为默认值
objectName.value = 'GameObject'
isActive.value = true
isStatic.value = false
selectedTag.value = 'Untagged'
selectedLayer.value = 'Default'
}
}, { immediate: true })
</script>
<style scoped>
.inspector-header-panel {
background: #393939;
border-bottom: 1px solid #2d2d2d;
padding: 8px 12px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.header-left {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
padding: 2px;
}
.object-icon {
width: 32px;
height: 32px;
background: #4a4a4a;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #5a5a5a;
color: #cccccc;
}
.header-right {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.object-name {
width: 100%;
}
.name-input-wrapper {
height: 24px;
}
.name-input-wrapper :deep(.text-input) {
height: 24px;
font-size: 12px;
padding: 0 6px;
}
.static-item {
gap: 6px;
}
.property-label {
width: 0px;
}
/* 自定义复选框 */
.checkbox-wrapper {
position: relative;
display: inline-block;
}
.custom-checkbox {
opacity: 0;
position: absolute;
width: 14px;
height: 14px;
margin: 0;
cursor: pointer;
}
.checkbox-label {
display: inline-block;
width: 14px;
height: 14px;
background: #2d2d2d;
border: 1px solid #4a4a4a;
border-radius: 2px;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.checkbox-label:hover {
border-color: #409eff;
}
.custom-checkbox:checked+.checkbox-label {
background: #409eff;
border-color: #409eff;
}
.custom-checkbox:checked+.checkbox-label::after {
content: '';
position: absolute;
left: 3px;
top: 1px;
width: 6px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 响应式设计 */
@media (max-width: 768px) {
.object-properties {
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.property-item {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,31 @@
// Header 组件类型定义
export interface HeaderData {
isActive: boolean
objectName: string
isStatic: boolean
selectedTag: string
selectedLayer: string
}
export interface HeaderProps {
// 如果有 props 的话可以在这里定义
}
export interface HeaderEmits {
// 如果有 emits 的话可以在这里定义
}
export type TagType = 'Untagged' | 'Respawn' | 'Finish' | 'EditorOnly' | 'MainCamera' | 'Player' | 'GameController'
export type LayerType = 'Default' | 'TransparentFX' | 'Ignore Raycast' | 'Water' | 'UI' | 'PostProcessing'
export interface TagOption {
label: string
value: TagType
}
export interface LayerOption {
label: string
value: LayerType
}

View File

@ -0,0 +1 @@
export const contain=["Light"]

View File

@ -0,0 +1,181 @@
<template>
<div class="light-panel" v-show="visible">
<CollapsibleHeader
title="灯光"
:expanded="isExpanded"
:enabled="isEnabled"
:checkable="false"
@update:expanded="isExpanded = $event"
@update:enabled="handleEnabledChange"
@action-click="handleActionClick"
>
<div class="light-content">
<!-- Type 属性 -->
<PropertyRow
label="类型"
tooltip="指定当前光源的类型。可能的类型有方向光、聚光灯、点光源和区域光。"
>
<Select
v-model="lightData.type"
:options="lightTypeOptions"
@change="updateLightType"
/>
</PropertyRow>
<!-- Color 属性 -->
<PropertyRow
label="颜色"
tooltip="此光源发出的光的颜色。"
>
<ColorInput
:model-value="lightData.color"
@update:model-value="updateLightColor"
/>
</PropertyRow>
<!-- Intensity 属性 -->
<PropertyRow
label="强度"
tooltip="光的亮度。数值越高产生的光越亮"
>
<Slider
v-model="lightData.intensity"
:min="0"
:max="10"
:step="0.1"
:show-value="true"
@change="updateLightIntensity"
/>
</PropertyRow>
</div>
</CollapsibleHeader>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { useInspectorPanelStore } from 'stores/InspectorPanel'
import { storeToRefs } from 'pinia'
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
import Select from 'components/BasicControls/Select/index.vue'
import ColorInput from 'components/BasicControls/Input/ColorInput.vue'
import Slider from 'components/BasicControls/Slider/index.vue'
import Switch from 'components/BasicControls/Switch/index.vue'
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS, colorUtils } from '../../../composables/useInspectorPanel'
import {contain} from "./index"
const inspectorPanelStore = useInspectorPanelStore()
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
// 使用通用的 Inspector Panel 逻辑
const {
isExpanded,
isEnabled,
createEnabledChangeHandler,
createActionClickHandler,
createShowMoreHandler
} = useInspectorPanel()
// Light数据
const lightData = reactive({ ...DEFAULT_COMPONENT_CONFIGS.light })
// 光源类型选项
const lightTypeOptions = [
{ label: '方向光', value: 'directional' },
{ label: '点光源', value: 'point' },
{ label: '聚光灯', value: 'spot' },
{ label: '区域光', value: 'area' }
]
const visible = ref<any>({})
const inspectorPanelData = ref<any>({})
watch(selecteInspectorData, (node: any) => {
if (!node) {
visible.value = false
return;
}
if(contain.includes(node.type)){
visible.value = true
}else{
visible.value = false
}
inspectorPanelData.value = node
})
// 更新函数
const updateLightType = () => {
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightType) {
(selecteInspectorData.value as any).lightType = lightData.type
}
}
const updateLightColor = (color: string) => {
lightData.color = color
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightColor) {
const rgb = colorUtils.hexStringToRgb(color)
const lightColorObj = (selecteInspectorData.value as any).lightColor
lightColorObj.r = rgb.r
lightColorObj.g = rgb.g
lightColorObj.b = rgb.b
}
}
const updateLightIntensity = () => {
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightIntensity !== undefined) {
(selecteInspectorData.value as any).lightIntensity = lightData.intensity
}
}
// 重置Light
const resetLight = () => {
Object.assign(lightData, DEFAULT_COMPONENT_CONFIGS.light)
updateLightType()
updateLightColor(lightData.color)
updateLightIntensity()
}
// 处理启用状态变化
const handleEnabledChange = createEnabledChangeHandler('光源', (enabled: boolean) => {
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.setEnabled) {
(selecteInspectorData.value as any).setEnabled(enabled)
}
})
// 处理操作按钮点击
const handleActionClick = createActionClickHandler(
resetLight,
createShowMoreHandler('光源')
)
// 监听选中对象变化
watch(selecteInspectorData, (node: any) => {
if (node) {
// 更新Light数据
if (node.lightType) {
lightData.type = node.lightType
}
if (node.lightColor) {
lightData.color = colorUtils.rgbToHexString(node.lightColor)
}
if (node.lightIntensity !== undefined) {
lightData.intensity = node.lightIntensity
}
isEnabled.value = node.isEnabled !== false
} else {
// 重置为默认值
Object.assign(lightData, DEFAULT_COMPONENT_CONFIGS.light)
isEnabled.value = true
}
}, { immediate: true })
</script>
<style scoped>
.light-content {
/* Remove redundant styling since CollapsibleHeader handles container */
}
</style>

View File

@ -0,0 +1,28 @@
// Light 组件类型定义
export interface RGB {
r: number
g: number
b: number
}
export interface LightData {
type: 'directional' | 'point' | 'spot' | 'area'
color: string
intensity: number
}
export interface LightProps {
// 如果有 props 的话可以在这里定义
}
export interface LightEmits {
// 如果有 emits 的话可以在这里定义
}
export type LightType = 'directional' | 'point' | 'spot' | 'area'
export interface LightTypeOption {
label: string
value: LightType
}

View File

@ -0,0 +1 @@
export const contain=["Material"]

View File

@ -0,0 +1,197 @@
<template>
<div class="material-panel" v-show="visible">
<CollapsibleHeader title="材质"
:expanded="isExpanded"
:enabled="isEnabled"
:checkable="false"
@update:expanded="isExpanded = $event"
@update:enabled="handleEnabledChange"
@action-click="handleActionClick">
<div class="material-content">
<!-- 材质类型 -->
<PropertyRow
label="材质类型"
tooltip="材质类型"
>
<Select
v-model="materialData.type"
:options="materialTypeOptions"
@change="updateMaterialProperty('type', $event)"
/>
</PropertyRow>
<!-- 基础颜色 -->
<PropertyRow
label="基础颜色"
tooltip="物体基础颜色"
>
<ColorInput
:model-value="colorToHex(materialData.albedoColor)"
@update:model-value="updateAlbedoColor"
/>
</PropertyRow>
<!-- 贴图 -->
<PropertyRow
label="基础贴图"
tooltip="颜色贴图"
>
<Field
v-model="albedoTextureValue"
accepted-type="Texture"
@change="updateAlbedoTexture"
/>
</PropertyRow>
<!-- 透明度 -->
<PropertyRow
label="透明度"
tooltip="材质透明度 (0-1)"
>
<Slider
v-model="materialData.alpha"
:min="0"
:max="1"
:step="0.01"
:show-value="true"
@update:model-value="updateMaterialProperty('alpha', $event)"
/>
</PropertyRow>
<!-- 透明模式 -->
<PropertyRow
label="透明模式"
tooltip="透明模式"
>
<Select
v-model="materialData.transparencyMode"
:options="transparencyModeOptions"
@change="updateMaterialProperty('transparencyMode', $event)"
/>
</PropertyRow>
</div>
</CollapsibleHeader>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
import Select from 'components/BasicControls/Select/index.vue'
import ColorInput from 'components/BasicControls/Input/ColorInput.vue'
import NumberInput from 'components/BasicControls/Input/index.vue'
import Slider from 'components/BasicControls/Slider/index.vue'
import Field from 'components/BasicControls/Field/index.vue'
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS, colorUtils } from '../../../composables/useInspectorPanel'
import type {
MaterialData,
Color3,
MaterialTypeOption,
TransparencyModeOption
} from './types'
import type { FieldValue } from 'components/BasicControls/Field/types'
import {contain} from "./index"
import { useInspectorPanelStore } from 'stores/InspectorPanel'
import { storeToRefs } from 'pinia'
const inspectorPanelStore = useInspectorPanelStore()
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
// 使用通用的 Inspector Panel 逻辑
const {
isExpanded,
isEnabled,
createEnabledChangeHandler,
createActionClickHandler,
createShowMoreHandler,
createPropertyUpdater
} = useInspectorPanel()
// Material 数据
const materialData = reactive<MaterialData>({ ...DEFAULT_COMPONENT_CONFIGS.material })
// 贴图字段值
const albedoTextureValue = ref<FieldValue | null>(null)
// 材质类型选项
const materialTypeOptions: MaterialTypeOption[] = [
{ label: 'PBR材质', value: 'PBRMaterial' }
]
// 透明模式选项
const transparencyModeOptions: TransparencyModeOption[] = [
{ label: '不透明', value: 0 },
{ label: '透明测试', value: 1 },
{ label: '透明混合', value: 2 },
{ label: '预乘透明混合', value: 3 }
]
const visible = ref<any>({})
const inspectorPanelData = ref<any>({})
watch(selecteInspectorData, (node: any) => {
if (!node) {
visible.value = false
return;
}
if(contain.includes(node.type)){
visible.value = true
}else{
visible.value = false
}
inspectorPanelData.value = node
})
// 更新 Material 属性
const updateMaterialProperty = createPropertyUpdater(materialData, '材质', (property: keyof MaterialData, value: any) => {
// 这里可以添加与 Babylon.js 的交互逻辑
// 例如updateSelectedMaterial(property, value)
})
// 更新基础颜色
const updateAlbedoColor = (hex: string) => {
materialData.albedoColor = colorUtils.hexToRgb(hex)
// console.log('基础颜色已更新:', materialData.albedoColor)
// 这里可以添加与 Babylon.js 的交互逻辑
}
// 更新贴图
const updateAlbedoTexture = (value: FieldValue | null) => {
materialData.albedoTexture = value?.path || null
// console.log('基础贴图已更新:', materialData.albedoTexture)
// 这里可以添加与 Babylon.js 的交互逻辑
}
// 重置 Material 设置
const resetMaterialSettings = () => {
Object.assign(materialData, DEFAULT_COMPONENT_CONFIGS.material)
albedoTextureValue.value = null
// console.log('材质设置已重置')
}
// 处理启用状态变化
const handleEnabledChange = createEnabledChangeHandler('材质', (enabled: boolean) => {
// 这里可以添加与 Babylon.js 的交互逻辑
})
// 处理操作按钮点击
const handleActionClick = createActionClickHandler(
resetMaterialSettings,
createShowMoreHandler('材质')
)
// 颜色转换为十六进制
const colorToHex = colorUtils.rgbToHex
</script>
<style scoped>
.material-content {
/* Remove redundant styling since CollapsibleHeader handles container */
}
</style>

View File

@ -0,0 +1,36 @@
// Material 组件类型定义
export type MaterialType = 'PBRMaterial'
export interface Color3 {
r: number
g: number
b: number
}
export interface MaterialData {
type: MaterialType
albedoColor: Color3
albedoTexture: string | null
alpha: number
transparencyMode: number
}
export interface MaterialProps {
// 可以添加其他 props
}
export interface MaterialEmits {
// 可以添加事件定义
}
export interface MaterialTypeOption {
label: string
value: MaterialType
}
// 透明模式选项
export interface TransparencyModeOption {
label: string
value: number
}

View File

@ -0,0 +1 @@
export const contain=["Mesh"]

View File

@ -0,0 +1,134 @@
<template>
<div class="mesh-panel" v-show="visible">
<CollapsibleHeader title="网格"
:expanded="isExpanded"
:enabled="isEnabled"
:checkable="false"
@update:expanded="isExpanded = $event"
@update:enabled="handleEnabledChange"
@action-click="handleActionClick">
<div class="mesh-properties">
<!-- 是否可被鼠标拾取 -->
<PropertyRow
label="可拾取"
tooltip="是否可被鼠标拾取"
>
<Switch
v-model="meshData.isPickable"
@change="updateMeshProperty('isPickable', $event)"
/>
</PropertyRow>
<!-- 是否参与碰撞检测 -->
<PropertyRow
label="碰撞检测"
tooltip="是否参与碰撞检测"
>
<Switch
v-model="meshData.checkCollisions"
@change="updateMeshProperty('checkCollisions', $event)"
/>
</PropertyRow>
<!-- 贴图 -->
<PropertyRow
label="材质"
tooltip="材质"
>
<Field
v-model="MaterialValue"
accepted-type="Material"
@change="updateAlbedoTexture"
/>
</PropertyRow>
</div>
</CollapsibleHeader>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
import Switch from 'components/BasicControls/Switch/index.vue'
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS } from '../../../composables/useInspectorPanel'
import type { MeshData } from './types'
import {contain} from "./index"
import { useInspectorPanelStore } from 'stores/InspectorPanel'
import { storeToRefs } from 'pinia'
import Field from 'components/BasicControls/Field/index.vue'
import type { FieldValue } from 'components/BasicControls/Field/types'
const inspectorPanelStore = useInspectorPanelStore()
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
// 使用通用的 Inspector Panel 逻辑
const {
isExpanded,
isEnabled,
createEnabledChangeHandler,
createActionClickHandler,
createShowMoreHandler,
createPropertyUpdater
} = useInspectorPanel()
// Mesh 数据
const meshData = reactive<MeshData>({ ...DEFAULT_COMPONENT_CONFIGS.mesh })
const visible = ref<any>({})
const inspectorPanelData = ref<any>({})
// 贴图字段值
const MaterialValue = ref<FieldValue | null>(null)
watch(selecteInspectorData, (node: any) => {
if (!node) {
visible.value = false
return;
}
if(contain.includes(node.type)){
visible.value = true
}else{
visible.value = false
}
inspectorPanelData.value = node
})
// 更新 Mesh 属性
const updateMeshProperty = createPropertyUpdater(meshData, '网格', (property: keyof MeshData, value: boolean) => {
// 这里可以添加与 Babylon.js 的交互逻辑
// 例如updateSelectedMesh(property, value)
})
// 重置 Mesh 设置
const resetMeshSettings = () => {
Object.assign(meshData, DEFAULT_COMPONENT_CONFIGS.mesh)
// console.log('网格设置已重置')
}
// 处理启用状态变化
const handleEnabledChange = createEnabledChangeHandler('网格', (enabled: boolean) => {
// 这里可以添加与 Babylon.js 的交互逻辑
})
// 处理操作按钮点击
const handleActionClick = createActionClickHandler(
resetMeshSettings,
createShowMoreHandler('网格')
)
const updateAlbedoTexture = (value: FieldValue | null) => {
console.log('更新基础贴图', value)
}
</script>
<style scoped>
.mesh-properties {
/* Remove redundant styling since CollapsibleHeader handles container */
}
</style>

View File

@ -0,0 +1,14 @@
// Mesh 组件类型定义
export interface MeshData {
isPickable: boolean
checkCollisions: boolean
}
export interface MeshProps {
// 可以添加其他 props
}
export interface MeshEmits {
// 可以添加事件定义
}

Some files were not shown because too many files have changed in this diff Show More