Forge Viewer:dbid に沿ったマークアップの表示

Forge Viewer:オーバーレイとシーン ビルダー では、Three.js メッシュを Forge Viewer カンバス上にオーバーレイ、または、シーン ビルダーを使って描画する方法をご紹介しました。この方法で、元のデザイン ファイルにない形状表現を実現することが可能になります。ただ、複雑な形状を多数配置すると Viewer 自体のパフォーマンスが低下したり、配置した肝心のメッシュが他のオブジェクトに紛れて目立たない、といった点も散見してきました。

Forge Viewer カンバス上の付加情報の表示という意味では、カンバス内に Three.js メッシュを表示する必要がない場合も存在します。デジタルツイン表現で特定のオブジェクトや位置にセンサー情報を明示したい場合などです。

Forge の登場以降、さまざまな方法が試行されてきましたが、ここでは、Three.js を使ったアプリでは比較的利用されている表現方法をご紹介しておきます。

Forge ブログでは、既に Placing custom markup by dbId 記事として紹介されているものです。この方法を使用すると、dbid と関連付けを持つ「マークアップ」表現を次のように実現することが出来るようになります。

仕組みとして、指定した dbid を使用して形状を構成する Three.js フラグメントを合成、その中心座標(ワールド座標)を  Viewer3D.worldToClient メソッドで HTML のスクリーン座標に変換、z-index 指定を用いて、カンバス上の適切な位置にオーバーレイすることで、CSS 表現された Label 要素をマークアップのように表示します。オービットやパンなどの画面操作には、Autodesk.Viewing.CAMERA_CHANGE_EVENT イベントを用いて検出、Label 位置を更新するものです。

同ブログで紹介している実装は Forge Viewer エクステンションになっているので、大きな変更を加えなくとも、そのままお手持ちの Forge Viewer で使用することが出来ます。冗長ですが、下記は、そのエクステンション コードを転記したものです。

class IconMarkupExtension extends Autodesk.Viewing.Extension {
    constructor(viewer, options) {
        super(viewer, options);
        this._group = null;
        this._button = null;
        this._icons = options.icons || [];
    }
    load() {
        const updateIconsCallback = () => {
            if (this._enabled) {
                this.updateIcons();
            }
        };
        this.viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateIconsCallback);
        this.viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, updateIconsCallback);
        this.viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, updateIconsCallback);
        this.viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, updateIconsCallback);
        return true;
    }
    unload() {
        // Clean our UI elements if we added any
        if (this._group) {
            this._group.removeControl(this._button);
            if (this._group.getNumberOfControls() === 0) {
                this.viewer.toolbar.removeControl(this._group);
            }
        }
        return true;
    }
    onToolbarCreated() {
        // Create a new toolbar group if it doesn't exist
        this._group = this.viewer.toolbar.getControl('customExtensions');
        if (!this._group) {
            this._group = new Autodesk.Viewing.UI.ControlGroup('customExtensions');
            this.viewer.toolbar.addControl(this._group);
        }
        // Add a new button to the toolbar group
        this._button = new Autodesk.Viewing.UI.Button('IconExtension');
        this._button.onClick = (ev) => {
            this._enabled = !this._enabled;
            this.showIcons(this._enabled);
            this._button.setState(this._enabled ? 0 : 1);
        };
        this._button.setToolTip(this.options.button.tooltip);
        this._button.container.children[0].classList.add('fas', this.options.button.icon);
        this._group.addControl(this._button);
    }
    showIcons(show) {
        const $viewer = $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer');
        // remove previous...
        $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer label.markup').remove();
        if (!show) return;
        // do we have anything to show?
        if (this._icons === undefined || this.icons === null) return;
        // do we have access to the instance tree?
        const tree = this.viewer.model.getInstanceTree();
        if (tree === undefined) { console.log('Loading tree...'); return; }
        const onClick = (e) => {
            if (this.options.onClick)
                this.options.onClick($(e.currentTarget).data('id'));
        };
        this._frags = {}
        for (var i = 0; i < this._icons.length; i++) {
            // we need to collect all the fragIds for a given dbId
            const icon = this._icons[i];
            this._frags['dbId' + icon.dbId] = []
            // create the label for the dbId
            const $label = $(`
<label class="markup update" data-id="${icon.dbId}">
<span class="${icon.css}"> ${icon.label || ''}</span>
</label>
`);
            $label.css('display', this.viewer.isNodeVisible(icon.dbId) ? 'block' : 'none');
            $label.on('click', onClick);
            $viewer.append($label);
            // now collect the fragIds
            const _this = this;
            tree.enumNodeFragments(icon.dbId, function (fragId) {
                _this._frags['dbId' + icon.dbId].push(fragId);
                _this.updateIcons(); // re-position of each fragId found
            });
        }
    }
    getModifiedWorldBoundingBox(dbId) {
        var fragList = this.viewer.model.getFragmentList();
        const nodebBox = new THREE.Box3()
        // for each fragId on the list, get the bounding box
        for (const fragId of this._frags['dbId' + dbId]) {
            const fragbBox = new THREE.Box3();
            fragList.getWorldBounds(fragId, fragbBox);
            nodebBox.union(fragbBox); // create a unifed bounding box
        }
        return nodebBox
    }
    updateIcons() {
        for (const label of $('#' + this.viewer.clientContainer.id + ' div.adsk-viewing-viewer.update')) {
            const $label = $(label);
            const id = $label.data('id');
            // get the center of the dbId (based on its fragIds bounding boxes)
            const pos = this.viewer.worldToClient(this.getModifiedWorldBoundingBox(id).center());
            // position the label center to it
            $label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
            $label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
            $label.css('display', this.viewer.isNodeVisible(id) ? 'block' : 'none');
        }
    }
}
Autodesk.Viewing.theExtensionManager.registerExtension('IconMarkupExtension', IconMarkupExtension);

