import { ExpressionManager, InternalModel, ModelSettings, MotionManager } from '@/cubism-common';
import { Live2DLoader } from '@/factory/Live2DLoader';
import { Live2DModel, Live2DModelOptions } from '@/Live2DModel';
import { logger } from '@/utils';
import { Middleware, runMiddlewares } from '@/utils/middleware';
import {
createInternalModel,
jsonToSettings,
setupEssentials,
setupOptionals,
urlToJSON,
waitUntilReady,
} from '@/factory/model-middlewares';
import { JSONObject } from '../types/helpers';
export interface Live2DFactoryOptions extends Live2DModelOptions {
/**
* String to use for crossOrigin properties on `
` elements when loading textures.
* @default undefined
*/
crossOrigin?: string;
/**
* Callback invoked when the model has been loaded.
* @default undefined
*/
onLoad?(): void;
/**
* Callback invoked when error occurs while loading the model.
* @default undefined
*/
onError?(e: Error): void;
}
/**
* The context transferred through the model creation middlewares.
*/
export interface Live2DFactoryContext {
source: any,
options: Live2DFactoryOptions;
live2dModel: Live2DModel;
internalModel?: InternalModel;
settings?: ModelSettings;
}
/**
* Represents a Cubism version.
*/
export interface Live2DRuntime {
/**
* The version number. Higher version takes priority when matching the runtime.
*/
version: number;
/**
* Checks if the source belongs to this runtime.
* @param source - Either a settings JSON object or a ModelSettings instance.
* @return True if the source belongs to this runtime.
*/
test(source: any): boolean;
// TODO: remove
ready(): Promise;
/**
* Checks if the data is a valid moc to create the core model.
* @param modelData - The moc content.
* @return True if the data is valid.
*/
isValidMoc(modelData: ArrayBuffer): boolean;
/**
* Creates a ModelSettings.
* @param json - The settings JSON object.
* @return Created ModelSettings.
*/
createModelSettings(json: JSONObject): ModelSettings;
/**
* Creates a core model.
* @param data - Content of the moc file.
* @return Created core model.
*/
createCoreModel(data: ArrayBuffer): any;
/**
* Creates an InternalModel.
* @param coreModel - Core model that *must* belong to this runtime.
* @param settings - ModelSettings of this model.
* @param options - Options that will be passed to the InternalModel's constructor.
* @return Created InternalModel.
*/
createInternalModel(coreModel: any, settings: ModelSettings, options?: Live2DFactoryOptions): InternalModel;
/**
* Creates a pose.
* @param coreModel - Core model that *must* belong to this runtime.
* @param data - Content of the pose file.
* @return Created pose.
*/
createPose(coreModel: any, data: any): any;
/**
* Creates a physics.
* @param coreModel - Core model that *must* belong to this runtime.
* @param data - Content of the physics file.
* @return Created physics.
*/
createPhysics(coreModel: any, data: any): any;
}
/**
* Handles all the network load tasks.
*
* - Model creation: requested by {@link Live2DModel.from}.
* - Motion loading: implements the load method of MotionManager.
* - Expression loading: implements the load method of ExpressionManager.
*/
export class Live2DFactory {
/**
* All registered runtimes, sorted by versions in descending order.
*/
static runtimes: Live2DRuntime[] = [];
static urlToJSON = urlToJSON;
static jsonToSettings = jsonToSettings;
static waitUntilReady = waitUntilReady;
static setupOptionals = setupOptionals;
static setupEssentials = setupEssentials;
static createInternalModel = createInternalModel;
/**
* Middlewares to run through when setting up a Live2DModel.
*/
static live2DModelMiddlewares: Middleware[] = [
urlToJSON, jsonToSettings, waitUntilReady, setupOptionals, setupEssentials, createInternalModel,
];
/**
* load tasks of each motion. The structure of each value in this map
* is the same as respective {@link MotionManager.definitions}.
*/
static motionTasksMap = new WeakMap | undefined)[]>>();
/**
* Load tasks of each expression.
*/
static expressionTasksMap = new WeakMap | undefined)[]>();
/**
* Registers a Live2DRuntime.
*/
static registerRuntime(runtime: Live2DRuntime) {
Live2DFactory.runtimes.push(runtime);
// higher version as higher priority
Live2DFactory.runtimes.sort((a, b) => b.version - a.version);
}
/**
* Finds a runtime that matches given source.
* @param source - Either a settings JSON object or a ModelSettings instance.
* @return The Live2DRuntime, or undefined if not found.
*/
static findRuntime(source: any): Live2DRuntime | undefined {
for (const runtime of Live2DFactory.runtimes) {
if (runtime.test(source)) {
return runtime;
}
}
}
/**
* Sets up a Live2DModel, populating it with all defined resources.
* @param live2dModel - The Live2DModel instance.
* @param source - Can be one of: settings file URL, settings JSON object, ModelSettings instance.
* @param options - Options for the process.
* @return Promise that resolves when all resources have been loaded, rejects when error occurs.
*/
static async setupLive2DModel(live2dModel: Live2DModel, source: string | object | IM['settings'], options?: Live2DFactoryOptions): Promise {
const textureLoaded = new Promise(resolve => live2dModel.once('textureLoaded', resolve));
const modelLoaded = new Promise(resolve => live2dModel.once('modelLoaded', resolve));
// because the "ready" event is supposed to be emitted after
// both the internal model and textures have been loaded,
// we should here wrap the emit() in a then() so it'll
// be executed after all the handlers of "modelLoaded" and "textureLoaded"
const readyEventEmitted = Promise.all([textureLoaded, modelLoaded]).then(() => live2dModel.emit('ready'));
await runMiddlewares(Live2DFactory.live2DModelMiddlewares, {
live2dModel: live2dModel as Live2DModel,
source,
options: options || {},
});
// the "load" event should never be emitted before "ready"
await readyEventEmitted;
live2dModel.emit('load');
}
/**
* Loads a Motion and registers the task to {@link motionTasksMap}. The task will be automatically
* canceled when its owner - the MotionManager instance - has been destroyed.
* @param motionManager - MotionManager that owns this Motion.
* @param group - The motion group.
* @param index - Index in the motion group.
* @return Promise that resolves with the Motion, or with undefined if it can't be loaded.
*/
static loadMotion(motionManager: MotionManager, group: string, index: number): Promise {
// errors in this method are always handled
const handleError = (e: any) => motionManager.emit('motionLoadError', group, index, e);
try {
const definition = motionManager.definitions[group] ?. [index];
if (!definition) {
return Promise.resolve(undefined);
}
if (!motionManager.listeners('destroy').includes(Live2DFactory.releaseTasks)) {
motionManager.once('destroy', Live2DFactory.releaseTasks);
}
let tasks = Live2DFactory.motionTasksMap.get(motionManager);
if (!tasks) {
tasks = {};
Live2DFactory.motionTasksMap.set(motionManager, tasks);
}
let taskGroup = tasks[group];
if (!taskGroup) {
taskGroup = [];
tasks[group] = taskGroup;
}
const path = motionManager.getMotionFile(definition);
taskGroup[index] ??= Live2DLoader.load({
url: path,
settings: motionManager.settings,
type: motionManager.motionDataType,
target: motionManager,
})
.then(data => {
const taskGroup = Live2DFactory.motionTasksMap.get(motionManager)?.[group];
if (taskGroup) {
delete taskGroup[index];
}
const motion = motionManager.createMotion(data, group, definition);
motionManager.emit('motionLoaded', group, index, motion);
return motion;
})
.catch(e => {
logger.warn(motionManager.tag, `Failed to load motion: ${path}\n`, e);
handleError(e);
});
return taskGroup[index]!;
} catch (e) {
logger.warn(motionManager.tag, `Failed to load motion at "${group}"[${index}]\n`, e);
handleError(e);
}
return Promise.resolve(undefined);
}
/**
* Loads an Expression and registers the task to {@link expressionTasksMap}. The task will be automatically
* canceled when its owner - the ExpressionManager instance - has been destroyed.
* @param expressionManager - ExpressionManager that owns this Expression.
* @param index - Index of the Expression.
* @return Promise that resolves with the Expression, or with undefined if it can't be loaded.
*/
static loadExpression(expressionManager: ExpressionManager, index: number): Promise {
// errors in this method are always handled
const handleError = (e: any) => expressionManager.emit('expressionLoadError', index, e);
try {
const definition = expressionManager.definitions[index];
if (!definition) {
return Promise.resolve(undefined);
}
if (!expressionManager.listeners('destroy').includes(Live2DFactory.releaseTasks)) {
expressionManager.once('destroy', Live2DFactory.releaseTasks);
}
let tasks = Live2DFactory.expressionTasksMap.get(expressionManager);
if (!tasks) {
tasks = [];
Live2DFactory.expressionTasksMap.set(expressionManager, tasks);
}
const path = expressionManager.getExpressionFile(definition);
tasks[index] ??= Live2DLoader.load({
url: path,
settings: expressionManager.settings,
type: 'json',
target: expressionManager,
})
.then(data => {
const tasks = Live2DFactory.expressionTasksMap.get(expressionManager);
if (tasks) {
delete tasks[index];
}
const expression = expressionManager.createExpression(data, definition);
expressionManager.emit('expressionLoaded', index, expression);
return expression;
})
.catch(e => {
logger.warn(expressionManager.tag, `Failed to load expression: ${path}\n`, e);
handleError(e);
});
return tasks[index]!;
} catch (e) {
logger.warn(expressionManager.tag, `Failed to load expression at [${index}]\n`, e);
handleError(e);
}
return Promise.resolve(undefined);
}
static releaseTasks(this: MotionManager | ExpressionManager) {
if (this instanceof MotionManager) {
Live2DFactory.motionTasksMap.delete(this);
} else {
Live2DFactory.expressionTasksMap.delete(this);
}
}
}
MotionManager.prototype['_loadMotion'] = function(group, index) {
return Live2DFactory.loadMotion(this, group, index);
};
ExpressionManager.prototype['_loadExpression'] = function(index) {
return Live2DFactory.loadExpression(this, index);
};