如何在 Forge 中创建多模型加载器和查看器

How to create a Multi-Model Loader and Viewer in Forge

我正在尝试使用 Nodejs 构建一个 Forge 多模型查看器,takes/loads 来自存储桶的多个模型并将它们显示在查看器中。

我使用了加载视图的教程,正在寻找一种将存储桶中的所有模型加载到 Forge Viewer 的解决方法。

如果我没理解错的话,你想在视图模型教程的顶部添加多模型支持,对吗?

为此,我们需要调整 ForgeViewer.js、ForgeTree.js 和 index.html

//  ForgeViewer.js 
/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

var viewer = null;

function launchViewer(models) {
  if (viewer != null) {
    viewer.tearDown()
    viewer.finish()
    viewer = null
    $("#forgeViewer").empty();
  }

  if (!models || models.length <= 0)
    return alert('Empty `models` input');

  var options = {
    env: 'MD20ProdUS',
    api: 'D3S',
    getAccessToken: getForgeToken
  };

  if (LMV_VIEWER_VERSION >= '7.48') {
    options.env = 'AutodeskProduction2';
    options.api = 'streamingV2';
  }

  Autodesk.Viewing.Initializer(options, () => {
    viewer = new Autodesk.Viewing.GuiViewer3D(document.getElementById('forgeViewer'));

    //load model one by one in sequence
    const util = new MultipleModelUtil(viewer);
    viewer.multipleModelUtil = util;

    // Use ShareCoordinates alignment instead
    // See https://github.com/yiskang/MultipleModelUtil for details
    // util.options = {
    //   alignment: MultipleModelAlignmentType.ShareCoordinates
    // };

    util.processModels(models);
  });
}

function getForgeToken(callback) {
  jQuery.ajax({
    url: '/api/forge/oauth/token',
    success: function (res) {
      callback(res.access_token, res.expires_in)
    }
  });
}
// ForgeTree.js
/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

$(document).ready(function () {
  prepareAppBucketTree();
  $('#refreshBuckets').click(function () {
    $('#appBuckets').jstree(true).refresh();
  });

  $('#createNewBucket').click(function () {
    createNewBucket();
  });

  $('#submitTranslation').click(function () {
    var treeNode = $('#appBuckets').jstree(true).get_selected(true)[0];
    submitTranslation(treeNode);
  });

  $('#createBucketModal').on('shown.bs.modal', function () {
    $("#newBucketKey").focus();
  });

  $('#CompositeTranslationModal').on('shown.bs.modal', function () {
    $("#rootDesignFilename").focus();
  });

  $('#hiddenUploadField').change(function () {
    var node = $('#appBuckets').jstree(true).get_selected(true)[0];
    var _this = this;
    if (_this.files.length == 0) return;
    var file = _this.files[0];
    switch (node.type) {
      case 'bucket':
        var formData = new FormData();
        formData.append('fileToUpload', file);
        formData.append('bucketKey', node.id);

        $.ajax({
          url: '/api/forge/oss/objects',
          data: formData,
          processData: false,
          contentType: false,
          type: 'POST',
          success: function (data) {
            $('#appBuckets').jstree(true).refresh_node(node);
            _this.value = '';
          }
        });
        break;
    }
  });

  $('#viewModels').click(function () {
    let treeInst = $('#appBuckets').jstree(true);
    let selectedNodeIds = $('#appBuckets').jstree('get_selected');
    let models = [];
    for (let i = 0; i < selectedNodeIds.length; i++) {
      let urn = selectedNodeIds[i];
      let node = treeInst.get_node(`${urn}_anchor`);
      if (!node || (node.type !== 'object'))
        continue;

      models.push({
        name: node.original.text,
        urn: `urn:${urn}`
      });
    }

    if (models.length <= 0 || (models.length !== selectedNodeIds.length))
      alert('Nothing selected or not all selected nodes are object-typed');

    launchViewer(models);
  });
});

function createNewBucket() {
  var bucketKey = $('#newBucketKey').val();
  var policyKey = $('#newBucketPolicyKey').val();
  jQuery.post({
    url: '/api/forge/oss/buckets',
    contentType: 'application/json',
    data: JSON.stringify({ 'bucketKey': bucketKey, 'policyKey': policyKey }),
    success: function (res) {
      $('#appBuckets').jstree(true).refresh();
      $('#createBucketModal').modal('toggle');
    },
    error: function (err) {
      if (err.status == 409)
        alert('Bucket already exists - 409: Duplicated')
      console.log(err);
    }
  });
}

