Пользовательские инструменты в Forge Viewer
Изучая API Forge Viewer-а, Вы, возможно, видели классы ToolController и ToolInterface в пространстве имен Viewing. Сегодня мы рассмотрим их подробнее.
Tool stack (стек инструментов)
Viewer одновременно использует множество инструментов, например, управление камерой, управление hotkey-ями или навигацию от первого лица. Они могут быть активны одновременно, обрабатывая входящие события и изменяя состояние и содержимое сцены viewer-а, в зависимости от приоритета каждого конкретного инструмента, формируя «стек инструментов». Класс ToolController управляет этим стеком и отправляет входящие события нужным инструментам. Он создается Viewer3D и может быть доступен из свойства viewer.toolController. ToolInterface – это интерфейс, который Вы можете реализовать для своего собственного инструмента. Давайте посмотрим, как это можно сделать.
Tool interface
Перед тем, как писать свой собственный код, давайте быстренько глянем код ToolInterface:
- /**
- * Базовый класс для реализации логики пользовательского взаимодействия
- *
- * Так же может быть использован как шаблон для создания нового инструмента
- * @constructor
- * @see Autodesk.Viewing.ToolController
- * @alias Autodesk.Viewing.ToolInterface
- */
- export function ToolInterface()
- {
- this.names = [ "unnamed" ];
- /**
- * Этот метод должен возвращать массив имен всех инструментов, реализованных в этом классе.
- * Обычно, это единственное имя, но также возможно создание разных вариантов пользовательских взаимодействий с помощью одного инструмента.
- * Когда инструмент регистрируется в ToolController, каждое имя регистрируется как доступный интерфейс.
- * @returns {array} Массив строк. Не должен быть пустым.
- */
- this.getNames = function() {
- return this.names;
- };
- /**
- * Опциональный метод для получения первого имени инструмента.
- * @returns {string} Имя по умолчанию.
- */
- this.getName = function() {
- return this.names[0];
- };
- /**
- * Этот метод должен возвращать приоритет инструмента в стеке.
- * Инструмент с высшим приоритетом будет получать события раньше.
- * @returns {number} Приоритет инструмента.
- */
- this.getPriority = function() {
- return 0;
- };
- /**
- * Метод вызывается из {@link Autodesk.Viewing.ToolController#registerTool}.
- * Используйте для инициализации.
- */
- this.register = function() {
- };
- /**
- * Метод вызывается из {@link Autodesk.Viewing.ToolController#deregisterTool}.
- * Используйте для очистки.
- */
- this.deregister = function() {
- };
- /**
- * Метод вызывается ToolController-ом при добавлении инструмента в список инструментов,
- * обрабатывающих события. После активации методы "handle*" можетбыть вызван,
- * если событие не было обработано инструментом с более высоким приоритетом. Метод "update" каждого активного инструмента также вызывается
- * в каждый цикл перерисовки.
- * @param {string} name - Имя активируемого инструмента.
- * @param {Autodesk.Viewing.Viewer3D} viewerApi – экземпляр Viewer-а.
- */
- this.activate = function(name, viewerApi) {
- };
- /**
- * Метод вызывается ToolController при удалении из списка активных инструментов
- * После деактивацииметоды "handle*" и "update"
- * более не будут вызываться
- * @param {string} name – Имя инструмента.
- */
- this.deactivate = function(name) {
- };
- /**
- * Метод вызывается ToolController один раз на frame и предоставляет каждому инструменту
- * возможность внести изменения в сцену или вид.
- * @param {number} highResTimestamp – Метка времени из requestAnimationFrame браузера.
- * @returns {boolean} Признак того, что в сцену или вид были внесены изменения
- * и необходимо полное обновление.
- */
- this.update = function(highResTimestamp) {
- return false;
- };
- /**
- * Метод вызывается при событии одинарного щелчка мышью.
- * @param {MouseEvent} event – Объект вызываемого события
- * @param {number} button – Номер нажатой кнопки (0 - левая, 1 - средняя, 2 - правая).
- * Примечание: номер нажатой кнопки может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleSingleClick = function( event, button ) {
- return false;
- };
- /**
- * Метод вызывается при событии двойного щелчка мышью.
- * @param {MouseEvent} event - Объект вызываемого события
- * @param {number} button - Номер нажатой кнопки (0 - левая, 1 - средняя, 2 - правая).
- * Примечание: номер нажатой кнопки может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleDoubleClick = function( event, button ) {
- return false;
- };
- /**
- * Метод вызывается при событии tap на устройствах, поддерживающих touch.
- * @param {Event} event - Объект вызываемого события. Свойства canvasX, canvasY содержат относительные координаты,
- * свойства normalizedX, normalizedY содержат
- * нормализованные координаты в диапазоне [-1, 1]. Свойство event.pointers - это массив, содержащий
- * один или два события, в зависимости от того, использованы при tap один или 2 пальца.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleSingleTap = function( event ) {
- return false;
- };
- /**
- * Метод вызывается при событии двойного tap на устройствах, поддерживающих touch.
- * @param {Event} event - Объект вызываемого события. Свойства canvasX, canvasY содержат относительные координаты,
- * свойства normalizedX, normalizedY содержат
- * нормализованные координаты в диапазоне [-1, 1]. Свойство event.pointers - это массив, содержащий
- * один или два события, в зависимости от того, использованы при tap один или 2 пальца.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleDoubleTap = function( event ) {
- return false;
- };
- /**
- * Метод вызывается при нажатии кнопки на клавиатуре.
- * @param {KeyboardEvent} event - Объект вызываемого события.
- * @param {number} keyCode - Числовой код нажатой кнопки.
- * Примечание: номер нажатой кнопки keyCode может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleKeyDown = function( event, keyCode ) {
- return false;
- };
- /**
- * Метод вызывается при отпускании кнопки на клавиатуре после нажатия.
- * @param {KeyboardEvent} event - Объект вызываемого события.
- * @param {number} keyCode - Числовой код нажатой кнопки.
- * Примечание: номер нажатой кнопки keyCode может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleKeyUp = function( event, keyCode ) {
- return false;
- };
- /**
- * Метод вызывается при событии прокрутки колеса мыши.
- * @param {number} delta – Числовое значение, характеризующее насколько было повернуто колесо мыши.
- * Примечание: значение может отличаться от оригинального в event для того, чтобы поведение инструмента было одинаковым в разных браузерах
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleWheelInput = function(delta) {
- return false;
- };
- /**
- * Метод вызывается при событии нажатия кнопки мыши.
- * @param {MouseEvent} event - Объект вызываемого события
- * @param {number} button - Номер нажатой кнопки (0 - левая, 1 - средняя, 2 - правая).
- * Примечание: номер нажатой кнопки может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleButtonDown = function(event, button) {
- return false;
- };
- /**
- * Метод вызывается при событии отпускания кнопки мыши.
- * @param {MouseEvent} event - Объект вызываемого события
- * @param {number} button - Номер нажатой кнопки (0 - левая, 1 - средняя, 2 - правая).
- * Примечание: номер нажатой кнопки может отличаться от соответствующего параметра в event
- * из-за переопределения кнопок в настройках и следует использовать именно это значение, а не свойство в объекте event.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleButtonUp = function(event, button) {
- return false;
- };
- /**
- * Метод вызывается при событии движения мыши.
- * @param {MouseEvent} event - Объект вызываемого события.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleMouseMove = function(event) {
- return false;
- };
- /**
- * Метод вызывается при событии touch жестов.
- * @param {Event} event - Объект вызываемого события. Свойство event.type содержит
- * тип жеста: dragstart, dragmove, dragend, panstart, panmove, panend,
- * pinchstart, pinchmove, pinchend, rotatestart, rotatemove, rotateend, drag3start, drag3move, drag3end.
- * Свойства event.canvas[XY] содержат соответствующую позицию жеста.
- * Свойства event.scale и event.rotation содержат значения масштабирования и поворота
- * соответственно. Свойства deltaX и deltaY содержат смещения.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleGesture = function(event) {
- return false;
- };
- /**
- * Метод вызывается при потере фокуса canvas-ом.
- * @param {FocusEvent} event - Объект вызываемого события.
- * @returns {boolean} False – если Вы хотите, чтобы инструменты с более низким приоритетом обработали это событие. True - чтобы не передавать событие далее по стеку
- */
- this.handleBlur = function(event) {
- return false;
- };
- /**
- * Метод вызывается при изменении размера canvas-а.
- * Новый размер canvas-а может быть получен из интерфейса Navigation с помощью метода getScreenViewport.
- * @see Autodesk.Viewing.Navigation
- */
- this.handleResize = function() {
- };
- }
Как Вы видите, интерфейс описывает несколько вещей. Есть методы для определения имён инструментов (поскольку класс может представлять сразу несколько инструментов) и приоритета. Инструменты с высшим приоритетом будут получать события раньше. Дальше идут несколько методов, определяющих жизненный цикл инструмента. Методы register/deregister вызываются когда ToolController запускает или останавливает инструмент. Методы activate/deactivate вызываются, когда инструмент становится активным или неактивным. Метод update вызывается многократно пока инструмент активен. Остальные методы интерфейса обрабатывают различные события.
Примечание: Вы, возможно, заметили, что методы интерфейса определены в конструкторе, что означает, что мы не можем просто переопределить их используя синтаксис классов. Существуют разные способы обхода данной проблемы. В нашем примере мы удалим методы объекта в нашем конструкторе класса и вместо них используем методы класса/прототипа.
Пользовательский инструмент
Давайте для примера создадим простой инструмент, позволяющий рисовать прямоугольные параллелепипеды и сферы. Вот как он будет работать:
- при событии mouse button down инструмент начнёт «рисовать» геометрию в плоскости XY
- при событии mouse up инструмент начнёт отслеживать события mouse move для контроля высоты геометрии
- при событии mouse click построение геометрии будет завершено
Базовый каркас нашего инструмента:
- class DrawTool extends Autodesk.Viewing.ToolInterface {
- constructor() {
- super();
- this.names = ['box-drawing-tool', 'sphere-drawing-tool'];
- // Hack: удаляем функции, определенные в *экземпляре* инструмента.
- // Мы хотим, чтобы tool controller вместо них вызывал методы нашего класса.
- delete this.register;
- delete this.deregister;
- delete this.activate;
- delete this.deactivate;
- delete this.getPriority;
- delete this.handleMouseMove;
- delete this.handleButtonDown;
- delete this.handleButtonUp;
- delete this.handleSingleClick;
- }
- register() {
- console.log('DrawTool registered.');
- }
- deregister() {
- console.log('DrawTool unregistered.');
- }
- activate(name, viewer) {
- console.log('DrawTool activated.');
- }
- deactivate(name) {
- console.log('DrawTool deactivated.');
- }
- getPriority() {
- return 42; // Или используйте любое число больше 0 (приоритета инструментов viewer-апо умолчанию)
- }
- update(highResTimestamp) {
- return false;
- }
- handleMouseMove(event) {
- return false;
- }
- handleButtonDown(event, button) {
- return false;
- }
- handleButtonUp(event, button) {
- return false;
- }
- handleSingleClick(event, button) {
- return false;
- }
- _update() {
- // Здесь мы будем обновлять геометрию
- }
- }
Теперь, давайте создадим реализацию методов activate и deactivate. При активации инструмента мы сохраним ссылку на объект viewer-а для дальнейшего использования, режим рисования (прямоугольный параллелепипед или сферу) в зависимости от имени активируемого инструмента и этап редактирования (рисуем основу в плоскости XY или определяем высоту создаваемой геометрии)
- activate(name, viewer) {
- this.viewer = viewer;
- this.mode = (name === 'box-drawing-tool') ? 'box' : 'sphere';
- this.state = ''; // может быть '' (не рисуем ничего), 'xy' (рисуем в плоскости XY), или 'z' (определяем высоту)
- console.log('DrawTool', name, 'activated.');
- }
- deactivate(name) {
- this.viewer = null;
- this.state = '';
- console.log('DrawTool', name, 'deactivated.');
- }
Для реализации первого этапа редактирования — определения размеров геометрии в плоскости XY переопределим методы handleButtonDown и handleButtonUp:
- handleButtonDown(event, button) {
- // Если нажата левая кнопка и мы пока ничего не редактируем
- if (button === 0 && this.state === '') {
- // Создаем новую геометрию и добавляем её на overlay
- if (this.mode === 'box') {
- const geometry = new THREE.BufferGeometry().fromGeometry(new THREE.BoxGeometry(1, 1, 1));
- const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 });
- this.mesh = new THREE.Mesh(geometry, material);
- } else {
- const geometry = new THREE.BufferGeometry().fromGeometry(new THREE.SphereGeometry(0.5, 16, 16));
- const material = new THREE.MeshPhongMaterial({ color: 0x0000ff });
- this.mesh = new THREE.Mesh(geometry, material);
- }
- this.viewer.impl.addOverlay('draw-tool-overlay', this.mesh);
- // Инициализируем 3 значения, контролирующие размер создаваемой геометрии (1ыйугол в плоскости XY, 2ойугол в плоскости XY и высоту)
- this.corner1 = this.corner2 = this.viewer.impl.intersectGround(event.clientX, event.clientY);
- this.height = 0.1;
- this._update();
- this.state = 'xy'; // Теперь рисуем в плоскости XY
- return true; // Теперь инструменты в стеке с приоритетом ниже не получат сообщение
- }
- // Иначе позволяем обработать событие другим инструментам и устанавливаем значение поля bypassed, чтобы пропускать обработку части других событий.
- this.bypassed = true;
- return false;
- }
- handleButtonUp(event, button) {
- // Левая кнопка была отжата и находимся в режиме рисования в плоскости XY
- if (button === 0 && this.state === 'xy') {
- // Обновляем 2 угол на плоскости XY. Переключаемся в режим "рисования высоты"
- this.corner2 = this.viewer.impl.intersectGround(event.clientX, event.clientY);
- this._update();
- this.state = 'z';
- this.lastClientY = event.clientY; // Сохраняем значение координаты Y, чтобы потом на её основе вычислять высоту
- return true; // Теперь инструменты в стеке с приоритетом ниже не получат сообщение
- }
- // Иначе позволяем обработать событие другим инструментам и устанавливаем значение поля bypassed, чтобы обрабатывать часть других событий
- this.bypassed = false;
- return false;
- }
Далее, переопределим метод handleMouseMove. В нашем случае, в зависимости от режима рисования, в котором находится инструмент, обновляем либо второй угол нашей геометрии в плоскости XY, либо её высоту:
- handleMouseMove(event) {
- if (!this.bypassed && this.state === 'xy') {
- // Мы в режиме "рисования в плоскости XY" и не обрабатываем событие другими методами
- this.corner2 = this.viewer.impl.intersectGround(event.clientX, event.clientY);
- this._update();
- return true;
- } else if (!this.bypassed && this.state === 'z') {
- // Мы в режиме "рисования высоты" и не обрабатываем событие другими методами
- this.height = this.lastClientY - event.clientY;
- this._update();
- return true;
- }
- // Иначе позволяем обработать событие другим инструментам
- return false;
- }
Теперь определим событие mouse single click, чтобы завершить размещение создаваемой геометрии:
- handleSingleClick(event, button) {
- // Нажата левая кнопка и мы в режиме "рисования высоты"
- if (button === 0 && this.state === 'z') {
- this.state = '';
- return true; // Теперь инструменты в стеке с приоритетом ниже не получат сообщение
- }
- // Иначе позволяем обработать событие другим инструментам
- return false;
- }
Теперь, когда мы закончили с методами обработки событий, осталось реализовать метод _update, который позиционирует и масштабирует текущую геометрию на основе значений corner1, corner2, и высоты:
- _update() {
- const { corner1, corner2, height, mesh } = this;
- const minX = Math.min(corner1.x, corner2.x), maxX = Math.max(corner1.x, corner2.x);
- const minY = Math.min(corner1.y, corner2.y), maxY = Math.max(corner1.y, corner2.y);
- mesh.position.x = minX + 0.5 * (maxX - minX);
- mesh.position.y = minY + 0.5 * (maxY - minY);
- mesh.position.z = 0.5 * height;
- mesh.scale.x = maxX - minX;
- mesh.scale.y = maxY - minY;
- mesh.scale.z = height;
- this.viewer.impl.invalidate(true, true, true);
- }
Теперь наш инструмент готов. Вы можете зарегистрировать и активировать его прямо из Viewer-а:
- const drawTool = new DrawTool();
- viewer.toolController.registerTool(drawTool);
- viewer.toolController.activateTool('box-drawing-tool');
Создадим расширение Viewer-а для работы с нашим новым инструментом:
- const BoxDrawToolName = 'box-draw-tool';
- const SphereDrawToolName = 'sphere-draw-tool';
- const DrawToolOverlay = 'draw-tool-overlay';
- class DrawToolExtension extends Autodesk.Viewing.Extension {
- constructor(viewer, options) {
- super(viewer, options);
- this.tool = new DrawTool();
- }
- load() {
- this.viewer.toolController.registerTool(this.tool);
- this.viewer.impl.createOverlayScene(DrawToolOverlay);
- console.log('DrawToolExtension loaded.');
- return true;
- }
- unload() {
- this.viewer.toolController.deregisterTool(this.tool);
- this.viewer.impl.removeOverlayScene(DrawToolOverlay);
- console.log('DrawToolExtension unloaded.');
- return true;
- }
- onToolbarCreated(toolbar) {
- const controller = this.viewer.toolController;
- this.button1 = new Autodesk.Viewing.UI.Button('box-draw-tool-button');
- this.button1.onClick = (ev) => {
- if (controller.isToolActivated(BoxDrawToolName)) {
- controller.deactivateTool(BoxDrawToolName);
- this.button1.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
- } else {
- controller.deactivateTool(SphereDrawToolName);
- controller.activateTool(BoxDrawToolName);
- this.button2.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
- this.button1.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
- }
- };
- this.button1.setToolTip('Box Draw Tool');
- this.button2 = new Autodesk.Viewing.UI.Button('sphere-draw-tool-button');
- this.button2.onClick = (ev) => {
- if (controller.isToolActivated(SphereDrawToolName)) {
- controller.deactivateTool(SphereDrawToolName);
- this.button2.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
- } else {
- controller.deactivateTool(BoxDrawToolName);
- controller.activateTool(SphereDrawToolName);
- this.button1.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
- this.button2.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
- }
- };
- this.button2.setToolTip('Sphere Draw Tool');
- this.group = new Autodesk.Viewing.UI.ControlGroup('draw-tool-group');
- this.group.addControl(this.button1);
- this.group.addControl(this.button2);
- toolbar.addControl(this.group);
- }
- }
При тестировании нового инструмента во Viewer-а, обратите внимание, как он взаимодействует с другими инструментами. Например, после завершения редактирования геометрии в плоскости XY перед установкой высоты с помощью клика мыши, вы можете использовать колесо мыши для масштабирования или нажать и удерживать левую кнопку мыши для поворота камеры. Магия стека инструментов!
Полный исходный код доступен здесь
Источник: https://forge.autodesk.com/blog/custom-tools-forge-viewer
Опубликовано 29.04.2020