Custom utility functions for entities and components

It would be useful to be able to add utility functions for components or entities. For example, suppose we want to create a utility function to duplicate an entity into a grid pattern. We could create a custom function to do this. This custom function could be linked to a particular component type, and could have arguments/parameters that can be configured through the UI. Creating such a custom function could be as simple as:

export class GridDuplicator extends EntityCustomFunction {
    public distance: number = 2.0;
    public rows: number = 1;
    public columns: number = 1;
    
    @Action
    public duplicate(entity: Entity) {
        const clipboard = new Clipboard(entity.world);
        clipboard.copy(entity);
        for (let z = 0; z < this.rows; ++z) {
            for (let x = 0; x < this.columns; ++x) {
                const pastedEntities = clipboard.paste();
                pastedEntities[0].translate(new Vec3(x * distance, 0, z * distance));
            }
        }
    }
}

This custom function can then be run through the UI (e.g. by right-clicking on an entity). This would bring up a dialog window, which would allow the user to configure the properties.

This feature was added to v0.25. Some examples are provided below:

@Name("Move To Floor")
@Icon("mdi:format-vertical-align-bottom")
export class MoveToFloorTool extends Tool<Entity> {
    override valid(object: Entity): boolean {
        return true;
    }

    override execute(object: Entity): void {
        const bbox = object.worldBoundingBox;
        const offset = new Vec3(0, bbox.min.y, 0);
        object.worldPosition = Vec3.subtract(object.worldPosition, offset);
    }
}

@Name("Purge Custom Materials")
@Icon("mdi:hammer-screwdriver")
export class PurgeUnusedMaterials extends Tool<World> {
    override valid(object: World): boolean {
        return true;
    }

    override execute(world: World): void {
        const library = world.materials.custom;
        const materials = new Set<CustomMaterial>(library.materials);
        for (const entity of world.descendants) {
            const graphics = entity.findComponent(GraphicsComponent);
            if (graphics !== null) {
                const material = graphics.material;
                if (material !== null) {
                    materials.delete(material);
                }
            }
        }

        for (const material of materials) {
            console.log("Removed: " + material.name);
            library.remove(material.name);
        }
    }
}

@Name("Component Tool")
@Icon("mdi:hammer-screwdriver")
export class MyComponentTool extends Tool<Component> {
    override valid(object: Component): boolean {
        return true;
    }

    override execute(object: Component): void {
        throw new Error("Method not implemented.");
    }
}