ForgeによるExtensionsの作成とViewerからのEventの取得

Autodesk ForgeはAutodesk.Viewing.Viewer3Dクラスから取得できる多様なイベント扱うことができます。イ今回はExtensionsと呼ばれる、機能拡張の仕組みと、Eventハンドリングについて説明します。

# Extensionsについて
Extensionsを使うことで、Viewerの要素（Viewerに指定したDIV要素）外から、Viewerに対する操作が可能になります。
例えば、HTMLのボタンからViewerのカメラ制御のEnable/Disableを切り替えるといったことです。
基本的なExtensionの作り方としては、追加したいExtensionファイルを作成し、

1. Autodesk.Viewing.Extensionを継承したクラスを作成
2. boolenを返すload()メソッドを定義
3. boolenを返すunload()メソッドを定義
4. ユニークなIDを使って自身を登録する

といった手順で実現できます。今回はAutodeskのチュートリアルに従って、Viewer内のオブジェクトを選択状態と、ナビゲーションツールの状態を取得するExtensionを作って、取得した情報を画面上に表示するといったことを試してみます。

# Viewerの準備
[前のエントリ](https://qiita.com/Takashi_Kasuya/items/3aa9016b7b67e5ca7b6a)を参考に基本的なビューワを作ります。[Autodeskのチュートリアル](https://developer.autodesk.com/en/docs/viewer/v5/tutorials/basic-viewer/)が一番シンプルです。

# ViewerからのEventを受け取るExtensionの作成
まずは、クライアントから参照できる位置に、eventhandler.jsという名前のファイルを作ります。（クラス名はEventTutorialですが）

```eventhandler.js
function EventsTutorial(viewer, options) {
    Autodesk.Viewing.Extension.call(this, viewer, options);
}

EventsTutorial.prototype = Object.create(Autodesk.Viewing.Extension.prototype);
EventsTutorial.prototype.constructor = EventsTutorial;

EventsTutorial.prototype.onNavigationModeEvent = function (event) {
    var domElem = document.getElementById('MyToolValue');
    domElem.innerText = event.id;
};

EventsTutorial.prototype.onSelectionEvent = function (event) {
    var currSelection = this.viewer.getSelection();
    var domElem = document.getElementById('MySelectionValue');
    domElem.innerText = currSelection.length;
};

EventsTutorial.prototype.load = function () {
    this.onSelectionBinded = this.onSelectionEvent.bind(this);
    this.onNavigationModeBinded = this.onNavigationModeEvent.bind(this);
    this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionBinded);
    this.viewer.addEventListener(Autodesk.Viewing.NAVIGATION_MODE_CHANGED_EVENT, this.onNavigationModeBinded);
    return true;
};

EventsTutorial.prototype.unload = function () {
    this.viewer.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionBinded);
    this.viewer.removeEventListener(Autodesk.Viewing.NAVIGATION_MODE_CHANGED_EVENT, this.onNavigationModeBinded);
    this.onSelectionBinded = null;
    this.onNavigationModeBinded = null;
    return true;
};

Autodesk.Viewing.theExtensionManager.registerExtension('EventsTutorial', EventsTutorial);
```

重要なのは、先に述べたようにAutodesk.Viewing.Extensionを拡張したクラスを作っているということです。prototype拡張を使って、loadやunloadメソッドを実装しています。onSelectionEvent, onNavigationModeEventがイベントハンドラです。HTML内のMyToolValue要素，MySekectionValue要素のinnerTextの情報を更新しています。これらのハンドラは、loadメソッド内のaddEventListenerで登録されますが、Extensionをunloadした時点でハンドラの登録を解除するように実装を工夫しています。
最後にregisterExtensionに作成したクラスを登録しています。第一引数は呼び出す際のIDですが、別にクラス名と揃っていなくても構いません。

# 表示用のHTMLファイルの更新
Extensionで補足した情報を表示するためのDIV要素(MySelectionValue,MyToolValue)を追加します。私はExpressのテンプレートで作っているので、PUGでの記載となります。作成したExtensionのJSファイルも忘れずにインポートしましょう。


```layout.pug
doctype html
html
  head
    title= title
    meta(name='viewport', content='width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=no')
    link(rel='stylesheet', href='/stylesheets/main.css')
    link(rel='stylesheet', href='https://developer.api.autodesk.com/modelderivative/v2/viewers/style.min.css', type='text/css')
    
  body
    block content
    script(src='https://developer.api.autodesk.com/modelderivative/v2/viewers/viewer3D.min.js')
    script(src='javascripts/viewer.js')
    script(src='javascripts/eventhandler.js')
```

```index.pug
extends layout

block content
  div#Buttons
    button#MyAwesomeLockButton Lock it!
    button#MyAwesomeUnlockButton Unlock it!
  div#MyViewerDiv
  div.my-custom-ui
    div Items selected:
        span#MySelectionValue 0
    div Navigaton tool:
        span#MyToolValue Unknown
```
# ExtensionをViewer３Dに読み込む
Viewerへの登録はregisterViewerの引数に、上記で指定したExtensionの登録名のリストを入れることで行います。リストなので、Extentionsの中に、カンマで区切って追加するのに加えて、上記の手順でJavascriptファイルの作成とインポートを行います。簡単ですね。

```viewer.js（抜粋）
var viewerApp;
var options = {
    env: 'AutodeskProduction',
    getAccessToken: function (onGetAccessToken) {
        var accessToken = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
        var expireTimeSeconds = 60 * 30;
        onGetAccessToken(accessToken, expireTimeSeconds);
    }
};
var documentId = 'urn:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

Autodesk.Viewing.Initializer(options, function onInitialized() {
    viewerApp = new Autodesk.Viewing.ViewingApplication('MyViewerDiv');
    // 追加のExtensionを指定
    var config3d = {
        extensions: ['EventsTutorial']
    };
    // Viewerに登録
    viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D, config3d);
    viewerApp.loadDocument(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
```
npm startなどで実行すると以下のようになります。画面上で選択したオブジェクトの数をSELECTION_CHANGED_EVENTでとらえ表示するとともに、NAVIGATION_MODE_CHANGED_EVENTで画面下部のナビゲーションツールの選択中のモード名を取得・表示しています。

<img width="1893" alt="viewer.png" src="https://qiita-image-store.s3.amazonaws.com/0/51703/f095505e-2fdb-c314-55ab-04ef009ea1f3.png">

#Viewer3Dで取得できるEventの種類
ベントは[Viewer3DのDoc](https://developer.autodesk.com/en/docs/viewer/v5/reference/javascript/viewer3d/)で確認できますが、表示に関するかなり細かいレベルでイベントが取れるようです。

| イベント名 | 説明 | 
| :--- | :--- | 
| AGGREGATE_SELECTION_CHANGED_EVENT | マルチモデルコンテキストの中で選択されたオブジェクトのリストが変わったときに発火| 
| ANIMATION_READY_EVENT | animationsが正しく初期化されたときに発火 |
| CAMERA_CHANGE_EVENT | カメラが変化したときに発火 |
| CUTPLANES_CHANGE_EVENT | Cutting planesが変化したときに発火 |
| ESCAPE_EVENT | ESCキーが押されたときに発火 |
| EXPLODE_CHANGE_EVENT | Explode toolが使われたときはどんなときも発火 |
| EXTENSION_LOADED_EVENT | Viewer Extentionが読み込まれたときに発火 |
| EXTENSION_UNLOADED_EVENT | Viewer ExtentionがUnloadされたときに発火 |
| FINAL_FRAME_RENDERED_CHANGED_EVENT | Viewerが表示すべきフレームをすべて表示しきったときに発火（ペンディングしているジオメトリや適用しようとしているEffectなどがなくなったということ） |
| FIT_TO_VIEW_EVENT | fitToViewオペレーションがなされたときに発火 |
| FRAGMENTS_LOADED_EVENT | fragmentsが要求に応じて読み込まれたときに発火 |
| FULLSCREEN_MODE_EVENT | スクリーンモードが変わったときに発火 |
| GEOMETRY_LOADED_EVENT | Model / Drawingの読み込みが終わったときに発火 |
| HIDE_EVENT | Viwerがオブジェクトを隠したときに発火 |
| HYPERLINK_EVENT | モデル中に埋め込まれているハイパーリンクがクリックされたときに発火 |
| ISOLATE_EVENT | Viewerがオブジェクトを隔離したときに発火（不可視やghostedの状態にしたとき）|
| LAYER_VISIBILITY_CHANGED_EVENT | 2DレイヤーのVisubilityが変化したときに発火 |
| LOAD_MISSING_GEOMETRY | 欠損したジオメトリが見えるような変化が何かしらviewに生じたときに発火 |
| MODEL_ROOT_LOADED_EVENT | 初期モデルのマニフェストの取得と解析が終わったときに発火 |
| MODEL_UNLOADED_EVENT | モデルがViewerから取り残されたときに発火 |
| NAVIGATION_MODE_CHANGED_EVENT | ナビゲーションツールが変更されたときに発火 |
| OBJECT_TREE_CREATED_EVENT | インスタンスツリー生成が成功したときに発火 |
| OBJECT_TREE_UNAVAILABLE_EVENT | インスタンスツリーの解析に失敗したときに発火 |
| PREF_CHANGED_EVENT | ユーザパフォーマンスプロパティが変化したときに発火 |
| PREF_RESET_EVENT | ユーザパフォーマンスプロパティがリセットされたときに発火 |
| PROGRESS_UPDATE_EVENT | Model / Drawingが読み込まれているときに継続的に発火（読み込みのパーセントが出せる） |
| RENDER_OPTION_CHANGED_EVENT | レンダリングオプションが変化したときに発火 |
| RENDER_PRESENTED_EVENT | レンダラが準備完了（presented)になったときには発火 |
| RESET_EVENT | モデルが初期状態にリセットされたときに発火 |
| RESTORE_DEFAULT_SETTINGS_EVENT | UIを使って、ユーザがデフォルトセッティングにリストアしたときに発火。他のAutodesk.Viewing.PREF_CHANGED_EVENTがすべて呼ばれたときに発火する |
| SELECTION_CHANGED_EVENT | 選択されたオブジェクトが変化したときに発火 |
| SHOW_EVENT | Viewerがオブジェクトのセットを表示したときに発火 |
|TEXTURES_LOADED_EVENT| Model / Drawingのテクスチャのダウンロードが終わったときに発火 |
|TEXTURES_LOADED_EVENT | ツールがActivate/Deactivate状態に変化したときに発火 |
| VIEWER_INITIALIZED | Viewerが初期化を完全に終えたときに発火 |
| VIEWER_RESIZE_EVENT | Viewerのサイズが変わったときに発火 |
| VIEWER_STATE_RESTORED_EVENT | Viewerの状態がリストアされたときに発火 |
| VIEWER_UNINITIALIZED | ViewerがUninitializeの状態のときに発火 |