import type { GraphLib } from '../model/data'; import { RuntimeContext } from '../runtime/context'; import { Supervisor } from '../runtime/supervisor'; import type { GraphData, GraphEdge, GraphNode, Point } from '../types'; import type { BaseLayoutOptions, Layout, LayoutWithIterations } from './types'; export type { BaseLayoutOptions }; /** * 布局基类 * * Base class for layouts */ export abstract class BaseLayout< O extends BaseLayoutOptions = BaseLayoutOptions, > implements Layout { public abstract readonly id: string; protected abstract getDefaultOptions(): O; protected initialOptions!: O; protected runtimeOptions!: O; protected context!: RuntimeContext; protected model!: GraphLib; protected supervisor: Supervisor | null = null; constructor(options?: Partial) { this.initialOptions = this.mergeOptions( this.getDefaultOptions(), options, ); } get options(): O { return this.runtimeOptions || this.initialOptions; } protected mergeOptions(base: O, patch?: Partial): O { return Object.assign({}, base, patch || {}); } public async execute( data: GraphData, userOptions?: Partial, ): Promise { this.runtimeOptions = this.mergeOptions( this.initialOptions, userOptions, ); const { node, edge, enableWorker } = this.runtimeOptions; this.context = new RuntimeContext(data, { node, edge }); this.model = this.context.graph; const shouldUseWorker = enableWorker && typeof Worker !== 'undefined'; if (shouldUseWorker) { await this.layoutInWorker(data, this.runtimeOptions); } else { await this.layout(this.runtimeOptions); } } protected abstract layout(options: O): Promise; protected async layoutInWorker(data: GraphData, options: O): Promise { try { if (!this.supervisor) { this.supervisor = new Supervisor(); } const result = await this.supervisor.execute(this.id, data, options); this.context?.replace(result); } catch (error) { console.error( 'Layout in worker failed, fallback to main thread layout.', error, ); // Fallback to main thread layout await this.layout(options); } } public forEachNode(callback: (node: GraphNode, index: number) => void) { this.context.forEachNode(callback); } public forEachEdge(callback: (edge: GraphEdge, index: number) => void) { this.context.forEachEdge(callback); } public destroy(): void { this.context?.destroy(); // @ts-ignore this.model = null; // @ts-ignore this.context = null; if (this.supervisor) { this.supervisor.destroy(); this.supervisor = null; } } } /** * 迭代布局基类 * * Base class for iterative layouts */ export abstract class BaseLayoutWithIterations< O extends BaseLayoutOptions = BaseLayoutOptions, > extends BaseLayout { abstract stop(): void; abstract tick(iterations: number): void; abstract restart(): void; abstract setFixedPosition(nodeId: string, position: Point | null): void; } /** * 判断布局是否为迭代布局 * * Determine whether the layout is an iterative layout */ export function isLayoutWithIterations( layout: any, ): layout is LayoutWithIterations { return !!layout.tick && !!layout.stop; }