添加材质切换,调节功能
@ -1,5 +1,3 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
# 材质编辑器
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
独立实现的WebCAD材质编辑器,提供材质预览,调节和上传功能。
|
15
package.json
@ -9,18 +9,21 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13",
|
||||
"three": "npm:three-cf@^0.122.9",
|
||||
"js-angusj-clipper": "^1.2.1",
|
||||
"polylabel": "^1.1.0",
|
||||
"@jscad/modeling": "^2.11.0",
|
||||
"fflate": "^0.8.2",
|
||||
"flatbush": "^3.3.0",
|
||||
"xaop": "^2.0.0",
|
||||
"webcad_ue4_api": "http://gitea.cf/cx/webcad-ue4-api/archive/3.20.0.tar.gz"
|
||||
"js-angusj-clipper": "^1.2.1",
|
||||
"pinia": "^3.0.2",
|
||||
"polylabel": "^1.1.0",
|
||||
"three": "npm:three-cf@^0.122.9",
|
||||
"vue": "^3.5.13",
|
||||
"webcad_ue4_api": "http://gitea.cf/cx/webcad-ue4-api/archive/3.20.0.tar.gz",
|
||||
"xaop": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"csstype": "^3.1.3",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.2.0",
|
||||
"vue-tsc": "^2.2.4"
|
||||
|
110
pnpm-lock.yaml
@ -11,12 +11,18 @@ importers:
|
||||
'@jscad/modeling':
|
||||
specifier: ^2.11.0
|
||||
version: 2.12.5
|
||||
fflate:
|
||||
specifier: ^0.8.2
|
||||
version: 0.8.2
|
||||
flatbush:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.1
|
||||
js-angusj-clipper:
|
||||
specifier: ^1.2.1
|
||||
version: 1.3.1
|
||||
pinia:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
|
||||
polylabel:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
@ -39,6 +45,9 @@ importers:
|
||||
'@vue/tsconfig':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
|
||||
csstype:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
typescript:
|
||||
specifier: ~5.7.2
|
||||
version: 5.7.3
|
||||
@ -369,6 +378,15 @@ packages:
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||
|
||||
'@vue/devtools-api@7.7.2':
|
||||
resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==}
|
||||
|
||||
'@vue/devtools-kit@7.7.2':
|
||||
resolution: {integrity: sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==}
|
||||
|
||||
'@vue/devtools-shared@7.7.2':
|
||||
resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==}
|
||||
|
||||
'@vue/language-core@2.2.8':
|
||||
resolution: {integrity: sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==}
|
||||
peerDependencies:
|
||||
@ -411,9 +429,16 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
birpc@0.2.19:
|
||||
resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
|
||||
copy-anything@3.0.5:
|
||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||
engines: {node: '>=12.13'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
@ -432,6 +457,9 @@ packages:
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
flatbush@3.3.1:
|
||||
resolution: {integrity: sha512-oKuPbtT+DS2CxH+9Vhbsq8HifmSCuOw+3Cy5zt/vCIrZl5KyengoTHDBLmtpZoBhcwa7/biNjgL1DwdLMJYm1A==}
|
||||
|
||||
@ -447,6 +475,13 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
is-what@4.1.16:
|
||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||
engines: {node: '>=12.13'}
|
||||
|
||||
js-angusj-clipper@1.3.1:
|
||||
resolution: {integrity: sha512-/qru4QXxN/gBbQjL4WaFl296YSM8kh5XKpNuNqfZhJ4t4Hw3KeLc5ERj3XHAeLi6pBrqeh6o9PFZUpS3QThEEQ==}
|
||||
|
||||
@ -457,6 +492,9 @@ packages:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
@ -468,9 +506,21 @@ packages:
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
pinia@3.0.2:
|
||||
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.4.4'
|
||||
vue: ^2.7.0 || ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
polylabel@1.1.0:
|
||||
resolution: {integrity: sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==}
|
||||
|
||||
@ -478,6 +528,9 @@ packages:
|
||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rollup@4.39.0:
|
||||
resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@ -487,6 +540,14 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
superjson@2.2.2:
|
||||
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
three-cf@0.122.9:
|
||||
resolution: {integrity: sha512-y+bvPYKI0yMNGF2flNsTbqloPiMAL4OKbIbR9QaQLRjNlz15Th+69ZtirU5ZASGXF/vQIvIrv+6OkxdSjiN89Q==}
|
||||
|
||||
@ -770,6 +831,24 @@ snapshots:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
'@vue/devtools-api@7.7.2':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.2
|
||||
|
||||
'@vue/devtools-kit@7.7.2':
|
||||
dependencies:
|
||||
'@vue/devtools-shared': 7.7.2
|
||||
birpc: 0.2.19
|
||||
hookable: 5.5.3
|
||||
mitt: 3.0.1
|
||||
perfect-debounce: 1.0.0
|
||||
speakingurl: 14.0.1
|
||||
superjson: 2.2.2
|
||||
|
||||
'@vue/devtools-shared@7.7.2':
|
||||
dependencies:
|
||||
rfdc: 1.4.1
|
||||
|
||||
'@vue/language-core@2.2.8(typescript@5.7.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.12
|
||||
@ -816,10 +895,16 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
birpc@0.2.19: {}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
copy-anything@3.0.5:
|
||||
dependencies:
|
||||
is-what: 4.1.16
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
@ -856,6 +941,8 @@ snapshots:
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
flatbush@3.3.1:
|
||||
dependencies:
|
||||
flatqueue: 1.2.1
|
||||
@ -867,6 +954,10 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
is-what@4.1.16: {}
|
||||
|
||||
js-angusj-clipper@1.3.1: {}
|
||||
|
||||
magic-string@0.30.17:
|
||||
@ -877,14 +968,25 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
pinia@3.0.2(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.2
|
||||
vue: 3.5.13(typescript@5.7.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
polylabel@1.1.0:
|
||||
dependencies:
|
||||
tinyqueue: 2.0.3
|
||||
@ -895,6 +997,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rollup@4.39.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
@ -923,6 +1027,12 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
superjson@2.2.2:
|
||||
dependencies:
|
||||
copy-anything: 3.0.5
|
||||
|
||||
three-cf@0.122.9: {}
|
||||
|
||||
tinyqueue@2.0.3: {}
|
||||
|
BIN
public/back-gray.webp
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/back.webp
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
public/bottom-gray.webp
Normal file
After Width: | Height: | Size: 454 KiB |
BIN
public/bottom.webp
Normal file
After Width: | Height: | Size: 438 KiB |
BIN
public/front-gray.webp
Normal file
After Width: | Height: | Size: 152 KiB |
BIN
public/front.webp
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
public/left-gray.webp
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
public/left.webp
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
public/right-gray.webp
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
public/right.webp
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
public/texture1.png
Normal file
After Width: | Height: | Size: 406 KiB |
BIN
public/top-gray.webp
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
public/top.webp
Normal file
After Width: | Height: | Size: 8.2 KiB |
@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import MaterialView from './components/MaterialView.vue';
|
||||
|
||||
// 禁用右键菜单
|
||||
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
45
src/api/Api.ts
Normal file
@ -0,0 +1,45 @@
|
||||
function GetCurHost() {
|
||||
let searchParams = new URLSearchParams(globalThis.location?.search);
|
||||
if (searchParams.has("server"))
|
||||
return searchParams.get("server");
|
||||
else {
|
||||
let hostname = globalThis.location?.hostname;
|
||||
switch (hostname) {
|
||||
case "cfcad.cn":
|
||||
case "www.cfcad.cn":
|
||||
return "https://api.cfcad.cn";
|
||||
case "vip.cfcad.cn":
|
||||
return "https://vapi.cfcad.cn";
|
||||
case "v.cfcad.cn":
|
||||
return "https://vapi.cfcad.cn";
|
||||
case "t.cfcad.cn":
|
||||
return "https://tapi.cfcad.cn:7779";
|
||||
case "tvip.cfcad.cn":
|
||||
return "https://tvapi.cfcad.cn:7779";
|
||||
case "tv.cfcad.cn":
|
||||
return "https://tvapi.cfcad.cn:7779";
|
||||
default:
|
||||
return "https://tapi.cfcad.cn:7779";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CURRENT_HOST = GetCurHost();
|
||||
export const ImgsUrl = {
|
||||
get: CURRENT_HOST + "/CAD-imageList",
|
||||
upload: CURRENT_HOST + "/CAD-imageUpload",
|
||||
delete: CURRENT_HOST + "/CAD-imageDelete",
|
||||
logo: CURRENT_HOST + "/CAD-logoUpload",
|
||||
update: CURRENT_HOST + "/CAD-imageUpdate"
|
||||
};
|
||||
export const MaterialUrls = {
|
||||
query: CURRENT_HOST + "/CAD-materialList",
|
||||
create: CURRENT_HOST + "/CAD-materialCreate",
|
||||
get: CURRENT_HOST + "/CAD-materialList",
|
||||
detail: CURRENT_HOST + "/CAD-materialDetail",
|
||||
delete: CURRENT_HOST + "/CAD-materialDelete",
|
||||
update: CURRENT_HOST + "/CAD-materialUpdate",
|
||||
move: CURRENT_HOST + "/CAD-materialMove",
|
||||
buy: CURRENT_HOST + "/Materials-openList",
|
||||
publishDetail: CURRENT_HOST + "/CAD-materialPublicDetail",
|
||||
};
|
71
src/api/Request.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { ImgsUrl } from "./Api";
|
||||
|
||||
export enum DirectoryId
|
||||
{
|
||||
None = "",
|
||||
FileDir = "1", //图片根目录
|
||||
MaterialDir = "2", //材质根目录
|
||||
ImgDir = "3", //图片根目录
|
||||
ToplineDir = "4", //材质根目录
|
||||
TemplateDir = "5", //模板根目录
|
||||
DrillingDir = "6", //排钻目录
|
||||
KnifePathDir = "7", //刀路目录
|
||||
Frame = "8", //图框目录
|
||||
CeilingContour = "9", //吊顶轮廓
|
||||
HistoryDit = "-1",//历史编辑目录
|
||||
}
|
||||
|
||||
export enum RequestStatus
|
||||
{
|
||||
NoLogin = 88888,
|
||||
Ok = 0,
|
||||
NoPermission = 102,//没有经过授权,不能登录该账号
|
||||
DeleteWarn1 = 401,
|
||||
DeleteWarn2 = 402,
|
||||
NoBuy = 3298, //未购买cad包月服务错误码
|
||||
NoBuy1 = 3299,//包月服务未充值
|
||||
NoBuy2 = 3300,//包月服务未生效
|
||||
NoBuy3 = 3301,//包月服务已失效
|
||||
NoBuy4 = 3412, //未购买渲染包月服务
|
||||
None = -1, //未知错误
|
||||
OffLine = 44444, //踢下线
|
||||
NoToken = 6600, //酷家乐未授权
|
||||
CreateTempNoLogo = 802, //导入模板时未查询到json文件logo
|
||||
}
|
||||
export interface IResponseData
|
||||
{
|
||||
err_code: RequestStatus;
|
||||
err_msg: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export async function PostJson<T = object>(
|
||||
url: string, body: Exclude<T, BodyInit>,
|
||||
isShowErrMsg = true)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
let res = await Post(url, JSON.stringify(body), isShowErrMsg);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function Post(url: string, body?: BodyInit, isShowErrMsg = true): Promise<IResponseData>
|
||||
{
|
||||
try
|
||||
{
|
||||
let res = await fetch(url, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
body,
|
||||
});
|
||||
let result = await res.json();
|
||||
return result;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
// ReportError(`请求url错误:${url}`);
|
||||
return { err_code: RequestStatus.None, err_msg: `请求失败,地址:${url}` };
|
||||
}
|
||||
}
|
10
src/assets/main.css
Normal file
@ -0,0 +1,10 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Noto Sans SC', "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
import { Vector3 } from 'three';
|
||||
import { Viewer } from './Viewer';
|
||||
import { KeyBoard, MouseKey } from './KeyEnum';
|
||||
|
||||
|
||||
//相机控制状态
|
||||
export enum CameraControlState
|
||||
{
|
||||
Null = 0, Pan = 1, Rotate = 2, Scale = 3
|
||||
}
|
||||
|
||||
export class CameraControls
|
||||
{
|
||||
m_TouthTypeList = [CameraControlState.Rotate, CameraControlState.Scale, CameraControlState.Pan];
|
||||
m_domElement: HTMLElement;//HTMLDocument
|
||||
//起始点击
|
||||
m_StartClickPoint: THREE.Vector3 = new THREE.Vector3();
|
||||
m_EndClickPoint: THREE.Vector3 = new THREE.Vector3();
|
||||
m_DollyStart: THREE.Vector2 = new THREE.Vector2();
|
||||
m_DollyEnd: THREE.Vector2 = new THREE.Vector2();
|
||||
|
||||
m_KeyDown = new Map<KeyBoard, boolean>();
|
||||
m_MouseDown = new Map<MouseKey, boolean>();
|
||||
|
||||
//状态
|
||||
m_State: CameraControlState = CameraControlState.Null;
|
||||
m_Viewer: Viewer;
|
||||
//左键使用旋转
|
||||
m_LeftUseRotate: boolean = true;
|
||||
|
||||
constructor(viewer: Viewer)
|
||||
{
|
||||
this.m_Viewer = viewer;
|
||||
this.m_domElement = viewer.Renderer.domElement.parentElement;
|
||||
this.RegisterEvent();
|
||||
}
|
||||
|
||||
RegisterEvent()
|
||||
{
|
||||
if (this.m_domElement)
|
||||
{
|
||||
this.m_domElement.addEventListener("mousedown", this.onMouseDown, false)
|
||||
this.m_domElement.addEventListener("mousemove", this.onMouseMove, false)
|
||||
this.m_domElement.addEventListener("mouseup", this.onMouseUp, false)
|
||||
window.addEventListener("keydown", this.onKeyDown, false);
|
||||
window.addEventListener("keyup", this.onKeyUp, false);
|
||||
this.m_domElement.addEventListener('wheel', this.onMouseWheel, false);
|
||||
|
||||
this.m_domElement.addEventListener('touchstart', this.onTouchStart, false);
|
||||
this.m_domElement.addEventListener('touchend', this.onTouchEnd, false);
|
||||
this.m_domElement.addEventListener('touchmove', this.onTouchMove, false);
|
||||
|
||||
window.addEventListener("blur", this.onBlur, false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 窗体失去焦点时.
|
||||
* @memberof CameraControls
|
||||
*/
|
||||
onBlur = () =>
|
||||
{
|
||||
this.m_KeyDown.clear();
|
||||
this.m_MouseDown.clear();
|
||||
}
|
||||
|
||||
//触屏开始事件
|
||||
onTouchStart = (event: TouchEvent) =>
|
||||
{
|
||||
this.m_Viewer.UpdateLockTarget();
|
||||
this.m_StartClickPoint.set(event.touches[0].pageX, event.touches[0].pageY, 0);
|
||||
if (event.touches.length < 4)
|
||||
{
|
||||
if (event.touches.length == 2)
|
||||
{
|
||||
var dx = event.touches[0].pageX - event.touches[1].pageX;
|
||||
var dy = event.touches[0].pageY - event.touches[1].pageY;
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
this.m_DollyStart.set(0, distance);
|
||||
}
|
||||
this.m_State = this.m_TouthTypeList[event.touches.length - 1];
|
||||
}
|
||||
}
|
||||
onTouchEnd = (event: TouchEvent) =>
|
||||
{
|
||||
this.m_State = CameraControlState.Null;
|
||||
}
|
||||
onTouchMove = (event: TouchEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.m_EndClickPoint.set(event.touches[0].pageX, event.touches[0].pageY, 0);
|
||||
|
||||
let vec = this.m_EndClickPoint.clone().sub(this.m_StartClickPoint);
|
||||
switch (this.m_State)
|
||||
{
|
||||
case CameraControlState.Pan:
|
||||
{
|
||||
this.m_Viewer.Pan(vec);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Scale:
|
||||
{
|
||||
var dx = event.touches[0].pageX - event.touches[1].pageX;
|
||||
var dy = event.touches[0].pageY - event.touches[1].pageY;
|
||||
|
||||
var distance = Math.sqrt(dx * dx + dy * dy);
|
||||
this.m_DollyEnd.set(0, distance);
|
||||
if (distance > this.m_DollyStart.y)
|
||||
{
|
||||
this.m_Viewer.Zoom(0.95);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.m_Viewer.Zoom(1.05)
|
||||
}
|
||||
this.m_DollyStart.copy(this.m_DollyEnd);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Rotate:
|
||||
{
|
||||
this.m_Viewer.Rotate(vec.multiplyScalar(2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.m_StartClickPoint.copy(this.m_EndClickPoint);
|
||||
this.m_Viewer.UpdateRender();
|
||||
}
|
||||
beginRotate()
|
||||
{
|
||||
this.m_State = CameraControlState.Rotate;
|
||||
this.m_Viewer.UpdateLockTarget();
|
||||
}
|
||||
|
||||
//最后一次按中键的时间
|
||||
lastMiddleClickTime = 0;
|
||||
//鼠标
|
||||
onMouseDown = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
let key: MouseKey = event.button;
|
||||
this.m_MouseDown.set(key, true);
|
||||
this.m_StartClickPoint.set(event.offsetX, event.offsetY, 0);
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case MouseKey.Left:
|
||||
{
|
||||
if (this.m_LeftUseRotate)
|
||||
{
|
||||
this.beginRotate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseKey.Middle:
|
||||
{
|
||||
let curTime = Date.now();
|
||||
let t = curTime - this.lastMiddleClickTime;
|
||||
this.lastMiddleClickTime = curTime;
|
||||
if (t < 350)
|
||||
{
|
||||
this.m_Viewer.ZoomAll();
|
||||
return;
|
||||
}
|
||||
if (this.m_KeyDown.get(KeyBoard.Control))
|
||||
{
|
||||
this.beginRotate();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.m_State = CameraControlState.Pan;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MouseKey.Right:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onMouseUp = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
this.m_State = CameraControlState.Null;
|
||||
this.m_MouseDown.set(event.button, false);
|
||||
}
|
||||
onMouseMove = (event: MouseEvent) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
this.m_EndClickPoint.set(event.offsetX, event.offsetY, 0);
|
||||
let changeVec = this.m_EndClickPoint.clone().sub(this.m_StartClickPoint);
|
||||
this.m_StartClickPoint.copy(this.m_EndClickPoint);
|
||||
if (
|
||||
(this.m_LeftUseRotate ||
|
||||
(this.m_KeyDown.get(KeyBoard.Control))
|
||||
)
|
||||
&& this.m_State == CameraControlState.Rotate
|
||||
)
|
||||
{
|
||||
this.m_Viewer.Rotate(changeVec);
|
||||
}
|
||||
switch (this.m_State)
|
||||
{
|
||||
case CameraControlState.Pan:
|
||||
{
|
||||
this.m_Viewer.Pan(changeVec);
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Rotate:
|
||||
{
|
||||
break;
|
||||
}
|
||||
case CameraControlState.Scale:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 鼠标滚轮事件
|
||||
*
|
||||
* @memberof CameraControls
|
||||
*/
|
||||
onMouseWheel = (event: WheelEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
let pt = new THREE.Vector3(event.offsetX, event.offsetY, 0);
|
||||
|
||||
this.m_Viewer.ScreenToWorld(pt, new Vector3().setFromMatrixColumn(this.m_Viewer.m_Camera.Camera.matrixWorld, 2));
|
||||
if (event.deltaY < 0)
|
||||
{
|
||||
this.m_Viewer.Zoom(0.6, pt);
|
||||
}
|
||||
else if (event.deltaY > 0)
|
||||
{
|
||||
this.m_Viewer.Zoom(1.4, pt);
|
||||
}
|
||||
}
|
||||
//按键
|
||||
onKeyDown = (event: KeyboardEvent) =>
|
||||
{
|
||||
this.m_KeyDown.set(event.keyCode, true);
|
||||
}
|
||||
onKeyUp = (event: KeyboardEvent) =>
|
||||
{
|
||||
this.m_KeyDown.set(event.keyCode, false);
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
|
||||
import { AmbientLight, BoxBufferGeometry, BufferGeometry, ConeBufferGeometry, CubeRefractionMapping, CubeTextureLoader, Geometry, Mesh, MeshPhysicalMaterial, Object3D, SphereBufferGeometry, sRGBEncoding, Texture, TorusBufferGeometry, TorusKnotBufferGeometry } from 'three';
|
||||
import { AmbientLight, BoxBufferGeometry, BufferGeometry, Color, ConeBufferGeometry, CubeRefractionMapping, CubeTextureLoader, FrontSide, Geometry, LinearEncoding, Mesh, MeshPhysicalMaterial, Object3D, SphereBufferGeometry, sRGBEncoding, Texture, TextureLoader, TorusBufferGeometry, TorusKnotBufferGeometry, Vector3 } from 'three';
|
||||
import { Singleton } from './Singleton';
|
||||
import { Viewer } from './Viewer';
|
||||
import { PMREMGenerator3 } from './PMREMGenerator2';
|
||||
import type { PhysicalMaterialRecord, TextureTableRecord } from 'webcad_ue4_api';
|
||||
import { MaterialEditorCamerControl } from './MaterialMouseControl';
|
||||
import { ref } from 'vue';
|
||||
import { MaterialEditorCameraControl } from './MaterialMouseControl';
|
||||
|
||||
async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
||||
const texture = textureRecord['texture'] as Texture;
|
||||
@ -29,9 +28,11 @@ async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
||||
*/
|
||||
export class MaterialEditor extends Singleton
|
||||
{
|
||||
private _pointerLocked = false;
|
||||
|
||||
Geometrys: Map<string, Geometry | BufferGeometry>;
|
||||
|
||||
CurGeometryName = ref("球");
|
||||
CurGeometryName = "球";
|
||||
Canvas: HTMLElement;
|
||||
ShowObject: Object3D;
|
||||
ShowMesh: Mesh;
|
||||
@ -68,14 +69,21 @@ export class MaterialEditor extends Singleton
|
||||
{
|
||||
this.Viewer = new Viewer(this.Canvas);
|
||||
// this.Viewer.PreViewer.Cursor.CursorObject.visible = false;
|
||||
// this.Viewer.CameraCtrl.CameraType = CameraType.PerspectiveCamera;
|
||||
// this.Viewer.UsePass = false;
|
||||
this.initScene();
|
||||
new MaterialEditorCamerControl(this.Viewer);
|
||||
new MaterialEditorCameraControl(this.Viewer, this.ShowObject.position);
|
||||
this.Viewer.ZoomAll();
|
||||
|
||||
// 初始化相机位置到观察物体的正后方
|
||||
// CameraUpdate中的Rotate参数类型并非标准的欧拉角。。。
|
||||
this.Viewer.RotateAround(new Vector3(0, -90 * 8, 0), this.ShowObject.position);
|
||||
this.Viewer.Pan(new Vector3(0, 0, -1500));
|
||||
this.Viewer.UpdateRender();
|
||||
this.Viewer.Fov = 90;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Canvas.appendChild(this.Viewer.Renderer.domElement);
|
||||
this.Canvas.appendChild(document);
|
||||
}
|
||||
|
||||
}
|
||||
@ -83,28 +91,18 @@ export class MaterialEditor extends Singleton
|
||||
{
|
||||
this.Canvas = canvas;
|
||||
this.initViewer();
|
||||
this.CurGeometryName.value = "球";
|
||||
this.CurGeometryName = "球";
|
||||
}
|
||||
initScene()
|
||||
{
|
||||
let scene = this.Viewer.Scene;
|
||||
this.ShowObject = new Object3D();
|
||||
let geo = this.Geometrys.get(this.CurGeometryName.value);
|
||||
let geo = this.Geometrys.get(this.CurGeometryName);
|
||||
this.ShowMesh = new Mesh(geo, this._MeshMaterial);
|
||||
this.ShowMesh.scale.set(1000, 1000, 1000);
|
||||
|
||||
this.ShowObject.add(this.ShowMesh);
|
||||
scene.add(this.ShowObject);
|
||||
// let remove = autorun(() =>
|
||||
// {
|
||||
// let geo = this.Geometrys.get(this.CurGeometryName.get());
|
||||
// if (geo)
|
||||
// {
|
||||
// this.ShowMesh.geometry = geo;
|
||||
// this.Viewer.UpdateRender();
|
||||
// }
|
||||
// });
|
||||
// end(this as MaterialEditor, this.dispose, remove);
|
||||
|
||||
//环境光
|
||||
let ambient = new AmbientLight();
|
||||
@ -119,14 +117,15 @@ export class MaterialEditor extends Singleton
|
||||
if (this.metaTexture) return this.metaTexture;
|
||||
if (this.metaPromise) return this.metaPromise;
|
||||
|
||||
return new Promise((res, rej) =>
|
||||
return new Promise(async (res, rej) =>
|
||||
{
|
||||
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
|
||||
new CubeTextureLoader().setPath('https://cdn.cfcad.cn/t/house/')
|
||||
new CubeTextureLoader().setPath('./')
|
||||
.load(urls, (t) =>
|
||||
{
|
||||
t.encoding = sRGBEncoding;
|
||||
t.mapping = CubeRefractionMapping;
|
||||
t.encoding = LinearEncoding;
|
||||
|
||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||
let ldrCubeRenderTarget = pmremGenerator.fromCubemap(t);
|
||||
@ -135,8 +134,6 @@ export class MaterialEditor extends Singleton
|
||||
res(this.metaTexture);
|
||||
|
||||
this.Viewer.Scene.background = this.metaTexture;
|
||||
// this.Viewer.Scene.background = new Color(255,0,0);
|
||||
|
||||
this.Viewer.UpdateRender();
|
||||
});
|
||||
});
|
||||
@ -151,12 +148,14 @@ export class MaterialEditor extends Singleton
|
||||
|
||||
this.exrPromise = new Promise<Texture>((res, rej) =>
|
||||
{
|
||||
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
|
||||
new CubeTextureLoader().setPath('https://cdn.cfcad.cn/t/house_gray/')
|
||||
let urls = ['right-gray.webp', 'left-gray.webp', 'top-gray.webp', 'bottom-gray.webp', 'front-gray.webp', 'back-gray.webp'];
|
||||
new CubeTextureLoader().setPath('./')
|
||||
.load(urls, (t) =>
|
||||
{
|
||||
t.encoding = sRGBEncoding;
|
||||
t.mapping = CubeRefractionMapping;
|
||||
t.encoding = LinearEncoding;
|
||||
|
||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||
let target = pmremGenerator.fromCubemap(t);
|
||||
this.exrTexture = target.texture;
|
||||
@ -198,21 +197,6 @@ export class MaterialEditor extends Singleton
|
||||
async Update()
|
||||
{
|
||||
let mat = this.ShowMesh.material as MeshPhysicalMaterial;
|
||||
if (this.Material.map && this.Material.map.Object && this.Material.useMap)
|
||||
{
|
||||
let texture = this.Material.map.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
if (this.Material.bumpMap && this.Material.bumpMap.Object)
|
||||
{
|
||||
let texture = this.Material.bumpMap.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
if (this.Material.roughnessMap && this.Material.roughnessMap.Object)
|
||||
{
|
||||
let texture = this.Material.roughnessMap.Object as TextureTableRecord;
|
||||
await textureRenderUpdate(texture);
|
||||
}
|
||||
mat.needsUpdate = true;
|
||||
|
||||
this._MeshMaterial.copy(this.Material.Material);
|
||||
|
@ -4,24 +4,25 @@ import type { Viewer } from './Viewer';
|
||||
/**
|
||||
* 材质编辑器的场景鼠标控制.
|
||||
*/
|
||||
export class MaterialEditorCamerControl
|
||||
{
|
||||
export class MaterialEditorCameraControl {
|
||||
private Viewer: Viewer;
|
||||
|
||||
//State.
|
||||
private _movement: Vector3 = new Vector3();
|
||||
private _MouseIsDown: boolean = false;
|
||||
private _StartPoint: Vector3 = new Vector3();
|
||||
private _EndPoint = new Vector3();
|
||||
private pointId: number;
|
||||
|
||||
constructor(view: Viewer)
|
||||
{
|
||||
this.Viewer = view;
|
||||
private _target: Vector3 | null = null;
|
||||
|
||||
get Target() { return this._target; }
|
||||
set Target(val: Vector3 | null) { this._target = val; }
|
||||
|
||||
constructor(view: Viewer, target: Vector3 | null = null) {
|
||||
this.Viewer = view;
|
||||
this.Target = target;
|
||||
this.initMouseControl();
|
||||
}
|
||||
initMouseControl()
|
||||
{
|
||||
|
||||
initMouseControl() {
|
||||
let el = this.Viewer.Renderer.domElement;
|
||||
el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false);
|
||||
el.addEventListener("mousedown", this.onMouseDown, false);
|
||||
@ -29,28 +30,23 @@ export class MaterialEditorCamerControl
|
||||
el.addEventListener("mouseup", this.onMouseUp, false);
|
||||
el.addEventListener('wheel', this.onMouseWheel, false);
|
||||
}
|
||||
onMouseDown = (event: MouseEvent) =>
|
||||
{
|
||||
onMouseDown = (event: MouseEvent) => {
|
||||
this.requestPointerLock();
|
||||
this._MouseIsDown = true;
|
||||
this._StartPoint.set(event.offsetX, event.offsetY, 0);
|
||||
};
|
||||
onMouseUp = (event: MouseEvent) =>
|
||||
{
|
||||
onMouseUp = (event: MouseEvent) => {
|
||||
this._MouseIsDown = false;
|
||||
this.exitPointerLock();
|
||||
};
|
||||
onMouseMove = (event: MouseEvent) =>
|
||||
{
|
||||
onMouseMove = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if (this._MouseIsDown)
|
||||
{
|
||||
this._EndPoint.set(event.offsetX, event.offsetY, 0);
|
||||
let changeVec: Vector3 = new Vector3();
|
||||
changeVec.subVectors(this._EndPoint, this._StartPoint);
|
||||
this._StartPoint.copy(this._EndPoint);
|
||||
if (this._MouseIsDown) {
|
||||
|
||||
this.Viewer.Rotate(changeVec);
|
||||
this._movement.set(event.movementX, event.movementY, 0);
|
||||
if (this.Target) {
|
||||
this.Viewer.RotateAround(this._movement, this.Target);
|
||||
}
|
||||
else this.Viewer.Rotate(this._movement);
|
||||
}
|
||||
};
|
||||
onMouseWheel = (event: WheelEvent) =>
|
||||
@ -65,21 +61,25 @@ export class MaterialEditorCamerControl
|
||||
}
|
||||
};
|
||||
|
||||
requestPointerLock()
|
||||
{
|
||||
if (this.Viewer.Renderer.domElement.setPointerCapture)
|
||||
this.Viewer.Renderer.domElement.setPointerCapture(this.pointId);
|
||||
requestPointerLock() {
|
||||
const dom = this.Viewer.Renderer.domElement;
|
||||
if (dom.setPointerCapture)
|
||||
dom.setPointerCapture(this.pointId);
|
||||
|
||||
if (dom.requestPointerLock)
|
||||
dom.requestPointerLock();
|
||||
}
|
||||
|
||||
exitPointerLock()
|
||||
{
|
||||
if (this.Viewer.Renderer.domElement.releasePointerCapture && this.pointId !== undefined)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Viewer.Renderer.domElement.releasePointerCapture(this.pointId);
|
||||
exitPointerLock() {
|
||||
const dom = this.Viewer.Renderer.domElement;
|
||||
if (dom.releasePointerCapture && this.pointId !== undefined) {
|
||||
try {
|
||||
dom.releasePointerCapture(this.pointId);
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
|
||||
if (document.exitPointerLock)
|
||||
document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
94
src/common/MaterialRenderer.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { AmbientLight, BoxBufferGeometry, Camera, Material, Mesh, MeshPhysicalMaterial, OrthographicCamera, PointLight, Scene, Sprite, SpriteMaterial, WebGLRenderer } from 'three';
|
||||
|
||||
export class MaterialRenderer
|
||||
{
|
||||
sprite: Sprite;
|
||||
sphere: Mesh;
|
||||
scene: Scene;
|
||||
camera: Camera;
|
||||
canvas: HTMLCanvasElement;
|
||||
renderer: WebGLRenderer;
|
||||
constructor()
|
||||
{
|
||||
//Renderer
|
||||
this.renderer = new WebGLRenderer({ alpha: true, antialias: true });
|
||||
this.renderer.setSize(300, 300);
|
||||
|
||||
//Canvas
|
||||
this.canvas = this.renderer.domElement;
|
||||
|
||||
//Camera
|
||||
this.camera = new OrthographicCamera(-1, 1, 1, -1, -1, 1);
|
||||
|
||||
//Scene
|
||||
this.scene = new Scene();
|
||||
|
||||
//Sphere
|
||||
this.sphere = new Mesh(new BoxBufferGeometry(2, 2, 2));
|
||||
this.scene.add(this.sphere);
|
||||
|
||||
//Sprite
|
||||
this.sprite = new Sprite();
|
||||
this.sprite.scale.set(2, 2, 1);
|
||||
this.scene.add(this.sprite);
|
||||
|
||||
//Ambient light
|
||||
var ambient = new AmbientLight();
|
||||
this.scene.add(ambient);
|
||||
|
||||
//Pontual light
|
||||
var point = new PointLight();
|
||||
point.position.set(-0.5, 1, 1.5);
|
||||
this.scene.add(point);
|
||||
}
|
||||
|
||||
//Set render size
|
||||
setSize(x: number, y: number)
|
||||
{
|
||||
this.renderer.setSize(x, y);
|
||||
}
|
||||
|
||||
update(material: Material)
|
||||
{
|
||||
//Set material
|
||||
if (material instanceof SpriteMaterial)
|
||||
{
|
||||
this.sphere.visible = false;
|
||||
this.sprite.visible = true;
|
||||
|
||||
this.sprite.material = material;
|
||||
this.camera.position.set(0, 0, 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.sprite.visible = false;
|
||||
this.sphere.visible = true;
|
||||
|
||||
this.sphere.material = material as MeshPhysicalMaterial;
|
||||
this.camera.position.set(0, 0, 1.5);
|
||||
}
|
||||
|
||||
this.camera.updateMatrix();
|
||||
|
||||
//Render
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
getBlob(material: Material): Promise<Blob>
|
||||
{
|
||||
this.update(material);
|
||||
return new Promise(res =>
|
||||
{
|
||||
this.canvas.toBlob(b => res(b));
|
||||
});
|
||||
}
|
||||
|
||||
render(material: Material)
|
||||
{
|
||||
this.update(material);
|
||||
|
||||
return this.canvas.toDataURL();
|
||||
};
|
||||
}
|
||||
|
||||
export const materialRenderer = new MaterialRenderer();
|
22
src/common/MaterialSerializer.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { CADFiler, Database, DuplicateRecordCloning, PhysicalMaterialRecord } from "webcad_ue4_api";
|
||||
|
||||
export function MaterialOut(material: PhysicalMaterialRecord): string
|
||||
{
|
||||
let db = new Database();
|
||||
debugger;
|
||||
db.WblockCloneObejcts(
|
||||
[material],
|
||||
db.MaterialTable,
|
||||
new Map(),
|
||||
DuplicateRecordCloning.Ignore
|
||||
);
|
||||
return JSON.stringify(db.FileWrite().Data);
|
||||
}
|
||||
|
||||
export function MaterialIn(fileData: Object[]): PhysicalMaterialRecord
|
||||
{
|
||||
let f = new CADFiler(fileData);
|
||||
let db = new Database().FileRead(f);
|
||||
db.hm.Enable = false;
|
||||
return db.MaterialTable.Symbols.entries().next().value[1];
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
import { Raycaster, Scene, Vector3, WebGLRenderer, type WebGLRendererParameters } from "three";
|
||||
import { Raycaster, Scene, Vector3, WebGLRenderer, type Vector, type WebGLRendererParameters } from "three";
|
||||
import { cZeroVec, GetBox, GetBoxArr } from "./GeUtils";
|
||||
import { PlaneExt } from "./PlaneExt";
|
||||
import { CameraUpdate } from "webcad_ue4_api";
|
||||
import { CameraControls } from "./CameraControls";
|
||||
import { CameraType, CameraUpdate } from "webcad_ue4_api";
|
||||
|
||||
export class Viewer {
|
||||
m_LookTarget: any;
|
||||
m_Camera: CameraUpdate = new CameraUpdate();
|
||||
CameraCtrl: CameraControls;
|
||||
protected NeedUpdate: boolean = true;
|
||||
Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型
|
||||
m_DomEl: HTMLElement; //画布容器
|
||||
@ -21,6 +19,15 @@ export class Viewer {
|
||||
return this._Scene;
|
||||
}
|
||||
|
||||
get Fov() {
|
||||
return this.m_Camera.Fov;
|
||||
}
|
||||
|
||||
set Fov(val: number) {
|
||||
this.m_Camera.Fov = val;
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
|
||||
UpdateRender()
|
||||
{
|
||||
this.NeedUpdate = true;
|
||||
@ -37,10 +44,10 @@ export class Viewer {
|
||||
this.initRender(canvasContainer);
|
||||
this.OnSize();
|
||||
this.StartRender();
|
||||
this.CameraCtrl = new CameraControls(this);
|
||||
window.addEventListener("resize", () => {
|
||||
this.OnSize();
|
||||
});
|
||||
this.InitCamera();
|
||||
}
|
||||
|
||||
//初始化render
|
||||
@ -73,6 +80,11 @@ export class Viewer {
|
||||
this.OnSize();
|
||||
}
|
||||
|
||||
InitCamera() {
|
||||
this.m_Camera.CameraType = CameraType.PerspectiveCamera;
|
||||
this.m_Camera.Near = 0.001;
|
||||
this.m_Camera.Camera.far = 1000000000;
|
||||
}
|
||||
OnSize = (width?: number, height?: number) => {
|
||||
this._Width = width ? width : this.m_DomEl.clientWidth;
|
||||
this._Height = height ? height : this.m_DomEl.clientHeight;
|
||||
@ -91,6 +103,7 @@ export class Viewer {
|
||||
requestAnimationFrame(this.StartRender);
|
||||
if (this._Scene != null && this.NeedUpdate) {
|
||||
this.Render();
|
||||
this.m_Camera.Update();
|
||||
this.NeedUpdate = false;
|
||||
}
|
||||
};
|
||||
@ -139,6 +152,10 @@ export class Viewer {
|
||||
this.m_Camera.Rotate(mouseMove, this.m_LookTarget);
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
RotateAround(movement: Vector3, center: Vector3) {
|
||||
this.m_Camera.Rotate(movement, center);
|
||||
this.NeedUpdate = true;
|
||||
}
|
||||
Pan(mouseMove: Vector3) {
|
||||
this.m_Camera.Pan(mouseMove);
|
||||
this.NeedUpdate = true;
|
||||
|
42
src/components/CfFlex.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="cf-flex" :style="{
|
||||
alignItems: props.align,
|
||||
justifyContent: props.justify,
|
||||
flexDirection: flexDirection,
|
||||
flexWrap: props.wrap ? 'wrap' : 'nowrap',
|
||||
gap: props.gap
|
||||
}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { type Property } from 'csstype';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
align?: Property.AlignItems;
|
||||
justify?: Property.JustifyContent;
|
||||
vertical?: boolean;
|
||||
wrap?: boolean;
|
||||
gap?: string;
|
||||
reverse?: boolean;
|
||||
}>(), {
|
||||
justify: 'normal',
|
||||
vertical: false,
|
||||
wrap: true
|
||||
});
|
||||
|
||||
const flexDirection = computed(() =>
|
||||
props.vertical ?
|
||||
props.reverse ? 'column-reverse' : 'column'
|
||||
: props.reverse ? 'row-reverse' : 'row')
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cf-flex
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
293
src/components/MaterialAdjuster.vue
Normal file
@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div vertical class="material-adjuster">
|
||||
<div class="adjust-section">
|
||||
<h3>模型预览</h3>
|
||||
<label>选择模型样式</label>
|
||||
<select v-model="CurrGeometry">
|
||||
<option v-for="name in Geometries" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="adjust-section" v-if="debugMode">
|
||||
<h3>纹理选择(DEBUG)</h3>
|
||||
<label>纹理链接</label>
|
||||
<input v-model.trim="textureLink" type="text" placeholder="在此键入纹理图像的URL..." />
|
||||
<button class="btn-primary" @click="scene.ChangeTextureAsync(textureLink)"
|
||||
style="margin-left: 1em;">应用</button>
|
||||
</div>
|
||||
<div class="adjust-section">
|
||||
<h3>材质调节</h3>
|
||||
|
||||
<label>金属度</label>
|
||||
<CfFlex gap="1em">
|
||||
<input v-model="model.metallic" type="range" min="0" max="1" step="0.01" />
|
||||
<span>{{ model.metallic }}</span>
|
||||
</CfFlex>
|
||||
|
||||
<label>粗糙度</label>
|
||||
<CfFlex gap="1em">
|
||||
<input v-model="model.roughness" type="range" min="0" max="1" step="0.01" />
|
||||
<span>{{ model.roughness }}</span>
|
||||
</CfFlex>
|
||||
|
||||
<label>法线强度</label>
|
||||
<CfFlex gap="1em">
|
||||
<input v-model="model.normalScale" type="range" min="0" max="1" step="0.01" />
|
||||
<span>{{ model.normalScale }}</span>
|
||||
</CfFlex>
|
||||
|
||||
<label>高光</label>
|
||||
<CfFlex gap="1em">
|
||||
<input v-model="model.emissiveIntensity" type="range" min="0" max="1" step="0.01" />
|
||||
<span>{{ model.emissiveIntensity }}</span>
|
||||
</CfFlex>
|
||||
</div>
|
||||
|
||||
<div class="adjust-section">
|
||||
<h3>纹理调节</h3>
|
||||
<label>启用纹理</label>
|
||||
<input type="checkbox" v-model="enableTexture" />
|
||||
|
||||
<label>平铺U</label>
|
||||
<select v-model="textureAdjustment.wrapS">
|
||||
<option value="0">镜像平铺</option>
|
||||
<option value="1">平铺</option>
|
||||
<option value="2">展开</option>
|
||||
</select>
|
||||
|
||||
<label>平铺V</label>
|
||||
<select v-model="textureAdjustment.wrapT">
|
||||
<option value="0">镜像平铺</option>
|
||||
<option value="1">平铺</option>
|
||||
<option value="2">展开</option>
|
||||
</select>
|
||||
|
||||
<label>旋转</label>
|
||||
<input v-model="textureAdjustment.rotation" type="number" step="0.01" min="0" max="359" />
|
||||
|
||||
<div>
|
||||
<label>偏移</label>
|
||||
<span style="margin-right: 0.5em;">X</span>
|
||||
<input v-model="textureAdjustment.moveX" type="number" step="0.01" style="max-width: 60px;" />
|
||||
 
|
||||
 
|
||||
<span style="margin-right: 0.5em;">Y</span>
|
||||
<input v-model="textureAdjustment.moveY" type="number" step="0.01" style="max-width: 60px;" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>尺寸</label>
|
||||
<span style="margin-right: 0.5em;">长</span>
|
||||
<input v-model="textureAdjustment.repeatX" type="number" step="0.01" style="max-width: 60px;" />
|
||||
 
|
||||
 
|
||||
<span style="margin-right: 0.5em;">宽</span>
|
||||
<input v-model="textureAdjustment.repeatY" type="number" step="0.01" style="max-width: 60px;" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="adjust-section">
|
||||
<h3>操作</h3>
|
||||
<fieldset v-if="debugMode" style="margin: 1em 0;">
|
||||
<legend>DEBUG</legend>
|
||||
<label>上传路径ID</label>
|
||||
<input v-model="materialRequest.dirId" type="text" placeholder="材质路径ID" />
|
||||
</fieldset>
|
||||
<label>材质名</label>
|
||||
<input v-model="materialRequest.materialName" type="text" placeholder="材质名" />
|
||||
|
||||
<CfFlex gap="1em">
|
||||
<button class="btn-success" style="min-width: 110px;" @click="HandleUpload">上传</button>
|
||||
<button class="btn-danger" style="min-width: 110px;" @click="HandleCancel">取消</button>
|
||||
</CfFlex>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ref, reactive, watch } from "vue"
|
||||
import { useScene, type TextureAdjustment } from "../stores/sceneStore";
|
||||
import { storeToRefs } from "pinia";
|
||||
import CfFlex from "./CfFlex.vue";
|
||||
import { DirectoryId } from "../api/Request";
|
||||
import { IsNullOrWhitespace } from "../helpers/helper.string";
|
||||
|
||||
const scene = useScene();
|
||||
|
||||
const debugMode = ref(true);
|
||||
const { CurrGeometry, Geometries, Material } = storeToRefs(scene);
|
||||
const enableTexture = ref(Material.value.useMap);
|
||||
const textureLink = ref('');
|
||||
const textureAdjustment = reactive<TextureAdjustment>({
|
||||
wrapS: 0,
|
||||
wrapT: 0,
|
||||
rotation: 0,
|
||||
repeatX: 1,
|
||||
repeatY: 1,
|
||||
moveX: 0,
|
||||
moveY: 0
|
||||
});
|
||||
const uploading = ref(false);
|
||||
const model = reactive({
|
||||
metallic: Material.value.matalness,
|
||||
roughness: Material.value.roughness,
|
||||
normalScale: Material.value.bumpScale,
|
||||
emissiveIntensity: Material.value.specular
|
||||
});
|
||||
const materialRequest = reactive({
|
||||
dirId: DirectoryId.MaterialDir, // 正常来说是2
|
||||
materialName: ''
|
||||
});
|
||||
|
||||
watch(model, async (val) => {
|
||||
await UpdateMaterial();
|
||||
});
|
||||
|
||||
watch(enableTexture, async (val) => {
|
||||
await EnableTexture(val);
|
||||
});
|
||||
|
||||
watch(textureAdjustment, async (val) => {
|
||||
UpdateTexture();
|
||||
});
|
||||
|
||||
async function UpdateMaterial() {
|
||||
Material.value.matalness = model.metallic;
|
||||
Material.value.roughness = model.roughness;
|
||||
Material.value.bumpScale = model.normalScale;
|
||||
Material.value.specular = model.emissiveIntensity;
|
||||
// Material.value.side = DoubleSide;
|
||||
await scene.UpdateMaterialAsync();
|
||||
}
|
||||
|
||||
function UpdateTexture() {
|
||||
scene.UpdateTexture(textureAdjustment);
|
||||
}
|
||||
|
||||
async function EnableTexture(enable: boolean) {
|
||||
Material.value.useMap = enable;
|
||||
await scene.UpdateMaterialAsync();
|
||||
}
|
||||
|
||||
async function HandleUpload() {
|
||||
try {
|
||||
if (IsNullOrWhitespace(materialRequest.materialName)) {
|
||||
alert('材质名称不可为空');
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
await scene.UploadMaterialAsync(materialRequest);
|
||||
uploading.value = false;
|
||||
alert("上传成功");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("上传材质出错,请检查网络连接或联系管理员");
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleCancel() {
|
||||
// TODO: 触发取消事件
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.material-adjuster
|
||||
{
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.adjust-section
|
||||
{
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
h3
|
||||
{
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
label
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
select
|
||||
{
|
||||
width: 220px;
|
||||
padding: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
input[type="range"]
|
||||
{
|
||||
width: 220px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
input[type="text"]
|
||||
{
|
||||
width: 220px;
|
||||
padding: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
input[type="number"]
|
||||
{
|
||||
width: 220px;
|
||||
padding: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.btn-success
|
||||
{
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-success:hover
|
||||
{
|
||||
background-color: #3e8e41;
|
||||
}
|
||||
|
||||
.btn-danger
|
||||
{
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-danger:hover
|
||||
{
|
||||
background-color: #da190b;
|
||||
}
|
||||
|
||||
.btn-primary
|
||||
{
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover
|
||||
{
|
||||
background-color: #0a7ed2;
|
||||
}
|
||||
</style>
|
@ -1,41 +1,32 @@
|
||||
<template>
|
||||
<div ref="container" style="width: 800px;height: 800px;"></div>
|
||||
{{ CurGeometryName }}
|
||||
<div v-for="geo in geometries">
|
||||
<button @click="changeGeometry(geo[0])">{{ geo[0] }}</button>
|
||||
</div>
|
||||
<CfFlex class="material-view">
|
||||
<div ref="container" style="width: 100%; height: 100%; flex: 3; box-sizing: border-box;" />
|
||||
<MaterialAdjuster style="flex: 1;overflow-y: auto; width: 100%; height: 100%; box-sizing: border-box;" />
|
||||
</CfFlex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { MaterialEditor } from '../common/MaterialEditor';
|
||||
import { PhysicalMaterialRecord } from 'webcad_ue4_api';
|
||||
import MaterialAdjuster from './MaterialAdjuster.vue';
|
||||
import { useScene } from '../stores/sceneStore';
|
||||
import CfFlex from './CfFlex.vue';
|
||||
|
||||
const scene = useScene();
|
||||
const container = useTemplateRef<HTMLElement>('container');
|
||||
|
||||
|
||||
let editor:MaterialEditor = MaterialEditor.GetInstance();
|
||||
const geometries = editor.Geometrys;
|
||||
const material = new PhysicalMaterialRecord();
|
||||
|
||||
const CurGeometryName = editor.CurGeometryName;
|
||||
onMounted(() => {
|
||||
editor.SetViewer(container.value);
|
||||
editor.setMaterial(material);
|
||||
|
||||
const view = editor.Viewer;
|
||||
view.OnSize(800, 800);
|
||||
view.ZoomAll();
|
||||
view.Zoom(2.1);
|
||||
})
|
||||
|
||||
function changeGeometry(geoName:string) {
|
||||
CurGeometryName.value = geoName;
|
||||
let geo = editor.Geometrys.get(CurGeometryName.value);
|
||||
if (geo) {
|
||||
editor.ShowMesh.geometry = geo;
|
||||
editor.Viewer.UpdateRender();
|
||||
}
|
||||
}
|
||||
|
||||
scene.Initial(container.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.material-view
|
||||
{
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
3
src/helpers/MathHelper.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
|
||||
|
||||
export default { clamp };
|
155
src/helpers/helper.compression.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import * as fflate from 'fflate'
|
||||
|
||||
/**
|
||||
* 使用deflate算法压缩文件
|
||||
* @param data
|
||||
*/
|
||||
export function Deflate(data: Uint8Array | string): Uint8Array {
|
||||
if (typeof data === 'string')
|
||||
data = StrToU8(data);
|
||||
return fflate.deflateSync(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步地使用deflate算法压缩文件
|
||||
* @param data
|
||||
*/
|
||||
export function DeflateAsync(data: Uint8Array | string): Promise<Uint8Array> {
|
||||
if (typeof data === 'string')
|
||||
data = StrToU8(data);
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const termiante = fflate.deflate(data as Uint8Array, (err, result) => {
|
||||
if (err)
|
||||
reject(err.message);
|
||||
else
|
||||
resolve(result);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用Zlib压缩文件
|
||||
* @param data
|
||||
*/
|
||||
export function Zlib(data: Uint8Array | string): Uint8Array {
|
||||
if (typeof data === "string")
|
||||
data = fflate.strToU8(data);
|
||||
return fflate.zlibSync(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步地使用Zlib压缩文件
|
||||
* @param data
|
||||
*/
|
||||
export async function ZlibAsync(data: Uint8Array | string): Promise<Uint8Array> {
|
||||
if (typeof data === 'string')
|
||||
data = StrToU8(data);
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const terminate = fflate.zlib(data as Uint8Array, (err, result) => {
|
||||
if (err)
|
||||
reject(err.message);
|
||||
else
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建zip文件
|
||||
* @param data Zip格式对象
|
||||
*/
|
||||
export function Zip(data: fflate.Zippable): Uint8Array {
|
||||
return fflate.zipSync(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步地创建zip文件
|
||||
* @param data Zip格式对象
|
||||
*/
|
||||
export async function ZipAsync(data: fflate.Zippable): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const terminate = fflate.zip(data, (err, result) => {
|
||||
if (err)
|
||||
reject(err.message);
|
||||
else
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压Zip文件
|
||||
* @param data
|
||||
*/
|
||||
export function Unzip(data: Uint8Array): fflate.Unzipped {
|
||||
return fflate.unzipSync(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步地解压Zip文件
|
||||
* @param data
|
||||
*/
|
||||
export function UnzipAsync(data: Uint8Array): Promise<fflate.Unzipped> {
|
||||
return new Promise<fflate.Unzipped>((resolve, reject) => {
|
||||
const terminate = fflate.unzip(data, (err, result) => {
|
||||
if (err)
|
||||
reject(err.message);
|
||||
else
|
||||
resolve(result);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩文件,支持GZip,Zlib或DEFLATE数据
|
||||
* @param data
|
||||
*/
|
||||
export function Decompress(data: Uint8Array): string | undefined {
|
||||
// decompressSync 存在eof问题
|
||||
let stringData: string | undefined = undefined;
|
||||
const utfDecode = new fflate.DecodeUTF8((data) => {
|
||||
stringData = data;
|
||||
});
|
||||
const dcmpStrm = new fflate.Decompress((chunk, final) => {
|
||||
utfDecode.push(chunk, final);
|
||||
});
|
||||
try {
|
||||
dcmpStrm.push(data);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return stringData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步地解压缩文件,支持GZip,Zlib或DEFLATE数据
|
||||
* @param data
|
||||
*/
|
||||
export async function DecompressAsync(data: Uint8Array, size: number | undefined = undefined): Promise<Uint8Array> {
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
const terminate = fflate.decompress(data, { size: size }, (err, result) => {
|
||||
if (err)
|
||||
reject(err.message);
|
||||
else
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转换为8位无符号整型数组
|
||||
* @param str
|
||||
*/
|
||||
export function StrToU8(str: string): Uint8Array {
|
||||
return fflate.strToU8(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将8位无符号整形数组转换为字符串
|
||||
* @param u8arr
|
||||
*/
|
||||
export function U8ToStr(u8arr: Uint8Array): string {
|
||||
return fflate.strFromU8(u8arr);
|
||||
}
|
19
src/helpers/helper.imageLoader.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ImageLoader } from "three";
|
||||
|
||||
let loader = new ImageLoader();
|
||||
export async function LoadImageFromUrl(url: string): Promise<HTMLImageElement>
|
||||
{
|
||||
return new Promise<HTMLImageElement>(async (res, rej) =>
|
||||
{
|
||||
if (!globalThis.document)
|
||||
{
|
||||
res(undefined);
|
||||
return;
|
||||
};
|
||||
|
||||
loader.load(url,
|
||||
img => res(img), e => { },
|
||||
err => res(undefined)
|
||||
);
|
||||
});
|
||||
}
|
7
src/helpers/helper.string.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function IsNullOrEmpty(str?: string | null) {
|
||||
return str === null || str === undefined || str === "";
|
||||
}
|
||||
|
||||
export function IsNullOrWhitespace(str?: string | null) {
|
||||
return str === null || str === undefined || str.trim() === "";
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
createApp(App).mount('#app')
|
||||
const pinia = createPinia();
|
||||
|
||||
createApp(App)
|
||||
.use(pinia)
|
||||
.mount('#app')
|
||||
|
||||
|
150
src/stores/sceneStore.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { MaterialEditor } from "../common/MaterialEditor";
|
||||
import { Database, ObjectId, PhysicalMaterialRecord, TextureTableRecord } from "webcad_ue4_api";
|
||||
import { LoadImageFromUrl } from "../helpers/helper.imageLoader";
|
||||
import { Texture } from "three";
|
||||
import { materialRenderer } from "../common/MaterialRenderer";
|
||||
import { ImgsUrl, MaterialUrls } from "../api/Api";
|
||||
import { Post, PostJson, RequestStatus } from "../api/Request";
|
||||
import { MaterialOut } from "../common/MaterialSerializer";
|
||||
import { DeflateAsync } from "../helpers/helper.compression";
|
||||
|
||||
export const useScene = defineStore('scene', () => {
|
||||
let _editor: MaterialEditor;
|
||||
const _currGeometry = ref<string>('球');
|
||||
const _currTexture = ref<Texture>();
|
||||
const CurrGeometry = computed({
|
||||
get: () => _currGeometry.value,
|
||||
set: (val: string) => ChangeGeometry(val)
|
||||
})
|
||||
const CurrTexture = computed<Texture>(() => _currTexture.value);
|
||||
const Geometries = ref<string[]>([]);
|
||||
const Material = ref<PhysicalMaterialRecord>(new PhysicalMaterialRecord());
|
||||
|
||||
function Initial(canvas: HTMLElement) {
|
||||
if (_editor) {
|
||||
console.warn("SceneStore has already been initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// 为Material配置一个ObjectId,否则其无法被序列化
|
||||
Material.value.objectId = new ObjectId(undefined, undefined);
|
||||
|
||||
_editor = MaterialEditor.GetInstance();
|
||||
Geometries.value = Array.from(_editor.Geometrys.keys());
|
||||
_currGeometry.value = _editor.CurGeometryName;
|
||||
|
||||
_editor.SetViewer(canvas);
|
||||
_editor.setMaterial((Material.value) as PhysicalMaterialRecord);
|
||||
|
||||
const view = _editor.Viewer;
|
||||
window.onresize = () => {
|
||||
view.OnSize(canvas.clientWidth, canvas.clientHeight);
|
||||
view.UpdateRender();
|
||||
}
|
||||
}
|
||||
|
||||
function ChangeGeometry(geoName: string) {
|
||||
_currGeometry.value = geoName;
|
||||
let geo = _editor.Geometrys.get(_currGeometry.value);
|
||||
if (geo) {
|
||||
_editor.ShowMesh.geometry = geo;
|
||||
_editor.Viewer.UpdateRender();
|
||||
}
|
||||
}
|
||||
|
||||
function Update() {
|
||||
_editor.Viewer.UpdateRender();
|
||||
}
|
||||
|
||||
async function UpdateMaterialAsync() {
|
||||
await Material.value.Update();
|
||||
await _editor.Update();
|
||||
Update();
|
||||
}
|
||||
|
||||
async function ChangeTextureAsync(url: string) {
|
||||
const record = new TextureTableRecord();
|
||||
record.objectId = new ObjectId(undefined, record);
|
||||
|
||||
const img = await LoadImageFromUrl(url);
|
||||
|
||||
_currTexture.value = record['texture'] as Texture;
|
||||
_currTexture.value.image = img;
|
||||
Material.value.map = record.Id;
|
||||
_currTexture.value.needsUpdate = true;
|
||||
await UpdateMaterialAsync();
|
||||
}
|
||||
|
||||
function UpdateTexture(adjustment: TextureAdjustment) {
|
||||
const texture = _currTexture.value;
|
||||
texture.wrapS = adjustment.wrapS;
|
||||
texture.wrapT = adjustment.wrapT;
|
||||
texture.anisotropy = 16;
|
||||
texture.rotation = adjustment.rotation;
|
||||
texture.repeat.set(adjustment.repeatX, adjustment.repeatY);
|
||||
texture.offset.set(adjustment.moveX, adjustment.moveY);
|
||||
texture.needsUpdate = true;
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
interface UploadMaterialRequest {
|
||||
dirId: string;
|
||||
materialName: string;
|
||||
}
|
||||
async function UploadMaterialAsync(request: UploadMaterialRequest) {
|
||||
const logoPath = await HandleUpdateLogo();
|
||||
const matJson = MaterialOut(Material.value as PhysicalMaterialRecord);
|
||||
const data = await PostJson(MaterialUrls.create, {
|
||||
dir_id: request,
|
||||
name: request.materialName,
|
||||
logo: logoPath,
|
||||
// jsonString -> Deflate -> BinaryString -> Base64
|
||||
file: btoa(String.fromCharCode(...await DeflateAsync(matJson))),
|
||||
zip_type: 'gzip',
|
||||
});
|
||||
if (data.err_code !== RequestStatus.Ok) {
|
||||
throw new Error(data.err_msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function HandleUpdateLogo() {
|
||||
const blob = await materialRenderer.getBlob(Material.value.Material);
|
||||
const file = new File([blob], "blob.png", { type: blob.type });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
let data = await Post(ImgsUrl.logo, formData);
|
||||
|
||||
let logoPath = "";
|
||||
if (data.err_code === RequestStatus.Ok) {
|
||||
logoPath = data.images.path;
|
||||
}
|
||||
return logoPath;
|
||||
}
|
||||
|
||||
return {
|
||||
CurrGeometry,
|
||||
Geometries,
|
||||
Material,
|
||||
Initial,
|
||||
Update,
|
||||
UpdateMaterialAsync,
|
||||
ChangeTextureAsync,
|
||||
UpdateTexture,
|
||||
UploadMaterialAsync,
|
||||
};
|
||||
});
|
||||
|
||||
export type TextureAdjustment = {
|
||||
wrapS: number,
|
||||
wrapT: number,
|
||||
rotation: number,
|
||||
repeatX: number,
|
||||
repeatY: number,
|
||||
moveX: number,
|
||||
moveY: number
|
||||
}
|