function prepareAppBucketTree() {
  $('#appBuckets').jstree({
    'core': {
      'themes': { "icons": true },
      'data': {
        "url": '/api/forge/oss/buckets',
        "dataType": "json",
        'multiple': true,
        "data": function (node) {
          return { "id": node.id };
        }
      }
    },
    'types': {
      'default': {
        'icon': 'glyphicon glyphicon-question-sign'
      },
      '#': {
        'icon': 'glyphicon glyphicon-cloud'
      },
      'bucket': {
        'icon': 'glyphicon glyphicon-folder-open'
      },
      'object': {
        'icon': 'glyphicon glyphicon-file'
      }
    },
    "checkbox": {
      keep_selected_style: false,
      three_state: false,
      deselect_all: true,
      cascade: 'none'
    },
    "plugins": ["types", "checkbox", "state", "sort", "contextmenu"],
    contextmenu: { items: autodeskCustomMenu }
  }).on('loaded.jstree', function () {
    $('#appBuckets').jstree('open_all');
    $('#viewModels').show();
  // }).bind("activate_node.jstree", function (evt, data) {
  //   if (data != null && data.node != null && data.node.type == 'object') {
  //     $("#forgeViewer").empty();
  //     var urn = data.node.id;
  //     jQuery.ajax({
  //       url: '/api/forge/modelderivative/manifest/' + urn,
  //       success: function (res) {
  //         if (res.progress === 'success' || res.progress === 'complete') launchViewer(urn);
  //         else $("#forgeViewer").html('The translation job still running: ' + res.progress + '. Please try again in a moment.');
  //       },
  //       error: function (err) {
  //         var msgButton = 'This file is not translated yet! ' +
  //           '<button class="btn btn-xs btn-info" onclick="translateObject()"><span class="glyphicon glyphicon-eye-open"></span> ' +
  //           'Start translation</button>'
  //         $("#forgeViewer").html(msgButton);
  //       }
  //     });
  //   }
  });
}

function autodeskCustomMenu(autodeskNode) {
  var items;

  switch (autodeskNode.type) {
    case "bucket":
      items = {
        uploadFile: {
          label: "Upload file",
          action: function () {
            uploadFile();
          },
          icon: 'glyphicon glyphicon-cloud-upload'
        }
      };
      break;
    case "object":
      items = {
        translateFile: {
          label: "Translate",
          action: function () {
            var treeNode = $('#appBuckets').jstree(true).get_selected(true)[0];
            translateObject(treeNode);
          },
          icon: 'glyphicon glyphicon-eye-open'
        }
      };
      break;
  }

  return items;
}

function uploadFile() {
  $('#hiddenUploadField').click();
}

function submitTranslation(node) {
  $("#forgeViewer").empty();
  if (node == null) node = $('#appBuckets').jstree(true).get_selected(true)[0];
  var bucketKey = node.parents[0];
  var objectKey = node.id;
  var rootDesignFilename = $('#rootDesignFilename').val();
  var isSvf2 = $('#outputFormat :selected').text() === 'SVF2';
  var xAdsForce = ($('#xAdsForce').is(':checked') === true);
  var data = { 'bucketKey': bucketKey, 'objectName': objectKey, 'isSvf2': (isSvf2 === true), 'xAdsForce': xAdsForce };

  if((rootDesignFilename && rootDesignFilename.trim() && rootDesignFilename.trim().length > 0)) {
    data.rootFilename = rootDesignFilename;
    data.compressedUrn = true;
  }

  jQuery.post({
    url: '/api/forge/modelderivative/jobs',
    contentType: 'application/json',
    data: JSON.stringify(data),
    success: function (res) {
      $('#CompositeTranslationModal').modal('hide');
      $("#forgeViewer").html('Translation started! Please try again in a moment.');
    },
  });
}

function translateObject() {
  $('#CompositeTranslationModal').modal('show');
}
<!DOCTYPE html>
<html>

<head>
  <title>Autodesk Forge Tutorial</title>
  <meta charset="utf-8" />
  <link rel="shortcut icon" href="https://github.com/Autodesk-Forge/learn.forge.viewmodels/raw/master/img/favicon.ico">
  <!-- Common packages: jQuery, Bootstrap, jsTree -->
  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/jstree.min.js"></script>
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/themes/default/style.min.css" />
  <!-- Autodesk Forge Viewer files -->
  <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.js"></script>

  <script src="https://unpkg.com/@tweenjs/tween.js@18.6.4/dist/tween.umd.js"></script>
  <!-- MultipleModelUtil -->
  <script src="http://cdn.jsdelivr.net/gh/yiskang/MultipleModelUtil/MultipleModelUtil.js"></script>
  <!-- this project files -->
  <link href="css/main.css" rel="stylesheet" />
  <script src="js/ForgeTree.js"></script>
  <script src="js/ForgeViewer.js"></script>
