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

[Editor] Simplify the draw layer code

and tweak a bit the highlight one (e.g. it's useless to have 64 bits floating point numbers
when 32 bits ones are enough).

It's a required step for the refactoring of the ink tool (in order to use the draw layer).
It avoids to call several functions acting on the same SVG element.
This commit is contained in:
Calixte Denizet 2024-11-21 15:14:17 +01:00
parent 07765e993e
commit 3c343acbb6
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(),