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

30/11/2020

Forge Viewer: Реализуем собственный вид markup-ов

Расширение Autodesk.Viewing.MarkupsCore предоставляет ограниченные возможности реализации собственных видов markup-ов. Давайте создадим markup в виде смайлика.

Что можно и что нельзя сделать

Перед тем, как начать программировать, давайте посмотрим, какие возможности нам предоставляет расширение (по крайней мере в версии 7.30 Viewer-а)

- Мы можем создавать собственные виды markup-ов, которые будут создаваться и управляться расширением Autodesk.Viewing.MarkupsCore

- Autodesk.Viewing.MarkupsCore может сериализовывать markup-ы в SVG, но...

- это расширение не можете десериализовывать такие markup-ы из SVG. Поэтому нам нужно будет парсить его самостоятельно

Реализация

Пользовательские markup-ы реализуются с помощью наследования нескольких классов из пространства имен Autodesk.Viewing.Extensions.Markups.Core:

- Markup - этот класс представляет собой экземпляры markup-ов заданного типа markup-а

- EditAction - представляет собой реализацию возможных действий с markup-ом, таких как, например, создание или удаление конкретного экземпляра

- EditMode - является "контроллером", ответственным за создание, обновление и удаление экземпляров markup-ов конкретного типа, для этого он использует EditAction-ы.

Примечание: пространства имен и классы доступные только после загрузки расширения Autodesk.Viewing.MarkupsCore. Поэтому, Вам следует убедиться в том, что Ваш код будет загружен после загрузки этого расширения.

Начнем с реализации неследника класса Markup. Наш класс должен определять, как минимум, следующие методы:

- getEditMode() возвращает соответствующий EditMode

- set(position, size) обновляет позицию и размер markup-а

- updateStyle() - обновляет SVG аттрибуты markup-а в зависимости от его состояния

- setMetadata() - сохраняет состояние в элементе SVG

 

Пространство имен Autodesk.Viewing.Extensions.Markups.Core.Utils инкапсулирует значительную часть общих для всех markup-ов алгоритмов, чем мы, соответственно, будем также пользоваться.

 

Код - JavaScript: [Выделить]
  1. const avemc = Autodesk.Viewing.Extensions.Markups.Core;
  2. const avemcu = Autodesk.Viewing.Extensions.Markups.Core.Utils;
  3.  
  4. class MarkupSmiley extends avemc.Markup {
  5.     constructor(id, editor) {
  6.         super(id, editor, ['stroke-width', 'stroke-color', 'stroke-opacity', 'fill-color', 'fill-opacity']);
  7.         this.type = 'smiley';
  8.         this.addMarkupMetadata = avemcu.addMarkupMetadata.bind(this);
  9.         this.shape = avemcu.createMarkupPathSvg();
  10.         this.bindDomEvents();
  11.     }
  12.  
  13.     // Возвращает новый объект EditMode для нашего типа markup-а
  14.     getEditMode() {
  15.         return new EditModeSmiley(this.editor);
  16.     }
  17.  
  18.     // Создает данные для элемента path (SVG) в зависимости от параметров markup-а.
  19.     getPath() {
  20.         const { size } = this;
  21.         if (size.x === 1 || size.y === 1) {
  22.             return [''];
  23.         }
  24.  
  25.         const strokeWidth = this.style['stroke-width'];
  26.         const width = size.x - strokeWidth;
  27.         const height = size.y - strokeWidth;
  28.         const radius = 0.5 * Math.min(width, height);
  29.         const path = [
  30.             // Head
  31.             'M', -radius, 0,
  32.             'A', radius, radius, 0, 0, 1, radius, 0,
  33.             'A', radius, radius, 0, 0, 1, -radius, 0,
  34.  
  35.             // Mouth
  36.             'M', -0.5 * radius, -0.5 * radius,
  37.             'A', radius, radius, 0, 0, 1, 0.5 * radius, -0.5 * radius,
  38.  
  39.             // Left eye (closed)
  40.             'M', -0.5 * radius, 0.5 * radius,
  41.             'A', radius, radius, 0, 0, 1, -0.1 * radius, 0.5 * radius,
  42.  
  43.             // Right eye (closed)
  44.             'M', 0.1 * radius, 0.5 * radius,
  45.             'A', radius, radius, 0, 0, 1, 0.5 * radius, 0.5 * radius,
  46.         ];
  47.         return path;
  48.     }
  49.  
  50.     // обновляет положение и размер markup-а
  51.     set(position, size) {
  52.         this.setSize(position, size.x, size.y);
  53.     }
  54.  
  55.     // Обновляет SVG в зависимости от стиля, положения и размера markup-а
  56.     updateStyle() {
  57.         const { style, shape } = this;
  58.         const path = this.getPath().join(' ');
  59.  
  60.         const strokeWidth = this.style['stroke-width'];
  61.         const strokeColor = this.highlighted ? this.highlightColor : avemcu.composeRGBAString(style['stroke-color'], style['stroke-opacity']);
  62.         const fillColor = avemcu.composeRGBAString(style['fill-color'], style['fill-opacity']);
  63.         const transform = this.getTransform();
  64.  
  65.         avemcu.setAttributeToMarkupSvg(shape, 'd', path);
  66.         avemcu.setAttributeToMarkupSvg(shape, 'stroke-width', strokeWidth);
  67.         avemcu.setAttributeToMarkupSvg(shape, 'stroke', strokeColor);
  68.         avemcu.setAttributeToMarkupSvg(shape, 'fill', fillColor);
  69.         avemcu.setAttributeToMarkupSvg(shape, 'transform', transform);
  70.         avemcu.updateMarkupPathSvgHitarea(shape, this.editor);
  71.     }
  72.  
  73.     // Сохраняет тип markup-а, позицию, размер и стили в SVG
  74.     setMetadata() {
  75.         const metadata = avemcu.cloneStyle(this.style);
  76.         metadata.type = this.type;
  77.         metadata.position = [this.position.x, this.position.y].join(' ');
  78.         metadata.size = [this.size.x, this.size.y].join(' ');
  79.         metadata.rotation = String(this.rotation);
  80.         return this.addMarkupMetadata(this.shape, metadata);
  81.     }
  82. }

