This commit is contained in:
yinsx
2026-01-05 10:14:33 +08:00
commit cf0b049abf
33 changed files with 3130 additions and 0 deletions

50
src/utils/Dictionary.ts Normal file
View File

@ -0,0 +1,50 @@
/**
* @file Dictionary.js
* @description 字典工具类 - 键值对存储
*/
/**
* 字典类 - 提供类似Map的键值对存储功能
*/
export class Dictionary<T = unknown> {
private items: Record<string, T> = {};
/** 检查是否存在指定键 */
Has(key: string): boolean {
return key in this.items;
}
/** 设置键值对 */
Set(key: string, value: T): void {
this.items[key] = value;
}
/** 移除指定键 */
Remove(key: string): boolean {
if (this.Has(key)) {
delete this.items[key];
return true;
}
return false;
}
/** 获取指定键的值 */
Get(key: string): T | undefined {
return this.Has(key) ? this.items[key] : undefined;
}
/** 获取所有键 */
Keys(): string[] {
return Object.keys(this.items);
}
/** 获取所有值 */
Values(): T[] {
return Object.values(this.items);
}
/** 清空字典 */
Clear(): void {
this.items = {};
}
}

25
src/utils/compressor.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* @file compressor.js
* @description 解压缩工具使用pako完成与GoBetterStudio相同的deflate/raw解码
*/
import pako from 'pako';
const decoder = new TextDecoder();
export const compressor = {
/** 压缩字符串或字节数据 */
compress(data: Uint8Array | string): Uint8Array {
const source = typeof data === 'string' ? new TextEncoder().encode(data) : data;
return pako.deflateRaw(source);
},
/** 解压缩字节数据并返回字符串 */
decompress(data: ArrayBuffer | Uint8Array): string {
const input = data instanceof Uint8Array ? data : new Uint8Array(data);
const uncompressed = pako.inflateRaw(input);
return decoder.decode(uncompressed);
}
};
export default compressor;

73
src/utils/cryptor.ts Normal file
View File

@ -0,0 +1,73 @@
import md5 from 'js-md5';
const KEY_SIZE = 256 / 32;
const ITERATIONS = 1000;
type EncryptedAsset = {
value: Uint8Array;
Timestamp: string;
};
type Credential = {
token: string;
uid: string;
};
type CryptoMaterial = {
obj: Uint8Array;
iv: Uint8Array;
salt: Uint8Array;
key: string;
};
function prepareCryptoMaterial(data: EncryptedAsset, info: Credential): CryptoMaterial {
const userIdPrefix = info.uid.slice(0, 16);
const iv = new TextEncoder().encode(userIdPrefix);
const obj = data.value.slice(7);
const saltHex = md5(`${info.token}${data.Timestamp}`);
const saltBytes = new Uint8Array((saltHex.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16)));
return { obj, iv, salt: saltBytes, key: info.token };
}
/** 从服务器返回的数据中异步解密出原始GLTF内容 */
export async function decryptAsync(data: EncryptedAsset, info: Credential): Promise<ArrayBuffer | null> {
const { obj, iv, salt, key } = prepareCryptoMaterial(data, info);
const derivedKey = await generateAesKeyAsync(key, salt, KEY_SIZE, ITERATIONS);
return aesDecryptAsync(obj, derivedKey, iv);
}
async function generateAesKeyAsync(secret: string, salt: Uint8Array, keySize: number, iterations: number): Promise<CryptoKey> {
const passwordBuffer = new TextEncoder().encode(secret);
const baseKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-1'
},
baseKey,
{ name: 'AES-CBC', length: keySize * 32 },
false,
['decrypt']
);
}
async function aesDecryptAsync(data: Uint8Array, key: CryptoKey, iv: Uint8Array): Promise<ArrayBuffer | null> {
try {
return await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
key,
data
);
} catch (error) {
console.error('解密失败:', error);
return null;
}
}

123
src/utils/event.ts Normal file
View File

@ -0,0 +1,123 @@
type Listener = {
callback: (...args: unknown[]) => void;
context?: unknown;
};
type EventMeta = {
type: string;
description: string;
listeners: Listener[];
};
/**
* 基础事件发射器
*/
export class Emitter {
private _events: Record<string, Listener[]> = {};
on(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
if (!this._events[name]) {
this._events[name] = [];
}
this._events[name].push({ callback, context });
return this;
}
once(name: string, callback: (...args: unknown[]) => void, context?: unknown): this {
const onceWrapper = (...args: unknown[]) => {
this.off(name, onceWrapper);
callback.apply(context, args);
};
return this.on(name, onceWrapper, context);
}
off(name?: string, callback?: (...args: unknown[]) => void): this {
if (!name) {
this._events = {};
return this;
}
if (!this._events[name]) return this;
if (!callback) {
delete this._events[name];
return this;
}
this._events[name] = this._events[name].filter(
listener => listener.callback !== callback
);
return this;
}
removeAllListeners(): this {
this._events = {};
return this;
}
emit(name: string, ...args: unknown[]): this {
if (!this._events[name]) return this;
this._events[name].forEach(listener => {
listener.callback.apply(listener.context, args);
});
return this;
}
listenerCount(name: string): number {
return this._events[name]?.length ?? 0;
}
}
/**
* 全局事件管理器
*/
export class EventManager extends Emitter {
private eventMap: Map<string, EventMeta>;
constructor() {
super();
this.eventMap = new Map();
}
registerEvent(type: string, description: string): void {
this.eventMap.set(type, { type, description, listeners: [] });
}
getRegisteredEvents(): EventMeta[] {
return Array.from(this.eventMap.values());
}
}
// 创建全局事件管理器实例
export const eventManager = new EventManager();
// 注册标准事件类型(描述使用英文,避免编码问题)
eventManager.registerEvent('load', 'resource load complete');
eventManager.registerEvent('load-progress', 'resource load progress');
eventManager.registerEvent('load-error', 'resource load error');
eventManager.registerEvent('animation-start', 'animation start');
eventManager.registerEvent('animation-end', 'animation end');
eventManager.registerEvent('animation-loop', 'animation loop');
eventManager.registerEvent('model-change', 'model change');
eventManager.registerEvent('camera-change', 'camera change');
eventManager.registerEvent('scene-ready', 'scene ready');
eventManager.registerEvent('dispose', 'component disposed');
// 导出便捷函数
export function on(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter {
return eventManager.on(eventName, callback, context);
}
export function off(eventName?: string, callback?: (...args: unknown[]) => void): Emitter {
return eventManager.off(eventName, callback);
}
export function once(eventName: string, callback: (...args: unknown[]) => void, context?: unknown): Emitter {
return eventManager.once(eventName, callback, context);
}
export function emit(eventName: string, ...args: unknown[]): Emitter {
return eventManager.emit(eventName, ...args);
}
export function removeAllListeners(eventName?: string): Emitter {
if (eventName) return eventManager.off(eventName);
return eventManager.removeAllListeners();
}