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-ов алгоритмов, чем мы, соответственно, будем также пользоваться.
- const avemc = Autodesk.Viewing.Extensions.Markups.Core;
- const avemcu = Autodesk.Viewing.Extensions.Markups.Core.Utils;
- class MarkupSmiley extends avemc.Markup {
- constructor(id, editor) {
- super(id, editor, ['stroke-width', 'stroke-color', 'stroke-opacity', 'fill-color', 'fill-opacity']);
- this.type = 'smiley';
- this.addMarkupMetadata = avemcu.addMarkupMetadata.bind(this);
- this.shape = avemcu.createMarkupPathSvg();
- this.bindDomEvents();
- }
- // Возвращает новый объект EditMode для нашего типа markup-а
- getEditMode() {
- return new EditModeSmiley(this.editor);
- }
- // Создает данные для элемента path (SVG) в зависимости от параметров markup-а.
- getPath() {
- const { size } = this;
- if (size.x === 1 || size.y === 1) {
- return [''];
- }
- const strokeWidth = this.style['stroke-width'];
- const width = size.x - strokeWidth;
- const height = size.y - strokeWidth;
- const radius = 0.5 * Math.min(width, height);
- const path = [
- // Head
- 'M', -radius, 0,
- 'A', radius, radius, 0, 0, 1, radius, 0,
- 'A', radius, radius, 0, 0, 1, -radius, 0,
- // Mouth
- 'M', -0.5 * radius, -0.5 * radius,
- 'A', radius, radius, 0, 0, 1, 0.5 * radius, -0.5 * radius,
- // Left eye (closed)
- 'M', -0.5 * radius, 0.5 * radius,
- 'A', radius, radius, 0, 0, 1, -0.1 * radius, 0.5 * radius,
- // Right eye (closed)
- 'M', 0.1 * radius, 0.5 * radius,
- 'A', radius, radius, 0, 0, 1, 0.5 * radius, 0.5 * radius,
- ];
- return path;
- }
- // обновляет положение и размер markup-а
- set(position, size) {
- this.setSize(position, size.x, size.y);
- }
- // Обновляет SVG в зависимости от стиля, положения и размера markup-а
- updateStyle() {
- const { style, shape } = this;
- const path = this.getPath().join(' ');
- const strokeWidth = this.style['stroke-width'];
- const strokeColor = this.highlighted ? this.highlightColor : avemcu.composeRGBAString(style['stroke-color'], style['stroke-opacity']);
- const fillColor = avemcu.composeRGBAString(style['fill-color'], style['fill-opacity']);
- const transform = this.getTransform();
- avemcu.setAttributeToMarkupSvg(shape, 'd', path);
- avemcu.setAttributeToMarkupSvg(shape, 'stroke-width', strokeWidth);
- avemcu.setAttributeToMarkupSvg(shape, 'stroke', strokeColor);
- avemcu.setAttributeToMarkupSvg(shape, 'fill', fillColor);
- avemcu.setAttributeToMarkupSvg(shape, 'transform', transform);
- avemcu.updateMarkupPathSvgHitarea(shape, this.editor);
- }
- // Сохраняет тип markup-а, позицию, размер и стили в SVG
- setMetadata() {
- const metadata = avemcu.cloneStyle(this.style);
- metadata.type = this.type;
- metadata.position = [this.position.x, this.position.y].join(' ');
- metadata.size = [this.size.x, this.size.y].join(' ');
- metadata.rotation = String(this.rotation);
- return this.addMarkupMetadata(this.shape, metadata);
- }
- }
Большая часть кода этого класса - это просто шаблон, который будет практически одинаковым для любого пользовательского типа markup-а. Единственный уникальный метод здесь - это getPath, где мы собираем данные для элемента SVG path нашего смайлика.
Дальше, объявим 3 класса-наследника EditAction для создания, обновления и удаления markup-а-смайлика. Опять же, значительная часть кода здесь будет шаблонная для обработки операций undo/redo.
- class SmileyCreateAction extends avemc.EditAction {
- constructor(editor, id, position, size, rotation, style) {
- super(editor, 'CREATE-SMILEY', id);
- this.selectOnExecution = false;
- this.position = { x: position.x, y: position.y };
- this.size = { x: size.x, y: size.y };
- this.rotation = rotation;
- this.style = avemcu.cloneStyle(style);
- }
- redo() {
- const editor = this.editor;
- const smiley = new MarkupSmiley(this.targetId, editor);
- editor.addMarkup(smiley);
- smiley.setSize(this.position, this.size.x, this.size.y);
- smiley.setRotation(this.rotation);
- smiley.setStyle(this.style);
- }
- undo() {
- const markup = this.editor.getMarkup(this.targetId);
- markup && this.editor.removeMarkup(markup);
- }
- }
- class SmileyUpdateAction extends avemc.EditAction {
- constructor(editor, smiley, position, size) {
- super(editor, 'UPDATE-SMILEY', smiley.id);
- this.newPosition = { x: position.x, y: position.y };
- this.newSize = { x: size.x, y: size.y };
- this.oldPosition = { x: smiley.position.x, y: smiley.position.y };
- this.oldSize = { x: smiley.size.x, y: smiley.size.y };
- }
- redo() {
- this.applyState(this.targetId, this.newPosition, this.newSize);
- }
- undo() {
- this.applyState(this.targetId, this.oldPosition, this.oldSize);
- }
- merge(action) {
- if (this.targetId === action.targetId && this.type === action.type) {
- this.newPosition = action.newPosition;
- this.newSize = action.newSize;
- return true;
- }
- return false;
- }
- applyState(targetId, position, size) {
- const smiley = this.editor.getMarkup(targetId);
- if(!smiley) {
- return;
- }
- // Different stroke widths make positions differ at sub-pixel level.
- const epsilon = 0.0001;
- if (Math.abs(smiley.position.x - position.x) > epsilon || Math.abs(smiley.size.y - size.y) > epsilon ||
- Math.abs(smiley.position.y - position.y) > epsilon || Math.abs(smiley.size.y - size.y) > epsilon) {
- smiley.set(position, size);
- }
- }
- isIdentity() {
- return (
- this.newPosition.x === this.oldPosition.x &&
- this.newPosition.y === this.oldPosition.y &&
- this.newSize.x === this.oldSize.x &&
- this.newSize.y === this.oldSize.y
- );
- }
- }
- class SmileyDeleteAction extends avemc.EditAction {
- constructor(editor, smiley) {
- super(editor, 'DELETE-SMILEY', smiley.id);
- this.createSmiley = new SmileyCreateAction(
- editor,
- smiley.id,
- smiley.position,
- smiley.size,
- smiley.rotation,
- smiley.getStyle()
- );
- }
- redo() {
- this.createSmiley.undo();
- }
- undo() {
- this.createSmiley.redo();
- }
Осталось определить контроллер (EditMode) для нашего смайлика. Этот класс, фактически, всего лишь определяет методы для обработки пользовательского ввода (onMouseDown, onMouseMove и deleteMarkup), запуская соответствующие действия.
- class EditModeSmiley extends avemc.EditMode {
- constructor(editor) {
- super(editor, 'smiley', ['stroke-width', 'stroke-color', 'stroke-opacity', 'fill-color', 'fill-opacity']);
- }
- deleteMarkup(markup, cantUndo) {
- markup = markup || this.selectedMarkup;
- if (markup && markup.type == this.type) {
- const action = new SmileyDeleteAction(this.editor, markup);
- action.addToHistory = !cantUndo;
- action.execute();
- return true;
- }
- return false;
- }
- onMouseMove(event) {
- super.onMouseMove(event);
- const { selectedMarkup, editor } = this;
- if (!selectedMarkup || !this.creating) {
- return;
- }
- let final = this.getFinalMouseDraggingPosition();
- final = editor.clientToMarkups(final.x, final.y);
- let position = {
- x: (this.firstPosition.x + final.x) * 0.5,
- y: (this.firstPosition.y + final.y) * 0.5
- };
- let size = this.size = {
- x: Math.abs(this.firstPosition.x - final.x),
- y: Math.abs(this.firstPosition.y - final.y)
- };
- const action = new SmileyUpdateAction(editor, selectedMarkup, position, size);
- action.execute();
- }
- onMouseDown() {
- super.onMouseDown();
- const { selectedMarkup, editor } = this;
- if (selectedMarkup) {
- return;
- }
- // Получаем центр и размер
- let mousePosition = editor.getMousePosition();
- this.initialX = mousePosition.x;
- this.initialY = mousePosition.y;
- let position = this.firstPosition = editor.clientToMarkups(this.initialX, this.initialY);
- let size = this.size = editor.sizeFromClientToMarkups(1, 1);
- editor.beginActionGroup();
- const markupId = editor.getId();
- const action = new SmileyCreateAction(editor, markupId, position, size, 0, this.style);
- action.execute();
- this.selectedMarkup = editor.getMarkup(markupId);
- this.creationBegin();
- }
- }
Вот собственно, и всё описание нашего markup-а-смайлика!
Осталось только интегрировать новый markup в наше web-приложение с Forge Viewer-ом. Как мы уже отмечали ранее, мы не можем просто загрузить наш код JavaScript с помощью тэга <script> в HTML, поскольку пространство имен Autodesk.Viewing.Extensions.Markups.Core и всё его содержимое будет доступно только после загрузки расширения Autodesk.Viewing.MarkupsCore. Поэтому, будем загружать код динамически, например, следующим образом:
- class SmileyExtension extends Autodesk.Viewing.Extension {
- async load() {
- await this.viewer.loadExtension('Autodesk.Viewing.MarkupsCore');
- await this.loadScript('/smiley-markup.js');
- return true;
- }
- unload() {
- return true;
- }
- loadScript(url) {
- return new Promise(function (resolve, reject) {
- const script = document.createElement('script');
- script.setAttribute('src', url);
- script.onload = resolve;
- script.onerror = reject;
- document.body.appendChild(script);
- });
- }
- startDrawing() {
- const markupExt = this.viewer.getExtension('Autodesk.Viewing.MarkupsCore');
- markupExt.show();
- markupExt.enterEditMode();
- markupExt.changeEditMode(new EditModeSmiley(markupExt));
- }
- stopDrawing() {
- const markupExt = this.viewer.getExtension('Autodesk.Viewing.MarkupsCore');
- markupExt.leaveEditMode();
- }
- }
- Autodesk.Viewing.theExtensionManager.registerExtension('SmileyExtension', SmileyExtension);
Если Вы загрузили расширение при инициализации viewer-а, то Вы можете перейти в режим рисования markup-а следующим образом:
- const ext = viewer.getExtension('SmileyExtension');
- ext.startDrawing();
Вот и всё! На пример приложения Вы можете посмотреть здесь, полный код - здесь.
Источник: https://forge.autodesk.com/blog/implementing-custom-markups
Опубликовано 30.11.2020