diff --git a/README.md b/README.md index 33895ab..cdd7748 100644 --- a/README.md +++ b/README.md @@ -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 ` diff --git a/src/api/Api.ts b/src/api/Api.ts new file mode 100644 index 0000000..11cb7a2 --- /dev/null +++ b/src/api/Api.ts @@ -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", +}; \ No newline at end of file diff --git a/src/api/Request.ts b/src/api/Request.ts new file mode 100644 index 0000000..498fa87 --- /dev/null +++ b/src/api/Request.ts @@ -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( + url: string, body: Exclude, + 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 +{ + 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}` }; + } +} diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..35ab72c --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,10 @@ +body { + margin: 0; + padding: 0; + font-family: 'Noto Sans SC', "Microsoft YaHei", Arial, sans-serif; +} + +html { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/src/common/CameraControls.ts b/src/common/CameraControls.ts deleted file mode 100644 index 88588ff..0000000 --- a/src/common/CameraControls.ts +++ /dev/null @@ -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(); - m_MouseDown = new Map(); - - //状态 - 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); - } -} diff --git a/src/common/MaterialEditor.ts b/src/common/MaterialEditor.ts index c511a95..a15aa3a 100644 --- a/src/common/MaterialEditor.ts +++ b/src/common/MaterialEditor.ts @@ -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; - 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((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); diff --git a/src/common/MaterialMouseControl.ts b/src/common/MaterialMouseControl.ts index 002805e..59e34ca 100644 --- a/src/common/MaterialMouseControl.ts +++ b/src/common/MaterialMouseControl.ts @@ -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(); } } diff --git a/src/common/MaterialRenderer.ts b/src/common/MaterialRenderer.ts new file mode 100644 index 0000000..8758456 --- /dev/null +++ b/src/common/MaterialRenderer.ts @@ -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 + { + 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(); \ No newline at end of file diff --git a/src/common/MaterialSerializer.ts b/src/common/MaterialSerializer.ts new file mode 100644 index 0000000..938344d --- /dev/null +++ b/src/common/MaterialSerializer.ts @@ -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]; +} \ No newline at end of file diff --git a/src/common/Viewer.ts b/src/common/Viewer.ts index 8810967..d7de98a 100644 --- a/src/common/Viewer.ts +++ b/src/common/Viewer.ts @@ -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; diff --git a/src/components/CfFlex.vue b/src/components/CfFlex.vue new file mode 100644 index 0000000..6e3327d --- /dev/null +++ b/src/components/CfFlex.vue @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/src/components/MaterialAdjuster.vue b/src/components/MaterialAdjuster.vue new file mode 100644 index 0000000..5082d3c --- /dev/null +++ b/src/components/MaterialAdjuster.vue @@ -0,0 +1,293 @@ + + + + 模型预览 + 选择模型样式 + + {{ name }} + + + + 纹理选择(DEBUG) + 纹理链接 + + 应用 + + + 材质调节 + + 金属度 + + + {{ model.metallic }} + + + 粗糙度 + + + {{ model.roughness }} + + + 法线强度 + + + {{ model.normalScale }} + + + 高光 + + + {{ model.emissiveIntensity }} + + + + + 纹理调节 + 启用纹理 + + + 平铺U + + 镜像平铺 + 平铺 + 展开 + + + 平铺V + + 镜像平铺 + 平铺 + 展开 + + + 旋转 + + + + 偏移 + X + +   +   + Y + + + + + 尺寸 + 长 + +   +   + 宽 + + + + + + + 操作 + + DEBUG + 上传路径ID + + + 材质名 + + + + 上传 + 取消 + + + + + + + + \ No newline at end of file diff --git a/src/components/MaterialView.vue b/src/components/MaterialView.vue index e2759e7..9bf2fa3 100644 --- a/src/components/MaterialView.vue +++ b/src/components/MaterialView.vue @@ -1,41 +1,32 @@ - - {{ CurGeometryName }} - - {{ geo[0] }} - + + + + -function changeGeometry(geoName:string) { - CurGeometryName.value = geoName; - let geo = editor.Geometrys.get(CurGeometryName.value); - if (geo) { - editor.ShowMesh.geometry = geo; - editor.Viewer.UpdateRender(); - } + \ No newline at end of file diff --git a/src/helpers/MathHelper.ts b/src/helpers/MathHelper.ts new file mode 100644 index 0000000..229b0b8 --- /dev/null +++ b/src/helpers/MathHelper.ts @@ -0,0 +1,3 @@ +const clamp = (val, min, max) => Math.min(Math.max(val, min), max); + +export default { clamp }; \ No newline at end of file diff --git a/src/helpers/helper.compression.ts b/src/helpers/helper.compression.ts new file mode 100644 index 0000000..3f4612c --- /dev/null +++ b/src/helpers/helper.compression.ts @@ -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 { + if (typeof data === 'string') + data = StrToU8(data); + return new Promise((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 { + if (typeof data === 'string') + data = StrToU8(data); + return new Promise((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 { + return new Promise((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 { + return new Promise((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 { + return new Promise((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); +} diff --git a/src/helpers/helper.imageLoader.ts b/src/helpers/helper.imageLoader.ts new file mode 100644 index 0000000..646db33 --- /dev/null +++ b/src/helpers/helper.imageLoader.ts @@ -0,0 +1,19 @@ +import { ImageLoader } from "three"; + +let loader = new ImageLoader(); +export async function LoadImageFromUrl(url: string): Promise +{ + return new Promise(async (res, rej) => + { + if (!globalThis.document) + { + res(undefined); + return; + }; + + loader.load(url, + img => res(img), e => { }, + err => res(undefined) + ); + }); +} \ No newline at end of file diff --git a/src/helpers/helper.string.ts b/src/helpers/helper.string.ts new file mode 100644 index 0000000..132a97d --- /dev/null +++ b/src/helpers/helper.string.ts @@ -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() === ""; +} diff --git a/src/main.ts b/src/main.ts index bf50ae6..345943b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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') diff --git a/src/stores/sceneStore.ts b/src/stores/sceneStore.ts new file mode 100644 index 0000000..290774f --- /dev/null +++ b/src/stores/sceneStore.ts @@ -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('球'); + const _currTexture = ref(); + const CurrGeometry = computed({ + get: () => _currGeometry.value, + set: (val: string) => ChangeGeometry(val) + }) + const CurrTexture = computed(() => _currTexture.value); + const Geometries = ref([]); + const Material = ref(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 +} \ No newline at end of file