diff --git a/package.json b/package.json index 56d64fa..e2b55ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "material-editor", "private": true, - "version": "1.0.2", + "version": "1.0.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src/common/MaterialEditor.ts b/src/common/MaterialEditor.ts index 5c6ab99..6018c81 100644 --- a/src/common/MaterialEditor.ts +++ b/src/common/MaterialEditor.ts @@ -29,8 +29,6 @@ async function textureRenderUpdate(textureRecord:TextureTableRecord){ */ export class MaterialEditor extends Singleton { - private _pointerLocked = false; - Geometrys: Map; CurGeometryName = "球"; @@ -38,6 +36,7 @@ export class MaterialEditor extends Singleton ShowObject: Object3D; ShowMesh: Mesh; Viewer: Viewer; + CameraControl?: MaterialEditorCameraControl; private _MeshMaterial: MeshPhysicalMaterial = new MeshPhysicalMaterial({}); //构造 @@ -72,7 +71,7 @@ export class MaterialEditor extends Singleton // this.Viewer.PreViewer.Cursor.CursorObject.visible = false; // this.Viewer.UsePass = false; this.initScene(); - new MaterialEditorCameraControl(this.Viewer, this.ShowObject.position); + this.CameraControl = new MaterialEditorCameraControl(this.Viewer, this.ShowObject.position); this.Viewer.ZoomAll(); // 初始化相机位置到观察物体的正后方 @@ -82,6 +81,9 @@ export class MaterialEditor extends Singleton this.Viewer.UpdateRender(); this.Viewer.Fov = 90; } + else { + console.warn("Viewer has already been initialized"); + } } SetViewer(canvas: HTMLElement) { @@ -186,8 +188,24 @@ export class MaterialEditor extends Singleton this.Update(); } - dispose() + Dispose() { + this.Geometrys.clear(); + this.Geometrys = undefined; + this.Canvas = undefined; + // object在Viewer中进行释放 + this.ShowObject = undefined; + this.ShowMesh = undefined; + + this.CameraControl?.dispose(); + this.Viewer?.Dispose(); + this.CameraControl = undefined; + this.Viewer = undefined; + this.exrPromise = undefined; + this.exrTexture?.dispose(); + this.exrTexture = undefined; + this.metaTexture?.dispose(); + this.metaTexture = undefined; } async Update() diff --git a/src/common/MaterialMouseControl.ts b/src/common/MaterialMouseControl.ts index 59e34ca..14b2e16 100644 --- a/src/common/MaterialMouseControl.ts +++ b/src/common/MaterialMouseControl.ts @@ -24,12 +24,24 @@ export class MaterialEditorCameraControl { initMouseControl() { let el = this.Viewer.Renderer.domElement; - el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false); + el.addEventListener("pointerdown", this.onPointerDown, false); el.addEventListener("mousedown", this.onMouseDown, false); el.addEventListener("mousemove", this.onMouseMove, false); el.addEventListener("mouseup", this.onMouseUp, false); el.addEventListener('wheel', this.onMouseWheel, false); } + + dispose() { + const el = this.Viewer.Renderer.domElement; + el.removeEventListener("pointerdown", this.onPointerDown); + el.removeEventListener("mousedown", this.onMouseDown); + el.removeEventListener("mousemove", this.onMouseMove); + el.removeEventListener("mouseup", this.onMouseUp); + el.removeEventListener('wheel', this.onMouseWheel, false); + } + + onPointerDown = (event: PointerEvent) => { this.pointId = event.pointerId; }; + onMouseDown = (event: MouseEvent) => { this.requestPointerLock(); this._MouseIsDown = true; @@ -49,14 +61,11 @@ export class MaterialEditorCameraControl { else this.Viewer.Rotate(this._movement); } }; - onMouseWheel = (event: WheelEvent) => - { - if (event.deltaY < 0) - { + onMouseWheel = (event: WheelEvent) => { + if (event.deltaY < 0) { this.Viewer.Zoom(0.6); } - else if (event.deltaY > 0) - { + else if (event.deltaY > 0) { this.Viewer.Zoom(1.4); } }; diff --git a/src/common/Singleton.ts b/src/common/Singleton.ts index 73d356a..9105419 100644 --- a/src/common/Singleton.ts +++ b/src/common/Singleton.ts @@ -2,13 +2,11 @@ let instanceMap = new Map(); -export interface PrototypeType extends Function -{ +export interface PrototypeType extends Function { prototype: T; } -export interface ConstructorFunctionType extends PrototypeType -{ +export interface ConstructorFunctionType extends PrototypeType { new(...args: any[]): T; } @@ -23,13 +21,11 @@ export type ConstructorType = Pr * //获得单例 * let a = A.GetInstance(); */ -export class Singleton -{ +export class Singleton { protected constructor() { } //ref:https://github.com/Microsoft/TypeScript/issues/5863 - static GetInstance(this: ConstructorType): T - { + static GetInstance(this: ConstructorType): T { if (instanceMap.has(this)) return instanceMap.get(this); //@ts-ignore @@ -37,4 +33,23 @@ export class Singleton instanceMap.set(this, __instance__); return __instance__; } + + /** + * 释放指定类型的单例实例。 + * @returns {boolean} 如果实例存在并被释放则返回 true,否则返回 false。 + */ + static ReleaseInstance(this: ConstructorType): boolean { + if (instanceMap.has(this)) { + const instance = instanceMap.get(this); + // 如果实例有清理方法,可以在这里调用 + if (typeof (instance as any).dispose === 'function') { + (instance as any).dispose(); + } + instanceMap.delete(this); + console.log(`Singleton instance of ${this.name} has been released.`); + return true; + } + console.log(`No singleton instance of ${this.name} found to release.`); + return false; + } } diff --git a/src/common/Viewer.ts b/src/common/Viewer.ts index d7de98a..6cfd61e 100644 --- a/src/common/Viewer.ts +++ b/src/common/Viewer.ts @@ -1,20 +1,25 @@ -import { Raycaster, Scene, Vector3, WebGLRenderer, type Vector, type WebGLRendererParameters } from "three"; +import { Raycaster, Scene, Vector3, WebGLRenderer, type WebGLRendererParameters, Object3D, Mesh, Line, Points, Material, Texture } from "three"; import { cZeroVec, GetBox, GetBoxArr } from "./GeUtils"; import { PlaneExt } from "./PlaneExt"; import { CameraType, CameraUpdate } from "webcad_ue4_api"; +import { disposeNode } from "../helpers/helper.three"; + export class Viewer { m_LookTarget: any; m_Camera: CameraUpdate = new CameraUpdate(); protected NeedUpdate: boolean = true; - Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型 - m_DomEl: HTMLElement; //画布容器 + Renderer!: WebGLRenderer; // 在 initRender 中初始化,使用 ! 断言 + m_DomEl: HTMLElement; _Height: number = 0; _Width: number = 0; _Scene: Scene = new Scene(); + private m_IsDisposed: boolean = false; // 添加一个标志来停止渲染循环和防止重复释放 + private m_AnimationFrameId: number | null = null; // 用于取消动画帧 + get Scene() { return this._Scene; } @@ -28,90 +33,95 @@ export class Viewer { this.NeedUpdate = true; } - UpdateRender() - { + UpdateRender() { this.NeedUpdate = true; } - - /** - * - * @param {HTMLElement} canvasContainer 可以传入一个div或者一个画布 - * @memberof Viewer - */ - constructor(canvasContainer: HTMLElement) { + constructor(canvasContainer: HTMLElement) { this.m_DomEl = canvasContainer; this.initRender(canvasContainer); - this.OnSize(); - this.StartRender(); - window.addEventListener("resize", () => { - this.OnSize(); - }); + this.SetSize(); // 初始尺寸设置 + this.StartRender(); // 开始渲染循环 + + // 注意: OnSize 是一个箭头函数属性,它的 this 上下文是固定的 + // 因此可以直接用于添加和移除事件监听器 + window.addEventListener("resize", this.OnResize); this.InitCamera(); } - //初始化render initRender(canvasContainer: HTMLElement) { let params: WebGLRendererParameters = { - antialias: true,//antialias:true/false是否开启反锯齿 - precision: "highp",//precision:highp/mediump/lowp着色精度选择 - alpha: true//alpha:true/false是否可以设置背景色透明 + antialias: true, + precision: "highp", + alpha: true }; this.Renderer = new WebGLRenderer(params); - //加到画布 canvasContainer.appendChild(this.Renderer.domElement); - this.Renderer.autoClear = true; - - //如果设置,那么它希望所有的纹理和颜色都是预乘的伽玛。默认值为false。 - // this.Renderer.gammaInput = true; - // this.Renderer.gammaOutput = true; - // this.Renderer.shadowMap.enabled = true; - // this.Renderer.toneMapping = ReinhardToneMapping; - //设置设备像素比。 这通常用于HiDPI设备,以防止模糊输出画布。 this.Renderer.setPixelRatio(window.devicePixelRatio); - this.Renderer.physicallyCorrectLights = true; - //this.Renderer.toneMappingExposure = Math.pow(1, 5.0); // to allow for very bright scenes. - - //设置它的背景色为黑色 + this.Renderer.physicallyCorrectLights = true; // r150 后默认 false, 根据需要设置 this.Renderer.setClearColor(0xffffff, 1); - - - this.OnSize(); + // this.OnSize(); // 在构造函数中调用,这里可以不用重复 } InitCamera() { this.m_Camera.CameraType = CameraType.PerspectiveCamera; this.m_Camera.Near = 0.001; - this.m_Camera.Camera.far = 1000000000; + if (this.m_Camera.Camera) { // 确保 CameraUpdate 内部的 Camera 已初始化 + this.m_Camera.Camera.far = 1000000000; + } } - OnSize = (width?: number, height?: number) => { + + OnResize = (evt: UIEvent) => this.SetSize(); + + SetSize = (width?: number, height?: number) => { + if (this.m_IsDisposed || !this.m_DomEl) return; // 如果已销毁或容器不存在,则不执行 + this._Width = width ? width : this.m_DomEl.clientWidth; this._Height = height ? height : this.m_DomEl.clientHeight; - //校验.成为2的倍数 避免外轮廓错误. - if (this._Width % 2 == 1) - this._Width -= 1; - if (this._Height % 2 == 1) - this._Height -= 1; + if (this._Width % 2 === 1) this._Width -= 1; + if (this._Height % 2 === 1) this._Height -= 1; - this.Renderer.setSize(this._Width, this._Height); - this.m_Camera.SetSize(this._Width, this._Height); + if (this.Renderer) { + this.Renderer.setSize(this._Width, this._Height); + } + if (this.m_Camera) { + this.m_Camera.SetSize(this._Width, this._Height); + } + this.NeedUpdate = true; // 尺寸变化后需要重新渲染 }; StartRender = () => { - requestAnimationFrame(this.StartRender); - if (this._Scene != null && this.NeedUpdate) { + if (this.m_IsDisposed) { + if (this.m_AnimationFrameId !== null) { + cancelAnimationFrame(this.m_AnimationFrameId); + this.m_AnimationFrameId = null; + } + return; + } + this.m_AnimationFrameId = requestAnimationFrame(this.StartRender); + + if (this._Scene && this.NeedUpdate) { // 确保 _Scene 存在 this.Render(); - this.m_Camera.Update(); + if (this.m_Camera) { // 确保 m_Camera 存在 + this.m_Camera.Update(); + } this.NeedUpdate = false; } }; + Render() { + if (this.m_IsDisposed || !this.Renderer || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) { + return; + } this.Renderer.render(this._Scene, this.m_Camera.Camera); } + // ScreenToWorld, WorldToScreen, UpdateLockTarget, Rotate, Pan, Zoom, etc. methods remain the same... + // ... (省略了您提供的其他方法,它们与 dispose 无直接关系,保持不变) ... ScreenToWorld(pt: Vector3, planVec?: Vector3) { + if (this.m_IsDisposed || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) return; //变换和求交点 let plan = new PlaneExt(planVec || new Vector3(0, 0, 1)); let raycaster = new Raycaster(); @@ -123,9 +133,10 @@ export class Viewer { } , this.m_Camera.Camera ); - plan.intersectRay(raycaster.ray, pt, true); + plan.intersectRay(raycaster.ray, pt, true); // 假设 intersectRay 修改 pt } WorldToScreen(pt: Vector3) { + if (this.m_IsDisposed || !this.m_Camera || !this.m_Camera.Camera) return; let widthHalf = this._Width * 0.5; let heightHalf = this._Height * 0.5; @@ -135,51 +146,133 @@ export class Viewer { pt.y = - (pt.y * heightHalf) + heightHalf; } - /** - * 更新视角观测目标(物体中心) - * - * @memberof Viewer - */ UpdateLockTarget() { - let renderList = this.Renderer.renderLists.get(this._Scene, this.m_Camera.Camera); - let box = GetBoxArr(renderList.opaque.map(o => o.object)); + if (this.m_IsDisposed || !this.Renderer || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) return; + // @ts-ignore access to private renderLists + const renderList = this.Renderer.renderLists.get(this._Scene, this.m_Camera.Camera); + // @ts-ignore + const box = GetBoxArr(renderList.opaque.map(o => o.object)); // 确保 GetBoxArr 定义正确 if (box) + // @ts-ignore this.m_LookTarget = box.getCenter(new Vector3()); else - this.m_LookTarget = cZeroVec; + // @ts-ignore + this.m_LookTarget = cZeroVec; // 确保 cZeroVec 定义正确 } Rotate(mouseMove: Vector3) { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.Rotate(mouseMove, this.m_LookTarget); this.NeedUpdate = true; } RotateAround(movement: Vector3, center: Vector3) { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.Rotate(movement, center); this.NeedUpdate = true; } Pan(mouseMove: Vector3) { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.Pan(mouseMove); this.NeedUpdate = true; } Zoom(scale: number, center?: Vector3) { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.Zoom(scale, center); this.NeedUpdate = true; } - ZoomAll() - { - this.m_Camera.ZoomExtentsBox3(GetBox(this._Scene, true)); + ZoomAll() { + if (this.m_IsDisposed || !this.m_Camera || !this._Scene) return; + // @ts-ignore + this.m_Camera.ZoomExtentsBox3(GetBox(this._Scene, true)); // 确保 GetBox 定义正确 this.NeedUpdate = true; } ViewToTop() { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.LookAt(new Vector3(0, 0, -1)); this.NeedUpdate = true; } ViewToFront() { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.LookAt(new Vector3(0, 1, 0)); this.NeedUpdate = true; } ViewToSwiso() { + if (this.m_IsDisposed || !this.m_Camera) return; this.m_Camera.LookAt(new Vector3(1, 1, -1)); this.NeedUpdate = true; } -} + + + /** + * 销毁 Viewer 实例并释放所有相关资源。 + */ + public Dispose(): void { + if (this.m_IsDisposed) { + return; // 防止重复释放 + } + + console.log("Disposing Viewer..."); + + // 1. 停止渲染循环 + this.m_IsDisposed = true; // 设置标志,渲染循环将在下一帧停止 + if (this.m_AnimationFrameId !== null) { + cancelAnimationFrame(this.m_AnimationFrameId); + this.m_AnimationFrameId = null; + } + + + // 2. 移除事件监听器 + window.removeEventListener("resize", this.OnResize); + // 如果 m_DomEl 上有其他直接添加的事件监听器,也应在此移除 + + // 3. 销毁场景内容 (几何体、材质、纹理) + if (this._Scene) { + // 遍历场景中的所有对象并释放它们的资源 + this._Scene.traverse((object) => { + disposeNode(object); + }); + + // 从场景中移除所有子对象 + // scene.clear() 也可以,但在此之前我们已经遍历并释放了资源 + while (this._Scene.children.length > 0) { + const child = this._Scene.children[0]; + // 确保子对象的资源已通过上面的 traverse 处理 + this._Scene.remove(child); + } + // console.log("Scene cleared and resources disposed."); + } + + + // 4. 销毁渲染器 + if (this.Renderer) { + // @ts-ignore access to private renderLists + if (this.Renderer.renderLists) { // 检查 renderLists 是否存在 + // @ts-ignore + this.Renderer.renderLists.dispose(); + // console.log("Renderer renderLists disposed."); + } + this.Renderer.dispose(); + // console.log("Renderer disposed."); + + // 从 DOM 中移除渲染器的 canvas 元素 + if (this.Renderer.domElement && this.Renderer.domElement.parentElement) { + this.Renderer.domElement.parentElement.removeChild(this.Renderer.domElement); + // console.log("Renderer DOM element removed."); + } + } + + // 5. 清除引用 (帮助垃圾回收) + // @ts-ignore + this._Scene = null; + // @ts-ignore + this.Renderer = null; + // @ts-ignore + this.m_DomEl = null; + // @ts-ignore + this.m_Camera = null; + // @ts-ignore + this.m_LookTarget = null; + + console.log("Viewer disposed successfully."); + } +} \ No newline at end of file diff --git a/src/components/MaterialView.vue b/src/components/MaterialView.vue index 8ac73e3..3053c03 100644 --- a/src/components/MaterialView.vue +++ b/src/components/MaterialView.vue @@ -5,7 +5,7 @@