import * as THREE from "three"; import { Text } from "@/core/libs/three-mesh-ui/src/three-mesh-ui.js"; import App from "@/core/app/App"; import { findUIPanelRoot } from "@/utils/scene/uipanel"; import { UIPanelElementController, UIPanelElementInit, UIPanelNode, extractUIPanelProps, normalizeUIPanelElementOptions, } from "./UIPanelElementBase"; const TextBase = Text as unknown as new (options?: Record) => THREE.Object3D; export default class UIPanelText extends TextBase { type = "UIPanelText"; isUIPanelElement = true; options: UIPanelNode; declare set: (options: Record) => void; declare addAfterUpdate: (fn: () => void) => void; declare removeAfterUpdate: (fn: () => void) => void; private controller: UIPanelElementController; private inlineRebuildQueued = false; private handleAdded = () => { const parent = this.parent as any; if (!parent || !this.isValidParent(parent)) { parent?.remove?.(this); return; } const panel = findUIPanelRoot(this) as any; if (!panel) return; this.controller.updateRootId(panel.uuid); panel.registerElement?.(this); panel.requestUpdate?.(); this.syncAnonymousInlineProxy(); }; private handleRemoved = () => { const rootId = (this as any).metadata?.__uiPanelRootId; const panel = rootId ? App.getObjectByUuid(rootId) : null; panel?.unregisterElement?.(this); }; private handleAfterUpdate = () => { if (this.reconcileAnonymousInlineChildren()) { return; } this.syncAnonymousInlineProxy(); }; constructor(options: Partial = {}, init: UIPanelElementInit = {}) { const normalized = normalizeUIPanelElementOptions(options, "text", "Text"); const { props } = extractUIPanelProps(normalized); super(props); this.options = normalized; this.name = normalized.name || "Text"; this.controller = new UIPanelElementController(this, this.options, this.type, init); this.addEventListener("added", this.handleAdded); this.addEventListener("removed", this.handleRemoved); this.addAfterUpdate?.(this.handleAfterUpdate); } updateRootId(rootId: string) { this.controller.updateRootId(rootId); } updateOptions(options: Partial) { if (options.name !== undefined) { this.options.name = options.name; this.name = options.name || this.name; } if (options.props) this.setProps(options.props); if (options.states !== undefined) this.setStates(options.states); } setProps(patch: Record) { this.controller.setProps(patch); if (Object.prototype.hasOwnProperty.call(patch, "textContent")) { this.syncAnonymousInlineProxy(); } } setStates(states?: Record>) { this.controller.setStates(states); const rootId = (this as any).metadata?.__uiPanelRootId; const panel = rootId ? App.getObjectByUuid(rootId) : null; panel?.refreshInteraction?.(); } applyState(state: string | null) { this.controller.applyState(state); } hasInteractiveState() { return this.controller.hasInteractiveState(); } dispose() { this.controller.dispose(); this.removeAfterUpdate?.(this.handleAfterUpdate); } toJSON(meta?: THREE.JSONMeta) { const snapshot = this.controller.detachInternalMeshes(); const inlineSnapshot = this.detachAnonymousInlineChildren(); const data = super.toJSON(meta) as any; this.restoreAnonymousInlineChildren(inlineSnapshot); this.controller.restoreInternalMeshes(snapshot); data.object.type = this.type; const options = JSON.parse(JSON.stringify(this.options)) as UIPanelNode; options.name = this.name; data.object.options = options; return data; } static fromJSON(json: { options: UIPanelNode }, init: UIPanelElementInit = {}) { return new UIPanelText(json.options, init); } private isValidParent(parent: any) { return parent?.type === "UIPanel" || parent?.type === "UIPanelBlock"; } private syncAnonymousInlineProxy() { this.children.forEach(child => { if (!this.isAnonymousInlineChild(child)) return; this.enableAnonymousInlineRaycast(child as any); child.traverse(node => { const anyNode = node as any; if (!anyNode.proxy) { anyNode.proxy = this; } }); }); } private reconcileAnonymousInlineChildren() { const anonymousChildren = this.children.filter(child => this.isAnonymousInlineChild(child)); if (anonymousChildren.length === 0) return false; const hasInline = anonymousChildren.some(child => (child as any).isInline); if (anonymousChildren.length === 1 && hasInline) { return false; } anonymousChildren.forEach(child => { this.remove(child); (child as any).clear?.(); }); const textProp = (this as any)._textContent; if (textProp) { textProp._needsUpdate = true; } this.queueInlineRebuild(); return true; } private queueInlineRebuild() { if (this.inlineRebuildQueued) return; this.inlineRebuildQueued = true; queueMicrotask(() => { this.inlineRebuildQueued = false; const panel = findUIPanelRoot(this) as any; panel?.requestUpdate?.(); }); } private detachAnonymousInlineChildren() { const snapshot: Array<{ child: THREE.Object3D; index: number }> = []; for (let i = this.children.length - 1; i >= 0; i--) { const child = this.children[i]; if (!this.isAnonymousInlineChild(child)) continue; this.children.splice(i, 1); child.parent = null; snapshot.push({ child, index: i }); } return snapshot; } private restoreAnonymousInlineChildren(snapshot: Array<{ child: THREE.Object3D; index: number }>) { if (!snapshot.length) return; snapshot.sort((a, b) => a.index - b.index).forEach(({ child, index }) => { if (child.parent === this) return; const targetIndex = Math.min(Math.max(index, 0), this.children.length); this.children.splice(targetIndex, 0, child); child.parent = this; }); } private isAnonymousInlineChild(child: THREE.Object3D) { return (child as any).name === "anonymousInline"; } private enableAnonymousInlineRaycast(child: any) { const fontMesh = child?._fontMesh; if (fontMesh?.isMesh) { fontMesh.raycast = THREE.Mesh.prototype.raycast; } } }