</head>

<body>
  <!-- Fixed navbar by Bootstrap: https://getbootstrap.com/examples/navbar-fixed-top/ -->
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container-fluid">
      <ul class="nav navbar-nav left">
        <li>
          <a href="http://developer.autodesk.com" target="_blank">
            <img alt="Autodesk Forge" src="//developer.static.autodesk.com/images/logo_forge-2-line.png" height="20">
          </a>
        </li>
      </ul>
    </div>
  </nav>
  <!-- End of navbar -->
  <div class="container-fluid fill">
    <div class="row fill">
      <div class="col-sm-2 fill">
        <div class="panel panel-default fill">
          <div class="panel-heading" data-toggle="tooltip">
            Buckets &amp; Objects
            <span id="refreshBuckets" class="glyphicon glyphicon-refresh" style="cursor: pointer"></span>
            <span id="viewModels" class="glyphicon glyphicon-eye-open" style="cursor: pointer; display: none" title="View selected models in the Forge Viewer"></span>
            <button class="btn btn-xs btn-info" style="float: right" id="showFormCreateBucket" data-toggle="modal" data-target="#createBucketModal">
              <span class="glyphicon glyphicon-folder-close"></span> New bucket
            </button>
          </div>
          <div id="appBuckets">
            tree here
          </div>
        </div>
      </div>
      <div class="col-sm-10 fill">
        <div id="forgeViewer"></div>
      </div>
    </div>
  </div>
  <form id="uploadFile" method='post' enctype="multipart/form-data">
    <input id="hiddenUploadField" type="file" name="theFile" style="visibility:hidden" />
  </form>
  <!-- Modal Create Bucket -->
  <div class="modal fade" id="createBucketModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Cancel">
            <span aria-hidden="true">&times;</span>
          </button>
          <h4 class="modal-title" id="myModalLabel">Create new bucket</h4>
        </div>
        <div class="modal-body">
          <input type="text" id="newBucketKey" class="form-control"> For demonstration purposes, objects (files)
          are NOT automatically translated. After you upload, right click on
          the object and select "Translate". Note: Technically your bucket name is required to be globally unique across
          the entire platform - to keep things simple with this tutorial your client ID will be prepended by default to
          your bucket name and in turn masked by the UI so you only have to make sure your bucket name is unique within
          your current Forge app.
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
          <button type="button" class="btn btn-primary" id="createNewBucket">Go ahead, create the bucket</button>
        </div>
      </div>
    </div>
  </div>

  <!-- Composite model translation -->
  <div class="modal fade" id="CompositeTranslationModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Cancel">
            <span aria-hidden="true">&times;</span>
          </button>
          <h4 class="modal-title" id="myModalLabel">Composite Translation</h4>
        </div>
        <div class="modal-body">
          <div class="form-horizontal">
            <div class="form-group">
              <label for="outputFormat" class="col-sm-3 control-label">Output Format</label>
              <div class="col-sm-9">
                <select id="outputFormat" class="form-control">
                  <option>SVF</option>
                  <option selected="selected">SVF2</option>
                </select>
              </div>
            </div>
            <div class="form-group" style="margin-bottom: 0;">
              <label for="rootDesignFilename" class="col-sm-3 control-label">Root Filename</label>
              <div class="col-sm-9">
                <input type="text" id="rootDesignFilename" class="form-control" placeholder="Enter the filename of the root design">
              </div>
              <div class="col-sm-12">
                <span class="help-block" style="margin-bottom: 0; padding-left: 20px;">Enter the filename of the root design for the composite model. If the file is not a composite one, please press "Go ahead, submit translation" button directly.</span>
              </div>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <div class="checkbox pull-left">
            <label>
              <input type="checkbox" id="xAdsForce"> Replace previous result
            </label>
          </div>
          <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
          <button type="button" class="btn btn-primary" id="submitTranslation">Go ahead, submit translation</button>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

以下是我所做的更改:https://github.com/yiskang/forge-viewmodels-nodejs-svf2/commit/5025b846970c2d95909b379f6704a51ca24caffd

这是演示快照: