1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

Merge pull request #16793 from calixteman/editor_resize_rotated

[Editor] Fix the resizing of an editor when it's rotated (bug 1847268)
This commit is contained in:
calixteman 2023-08-08 18:24:20 +02:00 committed by GitHub
commit e914870c14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 448 additions and 313 deletions

View file

@ -18,13 +18,8 @@
// eslint-disable-next-line max-len
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
import {
AnnotationEditorParamsType,
FeatureTest,
shadow,
unreachable,
} from "../../shared/util.js";
import { bindEvents, ColorManager } from "./tools.js";
import { FeatureTest, shadow, unreachable } from "../../shared/util.js";
/**
* @typedef {Object} AnnotationEditorParameters
@ -43,8 +38,6 @@ class AnnotationEditor {
#resizersDiv = null;
#resizePosition = null;
#boundFocusin = this.focusin.bind(this);
#boundFocusout = this.focusout.bind(this);
@ -328,13 +321,8 @@ class AnnotationEditor {
this.div.style.top = `${(100 * this.y).toFixed(2)}%`;
}
/**
* Convert a screen translation into a page one.
* @param {number} x
* @param {number} y
*/
screenToPageTranslation(x, y) {
switch (this.parentRotation) {
static #rotatePoint(x, y, angle) {
switch (angle) {
case 90:
return [y, -x];
case 180:
@ -346,21 +334,38 @@ class AnnotationEditor {
}
}
/**
* Convert a screen translation into a page one.
* @param {number} x
* @param {number} y
*/
screenToPageTranslation(x, y) {
return AnnotationEditor.#rotatePoint(x, y, this.parentRotation);
}
/**
* Convert a page translation into a screen one.
* @param {number} x
* @param {number} y
*/
pageTranslationToScreen(x, y) {
switch (this.parentRotation) {
case 90:
return [-y, x];
return AnnotationEditor.#rotatePoint(x, y, 360 - this.parentRotation);
}
#getRotationMatrix(rotation) {
switch (rotation) {
case 90: {
const [pageWidth, pageHeight] = this.pageDimensions;
return [0, -pageWidth / pageHeight, pageHeight / pageWidth, 0];
}
case 180:
return [-x, -y];
case 270:
return [y, -x];
return [-1, 0, 0, -1];
case 270: {
const [pageWidth, pageHeight] = this.pageDimensions;
return [0, pageWidth / pageHeight, -pageHeight / pageWidth, 0];
}
default:
return [x, y];
return [1, 0, 0, 1];
}
}
@ -449,26 +454,26 @@ class AnnotationEditor {
#resizerPointerdown(name, event) {
event.preventDefault();
this.#resizePosition = [event.clientX, event.clientY];
const boundResizerPointermove = this.#resizerPointermove.bind(this, name);
const savedDraggable = this._isDraggable;
this._isDraggable = false;
const resizingClassName = `resizing${name
.charAt(0)
.toUpperCase()}${name.slice(1)}`;
this.parent.div.classList.add(resizingClassName);
const pointerMoveOptions = { passive: true, capture: true };
window.addEventListener(
"pointermove",
boundResizerPointermove,
pointerMoveOptions
);
const savedX = this.x;
const savedY = this.y;
const savedWidth = this.width;
const 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 =
window.getComputedStyle(event.target).cursor;
const pointerUpCallback = () => {
// Stop the undo accumulation in order to have an undo action for each
// resize session.
this._uiManager.stopUndoAccumulation();
this._isDraggable = savedDraggable;
this.parent.div.classList.remove(resizingClassName);
window.removeEventListener("pointerup", pointerUpCallback);
window.removeEventListener("blur", pointerUpCallback);
window.removeEventListener(
@ -476,19 +481,53 @@ class AnnotationEditor {
boundResizerPointermove,
pointerMoveOptions
);
this.parent.div.style.cursor = savedParentCursor;
this.div.style.cursor = savedCursor;
const newX = this.x;
const newY = this.y;
const newWidth = this.width;
const newHeight = this.height;
if (
newX === savedX &&
newY === savedY &&
newWidth === savedWidth &&
newHeight === savedHeight
) {
return;
}
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();
this.parent.moveEditorInDOM(this);
},
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();
this.parent.moveEditorInDOM(this);
},
mustExec: true,
});
};
window.addEventListener("pointerup", pointerUpCallback);
// If the user switch to another window (with alt+tab), then we end the
// If the user switches to another window (with alt+tab), then we end the
// resize session.
window.addEventListener("blur", pointerUpCallback);
}
#resizerPointermove(name, event) {
const { clientX, clientY } = event;
const deltaX = clientX - this.#resizePosition[0];
const deltaY = clientY - this.#resizePosition[1];
this.#resizePosition[0] = clientX;
this.#resizePosition[1] = clientY;
const [parentWidth, parentHeight] = this.parentDimensions;
const savedX = this.x;
const savedY = this.y;
@ -496,204 +535,124 @@ class AnnotationEditor {
const savedHeight = this.height;
const minWidth = AnnotationEditor.MIN_SIZE / parentWidth;
const minHeight = AnnotationEditor.MIN_SIZE / parentHeight;
let cmd;
// 10000 because we multiply by 100 and use toFixed(2) in fixAndSetPosition.
// Without rounding, the positions of the corners other than the top left
// one can be slightly wrong.
const round = x => Math.round(x * 10000) / 10000;
const updatePosition = (width, height) => {
// We must take the parent dimensions as they are when undo/redo.
const [pWidth, pHeight] = this.parentDimensions;
this.setDims(pWidth * width, pHeight * height);
this.fixAndSetPosition();
};
const undo = () => {
this.width = savedWidth;
this.height = savedHeight;
this.x = savedX;
this.y = savedY;
updatePosition(savedWidth, savedHeight);
};
const rotationMatrix = this.#getRotationMatrix(this.rotation);
const transf = (x, y) => [
rotationMatrix[0] * x + rotationMatrix[2] * y,
rotationMatrix[1] * x + rotationMatrix[3] * y,
];
const invRotationMatrix = this.#getRotationMatrix(360 - this.rotation);
const invTransf = (x, y) => [
invRotationMatrix[0] * x + invRotationMatrix[2] * y,
invRotationMatrix[1] * x + invRotationMatrix[3] * y,
];
let getPoint;
let getOpposite;
let isDiagonal = false;
let isHorizontal = false;
switch (name) {
case "topLeft": {
if (Math.sign(deltaX) * Math.sign(deltaY) < 0) {
return;
}
const dist = Math.hypot(deltaX, deltaY);
const oldDiag = Math.hypot(
savedWidth * parentWidth,
savedHeight * parentHeight
);
const brX = round(savedX + savedWidth);
const brY = round(savedY + savedHeight);
const ratio = Math.max(
Math.min(
1 - Math.sign(deltaX) * (dist / oldDiag),
// Avoid the editor to be larger than the page.
1 / savedWidth,
1 / savedHeight
),
// Avoid the editor to be smaller than the minimum size.
minWidth / savedWidth,
minHeight / savedHeight
);
const newWidth = round(savedWidth * ratio);
const newHeight = round(savedHeight * ratio);
const newX = brX - newWidth;
const newY = brY - newHeight;
cmd = () => {
this.width = newWidth;
this.height = newHeight;
this.x = newX;
this.y = newY;
updatePosition(newWidth, newHeight);
};
case "topLeft":
isDiagonal = true;
getPoint = (w, h) => [0, 0];
getOpposite = (w, h) => [w, h];
break;
}
case "topMiddle": {
const bmY = round(this.y + savedHeight);
const newHeight = round(
Math.max(minHeight, Math.min(1, savedHeight - deltaY / parentHeight))
);
const newY = bmY - newHeight;
cmd = () => {
this.height = newHeight;
this.y = newY;
updatePosition(savedWidth, newHeight);
};
case "topMiddle":
getPoint = (w, h) => [w / 2, 0];
getOpposite = (w, h) => [w / 2, h];
break;
}
case "topRight": {
if (Math.sign(deltaX) * Math.sign(deltaY) > 0) {
return;
}
const dist = Math.hypot(deltaX, deltaY);
const oldDiag = Math.hypot(
this.width * parentWidth,
this.height * parentHeight
);
const blY = round(savedY + this.height);
const ratio = Math.max(
Math.min(
1 + Math.sign(deltaX) * (dist / oldDiag),
1 / savedWidth,
1 / savedHeight
),
minWidth / savedWidth,
minHeight / savedHeight
);
const newWidth = round(savedWidth * ratio);
const newHeight = round(savedHeight * ratio);
const newY = blY - newHeight;
cmd = () => {
this.width = newWidth;
this.height = newHeight;
this.y = newY;
updatePosition(newWidth, newHeight);
};
case "topRight":
isDiagonal = true;
getPoint = (w, h) => [w, 0];
getOpposite = (w, h) => [0, h];
break;
}
case "middleRight": {
const newWidth = round(
Math.max(minWidth, Math.min(1, savedWidth + deltaX / parentWidth))
);
cmd = () => {
this.width = newWidth;
updatePosition(newWidth, savedHeight);
};
case "middleRight":
isHorizontal = true;
getPoint = (w, h) => [w, h / 2];
getOpposite = (w, h) => [0, h / 2];
break;
}
case "bottomRight": {
if (Math.sign(deltaX) * Math.sign(deltaY) < 0) {
return;
}
const dist = Math.hypot(deltaX, deltaY);
const oldDiag = Math.hypot(
this.width * parentWidth,
this.height * parentHeight
);
const ratio = Math.max(
Math.min(
1 + Math.sign(deltaX) * (dist / oldDiag),
1 / savedWidth,
1 / savedHeight
),
minWidth / savedWidth,
minHeight / savedHeight
);
const newWidth = round(savedWidth * ratio);
const newHeight = round(savedHeight * ratio);
cmd = () => {
this.width = newWidth;
this.height = newHeight;
updatePosition(newWidth, newHeight);
};
case "bottomRight":
isDiagonal = true;
getPoint = (w, h) => [w, h];
getOpposite = (w, h) => [0, 0];
break;
}
case "bottomMiddle": {
const newHeight = round(
Math.max(minHeight, Math.min(1, savedHeight + deltaY / parentHeight))
);
cmd = () => {
this.height = newHeight;
updatePosition(savedWidth, newHeight);
};
case "bottomMiddle":
getPoint = (w, h) => [w / 2, h];
getOpposite = (w, h) => [w / 2, 0];
break;
}
case "bottomLeft": {
if (Math.sign(deltaX) * Math.sign(deltaY) > 0) {
return;
}
const dist = Math.hypot(deltaX, deltaY);
const oldDiag = Math.hypot(
this.width * parentWidth,
this.height * parentHeight
);
const trX = round(savedX + this.width);
const ratio = Math.max(
Math.min(
1 - Math.sign(deltaX) * (dist / oldDiag),
1 / savedWidth,
1 / savedHeight
),
minWidth / savedWidth,
minHeight / savedHeight
);
const newWidth = round(savedWidth * ratio);
const newHeight = round(savedHeight * ratio);
const newX = trX - newWidth;
cmd = () => {
this.width = newWidth;
this.height = newHeight;
this.x = newX;
updatePosition(newWidth, newHeight);
};
case "bottomLeft":
isDiagonal = true;
getPoint = (w, h) => [0, h];
getOpposite = (w, h) => [w, 0];
break;
}
case "middleLeft": {
const mrX = round(savedX + savedWidth);
const newWidth = round(
Math.max(minWidth, Math.min(1, savedWidth - deltaX / parentWidth))
);
const newX = mrX - newWidth;
cmd = () => {
this.width = newWidth;
this.x = newX;
updatePosition(newWidth, savedHeight);
};
case "middleLeft":
isHorizontal = true;
getPoint = (w, h) => [0, h / 2];
getOpposite = (w, h) => [w, h / 2];
break;
}
}
this.addCommands({
cmd,
undo,
mustExec: true,
type: AnnotationEditorParamsType.RESIZE,
overwriteIfSameType: true,
keepUndo: true,
});
const point = getPoint(savedWidth, savedHeight);
const oppositePoint = getOpposite(savedWidth, savedHeight);
let transfOppositePoint = transf(...oppositePoint);
const oppositeX = round(savedX + transfOppositePoint[0]);
const oppositeY = round(savedY + transfOppositePoint[1]);
let ratioX = 1;
let ratioY = 1;
let [deltaX, deltaY] = this.screenToPageTranslation(
event.movementX,
event.movementY
);
[deltaX, deltaY] = invTransf(deltaX / parentWidth, deltaY / parentHeight);
if (isDiagonal) {
const oldDiag = Math.hypot(savedWidth, savedHeight);
ratioX = ratioY = Math.max(
Math.min(
Math.hypot(
oppositePoint[0] - point[0] - deltaX,
oppositePoint[1] - point[1] - deltaY
) / oldDiag,
// Avoid the editor to be larger than the page.
1 / savedWidth,
1 / savedHeight
),
// Avoid the editor to be smaller than the minimum size.
minWidth / savedWidth,
minHeight / savedHeight
);
} else if (isHorizontal) {
ratioX =
Math.max(
minWidth,
Math.min(1, Math.abs(oppositePoint[0] - point[0] - deltaX))
) / savedWidth;
} else {
ratioY =
Math.max(
minHeight,
Math.min(1, Math.abs(oppositePoint[1] - point[1] - deltaY))
) / savedHeight;
}
const newWidth = round(savedWidth * ratioX);
const newHeight = round(savedHeight * ratioY);
transfOppositePoint = transf(...getOpposite(newWidth, newHeight));
const newX = oppositeX - transfOppositePoint[0];
const newY = oppositeY - transfOppositePoint[1];
this.width = newWidth;
this.height = newHeight;
this.x = newX;
this.y = newY;
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
this.fixAndSetPosition();
}
/**

View file

@ -305,12 +305,6 @@ class CommandManager {
this.#commands.push(save);
}
stopUndoAccumulation() {
if (this.#position !== -1) {
this.#commands[this.#position].type = NaN;
}
}
/**
* Undo the last command.
*/
@ -1294,10 +1288,6 @@ class AnnotationEditorUIManager {
return this.#selectedEditors.size !== 0;
}
stopUndoAccumulation() {
this.#commandManager.stopUndoAccumulation();
}
/**
* Undo the last command.
*/