BabylonJS/Spector.js

View on GitHub
src/backend/states/baseState.ts

Summary

Maintainability
D
2 days
Test Coverage
import { IContextInformation, WebGLRenderingContexts, ExtensionList } from "../types/contextInformation";
import { State, ICommandCapture, CommandCapturedCallbacks, CommandCaptureStatus } from "../../shared/capture/commandCapture";
import { WebGlObjects } from "../webGlObjects/baseWebGlObject";

export type StateData = { [key: string]: any };

export abstract class BaseState {
    public abstract get stateName(): string;

    protected readonly context: WebGLRenderingContexts;
    protected readonly contextVersion: number;
    protected readonly extensions: ExtensionList;
    protected readonly toggleCapture: (capture: boolean) => void;

    protected previousState: State;
    protected currentState: State;
    protected quickCapture: boolean;
    protected fullCapture: boolean;
    protected lastCommandName: string;

    private readonly changeCommandsByState: { [key: string]: string[] };
    private readonly consumeCommands: string[];
    private readonly commandNameToStates: { [commandName: string]: string[] };

    private capturedCommandsByState: { [key: string]: ICommandCapture[] };

    constructor(protected readonly options: IContextInformation) {
        this.context = options.context;
        this.contextVersion = options.contextVersion;
        this.extensions = options.extensions;
        this.toggleCapture = options.toggleCapture;

        this.consumeCommands = this.getConsumeCommands();
        this.changeCommandsByState = this.getChangeCommandsByState();
        this.commandNameToStates = this.getCommandNameToStates();
    }

    public get requireStartAndStopStates(): boolean {
        return true;
    }

    public startCapture(loadFromContext: boolean, quickCapture: boolean, fullCapture: boolean): State {
        this.quickCapture = quickCapture;
        this.fullCapture = fullCapture;
        this.capturedCommandsByState = {};
        if (loadFromContext && this.requireStartAndStopStates) {
            this.currentState = {};
            this.readFromContextNoSideEffects();
        }
        this.copyCurrentStateToPrevious();
        this.currentState = {};

        return this.previousState;
    }

    public stopCapture(): State {
        if (this.requireStartAndStopStates) {
            this.readFromContextNoSideEffects();
        }
        this.analyse(undefined);

        return this.currentState;
    }

    public registerCallbacks(callbacks: CommandCapturedCallbacks): void {
        for (const stateName in this.changeCommandsByState) {
            if (this.changeCommandsByState.hasOwnProperty(stateName)) {
                for (const changeCommand of this.changeCommandsByState[stateName]) {
                    callbacks[changeCommand] = callbacks[changeCommand] || [];
                    callbacks[changeCommand].push(this.onChangeCommand.bind(this));
                }
            }
        }

        for (const commandName of this.consumeCommands) {
            callbacks[commandName] = callbacks[commandName] || [];
            callbacks[commandName].push(this.onConsumeCommand.bind(this));
        }
    }

    public getStateData(): StateData {
        return this.currentState;
    }

    protected abstract readFromContext(): void;

    protected getConsumeCommands(): string[] {
        return [];
    }

    protected getChangeCommandsByState(): { [key: string]: string[] } {
        return {};
    }

    protected copyCurrentStateToPrevious(): void {
        if (!this.currentState) {
            return;
        }

        this.previousState = this.currentState;
    }

    protected onChangeCommand(command: ICommandCapture): void {
        const stateNames = this.commandNameToStates[command.name];
        for (const stateName of stateNames) {
            if (!this.isValidChangeCommand(command, stateName)) {
                return;
            }

            this.capturedCommandsByState[stateName] = this.capturedCommandsByState[stateName] || [];
            this.capturedCommandsByState[stateName].push(command);
        }
    }

    protected isValidChangeCommand(command: ICommandCapture, stateName: string): boolean {
        return true;
    }

    protected onConsumeCommand(command: ICommandCapture): void {
        if (!this.isValidConsumeCommand(command)) {
            return;
        }

        this.readFromContextNoSideEffects();
        this.analyse(command);
        this.storeCommandIds();
        command[this.stateName] = this.currentState;

        this.startCapture(false, this.quickCapture, this.fullCapture);
    }

    protected isValidConsumeCommand(command: ICommandCapture): boolean {
        this.lastCommandName = command?.name;
        return true;
    }

