Office 365 add in and BabylonJS

Hi everyone;

Today I want to share with you a very cool demo I realized for the French event Techdays 2015.

image

Yes, it’s me Sourire
Yes, I forgot to take off my badge during the keynote! Rire

Everything shown here is available in the official Office dev Github repository: https://github.com/OfficeDev/Excel-Add-in-JS-BabylonJS

image

Intro

The goal is to demonstrate the easy use of Office 365 dev application model and the BabylonJS framework.
The main purpose of this demo is to show the path-finding of people inside the Microsoft France Lobby as show in this screenshot:

If you want to know more about BabylonJS and how to use it, feel free, all about this fantastic framework is available here : https://www.babylonjs.com

In my opinion, here are the three main interesting points of this development:

  1. Use of the Office.js SDK to be able to communicate between my add in and the current worksheet.
  2. Use of the BabylonJS SDK to create a smooth and incredibles animations in a full 3D real time environment.
  3. Use of Typescript to be able to write a full comprehensible and maintainable JavaScript project (and by the way, I'm a C# developer, Typescript is therefore obvious for me)

Office Add In

First of all, the main url for "All-Whant-You-Need" about Office dev, is here : https://dev.office.com

image

Geting datas from Excel worksheet

The main source code is maintained in the App/Welcome/Tracker.ts file

The _setTableBinding method is called once the scene AND the file is completely loaded . I used the addFromNamedItemAsync method to create a basic binding:

By default, adding a table in Excel assigns the name "Table1" for the first table you add, "Table2" for the second table you add, and so on.
Be careful, In Excel, when specifying a table as a named item, you must fully qualify the name to include the worksheet name in the name of the table in this format: "Sheet1!Table1", as you can see in this code :

private _setTableBinding() {
    /// <signature>
    ///   <summary>Create a binding to later gate the datas</summary>
    /// </signature>

    Office.context.document.bindings.addFromNamedItemAsync("Sheet1!Table1",
        Office.BindingType.Table, { id: "positions" },
        (asyncResult: any) => {
            if (asyncResult.status === "failed") {
                console.log("Error: " + asyncResult.error.message);
            } else {
                try {
                    this._tableBinding = asyncResult.value;
                } catch (e) {
                    console.log(e);
                }
            }
        });
}

Before going in the BabylonJS code, you have to get all the datas from you Excel table. You will find the code in the playDatas() method:

As you can see, I used two useful options when I get datas with the getDataSync method :

  1. valueFormat : Office.ValueFormat.Unformatted : To be able to parse the current datas, useful for date, number etc …
  2. filterType: Office.FilterType.OnlyVisible : It’s a great option in my scenario, because with the Straightfoward filters in an Excel worksheet, I can isolate a particular path-finding !

More about getDataAsync method : https://msdn.microsoft.com/en-us/library/office/fp161073.aspx

// Get datas
this._tableBinding.getDataAsync(
    {
        // Options : We can get filtered or not datas
        filterType: Office.FilterType.OnlyVisible,
        // Options : To be able to work with those datas, no format will help, later
        valueFormat: Office.ValueFormat.Unformatted
    },
    (result) => {
        if (result.status === "succeeded") {
            // Rows
            var rows = result.value.rows;

            // etc ...
        }
    });

Localization

The UIStrings.js file is here to localize the application. Basically, you have to :

  1. Localize everything that is not handled by you : Title and Description in the ribbon for example
  2. Localize your application handled by you

In the manifest file (you should open it in XML mode) check that you have corrects values for each language you handle :

<DefaultLocale>en-US</DefaultLocale>

<DisplayName DefaultValue="Lobby pathfinding application">
   <Override Locale="fr-FR" Value="Gestion des passages"/>
</DisplayName>
<Description DefaultValue="An app which show the people pathfinding">
   <Override Locale="fr-FR" Value="Une application pour suivre les passages via le hall d'accueil"/>
</Description>

In the other hand, for everything else, you can use two kind of informatins to know what language you should apply. In my case I use the display language to localize my add in :

// Document language
var contentLanguage = Office.context.contentLanguage;
// Display language
var displayLanguage = Office.context.displayLanguage;

// Choose Display or Document language
var UIText = UIStrings.getLocaleStrings(displayLanguage);

BabylonJS

In this sample using BabylonJS is pretty straightforward. for those of you interested in, here are the main steps to make babylonJS works :

  1. Get a HTML5 canvas render surface
  2. Create a BabylonJS engine instance binded to your canvas
  3. Create a scene composed of :
    1. A complete 3D modelised scene (the lobby in this sample) (imported with the Babylon SceneLoader
    2. Add a free camera
    3. Add a light
  4. When my datas from my worksheet are loaded (and binded) create all the animations of every path-finding Sourire
  5. Render the scene .. in a render loop Sourire

The launchScene method is the main method where you initialize all what you need. This method is always called when your document  AND when Office is ready :

// launch the scene when office AND document are ready
Office.initialize = function (reason) {
    $(document).ready(function () {
        tracker.launchScene();
    }
});

Here is the launchScene method, as you can expect, we basically get the canvas, create an engine, create the scene, and call the render method :

public launchScene() {
     /// <signature>
     ///   <summary>Launch the scene </summary>
     /// </signature>

     try {

         // Check if BabylonJS is supported
         if (!BABYLON.Engine.isSupported()) {
             return;
         }

         if (this.engine) {
             this.engine.dispose();
             this.engine = null;
         }

         this.canvas = <HTMLCanvasElement>document.getElementById("renderCanvas");
         this.engine = new BABYLON.Engine(this.canvas, true);
         this.scene = this._createScene();

         // Here we go ! RENDERING BABY !
         this.engine.runRenderLoop(() => {
             this.scene.render();
         });
     } catch (e) {
         console.log(e.message);
     }
}

Before creating the animations, we create the scene, as I said earlier :

private _createScene() {
    /// <signature>
    ///   <summary>Creating the scene (see BabylonJS documentation) </summary>
    /// </signature>

    this.scene = new BABYLON.Scene(this.engine);

    this._freeCamera = new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, 5), this.scene);
    this._freeCamera.rotation = new BABYLON.Vector3(0, Math.PI, 0);

    this.scene.activeCamera = this._freeCamera;
    this._freeCamera.attachControl(this.canvas, true);

    var light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), this.scene);

    // Resizing the add in
    window.addEventListener("resize", () => { this.engine.resize(); });

    // The first parameter can be used to specify which mesh to import. Here we import all meshes
    BABYLON.SceneLoader.ImportMesh("", "/Assets/", "Welcome.babylon",
        this.scene, (newMeshes: BABYLON.AbstractMesh[]) => {

            // position Camera
            this._setFreeCameraPosition();

            //bind to existing datas
            this._setTableBinding();
        });

    return this.scene;
}

