添加材质切换,调节功能

This commit is contained in:
陈梓阳 2025-04-14 16:37:17 +08:00
parent 0855f15a5c
commit 0929d5b80c
35 changed files with 1144 additions and 372 deletions

View File

@ -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材质编辑器提供材质预览调节和上传功能。

View File

@ -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"

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
public/back.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/bottom-gray.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

BIN
public/bottom.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

BIN
public/front-gray.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
public/front.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
public/left-gray.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/left.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
public/right-gray.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
public/right.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
public/texture1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
public/top-gray.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
public/top.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -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
View 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
View 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
View 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;
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View 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();

View 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];
}

View File

@ -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
View 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>

View 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;" />
&NonBreakingSpace;
&NonBreakingSpace;
<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;" />
&NonBreakingSpace;
&NonBreakingSpace;
<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>

View File

@ -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);
scene.Initial(container.value);
});
const view = editor.Viewer;
view.OnSize(800, 800);
view.ZoomAll();
view.Zoom(2.1);
})
</script>
function changeGeometry(geoName:string) {
CurGeometryName.value = geoName;
let geo = editor.Geometrys.get(CurGeometryName.value);
if (geo) {
editor.ShowMesh.geometry = geo;
editor.Viewer.UpdateRender();
}
<style scoped>
.material-view
{
width: 100%;
height: 100vh;
box-sizing: border-box;
padding: 0;
margin: 0;
overflow: hidden;
}
</script>
</style>

View File

@ -0,0 +1,3 @@
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
export default { clamp };

View 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);
})
});
}
/**
* GZipZlib或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;
}
/**
* GZipZlib或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);
}

View 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)
);
});
}

View 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() === "";
}

View File

@ -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
View 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
}