ADN Open CIS
Сообщество программистов Autodesk в СНГ

16/09/2020

Forge Viewer: инструмент Snapper

Возможно, Вы уже задумывались о том, как бы хорошо было использовать привязки, предлагаемые такими инструмента Forge Viewer-а, как измерения (measure tool) и markups, в своих собственных проектах. Тогда у нас для Вас хорошие новости. В этой статье мы покажем, как Вы можете использовать их в своих расширениях Forge Viewer-а.

Алгоритмы привязок (snapper) инкапсулированы в классе Autodesk.Viewing.Extensions.Snapping.Snapper  расширения Autodesk.Snapping. Они реализованы как инструмент Forge Viewer-а (tool).

Мы ранее описывали для Вас основные концепции того, что мы называем инструментом (tool) Forge Viewer в статье: https://forge.autodesk.com/blog/custom-tools-forge-viewer. (см. также перевод на нашем сайте)

После добавления snapper-а в стек инструментов и его активации Forge Viewer начинает автоматически реагировать на действия пользователя. Вам, как разработчику собственного расширения остается добавить только обработку результатов, возвращаемых этим инструментом:

Код - JavaScript: [Выделить]
  1. // ...
  2.  
  3. viewer.loadExtension('Autodesk.Snapping');
  4.  
  5. // ...
  6.  
  7. let snapper = new Autodesk.Viewing.Extensions.Snapping.Snapper(viewer);
  8. viewer.toolController.registerTool(snapper);
  9. viewer.toolController.activateTool(snapper.getName());
  10.  
  11. // ...
  12.  
  13. if (snapper.isSnapped()) {
  14.     const result = snapper.getSnapResult();
  15.     const { SnapType } = Autodesk.Viewing.MeasureCommon;
  16.     switch (result.geomType) {
  17.         case SnapType.SNAP_VERTEX:
  18.             console.log('Snapped to vertex', result.geomVertex);
  19.             break;
  20.         case SnapType.SNAP_MIDPOINT:
  21.             console.log('Snapped to midpoint', result.geomVertex);
  22.             break;
  23.         case SnapType.SNAP_INTERSECTION:
  24.             console.log('Snapped to intersection', result.geomVertex);
  25.             break;
  26.         case SnapType.SNAP_CIRCLE_CENTER:
  27.             console.log('Snapped to circle center', result.geomVertex);
  28.             break;
  29.         case SnapType.RASTER_PIXEL:
  30.             console.log('Snapped to pixel', result.geomVertex);
  31.             break;
  32.         case SnapType.SNAP_EDGE:
  33.             console.log('Snapped to edge', result.geomEdge);
  34.             break;
  35.         case SnapType.SNAP_CIRCULARARC:
  36.             console.log('Snapped to circular arc', result.geomEdge);
  37.             break;
  38.         case SnapType.SNAP_CURVEDEDGE:
  39.             console.log('Snapped to curved edge', result.geomEdge);
  40.             break;
  41.         case SnapType.SNAP_FACE:
  42.             console.log('Snapped to face', result.geomFace);
  43.             break;
  44.         case SnapType.SNAP_CURVEDFACE:
  45.             console.log('Snapped to curved face', result.geomFace);
  46.             break;
  47.     }
  48. }
  49.  
  50. // ...

Объект, который возвращает метод snapper.getSnapResult() всегда будет содержать свойство geomType, по которому становится понятно, какой тип привязки был использован (см. Autodesk.Viewing.MeasureCommon.SnapType, где описаны все возможные значения). В зависимости от значения этого свойства, Вы можете в дальнейшем получить дополнительную информацию о привязке, такую как вершину (geomVertex), ребро (geomEdge) или поверхность (geomFace).

Ещё в этом объекте есть свойство "indicator". С его помощью Вы сможете контролировать визуальное представление, отображаемое в Forge Viewer:

Код - JavaScript: [Выделить]
  1. snapper.indicator.render(); // отобразить результаты привязки (mesh)
  2. snapper.indicator.clearOverlays(); // очистить привязку

Чтобы показать этот функционал на практическом примере, давайте создадим инструмент для рисования "3D областей" с использованием THREE.ExtrudeGeometry и этого замечательного инструмента привязки к вершинам, срединным точкам и пересечениям. Наш инструмент будет сохранять точки, выбранные пользователем и будет постоянно обновлять геометрию на сцене (overlay scene), включая текущую выбранную вершину (если такая существует). Контур выдавленной геометрии будет рассчитан на основе координат X, Y каждой точки, а высота будет определяться на основе минимального и максимального значений Z точек. После нажатия клавиши esc выдавленная геометрия будет «сохранена» на overlay сцене Forge Viewer-а, и будет запущено создание новой геометрии.