Animating every path-finding, thanks to BabylonJS, is pretty straightforward.
When the user will click the Play button, I call the playDatas() method and I create all the animations then launch them :

The code presented here is not complete, it just highlights what I think is important Sourire For the complete code, just refer the Github repo

for (var i = 0; i < lstGrp.length; i++) {
     var sphTab = lstGrp[i];

     // Every sphere must have a uniquename
     var sphereName = "d_" + sphTab[0][0] + "sphere";
     // Create the sphere
     var sphere = BABYLON.Mesh.CreateSphere(sphereName + "_sphere", 8, 4, this.scene);

     // Create a yellow texture
     var m = new BABYLON.StandardMaterial("texture2", this.scene);
     m.diffuseColor = new BABYLON.Color3(1, 1, 0); // yellow
     sphere.material = m;
     sphere.isPickable = true;

     // Create an animation path
     var animationPath = new BABYLON.Animation(sphereName + "_animation", "position", (30 * this.accelerate),
         BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
         BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);

     this._animations.push(animationPath);

     // Create all the frames for each sphere
     var keys = [];
     var maxFrame = 0;
     // in each group, each pos
     for (var j = 0; j < sphTab.length; j++) {

         var pos = new BABYLON.Vector3(posX, posY, posZ);
         keys.push({ frame: currentFrame, value: pos });

     }
     animationPath.setKeys(keys);
     sphere.animations.push(animationPath);

     // Launch the animation for this sphere
     this.scene.beginAnimation(sphere, 0, maxFrame, true);

}

Try it out

Get the sample

Clone or download the sample, open it with Visual Studio 2015.

Please check in add in project if the start object is "Accueil.xls".
This Excel file contains pre-populated datas, mandatory for the demo :

Hit F5 and try it !