200 lines
6.9 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|