mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 14:48:08 +02:00
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
17 changed files with 2560 additions and 1377 deletions
|
@ -4466,7 +4466,7 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
ink.set("Subtype", Name.get("Ink"));
|
||||
ink.set("CreationDate", `D:${getModificationDate()}`);
|
||||
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("Rotate", rotation);
|
||||
|
||||
|
@ -4523,28 +4523,29 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
appearanceBuffer.push("/R0 gs");
|
||||
}
|
||||
|
||||
const buffer = [];
|
||||
for (const { bezier } of paths) {
|
||||
buffer.length = 0;
|
||||
buffer.push(
|
||||
`${numberToString(bezier[0])} ${numberToString(bezier[1])} m`
|
||||
);
|
||||
if (bezier.length === 2) {
|
||||
buffer.push(
|
||||
`${numberToString(bezier[0])} ${numberToString(bezier[1])} l S`
|
||||
);
|
||||
} else {
|
||||
for (let i = 2, ii = bezier.length; i < ii; i += 6) {
|
||||
const curve = bezier
|
||||
.slice(i, i + 6)
|
||||
.map(numberToString)
|
||||
.join(" ");
|
||||
buffer.push(`${curve} c`);
|
||||
for (const outline of paths.lines) {
|
||||
for (let i = 0, ii = outline.length; i < ii; i += 6) {
|
||||
if (isNaN(outline[i])) {
|
||||
appearanceBuffer.push(
|
||||
`${numberToString(outline[i + 4])} ${numberToString(
|
||||
outline[i + 5]
|
||||
)} m`
|
||||
);
|
||||
} else {
|
||||
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
|
||||
appearanceBuffer.push(
|
||||
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " 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 appearanceStreamDict = new Dict(xref);
|
||||
|
@ -4587,18 +4588,17 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
|
||||
);
|
||||
for (let i = 6, ii = outline.length; i < ii; i += 6) {
|
||||
if (isNaN(outline[i]) || outline[i] === null) {
|
||||
if (isNaN(outline[i])) {
|
||||
appearanceBuffer.push(
|
||||
`${numberToString(outline[i + 4])} ${numberToString(
|
||||
outline[i + 5]
|
||||
)} l`
|
||||
);
|
||||
} else {
|
||||
const curve = outline
|
||||
.slice(i, i + 6)
|
||||
.map(numberToString)
|
||||
.join(" ");
|
||||
appearanceBuffer.push(`${curve} c`);
|
||||
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
|
||||
appearanceBuffer.push(
|
||||
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " c"
|
||||
);
|
||||
}
|
||||
}
|
||||
appearanceBuffer.push("h f");
|
||||
|
|
|
@ -183,11 +183,18 @@ class DrawLayer {
|
|||
this.updateProperties(id, properties);
|
||||
}
|
||||
|
||||
updateProperties(elementOrId, { root, bbox, rootClass, path }) {
|
||||
updateProperties(elementOrId, properties) {
|
||||
if (!properties) {
|
||||
return;
|
||||
}
|
||||
const { root, bbox, rootClass, path } = properties;
|
||||
const element =
|
||||
typeof elementOrId === "number"
|
||||
? this.#mapping.get(elementOrId)
|
||||
: elementOrId;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (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) {
|
||||
this.#toUpdate.delete(id);
|
||||
if (this.#parent === null) {
|
||||
|
|
|
@ -72,10 +72,10 @@ class AnnotationEditorLayer {
|
|||
|
||||
#hadPointerDown = false;
|
||||
|
||||
#isCleaningUp = false;
|
||||
|
||||
#isDisabling = false;
|
||||
|
||||
#drawingAC = null;
|
||||
|
||||
#textLayer = null;
|
||||
|
||||
#textSelectionAC = null;
|
||||
|
@ -160,12 +160,9 @@ class AnnotationEditorLayer {
|
|||
this.disableClick();
|
||||
return;
|
||||
case AnnotationEditorType.INK:
|
||||
// We always want to have an ink editor ready to draw in.
|
||||
this.addInkEditorIfNeeded(false);
|
||||
|
||||
this.disableTextSelection();
|
||||
this.togglePointerEvents(true);
|
||||
this.disableClick();
|
||||
this.enableClick();
|
||||
break;
|
||||
case AnnotationEditorType.HIGHLIGHT:
|
||||
this.enableTextSelection();
|
||||
|
@ -193,30 +190,6 @@ class AnnotationEditorLayer {
|
|||
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.
|
||||
* @param {boolean} isEditing
|
||||
|
@ -233,6 +206,10 @@ class AnnotationEditorLayer {
|
|||
this.#uiManager.addCommands(params);
|
||||
}
|
||||
|
||||
cleanUndoStack(type) {
|
||||
this.#uiManager.cleanUndoStack(type);
|
||||
}
|
||||
|
||||
toggleDrawing(enabled = false) {
|
||||
this.div.classList.toggle("drawing", !enabled);
|
||||
}
|
||||
|
@ -482,10 +459,6 @@ class AnnotationEditorLayer {
|
|||
this.#uiManager.removeEditor(editor);
|
||||
editor.div.remove();
|
||||
editor.isAttachedToDOM = false;
|
||||
|
||||
if (!this.#isCleaningUp) {
|
||||
this.addInkEditorIfNeeded(/* isCommitting = */ false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -766,6 +739,13 @@ class AnnotationEditorLayer {
|
|||
}
|
||||
this.#hadPointerDown = false;
|
||||
|
||||
if (
|
||||
this.#currentEditorType?.isDrawer &&
|
||||
this.#currentEditorType.supportMultipleDrawings
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#allowClick) {
|
||||
this.#allowClick = true;
|
||||
return;
|
||||
|
@ -808,10 +788,48 @@ class AnnotationEditorLayer {
|
|||
|
||||
this.#hadPointerDown = true;
|
||||
|
||||
if (this.#currentEditorType?.isDrawer) {
|
||||
this.startDrawingSession(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = this.#uiManager.getActive();
|
||||
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
|
||||
|
@ -828,10 +846,26 @@ class AnnotationEditorLayer {
|
|||
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() {
|
||||
this.commitOrRemove();
|
||||
if (this.#uiManager.getActive()?.parent === this) {
|
||||
// We need to commit the current editor before destroying the layer.
|
||||
this.#uiManager.commitOrRemove();
|
||||
|
@ -858,13 +892,11 @@ class AnnotationEditorLayer {
|
|||
// 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
|
||||
// an infinite loop.
|
||||
this.#isCleaningUp = true;
|
||||
for (const editor of this.#editors.values()) {
|
||||
if (editor.isEmpty()) {
|
||||
editor.remove();
|
||||
}
|
||||
}
|
||||
this.#isCleaningUp = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -896,6 +928,7 @@ class AnnotationEditorLayer {
|
|||
|
||||
const oldRotation = this.viewport.rotation;
|
||||
const rotation = viewport.rotation;
|
||||
|
||||
this.viewport = viewport;
|
||||
setLayerDimensions(this.div, { rotation });
|
||||
if (oldRotation !== rotation) {
|
||||
|
@ -903,7 +936,6 @@ class AnnotationEditorLayer {
|
|||
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";
|
||||
|
||||
class Outline {
|
||||
static PRECISION = 1e-4;
|
||||
|
||||
/**
|
||||
* @returns {string} The SVG path of the outline.
|
||||
*/
|
||||
|
@ -52,6 +54,49 @@ class Outline {
|
|||
}
|
||||
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 };
|
||||
|
|
|
@ -192,6 +192,10 @@ class AnnotationEditor {
|
|||
return Object.getPrototypeOf(this).constructor._type;
|
||||
}
|
||||
|
||||
static get isDrawer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static get _defaultLineColor() {
|
||||
return shadow(
|
||||
this,
|
||||
|
@ -441,6 +445,8 @@ class AnnotationEditor {
|
|||
this.x += x / width;
|
||||
this.y += y / height;
|
||||
|
||||
this._onTranslating(this.x, this.y);
|
||||
|
||||
this.fixAndSetPosition();
|
||||
}
|
||||
|
||||
|
@ -469,7 +475,10 @@ class AnnotationEditor {
|
|||
|
||||
drag(tx, ty) {
|
||||
this.#initialPosition ||= [this.x, this.y];
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
const {
|
||||
div,
|
||||
parentDimensions: [parentWidth, parentHeight],
|
||||
} = this;
|
||||
this.x += tx / parentWidth;
|
||||
this.y += ty / parentHeight;
|
||||
if (this.parent && (this.x < 0 || this.x > 1 || this.y < 0 || this.y > 1)) {
|
||||
|
@ -496,11 +505,29 @@ class AnnotationEditor {
|
|||
x += bx;
|
||||
y += by;
|
||||
|
||||
this.div.style.left = `${(100 * x).toFixed(2)}%`;
|
||||
this.div.style.top = `${(100 * y).toFixed(2)}%`;
|
||||
this.div.scrollIntoView({ block: "nearest" });
|
||||
const { style } = div;
|
||||
style.left = `${(100 * x).toFixed(2)}%`;
|
||||
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() {
|
||||
return (
|
||||
!!this.#initialPosition &&
|
||||
|
@ -546,7 +573,10 @@ class AnnotationEditor {
|
|||
* @param {number} [rotation] - the rotation of the page.
|
||||
*/
|
||||
fixAndSetPosition(rotation = this.rotation) {
|
||||
const [pageWidth, pageHeight] = this.pageDimensions;
|
||||
const {
|
||||
div: { style },
|
||||
pageDimensions: [pageWidth, pageHeight],
|
||||
} = this;
|
||||
let { x, y, width, height } = this;
|
||||
width *= pageWidth;
|
||||
height *= pageHeight;
|
||||
|
@ -581,7 +611,6 @@ class AnnotationEditor {
|
|||
x += bx;
|
||||
y += by;
|
||||
|
||||
const { style } = this.div;
|
||||
style.left = `${(100 * x).toFixed(2)}%`;
|
||||
style.top = `${(100 * y).toFixed(2)}%`;
|
||||
|
||||
|
@ -659,9 +688,10 @@ class AnnotationEditor {
|
|||
*/
|
||||
setDims(width, height) {
|
||||
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) {
|
||||
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)}%`;
|
||||
}
|
||||
if (!this.#keepAspectRatio && !heightPercent) {
|
||||
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(
|
||||
2
|
||||
)}%`;
|
||||
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -759,10 +787,12 @@ class AnnotationEditor {
|
|||
{ passive: false, signal }
|
||||
);
|
||||
window.addEventListener("contextmenu", noContextMenu, { signal });
|
||||
const savedX = this.x;
|
||||
const savedY = this.y;
|
||||
const savedWidth = this.width;
|
||||
const savedHeight = this.height;
|
||||
this.#savedDimensions = {
|
||||
savedX: this.x,
|
||||
savedY: this.y,
|
||||
savedWidth: this.width,
|
||||
savedHeight: this.height,
|
||||
};
|
||||
const savedParentCursor = this.parent.div.style.cursor;
|
||||
const savedCursor = this.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.div.style.cursor = savedCursor;
|
||||
|
||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
||||
this.#addResizeToUndoStack();
|
||||
};
|
||||
window.addEventListener("pointerup", pointerUpCallback, { signal });
|
||||
// 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 });
|
||||
}
|
||||
|
||||
#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 newY = this.y;
|
||||
const newWidth = this.width;
|
||||
|
@ -799,24 +851,8 @@ class AnnotationEditor {
|
|||
}
|
||||
|
||||
this.addCommands({
|
||||
cmd: () => {
|
||||
this.width = newWidth;
|
||||
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();
|
||||
},
|
||||
cmd: this.#resize.bind(this, newX, newY, newWidth, newHeight),
|
||||
undo: this.#resize.bind(this, savedX, savedY, savedWidth, savedHeight),
|
||||
mustExec: true,
|
||||
});
|
||||
}
|
||||
|
@ -960,8 +996,15 @@ class AnnotationEditor {
|
|||
|
||||
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
|
||||
this.fixAndSetPosition();
|
||||
|
||||
this._onResizing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the editor is being resized.
|
||||
*/
|
||||
_onResizing() {}
|
||||
|
||||
/**
|
||||
* Called when the alt text dialog is closed.
|
||||
*/
|
||||
|
@ -1194,9 +1237,12 @@ class AnnotationEditor {
|
|||
);
|
||||
}
|
||||
|
||||
this._onStartDragging();
|
||||
|
||||
const pointerUpCallback = e => {
|
||||
if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) {
|
||||
cancelDrag(e);
|
||||
this._onStopDragging();
|
||||
return;
|
||||
}
|
||||
stopEvent(e);
|
||||
|
@ -1208,6 +1254,10 @@ class AnnotationEditor {
|
|||
window.addEventListener("blur", pointerUpCallback, { signal });
|
||||
}
|
||||
|
||||
_onStartDragging() {}
|
||||
|
||||
_onStopDragging() {}
|
||||
|
||||
moveInDOM() {
|
||||
// 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
|
||||
|
@ -1226,6 +1276,7 @@ class AnnotationEditor {
|
|||
this.x = x;
|
||||
this.y = y;
|
||||
this.fixAndSetPosition();
|
||||
this._onTranslated();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1372,11 +1423,16 @@ class AnnotationEditor {
|
|||
}
|
||||
|
||||
/**
|
||||
* Rotate the editor.
|
||||
* Rotate the editor when the page is rotated.
|
||||
* @param {number} angle
|
||||
*/
|
||||
rotate(_angle) {}
|
||||
|
||||
/**
|
||||
* Resize the editor when the page is resized.
|
||||
*/
|
||||
resize() {}
|
||||
|
||||
/**
|
||||
* Serialize the editor when it has been deleted.
|
||||
* @returns {Object}
|
||||
|
@ -1622,11 +1678,7 @@ class AnnotationEditor {
|
|||
#stopResizing() {
|
||||
this.#isResizerEnabledForKeyboard = false;
|
||||
this.#setResizerTabIndex(-1);
|
||||
if (this.#savedDimensions) {
|
||||
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
|
||||
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
|
||||
this.#savedDimensions = null;
|
||||
}
|
||||
this.#addResizeToUndoStack();
|
||||
}
|
||||
|
||||
_stopResizingWithKeyboard() {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -411,6 +411,21 @@ class CommandManager {
|
|||
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() {
|
||||
this.#commands = null;
|
||||
}
|
||||
|
@ -1034,6 +1049,7 @@ class AnnotationEditorUIManager {
|
|||
for (const editor of this.#editorsToRescale) {
|
||||
editor.onScaleChanging();
|
||||
}
|
||||
this.currentLayer?.onScaleChanging();
|
||||
}
|
||||
|
||||
onRotationChanging({ pagesRotation }) {
|
||||
|
@ -1931,6 +1947,10 @@ class AnnotationEditorUIManager {
|
|||
}
|
||||
}
|
||||
|
||||
updateUIForDefaultProperties(editorType) {
|
||||
this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove an editor the current selection.
|
||||
* @param {AnnotationEditor} editor
|
||||
|
@ -1957,6 +1977,7 @@ class AnnotationEditorUIManager {
|
|||
* @param {AnnotationEditor} editor
|
||||
*/
|
||||
setSelected(editor) {
|
||||
this.currentLayer?.commitOrRemove();
|
||||
for (const ed of this.#selectedEditors) {
|
||||
if (ed !== editor) {
|
||||
ed.unselect();
|
||||
|
@ -2044,6 +2065,10 @@ class AnnotationEditorUIManager {
|
|||
});
|
||||
}
|
||||
|
||||
cleanUndoStack(type) {
|
||||
this.#commandManager.cleanType(type);
|
||||
}
|
||||
|
||||
#isEmpty() {
|
||||
if (this.#allEditors.size === 0) {
|
||||
return true;
|
||||
|
@ -2134,6 +2159,10 @@ class AnnotationEditorUIManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.currentLayer?.commitOrRemove()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasSelection) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ const AnnotationEditorParamsType = {
|
|||
HIGHLIGHT_THICKNESS: 33,
|
||||
HIGHLIGHT_FREE: 34,
|
||||
HIGHLIGHT_SHOW_ALL: 35,
|
||||
DRAW_STEP: 41,
|
||||
};
|
||||
|
||||
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
|
||||
|
|
|
@ -649,7 +649,7 @@ class Driver {
|
|||
|
||||
if (task.annotationStorage) {
|
||||
for (const annotation of Object.values(task.annotationStorage)) {
|
||||
const { bitmapName, quadPoints } = annotation;
|
||||
const { bitmapName, quadPoints, paths, outlines } = annotation;
|
||||
if (bitmapName) {
|
||||
promise = promise.then(async doc => {
|
||||
const response = await fetch(
|
||||
|
@ -687,6 +687,36 @@ class Driver {
|
|||
// like IRL (in order to avoid bugs like bug 1907958).
|
||||
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);
|
||||
|
||||
const rectBefore = await getRect(page, ".inkEditor canvas");
|
||||
const rectBefore = await getRect(page, ".canvasWrapper .draw");
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await kbUndo(page);
|
||||
|
@ -126,7 +126,7 @@ describe("Ink Editor", () => {
|
|||
await waitForStorageEntries(page, 1);
|
||||
}
|
||||
|
||||
const rectAfter = await getRect(page, ".inkEditor canvas");
|
||||
const rectAfter = await getRect(page, ".canvasWrapper .draw");
|
||||
|
||||
expect(Math.round(rectBefore.x))
|
||||
.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],
|
||||
"thickness": 3,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [
|
||||
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,
|
||||
"paths": {
|
||||
"lines": [
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
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
|
||||
],
|
||||
"points": [
|
||||
]
|
||||
],
|
||||
"points": [
|
||||
[
|
||||
73, 560.2277710847244, 76.7257911988625, 558.1025687477292,
|
||||
75.5128345111164, 559.4147224528562, 77.5, 557.2277710847244,
|
||||
92.5, 547.7277710847244, 109.21378602219673, 539.2873735223628,
|
||||
103.32868842191223, 542.3364518890394, 113.5, 536.2277710847244
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [71.5, 534.5, 115, 562],
|
||||
"rotation": 0
|
||||
|
@ -8330,22 +8350,37 @@
|
|||
"color": [255, 0, 0],
|
||||
"thickness": 1,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [
|
||||
417.61538461538464, 520.3461538461538, 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": [
|
||||
"paths": {
|
||||
"lines": [
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
417.61538461538464,
|
||||
520.3461538461538,
|
||||
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,
|
||||
520.3461538461538, 425.6923076923077, 520.3461538461538,
|
||||
433.7692307692308, 519.1923076923076
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
417.11538461538464, 510.46153846153845, 434.42307692307696,
|
||||
|
@ -8358,22 +8393,37 @@
|
|||
"color": [0, 255, 0],
|
||||
"thickness": 1,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [
|
||||
449.92307692307696, 526.6538461538462, 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": [
|
||||
"paths": {
|
||||
"lines": [
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
449.92307692307696,
|
||||
526.6538461538462,
|
||||
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,
|
||||
527.423076923077, 448.4807692307693, 533.0961538461539,
|
||||
447.0384615384616, 541.6538461538462
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
446.5384615384616, 526.1538461538462, 456.92307692307696,
|
||||
|
@ -8386,22 +8436,37 @@
|
|||
"color": [0, 0, 255],
|
||||
"thickness": 1,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [
|
||||
482.8461538461538, 511.6538461538462, 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": [
|
||||
"paths": {
|
||||
"lines": [
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
482.8461538461538,
|
||||
511.6538461538462,
|
||||
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,
|
||||
511.6538461538462, 475.9230769230769, 511.6538461538462,
|
||||
467.8461538461538, 511.6538461538462
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
467.1923076923077, 511.1538461538462, 483.3461538461538,
|
||||
|
@ -8414,22 +8479,37 @@
|
|||
"color": [0, 255, 255],
|
||||
"thickness": 1,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [
|
||||
445.9230769230769, 509.3846153846154, 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": [
|
||||
"paths": {
|
||||
"lines": [
|
||||
[
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
445.9230769230769,
|
||||
509.3846153846154,
|
||||
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,
|
||||
509.3846153846154, 444.38461538461536, 503.1346153846154,
|
||||
441.30769230769226, 493.2307692307693
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
436.03846153846155, 492.5769230769231, 446.4230769230769,
|
||||
|
@ -9599,12 +9679,12 @@
|
|||
"color": [53, 228, 47],
|
||||
"thickness": 20,
|
||||
"opacity": 1,
|
||||
"paths": [
|
||||
{
|
||||
"bezier": [279.9183673469388, 477.0105263157895],
|
||||
"points": [279.9183673469388, 477.0105263157895]
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"lines": [
|
||||
[null, null, null, null, 279.9183673469388, 477.0105263157895]
|
||||
],
|
||||
"points": [[279.9183673469388, 477.0105263157895]]
|
||||
},
|
||||
"pageIndex": 0,
|
||||
"rect": [
|
||||
269.9183673469388, 443.93684210526317, 312.9387755102041,
|
||||
|
|
|
@ -4448,21 +4448,54 @@ describe("annotation", function () {
|
|||
thickness: 1,
|
||||
opacity: 1,
|
||||
color: [0, 0, 0],
|
||||
paths: [
|
||||
{
|
||||
bezier: [
|
||||
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
|
||||
paths: {
|
||||
lines: [
|
||||
[
|
||||
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],
|
||||
},
|
||||
{
|
||||
bezier: [
|
||||
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
|
||||
926, 927,
|
||||
[
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
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,
|
||||
|
@ -4482,13 +4515,12 @@ describe("annotation", function () {
|
|||
const appearance = data[1].data;
|
||||
expect(appearance).toEqual(
|
||||
"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" +
|
||||
"0 G\n" +
|
||||
"10 11 m\n" +
|
||||
"12 13 14 15 16 17 c\n" +
|
||||
"22 23 24 25 26 27 c\n" +
|
||||
"S\n" +
|
||||
"910 911 m\n" +
|
||||
"912 913 914 915 916 917 c\n" +
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
|
@ -4513,21 +4545,54 @@ describe("annotation", function () {
|
|||
thickness: 1,
|
||||
opacity: 0.12,
|
||||
color: [0, 0, 0],
|
||||
paths: [
|
||||
{
|
||||
bezier: [
|
||||
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
|
||||
paths: {
|
||||
lines: [
|
||||
[
|
||||
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],
|
||||
},
|
||||
{
|
||||
bezier: [
|
||||
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
|
||||
926, 927,
|
||||
[
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
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,
|
||||
|
@ -4547,7 +4612,7 @@ describe("annotation", function () {
|
|||
const appearance = data[1].data;
|
||||
expect(appearance).toEqual(
|
||||
"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" +
|
||||
"1 w 1 J 1 j\n" +
|
||||
"0 G\n" +
|
||||
|
@ -4555,7 +4620,6 @@ describe("annotation", function () {
|
|||
"10 11 m\n" +
|
||||
"12 13 14 15 16 17 c\n" +
|
||||
"22 23 24 25 26 27 c\n" +
|
||||
"S\n" +
|
||||
"910 911 m\n" +
|
||||
"912 913 914 915 916 917 c\n" +
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
|
@ -4581,13 +4645,10 @@ describe("annotation", function () {
|
|||
thickness: 3,
|
||||
opacity: 1,
|
||||
color: [0, 255, 0],
|
||||
paths: [
|
||||
{
|
||||
bezier: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
// Useless in the printing case.
|
||||
points: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
},
|
||||
],
|
||||
paths: {
|
||||
lines: [[NaN, NaN, NaN, NaN, 1, 2, 3, 4, 5, 6, 7, 8]],
|
||||
points: [[1, 2, 3, 4, 5, 6, 7, 8]],
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
|
|
@ -66,20 +66,22 @@
|
|||
font-size: 0;
|
||||
}
|
||||
|
||||
.textLayer.highlighting {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
.textLayer {
|
||||
&.highlighting {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
|
||||
&:not(.free) span {
|
||||
cursor: var(--editorHighlight-editing-cursor);
|
||||
&:not(.free) span {
|
||||
cursor: var(--editorHighlight-editing-cursor);
|
||||
|
||||
&[role="img"] {
|
||||
&[role="img"] {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
}
|
||||
}
|
||||
|
||||
&.free span {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
}
|
||||
}
|
||||
|
||||
&.free span {
|
||||
cursor: var(--editorFreeHighlight-editing-cursor);
|
||||
}
|
||||
}
|
||||
|
||||
#viewerContainer.pdfPresentationMode:fullscreen,
|
||||
|
@ -154,6 +156,11 @@
|
|||
|
||||
.annotationEditorLayer.inkEditing {
|
||||
cursor: var(--editorInk-editing-cursor);
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.annotationEditorLayer .draw {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) {
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
svg {
|
||||
transform: none;
|
||||
|
||||
&.moving {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
&.highlight,
|
||||
&.highlightOutline {
|
||||
&[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 {
|
||||
--blend-mode: multiply;
|
||||
|
||||
|
|
|
@ -300,7 +300,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||
</div>
|
||||
<div class="editorParamsSetter">
|
||||
<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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue