mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-23 08:38:06 +02:00
[Editor] Add a new base class to allow to add a drawing in the SVG layer.
This patch makes a clear separation between the way to draw and the editing stuff. It adds a class DrawEditor which should be extended in order to create new drawing tools. As an example, the ink tool has been rewritten in order to use it.
This commit is contained in:
parent
22babd722f
commit
cee65fcd4e
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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue