Merge pull request #19093 from calixteman/refactor_ink
[Editor] Add a new base class to allow to add a drawing in the SVG layer.
This commit is contained in:
commit
ee0df62bc8
@ -4466,7 +4466,7 @@ class InkAnnotation extends MarkupAnnotation {
|
|||||||
ink.set("Subtype", Name.get("Ink"));
|
ink.set("Subtype", Name.get("Ink"));
|
||||||
ink.set("CreationDate", `D:${getModificationDate()}`);
|
ink.set("CreationDate", `D:${getModificationDate()}`);
|
||||||
ink.set("Rect", rect);
|
ink.set("Rect", rect);
|
||||||
ink.set("InkList", outlines?.points || paths.map(p => p.points));
|
ink.set("InkList", outlines?.points || paths.points);
|
||||||
ink.set("F", 4);
|
ink.set("F", 4);
|
||||||
ink.set("Rotate", rotation);
|
ink.set("Rotate", rotation);
|
||||||
|
|
||||||
@ -4523,28 +4523,29 @@ class InkAnnotation extends MarkupAnnotation {
|
|||||||
appearanceBuffer.push("/R0 gs");
|
appearanceBuffer.push("/R0 gs");
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = [];
|
for (const outline of paths.lines) {
|
||||||
for (const { bezier } of paths) {
|
for (let i = 0, ii = outline.length; i < ii; i += 6) {
|
||||||
buffer.length = 0;
|
if (isNaN(outline[i])) {
|
||||||
buffer.push(
|
appearanceBuffer.push(
|
||||||
`${numberToString(bezier[0])} ${numberToString(bezier[1])} m`
|
`${numberToString(outline[i + 4])} ${numberToString(
|
||||||
);
|
outline[i + 5]
|
||||||
if (bezier.length === 2) {
|
)} m`
|
||||||
buffer.push(
|
);
|
||||||
`${numberToString(bezier[0])} ${numberToString(bezier[1])} l S`
|
} else {
|
||||||
);
|
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
|
||||||
} else {
|
appearanceBuffer.push(
|
||||||
for (let i = 2, ii = bezier.length; i < ii; i += 6) {
|
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " c"
|
||||||
const curve = bezier
|
);
|
||||||
.slice(i, i + 6)
|
|
||||||
.map(numberToString)
|
|
||||||
.join(" ");
|
|
||||||
buffer.push(`${curve} c`);
|
|
||||||
}
|
}
|
||||||
buffer.push("S");
|
|
||||||
}
|
}
|
||||||
appearanceBuffer.push(buffer.join("\n"));
|
if (outline.length === 6) {
|
||||||
|
appearanceBuffer.push(
|
||||||
|
`${numberToString(outline[4])} ${numberToString(outline[5])} l`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
appearanceBuffer.push("S");
|
||||||
|
|
||||||
const appearance = appearanceBuffer.join("\n");
|
const appearance = appearanceBuffer.join("\n");
|
||||||
|
|
||||||
const appearanceStreamDict = new Dict(xref);
|
const appearanceStreamDict = new Dict(xref);
|
||||||
@ -4587,18 +4588,17 @@ class InkAnnotation extends MarkupAnnotation {
|
|||||||
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
|
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
|
||||||
);
|
);
|
||||||
for (let i = 6, ii = outline.length; i < ii; i += 6) {
|
for (let i = 6, ii = outline.length; i < ii; i += 6) {
|
||||||
if (isNaN(outline[i]) || outline[i] === null) {
|
if (isNaN(outline[i])) {
|
||||||
appearanceBuffer.push(
|
appearanceBuffer.push(
|
||||||
`${numberToString(outline[i + 4])} ${numberToString(
|
`${numberToString(outline[i + 4])} ${numberToString(
|
||||||
outline[i + 5]
|
outline[i + 5]
|
||||||
)} l`
|
)} l`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const curve = outline
|
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
|
||||||
.slice(i, i + 6)
|
appearanceBuffer.push(
|
||||||
.map(numberToString)
|
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " c"
|
||||||
.join(" ");
|
);
|
||||||
appearanceBuffer.push(`${curve} c`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appearanceBuffer.push("h f");
|
appearanceBuffer.push("h f");
|
||||||
|
|||||||
@ -183,11 +183,18 @@ class DrawLayer {
|
|||||||
this.updateProperties(id, properties);
|
this.updateProperties(id, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProperties(elementOrId, { root, bbox, rootClass, path }) {
|
updateProperties(elementOrId, properties) {
|
||||||
|
if (!properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { root, bbox, rootClass, path } = properties;
|
||||||
const element =
|
const element =
|
||||||
typeof elementOrId === "number"
|
typeof elementOrId === "number"
|
||||||
? this.#mapping.get(elementOrId)
|
? this.#mapping.get(elementOrId)
|
||||||
: elementOrId;
|
: elementOrId;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (root) {
|
if (root) {
|
||||||
this.#updateProperties(element, root);
|
this.#updateProperties(element, root);
|
||||||
}
|
}
|
||||||
@ -207,6 +214,19 @@ class DrawLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateParent(id, layer) {
|
||||||
|
if (layer === this) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = this.#mapping.get(id);
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.#parent.append(root);
|
||||||
|
this.#mapping.delete(id);
|
||||||
|
layer.#mapping.set(id, root);
|
||||||
|
}
|
||||||
|
|
||||||
remove(id) {
|
remove(id) {
|
||||||
this.#toUpdate.delete(id);
|
this.#toUpdate.delete(id);
|
||||||
if (this.#parent === null) {
|
if (this.#parent === null) {
|
||||||
|
|||||||
@ -72,10 +72,10 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
#hadPointerDown = false;
|
#hadPointerDown = false;
|
||||||
|
|
||||||
#isCleaningUp = false;
|
|
||||||
|
|
||||||
#isDisabling = false;
|
#isDisabling = false;
|
||||||
|
|
||||||
|
#drawingAC = null;
|
||||||
|
|
||||||
#textLayer = null;
|
#textLayer = null;
|
||||||
|
|
||||||
#textSelectionAC = null;
|
#textSelectionAC = null;
|
||||||
@ -160,12 +160,9 @@ class AnnotationEditorLayer {
|
|||||||
this.disableClick();
|
this.disableClick();
|
||||||
return;
|
return;
|
||||||
case AnnotationEditorType.INK:
|
case AnnotationEditorType.INK:
|
||||||
// We always want to have an ink editor ready to draw in.
|
|
||||||
this.addInkEditorIfNeeded(false);
|
|
||||||
|
|
||||||
this.disableTextSelection();
|
this.disableTextSelection();
|
||||||
this.togglePointerEvents(true);
|
this.togglePointerEvents(true);
|
||||||
this.disableClick();
|
this.enableClick();
|
||||||
break;
|
break;
|
||||||
case AnnotationEditorType.HIGHLIGHT:
|
case AnnotationEditorType.HIGHLIGHT:
|
||||||
this.enableTextSelection();
|
this.enableTextSelection();
|
||||||
@ -193,30 +190,6 @@ class AnnotationEditorLayer {
|
|||||||
return textLayer === this.#textLayer?.div;
|
return textLayer === this.#textLayer?.div;
|
||||||
}
|
}
|
||||||
|
|
||||||
addInkEditorIfNeeded(isCommitting) {
|
|
||||||
if (this.#uiManager.getMode() !== AnnotationEditorType.INK) {
|
|
||||||
// We don't want to add an ink editor if we're not in ink mode!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCommitting) {
|
|
||||||
// We're removing an editor but an empty one can already exist so in this
|
|
||||||
// case we don't need to create a new one.
|
|
||||||
for (const editor of this.#editors.values()) {
|
|
||||||
if (editor.isEmpty()) {
|
|
||||||
editor.setInBackground();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = this.createAndAddNewEditor(
|
|
||||||
{ offsetX: 0, offsetY: 0 },
|
|
||||||
/* isCentered = */ false
|
|
||||||
);
|
|
||||||
editor.setInBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the editing state.
|
* Set the editing state.
|
||||||
* @param {boolean} isEditing
|
* @param {boolean} isEditing
|
||||||
@ -233,6 +206,10 @@ class AnnotationEditorLayer {
|
|||||||
this.#uiManager.addCommands(params);
|
this.#uiManager.addCommands(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanUndoStack(type) {
|
||||||
|
this.#uiManager.cleanUndoStack(type);
|
||||||
|
}
|
||||||
|
|
||||||
toggleDrawing(enabled = false) {
|
toggleDrawing(enabled = false) {
|
||||||
this.div.classList.toggle("drawing", !enabled);
|
this.div.classList.toggle("drawing", !enabled);
|
||||||
}
|
}
|
||||||
@ -482,10 +459,6 @@ class AnnotationEditorLayer {
|
|||||||
this.#uiManager.removeEditor(editor);
|
this.#uiManager.removeEditor(editor);
|
||||||
editor.div.remove();
|
editor.div.remove();
|
||||||
editor.isAttachedToDOM = false;
|
editor.isAttachedToDOM = false;
|
||||||
|
|
||||||
if (!this.#isCleaningUp) {
|
|
||||||
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -766,6 +739,13 @@ class AnnotationEditorLayer {
|
|||||||
}
|
}
|
||||||
this.#hadPointerDown = false;
|
this.#hadPointerDown = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#currentEditorType?.isDrawer &&
|
||||||
|
this.#currentEditorType.supportMultipleDrawings
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.#allowClick) {
|
if (!this.#allowClick) {
|
||||||
this.#allowClick = true;
|
this.#allowClick = true;
|
||||||
return;
|
return;
|
||||||
@ -808,10 +788,48 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
this.#hadPointerDown = true;
|
this.#hadPointerDown = true;
|
||||||
|
|
||||||
|
if (this.#currentEditorType?.isDrawer) {
|
||||||
|
this.startDrawingSession(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const editor = this.#uiManager.getActive();
|
const editor = this.#uiManager.getActive();
|
||||||
this.#allowClick = !editor || editor.isEmpty();
|
this.#allowClick = !editor || editor.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startDrawingSession(event) {
|
||||||
|
this.div.focus();
|
||||||
|
if (this.#drawingAC) {
|
||||||
|
this.#currentEditorType.startDrawing(this, this.#uiManager, false, event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#uiManager.unselectAll();
|
||||||
|
this.#drawingAC = new AbortController();
|
||||||
|
const signal = this.#uiManager.combinedSignal(this.#drawingAC);
|
||||||
|
this.div.addEventListener(
|
||||||
|
"blur",
|
||||||
|
({ relatedTarget }) => {
|
||||||
|
if (relatedTarget && !this.div.contains(relatedTarget)) {
|
||||||
|
this.commitOrRemove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
this.#uiManager.disableUserSelect(true);
|
||||||
|
this.#currentEditorType.startDrawing(this, this.#uiManager, false, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
endDrawingSession() {
|
||||||
|
if (!this.#drawingAC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#drawingAC.abort();
|
||||||
|
this.#drawingAC = null;
|
||||||
|
this.#uiManager.disableUserSelect(false);
|
||||||
|
this.#currentEditorType.endDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {AnnotationEditor} editor
|
* @param {AnnotationEditor} editor
|
||||||
@ -828,10 +846,26 @@ class AnnotationEditorLayer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitOrRemove() {
|
||||||
|
if (this.#drawingAC) {
|
||||||
|
this.endDrawingSession();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onScaleChanging() {
|
||||||
|
if (!this.#drawingAC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#currentEditorType.onScaleChangingWhenDrawing(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the main editor.
|
* Destroy the main editor.
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.commitOrRemove();
|
||||||
if (this.#uiManager.getActive()?.parent === this) {
|
if (this.#uiManager.getActive()?.parent === this) {
|
||||||
// We need to commit the current editor before destroying the layer.
|
// We need to commit the current editor before destroying the layer.
|
||||||
this.#uiManager.commitOrRemove();
|
this.#uiManager.commitOrRemove();
|
||||||
@ -858,13 +892,11 @@ class AnnotationEditorLayer {
|
|||||||
// When we're cleaning up, some editors are removed but we don't want
|
// When we're cleaning up, some editors are removed but we don't want
|
||||||
// to add a new one which will induce an addition in this.#editors, hence
|
// to add a new one which will induce an addition in this.#editors, hence
|
||||||
// an infinite loop.
|
// an infinite loop.
|
||||||
this.#isCleaningUp = true;
|
|
||||||
for (const editor of this.#editors.values()) {
|
for (const editor of this.#editors.values()) {
|
||||||
if (editor.isEmpty()) {
|
if (editor.isEmpty()) {
|
||||||
editor.remove();
|
editor.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.#isCleaningUp = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -896,6 +928,7 @@ class AnnotationEditorLayer {
|
|||||||
|
|
||||||
const oldRotation = this.viewport.rotation;
|
const oldRotation = this.viewport.rotation;
|
||||||
const rotation = viewport.rotation;
|
const rotation = viewport.rotation;
|
||||||
|
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
setLayerDimensions(this.div, { rotation });
|
setLayerDimensions(this.div, { rotation });
|
||||||
if (oldRotation !== rotation) {
|
if (oldRotation !== rotation) {
|
||||||
@ -903,7 +936,6 @@ class AnnotationEditorLayer {
|
|||||||
editor.rotate(rotation);
|
editor.rotate(rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
852
src/display/editor/draw.js
Normal file
852
src/display/editor/draw.js
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
/* Copyright 2022 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AnnotationEditorParamsType, unreachable } from "../../shared/util.js";
|
||||||
|
import { noContextMenu, stopEvent } from "../display_utils.js";
|
||||||
|
import { AnnotationEditor } from "./editor.js";
|
||||||
|
|
||||||
|
class DrawingOptions {
|
||||||
|
#svgProperties = Object.create(null);
|
||||||
|
|
||||||
|
updateProperty(name, value) {
|
||||||
|
this[name] = value;
|
||||||
|
this.updateSVGProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProperties(properties) {
|
||||||
|
if (!properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [name, value] of Object.entries(properties)) {
|
||||||
|
this.updateProperty(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSVGProperty(name, value) {
|
||||||
|
this.#svgProperties[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
toSVGProperties() {
|
||||||
|
const root = this.#svgProperties;
|
||||||
|
this.#svgProperties = Object.create(null);
|
||||||
|
return { root };
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.#svgProperties = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAll(options = this) {
|
||||||
|
this.updateProperties(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
unreachable("Not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic draw editor.
|
||||||
|
*/
|
||||||
|
class DrawingEditor extends AnnotationEditor {
|
||||||
|
#drawOutlines = null;
|
||||||
|
|
||||||
|
#mustBeCommitted;
|
||||||
|
|
||||||
|
_drawId = null;
|
||||||
|
|
||||||
|
static _currentDrawId = -1;
|
||||||
|
|
||||||
|
static _currentDraw = null;
|
||||||
|
|
||||||
|
static _currentDrawingOptions = null;
|
||||||
|
|
||||||
|
static _currentParent = null;
|
||||||
|
|
||||||
|
static _INNER_MARGIN = 3;
|
||||||
|
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
this.#mustBeCommitted = params.mustBeCommitted || false;
|
||||||
|
|
||||||
|
if (params.drawOutlines) {
|
||||||
|
this.#createDrawOutlines(params);
|
||||||
|
this.#addToDrawLayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDrawOutlines({ drawOutlines, drawId, drawingOptions }) {
|
||||||
|
this.#drawOutlines = drawOutlines;
|
||||||
|
this._drawingOptions ||= drawingOptions;
|
||||||
|
|
||||||
|
if (drawId >= 0) {
|
||||||
|
this._drawId = drawId;
|
||||||
|
// We need to redraw the drawing because we changed the coordinates to be
|
||||||
|
// in the box coordinate system.
|
||||||
|
this.parent.drawLayer.finalizeDraw(
|
||||||
|
drawId,
|
||||||
|
drawOutlines.defaultProperties
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// We create a new drawing.
|
||||||
|
this._drawId = this.#createDrawing(drawOutlines, this.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateBbox(drawOutlines.box);
|
||||||
|
}
|
||||||
|
|
||||||
|
#createDrawing(drawOutlines, parent) {
|
||||||
|
const { id } = parent.drawLayer.draw(
|
||||||
|
DrawingEditor._mergeSVGProperties(
|
||||||
|
this._drawingOptions.toSVGProperties(),
|
||||||
|
drawOutlines.defaultSVGProperties
|
||||||
|
),
|
||||||
|
/* isPathUpdatable = */ false,
|
||||||
|
/* hasClip = */ false
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _mergeSVGProperties(p1, p2) {
|
||||||
|
const p1Keys = new Set(Object.keys(p1));
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(p2)) {
|
||||||
|
if (p1Keys.has(key)) {
|
||||||
|
Object.assign(p1[key], value);
|
||||||
|
} else {
|
||||||
|
p1[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @return {DrawingOptions} the default options to use for a new editor.
|
||||||
|
*/
|
||||||
|
static getDefaultDrawingOptions(_options) {
|
||||||
|
unreachable("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Map<AnnotationEditorParamsType, string>} a map between the
|
||||||
|
* parameter types and the name of the options.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line getter-return
|
||||||
|
static get typesMap() {
|
||||||
|
unreachable("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isDrawer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} `true` if several drawings can be added to the
|
||||||
|
* annotation.
|
||||||
|
*/
|
||||||
|
static get supportMultipleDrawings() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static updateDefaultParams(type, value) {
|
||||||
|
const propertyName = this.typesMap.get(type);
|
||||||
|
if (propertyName) {
|
||||||
|
this._defaultDrawingOptions.updateProperty(propertyName, value);
|
||||||
|
}
|
||||||
|
if (this._currentParent) {
|
||||||
|
this._currentDraw.updateProperty(propertyName, value);
|
||||||
|
this._currentParent.drawLayer.updateProperties(
|
||||||
|
this._currentDrawId,
|
||||||
|
this._defaultDrawingOptions.toSVGProperties()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
updateParams(type, value) {
|
||||||
|
const propertyName = this.constructor.typesMap.get(type);
|
||||||
|
if (propertyName) {
|
||||||
|
this._updateProperty(type, propertyName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static get defaultPropertiesToUpdate() {
|
||||||
|
const properties = [];
|
||||||
|
const options = this._defaultDrawingOptions;
|
||||||
|
for (const [type, name] of this.typesMap) {
|
||||||
|
properties.push([type, options[name]]);
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get propertiesToUpdate() {
|
||||||
|
const properties = [];
|
||||||
|
const { _drawingOptions } = this;
|
||||||
|
for (const [type, name] of this.constructor.typesMap) {
|
||||||
|
properties.push([type, _drawingOptions[name]]);
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a property and make this action undoable.
|
||||||
|
* @param {string} color
|
||||||
|
*/
|
||||||
|
_updateProperty(type, name, value) {
|
||||||
|
const options = this._drawingOptions;
|
||||||
|
const savedValue = options[name];
|
||||||
|
const setter = val => {
|
||||||
|
options.updateProperty(name, val);
|
||||||
|
const bbox = this.#drawOutlines.updateProperty(name, val);
|
||||||
|
if (bbox) {
|
||||||
|
this.#updateBbox(bbox);
|
||||||
|
}
|
||||||
|
this.parent?.drawLayer.updateProperties(
|
||||||
|
this._drawId,
|
||||||
|
options.toSVGProperties()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
this.addCommands({
|
||||||
|
cmd: setter.bind(this, value),
|
||||||
|
undo: setter.bind(this, savedValue),
|
||||||
|
post: this._uiManager.updateUI.bind(this._uiManager, this),
|
||||||
|
mustExec: true,
|
||||||
|
type,
|
||||||
|
overwriteIfSameType: true,
|
||||||
|
keepUndo: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_onResizing() {
|
||||||
|
this.parent?.drawLayer.updateProperties(
|
||||||
|
this._drawId,
|
||||||
|
DrawingEditor._mergeSVGProperties(
|
||||||
|
this.#drawOutlines.getPathResizingSVGProperties(
|
||||||
|
this.#convertToDrawSpace()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
bbox: this.#rotateBox(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_onResized() {
|
||||||
|
this.parent?.drawLayer.updateProperties(
|
||||||
|
this._drawId,
|
||||||
|
DrawingEditor._mergeSVGProperties(
|
||||||
|
this.#drawOutlines.getPathResizedSVGProperties(
|
||||||
|
this.#convertToDrawSpace()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
bbox: this.#rotateBox(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_onTranslating(x, y) {
|
||||||
|
this.parent?.drawLayer.updateProperties(this._drawId, {
|
||||||
|
bbox: this.#rotateBox(x, y),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_onTranslated() {
|
||||||
|
this.parent?.drawLayer.updateProperties(
|
||||||
|
this._drawId,
|
||||||
|
DrawingEditor._mergeSVGProperties(
|
||||||
|
this.#drawOutlines.getPathTranslatedSVGProperties(
|
||||||
|
this.#convertToDrawSpace(),
|
||||||
|
this.parentDimensions
|
||||||
|
),
|
||||||
|
{
|
||||||
|
bbox: this.#rotateBox(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onStartDragging() {
|
||||||
|
this.parent?.drawLayer.updateProperties(this._drawId, {
|
||||||
|
rootClass: {
|
||||||
|
moving: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onStopDragging() {
|
||||||
|
this.parent?.drawLayer.updateProperties(this._drawId, {
|
||||||
|
rootClass: {
|
||||||
|
moving: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
commit() {
|
||||||
|
super.commit();
|
||||||
|
|
||||||
|
this.disableEditMode();
|
||||||
|
this.disableEditing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
disableEditing() {
|
||||||
|
super.disableEditing();
|
||||||
|
this.div.classList.toggle("disabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
enableEditing() {
|
||||||
|
super.enableEditing();
|
||||||
|
this.div.classList.toggle("disabled", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
getBaseTranslation() {
|
||||||
|
// The editor itself doesn't have any CSS border (we're drawing one
|
||||||
|
// ourselves in using SVG).
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get isResizable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
onceAdded() {
|
||||||
|
if (!this.annotationElementId) {
|
||||||
|
this.parent.addUndoableEditor(this);
|
||||||
|
}
|
||||||
|
this._isDraggable = true;
|
||||||
|
if (this.#mustBeCommitted) {
|
||||||
|
this.#mustBeCommitted = false;
|
||||||
|
this.commit();
|
||||||
|
this.parent.setSelected(this);
|
||||||
|
this.div.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
remove() {
|
||||||
|
this.#cleanDrawLayer();
|
||||||
|
super.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
rebuild() {
|
||||||
|
if (!this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.rebuild();
|
||||||
|
if (this.div === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#addToDrawLayer();
|
||||||
|
this.#updateBbox(this.#drawOutlines.box);
|
||||||
|
|
||||||
|
if (!this.isAttachedToDOM) {
|
||||||
|
// At some point this editor was removed and we're rebuilding it,
|
||||||
|
// hence we must add it to its parent.
|
||||||
|
this.parent.add(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setParent(parent) {
|
||||||
|
let mustBeSelected = false;
|
||||||
|
if (this.parent && !parent) {
|
||||||
|
this._uiManager.removeShouldRescale(this);
|
||||||
|
this.#cleanDrawLayer();
|
||||||
|
} else if (parent) {
|
||||||
|
this._uiManager.addShouldRescale(this);
|
||||||
|
this.#addToDrawLayer(parent);
|
||||||
|
// If mustBeSelected is true it means that this editor was selected
|
||||||
|
// when its parent has been destroyed, hence we must select it again.
|
||||||
|
mustBeSelected =
|
||||||
|
!this.parent && this.div?.classList.contains("selectedEditor");
|
||||||
|
}
|
||||||
|
super.setParent(parent);
|
||||||
|
if (mustBeSelected) {
|
||||||
|
// We select it after the parent has been set.
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#cleanDrawLayer() {
|
||||||
|
if (this._drawId === null || !this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.parent.drawLayer.remove(this._drawId);
|
||||||
|
this._drawId = null;
|
||||||
|
|
||||||
|
// All the SVG properties must be reset in order to make it possible to
|
||||||
|
// undo.
|
||||||
|
this._drawingOptions.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#addToDrawLayer(parent = this.parent) {
|
||||||
|
if (this._drawId !== null && this.parent === parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._drawId !== null) {
|
||||||
|
// The parent has changed, we need to move the drawing to the new parent.
|
||||||
|
this.parent.drawLayer.updateParent(this._drawId, parent.drawLayer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._drawingOptions.updateAll();
|
||||||
|
this._drawId = this.#createDrawing(this.#drawOutlines, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#convertToParentSpace([x, y, width, height]) {
|
||||||
|
const {
|
||||||
|
parentDimensions: [pW, pH],
|
||||||
|
rotation,
|
||||||
|
} = this;
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return [y, 1 - x, width * (pH / pW), height * (pW / pH)];
|
||||||
|
case 180:
|
||||||
|
return [1 - x, 1 - y, width, height];
|
||||||
|
case 270:
|
||||||
|
return [1 - y, x, width * (pH / pW), height * (pW / pH)];
|
||||||
|
default:
|
||||||
|
return [x, y, width, height];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#convertToDrawSpace() {
|
||||||
|
const {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
parentDimensions: [pW, pH],
|
||||||
|
rotation,
|
||||||
|
} = this;
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return [1 - y, x, width * (pW / pH), height * (pH / pW)];
|
||||||
|
case 180:
|
||||||
|
return [1 - x, 1 - y, width, height];
|
||||||
|
case 270:
|
||||||
|
return [y, 1 - x, width * (pW / pH), height * (pH / pW)];
|
||||||
|
default:
|
||||||
|
return [x, y, width, height];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateBbox(bbox) {
|
||||||
|
[this.x, this.y, this.width, this.height] =
|
||||||
|
this.#convertToParentSpace(bbox);
|
||||||
|
if (this.div) {
|
||||||
|
this.fixAndSetPosition();
|
||||||
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||||
|
this.setDims(this.width * parentWidth, this.height * parentHeight);
|
||||||
|
}
|
||||||
|
this._onResized();
|
||||||
|
}
|
||||||
|
|
||||||
|
#rotateBox() {
|
||||||
|
// We've to deal with two rotations: the rotation of the annotation and the
|
||||||
|
// rotation of the parent page.
|
||||||
|
// When the page is rotated, all the layers are just rotated thanks to CSS
|
||||||
|
// but there is a notable exception: the canvas wrapper.
|
||||||
|
// The canvas wrapper is not rotated but the dimensions are (or not) swapped
|
||||||
|
// and the page is redrawn with the rotation applied to the canvas.
|
||||||
|
// The drawn layer is under the canvas wrapper and is not rotated so we have
|
||||||
|
// to "manually" rotate the coordinates.
|
||||||
|
//
|
||||||
|
// The coordinates (this.x, this.y) correspond to the top-left corner of
|
||||||
|
// the editor after it has been rotated in the page coordinate system.
|
||||||
|
|
||||||
|
const {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rotation,
|
||||||
|
parentRotation,
|
||||||
|
parentDimensions: [pW, pH],
|
||||||
|
} = this;
|
||||||
|
switch ((rotation * 4 + parentRotation) / 90) {
|
||||||
|
case 1:
|
||||||
|
// 0 -> 90
|
||||||
|
return [1 - y - height, x, height, width];
|
||||||
|
case 2:
|
||||||
|
// 0 -> 180
|
||||||
|
return [1 - x - width, 1 - y - height, width, height];
|
||||||
|
case 3:
|
||||||
|
// 0 -> 270
|
||||||
|
return [y, 1 - x - width, height, width];
|
||||||
|
case 4:
|
||||||
|
// 90 -> 0
|
||||||
|
return [
|
||||||
|
x,
|
||||||
|
y - width * (pW / pH),
|
||||||
|
height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
];
|
||||||
|
case 5:
|
||||||
|
// 90 -> 90
|
||||||
|
return [1 - y, x, width * (pW / pH), height * (pH / pW)];
|
||||||
|
case 6:
|
||||||
|
// 90 -> 180
|
||||||
|
return [
|
||||||
|
1 - x - height * (pH / pW),
|
||||||
|
1 - y,
|
||||||
|
height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
];
|
||||||
|
case 7:
|
||||||
|
// 90 -> 270
|
||||||
|
return [
|
||||||
|
y - width * (pW / pH),
|
||||||
|
1 - x - height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
height * (pH / pW),
|
||||||
|
];
|
||||||
|
case 8:
|
||||||
|
// 180 -> 0
|
||||||
|
return [x - width, y - height, width, height];
|
||||||
|
case 9:
|
||||||
|
// 180 -> 90
|
||||||
|
return [1 - y, x - width, height, width];
|
||||||
|
case 10:
|
||||||
|
// 180 -> 180
|
||||||
|
return [1 - x, 1 - y, width, height];
|
||||||
|
case 11:
|
||||||
|
// 180 -> 270
|
||||||
|
return [y - height, 1 - x, height, width];
|
||||||
|
case 12:
|
||||||
|
// 270 -> 0
|
||||||
|
return [
|
||||||
|
x - height * (pH / pW),
|
||||||
|
y,
|
||||||
|
height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
];
|
||||||
|
case 13:
|
||||||
|
// 270 -> 90
|
||||||
|
return [
|
||||||
|
1 - y - width * (pW / pH),
|
||||||
|
x - height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
height * (pH / pW),
|
||||||
|
];
|
||||||
|
case 14:
|
||||||
|
// 270 -> 180
|
||||||
|
return [
|
||||||
|
1 - x,
|
||||||
|
1 - y - width * (pW / pH),
|
||||||
|
height * (pH / pW),
|
||||||
|
width * (pW / pH),
|
||||||
|
];
|
||||||
|
case 15:
|
||||||
|
// 270 -> 270
|
||||||
|
return [y, 1 - x, width * (pW / pH), height * (pH / pW)];
|
||||||
|
default:
|
||||||
|
// 0 -> 0
|
||||||
|
return [x, y, width, height];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
rotate() {
|
||||||
|
if (!this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.parent.drawLayer.updateProperties(
|
||||||
|
this._drawId,
|
||||||
|
DrawingEditor._mergeSVGProperties(
|
||||||
|
{
|
||||||
|
bbox: this.#rotateBox(),
|
||||||
|
},
|
||||||
|
this.#drawOutlines.updateRotation(
|
||||||
|
(this.parentRotation - this.rotation + 360) % 360
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onScaleChanging() {
|
||||||
|
if (!this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#updateBbox(
|
||||||
|
this.#drawOutlines.updateParentDimensions(
|
||||||
|
this.parentDimensions,
|
||||||
|
this.parent.scale
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static onScaleChangingWhenDrawing() {}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
render() {
|
||||||
|
if (this.div) {
|
||||||
|
return this.div;
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = super.render();
|
||||||
|
div.classList.add("draw");
|
||||||
|
|
||||||
|
const drawDiv = document.createElement("div");
|
||||||
|
div.append(drawDiv);
|
||||||
|
drawDiv.setAttribute("aria-hidden", "true");
|
||||||
|
drawDiv.className = "internal";
|
||||||
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||||
|
this.setDims(this.width * parentWidth, this.height * parentHeight);
|
||||||
|
this._uiManager.addShouldRescale(this);
|
||||||
|
this.disableEditing();
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new drawer instance.
|
||||||
|
* @param {number} x - The x coordinate of the event.
|
||||||
|
* @param {number} y - The y coordinate of the event.
|
||||||
|
* @param {number} parentWidth - The parent width.
|
||||||
|
* @param {number} parentHeight - The parent height.
|
||||||
|
* @param {number} rotation - The parent rotation.
|
||||||
|
*/
|
||||||
|
static createDrawerInstance(_x, _y, _parentWidth, _parentHeight, _rotation) {
|
||||||
|
unreachable("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
static startDrawing(
|
||||||
|
parent,
|
||||||
|
uiManager,
|
||||||
|
_isLTR,
|
||||||
|
{ target, offsetX: x, offsetY: y }
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
viewport: { rotation },
|
||||||
|
} = parent;
|
||||||
|
const { width: parentWidth, height: parentHeight } =
|
||||||
|
target.getBoundingClientRect();
|
||||||
|
const ac = new AbortController();
|
||||||
|
const signal = parent.combinedSignal(ac);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"pointerup",
|
||||||
|
e => {
|
||||||
|
ac.abort();
|
||||||
|
parent.toggleDrawing(true);
|
||||||
|
this._endDraw(e);
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
window.addEventListener(
|
||||||
|
"pointerdown",
|
||||||
|
stopEvent /* Avoid to have undesired clicks during drawing. */,
|
||||||
|
{
|
||||||
|
capture: true,
|
||||||
|
passive: false,
|
||||||
|
signal,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.addEventListener("contextmenu", noContextMenu, { signal });
|
||||||
|
target.addEventListener("pointermove", this._drawMove.bind(this), {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
parent.toggleDrawing();
|
||||||
|
|
||||||
|
if (this._currentDraw) {
|
||||||
|
parent.drawLayer.updateProperties(
|
||||||
|
this._currentDrawId,
|
||||||
|
this._currentDraw.startNew(x, y, parentWidth, parentHeight, rotation)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiManager.updateUIForDefaultProperties(this);
|
||||||
|
|
||||||
|
this._currentDraw = this.createDrawerInstance(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
parentWidth,
|
||||||
|
parentHeight,
|
||||||
|
rotation
|
||||||
|
);
|
||||||
|
this._currentDrawingOptions = this.getDefaultDrawingOptions();
|
||||||
|
this._currentParent = parent;
|
||||||
|
|
||||||
|
({ id: this._currentDrawId } = parent.drawLayer.draw(
|
||||||
|
this._mergeSVGProperties(
|
||||||
|
this._currentDrawingOptions.toSVGProperties(),
|
||||||
|
this._currentDraw.defaultSVGProperties
|
||||||
|
),
|
||||||
|
/* isPathUpdatable = */ true,
|
||||||
|
/* hasClip = */ false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
static _drawMove({ offsetX, offsetY }) {
|
||||||
|
this._currentParent.drawLayer.updateProperties(
|
||||||
|
this._currentDrawId,
|
||||||
|
this._currentDraw.add(offsetX, offsetY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _endDraw({ offsetX, offsetY }) {
|
||||||
|
const parent = this._currentParent;
|
||||||
|
parent.drawLayer.updateProperties(
|
||||||
|
this._currentDrawId,
|
||||||
|
this._currentDraw.end(offsetX, offsetY)
|
||||||
|
);
|
||||||
|
if (this.supportMultipleDrawings) {
|
||||||
|
const draw = this._currentDraw;
|
||||||
|
const drawId = this._currentDrawId;
|
||||||
|
const lastElement = draw.getLastElement();
|
||||||
|
parent.addCommands({
|
||||||
|
cmd: () => {
|
||||||
|
parent.drawLayer.updateProperties(
|
||||||
|
drawId,
|
||||||
|
draw.setLastElement(lastElement)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
undo: () => {
|
||||||
|
parent.drawLayer.updateProperties(drawId, draw.removeLastElement());
|
||||||
|
},
|
||||||
|
mustExec: false,
|
||||||
|
type: AnnotationEditorParamsType.DRAW_STEP,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.endDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
static endDrawing() {
|
||||||
|
const parent = this._currentParent;
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.toggleDrawing(true);
|
||||||
|
parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP);
|
||||||
|
|
||||||
|
if (!this._currentDraw.isEmpty()) {
|
||||||
|
const {
|
||||||
|
pageDimensions: [pageWidth, pageHeight],
|
||||||
|
scale,
|
||||||
|
} = parent;
|
||||||
|
|
||||||
|
parent.createAndAddNewEditor({ offsetX: 0, offsetY: 0 }, false, {
|
||||||
|
drawId: this._currentDrawId,
|
||||||
|
drawOutlines: this._currentDraw.getOutlines(
|
||||||
|
pageWidth * scale,
|
||||||
|
pageHeight * scale,
|
||||||
|
scale,
|
||||||
|
this._INNER_MARGIN
|
||||||
|
),
|
||||||
|
drawingOptions: this._currentDrawingOptions,
|
||||||
|
mustBeCommitted: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
parent.drawLayer.remove(this._currentDrawId);
|
||||||
|
}
|
||||||
|
this._currentDrawId = -1;
|
||||||
|
this._currentDraw = null;
|
||||||
|
this._currentDrawingOptions = null;
|
||||||
|
this._currentParent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the drawing options.
|
||||||
|
* @param {Object} _data
|
||||||
|
*/
|
||||||
|
createDrawingOptions(_data) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the drawing outlines.
|
||||||
|
* @param {number} pageX - The x coordinate of the page.
|
||||||
|
* @param {number} pageY - The y coordinate of the page.
|
||||||
|
* @param {number} pageWidth - The width of the page.
|
||||||
|
* @param {number} pageHeight - The height of the page.
|
||||||
|
* @param {number} innerWidth - The inner width.
|
||||||
|
* @param {Object} data - The data to deserialize.
|
||||||
|
* @returns {Object} The deserialized outlines.
|
||||||
|
*/
|
||||||
|
static deserializeDraw(
|
||||||
|
_pageX,
|
||||||
|
_pageY,
|
||||||
|
_pageWidth,
|
||||||
|
_pageHeight,
|
||||||
|
_innerWidth,
|
||||||
|
_data
|
||||||
|
) {
|
||||||
|
unreachable("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
static async deserialize(data, parent, uiManager) {
|
||||||
|
const {
|
||||||
|
rawDims: { pageWidth, pageHeight, pageX, pageY },
|
||||||
|
} = parent.viewport;
|
||||||
|
const drawOutlines = this.deserializeDraw(
|
||||||
|
pageX,
|
||||||
|
pageY,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
this._INNER_MARGIN,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
const editor = await super.deserialize(data, parent, uiManager);
|
||||||
|
editor.createDrawingOptions(data);
|
||||||
|
editor.#createDrawOutlines({ drawOutlines });
|
||||||
|
editor.#addToDrawLayer();
|
||||||
|
editor.onScaleChanging();
|
||||||
|
editor.rotate();
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeDraw(isForCopying) {
|
||||||
|
const [pageX, pageY] = this.pageTranslation;
|
||||||
|
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||||
|
return this.#drawOutlines.serialize(
|
||||||
|
[pageX, pageY, pageWidth, pageHeight],
|
||||||
|
isForCopying
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
renderAnnotationElement(annotation) {
|
||||||
|
annotation.updateEdited({
|
||||||
|
rect: this.getRect(0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static canCreateNewEmptyEditor() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DrawingEditor, DrawingOptions };
|
||||||
853
src/display/editor/drawers/inkdraw.js
Normal file
853
src/display/editor/drawers/inkdraw.js
Normal file
@ -0,0 +1,853 @@
|
|||||||
|
/* Copyright 2024 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Outline } from "./outline.js";
|
||||||
|
import { Util } from "../../../shared/util.js";
|
||||||
|
|
||||||
|
class InkDrawOutliner {
|
||||||
|
// The last 3 points of the line.
|
||||||
|
#last = new Float64Array(6);
|
||||||
|
|
||||||
|
#line;
|
||||||
|
|
||||||
|
#lines;
|
||||||
|
|
||||||
|
#rotation;
|
||||||
|
|
||||||
|
#thickness;
|
||||||
|
|
||||||
|
#points;
|
||||||
|
|
||||||
|
#lastSVGPath = "";
|
||||||
|
|
||||||
|
#lastIndex = 0;
|
||||||
|
|
||||||
|
#outlines = new InkDrawOutline();
|
||||||
|
|
||||||
|
#parentWidth;
|
||||||
|
|
||||||
|
#parentHeight;
|
||||||
|
|
||||||
|
constructor(x, y, parentWidth, parentHeight, rotation, thickness) {
|
||||||
|
this.#parentWidth = parentWidth;
|
||||||
|
this.#parentHeight = parentHeight;
|
||||||
|
this.#rotation = rotation;
|
||||||
|
this.#thickness = thickness;
|
||||||
|
|
||||||
|
[x, y] = this.#normalizePoint(x, y);
|
||||||
|
|
||||||
|
const line = (this.#line = [NaN, NaN, NaN, NaN, x, y]);
|
||||||
|
this.#points = [x, y];
|
||||||
|
this.#lines = [{ line, points: this.#points }];
|
||||||
|
this.#last.set(line, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProperty(name, value) {
|
||||||
|
if (name === "stroke-width") {
|
||||||
|
this.#thickness = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#normalizePoint(x, y) {
|
||||||
|
return Outline._normalizePoint(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
this.#parentWidth,
|
||||||
|
this.#parentHeight,
|
||||||
|
this.#rotation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return !this.#lines || this.#lines.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(x, y) {
|
||||||
|
// The point is in canvas coordinates which means that there is no rotation.
|
||||||
|
// It's the same as parent coordinates.
|
||||||
|
[x, y] = this.#normalizePoint(x, y);
|
||||||
|
const [x1, y1, x2, y2] = this.#last.subarray(2, 6);
|
||||||
|
const diffX = x - x2;
|
||||||
|
const diffY = y - y2;
|
||||||
|
const d = Math.hypot(this.#parentWidth * diffX, this.#parentHeight * diffY);
|
||||||
|
if (d <= 2) {
|
||||||
|
// The idea is to avoid garbage points around the last point.
|
||||||
|
// When the points are too close, it just leads to bad normal vectors and
|
||||||
|
// control points.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#points.push(x, y);
|
||||||
|
|
||||||
|
if (isNaN(x1)) {
|
||||||
|
// We've only one point.
|
||||||
|
this.#last.set([x2, y2, x, y], 2);
|
||||||
|
this.#line.push(NaN, NaN, NaN, NaN, x, y);
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(this.#last[0])) {
|
||||||
|
// We've only two points.
|
||||||
|
this.#line.splice(6, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#last.set([x1, y1, x2, y2, x, y], 0);
|
||||||
|
this.#line.push(
|
||||||
|
(x1 + 5 * x2) / 6,
|
||||||
|
(y1 + 5 * y2) / 6,
|
||||||
|
(5 * x2 + x) / 6,
|
||||||
|
(5 * y2 + y) / 6,
|
||||||
|
(x2 + x) / 2,
|
||||||
|
(y2 + y) / 2
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
end(x, y) {
|
||||||
|
const change = this.add(x, y);
|
||||||
|
if (change) {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
if (this.#points.length === 2) {
|
||||||
|
// We've only one point.
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
startNew(x, y, parentWidth, parentHeight, rotation) {
|
||||||
|
this.#parentWidth = parentWidth;
|
||||||
|
this.#parentHeight = parentHeight;
|
||||||
|
this.#rotation = rotation;
|
||||||
|
|
||||||
|
[x, y] = this.#normalizePoint(x, y);
|
||||||
|
|
||||||
|
const line = (this.#line = [NaN, NaN, NaN, NaN, x, y]);
|
||||||
|
this.#points = [x, y];
|
||||||
|
const last = this.#lines.at(-1);
|
||||||
|
if (last) {
|
||||||
|
last.line = new Float32Array(last.line);
|
||||||
|
last.points = new Float32Array(last.points);
|
||||||
|
}
|
||||||
|
this.#lines.push({ line, points: this.#points });
|
||||||
|
this.#last.set(line, 0);
|
||||||
|
this.#lastIndex = 0;
|
||||||
|
this.toSVGPath();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastElement() {
|
||||||
|
return this.#lines.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastElement(element) {
|
||||||
|
if (!this.#lines) {
|
||||||
|
return this.#outlines.setLastElement(element);
|
||||||
|
}
|
||||||
|
this.#lines.push(element);
|
||||||
|
this.#line = element.line;
|
||||||
|
this.#points = element.points;
|
||||||
|
this.#lastIndex = 0;
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLastElement() {
|
||||||
|
if (!this.#lines) {
|
||||||
|
return this.#outlines.removeLastElement();
|
||||||
|
}
|
||||||
|
this.#lines.pop();
|
||||||
|
this.#lastSVGPath = "";
|
||||||
|
for (let i = 0, ii = this.#lines.length; i < ii; i++) {
|
||||||
|
const { line, points } = this.#lines[i];
|
||||||
|
this.#line = line;
|
||||||
|
this.#points = points;
|
||||||
|
this.#lastIndex = 0;
|
||||||
|
this.toSVGPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.#lastSVGPath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toSVGPath() {
|
||||||
|
const firstX = Outline.svgRound(this.#line[4]);
|
||||||
|
const firstY = Outline.svgRound(this.#line[5]);
|
||||||
|
if (this.#points.length === 2) {
|
||||||
|
this.#lastSVGPath = `${this.#lastSVGPath} M ${firstX} ${firstY} Z`;
|
||||||
|
return this.#lastSVGPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#points.length <= 6) {
|
||||||
|
// We've 2 or 3 points.
|
||||||
|
const i = this.#lastSVGPath.lastIndexOf("M");
|
||||||
|
this.#lastSVGPath = `${this.#lastSVGPath.slice(0, i)} M ${firstX} ${firstY}`;
|
||||||
|
this.#lastIndex = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#points.length === 4) {
|
||||||
|
const secondX = Outline.svgRound(this.#line[10]);
|
||||||
|
const secondY = Outline.svgRound(this.#line[11]);
|
||||||
|
this.#lastSVGPath = `${this.#lastSVGPath} L ${secondX} ${secondY}`;
|
||||||
|
this.#lastIndex = 12;
|
||||||
|
return this.#lastSVGPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = [];
|
||||||
|
if (this.#lastIndex === 0) {
|
||||||
|
buffer.push(`M ${firstX} ${firstY}`);
|
||||||
|
this.#lastIndex = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this.#lastIndex, ii = this.#line.length; i < ii; i += 6) {
|
||||||
|
const [c1x, c1y, c2x, c2y, x, y] = this.#line
|
||||||
|
.slice(i, i + 6)
|
||||||
|
.map(Outline.svgRound);
|
||||||
|
buffer.push(`C${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
|
||||||
|
}
|
||||||
|
this.#lastSVGPath += buffer.join(" ");
|
||||||
|
this.#lastIndex = this.#line.length;
|
||||||
|
|
||||||
|
return this.#lastSVGPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutlines(parentWidth, parentHeight, scale, innerMargin) {
|
||||||
|
const last = this.#lines.at(-1);
|
||||||
|
last.line = new Float32Array(last.line);
|
||||||
|
last.points = new Float32Array(last.points);
|
||||||
|
|
||||||
|
this.#outlines.build(
|
||||||
|
this.#lines,
|
||||||
|
parentWidth,
|
||||||
|
parentHeight,
|
||||||
|
scale,
|
||||||
|
this.#rotation,
|
||||||
|
this.#thickness,
|
||||||
|
innerMargin
|
||||||
|
);
|
||||||
|
|
||||||
|
// We reset everything: the drawing is done.
|
||||||
|
this.#last = null;
|
||||||
|
this.#line = null;
|
||||||
|
this.#lines = null;
|
||||||
|
this.#lastSVGPath = null;
|
||||||
|
|
||||||
|
return this.#outlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultSVGProperties() {
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: "0 0 10000 10000",
|
||||||
|
},
|
||||||
|
rootClass: {
|
||||||
|
draw: true,
|
||||||
|
},
|
||||||
|
bbox: [0, 0, 1, 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InkDrawOutline extends Outline {
|
||||||
|
#bbox;
|
||||||
|
|
||||||
|
#currentRotation = 0;
|
||||||
|
|
||||||
|
#innerMargin;
|
||||||
|
|
||||||
|
#lines;
|
||||||
|
|
||||||
|
#parentWidth;
|
||||||
|
|
||||||
|
#parentHeight;
|
||||||
|
|
||||||
|
#parentScale;
|
||||||
|
|
||||||
|
#rotation;
|
||||||
|
|
||||||
|
#thickness;
|
||||||
|
|
||||||
|
build(
|
||||||
|
lines,
|
||||||
|
parentWidth,
|
||||||
|
parentHeight,
|
||||||
|
parentScale,
|
||||||
|
rotation,
|
||||||
|
thickness,
|
||||||
|
innerMargin
|
||||||
|
) {
|
||||||
|
this.#parentWidth = parentWidth;
|
||||||
|
this.#parentHeight = parentHeight;
|
||||||
|
this.#parentScale = parentScale;
|
||||||
|
this.#rotation = rotation;
|
||||||
|
this.#thickness = thickness;
|
||||||
|
this.#innerMargin = innerMargin ?? 0;
|
||||||
|
this.#lines = lines;
|
||||||
|
|
||||||
|
this.#computeBbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastElement(element) {
|
||||||
|
this.#lines.push(element);
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLastElement() {
|
||||||
|
this.#lines.pop();
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toSVGPath() {
|
||||||
|
const buffer = [];
|
||||||
|
for (const { line } of this.#lines) {
|
||||||
|
buffer.push(`M${Outline.svgRound(line[4])} ${Outline.svgRound(line[5])}`);
|
||||||
|
if (line.length === 6) {
|
||||||
|
buffer.push("Z");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.length === 12) {
|
||||||
|
buffer.push(
|
||||||
|
`L${Outline.svgRound(line[10])} ${Outline.svgRound(line[11])}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (let i = 6, ii = line.length; i < ii; i += 6) {
|
||||||
|
const [c1x, c1y, c2x, c2y, x, y] = line
|
||||||
|
.subarray(i, i + 6)
|
||||||
|
.map(Outline.svgRound);
|
||||||
|
buffer.push(`C${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize([pageX, pageY, pageWidth, pageHeight], isForCopying) {
|
||||||
|
const serializedLines = [];
|
||||||
|
const serializedPoints = [];
|
||||||
|
const [x, y, width, height] = this.#getBBoxWithNoMargin();
|
||||||
|
let tx, ty, sx, sy, x1, y1, x2, y2, rescaleFn;
|
||||||
|
|
||||||
|
switch (this.#rotation) {
|
||||||
|
case 0:
|
||||||
|
rescaleFn = Outline._rescale;
|
||||||
|
tx = pageX;
|
||||||
|
ty = pageY + pageHeight;
|
||||||
|
sx = pageWidth;
|
||||||
|
sy = -pageHeight;
|
||||||
|
x1 = pageX + x * pageWidth;
|
||||||
|
y1 = pageY + (1 - y - height) * pageHeight;
|
||||||
|
x2 = pageX + (x + width) * pageWidth;
|
||||||
|
y2 = pageY + (1 - y) * pageHeight;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
rescaleFn = Outline._rescaleAndSwap;
|
||||||
|
tx = pageX;
|
||||||
|
ty = pageY;
|
||||||
|
sx = pageWidth;
|
||||||
|
sy = pageHeight;
|
||||||
|
x1 = pageX + y * pageWidth;
|
||||||
|
y1 = pageY + x * pageHeight;
|
||||||
|
x2 = pageX + (y + height) * pageWidth;
|
||||||
|
y2 = pageY + (x + width) * pageHeight;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
rescaleFn = Outline._rescale;
|
||||||
|
tx = pageX + pageWidth;
|
||||||
|
ty = pageY;
|
||||||
|
sx = -pageWidth;
|
||||||
|
sy = pageHeight;
|
||||||
|
x1 = pageX + (1 - x - width) * pageWidth;
|
||||||
|
y1 = pageY + y * pageHeight;
|
||||||
|
x2 = pageX + (1 - x) * pageWidth;
|
||||||
|
y2 = pageY + (y + height) * pageHeight;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
rescaleFn = Outline._rescaleAndSwap;
|
||||||
|
tx = pageX + pageWidth;
|
||||||
|
ty = pageY + pageHeight;
|
||||||
|
sx = -pageWidth;
|
||||||
|
sy = -pageHeight;
|
||||||
|
x1 = pageX + (1 - y - height) * pageWidth;
|
||||||
|
y1 = pageY + (1 - x - width) * pageHeight;
|
||||||
|
x2 = pageX + (1 - y) * pageWidth;
|
||||||
|
y2 = pageY + (1 - x) * pageHeight;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { line, points } of this.#lines) {
|
||||||
|
serializedLines.push(
|
||||||
|
rescaleFn(
|
||||||
|
line,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
isForCopying ? new Array(line.length) : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
serializedPoints.push(
|
||||||
|
rescaleFn(
|
||||||
|
points,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
isForCopying ? new Array(points.length) : null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lines: serializedLines,
|
||||||
|
points: serializedPoints,
|
||||||
|
rect: [x1, y1, x2, y2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static deserialize(
|
||||||
|
pageX,
|
||||||
|
pageY,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
innerMargin,
|
||||||
|
{ paths: { lines, points }, rotation, thickness }
|
||||||
|
) {
|
||||||
|
const newLines = [];
|
||||||
|
let tx, ty, sx, sy, rescaleFn;
|
||||||
|
switch (rotation) {
|
||||||
|
case 0:
|
||||||
|
rescaleFn = Outline._rescale;
|
||||||
|
tx = -pageX / pageWidth;
|
||||||
|
ty = pageY / pageHeight + 1;
|
||||||
|
sx = 1 / pageWidth;
|
||||||
|
sy = -1 / pageHeight;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
rescaleFn = Outline._rescaleAndSwap;
|
||||||
|
tx = -pageY / pageHeight;
|
||||||
|
ty = -pageX / pageWidth;
|
||||||
|
sx = 1 / pageHeight;
|
||||||
|
sy = 1 / pageWidth;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
rescaleFn = Outline._rescale;
|
||||||
|
tx = pageX / pageWidth + 1;
|
||||||
|
ty = -pageY / pageHeight;
|
||||||
|
sx = -1 / pageWidth;
|
||||||
|
sy = 1 / pageHeight;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
rescaleFn = Outline._rescaleAndSwap;
|
||||||
|
tx = pageY / pageHeight + 1;
|
||||||
|
ty = pageX / pageWidth + 1;
|
||||||
|
sx = -1 / pageHeight;
|
||||||
|
sy = -1 / pageWidth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, ii = lines.length; i < ii; i++) {
|
||||||
|
newLines.push({
|
||||||
|
line: rescaleFn(
|
||||||
|
lines[i].map(x => x ?? NaN),
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
sx,
|
||||||
|
sy
|
||||||
|
),
|
||||||
|
points: rescaleFn(
|
||||||
|
points[i].map(x => x ?? NaN),
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
sx,
|
||||||
|
sy
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlines = new InkDrawOutline();
|
||||||
|
outlines.build(
|
||||||
|
newLines,
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
1,
|
||||||
|
rotation,
|
||||||
|
thickness,
|
||||||
|
innerMargin
|
||||||
|
);
|
||||||
|
|
||||||
|
return outlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getMarginComponents(thickness = this.#thickness) {
|
||||||
|
const margin = this.#innerMargin + (thickness / 2) * this.#parentScale;
|
||||||
|
return this.#rotation % 180 === 0
|
||||||
|
? [margin / this.#parentWidth, margin / this.#parentHeight]
|
||||||
|
: [margin / this.#parentHeight, margin / this.#parentWidth];
|
||||||
|
}
|
||||||
|
|
||||||
|
#getBBoxWithNoMargin() {
|
||||||
|
const [x, y, width, height] = this.#bbox;
|
||||||
|
const [marginX, marginY] = this.#getMarginComponents(0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
x + marginX,
|
||||||
|
y + marginY,
|
||||||
|
width - 2 * marginX,
|
||||||
|
height - 2 * marginY,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#computeBbox() {
|
||||||
|
const bbox = (this.#bbox = new Float32Array([
|
||||||
|
Infinity,
|
||||||
|
Infinity,
|
||||||
|
-Infinity,
|
||||||
|
-Infinity,
|
||||||
|
]));
|
||||||
|
|
||||||
|
for (const { line } of this.#lines) {
|
||||||
|
if (line.length <= 12) {
|
||||||
|
// We've only one or two points => no bezier curve.
|
||||||
|
for (let i = 4, ii = line.length; i < ii; i += 6) {
|
||||||
|
const [x, y] = line.subarray(i, i + 2);
|
||||||
|
bbox[0] = Math.min(bbox[0], x);
|
||||||
|
bbox[1] = Math.min(bbox[1], y);
|
||||||
|
bbox[2] = Math.max(bbox[2], x);
|
||||||
|
bbox[3] = Math.max(bbox[3], y);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lastX = line[4],
|
||||||
|
lastY = line[5];
|
||||||
|
for (let i = 6, ii = line.length; i < ii; i += 6) {
|
||||||
|
const [c1x, c1y, c2x, c2y, x, y] = line.subarray(i, i + 6);
|
||||||
|
Util.bezierBoundingBox(lastX, lastY, c1x, c1y, c2x, c2y, x, y, bbox);
|
||||||
|
lastX = x;
|
||||||
|
lastY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [marginX, marginY] = this.#getMarginComponents();
|
||||||
|
bbox[0] = Math.min(1, Math.max(0, bbox[0] - marginX));
|
||||||
|
bbox[1] = Math.min(1, Math.max(0, bbox[1] - marginY));
|
||||||
|
bbox[2] = Math.min(1, Math.max(0, bbox[2] + marginX));
|
||||||
|
bbox[3] = Math.min(1, Math.max(0, bbox[3] + marginY));
|
||||||
|
|
||||||
|
bbox[2] -= bbox[0];
|
||||||
|
bbox[3] -= bbox[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
get box() {
|
||||||
|
return this.#bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProperty(name, value) {
|
||||||
|
if (name === "stroke-width") {
|
||||||
|
return this.#updateThickness(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateThickness(thickness) {
|
||||||
|
const [oldMarginX, oldMarginY] = this.#getMarginComponents();
|
||||||
|
this.#thickness = thickness;
|
||||||
|
const [newMarginX, newMarginY] = this.#getMarginComponents();
|
||||||
|
const [diffMarginX, diffMarginY] = [
|
||||||
|
newMarginX - oldMarginX,
|
||||||
|
newMarginY - oldMarginY,
|
||||||
|
];
|
||||||
|
const bbox = this.#bbox;
|
||||||
|
bbox[0] -= diffMarginX;
|
||||||
|
bbox[1] -= diffMarginY;
|
||||||
|
bbox[2] += 2 * diffMarginX;
|
||||||
|
bbox[3] += 2 * diffMarginY;
|
||||||
|
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParentDimensions([width, height], scale) {
|
||||||
|
const [oldMarginX, oldMarginY] = this.#getMarginComponents();
|
||||||
|
this.#parentWidth = width;
|
||||||
|
this.#parentHeight = height;
|
||||||
|
this.#parentScale = scale;
|
||||||
|
const [newMarginX, newMarginY] = this.#getMarginComponents();
|
||||||
|
const diffMarginX = newMarginX - oldMarginX;
|
||||||
|
const diffMarginY = newMarginY - oldMarginY;
|
||||||
|
|
||||||
|
const bbox = this.#bbox;
|
||||||
|
bbox[0] -= diffMarginX;
|
||||||
|
bbox[1] -= diffMarginY;
|
||||||
|
bbox[2] += 2 * diffMarginX;
|
||||||
|
bbox[3] += 2 * diffMarginY;
|
||||||
|
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRotation(rotation) {
|
||||||
|
this.#currentRotation = rotation;
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
transform: this.rotationTransform,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewBox() {
|
||||||
|
return this.#bbox.map(Outline.svgRound).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultProperties() {
|
||||||
|
const [x, y] = this.#bbox;
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: this.viewBox,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
"transform-origin": `${Outline.svgRound(x)} ${Outline.svgRound(y)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get rotationTransform() {
|
||||||
|
const [, , width, height] = this.#bbox;
|
||||||
|
let a = 0,
|
||||||
|
b = 0,
|
||||||
|
c = 0,
|
||||||
|
d = 0,
|
||||||
|
e = 0,
|
||||||
|
f = 0;
|
||||||
|
switch (this.#currentRotation) {
|
||||||
|
case 90:
|
||||||
|
b = height / width;
|
||||||
|
c = -width / height;
|
||||||
|
e = width;
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
a = -1;
|
||||||
|
d = -1;
|
||||||
|
e = width;
|
||||||
|
f = height;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
b = -height / width;
|
||||||
|
c = width / height;
|
||||||
|
f = height;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return `matrix(${a} ${b} ${c} ${d} ${Outline.svgRound(e)} ${Outline.svgRound(f)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPathResizingSVGProperties([newX, newY, newWidth, newHeight]) {
|
||||||
|
const [marginX, marginY] = this.#getMarginComponents();
|
||||||
|
const [x, y, width, height] = this.#bbox;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(width - marginX) <= Outline.PRECISION ||
|
||||||
|
Math.abs(height - marginY) <= Outline.PRECISION
|
||||||
|
) {
|
||||||
|
// Center the path in the new bounding box.
|
||||||
|
const tx = newX + newWidth / 2 - (x + width / 2);
|
||||||
|
const ty = newY + newHeight / 2 - (y + height / 2);
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
|
||||||
|
transform: `${this.rotationTransform} translate(${tx} ${ty})`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We compute the following transform:
|
||||||
|
// 1. Translate the path to the origin (-marginX, -marginY).
|
||||||
|
// 2. Scale the path to the new size:
|
||||||
|
// ((newWidth - 2*marginX) / (bbox.width - 2*marginX),
|
||||||
|
// (newHeight - 2*marginY) / (bbox.height - 2*marginY)).
|
||||||
|
// 3. Translate the path back to its original position
|
||||||
|
// (marginX, marginY).
|
||||||
|
// 4. Scale the inverse of bbox scaling:
|
||||||
|
// (bbox.width / newWidth, bbox.height / newHeight).
|
||||||
|
|
||||||
|
const s1x = (newWidth - 2 * marginX) / (width - 2 * marginX);
|
||||||
|
const s1y = (newHeight - 2 * marginY) / (height - 2 * marginY);
|
||||||
|
const s2x = width / newWidth;
|
||||||
|
const s2y = height / newHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: {
|
||||||
|
"transform-origin": `${Outline.svgRound(x)} ${Outline.svgRound(y)}`,
|
||||||
|
transform:
|
||||||
|
`${this.rotationTransform} scale(${s2x} ${s2y}) ` +
|
||||||
|
`translate(${Outline.svgRound(marginX)} ${Outline.svgRound(marginY)}) scale(${s1x} ${s1y}) ` +
|
||||||
|
`translate(${Outline.svgRound(-marginX)} ${Outline.svgRound(-marginY)})`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getPathResizedSVGProperties([newX, newY, newWidth, newHeight]) {
|
||||||
|
const [marginX, marginY] = this.#getMarginComponents();
|
||||||
|
const bbox = this.#bbox;
|
||||||
|
const [x, y, width, height] = bbox;
|
||||||
|
|
||||||
|
bbox[0] = newX;
|
||||||
|
bbox[1] = newY;
|
||||||
|
bbox[2] = newWidth;
|
||||||
|
bbox[3] = newHeight;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(width - marginX) <= Outline.PRECISION ||
|
||||||
|
Math.abs(height - marginY) <= Outline.PRECISION
|
||||||
|
) {
|
||||||
|
// Center the path in the new bounding box.
|
||||||
|
const tx = newX + newWidth / 2 - (x + width / 2);
|
||||||
|
const ty = newY + newHeight / 2 - (y + height / 2);
|
||||||
|
for (const { line, points } of this.#lines) {
|
||||||
|
Outline._translate(line, tx, ty, line);
|
||||||
|
Outline._translate(points, tx, ty, points);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: this.viewBox,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
|
||||||
|
transform: this.rotationTransform || null,
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// We compute the following transform:
|
||||||
|
// 1. Translate the path to the origin (-(x + marginX), -(y + marginY)).
|
||||||
|
// 2. Scale the path to the new size:
|
||||||
|
// ((newWidth - 2*marginX) / (bbox.width - 2*marginX),
|
||||||
|
// (newHeight - 2*marginY) / (bbox.height - 2*marginY)).
|
||||||
|
// 3. Translate the path back to its new position
|
||||||
|
// (newX + marginX,y newY + marginY).
|
||||||
|
|
||||||
|
const s1x = (newWidth - 2 * marginX) / (width - 2 * marginX);
|
||||||
|
const s1y = (newHeight - 2 * marginY) / (height - 2 * marginY);
|
||||||
|
const tx = -s1x * (x + marginX) + newX + marginX;
|
||||||
|
const ty = -s1y * (y + marginY) + newY + marginY;
|
||||||
|
|
||||||
|
if (s1x !== 1 || s1y !== 1 || tx !== 0 || ty !== 0) {
|
||||||
|
for (const { line, points } of this.#lines) {
|
||||||
|
Outline._rescale(line, tx, ty, s1x, s1y, line);
|
||||||
|
Outline._rescale(points, tx, ty, s1x, s1y, points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: this.viewBox,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
|
||||||
|
transform: this.rotationTransform || null,
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getPathTranslatedSVGProperties([newX, newY], parentDimensions) {
|
||||||
|
const [newParentWidth, newParentHeight] = parentDimensions;
|
||||||
|
const bbox = this.#bbox;
|
||||||
|
const tx = newX - bbox[0];
|
||||||
|
const ty = newY - bbox[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#parentWidth === newParentWidth &&
|
||||||
|
this.#parentHeight === newParentHeight
|
||||||
|
) {
|
||||||
|
// We don't change the parent dimensions so it's a simple translation.
|
||||||
|
for (const { line, points } of this.#lines) {
|
||||||
|
Outline._translate(line, tx, ty, line);
|
||||||
|
Outline._translate(points, tx, ty, points);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sx = this.#parentWidth / newParentWidth;
|
||||||
|
const sy = this.#parentHeight / newParentHeight;
|
||||||
|
this.#parentWidth = newParentWidth;
|
||||||
|
this.#parentHeight = newParentHeight;
|
||||||
|
|
||||||
|
for (const { line, points } of this.#lines) {
|
||||||
|
Outline._rescale(line, tx, ty, sx, sy, line);
|
||||||
|
Outline._rescale(points, tx, ty, sx, sy, points);
|
||||||
|
}
|
||||||
|
bbox[2] *= sx;
|
||||||
|
bbox[3] *= sy;
|
||||||
|
}
|
||||||
|
bbox[0] = newX;
|
||||||
|
bbox[1] = newY;
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: this.viewBox,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultSVGProperties() {
|
||||||
|
const bbox = this.#bbox;
|
||||||
|
return {
|
||||||
|
root: {
|
||||||
|
viewBox: this.viewBox,
|
||||||
|
},
|
||||||
|
rootClass: {
|
||||||
|
draw: true,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
d: this.toSVGPath(),
|
||||||
|
"transform-origin": `${Outline.svgRound(bbox[0])} ${Outline.svgRound(bbox[1])}`,
|
||||||
|
transform: this.rotationTransform || null,
|
||||||
|
},
|
||||||
|
bbox,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InkDrawOutline, InkDrawOutliner };
|
||||||
@ -16,6 +16,8 @@
|
|||||||
import { unreachable } from "../../../shared/util.js";
|
import { unreachable } from "../../../shared/util.js";
|
||||||
|
|
||||||
class Outline {
|
class Outline {
|
||||||
|
static PRECISION = 1e-4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string} The SVG path of the outline.
|
* @returns {string} The SVG path of the outline.
|
||||||
*/
|
*/
|
||||||
@ -52,6 +54,49 @@ class Outline {
|
|||||||
}
|
}
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _translate(src, tx, ty, dest) {
|
||||||
|
dest ||= new Float32Array(src.length);
|
||||||
|
for (let i = 0, ii = src.length; i < ii; i += 2) {
|
||||||
|
dest[i] = tx + src[i];
|
||||||
|
dest[i + 1] = ty + src[i + 1];
|
||||||
|
}
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
static svgRound(x) {
|
||||||
|
// 0.1234 will be 1234 and this way we economize 2 bytes per number.
|
||||||
|
// Of course, it makes sense only when the viewBox is [0 0 10000 10000].
|
||||||
|
// And it helps to avoid bugs like:
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1929340
|
||||||
|
return Math.round(x * 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static _normalizePoint(x, y, parentWidth, parentHeight, rotation) {
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return [1 - y / parentWidth, x / parentHeight];
|
||||||
|
case 180:
|
||||||
|
return [1 - x / parentWidth, 1 - y / parentHeight];
|
||||||
|
case 270:
|
||||||
|
return [y / parentWidth, 1 - x / parentHeight];
|
||||||
|
default:
|
||||||
|
return [x / parentWidth, y / parentHeight];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _normalizePagePoint(x, y, rotation) {
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return [1 - y, x];
|
||||||
|
case 180:
|
||||||
|
return [1 - x, 1 - y];
|
||||||
|
case 270:
|
||||||
|
return [y, 1 - x];
|
||||||
|
default:
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Outline };
|
export { Outline };
|
||||||
|
|||||||
@ -192,6 +192,10 @@ class AnnotationEditor {
|
|||||||
return Object.getPrototypeOf(this).constructor._type;
|
return Object.getPrototypeOf(this).constructor._type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get isDrawer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static get _defaultLineColor() {
|
static get _defaultLineColor() {
|
||||||
return shadow(
|
return shadow(
|
||||||
this,
|
this,
|
||||||
@ -441,6 +445,8 @@ class AnnotationEditor {
|
|||||||
this.x += x / width;
|
this.x += x / width;
|
||||||
this.y += y / height;
|
this.y += y / height;
|
||||||
|
|
||||||
|
this._onTranslating(this.x, this.y);
|
||||||
|
|
||||||
this.fixAndSetPosition();
|
this.fixAndSetPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,7 +475,10 @@ class AnnotationEditor {
|
|||||||
|
|
||||||
drag(tx, ty) {
|
drag(tx, ty) {
|
||||||
this.#initialPosition ||= [this.x, this.y];
|
this.#initialPosition ||= [this.x, this.y];
|
||||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
const {
|
||||||
|
div,
|
||||||
|
parentDimensions: [parentWidth, parentHeight],
|
||||||
|
} = this;
|
||||||
this.x += tx / parentWidth;
|
this.x += tx / parentWidth;
|
||||||
this.y += ty / parentHeight;
|
this.y += ty / parentHeight;
|
||||||
if (this.parent && (this.x < 0 || this.x > 1 || this.y < 0 || this.y > 1)) {
|
if (this.parent && (this.x < 0 || this.x > 1 || this.y < 0 || this.y > 1)) {
|
||||||
@ -496,11 +505,29 @@ class AnnotationEditor {
|
|||||||
x += bx;
|
x += bx;
|
||||||
y += by;
|
y += by;
|
||||||
|
|
||||||
this.div.style.left = `${(100 * x).toFixed(2)}%`;
|
const { style } = div;
|
||||||
this.div.style.top = `${(100 * y).toFixed(2)}%`;
|
style.left = `${(100 * x).toFixed(2)}%`;
|
||||||
this.div.scrollIntoView({ block: "nearest" });
|
style.top = `${(100 * y).toFixed(2)}%`;
|
||||||
|
|
||||||
|
this._onTranslating(x, y);
|
||||||
|
|
||||||
|
div.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the editor is being translated.
|
||||||
|
* @param {number} x - in page coordinates.
|
||||||
|
* @param {number} y - in page coordinates.
|
||||||
|
*/
|
||||||
|
_onTranslating(x, y) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the editor has been translated.
|
||||||
|
* @param {number} x - in page coordinates.
|
||||||
|
* @param {number} y - in page coordinates.
|
||||||
|
*/
|
||||||
|
_onTranslated(x, y) {}
|
||||||
|
|
||||||
get _hasBeenMoved() {
|
get _hasBeenMoved() {
|
||||||
return (
|
return (
|
||||||
!!this.#initialPosition &&
|
!!this.#initialPosition &&
|
||||||
@ -546,7 +573,10 @@ class AnnotationEditor {
|
|||||||
* @param {number} [rotation] - the rotation of the page.
|
* @param {number} [rotation] - the rotation of the page.
|
||||||
*/
|
*/
|
||||||
fixAndSetPosition(rotation = this.rotation) {
|
fixAndSetPosition(rotation = this.rotation) {
|
||||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
const {
|
||||||
|
div: { style },
|
||||||
|
pageDimensions: [pageWidth, pageHeight],
|
||||||
|
} = this;
|
||||||
let { x, y, width, height } = this;
|
let { x, y, width, height } = this;
|
||||||
width *= pageWidth;
|
width *= pageWidth;
|
||||||
height *= pageHeight;
|
height *= pageHeight;
|
||||||
@ -581,7 +611,6 @@ class AnnotationEditor {
|
|||||||
x += bx;
|
x += bx;
|
||||||
y += by;
|
y += by;
|
||||||
|
|
||||||
const { style } = this.div;
|
|
||||||
style.left = `${(100 * x).toFixed(2)}%`;
|
style.left = `${(100 * x).toFixed(2)}%`;
|
||||||
style.top = `${(100 * y).toFixed(2)}%`;
|
style.top = `${(100 * y).toFixed(2)}%`;
|
||||||
|
|
||||||
@ -659,9 +688,10 @@ class AnnotationEditor {
|
|||||||
*/
|
*/
|
||||||
setDims(width, height) {
|
setDims(width, height) {
|
||||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||||
this.div.style.width = `${((100 * width) / parentWidth).toFixed(2)}%`;
|
const { style } = this.div;
|
||||||
|
style.width = `${((100 * width) / parentWidth).toFixed(2)}%`;
|
||||||
if (!this.#keepAspectRatio) {
|
if (!this.#keepAspectRatio) {
|
||||||
this.div.style.height = `${((100 * height) / parentHeight).toFixed(2)}%`;
|
style.height = `${((100 * height) / parentHeight).toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,9 +709,7 @@ class AnnotationEditor {
|
|||||||
style.width = `${((100 * parseFloat(width)) / parentWidth).toFixed(2)}%`;
|
style.width = `${((100 * parseFloat(width)) / parentWidth).toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
if (!this.#keepAspectRatio && !heightPercent) {
|
if (!this.#keepAspectRatio && !heightPercent) {
|
||||||
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(
|
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(2)}%`;
|
||||||
2
|
|
||||||
)}%`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,10 +787,12 @@ class AnnotationEditor {
|
|||||||
{ passive: false, signal }
|
{ passive: false, signal }
|
||||||
);
|
);
|
||||||
window.addEventListener("contextmenu", noContextMenu, { signal });
|
window.addEventListener("contextmenu", noContextMenu, { signal });
|
||||||
const savedX = this.x;
|
this.#savedDimensions = {
|
||||||
const savedY = this.y;
|
savedX: this.x,
|
||||||
const savedWidth = this.width;
|
savedY: this.y,
|
||||||
const savedHeight = this.height;
|
savedWidth: this.width,
|
||||||
|
savedHeight: this.height,
|
||||||
|
};
|
||||||
const savedParentCursor = this.parent.div.style.cursor;
|
const savedParentCursor = this.parent.div.style.cursor;
|
||||||
const savedCursor = this.div.style.cursor;
|
const savedCursor = this.div.style.cursor;
|
||||||
this.div.style.cursor = this.parent.div.style.cursor =
|
this.div.style.cursor = this.parent.div.style.cursor =
|
||||||
@ -776,7 +806,7 @@ class AnnotationEditor {
|
|||||||
this.parent.div.style.cursor = savedParentCursor;
|
this.parent.div.style.cursor = savedParentCursor;
|
||||||
this.div.style.cursor = savedCursor;
|
this.div.style.cursor = savedCursor;
|
||||||
|
|
||||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
this.#addResizeToUndoStack();
|
||||||
};
|
};
|
||||||
window.addEventListener("pointerup", pointerUpCallback, { signal });
|
window.addEventListener("pointerup", pointerUpCallback, { signal });
|
||||||
// If the user switches to another window (with alt+tab), then we end the
|
// If the user switches to another window (with alt+tab), then we end the
|
||||||
@ -784,7 +814,29 @@ class AnnotationEditor {
|
|||||||
window.addEventListener("blur", pointerUpCallback, { signal });
|
window.addEventListener("blur", pointerUpCallback, { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight) {
|
#resize(x, y, width, height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||||
|
this.setDims(parentWidth * width, parentHeight * height);
|
||||||
|
this.fixAndSetPosition();
|
||||||
|
this._onResized();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the editor has been resized.
|
||||||
|
*/
|
||||||
|
_onResized() {}
|
||||||
|
|
||||||
|
#addResizeToUndoStack() {
|
||||||
|
if (!this.#savedDimensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
|
||||||
|
this.#savedDimensions = null;
|
||||||
|
|
||||||
const newX = this.x;
|
const newX = this.x;
|
||||||
const newY = this.y;
|
const newY = this.y;
|
||||||
const newWidth = this.width;
|
const newWidth = this.width;
|
||||||
@ -799,24 +851,8 @@ class AnnotationEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.addCommands({
|
this.addCommands({
|
||||||
cmd: () => {
|
cmd: this.#resize.bind(this, newX, newY, newWidth, newHeight),
|
||||||
this.width = newWidth;
|
undo: this.#resize.bind(this, savedX, savedY, savedWidth, savedHeight),
|
||||||
this.height = newHeight;
|
|
||||||
this.x = newX;
|
|
||||||
this.y = newY;
|
|
||||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
|
||||||
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
|
||||||
this.fixAndSetPosition();
|
|
||||||
},
|
|
||||||
undo: () => {
|
|
||||||
this.width = savedWidth;
|
|
||||||
this.height = savedHeight;
|
|
||||||
this.x = savedX;
|
|
||||||
this.y = savedY;
|
|
||||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
|
||||||
this.setDims(parentWidth * savedWidth, parentHeight * savedHeight);
|
|
||||||
this.fixAndSetPosition();
|
|
||||||
},
|
|
||||||
mustExec: true,
|
mustExec: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -960,8 +996,15 @@ class AnnotationEditor {
|
|||||||
|
|
||||||
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
||||||
this.fixAndSetPosition();
|
this.fixAndSetPosition();
|
||||||
|
|
||||||
|
this._onResizing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the editor is being resized.
|
||||||
|
*/
|
||||||
|
_onResizing() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the alt text dialog is closed.
|
* Called when the alt text dialog is closed.
|
||||||
*/
|
*/
|
||||||
@ -1194,9 +1237,12 @@ class AnnotationEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._onStartDragging();
|
||||||
|
|
||||||
const pointerUpCallback = e => {
|
const pointerUpCallback = e => {
|
||||||
if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) {
|
if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) {
|
||||||
cancelDrag(e);
|
cancelDrag(e);
|
||||||
|
this._onStopDragging();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopEvent(e);
|
stopEvent(e);
|
||||||
@ -1208,6 +1254,10 @@ class AnnotationEditor {
|
|||||||
window.addEventListener("blur", pointerUpCallback, { signal });
|
window.addEventListener("blur", pointerUpCallback, { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onStartDragging() {}
|
||||||
|
|
||||||
|
_onStopDragging() {}
|
||||||
|
|
||||||
moveInDOM() {
|
moveInDOM() {
|
||||||
// Moving the editor in the DOM can be expensive, so we wait a bit before.
|
// Moving the editor in the DOM can be expensive, so we wait a bit before.
|
||||||
// It's important to not block the UI (for example when changing the font
|
// It's important to not block the UI (for example when changing the font
|
||||||
@ -1226,6 +1276,7 @@ class AnnotationEditor {
|
|||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.fixAndSetPosition();
|
this.fixAndSetPosition();
|
||||||
|
this._onTranslated();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1372,11 +1423,16 @@ class AnnotationEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotate the editor.
|
* Rotate the editor when the page is rotated.
|
||||||
* @param {number} angle
|
* @param {number} angle
|
||||||
*/
|
*/
|
||||||
rotate(_angle) {}
|
rotate(_angle) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the editor when the page is resized.
|
||||||
|
*/
|
||||||
|
resize() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize the editor when it has been deleted.
|
* Serialize the editor when it has been deleted.
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
@ -1622,11 +1678,7 @@ class AnnotationEditor {
|
|||||||
#stopResizing() {
|
#stopResizing() {
|
||||||
this.#isResizerEnabledForKeyboard = false;
|
this.#isResizerEnabledForKeyboard = false;
|
||||||
this.#setResizerTabIndex(-1);
|
this.#setResizerTabIndex(-1);
|
||||||
if (this.#savedDimensions) {
|
this.#addResizeToUndoStack();
|
||||||
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
|
|
||||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
|
||||||
this.#savedDimensions = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopResizingWithKeyboard() {
|
_stopResizingWithKeyboard() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -411,6 +411,21 @@ class CommandManager {
|
|||||||
return this.#position < this.#commands.length - 1;
|
return this.#position < this.#commands.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanType(type) {
|
||||||
|
if (this.#position === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = this.#position; i >= 0; i--) {
|
||||||
|
if (this.#commands[i].type !== type) {
|
||||||
|
this.#commands.splice(i + 1, this.#position - i);
|
||||||
|
this.#position = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#commands.length = 0;
|
||||||
|
this.#position = -1;
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.#commands = null;
|
this.#commands = null;
|
||||||
}
|
}
|
||||||
@ -1034,6 +1049,7 @@ class AnnotationEditorUIManager {
|
|||||||
for (const editor of this.#editorsToRescale) {
|
for (const editor of this.#editorsToRescale) {
|
||||||
editor.onScaleChanging();
|
editor.onScaleChanging();
|
||||||
}
|
}
|
||||||
|
this.currentLayer?.onScaleChanging();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRotationChanging({ pagesRotation }) {
|
onRotationChanging({ pagesRotation }) {
|
||||||
@ -1931,6 +1947,10 @@ class AnnotationEditorUIManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUIForDefaultProperties(editorType) {
|
||||||
|
this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add or remove an editor the current selection.
|
* Add or remove an editor the current selection.
|
||||||
* @param {AnnotationEditor} editor
|
* @param {AnnotationEditor} editor
|
||||||
@ -1957,6 +1977,7 @@ class AnnotationEditorUIManager {
|
|||||||
* @param {AnnotationEditor} editor
|
* @param {AnnotationEditor} editor
|
||||||
*/
|
*/
|
||||||
setSelected(editor) {
|
setSelected(editor) {
|
||||||
|
this.currentLayer?.commitOrRemove();
|
||||||
for (const ed of this.#selectedEditors) {
|
for (const ed of this.#selectedEditors) {
|
||||||
if (ed !== editor) {
|
if (ed !== editor) {
|
||||||
ed.unselect();
|
ed.unselect();
|
||||||
@ -2044,6 +2065,10 @@ class AnnotationEditorUIManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanUndoStack(type) {
|
||||||
|
this.#commandManager.cleanType(type);
|
||||||
|
}
|
||||||
|
|
||||||
#isEmpty() {
|
#isEmpty() {
|
||||||
if (this.#allEditors.size === 0) {
|
if (this.#allEditors.size === 0) {
|
||||||
return true;
|
return true;
|
||||||
@ -2134,6 +2159,10 @@ class AnnotationEditorUIManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.currentLayer?.commitOrRemove()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.hasSelection) {
|
if (!this.hasSelection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,7 @@ const AnnotationEditorParamsType = {
|
|||||||
HIGHLIGHT_THICKNESS: 33,
|
HIGHLIGHT_THICKNESS: 33,
|
||||||
HIGHLIGHT_FREE: 34,
|
HIGHLIGHT_FREE: 34,
|
||||||
HIGHLIGHT_SHOW_ALL: 35,
|
HIGHLIGHT_SHOW_ALL: 35,
|
||||||
|
DRAW_STEP: 41,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
||||||
|
|||||||
@ -649,7 +649,7 @@ class Driver {
|
|||||||
|
|
||||||
if (task.annotationStorage) {
|
if (task.annotationStorage) {
|
||||||
for (const annotation of Object.values(task.annotationStorage)) {
|
for (const annotation of Object.values(task.annotationStorage)) {
|
||||||
const { bitmapName, quadPoints } = annotation;
|
const { bitmapName, quadPoints, paths, outlines } = annotation;
|
||||||
if (bitmapName) {
|
if (bitmapName) {
|
||||||
promise = promise.then(async doc => {
|
promise = promise.then(async doc => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@ -687,6 +687,36 @@ class Driver {
|
|||||||
// like IRL (in order to avoid bugs like bug 1907958).
|
// like IRL (in order to avoid bugs like bug 1907958).
|
||||||
annotation.quadPoints = new Float32Array(quadPoints);
|
annotation.quadPoints = new Float32Array(quadPoints);
|
||||||
}
|
}
|
||||||
|
if (paths) {
|
||||||
|
for (let i = 0, ii = paths.lines.length; i < ii; i++) {
|
||||||
|
paths.lines[i] = Float32Array.from(
|
||||||
|
paths.lines[i],
|
||||||
|
x => x ?? NaN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (let i = 0, ii = paths.points.length; i < ii; i++) {
|
||||||
|
paths.points[i] = Float32Array.from(
|
||||||
|
paths.points[i],
|
||||||
|
x => x ?? NaN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outlines) {
|
||||||
|
if (Array.isArray(outlines)) {
|
||||||
|
for (let i = 0, ii = outlines.length; i < ii; i++) {
|
||||||
|
outlines[i] = Float32Array.from(outlines[i], x => x ?? NaN);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outlines.outline = Float32Array.from(
|
||||||
|
outlines.outline,
|
||||||
|
x => x ?? NaN
|
||||||
|
);
|
||||||
|
outlines.points = Float32Array.from(
|
||||||
|
outlines.points,
|
||||||
|
x => x ?? NaN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -117,7 +117,7 @@ describe("Ink Editor", () => {
|
|||||||
|
|
||||||
await commit(page);
|
await commit(page);
|
||||||
|
|
||||||
const rectBefore = await getRect(page, ".inkEditor canvas");
|
const rectBefore = await getRect(page, ".canvasWrapper .draw");
|
||||||
|
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
await kbUndo(page);
|
await kbUndo(page);
|
||||||
@ -126,7 +126,7 @@ describe("Ink Editor", () => {
|
|||||||
await waitForStorageEntries(page, 1);
|
await waitForStorageEntries(page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rectAfter = await getRect(page, ".inkEditor canvas");
|
const rectAfter = await getRect(page, ".canvasWrapper .draw");
|
||||||
|
|
||||||
expect(Math.round(rectBefore.x))
|
expect(Math.round(rectBefore.x))
|
||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
@ -453,4 +453,118 @@ describe("Ink Editor", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Drawing must unselect all", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that when we start to draw then the editors are unselected", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToInk(page);
|
||||||
|
const rect = await getRect(page, ".annotationEditorLayer");
|
||||||
|
|
||||||
|
let xStart = rect.x + 10;
|
||||||
|
const yStart = rect.y + 10;
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const clickHandle = await waitForPointerUp(page);
|
||||||
|
await page.mouse.move(xStart, yStart);
|
||||||
|
await page.mouse.down();
|
||||||
|
if (i === 1) {
|
||||||
|
expect(await getSelectedEditors(page))
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual([]);
|
||||||
|
}
|
||||||
|
await page.mouse.move(xStart + 50, yStart + 50);
|
||||||
|
await page.mouse.up();
|
||||||
|
await awaitPromise(clickHandle);
|
||||||
|
await commit(page);
|
||||||
|
xStart += 70;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Selected editor must be updated even if the page has been destroyed", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that the color has been changed", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToInk(page);
|
||||||
|
|
||||||
|
const rect = await getRect(page, ".annotationEditorLayer");
|
||||||
|
|
||||||
|
const x = rect.x + 20;
|
||||||
|
const y = rect.y + 20;
|
||||||
|
const clickHandle = await waitForPointerUp(page);
|
||||||
|
await page.mouse.move(x, y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(x + 50, y + 50);
|
||||||
|
await page.mouse.up();
|
||||||
|
await awaitPromise(clickHandle);
|
||||||
|
|
||||||
|
await commit(page);
|
||||||
|
|
||||||
|
const drawSelector = `.page[data-page-number = "1"] .canvasWrapper .draw`;
|
||||||
|
await page.waitForSelector(drawSelector, { visible: true });
|
||||||
|
let color = await page.evaluate(sel => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
return el.getAttribute("stroke");
|
||||||
|
}, drawSelector);
|
||||||
|
expect(color).toEqual("#000000");
|
||||||
|
|
||||||
|
const oneToFourteen = Array.from(new Array(13).keys(), n => n + 2);
|
||||||
|
for (const pageNumber of oneToFourteen) {
|
||||||
|
await scrollIntoView(
|
||||||
|
page,
|
||||||
|
`.page[data-page-number = "${pageNumber}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const red = "#ff0000";
|
||||||
|
page.evaluate(value => {
|
||||||
|
window.PDFViewerApplication.eventBus.dispatch(
|
||||||
|
"switchannotationeditorparams",
|
||||||
|
{
|
||||||
|
source: null,
|
||||||
|
type: window.pdfjsLib.AnnotationEditorParamsType.INK_COLOR,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, red);
|
||||||
|
|
||||||
|
const fourteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
|
||||||
|
for (const pageNumber of fourteenToOne) {
|
||||||
|
await scrollIntoView(
|
||||||
|
page,
|
||||||
|
`.page[data-page-number = "${pageNumber}"]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await page.waitForSelector(drawSelector, { visible: true });
|
||||||
|
color = await page.evaluate(sel => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
return el.getAttribute("stroke");
|
||||||
|
}, drawSelector);
|
||||||
|
expect(color).toEqual(red);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8244,24 +8244,44 @@
|
|||||||
"color": [255, 0, 0],
|
"color": [255, 0, 0],
|
||||||
"thickness": 3,
|
"thickness": 3,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [
|
[
|
||||||
73, 560.2277710847244, 74.30408044851005, 561.5318515332344,
|
null,
|
||||||
76.89681158113368, 557.7555609512324, 77.5, 557.2277710847244,
|
null,
|
||||||
81.95407020558315, 553.3304596548392, 87.4811839685984,
|
null,
|
||||||
550.8645311043504, 92.5, 547.7277710847244, 97.38795894206055,
|
null,
|
||||||
544.6727967459365, 109.48854351637208, 540.2392275683522, 113.5,
|
73,
|
||||||
|
560.2277710847244,
|
||||||
|
74.30408044851005,
|
||||||
|
561.5318515332344,
|
||||||
|
76.89681158113368,
|
||||||
|
557.7555609512324,
|
||||||
|
77.5,
|
||||||
|
557.2277710847244,
|
||||||
|
81.95407020558315,
|
||||||
|
553.3304596548392,
|
||||||
|
87.4811839685984,
|
||||||
|
550.8645311043504,
|
||||||
|
92.5,
|
||||||
|
547.7277710847244,
|
||||||
|
97.38795894206055,
|
||||||
|
544.6727967459365,
|
||||||
|
109.48854351637208,
|
||||||
|
540.2392275683522,
|
||||||
|
113.5,
|
||||||
536.2277710847244
|
536.2277710847244
|
||||||
],
|
]
|
||||||
"points": [
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
73, 560.2277710847244, 76.7257911988625, 558.1025687477292,
|
73, 560.2277710847244, 76.7257911988625, 558.1025687477292,
|
||||||
75.5128345111164, 559.4147224528562, 77.5, 557.2277710847244,
|
75.5128345111164, 559.4147224528562, 77.5, 557.2277710847244,
|
||||||
92.5, 547.7277710847244, 109.21378602219673, 539.2873735223628,
|
92.5, 547.7277710847244, 109.21378602219673, 539.2873735223628,
|
||||||
103.32868842191223, 542.3364518890394, 113.5, 536.2277710847244
|
103.32868842191223, 542.3364518890394, 113.5, 536.2277710847244
|
||||||
]
|
]
|
||||||
}
|
]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [71.5, 534.5, 115, 562],
|
"rect": [71.5, 534.5, 115, 562],
|
||||||
"rotation": 0
|
"rotation": 0
|
||||||
@ -8330,22 +8350,37 @@
|
|||||||
"color": [255, 0, 0],
|
"color": [255, 0, 0],
|
||||||
"thickness": 1,
|
"thickness": 1,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [
|
[
|
||||||
417.61538461538464, 520.3461538461538, 419.15384615384613,
|
null,
|
||||||
520.3461538461538, 421.0769230769231, 520.3461538461538,
|
null,
|
||||||
423.38461538461536, 520.3461538461538, 425.6923076923077,
|
null,
|
||||||
520.3461538461538, 429.15384615384613, 519.9615384615385,
|
null,
|
||||||
433.7692307692308, 519.1923076923076
|
417.61538461538464,
|
||||||
],
|
520.3461538461538,
|
||||||
"points": [
|
419.15384615384613,
|
||||||
|
520.3461538461538,
|
||||||
|
421.0769230769231,
|
||||||
|
520.3461538461538,
|
||||||
|
423.38461538461536,
|
||||||
|
520.3461538461538,
|
||||||
|
425.6923076923077,
|
||||||
|
520.3461538461538,
|
||||||
|
429.15384615384613,
|
||||||
|
519.9615384615385,
|
||||||
|
433.7692307692308,
|
||||||
|
519.1923076923076
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
417.61538461538464, 520.3461538461538, 419.15384615384613,
|
417.61538461538464, 520.3461538461538, 419.15384615384613,
|
||||||
520.3461538461538, 425.6923076923077, 520.3461538461538,
|
520.3461538461538, 425.6923076923077, 520.3461538461538,
|
||||||
433.7692307692308, 519.1923076923076
|
433.7692307692308, 519.1923076923076
|
||||||
]
|
]
|
||||||
}
|
]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [
|
"rect": [
|
||||||
417.11538461538464, 510.46153846153845, 434.42307692307696,
|
417.11538461538464, 510.46153846153845, 434.42307692307696,
|
||||||
@ -8358,22 +8393,37 @@
|
|||||||
"color": [0, 255, 0],
|
"color": [0, 255, 0],
|
||||||
"thickness": 1,
|
"thickness": 1,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [
|
[
|
||||||
449.92307692307696, 526.6538461538462, 449.92307692307696,
|
null,
|
||||||
527.423076923077, 449.6346153846154, 528.8653846153846,
|
null,
|
||||||
449.0576923076924, 530.9807692307693, 448.4807692307693,
|
null,
|
||||||
533.0961538461539, 447.8076923076924, 536.6538461538462,
|
null,
|
||||||
447.0384615384616, 541.6538461538462
|
449.92307692307696,
|
||||||
],
|
526.6538461538462,
|
||||||
"points": [
|
449.92307692307696,
|
||||||
|
527.423076923077,
|
||||||
|
449.6346153846154,
|
||||||
|
528.8653846153846,
|
||||||
|
449.0576923076924,
|
||||||
|
530.9807692307693,
|
||||||
|
448.4807692307693,
|
||||||
|
533.0961538461539,
|
||||||
|
447.8076923076924,
|
||||||
|
536.6538461538462,
|
||||||
|
447.0384615384616,
|
||||||
|
541.6538461538462
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
449.92307692307696, 526.6538461538462, 449.92307692307696,
|
449.92307692307696, 526.6538461538462, 449.92307692307696,
|
||||||
527.423076923077, 448.4807692307693, 533.0961538461539,
|
527.423076923077, 448.4807692307693, 533.0961538461539,
|
||||||
447.0384615384616, 541.6538461538462
|
447.0384615384616, 541.6538461538462
|
||||||
]
|
]
|
||||||
}
|
]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [
|
"rect": [
|
||||||
446.5384615384616, 526.1538461538462, 456.92307692307696,
|
446.5384615384616, 526.1538461538462, 456.92307692307696,
|
||||||
@ -8386,22 +8436,37 @@
|
|||||||
"color": [0, 0, 255],
|
"color": [0, 0, 255],
|
||||||
"thickness": 1,
|
"thickness": 1,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [
|
[
|
||||||
482.8461538461538, 511.6538461538462, 482.07692307692304,
|
null,
|
||||||
511.6538461538462, 480.53846153846155, 511.6538461538462,
|
null,
|
||||||
478.23076923076917, 511.6538461538462, 475.9230769230769,
|
null,
|
||||||
511.6538461538462, 472.46153846153845, 511.6538461538462,
|
null,
|
||||||
467.8461538461538, 511.6538461538462
|
482.8461538461538,
|
||||||
],
|
511.6538461538462,
|
||||||
"points": [
|
482.07692307692304,
|
||||||
|
511.6538461538462,
|
||||||
|
480.53846153846155,
|
||||||
|
511.6538461538462,
|
||||||
|
478.23076923076917,
|
||||||
|
511.6538461538462,
|
||||||
|
475.9230769230769,
|
||||||
|
511.6538461538462,
|
||||||
|
472.46153846153845,
|
||||||
|
511.6538461538462,
|
||||||
|
467.8461538461538,
|
||||||
|
511.6538461538462
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
482.8461538461538, 511.6538461538462, 482.07692307692304,
|
482.8461538461538, 511.6538461538462, 482.07692307692304,
|
||||||
511.6538461538462, 475.9230769230769, 511.6538461538462,
|
511.6538461538462, 475.9230769230769, 511.6538461538462,
|
||||||
467.8461538461538, 511.6538461538462
|
467.8461538461538, 511.6538461538462
|
||||||
]
|
]
|
||||||
}
|
]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [
|
"rect": [
|
||||||
467.1923076923077, 511.1538461538462, 483.3461538461538,
|
467.1923076923077, 511.1538461538462, 483.3461538461538,
|
||||||
@ -8414,22 +8479,37 @@
|
|||||||
"color": [0, 255, 255],
|
"color": [0, 255, 255],
|
||||||
"thickness": 1,
|
"thickness": 1,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [
|
[
|
||||||
445.9230769230769, 509.3846153846154, 445.5384615384615,
|
null,
|
||||||
509.3846153846154, 445.15384615384613, 508.1346153846154,
|
null,
|
||||||
444.7692307692307, 505.6346153846154, 444.38461538461536,
|
null,
|
||||||
503.1346153846154, 443.23076923076917, 499.00000000000006,
|
null,
|
||||||
441.30769230769226, 493.2307692307693
|
445.9230769230769,
|
||||||
],
|
509.3846153846154,
|
||||||
"points": [
|
445.5384615384615,
|
||||||
|
509.3846153846154,
|
||||||
|
445.15384615384613,
|
||||||
|
508.1346153846154,
|
||||||
|
444.7692307692307,
|
||||||
|
505.6346153846154,
|
||||||
|
444.38461538461536,
|
||||||
|
503.1346153846154,
|
||||||
|
443.23076923076917,
|
||||||
|
499.00000000000006,
|
||||||
|
441.30769230769226,
|
||||||
|
493.2307692307693
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
445.9230769230769, 509.3846153846154, 445.5384615384615,
|
445.9230769230769, 509.3846153846154, 445.5384615384615,
|
||||||
509.3846153846154, 444.38461538461536, 503.1346153846154,
|
509.3846153846154, 444.38461538461536, 503.1346153846154,
|
||||||
441.30769230769226, 493.2307692307693
|
441.30769230769226, 493.2307692307693
|
||||||
]
|
]
|
||||||
}
|
]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [
|
"rect": [
|
||||||
436.03846153846155, 492.5769230769231, 446.4230769230769,
|
436.03846153846155, 492.5769230769231, 446.4230769230769,
|
||||||
@ -9599,12 +9679,12 @@
|
|||||||
"color": [53, 228, 47],
|
"color": [53, 228, 47],
|
||||||
"thickness": 20,
|
"thickness": 20,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"paths": [
|
"paths": {
|
||||||
{
|
"lines": [
|
||||||
"bezier": [279.9183673469388, 477.0105263157895],
|
[null, null, null, null, 279.9183673469388, 477.0105263157895]
|
||||||
"points": [279.9183673469388, 477.0105263157895]
|
],
|
||||||
}
|
"points": [[279.9183673469388, 477.0105263157895]]
|
||||||
],
|
},
|
||||||
"pageIndex": 0,
|
"pageIndex": 0,
|
||||||
"rect": [
|
"rect": [
|
||||||
269.9183673469388, 443.93684210526317, 312.9387755102041,
|
269.9183673469388, 443.93684210526317, 312.9387755102041,
|
||||||
|
|||||||
@ -4448,21 +4448,54 @@ describe("annotation", function () {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
color: [0, 0, 0],
|
color: [0, 0, 0],
|
||||||
paths: [
|
paths: {
|
||||||
{
|
lines: [
|
||||||
bezier: [
|
[
|
||||||
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
],
|
],
|
||||||
points: [1, 2, 3, 4, 5, 6, 7, 8],
|
[
|
||||||
},
|
NaN,
|
||||||
{
|
NaN,
|
||||||
bezier: [
|
NaN,
|
||||||
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
|
NaN,
|
||||||
926, 927,
|
910,
|
||||||
|
911,
|
||||||
|
912,
|
||||||
|
913,
|
||||||
|
914,
|
||||||
|
915,
|
||||||
|
916,
|
||||||
|
917,
|
||||||
|
922,
|
||||||
|
923,
|
||||||
|
924,
|
||||||
|
925,
|
||||||
|
926,
|
||||||
|
927,
|
||||||
],
|
],
|
||||||
points: [91, 92, 93, 94, 95, 96, 97, 98],
|
],
|
||||||
},
|
points: [
|
||||||
],
|
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
[91, 92, 93, 94, 95, 96, 97, 98],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
null,
|
null,
|
||||||
@ -4482,13 +4515,12 @@ describe("annotation", function () {
|
|||||||
const appearance = data[1].data;
|
const appearance = data[1].data;
|
||||||
expect(appearance).toEqual(
|
expect(appearance).toEqual(
|
||||||
"2 0 obj\n" +
|
"2 0 obj\n" +
|
||||||
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 129>> stream\n" +
|
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 127>> stream\n" +
|
||||||
"1 w 1 J 1 j\n" +
|
"1 w 1 J 1 j\n" +
|
||||||
"0 G\n" +
|
"0 G\n" +
|
||||||
"10 11 m\n" +
|
"10 11 m\n" +
|
||||||
"12 13 14 15 16 17 c\n" +
|
"12 13 14 15 16 17 c\n" +
|
||||||
"22 23 24 25 26 27 c\n" +
|
"22 23 24 25 26 27 c\n" +
|
||||||
"S\n" +
|
|
||||||
"910 911 m\n" +
|
"910 911 m\n" +
|
||||||
"912 913 914 915 916 917 c\n" +
|
"912 913 914 915 916 917 c\n" +
|
||||||
"922 923 924 925 926 927 c\n" +
|
"922 923 924 925 926 927 c\n" +
|
||||||
@ -4513,21 +4545,54 @@ describe("annotation", function () {
|
|||||||
thickness: 1,
|
thickness: 1,
|
||||||
opacity: 0.12,
|
opacity: 0.12,
|
||||||
color: [0, 0, 0],
|
color: [0, 0, 0],
|
||||||
paths: [
|
paths: {
|
||||||
{
|
lines: [
|
||||||
bezier: [
|
[
|
||||||
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
],
|
],
|
||||||
points: [1, 2, 3, 4, 5, 6, 7, 8],
|
[
|
||||||
},
|
NaN,
|
||||||
{
|
NaN,
|
||||||
bezier: [
|
NaN,
|
||||||
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
|
NaN,
|
||||||
926, 927,
|
910,
|
||||||
|
911,
|
||||||
|
912,
|
||||||
|
913,
|
||||||
|
914,
|
||||||
|
915,
|
||||||
|
916,
|
||||||
|
917,
|
||||||
|
922,
|
||||||
|
923,
|
||||||
|
924,
|
||||||
|
925,
|
||||||
|
926,
|
||||||
|
927,
|
||||||
],
|
],
|
||||||
points: [91, 92, 93, 94, 95, 96, 97, 98],
|
],
|
||||||
},
|
points: [
|
||||||
],
|
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
[91, 92, 93, 94, 95, 96, 97, 98],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
null,
|
null,
|
||||||
@ -4547,7 +4612,7 @@ describe("annotation", function () {
|
|||||||
const appearance = data[1].data;
|
const appearance = data[1].data;
|
||||||
expect(appearance).toEqual(
|
expect(appearance).toEqual(
|
||||||
"2 0 obj\n" +
|
"2 0 obj\n" +
|
||||||
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 136 /Resources " +
|
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 134 /Resources " +
|
||||||
"<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" +
|
"<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" +
|
||||||
"1 w 1 J 1 j\n" +
|
"1 w 1 J 1 j\n" +
|
||||||
"0 G\n" +
|
"0 G\n" +
|
||||||
@ -4555,7 +4620,6 @@ describe("annotation", function () {
|
|||||||
"10 11 m\n" +
|
"10 11 m\n" +
|
||||||
"12 13 14 15 16 17 c\n" +
|
"12 13 14 15 16 17 c\n" +
|
||||||
"22 23 24 25 26 27 c\n" +
|
"22 23 24 25 26 27 c\n" +
|
||||||
"S\n" +
|
|
||||||
"910 911 m\n" +
|
"910 911 m\n" +
|
||||||
"912 913 914 915 916 917 c\n" +
|
"912 913 914 915 916 917 c\n" +
|
||||||
"922 923 924 925 926 927 c\n" +
|
"922 923 924 925 926 927 c\n" +
|
||||||
@ -4581,13 +4645,10 @@ describe("annotation", function () {
|
|||||||
thickness: 3,
|
thickness: 3,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
color: [0, 255, 0],
|
color: [0, 255, 0],
|
||||||
paths: [
|
paths: {
|
||||||
{
|
lines: [[NaN, NaN, NaN, NaN, 1, 2, 3, 4, 5, 6, 7, 8]],
|
||||||
bezier: [1, 2, 3, 4, 5, 6, 7, 8],
|
points: [[1, 2, 3, 4, 5, 6, 7, 8]],
|
||||||
// Useless in the printing case.
|
},
|
||||||
points: [1, 2, 3, 4, 5, 6, 7, 8],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -66,20 +66,22 @@
|
|||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textLayer.highlighting {
|
.textLayer {
|
||||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
&.highlighting {
|
||||||
|
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||||
|
|
||||||
&:not(.free) span {
|
&:not(.free) span {
|
||||||
cursor: var(--editorHighlight-editing-cursor);
|
cursor: var(--editorHighlight-editing-cursor);
|
||||||
|
|
||||||
&[role="img"] {
|
&[role="img"] {
|
||||||
|
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.free span {
|
||||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.free span {
|
|
||||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewerContainer.pdfPresentationMode:fullscreen,
|
#viewerContainer.pdfPresentationMode:fullscreen,
|
||||||
@ -154,6 +156,11 @@
|
|||||||
|
|
||||||
.annotationEditorLayer.inkEditing {
|
.annotationEditorLayer.inkEditing {
|
||||||
cursor: var(--editorInk-editing-cursor);
|
cursor: var(--editorInk-editing-cursor);
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotationEditorLayer .draw {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) {
|
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) {
|
||||||
|
|||||||
@ -17,6 +17,10 @@
|
|||||||
svg {
|
svg {
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|
||||||
|
&.moving {
|
||||||
|
z-index: 100000;
|
||||||
|
}
|
||||||
|
|
||||||
&.highlight,
|
&.highlight,
|
||||||
&.highlightOutline {
|
&.highlightOutline {
|
||||||
&[data-main-rotation="90"] {
|
&[data-main-rotation="90"] {
|
||||||
@ -41,6 +45,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.draw {
|
||||||
|
position: absolute;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
|
||||||
|
&[data-draw-rotation="90"] {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-draw-rotation="180"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-draw-rotation="270"] {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
--blend-mode: multiply;
|
--blend-mode: multiply;
|
||||||
|
|
||||||
|
|||||||
@ -300,7 +300,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
</div>
|
</div>
|
||||||
<div class="editorParamsSetter">
|
<div class="editorParamsSetter">
|
||||||
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
|
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
|
||||||
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="0">
|
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="1" min="0.05" max="1" step="0.05" tabindex="0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user