Обратите внимание, что в этой статье мы рассматриваем применение инструмента привязок для 3D моделей, но данный функционал также доступен и для 2D чертежей.

Реализация нашего инструмента может выглядеть следующим образом:

Код - JavaScript: [Выделить]
  1. const DrawBoundsToolName = 'draw-bounds-tool';
  2. const DrawBoundsOverlayName = 'draw-bounds-overlay';
  3.  
  4. class DrawBoundsTool extends Autodesk.Viewing.ToolInterface {
  5.     constructor(viewer) {
  6.         super();
  7.         this.viewer = viewer;
  8.         this.names = [DrawBoundsToolName];
  9.         this.active = false;
  10.         this.snapper = null;
  11.         this.points = []; // Нарисованные точки контура
  12.         this.mesh = null; // Геометрия (Mesh) нарисованной 3D-области
  13.         // Hack: удаляем методы, определенные в ToolInterface delete functions defined on the *instance* of a ToolInterface (мы хотим, чтобы взамен вызывались наши собственные методы)
  14.         delete this.register;
  15.         delete this.deregister;
  16.         delete this.activate;
  17.         delete this.deactivate;
  18.         delete this.getPriority;
  19.         delete this.handleMouseMove;
  20.         delete this.handleSingleClick;
  21.         delete this.handleKeyUp;
  22.     }
  23.  
  24.     register() {
  25.         this.snapper = new Autodesk.Viewing.Extensions.Snapping.Snapper(this.viewer, { renderSnappedGeometry: true, renderSnappedTopology: true });
  26.         this.viewer.toolController.registerTool(this.snapper);
  27.         this.viewer.toolController.activateTool(this.snapper.getName());
  28.         console.log('DrawBoundsTool registered.');
  29.     }
  30.  
  31.     deregister() {
  32.         this.viewer.toolController.deactivateTool(this.snapper.getName());
  33.         this.viewer.toolController.deregisterTool(this.snapper);
  34.         this.snapper = null;
  35.         console.log('DrawBoundsTool unregistered.');
  36.     }
  37.  
  38.     activate(name, viewer) {
  39.         if (!this.active) {
  40.             this.viewer.overlays.addScene(DrawBoundsOverlayName);
  41.             console.log('DrawBoundsTool activated.');
  42.             this.active = true;
  43.         }
  44.     }
  45.  
  46.     deactivate(name) {
  47.         if (this.active) {
  48.             this.viewer.overlays.removeScene(DrawBoundsOverlayName);
  49.             console.log('DrawBoundsTool deactivated.');
  50.             this.active = false;
  51.         }
  52.     }
  53.  
  54.     getPriority() {
  55.         return 42; // Вы можете использовать любое число (больше 0). 0 - приоритет tools-ов Viewer-а по умолчанию
  56.     }
  57.  
  58.     handleMouseMove(event) {
  59.         if (!this.active) {
  60.             return false;
  61.         }
  62.  
  63.         this.snapper.indicator.clearOverlays();
  64.         if (this.snapper.isSnapped()) {
  65.             const result = this.snapper.getSnapResult();
  66.             const { SnapType } = Autodesk.Viewing.MeasureCommon;
  67.             switch (result.geomType) {
  68.                 case SnapType.SNAP_VERTEX:
  69.                 case SnapType.SNAP_MIDPOINT:
  70.                 case SnapType.SNAP_INTERSECTION:
  71.                 case SnapType.SNAP_CIRCLE_CENTER:
  72.                 case SnapType.RASTER_PIXEL:
  73.                     // console.log('Snapped to vertex', result.geomVertex);
  74.                     this.snapper.indicator.render();
  75.                     this._update(result.geomVertex);
  76.                     break;
  77.                 case SnapType.SNAP_EDGE:
  78.                 case SnapType.SNAP_CIRCULARARC:
  79.                 case SnapType.SNAP_CURVEDEDGE:
  80.                     // console.log('Snapped to edge', result.geomEdge);
  81.                     break;
  82.                 case SnapType.SNAP_FACE:
  83.                 case SnapType.SNAP_CURVEDFACE:
  84.                     // console.log('Snapped to face', result.geomFace);
  85.                     break;
  86.             }
  87.         }
  88.         return false;
  89.     }
  90.  
  91.     handleSingleClick(event, button) {
  92.         if (!this.active) {
  93.             return false;
  94.         }
  95.  
  96.         if (button === 0 && this.snapper.isSnapped()) {
  97.             const result = this.snapper.getSnapResult();
  98.             const { SnapType } = Autodesk.Viewing.MeasureCommon;
  99.             switch (result.geomType) {
  100.                 case SnapType.SNAP_VERTEX:
  101.                 case SnapType.SNAP_MIDPOINT:
  102.                 case SnapType.SNAP_INTERSECTION:
  103.                 case SnapType.SNAP_CIRCLE_CENTER:
  104.                 case SnapType.RASTER_PIXEL:
  105.                     this.points.push(result.geomVertex.clone());
  106.                     this._update();
  107.                     break;
  108.                 default:
  109.                     // Не используем другие типы привязки
  110.                     break;
  111.             }
  112.             return true; // Событие не отправляется другим инструментам в стеке
  113.         }
  114.         return false;
  115.     }
  116.  
  117.     handleKeyUp(event, keyCode) {
  118.         if (this.active) {
  119.             if (keyCode === 27) {
  120.                 // Теперь будет создаваться новая геометрия 3D области
  121.                 this.points = [];
  122.                 this.mesh = null;
  123.                 return true;
  124.             }
  125.         }
  126.         return false;
  127.     }
  128.  
  129.     _update(intermediatePoint = null) {
  130.         if ((this.points.length + (intermediatePoint ? 1 : 0)) > 2) {
  131.             if (this.mesh) {
  132.                 this.viewer.overlays.removeMesh(this.mesh, DrawBoundsOverlayName);
  133.             }
  134.             let minZ = this.points[0].z, maxZ = this.points[0].z;
  135.             let shape = new THREE.Shape();
  136.             shape.moveTo(this.points[0].x, this.points[0].y);
  137.             for (let i = 1; i < this.points.length; i++) {
  138.                 shape.lineTo(this.points[i].x, this.points[i].y);
  139.                 minZ = Math.min(minZ, this.points[i].z);
  140.                 maxZ = Math.max(maxZ, this.points[i].z);
  141.             }
  142.             if (intermediatePoint) {
  143.                 shape.lineTo(intermediatePoint.x, intermediatePoint.y);
  144.                 minZ = Math.min(minZ, intermediatePoint.z);
  145.                 maxZ = Math.max(maxZ, intermediatePoint.z);
  146.             }
  147.             let geometry = new THREE.BufferGeometry().fromGeometry(new THREE.ExtrudeGeometry(shape, { steps: 1, amount: maxZ - minZ, bevelEnabled: false }));
  148.             let material = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, transparent: true });
  149.             this.mesh = new THREE.Mesh(geometry, material);
  150.             this.mesh.position.z = minZ;
  151.             this.viewer.overlays.addMesh(this.mesh, DrawBoundsOverlayName);
  152.             this.viewer.impl.sceneUpdated(true);
  153.         }
  154.     }
  155. }

