前回、 Learn Autodesk Forge(日本語)の Design Automation API のセクション、モデルを修正する セクションで公開されている learn.forge.designautomation サンプルのソースコードを入手して、ローカル環境で実行する手順をご紹介しました。
このサンプルでは、Design Autiomation API での処理結果を Revit プロジェクトファイルのダウンロードリンクを表示して終わってしまうので、ここでは、Bucket に保存されている Revit プロジェクトファイルを Model Derivative API で SVF に変換して、Forge Viewer に表示する部分を追記していきます。
まず前提となりますが、このサンプルは、ASP.NET Core MVC を使用して実装されています。
ASP.NET Core MVC は、モデル ビュー コントローラー デザイン パターンを使用して、Web アプリと API をビルドするための豊富なフレームワークです。
モデル ビュー コントローラー (MVC) アーキテクチャ パターンについては、こちらの記事を参照ください。
1. (サーバーサイド) Model Derivative API の呼び出し処理を追加
Bucket に保存されている Revit プロジェクトファイルを Forge Viewer で表示するには、Model Derivative API を使用して、事前に SVF ファイルに変換する必要があります。
このサンプルでは、コントローラーに該当する DesignAutomationController.cs から、Forge の API を呼び出しています。
DesignAutomationController クラスでは、属性ルーティングという方法によって、クライアントからのリクエストを振り分けています。
※ 属性ルーティングについては、ASP.NET Core でのコントローラー アクションへのルーティングの解説ページをご参照ください。
Visual Studio 2022 で forgeSample プロジェクトの Controllers フォルダ配下にある DesignAutomationController.cs を開き、DesignAutomationController クラスに、下記の 2つのメソッド(StartTranslation メソッドと GetManifest メソッド)を追加してください。
- StartTranslation メソッドは、Revit プロジェクトファイルを SVF に変換する処理を呼び出します。
- GetManifest メソッドは、マニフェストファイルを取得して、変換処理の進捗状況を確認します。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation\forgeSample\Controllers\DesignAutomationController.cs
[HttpPost]
[HttpPost]
[Route("api/forge/modelderivative/job")]
public async Task<IActionResult> StartTranslation([FromBody]JObject translateSpecs)
{
string urn = translateSpecs["urn"].Value<string>();
dynamic oauth = await OAuthController.GetInternalAsync();
List<JobPayloadItem> outputs = new List<JobPayloadItem>()
{
new JobPayloadItem(
JobPayloadItem.TypeEnum.Svf,
new List<JobPayloadItem.ViewsEnum>()
{
JobPayloadItem.ViewsEnum._2d,
JobPayloadItem.ViewsEnum._3d
})
};
JobPayload job;
job = new JobPayload(new JobPayloadInput(urn), new JobPayloadOutput(outputs));
// start the translation
DerivativesApi derivative = new DerivativesApi();
derivative.Configuration.AccessToken = oauth.access_token;
dynamic jobPosted = await derivative.TranslateAsync(job);
return Ok();
}
[HttpGet]
[Route("api/forge/modelderivative/manifest")]
public async Task<IActionResult> GetManifest([FromQuery]string urn)
{
DerivativesApi derivative = new DerivativesApi();
dynamic result = await derivative.GetManifestAsyncWithHttpInfo(urn);
return Ok(new { Status = (string)result.Data.status, Progress = (string)result.Data.progress });
}
2. (サーバーサイド) WorkItem のコールバック処理で Revit プロジェクトファイルの objectId を返却
Design Automation API の WorkItem を POST する際に、"onComplete" という引数にコールバック URL を設定することができます。
この引数を事前に設定しておけば、WorkItem の完了時に、指定の URL を自動的に呼び出してくれます。詳細は、こちらのページに記載されています。
このサンプルでは、該当の処理が DesignAutomationController クラスの中に実装されています。
変数 callbackUrl で設定しているコールバック URL には、クエリパラメータが組み込まれていますが、Model Derivative API による変換処理を実行するためには、BucketKey が必要になります。
StartWorkitem メソッドの下記の変数 callbackUrl で、3つ目のクエリパラメータとして、bucketKey を追加してください。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation\forgeSample\Controllers\DesignAutomationController.cs
// prepare & submit workitem string callbackUrl = string.Format("{0}/api/forge/callback/designautomation?id={1}&outputFileName={2}&bucketKey={3}", OAuthController.GetAppSetting("FORGE_WEBHOOK_URL"), browerConnectionId, outputFileNameOSS, bucketKey); WorkItem workItemSpec = new WorkItem() { ActivityId = activityName, Arguments = new Dictionary<string, IArgument>() { { "inputFile", inputFileArgument }, { "inputJson", inputJsonArgument }, { "outputFile", outputFileArgument }, { "onComplete", new XrefTreeArgument { Verb = Verb.Post, Url = callbackUrl } } } };
そして、コールバック URLがリクエストされた際に呼び出される OnCallback メソッドでは、引数で bucketKey を取得するように変更します。
OnCallback メソッドの最後に、Bucket に保存されている Revit プロジェクトファイルの objectId を取得し、Base64 エンコードした文字列をクライアントに返す処理を追加してください。
Forge Viewer の表示に必要な objectId については、こちらのページをご参照ください。
まず、名前空間 using System.Text; をインポートしてください。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation\forgeSample\Controllers\DesignAutomationController.cs
/// /// Callback from Design Automation Workitem (onProgress or onComplete) /// [HttpPost] [Route("/api/forge/callback/designautomation")] public async Task<IActionResult> OnCallback(string id, string outputFileName, string bucketKey, [FromBody]dynamic body) { try { // your webhook should return immediately! we can use Hangfire to schedule a job JObject bodyJson = JObject.Parse((string)body.ToString()); await _hubContext.Clients.Client(id).SendAsync("onComplete", bodyJson.ToString()); var client = new RestClient(bodyJson["reportUrl"].Value<string>()); var request = new RestRequest(string.Empty); byte[] bs = client.DownloadData(request); string report = System.Text.Encoding.Default.GetString(bs); await _hubContext.Clients.Client(id).SendAsync("onComplete", report); ObjectsApi objectsApi = new ObjectsApi(); dynamic signedUrl = await objectsApi.CreateSignedResourceAsyncWithHttpInfo(NickName.ToLower() + "_designautomation", outputFileName, new PostBucketsSigned(10), "read"); await _hubContext.Clients.Client(id).SendAsync("downloadResult", (string)(signedUrl.Data.signedUrl)); dynamic objectDetail = await objectsApi.GetObjectDetailsAsyncWithHttpInfo(bucketKey, outputFileName); Encoding encoding = Encoding.UTF8; string objectUrn = (string)(objectDetail.Data.objectId); byte[] bytes = encoding.GetBytes(objectUrn); string urn = System.Convert.ToBase64String(bytes); await _hubContext.Clients.Client(id).SendAsync("translateResult", urn); } catch (Exception e) { } // ALWAYS return ok (200) return Ok(); }
ここで追加した処理の中で、最後に SendAsync() メソッドを呼び出していることがわかります。
これは、非同期でクライアントにレスポンスを返す処理ですが、このサンプルでは、ASP.NET Core SignalR という仕組みを利用して、プッシュ通知を実装しています。
ASP.NET Core SignalR は、アプリへのリアルタイム Web 機能の追加を簡素化するオープン ソース ライブラリです。
リアルタイム Web 機能は、サーバー側コードからクライアントにコンテンツを即座にプッシュすることを可能にします。
SignalR を利用してサーバーが SignalR ハブを作成すると、サーバーは、このハブに接続しているクライアントとリアルタイムに通信することができるようになります。
サーバー コードでは、クライアントによって呼び出されるメソッドを定義します。
クライアント コードでは、サーバーから呼び出されるメソッドを定義します。
このサンプルでは、SendAsync() メソッドの第1引数で指定している文字列は、クライアントサイドの JavaScript で呼び出すメソッドに対応しています。
3. (クライアントサイド) SignalR ハブで呼び出されるメソッドで、SVF 変換ボタンを追加
クライアントサイドの処理は、ForgeDesignAutomation.js に実装されています。
ForgeDesignAutomation.js を開いて、startConnection() メソッドの中に、下記の translateResult メソッドを追加してください。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation.modified\forgeSample\wwwroot\js\ForgeDesignAutomation.js
function startConnection(onReady) {
if (connection && connection.connectionState) { if (onReady) onReady(); return; }
connection = new signalR.HubConnectionBuilder().withUrl("/api/signalr/designautomation").build();
connection.start()
.then(function () {
connection.invoke('getConnectionId')
.then(function (id) {
connectionId = id; // we'll need this...
if (onReady) onReady();
});
});
connection.on("downloadResult", function (url) {
writeLog('<a href="' + url +'">Download result file here</a>');
});
connection.on("translateResult", function (urn) {
writeLog('<button class="btn btn-primary btn-start-translation" data-object-urn="' + urn + '" style="width: 200px;margin: 5px 0px;">Start Translation</button>');
});
connection.on("onComplete", function (message) {
writeLog(message);
});
}
これで、WorkItem のコールバック処理から、クライアントサイドの translateResult メソッドが呼び出され、画面の出力コンポーネントに SVF 変換ボタンを追加することができました。
WorkItem のコールバック処理で、SendAsync() メソッドの第2引数で渡した Revit プロジェクトファイルの objectId (Base64 エンコード済み)は、メソッドの引数で受け取り、独自データ属性によって、ボタンの属性データに保持させておきます。
この値は、以降に追加するボタンにも渡していきます。
4. (クライアントサイド) SVF 変換とマニフェスト取得のリクエスト処理を追加
ForgeDesignAutomation.js に、SVF 変換ボタンをクリックした際に呼び出されるリクエストの処理を追加します。
jQuery.ajax の success コールバックメソッドで、変換処理の完了時に、進捗状況を確認するためのボタンを追加しています。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation.modified\forgeSample\wwwroot\js\ForgeDesignAutomation.js
function startTranslation(e) {
var urn = $(e.currentTarget).data('object-urn');
writeLog('Start translation: ' + urn);
jQuery.ajax({
url: 'api/forge/modelderivative/job',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
urn: urn
}),
success: function (res) {
writeLog('<button class="btn btn-primary btn-get-manifest" data-object-urn="' + urn + '" style="width: 200px;margin: 5px 0px;">Get Manifest</button>');
}
});
}
マニフェストを取得するメソッドも追加します。
jQuery.ajax の success コールバックメソッドでは、進捗状況を出力し、変換処理が完了していれば、 Viewer を起動するボタンを追加します。
function getManifest(e) {
var urn = $(e.currentTarget).data('object-urn');
jQuery.ajax({
url: 'api/forge/modelderivative/manifest',
method: 'GET',
data: {
urn: urn
},
success: function (res) {
writeLog('Translation Status: ' + res.status + ', Progress: ' + res.progress);
if (res.status == 'success') {
writeLog('<button class="btn btn-primary btn-launch-viewer" data-object-urn="' + urn + '" style="width: 200px;margin: 5px 0px;">Launch Viewer</button>');
$('#launchViewer').click(launchViewer);
}
}
});
}
5. (クライアントサイド) 各ボタンにクリックイベントを追加
「Start Translation」ボタン、「Get Manifest」ボタン、「Launch Viewer」ボタンにクリックイベントを追加して、それぞれに対応するメソッドを呼び出す処理を記述してください。。
これらのボタンは、WorkItem を作成する度に動的に追加されていくため、出力コンポーネントに同じボタンが複数表示される可能性があります。
そのため、ドキュメントロード時に、.on( events [, selector ] [, data ], handler )メソッドを実行して、予めボタン要素にイベントハンドラをアタッチしておきます。
.on()メソッドは、マッチした要素に任意のイベントをバインドするメソッドですが、対象となるエレメントは現在マッチしているものも含め、将来的にマッチするものも対象となります。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation.modified\forgeSample\wwwroot\js\ForgeDesignAutomation.js
$(document).ready(function () {
prepareLists();
$('#clearAccount').click(clearAccount);
$('#defineActivityShow').click(defineActivityModal);
$('#createAppBundleActivity').click(createAppBundleActivity);
$('#startWorkitem').click(startWorkitem);
$('#outputlog').on("click", '.btn-start-translation', startTranslation);
$('#outputlog').on("click", '.btn-get-manifest', getManifest);
$('#outputlog').on("click", '.btn-launch-viewer', launchViewer);
startConnection();
});
6. (クライアントサイド) Forge Viewer コンポーネントの追加
index.html を開き、Forge Viewer のライブラリを読み込んでください。
そして、forgeViewer という ID を持つ DIV タグを追加してください。
編集するファイル
- C:\Users\ogasawr\GitHub Repo\learn.forge.designautomation\forgeSample\wwwroot\index.html
<head>
<title>Autodesk Forge - Design Automation</title>
~中略~
<!-- Autodesk Forge Viewer files -->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=no" />
<meta charset="utf-8">
<link rel="stylesheet" href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.min.css" type="text/css">
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js"></script>
<style>
body {
margin: 0;
}
#forgeViewer {
width: 100%;
height: 100%;
margin: 0;
background-color: #F0F8FF;
}
</style>
</head>
<div class="container-fluid" style="margin-top: 70px;">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label for="width">Width:</label>
<input type="number" class="form-control" id="width" placeholder="Enter new width value">
</div>
<div class="form-group">
<label for="height">Height:</label>
<input type="number" class="form-control" id="height" placeholder="Enter new height value">
</div>
<div class="form-group">
<label for="inputFile">Input file</label>
<input type="file" class="form-control-file" id="inputFile">
</div>
<div class="form-group">
<label for="activity">Existing activities</label>
<select class="form-control" id="activity"> </select>
</div>
<center><button class="btn btn-primary" id="startWorkitem">Start workitem</button></center><br />
</div>
<div class="col-sm-8">
<pre id="outputlog" style="height: calc(100vh - 120px); overflow-y: scroll;"></pre>
</div>
</div>
<div class="row">
<div class="col-sm-12" style="height: calc(100vh - 120px)">
<div id="forgeViewer"></div>
</div>
</div>
</div>
7. (クライアントサイド) Forge Viewer の初期化処理を追加
ForgeDesignAutomation.js に、下記の Forge Viewer の初期化処理を追加してください。
Forge Viewer を起動するためには、アクセストークンと Revit プロジェクトファイルの objectId (Base64 エンコード済み)が必要になります。
前者は、既に値を取得し、ボタンの独自データ属性として保持していますが、アクセストークンを取得する処理はまだ実装していません。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation\forgeSample\wwwroot\js\ForgeDesignAutomation.js
var viewer;
function launchViewer(e) {
var urn = $(e.currentTarget).data('object-urn');
writeLog('launchViewer: ' + urn);
var options = {
env: 'AutodeskProduction2',
api: 'streamingV2', // for models uploaded to EMEA change this option to 'streamingV2_EU'
getAccessToken: getForgeToken
};
var documentId = 'urn:' + urn;
Autodesk.Viewing.Initializer(options, function () {
var htmlDiv = document.getElementById('forgeViewer');
viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
var startedCode = viewer.start();
if (startedCode > 0) {
console.error('Failed to create a Viewer: WebGL not supported.');
return;
}
console.log('Initialization complete, loading a model next...');
Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
}
function onDocumentLoadSuccess(viewerDocument) {
var defaultModel = viewerDocument.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(viewerDocument, defaultModel);
}
function onDocumentLoadFailure() {
console.error('Failed fetching Forge manifest');
}
function getForgeToken(callback) {
jQuery.ajax({
url: '/api/forge/oauth/token',
success: function (res) {
callback(res.access_token, res.expires_in);
}
});
}
8. (サーバーサイド) Authentication API で、パブリックトークンを取得
このサンプルでは、Design Automation API と Data Management API を利用するために、サーバーサイドでのみ利用することを前提として、アクセストークンを取得するように実装されています。
Forge を利用するにあたっては、セキュリティの観点から、サーバーサイドで利用するアクセストークンと、クライアントサイドで利用するアクセストークンを、それぞれ分けて利用するよう推奨しております。
そして、前者をインターナルトークン、後者をパブリックトークンとして、それぞれに異なるスコープを割り当てます。
今回の場合は、インターナルトークンのスコープには下記のスコープを許可しています。
- Scope.BucketCreate, Scope.BucketRead, Scope.BucketDelete, Scope.DataRead, Scope.DataWrite, Scope.DataCreate, Scope.CodeAll
Forge Viewer で表示する上では、これらのスコープは必要なく、最低限の Scope.ViewablesRead だけが割り当てられていれば問題ありません。
OAuthController.cs を開いてください。
ここでは、インターナルトークンのみが実装されていることがわかります。
編集するファイル
- C:\Users\<Windows ユーザ名>\<任意のディレクトリ>\learn.forge.designautomation\forgeSample\Controllers\OAuthController.cs
下記のように、変数 PublicToken と、クライアントサイドに返却する処理 GetPublicAsync メソッドを追加してください。
private static dynamic InternalToken { get; set; }
private static dynamic PublicToken { get; set; }
[HttpGet]
[Route("api/forge/oauth/token")]
public async Task<dynamic> GetPublicAsync()
{
if (PublicToken == null || PublicToken.ExpiresAt < DateTime.UtcNow)
{
PublicToken = await Get2LeggedTokenAsync(new Scope[] { Scope.ViewablesRead });
PublicToken.ExpiresAt = DateTime.UtcNow.AddSeconds(PublicToken.expires_in);
}
return PublicToken;
}
これで準備は完了です。
9. サンプルの実行
前回と同じ手順でサンプルを実行してみましょう。
コマンド プロンプト を起動し、CD コマンドで ngrok.exe が保存されているディレクトリに移動します。
そして、下記のコマンドを実行してください。
-
ngrok http 3000 --host-header=localhost:3000 --scheme http
ngrok ツールを再起動すると、割り当てられたアドレスも変更しますので、新しく割り当てられたアドレスを環境変数 FORGE_WEBHOOK_URL に設定してください。
WorkItem の処理が終了すると、「Download result file here」リンクの後に、「Start Translation」ボタンが表示されるはずです。
「Start Translation」ボタンをクリックすると、SVF 変換処理が始まり、「Get Manifest」ボタンが表示されます。
「Get Manifest」ボタンをクリックして進捗状況を取得し、処理が完了していれば、「Launch Viewer」ボタンが表示されます。
「Launch Viewer」ボタンをクリックすると、Forge Viewer が起動して、Revit プロジェクトファイルを表示することができます。
今回は、learn.forge.designautomation サンプルに、Bucket に保存されている Revit プロジェクトファイルを Model Derivative API で変換して、Forge Viewer に表示する部分を追加しました。
Learn Autodesk Forge(日本語)チュートリアルの モデルを表示する セクションでは、別の Forge アプリとして、Bucket に保存されている Revit プロジェクトファイルを Model Derivative API で変換して、Forge Viewer に表示するサンプルが公開されています。
Bucket のファイルをツリー構造のコンポーネントで表示する ForgeTree.js も参考になりますので、ご興味ある方はぜひお試しください。
By Ryuji Ogasawara
コメント
コメントフィードを購読すればディスカッションを追いかけることができます。