Some test text!
Web / Guides
Platform
Documentation
WebViewer introduced freeform rotation for 8 annotation types: Line, Polygon, Polyline, Freehand, Ellipse, Rectangle, Stamp, FreeText. This guide will walk you through the steps to enable freeform rotation on a custom path-based annotation.
First let's define what's a path-based annotation: If the annotation can be drawn based on an array of points alone, then it's a path-based annnotation.
It's not the purpose of this guide to thoroughly talk about the steps to create a custom annotation. That said, we will just post the base code for the custom annotation.
If you are looking for a more in-depth explanation on how to create a custom annotation, please check this guide .
Webviewer(...).then((instance) => {
const { Core, UI } = instance;
const { Annotations, annotationManager, Tools, documentViewer } = Core;
// Beginning Custom Annotation Class
class RotatablePath extends Annotations.CustomAnnotation {
constructor() {
super('rotatable-path');
this.Subject = 'RotatablePath';
this.path = [
new Annotations.Point(this.X, this.Y),
new Annotations.Point(this.X + this.Width, this.Y),
new Annotations.Point(this.X + this.Width, this.Y + this.Height),
new Annotations.Point(this.X, this.Y + this.Height),
];
this.selectionModel = RotatableSelectionModel;
}
getPath() {
return this.path;
}
setPath(newPath) {
this.path = newPath;
}
draw(ctx, pageMatrix) {
this.setStyles(ctx, pageMatrix);
ctx.beginPath();
ctx.moveTo(this.path[0].x, this.path[0].y);
ctx.lineTo(this.path[1].x, this.path[1].y);
ctx.lineTo(this.path[2].x, this.path[2].y);
ctx.lineTo(this.path[3].x, this.path[3].y);
ctx.lineTo(this.path[0].x, this.path[0].y);
ctx.lineTo(this.path[2].x, this.path[2].y);
ctx.moveTo(this.path[1].x, this.path[1].y);
ctx.lineTo(this.path[3].x, this.path[3].y);
ctx.stroke();
}
resize(rect) {
const { x1, y1 } = rect;
const deltaX = x1 - this['X'];
const deltaY = y1 - this['Y'];
if (deltaX === 0 && deltaY === 0) {
return;
}
for (let i = 0; i < this.path.length; i++) {
this.path[i].translate(deltaX, deltaY);
}
this.adjustRect();
}
adjustRect() {
let minX = this.path[0]['x'];
let minY = this.path[0]['y'];
let maxX = this.path[0]['x'];
let maxY = this.path[0]['y'];
this.path.forEach((point) => {
if (point.x < minX) {
minX = point.x;
}
if (point.x > maxX) {
maxX = point.x;
}
if (point.y < minY) {
minY = point.y;
}
if (point.y > maxY) {
maxY = point.y;
}
});
this['X'] = minX;
this['Y'] = minY;
this['Width'] = maxX - minX;
this['Height'] = maxY - minY;
}
serialize(element, pageMatrix) {
this.setCustomData('trn-path', JSON.stringify(this.path));
return Annotations.CustomAnnotation.prototype.serialize.apply(this, arguments);
}
deserialize(element) {
Annotations.CustomAnnotation.prototype.deserialize.apply(this, arguments);
this.path = JSON.parse(this.getCustomData('trn-path'));
}
}
RotatablePath.prototype.elementName = 'rotatable-path';
annotationManager.registerAnnotationType(RotatablePath.prototype.elementName, RotatablePath);
// End Custom Annotation Class
// Beginning Control Handle
class RotatablePathControlHandle extends Annotations.PathControlHandle {
constructor(x, y, width, height, pathIndex) {
super(x, y, width, height, pathIndex);
this.pathIndex = pathIndex;
}
move(annotation, deltaX, deltaY) {
annotation.path[this.pathIndex] = new Annotations.Point(annotation.path[this.pathIndex].x + deltaX, annotation.path[this.pathIndex].y + deltaY);
annotation.adjustRect();
return true;
}
}
// End Control Handle
// Beginning Selection Model
class RotatableSelectionModel extends Annotations.SelectionModel {
constructor(annotation, canModify, isSelected, documentViewer) {
super(annotation, canModify, isSelected, documentViewer);
const controlHandles = this.getControlHandles();
if (canModify) {
for (let i = 0; i < annotation.path.length; i++) {
controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
}
}
}
}
// End Selection Model
// Beginning Tool
class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
constructor(documentViewer) {
super(documentViewer, RotatablePath);
}
mouseMove(e) {
super.mouseMove(e);
if (this.annotation) {
this.annotation.path[0].x = this.annotation.X;
this.annotation.path[0].y = this.annotation.Y;
this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
this.annotation.path[1].y = this.annotation.Y;
this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
this.annotation.path[3].x = this.annotation.X;
this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;
annotationManager.redrawAnnotation(this.annotation);
}
}
}
const rotatableToolName = 'AnnotationCreateRotatable';
const rotatableTool = new RotatableCreateTool(documentViewer);
UI.registerTool({
toolName: rotatableToolName,
toolObject: rotatableTool,
buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
'<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'</svg>',
buttonName: 'rotatableToolButton',
tooltip: 'Rotatable'
}, RotatablePath);
UI.setHeaderItems((header) => {
header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
type: 'toolButton',
toolName: rotatableToolName
});
});
// End Tool
});
WebViewer provides a convenient mixin out of the box for you to plug into the custom path-based annotation class. The PathCustomAnnotationRotationMixin will add the following methods:
Notice that this mixin already includes adjustRect and serialize / deserialize methods, so you can remove them from the custom annotation class body.
Lastly, for this mixin to work properly, the annotation class must implement two methods:
// Beginning Custom Annotation Class
class RotatablePath extends Annotations.CustomAnnotation {
...
}
Object.assign(RotatablePath.prototype, Annotations.RotationUtils.PathCustomAnnotationRotationMixin);
RotatablePath.prototype.elementName = 'rotatable-path';
annotationManager.registerAnnotationType(RotatablePath.prototype.elementName, RotatablePath);
// End Custom Annotation Class
});
In order to be able to actually rotate the annotation with the mouse movement, you will need to add a rotation control handle. In WebViewer, this is done by a selection model.
In this custom selection model, you have only to check if the rotation control handle is enabled and add the control handle itself to the list of control handles.
// Beginning Custom Annotation Class
...
// End Custom Annotation Class
// Beginning Selection Model
class RotatableSelectionModel extends Annotations.SelectionModel {
constructor(annotation, canModify, isSelected, documentViewer) {
super(annotation, canModify, isSelected, documentViewer);
const controlHandles = this.getControlHandles();
if (canModify) {
for (let i = 0; i < annotation.path.length; i++) {
controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
}
if (documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
}
}
}
}
// End Selection Model
// Beginning Tool
...
// End Tool
Webviewer(...).then((instance) => {
const { Core, UI } = instance;
const { Annotations, annotationManager, Tools, documentViewer } = Core;
// Beginning Custom Annotation Class
class RotatablePath extends Annotations.CustomAnnotation {
constructor() {
super('rotatable-path');
this.Subject = 'RotatablePath';
this.path = [
new Annotations.Point(this.X, this.Y),
new Annotations.Point(this.X + this.Width, this.Y),
new Annotations.Point(this.X + this.Width, this.Y + this.Height),
new Annotations.Point(this.X, this.Y + this.Height),
];
this.selectionModel = RotatableSelectionModel;
}
getPath() {
return this.path;
}
setPath(newPath) {
this.path = newPath;
}
draw(ctx, pageMatrix) {
this.setStyles(ctx, pageMatrix);
ctx.beginPath();
ctx.moveTo(this.path[0].x, this.path[0].y);
ctx.lineTo(this.path[1].x, this.path[1].y);
ctx.lineTo(this.path[2].x, this.path[2].y);
ctx.lineTo(this.path[3].x, this.path[3].y);
ctx.lineTo(this.path[0].x, this.path[0].y);
ctx.lineTo(this.path[2].x, this.path[2].y);
ctx.moveTo(this.path[1].x, this.path[1].y);
ctx.lineTo(this.path[3].x, this.path[3].y);
ctx.stroke();
}
resize(rect) {
const { x1, y1 } = rect;
const deltaX = x1 - this['X'];
const deltaY = y1 - this['Y'];
if (deltaX === 0 && deltaY === 0) {
return;
}
for (let i = 0; i < this.path.length; i++) {
this.path[i].translate(deltaX, deltaY);
}
this.adjustRect();
}
}
Object.assign(RotatablePath.prototype, Annotations.RotationUtils.PathCustomAnnotationRotationMixin);
RotatablePath.prototype.elementName = 'rotatable-path';
annotationManager.registerAnnotationType(RotatablePath.prototype.elementName, RotatablePath);
// End Custom Annotation Class
// Beginning Control Handle
class RotatablePathControlHandle extends Annotations.PathControlHandle {
constructor(x, y, width, height, pathIndex) {
super(x, y, width, height, pathIndex);
this.pathIndex = pathIndex;
}
move(annotation, deltaX, deltaY) {
annotation.getPath()[this.pathIndex] = new Annotations.Point(annotation.getPath()[this.pathIndex].x + deltaX, annotation.getPath()[this.pathIndex].y + deltaY);
annotation.adjustRect();
return true;
}
}
// End Control Handle
// Beginning Selection Model
class RotatableSelectionModel extends Annotations.SelectionModel {
constructor(annotation, canModify, isSelected, documentViewer) {
super(annotation, canModify, isSelected, documentViewer);
const controlHandles = this.getControlHandles();
if (canModify) {
for (let i = 0; i < annotation.path.length; i++) {
controlHandles.push(new RotatablePathControlHandle(annotation.path[i].x, annotation.path[i].y, Annotations.ControlHandle['handleWidth'], Annotations.ControlHandle['handleWidth'], i));
}
if (documentViewer.getAnnotationManager().isFreeformRotationEnabled() && annotation.hasRotationControlEnabled()) {
controlHandles.push(new Annotations.RotationControlHandle(Annotations.ControlHandle['rotationHandleWidth'], Annotations.ControlHandle['rotationHandleHeight'], 40, annotation, documentViewer));
}
}
}
}
// End Selection Model
// Beginning Tool
class RotatableCreateTool extends Tools.GenericAnnotationCreateTool {
constructor(documentViewer) {
super(documentViewer, RotatablePath);
}
mouseMove(e) {
super.mouseMove(e);
if (this.annotation) {
this.annotation.path[0].x = this.annotation.X;
this.annotation.path[0].y = this.annotation.Y;
this.annotation.path[1].x = this.annotation.X + this.annotation.Width;
this.annotation.path[1].y = this.annotation.Y;
this.annotation.path[2].x = this.annotation.X + this.annotation.Width;
this.annotation.path[2].y = this.annotation.Y + this.annotation.Height;
this.annotation.path[3].x = this.annotation.X;
this.annotation.path[3].y = this.annotation.Y + this.annotation.Height;
annotationManager.redrawAnnotation(this.annotation);
}
}
}
const rotatableToolName = 'AnnotationCreateRotatable';
const rotatableTool = new RotatableCreateTool(documentViewer);
UI.registerTool({
toolName: rotatableToolName,
toolObject: rotatableTool,
buttonImage: '<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 64 64">' +
'<line x1="9.37" x2="54.63" y1="9.37" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="9.37" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="54.63" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="54.63" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="9.37" y2="54.63" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'<line x1="9.37" x2="54.63" y1="54.63" y2="9.37" fill="none" stroke="#010101" stroke-miterlimit="10" stroke-width="4"/>' +
'</svg>',
buttonName: 'rotatableToolButton',
tooltip: 'Rotatable'
}, RotatablePath);
UI.setHeaderItems((header) => {
header.getHeader('toolbarGroup-Shapes').get('freeHandToolGroupButton').insertBefore({
type: 'toolButton',
toolName: rotatableToolName
});
});
// End Tool
});
Get the answers you need: Support