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.
Yes, it’s me
Yes, I forgot to take off my badge during the keynote!
Everything shown here is available in the official Office dev Github repository: https://github.com/OfficeDev/Excel-Add-in-JS-BabylonJS
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:
- Use of the Office.js SDK to be able to communicate between my add in and the current worksheet.
- Use of the BabylonJS SDK to create a smooth and incredibles animations in a full 3D real time environment.
- 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
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 :
- valueFormat : Office.ValueFormat.Unformatted : To be able to parse the current datas, useful for date, number etc …
- 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 :
- Localize everything that is not handled by you : Title and Description in the ribbon for example
- 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 :
- Get a HTML5 canvas render surface
- Create a BabylonJS engine instance binded to your canvas
- Create a scene composed of :
- A complete 3D modelised scene (the lobby in this sample) (imported with the Babylon SceneLoader
- Add a free camera
- Add a light
- When my datas from my worksheet are loaded (and binded) create all the animations of every path-finding
- Render the scene .. in a render loop
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 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 !