TkAstral3D/packages/sdk/lib/core/objects/UIPanelText.ts
2026-04-08 15:34:43 +08:00

200 lines
6.9 KiB
TypeScript

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<string, any>) => THREE.Object3D;
export default class UIPanelText extends TextBase {
type = "UIPanelText";
isUIPanelElement = true;
options: UIPanelNode;
declare set: (options: Record<string, any>) => 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<UIPanelNode> = {}, 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<UIPanelNode>) {
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<string, any>) {
this.controller.setProps(patch);
if (Object.prototype.hasOwnProperty.call(patch, "textContent")) {
this.syncAnonymousInlineProxy();
}
}
setStates(states?: Record<string, Record<string, any>>) {
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;
}
}
}