添加材质切换,调节功能
@ -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.
|
独立实现的WebCAD材质编辑器,提供材质预览,调节和上传功能。
|
||||||
|
|
||||||
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).
|
|
15
package.json
@ -9,18 +9,21 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@jscad/modeling": "^2.11.0",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
"flatbush": "^3.3.0",
|
"flatbush": "^3.3.0",
|
||||||
"xaop": "^2.0.0",
|
"js-angusj-clipper": "^1.2.1",
|
||||||
"webcad_ue4_api": "http://gitea.cf/cx/webcad-ue4-api/archive/3.20.0.tar.gz"
|
"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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"csstype": "^3.1.3",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.0",
|
||||||
"vue-tsc": "^2.2.4"
|
"vue-tsc": "^2.2.4"
|
||||||
|
110
pnpm-lock.yaml
@ -11,12 +11,18 @@ importers:
|
|||||||
'@jscad/modeling':
|
'@jscad/modeling':
|
||||||
specifier: ^2.11.0
|
specifier: ^2.11.0
|
||||||
version: 2.12.5
|
version: 2.12.5
|
||||||
|
fflate:
|
||||||
|
specifier: ^0.8.2
|
||||||
|
version: 0.8.2
|
||||||
flatbush:
|
flatbush:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
js-angusj-clipper:
|
js-angusj-clipper:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.3.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:
|
polylabel:
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
@ -39,6 +45,9 @@ importers:
|
|||||||
'@vue/tsconfig':
|
'@vue/tsconfig':
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
|
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:
|
typescript:
|
||||||
specifier: ~5.7.2
|
specifier: ~5.7.2
|
||||||
version: 5.7.3
|
version: 5.7.3
|
||||||
@ -369,6 +378,15 @@ packages:
|
|||||||
'@vue/compiler-vue2@2.7.16':
|
'@vue/compiler-vue2@2.7.16':
|
||||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
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':
|
'@vue/language-core@2.2.8':
|
||||||
resolution: {integrity: sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==}
|
resolution: {integrity: sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -411,9 +429,16 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
birpc@0.2.19:
|
||||||
|
resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
|
||||||
|
|
||||||
brace-expansion@2.0.1:
|
brace-expansion@2.0.1:
|
||||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||||
|
|
||||||
|
copy-anything@3.0.5:
|
||||||
|
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@ -432,6 +457,9 @@ packages:
|
|||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
|
fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
flatbush@3.3.1:
|
flatbush@3.3.1:
|
||||||
resolution: {integrity: sha512-oKuPbtT+DS2CxH+9Vhbsq8HifmSCuOw+3Cy5zt/vCIrZl5KyengoTHDBLmtpZoBhcwa7/biNjgL1DwdLMJYm1A==}
|
resolution: {integrity: sha512-oKuPbtT+DS2CxH+9Vhbsq8HifmSCuOw+3Cy5zt/vCIrZl5KyengoTHDBLmtpZoBhcwa7/biNjgL1DwdLMJYm1A==}
|
||||||
|
|
||||||
@ -447,6 +475,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
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:
|
js-angusj-clipper@1.3.1:
|
||||||
resolution: {integrity: sha512-/qru4QXxN/gBbQjL4WaFl296YSM8kh5XKpNuNqfZhJ4t4Hw3KeLc5ERj3XHAeLi6pBrqeh6o9PFZUpS3QThEEQ==}
|
resolution: {integrity: sha512-/qru4QXxN/gBbQjL4WaFl296YSM8kh5XKpNuNqfZhJ4t4Hw3KeLc5ERj3XHAeLi6pBrqeh6o9PFZUpS3QThEEQ==}
|
||||||
|
|
||||||
@ -457,6 +492,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
mitt@3.0.1:
|
||||||
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
muggle-string@0.4.1:
|
muggle-string@0.4.1:
|
||||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||||
|
|
||||||
@ -468,9 +506,21 @@ packages:
|
|||||||
path-browserify@1.0.1:
|
path-browserify@1.0.1:
|
||||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
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:
|
polylabel@1.1.0:
|
||||||
resolution: {integrity: sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==}
|
resolution: {integrity: sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==}
|
||||||
|
|
||||||
@ -478,6 +528,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
rfdc@1.4.1:
|
||||||
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
rollup@4.39.0:
|
rollup@4.39.0:
|
||||||
resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
|
resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@ -487,6 +540,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
three-cf@0.122.9:
|
||||||
resolution: {integrity: sha512-y+bvPYKI0yMNGF2flNsTbqloPiMAL4OKbIbR9QaQLRjNlz15Th+69ZtirU5ZASGXF/vQIvIrv+6OkxdSjiN89Q==}
|
resolution: {integrity: sha512-y+bvPYKI0yMNGF2flNsTbqloPiMAL4OKbIbR9QaQLRjNlz15Th+69ZtirU5ZASGXF/vQIvIrv+6OkxdSjiN89Q==}
|
||||||
|
|
||||||
@ -770,6 +831,24 @@ snapshots:
|
|||||||
de-indent: 1.0.2
|
de-indent: 1.0.2
|
||||||
he: 1.2.0
|
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)':
|
'@vue/language-core@2.2.8(typescript@5.7.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/language-core': 2.4.12
|
'@volar/language-core': 2.4.12
|
||||||
@ -816,10 +895,16 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
birpc@0.2.19: {}
|
||||||
|
|
||||||
brace-expansion@2.0.1:
|
brace-expansion@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
copy-anything@3.0.5:
|
||||||
|
dependencies:
|
||||||
|
is-what: 4.1.16
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
@ -856,6 +941,8 @@ snapshots:
|
|||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
|
fflate@0.8.2: {}
|
||||||
|
|
||||||
flatbush@3.3.1:
|
flatbush@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flatqueue: 1.2.1
|
flatqueue: 1.2.1
|
||||||
@ -867,6 +954,10 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
|
is-what@4.1.16: {}
|
||||||
|
|
||||||
js-angusj-clipper@1.3.1: {}
|
js-angusj-clipper@1.3.1: {}
|
||||||
|
|
||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
@ -877,14 +968,25 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.1
|
brace-expansion: 2.0.1
|
||||||
|
|
||||||
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
muggle-string@0.4.1: {}
|
muggle-string@0.4.1: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
path-browserify@1.0.1: {}
|
path-browserify@1.0.1: {}
|
||||||
|
|
||||||
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
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:
|
polylabel@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyqueue: 2.0.3
|
tinyqueue: 2.0.3
|
||||||
@ -895,6 +997,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rollup@4.39.0:
|
rollup@4.39.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.7
|
'@types/estree': 1.0.7
|
||||||
@ -923,6 +1027,12 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
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: {}
|
three-cf@0.122.9: {}
|
||||||
|
|
||||||
tinyqueue@2.0.3: {}
|
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">
|
<script setup lang="ts">
|
||||||
import MaterialView from './components/MaterialView.vue';
|
import MaterialView from './components/MaterialView.vue';
|
||||||
|
|
||||||
|
// 禁用右键菜单
|
||||||
|
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 { Singleton } from './Singleton';
|
||||||
import { Viewer } from './Viewer';
|
import { Viewer } from './Viewer';
|
||||||
import { PMREMGenerator3 } from './PMREMGenerator2';
|
import { PMREMGenerator3 } from './PMREMGenerator2';
|
||||||
import type { PhysicalMaterialRecord, TextureTableRecord } from 'webcad_ue4_api';
|
import type { PhysicalMaterialRecord, TextureTableRecord } from 'webcad_ue4_api';
|
||||||
import { MaterialEditorCamerControl } from './MaterialMouseControl';
|
import { MaterialEditorCameraControl } from './MaterialMouseControl';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
||||||
const texture = textureRecord['texture'] as Texture;
|
const texture = textureRecord['texture'] as Texture;
|
||||||
@ -29,9 +28,11 @@ async function textureRenderUpdate(textureRecord:TextureTableRecord){
|
|||||||
*/
|
*/
|
||||||
export class MaterialEditor extends Singleton
|
export class MaterialEditor extends Singleton
|
||||||
{
|
{
|
||||||
|
private _pointerLocked = false;
|
||||||
|
|
||||||
Geometrys: Map<string, Geometry | BufferGeometry>;
|
Geometrys: Map<string, Geometry | BufferGeometry>;
|
||||||
|
|
||||||
CurGeometryName = ref("球");
|
CurGeometryName = "球";
|
||||||
Canvas: HTMLElement;
|
Canvas: HTMLElement;
|
||||||
ShowObject: Object3D;
|
ShowObject: Object3D;
|
||||||
ShowMesh: Mesh;
|
ShowMesh: Mesh;
|
||||||
@ -68,14 +69,21 @@ export class MaterialEditor extends Singleton
|
|||||||
{
|
{
|
||||||
this.Viewer = new Viewer(this.Canvas);
|
this.Viewer = new Viewer(this.Canvas);
|
||||||
// this.Viewer.PreViewer.Cursor.CursorObject.visible = false;
|
// this.Viewer.PreViewer.Cursor.CursorObject.visible = false;
|
||||||
// this.Viewer.CameraCtrl.CameraType = CameraType.PerspectiveCamera;
|
|
||||||
// this.Viewer.UsePass = false;
|
// this.Viewer.UsePass = false;
|
||||||
this.initScene();
|
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
|
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.Canvas = canvas;
|
||||||
this.initViewer();
|
this.initViewer();
|
||||||
this.CurGeometryName.value = "球";
|
this.CurGeometryName = "球";
|
||||||
}
|
}
|
||||||
initScene()
|
initScene()
|
||||||
{
|
{
|
||||||
let scene = this.Viewer.Scene;
|
let scene = this.Viewer.Scene;
|
||||||
this.ShowObject = new Object3D();
|
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 = new Mesh(geo, this._MeshMaterial);
|
||||||
this.ShowMesh.scale.set(1000, 1000, 1000);
|
this.ShowMesh.scale.set(1000, 1000, 1000);
|
||||||
|
|
||||||
this.ShowObject.add(this.ShowMesh);
|
this.ShowObject.add(this.ShowMesh);
|
||||||
scene.add(this.ShowObject);
|
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();
|
let ambient = new AmbientLight();
|
||||||
@ -119,14 +117,15 @@ export class MaterialEditor extends Singleton
|
|||||||
if (this.metaTexture) return this.metaTexture;
|
if (this.metaTexture) return this.metaTexture;
|
||||||
if (this.metaPromise) return this.metaPromise;
|
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'];
|
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) =>
|
.load(urls, (t) =>
|
||||||
{
|
{
|
||||||
t.encoding = sRGBEncoding;
|
t.encoding = sRGBEncoding;
|
||||||
t.mapping = CubeRefractionMapping;
|
t.mapping = CubeRefractionMapping;
|
||||||
|
t.encoding = LinearEncoding;
|
||||||
|
|
||||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||||
let ldrCubeRenderTarget = pmremGenerator.fromCubemap(t);
|
let ldrCubeRenderTarget = pmremGenerator.fromCubemap(t);
|
||||||
@ -135,8 +134,6 @@ export class MaterialEditor extends Singleton
|
|||||||
res(this.metaTexture);
|
res(this.metaTexture);
|
||||||
|
|
||||||
this.Viewer.Scene.background = this.metaTexture;
|
this.Viewer.Scene.background = this.metaTexture;
|
||||||
// this.Viewer.Scene.background = new Color(255,0,0);
|
|
||||||
|
|
||||||
this.Viewer.UpdateRender();
|
this.Viewer.UpdateRender();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -151,12 +148,14 @@ export class MaterialEditor extends Singleton
|
|||||||
|
|
||||||
this.exrPromise = new Promise<Texture>((res, rej) =>
|
this.exrPromise = new Promise<Texture>((res, rej) =>
|
||||||
{
|
{
|
||||||
let urls = ['right.webp', 'left.webp', 'top.webp', 'bottom.webp', 'front.webp', 'back.webp'];
|
let urls = ['right-gray.webp', 'left-gray.webp', 'top-gray.webp', 'bottom-gray.webp', 'front-gray.webp', 'back-gray.webp'];
|
||||||
new CubeTextureLoader().setPath('https://cdn.cfcad.cn/t/house_gray/')
|
new CubeTextureLoader().setPath('./')
|
||||||
.load(urls, (t) =>
|
.load(urls, (t) =>
|
||||||
{
|
{
|
||||||
t.encoding = sRGBEncoding;
|
t.encoding = sRGBEncoding;
|
||||||
t.mapping = CubeRefractionMapping;
|
t.mapping = CubeRefractionMapping;
|
||||||
|
t.encoding = LinearEncoding;
|
||||||
|
|
||||||
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
let pmremGenerator = new PMREMGenerator3(this.Viewer.Renderer);
|
||||||
let target = pmremGenerator.fromCubemap(t);
|
let target = pmremGenerator.fromCubemap(t);
|
||||||
this.exrTexture = target.texture;
|
this.exrTexture = target.texture;
|
||||||
@ -198,21 +197,6 @@ export class MaterialEditor extends Singleton
|
|||||||
async Update()
|
async Update()
|
||||||
{
|
{
|
||||||
let mat = this.ShowMesh.material as MeshPhysicalMaterial;
|
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;
|
mat.needsUpdate = true;
|
||||||
|
|
||||||
this._MeshMaterial.copy(this.Material.Material);
|
this._MeshMaterial.copy(this.Material.Material);
|
||||||
|
@ -4,24 +4,25 @@ import type { Viewer } from './Viewer';
|
|||||||
/**
|
/**
|
||||||
* 材质编辑器的场景鼠标控制.
|
* 材质编辑器的场景鼠标控制.
|
||||||
*/
|
*/
|
||||||
export class MaterialEditorCamerControl
|
export class MaterialEditorCameraControl {
|
||||||
{
|
|
||||||
private Viewer: Viewer;
|
private Viewer: Viewer;
|
||||||
|
|
||||||
//State.
|
private _movement: Vector3 = new Vector3();
|
||||||
private _MouseIsDown: boolean = false;
|
private _MouseIsDown: boolean = false;
|
||||||
private _StartPoint: Vector3 = new Vector3();
|
|
||||||
private _EndPoint = new Vector3();
|
|
||||||
private pointId: number;
|
private pointId: number;
|
||||||
|
|
||||||
constructor(view: Viewer)
|
private _target: Vector3 | null = null;
|
||||||
{
|
|
||||||
this.Viewer = view;
|
|
||||||
|
|
||||||
|
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();
|
this.initMouseControl();
|
||||||
}
|
}
|
||||||
initMouseControl()
|
|
||||||
{
|
initMouseControl() {
|
||||||
let el = this.Viewer.Renderer.domElement;
|
let el = this.Viewer.Renderer.domElement;
|
||||||
el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false);
|
el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false);
|
||||||
el.addEventListener("mousedown", this.onMouseDown, false);
|
el.addEventListener("mousedown", this.onMouseDown, false);
|
||||||
@ -29,28 +30,23 @@ export class MaterialEditorCamerControl
|
|||||||
el.addEventListener("mouseup", this.onMouseUp, false);
|
el.addEventListener("mouseup", this.onMouseUp, false);
|
||||||
el.addEventListener('wheel', this.onMouseWheel, false);
|
el.addEventListener('wheel', this.onMouseWheel, false);
|
||||||
}
|
}
|
||||||
onMouseDown = (event: MouseEvent) =>
|
onMouseDown = (event: MouseEvent) => {
|
||||||
{
|
|
||||||
this.requestPointerLock();
|
this.requestPointerLock();
|
||||||
this._MouseIsDown = true;
|
this._MouseIsDown = true;
|
||||||
this._StartPoint.set(event.offsetX, event.offsetY, 0);
|
|
||||||
};
|
};
|
||||||
onMouseUp = (event: MouseEvent) =>
|
onMouseUp = (event: MouseEvent) => {
|
||||||
{
|
|
||||||
this._MouseIsDown = false;
|
this._MouseIsDown = false;
|
||||||
this.exitPointerLock();
|
this.exitPointerLock();
|
||||||
};
|
};
|
||||||
onMouseMove = (event: MouseEvent) =>
|
onMouseMove = (event: MouseEvent) => {
|
||||||
{
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this._MouseIsDown)
|
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);
|
|
||||||
|
|
||||||
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) =>
|
onMouseWheel = (event: WheelEvent) =>
|
||||||
@ -65,21 +61,25 @@ export class MaterialEditorCamerControl
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
requestPointerLock()
|
requestPointerLock() {
|
||||||
{
|
const dom = this.Viewer.Renderer.domElement;
|
||||||
if (this.Viewer.Renderer.domElement.setPointerCapture)
|
if (dom.setPointerCapture)
|
||||||
this.Viewer.Renderer.domElement.setPointerCapture(this.pointId);
|
dom.setPointerCapture(this.pointId);
|
||||||
|
|
||||||
|
if (dom.requestPointerLock)
|
||||||
|
dom.requestPointerLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
exitPointerLock()
|
exitPointerLock() {
|
||||||
{
|
const dom = this.Viewer.Renderer.domElement;
|
||||||
if (this.Viewer.Renderer.domElement.releasePointerCapture && this.pointId !== undefined)
|
if (dom.releasePointerCapture && this.pointId !== undefined) {
|
||||||
{
|
try {
|
||||||
try
|
dom.releasePointerCapture(this.pointId);
|
||||||
{
|
|
||||||
this.Viewer.Renderer.domElement.releasePointerCapture(this.pointId);
|
|
||||||
}
|
}
|
||||||
catch (error) { }
|
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 { cZeroVec, GetBox, GetBoxArr } from "./GeUtils";
|
||||||
import { PlaneExt } from "./PlaneExt";
|
import { PlaneExt } from "./PlaneExt";
|
||||||
import { CameraUpdate } from "webcad_ue4_api";
|
import { CameraType, CameraUpdate } from "webcad_ue4_api";
|
||||||
import { CameraControls } from "./CameraControls";
|
|
||||||
|
|
||||||
export class Viewer {
|
export class Viewer {
|
||||||
m_LookTarget: any;
|
m_LookTarget: any;
|
||||||
m_Camera: CameraUpdate = new CameraUpdate();
|
m_Camera: CameraUpdate = new CameraUpdate();
|
||||||
CameraCtrl: CameraControls;
|
|
||||||
protected NeedUpdate: boolean = true;
|
protected NeedUpdate: boolean = true;
|
||||||
Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型
|
Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型
|
||||||
m_DomEl: HTMLElement; //画布容器
|
m_DomEl: HTMLElement; //画布容器
|
||||||
@ -21,6 +19,15 @@ export class Viewer {
|
|||||||
return this._Scene;
|
return this._Scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get Fov() {
|
||||||
|
return this.m_Camera.Fov;
|
||||||
|
}
|
||||||
|
|
||||||
|
set Fov(val: number) {
|
||||||
|
this.m_Camera.Fov = val;
|
||||||
|
this.NeedUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
UpdateRender()
|
UpdateRender()
|
||||||
{
|
{
|
||||||
this.NeedUpdate = true;
|
this.NeedUpdate = true;
|
||||||
@ -37,10 +44,10 @@ export class Viewer {
|
|||||||
this.initRender(canvasContainer);
|
this.initRender(canvasContainer);
|
||||||
this.OnSize();
|
this.OnSize();
|
||||||
this.StartRender();
|
this.StartRender();
|
||||||
this.CameraCtrl = new CameraControls(this);
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
this.OnSize();
|
this.OnSize();
|
||||||
});
|
});
|
||||||
|
this.InitCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
//初始化render
|
//初始化render
|
||||||
@ -73,6 +80,11 @@ export class Viewer {
|
|||||||
this.OnSize();
|
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) => {
|
OnSize = (width?: number, height?: number) => {
|
||||||
this._Width = width ? width : this.m_DomEl.clientWidth;
|
this._Width = width ? width : this.m_DomEl.clientWidth;
|
||||||
this._Height = height ? height : this.m_DomEl.clientHeight;
|
this._Height = height ? height : this.m_DomEl.clientHeight;
|
||||||
@ -91,6 +103,7 @@ export class Viewer {
|
|||||||
requestAnimationFrame(this.StartRender);
|
requestAnimationFrame(this.StartRender);
|
||||||
if (this._Scene != null && this.NeedUpdate) {
|
if (this._Scene != null && this.NeedUpdate) {
|
||||||
this.Render();
|
this.Render();
|
||||||
|
this.m_Camera.Update();
|
||||||
this.NeedUpdate = false;
|
this.NeedUpdate = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -139,6 +152,10 @@ export class Viewer {
|
|||||||
this.m_Camera.Rotate(mouseMove, this.m_LookTarget);
|
this.m_Camera.Rotate(mouseMove, this.m_LookTarget);
|
||||||
this.NeedUpdate = true;
|
this.NeedUpdate = true;
|
||||||
}
|
}
|
||||||
|
RotateAround(movement: Vector3, center: Vector3) {
|
||||||
|
this.m_Camera.Rotate(movement, center);
|
||||||
|
this.NeedUpdate = true;
|
||||||
|
}
|
||||||
Pan(mouseMove: Vector3) {
|
Pan(mouseMove: Vector3) {
|
||||||
this.m_Camera.Pan(mouseMove);
|
this.m_Camera.Pan(mouseMove);
|
||||||
this.NeedUpdate = true;
|
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>
|
<template>
|
||||||
<div ref="container" style="width: 800px;height: 800px;"></div>
|
<CfFlex class="material-view">
|
||||||
{{ CurGeometryName }}
|
<div ref="container" style="width: 100%; height: 100%; flex: 3; box-sizing: border-box;" />
|
||||||
<div v-for="geo in geometries">
|
<MaterialAdjuster style="flex: 1;overflow-y: auto; width: 100%; height: 100%; box-sizing: border-box;" />
|
||||||
<button @click="changeGeometry(geo[0])">{{ geo[0] }}</button>
|
</CfFlex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, useTemplateRef } from 'vue';
|
import { onMounted, useTemplateRef } from 'vue';
|
||||||
import { MaterialEditor } from '../common/MaterialEditor';
|
import MaterialAdjuster from './MaterialAdjuster.vue';
|
||||||
import { PhysicalMaterialRecord } from 'webcad_ue4_api';
|
import { useScene } from '../stores/sceneStore';
|
||||||
|
import CfFlex from './CfFlex.vue';
|
||||||
|
|
||||||
|
const scene = useScene();
|
||||||
const container = useTemplateRef<HTMLElement>('container');
|
const container = useTemplateRef<HTMLElement>('container');
|
||||||
|
|
||||||
|
|
||||||
let editor:MaterialEditor = MaterialEditor.GetInstance();
|
|
||||||
const geometries = editor.Geometrys;
|
|
||||||
const material = new PhysicalMaterialRecord();
|
|
||||||
|
|
||||||
const CurGeometryName = editor.CurGeometryName;
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
editor.SetViewer(container.value);
|
scene.Initial(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</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 { createApp } from 'vue'
|
||||||
|
|
||||||
import App from './App.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
|
||||||
|
}
|