    protected analyse(consumeCommand: ICommandCapture): void {
        for (const stateName in this.capturedCommandsByState) {
            if (this.capturedCommandsByState.hasOwnProperty(stateName)) {
                const commands = this.capturedCommandsByState[stateName];
                const lengthM1 = commands.length - 1;
                if (lengthM1 >= 0) {
                    if (consumeCommand) {
                        for (let i = 0; i < lengthM1; i++) {
                            const redundantCommand = commands[i];
                            redundantCommand.consumeCommandId = consumeCommand.id;
                            this.changeCommandCaptureStatus(redundantCommand, CommandCaptureStatus.Redundant);
                        }

                        const isStateEnabled = this.isStateEnableNoSideEffects(stateName, consumeCommand.commandArguments);
                        const command = commands[lengthM1];
                        command.consumeCommandId = consumeCommand.id;

                        if (!this.areStatesEquals(this.currentState[stateName], this.previousState[stateName])) {
                            if (isStateEnabled) {
                                this.changeCommandCaptureStatus(command, CommandCaptureStatus.Valid);
                            }
                            else {
                                this.changeCommandCaptureStatus(command, CommandCaptureStatus.Disabled);
                            }
                        }
                        else {
                            this.changeCommandCaptureStatus(command, CommandCaptureStatus.Redundant);
                        }
                    }
                    else {
                        for (let i = 0; i < commands.length; i++) {
                            const command = commands[i];
                            this.changeCommandCaptureStatus(command, CommandCaptureStatus.Unused);
                        }
                    }
                }
            }
        }
    }

    protected storeCommandIds(): void {
        const commandIdsStates = ["unusedCommandIds", "disabledCommandIds", "redundantCommandIds", "validCommandIds"];
        for (const commandIdsStatus of commandIdsStates) {
            this.currentState[commandIdsStatus] = [] as any;
        }

        for (const stateName in this.capturedCommandsByState) {
            if (this.capturedCommandsByState.hasOwnProperty(stateName)) {
                const commands = this.capturedCommandsByState[stateName];
                for (const command of commands) {
                    switch (command.status) {
                        case CommandCaptureStatus.Unused:
                            this.currentState["unusedCommandIds"].push(command.id);
                            break;
                        case CommandCaptureStatus.Disabled:
                            this.currentState["disabledCommandIds"].push(command.id);
                            break;
                        case CommandCaptureStatus.Redundant:
                            this.currentState["redundantCommandIds"].push(command.id);
                            break;
                        case CommandCaptureStatus.Valid:
                            this.currentState["validCommandIds"].push(command.id);
                            break;
                    }
                }
            }
        }

        for (const commandIdsStatus of commandIdsStates) {
            if (!this.currentState[commandIdsStatus].length) {
                delete this.currentState[commandIdsStatus];
            }
        }
    }

    protected changeCommandCaptureStatus(capture: ICommandCapture, status: CommandCaptureStatus): boolean {
        if (capture.status < status) {
            capture.status = status;
            return true;
        }

        return false;
    }

    protected areStatesEquals(a: any, b: any): boolean {
        if (typeof a !== typeof b) {
            return false;
        }

        if (a && !b) {
            return false;
        }

        if (b && !a) {
            return false;
        }

        if (a === undefined || a === null) {
            return true;
        }

        if (a.length && b.length && typeof a !== "string") {
            if (a.length !== b.length) {
                return false;
            }

            for (let i = 0; i < a.length; i++) {
                if (a[i] !== b[i]) {
                    return false;
                }
            }

            return true;
        }

        return a === b;
    }

    protected isStateEnable(stateName: string, args: IArguments): boolean {
        return true;
    }

    protected getSpectorData(object: any): any {
        if (!object) {
            return undefined;
        }

        return {
            __SPECTOR_Object_TAG: WebGlObjects.getWebGlObjectTag(object) || this.options.tagWebGlObject(object),
            __SPECTOR_Object_CustomData: object.__SPECTOR_Object_CustomData,
            __SPECTOR_Metadata: object.__SPECTOR_Metadata,
        };
    }

    private readFromContextNoSideEffects(): void {
        this.toggleCapture(false);
        this.readFromContext();
        this.toggleCapture(true);
    }

    private isStateEnableNoSideEffects(stateName: string, args: IArguments): boolean {
        this.toggleCapture(false);
        const enable = this.isStateEnable(stateName, args);
        this.toggleCapture(true);
        return enable;
    }

    private getCommandNameToStates(): { [key: string]: string[] } {
        const result: { [key: string]: string[] } = {};
        for (const stateName in this.changeCommandsByState) {
            if (this.changeCommandsByState.hasOwnProperty(stateName)) {
                for (const changeCommand of this.changeCommandsByState[stateName]) {
                    result[changeCommand] = result[changeCommand] || [];
                    result[changeCommand].push(stateName);
                }
            }
        }
        return result;
    }
}