Теперь создадим extension для Forge Viewer-а, чтобы наш код было проще использовать в других проектах. Для корректной работы нам необходимо, чтобы было загружено расширение Autodesk.Snapping, содержащее класс Autodesk.Viewing.Extensions.Snapping.Snapper:

Код - JavaScript: [Выделить]
  1. class DrawBoundsToolExtension extends Autodesk.Viewing.Extension {
  2.     constructor(viewer, options) {
  3.         super(viewer, options);
  4.         this.tool = new DrawBoundsTool(viewer);
  5.         this.button = null;
  6.     }
  7.  
  8.     async load() {
  9.         await this.viewer.loadExtension('Autodesk.Snapping');
  10.         this.viewer.toolController.registerTool(this.tool);
  11.         console.log('DrawBoundsToolExtension has been loaded.');
  12.         return true;
  13.     }
  14.  
  15.     async unload() {
  16.         this.viewer.toolController.deregisterTool(this.tool);
  17.         console.log('DrawBoundsToolExtension has been unloaded.');
  18.         return true;
  19.     }
  20.  
  21.     onToolbarCreated(toolbar) {
  22.         const controller = this.viewer.toolController;
  23.         this.button = new Autodesk.Viewing.UI.Button('draw-bounds-tool-button');
  24.         this.button.onClick = (ev) => {
  25.             if (controller.isToolActivated(DrawBoundsToolName)) {
  26.                 controller.deactivateTool(DrawBoundsToolName);
  27.                 this.button.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
  28.             } else {
  29.                 controller.activateTool(DrawBoundsToolName);
  30.                 this.button.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
  31.             }
  32.         };
  33.         this.button.setToolTip('Draw Bounds Tool');
  34.         this.group = new Autodesk.Viewing.UI.ControlGroup('draw-tool-group');
  35.         this.group.addControl(this.button);
  36.         toolbar.addControl(this.group);
  37.     }
  38. }
  39.  
  40. Autodesk.Viewing.theExtensionManager.registerExtension('DrawBoundsToolExtension', DrawBoundsToolExtension);

Вы можете найти полный код на GitHub, в особенности обратите внимание на DrawBoundsToolExtension.js

Источник: https://forge.autodesk.com/blog/snappy-viewer-tools

Автор перевода: Александр Игнатович
Опубликовано 16.09.2020