1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-23 08:38:06 +02:00

Merge pull request #19085 from calixteman/simplify_drawer

[Editor] Simplify the draw layer code
This commit is contained in:
calixteman 2024-11-22 09:16:48 +01:00 committed by GitHub
commit 5133e6b666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 322 additions and 234 deletions

View file

@ -55,7 +55,7 @@ class DrawLayer {
return shadow(this, "_svgFactory", new DOMSVGFactory());
}
static #setBox(element, { x = 0, y = 0, width = 1, height = 1 } = {}) {
static #setBox(element, [x, y, width, height]) {
const { style } = element;
style.top = `${100 * y}%`;
style.left = `${100 * x}%`;
@ -63,11 +63,10 @@ class DrawLayer {
style.height = `${100 * height}%`;
}
#createSVG(box) {
#createSVG() {
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
this.#parent.append(svg);
svg.setAttribute("aria-hidden", true);
DrawLayer.#setBox(svg, box);
return svg;
}
@ -86,10 +85,19 @@ class DrawLayer {
return clipPathId;
}
draw(outlines, color, opacity, isPathUpdatable = false) {
#updateProperties(element, properties) {
for (const [key, value] of Object.entries(properties)) {
if (value === null) {
element.removeAttribute(key);
} else {
element.setAttribute(key, value);
}
}
}
draw(properties, isPathUpdatable = false, hasClip = false) {
const id = this.#id++;
const root = this.#createSVG(outlines.box);
root.classList.add(...outlines.classNamesForDrawing);
const root = this.#createSVG();
const defs = DrawLayer._svgFactory.createElement("defs");
root.append(defs);
@ -97,45 +105,42 @@ class DrawLayer {
defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId);
path.setAttribute("d", outlines.toSVGPath());
path.setAttribute("vector-effect", "non-scaling-stroke");
if (isPathUpdatable) {
this.#toUpdate.set(id, path);
}
// Create the clipping path for the editor div.
const clipPathId = this.#createClipPath(defs, pathId);
const clipPathId = hasClip ? this.#createClipPath(defs, pathId) : null;
const use = DrawLayer._svgFactory.createElement("use");
root.append(use);
root.setAttribute("fill", color);
root.setAttribute("fill-opacity", opacity);
use.setAttribute("href", `#${pathId}`);
this.updateProperties(root, properties);
this.#mapping.set(id, root);
return { id, clipPathId: `url(#${clipPathId})` };
}
drawOutline(outlines) {
drawOutline(properties, mustRemoveSelfIntersections) {
// We cannot draw the outline directly in the SVG for highlights because
// it composes with its parent with mix-blend-mode: multiply.
// But the outline has a different mix-blend-mode, so we need to draw it in
// its own SVG.
const id = this.#id++;
const root = this.#createSVG(outlines.box);
root.classList.add(...outlines.classNamesForOutlining);
const root = this.#createSVG();
const defs = DrawLayer._svgFactory.createElement("defs");
root.append(defs);
const path = DrawLayer._svgFactory.createElement("path");
defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId);
path.setAttribute("d", outlines.toSVGPath());
path.setAttribute("vector-effect", "non-scaling-stroke");
let maskId;
if (outlines.mustRemoveSelfIntersections) {
if (mustRemoveSelfIntersections) {
const mask = DrawLayer._svgFactory.createElement("mask");
defs.append(mask);
maskId = `mask_p${this.pageIndex}_${id}`;
@ -166,59 +171,40 @@ class DrawLayer {
use1.classList.add("mainOutline");
use2.classList.add("secondaryOutline");
this.updateProperties(root, properties);
this.#mapping.set(id, root);
return id;
}
finalizeLine(id, line) {
const path = this.#toUpdate.get(id);
finalizeDraw(id, properties) {
this.#toUpdate.delete(id);
this.updateBox(id, line.box);
path.setAttribute("d", line.toSVGPath());
this.updateProperties(id, properties);
}
updateLine(id, line) {
const root = this.#mapping.get(id);
const defs = root.firstChild;
const path = defs.firstChild;
path.setAttribute("d", line.toSVGPath());
}
updatePath(id, line) {
this.#toUpdate.get(id).setAttribute("d", line.toSVGPath());
}
updateBox(id, box) {
DrawLayer.#setBox(this.#mapping.get(id), box);
}
show(id, visible) {
this.#mapping.get(id).classList.toggle("hidden", !visible);
}
rotate(id, angle) {
this.#mapping.get(id).setAttribute("data-main-rotation", angle);
}
changeColor(id, color) {
this.#mapping.get(id).setAttribute("fill", color);
}
changeOpacity(id, opacity) {
this.#mapping.get(id).setAttribute("fill-opacity", opacity);
}
addClass(id, className) {
this.#mapping.get(id).classList.add(className);
}
removeClass(id, className) {
this.#mapping.get(id).classList.remove(className);
}
getSVGRoot(id) {
return this.#mapping.get(id);
updateProperties(elementOrId, { root, bbox, rootClass, path }) {
const element =
typeof elementOrId === "number"
? this.#mapping.get(elementOrId)
: elementOrId;
if (root) {
this.#updateProperties(element, root);
}
if (bbox) {
DrawLayer.#setBox(element, bbox);
}
if (rootClass) {
const { classList } = element;
for (const [className, value] of Object.entries(rootClass)) {
classList.toggle(className, value);
}
}
if (path) {
const defs = element.firstChild;
const pathElement = defs.firstChild;
this.#updateProperties(pathElement, path);
}
}
remove(id) {

View file

@ -34,7 +34,7 @@ class FreeDrawOutliner {
// We track the last 3 points in order to be able to:
// - compute the normal of the line,
// - compute the control points of the quadratic Bézier curve.
#last = new Float64Array(18);
#last = new Float32Array(18);
#lastX;
@ -302,7 +302,7 @@ class FreeDrawOutliner {
const last = this.#last;
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
const points = new Float64Array((this.#points?.length ?? 0) + 2);
const points = new Float32Array((this.#points?.length ?? 0) + 2);
for (let i = 0, ii = points.length - 2; i < ii; i += 2) {
points[i] = (this.#points[i] - layerX) / layerWidth;
points[i + 1] = (this.#points[i + 1] - layerY) / layerHeight;
@ -315,7 +315,7 @@ class FreeDrawOutliner {
return this.#getOutlineTwoPoints(points);
}
const outline = new Float64Array(
const outline = new Float32Array(
this.#top.length + 24 + this.#bottom.length
);
let N = top.length;
@ -360,7 +360,7 @@ class FreeDrawOutliner {
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
this.#getLastCoords();
const outline = new Float64Array(36);
const outline = new Float32Array(36);
outline.set(
[
NaN,
@ -460,7 +460,7 @@ class FreeDrawOutliner {
class FreeDrawOutline extends Outline {
#box;
#bbox = null;
#bbox = new Float32Array(4);
#innerMargin;
@ -480,9 +480,10 @@ class FreeDrawOutline extends Outline {
this.#scaleFactor = scaleFactor;
this.#innerMargin = innerMargin;
this.#isLTR = isLTR;
this.lastPoint = [NaN, NaN];
this.#computeMinMax(isLTR);
const { x, y, width, height } = this.#bbox;
const [x, y, width, height] = this.#bbox;
for (let i = 0, ii = outline.length; i < ii; i += 2) {
outline[i] = (outline[i] - x) / width;
outline[i + 1] = (outline[i + 1] - y) / height;
@ -517,49 +518,43 @@ class FreeDrawOutline extends Outline {
let points;
switch (rotation) {
case 0:
outline = this.#rescale(this.#outline, blX, trY, width, -height);
points = this.#rescale(this.#points, blX, trY, width, -height);
outline = Outline._rescale(this.#outline, blX, trY, width, -height);
points = Outline._rescale(this.#points, blX, trY, width, -height);
break;
case 90:
outline = this.#rescaleAndSwap(this.#outline, blX, blY, width, height);
points = this.#rescaleAndSwap(this.#points, blX, blY, width, height);
outline = Outline._rescaleAndSwap(
this.#outline,
blX,
blY,
width,
height
);
points = Outline._rescaleAndSwap(this.#points, blX, blY, width, height);
break;
case 180:
outline = this.#rescale(this.#outline, trX, blY, -width, height);
points = this.#rescale(this.#points, trX, blY, -width, height);
outline = Outline._rescale(this.#outline, trX, blY, -width, height);
points = Outline._rescale(this.#points, trX, blY, -width, height);
break;
case 270:
outline = this.#rescaleAndSwap(
outline = Outline._rescaleAndSwap(
this.#outline,
trX,
trY,
-width,
-height
);
points = this.#rescaleAndSwap(this.#points, trX, trY, -width, -height);
points = Outline._rescaleAndSwap(
this.#points,
trX,
trY,
-width,
-height
);
break;
}
return { outline: Array.from(outline), points: [Array.from(points)] };
}
#rescale(src, tx, ty, sx, sy) {
const dest = new Float64Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i] * sx;
dest[i + 1] = ty + src[i + 1] * sy;
}
return dest;
}
#rescaleAndSwap(src, tx, ty, sx, sy) {
const dest = new Float64Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i + 1] * sx;
dest[i + 1] = ty + src[i] * sy;
}
return dest;
}
#computeMinMax(isLTR) {
const outline = this.#outline;
let lastX = outline[4];
@ -605,11 +600,12 @@ class FreeDrawOutline extends Outline {
lastY = outline[i + 5];
}
const x = minX - this.#innerMargin,
y = minY - this.#innerMargin,
width = maxX - minX + 2 * this.#innerMargin,
height = maxY - minY + 2 * this.#innerMargin;
this.#bbox = { x, y, width, height, lastPoint: [lastPointX, lastPointY] };
const bbox = this.#bbox;
bbox[0] = minX - this.#innerMargin;
bbox[1] = minY - this.#innerMargin;
bbox[2] = maxX - minX + 2 * this.#innerMargin;
bbox[3] = maxY - minY + 2 * this.#innerMargin;
this.lastPoint = [lastPointX, lastPointY];
}
get box() {
@ -629,7 +625,7 @@ class FreeDrawOutline extends Outline {
getNewOutline(thickness, innerMargin) {
// Build the outline of the highlight to use as the focus outline.
const { x, y, width, height } = this.#bbox;
const [x, y, width, height] = this.#bbox;
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
const sx = width * layerWidth;
const sy = height * layerHeight;
@ -654,10 +650,6 @@ class FreeDrawOutline extends Outline {
}
return outliner.getOutlines();
}
get mustRemoveSelfIntersections() {
return true;
}
}
export { FreeDrawOutline, FreeDrawOutliner };

View file

@ -19,6 +19,8 @@ import { Outline } from "./outline.js";
class HighlightOutliner {
#box;
#lastPoint;
#verticalEdges = [];
#intervals = [];
@ -77,13 +79,13 @@ class HighlightOutliner {
edge[2] = (y2 - shiftedMinY) / bboxHeight;
}
this.#box = {
x: shiftedMinX,
y: shiftedMinY,
width: bboxWidth,
height: bboxHeight,
lastPoint,
};
this.#box = new Float32Array([
shiftedMinX,
shiftedMinY,
bboxWidth,
bboxHeight,
]);
this.#lastPoint = lastPoint;
}
getOutlines() {
@ -173,7 +175,7 @@ class HighlightOutliner {
}
outline.push(lastPointX, lastPointY);
}
return new HighlightOutline(outlines, this.#box);
return new HighlightOutline(outlines, this.#box, this.#lastPoint);
}
#binarySearch(y) {
@ -267,10 +269,11 @@ class HighlightOutline extends Outline {
#outlines;
constructor(outlines, box) {
constructor(outlines, box, lastPoint) {
super();
this.#outlines = outlines;
this.#box = box;
this.lastPoint = lastPoint;
}
toSVGPath() {
@ -319,10 +322,6 @@ class HighlightOutline extends Outline {
return this.#box;
}
get classNamesForDrawing() {
return ["highlight"];
}
get classNamesForOutlining() {
return ["highlightOutline"];
}
@ -339,21 +338,9 @@ class FreeHighlightOutliner extends FreeDrawOutliner {
isLTR
);
}
get classNamesForDrawing() {
return ["highlight", "free"];
}
}
class FreeHighlightOutline extends FreeDrawOutline {
get classNamesForDrawing() {
return ["highlight", "free"];
}
get classNamesForOutlining() {
return ["highlightOutline", "free"];
}
newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) {
return new FreeHighlightOutliner(
point,

View file

@ -35,20 +35,22 @@ class Outline {
unreachable("Abstract method `serialize` must be implemented.");
}
// eslint-disable-next-line getter-return
get classNamesForDrawing() {
unreachable("Abstract getter `classNamesForDrawing` must be implemented.");
static _rescale(src, tx, ty, sx, sy, dest) {
dest ||= new Float32Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i] * sx;
dest[i + 1] = ty + src[i + 1] * sy;
}
return dest;
}
// eslint-disable-next-line getter-return
get classNamesForOutlining() {
unreachable(
"Abstract getter `classNamesForOutlining` must be implemented."
);
}
get mustRemoveSelfIntersections() {
return false;
static _rescaleAndSwap(src, tx, ty, sx, sy, dest) {
dest ||= new Float32Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i + 1] * sx;
dest[i + 1] = ty + src[i] * sy;
}
return dest;
}
}

View file

@ -157,12 +157,7 @@ class HighlightEditor extends AnnotationEditor {
/* borderWidth = */ 0.001
);
this.#highlightOutlines = outliner.getOutlines();
({
x: this.x,
y: this.y,
width: this.width,
height: this.height,
} = this.#highlightOutlines.box);
[this.x, this.y, this.width, this.height] = this.#highlightOutlines.box;
const outlinerForOutline = new HighlightOutliner(
this.#boxes,
@ -173,7 +168,7 @@ class HighlightEditor extends AnnotationEditor {
this.#focusOutlines = outlinerForOutline.getOutlines();
// The last point is in the pages coordinate system.
const { lastPoint } = this.#focusOutlines.box;
const { lastPoint } = this.#focusOutlines;
this.#lastPoint = [
(lastPoint[0] - this.x) / this.width,
(lastPoint[1] - this.y) / this.height,
@ -195,26 +190,44 @@ class HighlightEditor extends AnnotationEditor {
this.#clipPathId = clipPathId;
// We need to redraw the highlight because we change the coordinates to be
// in the box coordinate system.
this.parent.drawLayer.finalizeLine(highlightId, highlightOutlines);
this.#outlineId = this.parent.drawLayer.drawOutline(this.#focusOutlines);
this.parent.drawLayer.finalizeDraw(highlightId, {
bbox: highlightOutlines.box,
path: {
d: highlightOutlines.toSVGPath(),
},
});
this.#outlineId = this.parent.drawLayer.drawOutline(
{
rootClass: {
highlightOutline: true,
free: true,
},
bbox: this.#focusOutlines.box,
path: {
d: this.#focusOutlines.toSVGPath(),
},
},
/* mustRemoveSelfIntersections = */ true
);
} else if (this.parent) {
const angle = this.parent.viewport.rotation;
this.parent.drawLayer.updateLine(this.#id, highlightOutlines);
this.parent.drawLayer.updateBox(
this.#id,
HighlightEditor.#rotateBbox(
this.parent.drawLayer.updateProperties(this.#id, {
bbox: HighlightEditor.#rotateBbox(
this.#highlightOutlines.box,
(angle - this.rotation + 360) % 360
)
);
this.parent.drawLayer.updateLine(this.#outlineId, this.#focusOutlines);
this.parent.drawLayer.updateBox(
this.#outlineId,
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
);
),
path: {
d: highlightOutlines.toSVGPath(),
},
});
this.parent.drawLayer.updateProperties(this.#outlineId, {
bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle),
path: {
d: this.#focusOutlines.toSVGPath(),
},
});
}
const { x, y, width, height } = highlightOutlines.box;
const [x, y, width, height] = highlightOutlines.box;
switch (this.rotation) {
case 0:
this.x = x;
@ -246,7 +259,7 @@ class HighlightEditor extends AnnotationEditor {
}
}
const { lastPoint } = this.#focusOutlines.box;
const { lastPoint } = this.#focusOutlines;
this.#lastPoint = [(lastPoint[0] - x) / width, (lastPoint[1] - y) / height];
}
@ -324,10 +337,14 @@ class HighlightEditor extends AnnotationEditor {
#updateColor(color) {
const setColorAndOpacity = (col, opa) => {
this.color = col;
this.parent?.drawLayer.changeColor(this.#id, col);
this.#colorPicker?.updateColor(col);
this.#opacity = opa;
this.parent?.drawLayer.changeOpacity(this.#id, opa);
this.parent?.drawLayer.updateProperties(this.#id, {
root: {
fill: col,
"fill-opacity": opa,
},
});
this.#colorPicker?.updateColor(col);
};
const savedColor = this.color;
const savedOpacity = this.#opacity;
@ -503,46 +520,53 @@ class HighlightEditor extends AnnotationEditor {
return;
}
({ id: this.#id, clipPathId: this.#clipPathId } = parent.drawLayer.draw(
this.#highlightOutlines,
this.color,
this.#opacity
{
bbox: this.#highlightOutlines.box,
root: {
viewBox: "0 0 1 1",
fill: this.color,
"fill-opacity": this.#opacity,
},
rootClass: {
highlight: true,
free: this.#isFreeHighlight,
},
path: {
d: this.#highlightOutlines.toSVGPath(),
},
},
/* isPathUpdatable = */ false,
/* hasClip = */ true
));
this.#outlineId = parent.drawLayer.drawOutline(this.#focusOutlines);
this.#outlineId = parent.drawLayer.drawOutline(
{
rootClass: {
highlightOutline: true,
free: this.#isFreeHighlight,
},
bbox: this.#focusOutlines.box,
path: {
d: this.#focusOutlines.toSVGPath(),
},
},
/* mustRemoveSelfIntersections = */ this.#isFreeHighlight
);
if (this.#highlightDiv) {
this.#highlightDiv.style.clipPath = this.#clipPathId;
}
}
static #rotateBbox({ x, y, width, height }, angle) {
static #rotateBbox([x, y, width, height], angle) {
switch (angle) {
case 90:
return {
x: 1 - y - height,
y: x,
width: height,
height: width,
};
return [1 - y - height, x, height, width];
case 180:
return {
x: 1 - x - width,
y: 1 - y - height,
width,
height,
};
return [1 - x - width, 1 - y - height, width, height];
case 270:
return {
x: y,
y: 1 - x - width,
width: height,
height: width,
};
return [y, 1 - x - width, height, width];
}
return {
x,
y,
width,
height,
};
return [x, y, width, height];
}
/** @inheritdoc */
@ -555,15 +579,23 @@ class HighlightEditor extends AnnotationEditor {
box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle);
} else {
// An highlight annotation is always drawn horizontally.
box = HighlightEditor.#rotateBbox(this, angle);
box = HighlightEditor.#rotateBbox(
[this.x, this.y, this.width, this.height],
angle
);
}
drawLayer.rotate(this.#id, angle);
drawLayer.rotate(this.#outlineId, angle);
drawLayer.updateBox(this.#id, box);
drawLayer.updateBox(
this.#outlineId,
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
);
drawLayer.updateProperties(this.#id, {
bbox: box,
root: {
"data-main-rotation": angle,
},
});
drawLayer.updateProperties(this.#outlineId, {
bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle),
root: {
"data-main-rotation": angle,
},
});
}
/** @inheritdoc */
@ -600,13 +632,21 @@ class HighlightEditor extends AnnotationEditor {
pointerover() {
if (!this.isSelected) {
this.parent.drawLayer.addClass(this.#outlineId, "hovered");
this.parent?.drawLayer.updateProperties(this.#outlineId, {
rootClass: {
hovered: true,
},
});
}
}
pointerleave() {
if (!this.isSelected) {
this.parent.drawLayer.removeClass(this.#outlineId, "hovered");
this.parent?.drawLayer.updateProperties(this.#outlineId, {
rootClass: {
hovered: false,
},
});
}
}
@ -646,8 +686,12 @@ class HighlightEditor extends AnnotationEditor {
if (!this.#outlineId) {
return;
}
this.parent?.drawLayer.removeClass(this.#outlineId, "hovered");
this.parent?.drawLayer.addClass(this.#outlineId, "selected");
this.parent?.drawLayer.updateProperties(this.#outlineId, {
rootClass: {
hovered: false,
selected: true,
},
});
}
/** @inheritdoc */
@ -656,7 +700,11 @@ class HighlightEditor extends AnnotationEditor {
if (!this.#outlineId) {
return;
}
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
this.parent?.drawLayer.updateProperties(this.#outlineId, {
rootClass: {
selected: false,
},
});
if (!this.#isFreeHighlight) {
this.#setCaret(/* start = */ false);
}
@ -671,8 +719,16 @@ class HighlightEditor extends AnnotationEditor {
show(visible = this._isVisible) {
super.show(visible);
if (this.parent) {
this.parent.drawLayer.show(this.#id, visible);
this.parent.drawLayer.show(this.#outlineId, visible);
this.parent.drawLayer.updateProperties(this.#id, {
rootClass: {
hidden: !visible,
},
});
this.parent.drawLayer.updateProperties(this.#outlineId, {
rootClass: {
hidden: !visible,
},
});
}
}
@ -755,17 +811,34 @@ class HighlightEditor extends AnnotationEditor {
);
({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
parent.drawLayer.draw(
this._freeHighlight,
this._defaultColor,
this._defaultOpacity,
/* isPathUpdatable = */ true
{
bbox: [0, 0, 1, 1],
root: {
viewBox: "0 0 1 1",
fill: this._defaultColor,
"fill-opacity": this._defaultOpacity,
},
rootClass: {
highlight: true,
free: true,
},
path: {
d: this._freeHighlight.toSVGPath(),
},
},
/* isPathUpdatable = */ true,
/* hasClip = */ true
));
}
static #highlightMove(parent, event) {
if (this._freeHighlight.add(event)) {
// Redraw only if the point has been added.
parent.drawLayer.updatePath(this._freeHighlightId, this._freeHighlight);
parent.drawLayer.updateProperties(this._freeHighlightId, {
path: {
d: this._freeHighlight.toSVGPath(),
},
});
}
}
@ -886,10 +959,23 @@ class HighlightEditor extends AnnotationEditor {
outliner.add(point);
}
const { id, clipPathId } = parent.drawLayer.draw(
outliner,
editor.color,
editor._defaultOpacity,
/* isPathUpdatable = */ true
{
bbox: [0, 0, 1, 1],
root: {
viewBox: "0 0 1 1",
fill: editor.color,
"fill-opacity": editor._defaultOpacity,
},
rootClass: {
highlight: true,
free: true,
},
path: {
d: outliner.toSVGPath(),
},
},
/* isPathUpdatable = */ true,
/* hasClip = */ true
);
editor.#createFreeOutlines({
highlightOutlines: outliner.getOutlines(),