このエクステンション、IconMarkupExtension エクステンションを使用するには、JQuery JavaScript ライブラリを参照する必要があるほか、ブログ記事では、<Label> タグのフォント表現と同じようにアイコン リソースを利用出来るよう、Font Awesome というライブラリも使用しています。

fa-exclamation のように特定のアイコン指定と同時に、fas、far などのアイコン スタイルを CSS スタイル名で指定することが出来るので便利です。上記の例では、一部、点滅などのアニメーションを見て取れますが、このような機能も Font Awesome 内に含まれます。

次のコードは、先の例で示したマークアップを作成するものです。dbid と同じ値(data-id)を HTML 要素に割り当てているので、マークアップへのクリック イベントで渡された ID を使って、Forge Viewer 上のオブジェクトを Viewer3D.Select メソッドで選択、また、Viewer3D.fitToView メソッドで拡大表示していることがわかります。

_viewer.loadExtension('IconMarkupExtension', {
    button: {
        icon: 'fa-thermometer-half',
        tooltip: 'Show Temperature'
    },
    icons: [
        { dbId: 83068, label: '非常階段:26°C', css: 'fa-thermometer-half fas temperatureBorder' },
        { dbId: 2886, label: ' 通用口:27°C', css: 'fa-thermometer-half temperatureOk fas temperatureBorder' },
        { dbId: 111934, label: '熱交換器:49°C', css: 'fa-thermometer-half temperatureHigh fas temperatureBorder' },
        { dbId: 2917, label: '要点検', css: 'iconWarning fas fa-exclamation fa-lg faa-horizontal animated' },
        { dbId: 111540, label: '故障', css: 'iconWarning  fas fa-exclamation-triangle fa-2x faa-flash animated' },
    ],
    onClick: (id) => {
        _viewer.select(id);
        _viewer.utilities.fitToView();
    }
});

もちろん、デベロッパーツールを使って表示されマークアップをチェックすると、実際の HTML 要素を確認することが出来ます。

Viewer 本体への負荷が少なく、一般的な HTML5 / CSS3 が適用出来るので、簡単に実装することが出来ます。参考としてご確認いただければと思います。

By Toshiaki Isezaki

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading