diff --git a/packages/editor/src/components/code/CodeEditor.vue b/packages/editor/src/components/code/CodeEditor.vue index 8d1275b..679801e 100644 --- a/packages/editor/src/components/code/CodeEditor.vue +++ b/packages/editor/src/components/code/CodeEditor.vue @@ -78,6 +78,7 @@ async function initMonaco() { monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false, + diagnosticCodesToIgnore: [80002], }); // 如果使用 Webpack 或其他打包工具,可以使用 importScripts monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ diff --git a/packages/sdk/lib/core/viewer/Viewer.ts b/packages/sdk/lib/core/viewer/Viewer.ts index 29164de..82ba221 100644 --- a/packages/sdk/lib/core/viewer/Viewer.ts +++ b/packages/sdk/lib/core/viewer/Viewer.ts @@ -119,6 +119,11 @@ export interface ViewerModules { tilesManage:TilesManage, } +export interface InstallScriptsOptions { + invokeLoaded?: boolean; + loadedScriptNames?: string[]; +} + CameraControls.install({ THREE: { Vector2: THREE.Vector2, @@ -140,25 +145,14 @@ const onDoubleClickPosition = new THREE.Vector2(); // 表示animate()函数被多次调用累积时间,用于限制FPS let timeStamp = 0; -// 事件绑定 -const Fn: any = { - pointerdown: null, - pointerup: null, - pointermove: null, - keydown: null, - keyup: null, - touchstart: null, - dblclick: null, -} - // 脚本管理数据结构 type EventHandlers = { [eventName: string]: { [uuid: string]: Function[]; }; }; -// 脚本中可写的所有事件 -let events: EventHandlers = { + +const createScriptEvents = (): EventHandlers => ({ loaded: {}, beforeAnimation: {}, afterAnimation: {}, @@ -168,6 +162,7 @@ let events: EventHandlers = { afterDestroy: {}, onPick: {}, onDoubleClick: {}, + bindDataChange: {}, onKeyDown: {}, onKeyUp: {}, onPointerDown: {}, @@ -175,9 +170,7 @@ let events: EventHandlers = { onPointerMove: {}, onTouchStart: {}, onTouchEnd: {}, -}; -// UUID 到事件的映射 -const uuidEventMap: Map> = new Map(); +}); export default class Viewer extends THREE.EventDispatcher { public container: HTMLElement; @@ -197,6 +190,18 @@ export default class Viewer extends THREE.EventDispatcher { public css2DRenderer: CSS2DRenderer = new CSS2DRenderer(); public css3DRenderer: CSS3DRenderer = new CSS3DRenderer(); public timer = new Timer(); + public events: EventHandlers = createScriptEvents(); + public uuidEventMap: Map> = new Map(); + public fns: any = { + mousedown: null, + pointerdown: null, + pointerup: null, + pointermove: null, + keydown: null, + keyup: null, + touchstart: null, + dblclick: null, + }; //整个主场景的box3 public sceneBox3 = new THREE.Box3(); public package: Package; @@ -362,6 +367,8 @@ export default class Viewer extends THREE.EventDispatcher { set enableScript(enable: boolean) { if (enable === this.enableScript) return; + this.options.enableScript = enable; + if (enable) { this.installScripts(); } else { @@ -591,14 +598,14 @@ export default class Viewer extends THREE.EventDispatcher { * 初始化事件监听 */ initEvent() { - Fn.pointerdown = this.onPointerDown.bind(this); - this.container.addEventListener('pointerdown', Fn.pointerdown); - Fn.pointermove = this.onPointerMove.bind(this); - this.container.addEventListener('pointermove', Fn.pointermove); - Fn.touchstart = this.onTouchStart.bind(this); - this.container.addEventListener('touchstart', Fn.touchstart); - Fn.dblclick = this.onDoubleClick.bind(this) - this.container.addEventListener('dblclick', Fn.dblclick); + this.fns.pointerdown = this.onPointerDown.bind(this); + this.container.addEventListener('pointerdown', this.fns.pointerdown); + this.fns.pointermove = this.onPointerMove.bind(this); + this.container.addEventListener('pointermove', this.fns.pointermove); + this.fns.touchstart = this.onTouchStart.bind(this); + this.container.addEventListener('touchstart', this.fns.touchstart); + this.fns.dblclick = this.onDoubleClick.bind(this) + this.container.addEventListener('dblclick', this.fns.dblclick); } /** @@ -606,9 +613,13 @@ export default class Viewer extends THREE.EventDispatcher { * @param uuids 传入此参数则仅组装此数组下Object.uuid的脚本 * @param filterName 传入此参数则仅组装此数组下Object.uuid的脚本中name匹配的脚本 */ - installScripts(uuids?: string | string[], filterName: string = "") { + installScripts(uuids?: string | string[], filterName: string = "", options: InstallScriptsOptions = {}) { if (!this.enableScript) return; + const { invokeLoaded = false, loadedScriptNames = [] } = options; + const loadedScriptNameSet = new Set(loadedScriptNames.filter(Boolean)); + const shouldInvokeLoadedAfterInstall = invokeLoaded || loadedScriptNameSet.size > 0; + // 注册 Helper const helper = new ScriptHelper(this.scene); @@ -622,7 +633,7 @@ export default class Viewer extends THREE.EventDispatcher { } // 拼接下方闭包函数返回的结构,即返回脚本中写的支持的事件函数 - const validEvents = Object.keys(events); + const validEvents = Object.keys(this.events); // 准备返回结构 validEvents.forEach(eventName => { @@ -646,7 +657,8 @@ export default class Viewer extends THREE.EventDispatcher { // 一个模型允许存在多个脚本 const scripts = App.scripts[uuid] || []; - const uuidEvents = uuidEventMap.get(uuid) || new Map(); + const uuidEvents = this.uuidEventMap.get(uuid) || new Map(); + const scriptsNeedLoaded = new Set(); scripts.forEach(script => { // 如果存在需要按照name过滤 @@ -660,6 +672,11 @@ export default class Viewer extends THREE.EventDispatcher { this.camera, this.modules.controls, this.timer, fns.render ); + const shouldInvokeLoaded = loadedScriptNameSet.size > 0 ? loadedScriptNameSet.has(script.name) : invokeLoaded; + if (shouldInvokeLoaded && typeof functions.loaded === "function") { + scriptsNeedLoaded.add(script.name); + } + Object.entries(functions).forEach(([name, fn]) => { if (!fn || !validEvents.includes(name)) { if (fn && !validEvents.includes(name)) { @@ -672,12 +689,16 @@ export default class Viewer extends THREE.EventDispatcher { const {type, target, ...params} = e; // 点击事件只分发给对应模型 - if (["onPick", "onDoubleClick"].includes(name)) { - const {intersect, object: _object} = params; + if (["onPick", "onDoubleClick", "bindDataChange"].includes(name)) { + const {intersect, object: _object, data, config, index} = params; if (_object.uuid !== object.uuid) return; - (fn as Function).bind(object)(intersect as THREE.Intersection); + if (name === "bindDataChange") { + (fn as Function).bind(object)(data, config, index); + } else { + (fn as Function).bind(object)(intersect as THREE.Intersection); + } } else { if (isEmptyObject(params)) { (fn as Function).bind(object)(); @@ -688,8 +709,8 @@ export default class Viewer extends THREE.EventDispatcher { } // 添加到全局事件集合 - if (!events[name][uuid]) events[name][uuid] = []; - events[name][uuid].push(boundFn); + if (!this.events[name][uuid]) this.events[name][uuid] = []; + this.events[name][uuid].push(boundFn); // 添加到 UUID 事件映射 if (!uuidEvents.has(name)) uuidEvents.set(name, []); @@ -704,7 +725,13 @@ export default class Viewer extends THREE.EventDispatcher { }); // 更新 UUID 映射 - uuidEventMap.set(uuid, uuidEvents); + this.uuidEventMap.set(uuid, uuidEvents); + + if (shouldInvokeLoadedAfterInstall) { + scriptsNeedLoaded.forEach(scriptName => { + this.invokeInstalledScriptEvent(uuid, scriptName, "loaded"); + }); + } }; // 处理指定 UUID 或全部 @@ -714,15 +741,15 @@ export default class Viewer extends THREE.EventDispatcher { Object.keys(App.scripts).forEach(processUuid); } - if (!Fn.keydown) { - Fn.keydown = (event: KeyboardEvent) => { + if (!this.fns.keydown) { + this.fns.keydown = (event: KeyboardEvent) => { this.dispatchEvent({type: "onKeyDown", event}) } - window.addEventListener('keydown', Fn.keydown); - Fn.keyup = (event: KeyboardEvent) => { + window.addEventListener('keydown', this.fns.keydown); + this.fns.keyup = (event: KeyboardEvent) => { this.dispatchEvent({type: "onKeyUp", event}) } - window.addEventListener('keyup', Fn.keyup); + window.addEventListener('keyup', this.fns.keyup); } } @@ -732,9 +759,9 @@ export default class Viewer extends THREE.EventDispatcher { * @param filterName 传入此参数则仅卸载此Object.uuid的脚本中name匹配的脚本 */ uninstallScriptsByUuid(uuid: string, filterName: string = "") { - if (!uuidEventMap.has(uuid)) return; + if (!this.uuidEventMap.has(uuid)) return; - const uuidEvents = uuidEventMap.get(uuid)!; + const uuidEvents = this.uuidEventMap.get(uuid)!; const uuidEventsArray = Array.from(uuidEvents); for (let i = uuidEventsArray.length - 1; i >= 0; i--) { @@ -756,7 +783,7 @@ export default class Viewer extends THREE.EventDispatcher { } // 全局事件集合 - const es = events[eventName][uuid]; + const es = this.events[eventName][uuid]; // 移除相应函数 const ei = es.findIndex(f => f === sc.fn); if (ei !== -1) { @@ -764,40 +791,79 @@ export default class Viewer extends THREE.EventDispatcher { } if (es.length === 0) { - delete events[eventName][uuid]; + delete this.events[eventName][uuid]; } } } // 清理 UUID 映射 if (Array.from(uuidEvents.keys()).length === 0) { - uuidEventMap.delete(uuid); + this.uuidEventMap.delete(uuid); } } + invokeInstalledScriptEvent( + uuid: string, + scriptName: string, + eventName: T, + payload: Partial = {} + ): boolean { + const uuidEvents = this.uuidEventMap.get(uuid); + if (!uuidEvents) return false; + + const handlers = uuidEvents.get(String(eventName)); + if (!handlers || handlers.length === 0) return false; + + let executed = false; + handlers.forEach(handler => { + if (handler.name !== scriptName) return; + + try { + handler.fn({ + ...payload, + type: eventName, + target: this, + }); + executed = true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + App.log.error(`[Script] 执行 ${scriptName}.${String(eventName)} 失败: ${message}`); + } + }); + + return executed; + } + + reinstallObjectScripts(uuid: string, loadedScriptNames: string[] = []): void { + if (!uuid) return; + + const uniqueLoadedScriptNames = Array.from(new Set(loadedScriptNames.filter(Boolean))); + + this.uninstallScriptsByUuid(uuid); + this.installScripts([uuid], "", uniqueLoadedScriptNames.length > 0 ? { loadedScriptNames: uniqueLoadedScriptNames } : undefined); + } + /** * 卸载所有脚本 */ unInstallScripts() { - if (this.enableScript) return; - // 直接遍历 UUID 映射,复杂度 O(n) - uuidEventMap.forEach((_, uuid) => { + this.uuidEventMap.forEach((_, uuid) => { this.uninstallScriptsByUuid(uuid); }); // 重置数据结构 - uuidEventMap.clear(); - Object.keys(events).forEach(event => { - events[event] = {}; + this.uuidEventMap.clear(); + Object.keys(this.events).forEach(event => { + this.events[event] = {}; }); - if (Fn.keydown) { - window.removeEventListener('keydown', Fn.keydown); - Fn.keydown = null; + if (this.fns.keydown) { + window.removeEventListener('keydown', this.fns.keydown); + this.fns.keydown = null; - window.removeEventListener('keyup', Fn.keyup); - Fn.keyup = null; + window.removeEventListener('keyup', this.fns.keyup); + this.fns.keyup = null; } } @@ -904,8 +970,8 @@ export default class Viewer extends THREE.EventDispatcher { event.preventDefault(); const array = getMousePosition(this.container, event.clientX, event.clientY); onDownPosition.fromArray(array); - Fn.pointerup = this.onPointerUp.bind(this); - document.addEventListener('pointerup', Fn.pointerup); + this.fns.pointerup = this.onPointerUp.bind(this); + document.addEventListener('pointerup', this.fns.pointerup); } /** @@ -918,8 +984,8 @@ export default class Viewer extends THREE.EventDispatcher { const array = getMousePosition(this.container, event.clientX, event.clientY); onUpPosition.fromArray(array); this.handleClick(); - document.removeEventListener('pointerup', Fn.pointerup); - Fn.pointerup = null; + document.removeEventListener('pointerup', this.fns.pointerup); + this.fns.pointerup = null; } /** @@ -940,8 +1006,8 @@ export default class Viewer extends THREE.EventDispatcher { const touch = event.changedTouches[0]; const array = getMousePosition(this.container, touch.clientX, touch.clientY); onDownPosition.fromArray(array); - Fn.pointerup = this.onTouchEnd.bind(this); - document.addEventListener('touchend', Fn.pointerup); + this.fns.pointerup = this.onTouchEnd.bind(this); + document.addEventListener('touchend', this.fns.pointerup); } /** @@ -955,8 +1021,8 @@ export default class Viewer extends THREE.EventDispatcher { const array = getMousePosition(this.container, touch.clientX, touch.clientY); onUpPosition.fromArray(array); this.handleClick(); - document.removeEventListener('touchend', Fn.pointerup); - Fn.pointerup = null; + document.removeEventListener('touchend', this.fns.pointerup); + this.fns.pointerup = null; } /** @@ -1133,14 +1199,14 @@ export default class Viewer extends THREE.EventDispatcher { dispose() { this.dispatchEvent({type: "beforeDestroy"}); - this.container.removeEventListener('mousedown', Fn.mousedown); - Fn.mousedown = null; - this.container.removeEventListener('pointermove', Fn.pointermove); - Fn.pointermove = null; - this.container.removeEventListener('touchstart', Fn.touchstart); - Fn.touchstart = null; - this.container.removeEventListener('dblclick', Fn.dblclick); - Fn.dblclick = null; + this.container.removeEventListener('mousedown', this.fns.mousedown); + this.fns.mousedown = null; + this.container.removeEventListener('pointermove', this.fns.pointermove); + this.fns.pointermove = null; + this.container.removeEventListener('touchstart', this.fns.touchstart); + this.fns.touchstart = null; + this.container.removeEventListener('dblclick', this.fns.dblclick); + this.fns.dblclick = null; Object.keys(this.modules).forEach(key => { if (this.modules[key].dispose) { diff --git a/packages/sdk/lib/core/viewer/modules/Signals.ts b/packages/sdk/lib/core/viewer/modules/Signals.ts index 06cf976..9886115 100644 --- a/packages/sdk/lib/core/viewer/modules/Signals.ts +++ b/packages/sdk/lib/core/viewer/modules/Signals.ts @@ -2,15 +2,21 @@ import * as THREE from "three"; import {RoomEnvironment} from "three/examples/jsm/environments/RoomEnvironment.js"; import {ShaderPass} from "three/examples/jsm/postprocessing/ShaderPass.js"; import {Effect} from "./Effect"; -import {useAddSignal} from "@/hooks"; +import {useAddSignal, useRemoveSignal} from "@/hooks"; import Viewer from "../Viewer"; import App from "@/core/app/App"; import {focusObject} from "@/utils/scene/controls.ts"; +type SignalListenerRecord = { + name: string; + listener: (...params: any[]) => void; +}; + export class Signals { private readonly viewer: Viewer; private useBackgroundAsEnvironment = false; + private readonly signalListeners: SignalListenerRecord[] = []; constructor(viewer:Viewer) { this.viewer = viewer; @@ -18,41 +24,46 @@ export class Signals { this.init(); } + private registerSignal(name: string, listener: (...params: any[]) => void) { + this.signalListeners.push({ name, listener }); + useAddSignal(name, listener); + } + init() { - useAddSignal("sceneCleared", this.sceneCleared.bind(this)); - useAddSignal("transformModeChanged", this.transformModeChanged.bind(this)); - useAddSignal("snapChanged", this.snapChanged.bind(this)); - useAddSignal("spaceChanged", this.spaceChanged.bind(this)); - useAddSignal("effectEnabledChange", this.effectEnabledChange.bind(this)); + this.registerSignal("sceneCleared", this.sceneCleared.bind(this)); + this.registerSignal("transformModeChanged", this.transformModeChanged.bind(this)); + this.registerSignal("snapChanged", this.snapChanged.bind(this)); + this.registerSignal("spaceChanged", this.spaceChanged.bind(this)); + this.registerSignal("effectEnabledChange", this.effectEnabledChange.bind(this)); - useAddSignal("rendererUpdated", this.rendererUpdated.bind(this)); - useAddSignal("rendererCreated", this.rendererCreated.bind(this)); - useAddSignal("rendererConfigUpdate", this.rendererConfigUpdate.bind(this)); - useAddSignal("rendererDetectKTX2Support", this.rendererDetectKTX2Support.bind(this)); + this.registerSignal("rendererUpdated", this.rendererUpdated.bind(this)); + this.registerSignal("rendererCreated", this.rendererCreated.bind(this)); + this.registerSignal("rendererConfigUpdate", this.rendererConfigUpdate.bind(this)); + this.registerSignal("rendererDetectKTX2Support", this.rendererDetectKTX2Support.bind(this)); - useAddSignal("sceneBackgroundChanged", this.sceneBackgroundChanged.bind(this)); - useAddSignal("sceneEnvironmentChanged", this.sceneEnvironmentChanged.bind(this)); - useAddSignal("sceneGraphChanged", this.sceneGraphChanged.bind(this)); - useAddSignal("cameraChanged", this.cameraChanged.bind(this)); - useAddSignal("cameraReset", this.viewer.updateAspectRatio.bind(this.viewer)); - useAddSignal("viewportCameraChanged", this.viewportCameraChanged.bind(this)); - useAddSignal("viewportShadingChanged", this.viewportShadingChanged.bind(this)); + this.registerSignal("sceneBackgroundChanged", this.sceneBackgroundChanged.bind(this)); + this.registerSignal("sceneEnvironmentChanged", this.sceneEnvironmentChanged.bind(this)); + this.registerSignal("sceneGraphChanged", this.sceneGraphChanged.bind(this)); + this.registerSignal("cameraChanged", this.cameraChanged.bind(this)); + this.registerSignal("cameraReset", this.viewer.updateAspectRatio.bind(this.viewer)); + this.registerSignal("viewportCameraChanged", this.viewportCameraChanged.bind(this)); + this.registerSignal("viewportShadingChanged", this.viewportShadingChanged.bind(this)); - useAddSignal("objectSelected", this.objectSelected.bind(this)); - useAddSignal("objectFocused", this.objectFocused.bind(this)); - useAddSignal("objectAdded", this.objectAdded.bind(this)); - useAddSignal("objectChanged", this.objectChanged.bind(this)); - useAddSignal("objectRemoved", this.objectRemoved.bind(this)); + this.registerSignal("objectSelected", this.objectSelected.bind(this)); + this.registerSignal("objectFocused", this.objectFocused.bind(this)); + this.registerSignal("objectAdded", this.objectAdded.bind(this)); + this.registerSignal("objectChanged", this.objectChanged.bind(this)); + this.registerSignal("objectRemoved", this.objectRemoved.bind(this)); - useAddSignal("geometryChanged", this.geometryChanged.bind(this)); - useAddSignal("materialChanged", this.materialChanged.bind(this)); + this.registerSignal("geometryChanged", this.geometryChanged.bind(this)); + this.registerSignal("materialChanged", this.materialChanged.bind(this)); - useAddSignal("sceneResize", this.sceneResize.bind(this)); - useAddSignal("showGridChanged", this.showGridChanged.bind(this)); + this.registerSignal("sceneResize", this.sceneResize.bind(this)); + this.registerSignal("showGridChanged", this.showGridChanged.bind(this)); - useAddSignal("scriptAdded",this.scriptAdded.bind(this)); - useAddSignal("scriptRemoved",this.scriptRemoved.bind(this)); - useAddSignal("scriptChanged",this.scriptChanged.bind(this)); + this.registerSignal("scriptAdded",this.scriptAdded.bind(this)); + this.registerSignal("scriptRemoved",this.scriptRemoved.bind(this)); + this.registerSignal("scriptChanged",this.scriptChanged.bind(this)); } /** @@ -66,6 +77,8 @@ export class Signals { * 清空 */ sceneCleared() { + this.viewer.unInstallScripts(); + this.viewer.modules.controls.setTarget(0, 0, 0,true); this.viewer.pathtracer?.reset(); @@ -453,15 +466,29 @@ export class Signals { /** * 添加脚本 */ - scriptAdded(object:THREE.Object3D, _:ISceneScript){ - this.viewer.installScripts([object.uuid]); + scriptAdded(object:THREE.Object3D, sc:ISceneScript){ + if (!object?.uuid || !sc?.name) return; + + try { + this.viewer.reinstallObjectScripts(object.uuid, [sc.name]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + App.log.error(`[Script] 热安装脚本 ${sc.name} 失败: ${message}`); + } } /** * 移除脚本 */ scriptRemoved(object:THREE.Object3D, sc:ISceneScript){ - this.viewer.uninstallScriptsByUuid(object.uuid,sc.name); + if (!object?.uuid) return; + + try { + this.viewer.reinstallObjectScripts(object.uuid); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + App.log.error(`[Script] 卸载脚本 ${sc?.name || "unknown"} 失败: ${message}`); + } } /** @@ -469,8 +496,14 @@ export class Signals { */ scriptChanged(attributeName:string,object:THREE.Object3D, sc:ISceneScript){ if(attributeName !== "source") return; + if (!object?.uuid || !sc?.name) return; - this.viewer.installScripts([object.uuid],sc.name); + try { + this.viewer.reinstallObjectScripts(object.uuid, [sc.name]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + App.log.error(`[Script] 热更新脚本 ${sc.name} 失败: ${message}`); + } } /** @@ -479,4 +512,11 @@ export class Signals { render(){ this.viewer.render(); } + + dispose() { + this.signalListeners.forEach(({ name, listener }) => { + useRemoveSignal(name, listener); + }); + this.signalListeners.length = 0; + } } \ No newline at end of file