Большая часть кода этого класса - это просто шаблон, который будет практически одинаковым для любого пользовательского типа markup-а. Единственный уникальный метод здесь - это getPath, где мы собираем данные для элемента SVG path нашего смайлика.

 

Дальше, объявим 3 класса-наследника EditAction для создания, обновления и удаления markup-а-смайлика. Опять же, значительная часть кода здесь будет шаблонная для обработки операций undo/redo.

Код - JavaScript: [Выделить]
  1. class SmileyCreateAction extends avemc.EditAction {
  2.     constructor(editor, id, position, size, rotation, style) {
  3.         super(editor, 'CREATE-SMILEY', id);
  4.         this.selectOnExecution = false;
  5.         this.position = { x: position.x, y: position.y };
  6.         this.size = { x: size.x, y: size.y };
  7.         this.rotation = rotation;
  8.         this.style = avemcu.cloneStyle(style);
  9.     }
  10.  
  11.     redo() {
  12.         const editor = this.editor;
  13.         const smiley = new MarkupSmiley(this.targetId, editor);
  14.         editor.addMarkup(smiley);
  15.         smiley.setSize(this.position, this.size.x, this.size.y);
  16.         smiley.setRotation(this.rotation);
  17.         smiley.setStyle(this.style);
  18.     }
  19.  
  20.     undo() {
  21.         const markup = this.editor.getMarkup(this.targetId);
  22.         markup && this.editor.removeMarkup(markup);
  23.     }
  24. }
  25.  
  26. class SmileyUpdateAction extends avemc.EditAction {
  27.     constructor(editor, smiley, position, size) {
  28.         super(editor, 'UPDATE-SMILEY', smiley.id);
  29.         this.newPosition = { x: position.x, y: position.y };
  30.         this.newSize = { x: size.x, y: size.y };
  31.         this.oldPosition = { x: smiley.position.x, y: smiley.position.y };
  32.         this.oldSize = { x: smiley.size.x, y: smiley.size.y };
  33.     }
  34.  
  35.     redo() {
  36.         this.applyState(this.targetId, this.newPosition, this.newSize);
  37.     }
  38.  
  39.     undo() {
  40.         this.applyState(this.targetId, this.oldPosition, this.oldSize);
  41.     }
  42.  
  43.     merge(action) {
  44.         if (this.targetId === action.targetId && this.type === action.type) {
  45.             this.newPosition = action.newPosition;
  46.             this.newSize = action.newSize;
  47.             return true;
  48.         }
  49.         return false;
  50.     }
  51.  
  52.     applyState(targetId, position, size) {
  53.         const smiley = this.editor.getMarkup(targetId);
  54.         if(!smiley) {
  55.             return;
  56.         }
  57.  
  58.         // Different stroke widths make positions differ at sub-pixel level.
  59.         const epsilon = 0.0001;
  60.         if (Math.abs(smiley.position.x - position.x) > epsilon || Math.abs(smiley.size.y - size.y) > epsilon ||
  61.             Math.abs(smiley.position.y - position.y) > epsilon || Math.abs(smiley.size.y - size.y) > epsilon) {
  62.             smiley.set(position, size);
  63.         }
  64.     }
  65.  
  66.     isIdentity() {
  67.         return (
  68.             this.newPosition.x === this.oldPosition.x &&
  69.             this.newPosition.y === this.oldPosition.y &&
  70.             this.newSize.x === this.oldSize.x &&
  71.             this.newSize.y === this.oldSize.y
  72.         );
  73.     }
  74. }
  75.  
  76. class SmileyDeleteAction extends avemc.EditAction {
  77.     constructor(editor, smiley) {
  78.         super(editor, 'DELETE-SMILEY', smiley.id);
  79.         this.createSmiley = new SmileyCreateAction(
  80.             editor,
  81.             smiley.id,
  82.             smiley.position,
  83.             smiley.size,
  84.             smiley.rotation,
  85.             smiley.getStyle()
  86.         );
  87.     }
  88.  
  89.     redo() {
  90.         this.createSmiley.undo();
  91.     }
  92.  
  93.     undo() {
  94.         this.createSmiley.redo();
  95.     }

Осталось определить контроллер (EditMode) для нашего смайлика. Этот класс, фактически, всего лишь определяет методы для обработки пользовательского ввода (onMouseDown, onMouseMove и deleteMarkup), запуская соответствующие действия.

Код - JavaScript: [Выделить]
  1. class EditModeSmiley extends avemc.EditMode {
  2.     constructor(editor) {
  3.         super(editor, 'smiley', ['stroke-width', 'stroke-color', 'stroke-opacity', 'fill-color', 'fill-opacity']);
  4.     }
  5.  
  6.     deleteMarkup(markup, cantUndo) {
  7.         markup = markup || this.selectedMarkup;
  8.         if (markup && markup.type == this.type) {
  9.             const action = new SmileyDeleteAction(this.editor, markup);
  10.             action.addToHistory = !cantUndo;
  11.             action.execute();
  12.             return true;
  13.         }
  14.         return false;
  15.     }
  16.  
  17.     onMouseMove(event) {
  18.         super.onMouseMove(event);
  19.  
  20.         const { selectedMarkup, editor } = this;
  21.         if (!selectedMarkup || !this.creating) {
  22.             return;
  23.         }
  24.  
  25.         let final = this.getFinalMouseDraggingPosition();
  26.         final = editor.clientToMarkups(final.x, final.y);
  27.         let position = {
  28.             x: (this.firstPosition.x + final.x) * 0.5,
  29.             y: (this.firstPosition.y + final.y) * 0.5
  30.         };
  31.         let size = this.size = {
  32.             x:  Math.abs(this.firstPosition.x - final.x),
  33.             y: Math.abs(this.firstPosition.y - final.y)
  34.         };
  35.         const action = new SmileyUpdateAction(editor, selectedMarkup, position, size);
  36.         action.execute();
  37.     }
  38.  
  39.     onMouseDown() {
  40.         super.onMouseDown();
  41.         const { selectedMarkup, editor } = this;
  42.         if (selectedMarkup) {
  43.             return;
  44.         }
  45.  
  46.         // Получаем центр и размер
  47.         let mousePosition = editor.getMousePosition();
  48.         this.initialX = mousePosition.x;
  49.         this.initialY = mousePosition.y;
  50.         let position = this.firstPosition = editor.clientToMarkups(this.initialX, this.initialY);
  51.         let size = this.size = editor.sizeFromClientToMarkups(1, 1);
  52.  
  53.         editor.beginActionGroup();
  54.         const markupId = editor.getId();
  55.         const action = new SmileyCreateAction(editor, markupId, position, size, 0, this.style);
  56.         action.execute();
  57.  
  58.         this.selectedMarkup = editor.getMarkup(markupId);
  59.         this.creationBegin();
  60.     }
  61. }

Вот собственно, и всё описание нашего markup-а-смайлика!

Осталось только интегрировать новый markup в наше web-приложение с Forge Viewer-ом. Как мы уже отмечали ранее, мы не можем просто загрузить наш код JavaScript с помощью тэга <script> в HTML, поскольку пространство имен Autodesk.Viewing.Extensions.Markups.Core и всё его содержимое будет доступно только после загрузки расширения Autodesk.Viewing.MarkupsCore. Поэтому, будем загружать код динамически, например, следующим образом:

Код - JavaScript: [Выделить]
  1. class SmileyExtension extends Autodesk.Viewing.Extension {
  2.     async load() {
  3.         await this.viewer.loadExtension('Autodesk.Viewing.MarkupsCore');
  4.         await this.loadScript('/smiley-markup.js');
  5.         return true;
  6.     }
  7.  
  8.     unload() {
  9.         return true;
  10.     }
  11.  
  12.     loadScript(url) {
  13.         return new Promise(function (resolve, reject) {
  14.             const script = document.createElement('script');
  15.             script.setAttribute('src', url);
  16.             script.onload = resolve;
  17.             script.onerror = reject;
  18.             document.body.appendChild(script);
  19.         });
  20.     }
  21.  
  22.     startDrawing() {
  23.         const markupExt = this.viewer.getExtension('Autodesk.Viewing.MarkupsCore');
  24.         markupExt.show();
  25.         markupExt.enterEditMode();
  26.         markupExt.changeEditMode(new EditModeSmiley(markupExt));
  27.     }
  28.  
  29.     stopDrawing() {
  30.         const markupExt = this.viewer.getExtension('Autodesk.Viewing.MarkupsCore');
  31.         markupExt.leaveEditMode();
  32.     }
  33. }
  34.  
  35. Autodesk.Viewing.theExtensionManager.registerExtension('SmileyExtension', SmileyExtension);

Если Вы загрузили расширение при инициализации viewer-а, то Вы можете перейти в режим рисования markup-а следующим образом:

 

Код - JavaScript: [Выделить]
  1. const ext = viewer.getExtension('SmileyExtension');
  2. ext.startDrawing();

Вот и всё! На пример приложения Вы можете посмотреть здесь, полный код - здесь.

Источник: https://forge.autodesk.com/blog/implementing-custom-markups

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