Merge pull request #18972 from calixteman/refactor_highlight
[Editor] Refactor the free highlight stuff in order to be able to use the code for more general drawing
This commit is contained in:
commit
f142fb8c28
@ -86,13 +86,11 @@ class DrawLayer {
|
|||||||
return clipPathId;
|
return clipPathId;
|
||||||
}
|
}
|
||||||
|
|
||||||
highlight(outlines, color, opacity, isPathUpdatable = false) {
|
draw(outlines, color, opacity, isPathUpdatable = false) {
|
||||||
const id = this.#id++;
|
const id = this.#id++;
|
||||||
const root = this.#createSVG(outlines.box);
|
const root = this.#createSVG(outlines.box);
|
||||||
root.classList.add("highlight");
|
root.classList.add(...outlines.classNamesForDrawing);
|
||||||
if (outlines.free) {
|
|
||||||
root.classList.add("free");
|
|
||||||
}
|
|
||||||
const defs = DrawLayer._svgFactory.createElement("defs");
|
const defs = DrawLayer._svgFactory.createElement("defs");
|
||||||
root.append(defs);
|
root.append(defs);
|
||||||
const path = DrawLayer._svgFactory.createElement("path");
|
const path = DrawLayer._svgFactory.createElement("path");
|
||||||
@ -119,14 +117,14 @@ class DrawLayer {
|
|||||||
return { id, clipPathId: `url(#${clipPathId})` };
|
return { id, clipPathId: `url(#${clipPathId})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
highlightOutline(outlines) {
|
drawOutline(outlines) {
|
||||||
// We cannot draw the outline directly in the SVG for highlights because
|
// We cannot draw the outline directly in the SVG for highlights because
|
||||||
// it composes with its parent with mix-blend-mode: multiply.
|
// 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
|
// But the outline has a different mix-blend-mode, so we need to draw it in
|
||||||
// its own SVG.
|
// its own SVG.
|
||||||
const id = this.#id++;
|
const id = this.#id++;
|
||||||
const root = this.#createSVG(outlines.box);
|
const root = this.#createSVG(outlines.box);
|
||||||
root.classList.add("highlightOutline");
|
root.classList.add(...outlines.classNamesForOutlining);
|
||||||
const defs = DrawLayer._svgFactory.createElement("defs");
|
const defs = DrawLayer._svgFactory.createElement("defs");
|
||||||
root.append(defs);
|
root.append(defs);
|
||||||
const path = DrawLayer._svgFactory.createElement("path");
|
const path = DrawLayer._svgFactory.createElement("path");
|
||||||
@ -137,8 +135,7 @@ class DrawLayer {
|
|||||||
path.setAttribute("vector-effect", "non-scaling-stroke");
|
path.setAttribute("vector-effect", "non-scaling-stroke");
|
||||||
|
|
||||||
let maskId;
|
let maskId;
|
||||||
if (outlines.free) {
|
if (outlines.mustRemoveSelfIntersections) {
|
||||||
root.classList.add("free");
|
|
||||||
const mask = DrawLayer._svgFactory.createElement("mask");
|
const mask = DrawLayer._svgFactory.createElement("mask");
|
||||||
defs.append(mask);
|
defs.append(mask);
|
||||||
maskId = `mask_p${this.pageIndex}_${id}`;
|
maskId = `mask_p${this.pageIndex}_${id}`;
|
||||||
@ -188,11 +185,6 @@ class DrawLayer {
|
|||||||
path.setAttribute("d", line.toSVGPath());
|
path.setAttribute("d", line.toSVGPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFreeHighlight(id) {
|
|
||||||
this.remove(id);
|
|
||||||
this.#toUpdate.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePath(id, line) {
|
updatePath(id, line) {
|
||||||
this.#toUpdate.get(id).setAttribute("d", line.toSVGPath());
|
this.#toUpdate.get(id).setAttribute("d", line.toSVGPath());
|
||||||
}
|
}
|
||||||
@ -230,6 +222,7 @@ class DrawLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(id) {
|
remove(id) {
|
||||||
|
this.#toUpdate.delete(id);
|
||||||
if (this.#parent === null) {
|
if (this.#parent === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -243,6 +236,7 @@ class DrawLayer {
|
|||||||
root.remove();
|
root.remove();
|
||||||
}
|
}
|
||||||
this.#mapping.clear();
|
this.#mapping.clear();
|
||||||
|
this.#toUpdate.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* Copyright 2023 Mozilla Foundation
|
/* Copyright 2024 Mozilla Foundation
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -13,337 +13,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Util } from "../../shared/util.js";
|
import { Outline } from "./outline.js";
|
||||||
|
import { Util } from "../../../shared/util.js";
|
||||||
|
|
||||||
class Outliner {
|
class FreeDrawOutliner {
|
||||||
#box;
|
|
||||||
|
|
||||||
#verticalEdges = [];
|
|
||||||
|
|
||||||
#intervals = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct an outliner.
|
|
||||||
* @param {Array<Object>} boxes - An array of axis-aligned rectangles.
|
|
||||||
* @param {number} borderWidth - The width of the border of the boxes, it
|
|
||||||
* allows to make the boxes bigger (or smaller).
|
|
||||||
* @param {number} innerMargin - The margin between the boxes and the
|
|
||||||
* outlines. It's important to not have a null innerMargin when we want to
|
|
||||||
* draw the outline else the stroked outline could be clipped because of its
|
|
||||||
* width.
|
|
||||||
* @param {boolean} isLTR - true if we're in LTR mode. It's used to determine
|
|
||||||
* the last point of the boxes.
|
|
||||||
*/
|
|
||||||
constructor(boxes, borderWidth = 0, innerMargin = 0, isLTR = true) {
|
|
||||||
let minX = Infinity;
|
|
||||||
let maxX = -Infinity;
|
|
||||||
let minY = Infinity;
|
|
||||||
let maxY = -Infinity;
|
|
||||||
|
|
||||||
// We round the coordinates to slightly reduce the number of edges in the
|
|
||||||
// final outlines.
|
|
||||||
const NUMBER_OF_DIGITS = 4;
|
|
||||||
const EPSILON = 10 ** -NUMBER_OF_DIGITS;
|
|
||||||
|
|
||||||
// The coordinates of the boxes are in the page coordinate system.
|
|
||||||
for (const { x, y, width, height } of boxes) {
|
|
||||||
const x1 = Math.floor((x - borderWidth) / EPSILON) * EPSILON;
|
|
||||||
const x2 = Math.ceil((x + width + borderWidth) / EPSILON) * EPSILON;
|
|
||||||
const y1 = Math.floor((y - borderWidth) / EPSILON) * EPSILON;
|
|
||||||
const y2 = Math.ceil((y + height + borderWidth) / EPSILON) * EPSILON;
|
|
||||||
const left = [x1, y1, y2, true];
|
|
||||||
const right = [x2, y1, y2, false];
|
|
||||||
this.#verticalEdges.push(left, right);
|
|
||||||
|
|
||||||
minX = Math.min(minX, x1);
|
|
||||||
maxX = Math.max(maxX, x2);
|
|
||||||
minY = Math.min(minY, y1);
|
|
||||||
maxY = Math.max(maxY, y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bboxWidth = maxX - minX + 2 * innerMargin;
|
|
||||||
const bboxHeight = maxY - minY + 2 * innerMargin;
|
|
||||||
const shiftedMinX = minX - innerMargin;
|
|
||||||
const shiftedMinY = minY - innerMargin;
|
|
||||||
const lastEdge = this.#verticalEdges.at(isLTR ? -1 : -2);
|
|
||||||
const lastPoint = [lastEdge[0], lastEdge[2]];
|
|
||||||
|
|
||||||
// Convert the coordinates of the edges into box coordinates.
|
|
||||||
for (const edge of this.#verticalEdges) {
|
|
||||||
const [x, y1, y2] = edge;
|
|
||||||
edge[0] = (x - shiftedMinX) / bboxWidth;
|
|
||||||
edge[1] = (y1 - shiftedMinY) / bboxHeight;
|
|
||||||
edge[2] = (y2 - shiftedMinY) / bboxHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#box = {
|
|
||||||
x: shiftedMinX,
|
|
||||||
y: shiftedMinY,
|
|
||||||
width: bboxWidth,
|
|
||||||
height: bboxHeight,
|
|
||||||
lastPoint,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getOutlines() {
|
|
||||||
// We begin to sort lexicographically the vertical edges by their abscissa,
|
|
||||||
// and then by their ordinate.
|
|
||||||
this.#verticalEdges.sort(
|
|
||||||
(a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
// We're now using a sweep line algorithm to find the outlines.
|
|
||||||
// We start with the leftmost vertical edge, and we're going to iterate
|
|
||||||
// over all the vertical edges from left to right.
|
|
||||||
// Each time we encounter a left edge, we're going to insert the interval
|
|
||||||
// [y1, y2] in the set of intervals.
|
|
||||||
// This set of intervals is used to break the vertical edges into chunks:
|
|
||||||
// we only take the part of the vertical edge that isn't in the union of
|
|
||||||
// the intervals.
|
|
||||||
const outlineVerticalEdges = [];
|
|
||||||
for (const edge of this.#verticalEdges) {
|
|
||||||
if (edge[3]) {
|
|
||||||
// Left edge.
|
|
||||||
outlineVerticalEdges.push(...this.#breakEdge(edge));
|
|
||||||
this.#insert(edge);
|
|
||||||
} else {
|
|
||||||
// Right edge.
|
|
||||||
this.#remove(edge);
|
|
||||||
outlineVerticalEdges.push(...this.#breakEdge(edge));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.#getOutlines(outlineVerticalEdges);
|
|
||||||
}
|
|
||||||
|
|
||||||
#getOutlines(outlineVerticalEdges) {
|
|
||||||
const edges = [];
|
|
||||||
const allEdges = new Set();
|
|
||||||
|
|
||||||
for (const edge of outlineVerticalEdges) {
|
|
||||||
const [x, y1, y2] = edge;
|
|
||||||
edges.push([x, y1, edge], [x, y2, edge]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We sort lexicographically the vertices of each edge by their ordinate and
|
|
||||||
// by their abscissa.
|
|
||||||
// Every pair (v_2i, v_{2i + 1}) of vertices defines a horizontal edge.
|
|
||||||
// So for every vertical edge, we're going to add the two vertical edges
|
|
||||||
// which are connected to it through a horizontal edge.
|
|
||||||
edges.sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
|
||||||
for (let i = 0, ii = edges.length; i < ii; i += 2) {
|
|
||||||
const edge1 = edges[i][2];
|
|
||||||
const edge2 = edges[i + 1][2];
|
|
||||||
edge1.push(edge2);
|
|
||||||
edge2.push(edge1);
|
|
||||||
allEdges.add(edge1);
|
|
||||||
allEdges.add(edge2);
|
|
||||||
}
|
|
||||||
const outlines = [];
|
|
||||||
let outline;
|
|
||||||
|
|
||||||
while (allEdges.size > 0) {
|
|
||||||
const edge = allEdges.values().next().value;
|
|
||||||
let [x, y1, y2, edge1, edge2] = edge;
|
|
||||||
allEdges.delete(edge);
|
|
||||||
let lastPointX = x;
|
|
||||||
let lastPointY = y1;
|
|
||||||
|
|
||||||
outline = [x, y2];
|
|
||||||
outlines.push(outline);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
let e;
|
|
||||||
if (allEdges.has(edge1)) {
|
|
||||||
e = edge1;
|
|
||||||
} else if (allEdges.has(edge2)) {
|
|
||||||
e = edge2;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
allEdges.delete(e);
|
|
||||||
[x, y1, y2, edge1, edge2] = e;
|
|
||||||
|
|
||||||
if (lastPointX !== x) {
|
|
||||||
outline.push(lastPointX, lastPointY, x, lastPointY === y1 ? y1 : y2);
|
|
||||||
lastPointX = x;
|
|
||||||
}
|
|
||||||
lastPointY = lastPointY === y1 ? y2 : y1;
|
|
||||||
}
|
|
||||||
outline.push(lastPointX, lastPointY);
|
|
||||||
}
|
|
||||||
return new HighlightOutline(outlines, this.#box);
|
|
||||||
}
|
|
||||||
|
|
||||||
#binarySearch(y) {
|
|
||||||
const array = this.#intervals;
|
|
||||||
let start = 0;
|
|
||||||
let end = array.length - 1;
|
|
||||||
|
|
||||||
while (start <= end) {
|
|
||||||
const middle = (start + end) >> 1;
|
|
||||||
const y1 = array[middle][0];
|
|
||||||
if (y1 === y) {
|
|
||||||
return middle;
|
|
||||||
}
|
|
||||||
if (y1 < y) {
|
|
||||||
start = middle + 1;
|
|
||||||
} else {
|
|
||||||
end = middle - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#insert([, y1, y2]) {
|
|
||||||
const index = this.#binarySearch(y1);
|
|
||||||
this.#intervals.splice(index, 0, [y1, y2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#remove([, y1, y2]) {
|
|
||||||
const index = this.#binarySearch(y1);
|
|
||||||
for (let i = index; i < this.#intervals.length; i++) {
|
|
||||||
const [start, end] = this.#intervals[i];
|
|
||||||
if (start !== y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (start === y1 && end === y2) {
|
|
||||||
this.#intervals.splice(i, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = index - 1; i >= 0; i--) {
|
|
||||||
const [start, end] = this.#intervals[i];
|
|
||||||
if (start !== y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (start === y1 && end === y2) {
|
|
||||||
this.#intervals.splice(i, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#breakEdge(edge) {
|
|
||||||
const [x, y1, y2] = edge;
|
|
||||||
const results = [[x, y1, y2]];
|
|
||||||
const index = this.#binarySearch(y2);
|
|
||||||
for (let i = 0; i < index; i++) {
|
|
||||||
const [start, end] = this.#intervals[i];
|
|
||||||
for (let j = 0, jj = results.length; j < jj; j++) {
|
|
||||||
const [, y3, y4] = results[j];
|
|
||||||
if (end <= y3 || y4 <= start) {
|
|
||||||
// There is no intersection between the interval and the edge, hence
|
|
||||||
// we keep it as is.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (y3 >= start) {
|
|
||||||
if (y4 > end) {
|
|
||||||
results[j][1] = end;
|
|
||||||
} else {
|
|
||||||
if (jj === 1) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// The edge is included in the interval, hence we remove it.
|
|
||||||
results.splice(j, 1);
|
|
||||||
j--;
|
|
||||||
jj--;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
results[j][2] = start;
|
|
||||||
if (y4 > end) {
|
|
||||||
results.push([x, end, y4]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Outline {
|
|
||||||
/**
|
|
||||||
* @returns {string} The SVG path of the outline.
|
|
||||||
*/
|
|
||||||
toSVGPath() {
|
|
||||||
throw new Error("Abstract method `toSVGPath` must be implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Object|null} The bounding box of the outline.
|
|
||||||
*/
|
|
||||||
get box() {
|
|
||||||
throw new Error("Abstract getter `box` must be implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(_bbox, _rotation) {
|
|
||||||
throw new Error("Abstract method `serialize` must be implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
get free() {
|
|
||||||
return this instanceof FreeHighlightOutline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HighlightOutline extends Outline {
|
|
||||||
#box;
|
|
||||||
|
|
||||||
#outlines;
|
|
||||||
|
|
||||||
constructor(outlines, box) {
|
|
||||||
super();
|
|
||||||
this.#outlines = outlines;
|
|
||||||
this.#box = box;
|
|
||||||
}
|
|
||||||
|
|
||||||
toSVGPath() {
|
|
||||||
const buffer = [];
|
|
||||||
for (const polygon of this.#outlines) {
|
|
||||||
let [prevX, prevY] = polygon;
|
|
||||||
buffer.push(`M${prevX} ${prevY}`);
|
|
||||||
for (let i = 2; i < polygon.length; i += 2) {
|
|
||||||
const x = polygon[i];
|
|
||||||
const y = polygon[i + 1];
|
|
||||||
if (x === prevX) {
|
|
||||||
buffer.push(`V${y}`);
|
|
||||||
prevY = y;
|
|
||||||
} else if (y === prevY) {
|
|
||||||
buffer.push(`H${x}`);
|
|
||||||
prevX = x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer.push("Z");
|
|
||||||
}
|
|
||||||
return buffer.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize the outlines into the PDF page coordinate system.
|
|
||||||
* @param {Array<number>} _bbox - the bounding box of the annotation.
|
|
||||||
* @param {number} _rotation - the rotation of the annotation.
|
|
||||||
* @returns {Array<Array<number>>}
|
|
||||||
*/
|
|
||||||
serialize([blX, blY, trX, trY], _rotation) {
|
|
||||||
const outlines = [];
|
|
||||||
const width = trX - blX;
|
|
||||||
const height = trY - blY;
|
|
||||||
for (const outline of this.#outlines) {
|
|
||||||
const points = new Array(outline.length);
|
|
||||||
for (let i = 0; i < outline.length; i += 2) {
|
|
||||||
points[i] = blX + outline[i] * width;
|
|
||||||
points[i + 1] = trY - outline[i + 1] * height;
|
|
||||||
}
|
|
||||||
outlines.push(points);
|
|
||||||
}
|
|
||||||
return outlines;
|
|
||||||
}
|
|
||||||
|
|
||||||
get box() {
|
|
||||||
return this.#box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FreeOutliner {
|
|
||||||
#box;
|
#box;
|
||||||
|
|
||||||
#bottom = [];
|
#bottom = [];
|
||||||
@ -381,7 +54,7 @@ class FreeOutliner {
|
|||||||
|
|
||||||
static #MIN_DIFF = 2;
|
static #MIN_DIFF = 2;
|
||||||
|
|
||||||
static #MIN = FreeOutliner.#MIN_DIST + FreeOutliner.#MIN_DIFF;
|
static #MIN = FreeDrawOutliner.#MIN_DIST + FreeDrawOutliner.#MIN_DIFF;
|
||||||
|
|
||||||
constructor({ x, y }, box, scaleFactor, thickness, isLTR, innerMargin = 0) {
|
constructor({ x, y }, box, scaleFactor, thickness, isLTR, innerMargin = 0) {
|
||||||
this.#box = box;
|
this.#box = box;
|
||||||
@ -389,16 +62,12 @@ class FreeOutliner {
|
|||||||
this.#isLTR = isLTR;
|
this.#isLTR = isLTR;
|
||||||
this.#last.set([NaN, NaN, NaN, NaN, x, y], 6);
|
this.#last.set([NaN, NaN, NaN, NaN, x, y], 6);
|
||||||
this.#innerMargin = innerMargin;
|
this.#innerMargin = innerMargin;
|
||||||
this.#min_dist = FreeOutliner.#MIN_DIST * scaleFactor;
|
this.#min_dist = FreeDrawOutliner.#MIN_DIST * scaleFactor;
|
||||||
this.#min = FreeOutliner.#MIN * scaleFactor;
|
this.#min = FreeDrawOutliner.#MIN * scaleFactor;
|
||||||
this.#scaleFactor = scaleFactor;
|
this.#scaleFactor = scaleFactor;
|
||||||
this.#points.push(x, y);
|
this.#points.push(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
get free() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
// When we add a second point then this.#last.slice(6) will be something
|
// When we add a second point then this.#last.slice(6) will be something
|
||||||
// like [NaN, NaN, firstX, firstY, secondX, secondY,...] so having a NaN
|
// like [NaN, NaN, firstX, firstY, secondX, secondY,...] so having a NaN
|
||||||
@ -544,21 +213,10 @@ class FreeOutliner {
|
|||||||
}
|
}
|
||||||
const top = this.#top;
|
const top = this.#top;
|
||||||
const bottom = this.#bottom;
|
const bottom = this.#bottom;
|
||||||
const lastTop = this.#last.subarray(4, 6);
|
|
||||||
const lastBottom = this.#last.subarray(16, 18);
|
|
||||||
const [x, y, width, height] = this.#box;
|
|
||||||
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
|
||||||
this.#getLastCoords();
|
|
||||||
|
|
||||||
if (isNaN(this.#last[6]) && !this.isEmpty()) {
|
if (isNaN(this.#last[6]) && !this.isEmpty()) {
|
||||||
// We've only two points.
|
// We've only two points.
|
||||||
return `M${(this.#last[2] - x) / width} ${
|
return this.#toSVGPathTwoPoints();
|
||||||
(this.#last[3] - y) / height
|
|
||||||
} L${(this.#last[4] - x) / width} ${(this.#last[5] - y) / height} L${lastTopX} ${lastTopY} L${lastBottomX} ${lastBottomY} L${
|
|
||||||
(this.#last[16] - x) / width
|
|
||||||
} ${(this.#last[17] - y) / height} L${(this.#last[14] - x) / width} ${
|
|
||||||
(this.#last[15] - y) / height
|
|
||||||
} Z`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = [];
|
const buffer = [];
|
||||||
@ -575,11 +233,8 @@ class FreeOutliner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.push(
|
this.#toSVGPathEnd(buffer);
|
||||||
`L${(lastTop[0] - x) / width} ${(lastTop[1] - y) / height} L${lastTopX} ${lastTopY} L${lastBottomX} ${lastBottomY} L${
|
|
||||||
(lastBottom[0] - x) / width
|
|
||||||
} ${(lastBottom[1] - y) / height}`
|
|
||||||
);
|
|
||||||
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
||||||
if (isNaN(bottom[i])) {
|
if (isNaN(bottom[i])) {
|
||||||
buffer.push(`L${bottom[i + 4]} ${bottom[i + 5]}`);
|
buffer.push(`L${bottom[i + 4]} ${bottom[i + 5]}`);
|
||||||
@ -591,17 +246,60 @@ class FreeOutliner {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.push(`L${bottom[4]} ${bottom[5]} Z`);
|
|
||||||
|
this.#toSVGPathStart(buffer);
|
||||||
|
|
||||||
return buffer.join(" ");
|
return buffer.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#toSVGPathTwoPoints() {
|
||||||
|
const [x, y, width, height] = this.#box;
|
||||||
|
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
||||||
|
this.#getLastCoords();
|
||||||
|
|
||||||
|
return `M${(this.#last[2] - x) / width} ${
|
||||||
|
(this.#last[3] - y) / height
|
||||||
|
} L${(this.#last[4] - x) / width} ${(this.#last[5] - y) / height} L${lastTopX} ${lastTopY} L${lastBottomX} ${lastBottomY} L${
|
||||||
|
(this.#last[16] - x) / width
|
||||||
|
} ${(this.#last[17] - y) / height} L${(this.#last[14] - x) / width} ${
|
||||||
|
(this.#last[15] - y) / height
|
||||||
|
} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toSVGPathStart(buffer) {
|
||||||
|
const bottom = this.#bottom;
|
||||||
|
buffer.push(`L${bottom[4]} ${bottom[5]} Z`);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toSVGPathEnd(buffer) {
|
||||||
|
const [x, y, width, height] = this.#box;
|
||||||
|
const lastTop = this.#last.subarray(4, 6);
|
||||||
|
const lastBottom = this.#last.subarray(16, 18);
|
||||||
|
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
||||||
|
this.#getLastCoords();
|
||||||
|
|
||||||
|
buffer.push(
|
||||||
|
`L${(lastTop[0] - x) / width} ${(lastTop[1] - y) / height} L${lastTopX} ${lastTopY} L${lastBottomX} ${lastBottomY} L${
|
||||||
|
(lastBottom[0] - x) / width
|
||||||
|
} ${(lastBottom[1] - y) / height}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
newFreeDrawOutline(outline, points, box, scaleFactor, innerMargin, isLTR) {
|
||||||
|
return new FreeDrawOutline(
|
||||||
|
outline,
|
||||||
|
points,
|
||||||
|
box,
|
||||||
|
scaleFactor,
|
||||||
|
innerMargin,
|
||||||
|
isLTR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getOutlines() {
|
getOutlines() {
|
||||||
const top = this.#top;
|
const top = this.#top;
|
||||||
const bottom = this.#bottom;
|
const bottom = this.#bottom;
|
||||||
const last = this.#last;
|
const last = this.#last;
|
||||||
const lastTop = last.subarray(4, 6);
|
|
||||||
const lastBottom = last.subarray(16, 18);
|
|
||||||
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
||||||
|
|
||||||
const points = new Float64Array((this.#points?.length ?? 0) + 2);
|
const points = new Float64Array((this.#points?.length ?? 0) + 2);
|
||||||
@ -611,61 +309,10 @@ class FreeOutliner {
|
|||||||
}
|
}
|
||||||
points[points.length - 2] = (this.#lastX - layerX) / layerWidth;
|
points[points.length - 2] = (this.#lastX - layerX) / layerWidth;
|
||||||
points[points.length - 1] = (this.#lastY - layerY) / layerHeight;
|
points[points.length - 1] = (this.#lastY - layerY) / layerHeight;
|
||||||
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
|
||||||
this.#getLastCoords();
|
|
||||||
|
|
||||||
if (isNaN(last[6]) && !this.isEmpty()) {
|
if (isNaN(last[6]) && !this.isEmpty()) {
|
||||||
// We've only two points.
|
// We've only two points.
|
||||||
const outline = new Float64Array(36);
|
return this.#getOutlineTwoPoints(points);
|
||||||
outline.set(
|
|
||||||
[
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
(last[2] - layerX) / layerWidth,
|
|
||||||
(last[3] - layerY) / layerHeight,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
(last[4] - layerX) / layerWidth,
|
|
||||||
(last[5] - layerY) / layerHeight,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
lastTopX,
|
|
||||||
lastTopY,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
lastBottomX,
|
|
||||||
lastBottomY,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
(last[16] - layerX) / layerWidth,
|
|
||||||
(last[17] - layerY) / layerHeight,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
NaN,
|
|
||||||
(last[14] - layerX) / layerWidth,
|
|
||||||
(last[15] - layerY) / layerHeight,
|
|
||||||
],
|
|
||||||
0
|
|
||||||
);
|
|
||||||
return new FreeHighlightOutline(
|
|
||||||
outline,
|
|
||||||
points,
|
|
||||||
this.#box,
|
|
||||||
this.#scaleFactor,
|
|
||||||
this.#innerMargin,
|
|
||||||
this.#isLTR
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const outline = new Float64Array(
|
const outline = new Float64Array(
|
||||||
@ -681,6 +328,102 @@ class FreeOutliner {
|
|||||||
outline[i + 1] = top[i + 1];
|
outline[i + 1] = top[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
N = this.#getOutlineEnd(outline, N);
|
||||||
|
|
||||||
|
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
||||||
|
for (let j = 0; j < 6; j += 2) {
|
||||||
|
if (isNaN(bottom[i + j])) {
|
||||||
|
outline[N] = outline[N + 1] = NaN;
|
||||||
|
N += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
outline[N] = bottom[i + j];
|
||||||
|
outline[N + 1] = bottom[i + j + 1];
|
||||||
|
N += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#getOutlineStart(outline, N);
|
||||||
|
|
||||||
|
return this.newFreeDrawOutline(
|
||||||
|
outline,
|
||||||
|
points,
|
||||||
|
this.#box,
|
||||||
|
this.#scaleFactor,
|
||||||
|
this.#innerMargin,
|
||||||
|
this.#isLTR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOutlineTwoPoints(points) {
|
||||||
|
const last = this.#last;
|
||||||
|
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
||||||
|
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
||||||
|
this.#getLastCoords();
|
||||||
|
const outline = new Float64Array(36);
|
||||||
|
outline.set(
|
||||||
|
[
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
(last[2] - layerX) / layerWidth,
|
||||||
|
(last[3] - layerY) / layerHeight,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
(last[4] - layerX) / layerWidth,
|
||||||
|
(last[5] - layerY) / layerHeight,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
lastTopX,
|
||||||
|
lastTopY,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
lastBottomX,
|
||||||
|
lastBottomY,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
(last[16] - layerX) / layerWidth,
|
||||||
|
(last[17] - layerY) / layerHeight,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
NaN,
|
||||||
|
(last[14] - layerX) / layerWidth,
|
||||||
|
(last[15] - layerY) / layerHeight,
|
||||||
|
],
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return this.newFreeDrawOutline(
|
||||||
|
outline,
|
||||||
|
points,
|
||||||
|
this.#box,
|
||||||
|
this.#scaleFactor,
|
||||||
|
this.#innerMargin,
|
||||||
|
this.#isLTR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOutlineStart(outline, pos) {
|
||||||
|
const bottom = this.#bottom;
|
||||||
|
outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], pos);
|
||||||
|
return (pos += 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOutlineEnd(outline, pos) {
|
||||||
|
const lastTop = this.#last.subarray(4, 6);
|
||||||
|
const lastBottom = this.#last.subarray(16, 18);
|
||||||
|
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
|
||||||
|
const [lastTopX, lastTopY, lastBottomX, lastBottomY] =
|
||||||
|
this.#getLastCoords();
|
||||||
outline.set(
|
outline.set(
|
||||||
[
|
[
|
||||||
NaN,
|
NaN,
|
||||||
@ -708,35 +451,13 @@ class FreeOutliner {
|
|||||||
(lastBottom[0] - layerX) / layerWidth,
|
(lastBottom[0] - layerX) / layerWidth,
|
||||||
(lastBottom[1] - layerY) / layerHeight,
|
(lastBottom[1] - layerY) / layerHeight,
|
||||||
],
|
],
|
||||||
N
|
pos
|
||||||
);
|
|
||||||
N += 24;
|
|
||||||
|
|
||||||
for (let i = bottom.length - 6; i >= 6; i -= 6) {
|
|
||||||
for (let j = 0; j < 6; j += 2) {
|
|
||||||
if (isNaN(bottom[i + j])) {
|
|
||||||
outline[N] = outline[N + 1] = NaN;
|
|
||||||
N += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
outline[N] = bottom[i + j];
|
|
||||||
outline[N + 1] = bottom[i + j + 1];
|
|
||||||
N += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], N);
|
|
||||||
return new FreeHighlightOutline(
|
|
||||||
outline,
|
|
||||||
points,
|
|
||||||
this.#box,
|
|
||||||
this.#scaleFactor,
|
|
||||||
this.#innerMargin,
|
|
||||||
this.#isLTR
|
|
||||||
);
|
);
|
||||||
|
return (pos += 24);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FreeHighlightOutline extends Outline {
|
class FreeDrawOutline extends Outline {
|
||||||
#box;
|
#box;
|
||||||
|
|
||||||
#bbox = null;
|
#bbox = null;
|
||||||
@ -895,6 +616,17 @@ class FreeHighlightOutline extends Outline {
|
|||||||
return this.#bbox;
|
return this.#bbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) {
|
||||||
|
return new FreeDrawOutliner(
|
||||||
|
point,
|
||||||
|
box,
|
||||||
|
scaleFactor,
|
||||||
|
thickness,
|
||||||
|
isLTR,
|
||||||
|
innerMargin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getNewOutline(thickness, innerMargin) {
|
getNewOutline(thickness, innerMargin) {
|
||||||
// Build the outline of the highlight to use as the focus outline.
|
// 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;
|
||||||
@ -903,7 +635,7 @@ class FreeHighlightOutline extends Outline {
|
|||||||
const sy = height * layerHeight;
|
const sy = height * layerHeight;
|
||||||
const tx = x * layerWidth + layerX;
|
const tx = x * layerWidth + layerX;
|
||||||
const ty = y * layerHeight + layerY;
|
const ty = y * layerHeight + layerY;
|
||||||
const outliner = new FreeOutliner(
|
const outliner = this.newOutliner(
|
||||||
{
|
{
|
||||||
x: this.#points[0] * sx + tx,
|
x: this.#points[0] * sx + tx,
|
||||||
y: this.#points[1] * sy + ty,
|
y: this.#points[1] * sy + ty,
|
||||||
@ -922,6 +654,10 @@ class FreeHighlightOutline extends Outline {
|
|||||||
}
|
}
|
||||||
return outliner.getOutlines();
|
return outliner.getOutlines();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mustRemoveSelfIntersections() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FreeOutliner, Outliner };
|
export { FreeDrawOutline, FreeDrawOutliner };
|
||||||
369
src/display/editor/drawers/highlight.js
Normal file
369
src/display/editor/drawers/highlight.js
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/* Copyright 2023 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 { FreeDrawOutline, FreeDrawOutliner } from "./freedraw.js";
|
||||||
|
import { Outline } from "./outline.js";
|
||||||
|
|
||||||
|
class HighlightOutliner {
|
||||||
|
#box;
|
||||||
|
|
||||||
|
#verticalEdges = [];
|
||||||
|
|
||||||
|
#intervals = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an outliner.
|
||||||
|
* @param {Array<Object>} boxes - An array of axis-aligned rectangles.
|
||||||
|
* @param {number} borderWidth - The width of the border of the boxes, it
|
||||||
|
* allows to make the boxes bigger (or smaller).
|
||||||
|
* @param {number} innerMargin - The margin between the boxes and the
|
||||||
|
* outlines. It's important to not have a null innerMargin when we want to
|
||||||
|
* draw the outline else the stroked outline could be clipped because of its
|
||||||
|
* width.
|
||||||
|
* @param {boolean} isLTR - true if we're in LTR mode. It's used to determine
|
||||||
|
* the last point of the boxes.
|
||||||
|
*/
|
||||||
|
constructor(boxes, borderWidth = 0, innerMargin = 0, isLTR = true) {
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
// We round the coordinates to slightly reduce the number of edges in the
|
||||||
|
// final outlines.
|
||||||
|
const NUMBER_OF_DIGITS = 4;
|
||||||
|
const EPSILON = 10 ** -NUMBER_OF_DIGITS;
|
||||||
|
|
||||||
|
// The coordinates of the boxes are in the page coordinate system.
|
||||||
|
for (const { x, y, width, height } of boxes) {
|
||||||
|
const x1 = Math.floor((x - borderWidth) / EPSILON) * EPSILON;
|
||||||
|
const x2 = Math.ceil((x + width + borderWidth) / EPSILON) * EPSILON;
|
||||||
|
const y1 = Math.floor((y - borderWidth) / EPSILON) * EPSILON;
|
||||||
|
const y2 = Math.ceil((y + height + borderWidth) / EPSILON) * EPSILON;
|
||||||
|
const left = [x1, y1, y2, true];
|
||||||
|
const right = [x2, y1, y2, false];
|
||||||
|
this.#verticalEdges.push(left, right);
|
||||||
|
|
||||||
|
minX = Math.min(minX, x1);
|
||||||
|
maxX = Math.max(maxX, x2);
|
||||||
|
minY = Math.min(minY, y1);
|
||||||
|
maxY = Math.max(maxY, y2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bboxWidth = maxX - minX + 2 * innerMargin;
|
||||||
|
const bboxHeight = maxY - minY + 2 * innerMargin;
|
||||||
|
const shiftedMinX = minX - innerMargin;
|
||||||
|
const shiftedMinY = minY - innerMargin;
|
||||||
|
const lastEdge = this.#verticalEdges.at(isLTR ? -1 : -2);
|
||||||
|
const lastPoint = [lastEdge[0], lastEdge[2]];
|
||||||
|
|
||||||
|
// Convert the coordinates of the edges into box coordinates.
|
||||||
|
for (const edge of this.#verticalEdges) {
|
||||||
|
const [x, y1, y2] = edge;
|
||||||
|
edge[0] = (x - shiftedMinX) / bboxWidth;
|
||||||
|
edge[1] = (y1 - shiftedMinY) / bboxHeight;
|
||||||
|
edge[2] = (y2 - shiftedMinY) / bboxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#box = {
|
||||||
|
x: shiftedMinX,
|
||||||
|
y: shiftedMinY,
|
||||||
|
width: bboxWidth,
|
||||||
|
height: bboxHeight,
|
||||||
|
lastPoint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutlines() {
|
||||||
|
// We begin to sort lexicographically the vertical edges by their abscissa,
|
||||||
|
// and then by their ordinate.
|
||||||
|
this.#verticalEdges.sort(
|
||||||
|
(a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
||||||
|
);
|
||||||
|
|
||||||
|
// We're now using a sweep line algorithm to find the outlines.
|
||||||
|
// We start with the leftmost vertical edge, and we're going to iterate
|
||||||
|
// over all the vertical edges from left to right.
|
||||||
|
// Each time we encounter a left edge, we're going to insert the interval
|
||||||
|
// [y1, y2] in the set of intervals.
|
||||||
|
// This set of intervals is used to break the vertical edges into chunks:
|
||||||
|
// we only take the part of the vertical edge that isn't in the union of
|
||||||
|
// the intervals.
|
||||||
|
const outlineVerticalEdges = [];
|
||||||
|
for (const edge of this.#verticalEdges) {
|
||||||
|
if (edge[3]) {
|
||||||
|
// Left edge.
|
||||||
|
outlineVerticalEdges.push(...this.#breakEdge(edge));
|
||||||
|
this.#insert(edge);
|
||||||
|
} else {
|
||||||
|
// Right edge.
|
||||||
|
this.#remove(edge);
|
||||||
|
outlineVerticalEdges.push(...this.#breakEdge(edge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.#getOutlines(outlineVerticalEdges);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getOutlines(outlineVerticalEdges) {
|
||||||
|
const edges = [];
|
||||||
|
const allEdges = new Set();
|
||||||
|
|
||||||
|
for (const edge of outlineVerticalEdges) {
|
||||||
|
const [x, y1, y2] = edge;
|
||||||
|
edges.push([x, y1, edge], [x, y2, edge]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We sort lexicographically the vertices of each edge by their ordinate and
|
||||||
|
// by their abscissa.
|
||||||
|
// Every pair (v_2i, v_{2i + 1}) of vertices defines a horizontal edge.
|
||||||
|
// So for every vertical edge, we're going to add the two vertical edges
|
||||||
|
// which are connected to it through a horizontal edge.
|
||||||
|
edges.sort((a, b) => a[1] - b[1] || a[0] - b[0]);
|
||||||
|
for (let i = 0, ii = edges.length; i < ii; i += 2) {
|
||||||
|
const edge1 = edges[i][2];
|
||||||
|
const edge2 = edges[i + 1][2];
|
||||||
|
edge1.push(edge2);
|
||||||
|
edge2.push(edge1);
|
||||||
|
allEdges.add(edge1);
|
||||||
|
allEdges.add(edge2);
|
||||||
|
}
|
||||||
|
const outlines = [];
|
||||||
|
let outline;
|
||||||
|
|
||||||
|
while (allEdges.size > 0) {
|
||||||
|
const edge = allEdges.values().next().value;
|
||||||
|
let [x, y1, y2, edge1, edge2] = edge;
|
||||||
|
allEdges.delete(edge);
|
||||||
|
let lastPointX = x;
|
||||||
|
let lastPointY = y1;
|
||||||
|
|
||||||
|
outline = [x, y2];
|
||||||
|
outlines.push(outline);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let e;
|
||||||
|
if (allEdges.has(edge1)) {
|
||||||
|
e = edge1;
|
||||||
|
} else if (allEdges.has(edge2)) {
|
||||||
|
e = edge2;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
allEdges.delete(e);
|
||||||
|
[x, y1, y2, edge1, edge2] = e;
|
||||||
|
|
||||||
|
if (lastPointX !== x) {
|
||||||
|
outline.push(lastPointX, lastPointY, x, lastPointY === y1 ? y1 : y2);
|
||||||
|
lastPointX = x;
|
||||||
|
}
|
||||||
|
lastPointY = lastPointY === y1 ? y2 : y1;
|
||||||
|
}
|
||||||
|
outline.push(lastPointX, lastPointY);
|
||||||
|
}
|
||||||
|
return new HighlightOutline(outlines, this.#box);
|
||||||
|
}
|
||||||
|
|
||||||
|
#binarySearch(y) {
|
||||||
|
const array = this.#intervals;
|
||||||
|
let start = 0;
|
||||||
|
let end = array.length - 1;
|
||||||
|
|
||||||
|
while (start <= end) {
|
||||||
|
const middle = (start + end) >> 1;
|
||||||
|
const y1 = array[middle][0];
|
||||||
|
if (y1 === y) {
|
||||||
|
return middle;
|
||||||
|
}
|
||||||
|
if (y1 < y) {
|
||||||
|
start = middle + 1;
|
||||||
|
} else {
|
||||||
|
end = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#insert([, y1, y2]) {
|
||||||
|
const index = this.#binarySearch(y1);
|
||||||
|
this.#intervals.splice(index, 0, [y1, y2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#remove([, y1, y2]) {
|
||||||
|
const index = this.#binarySearch(y1);
|
||||||
|
for (let i = index; i < this.#intervals.length; i++) {
|
||||||
|
const [start, end] = this.#intervals[i];
|
||||||
|
if (start !== y1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (start === y1 && end === y2) {
|
||||||
|
this.#intervals.splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
|
const [start, end] = this.#intervals[i];
|
||||||
|
if (start !== y1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (start === y1 && end === y2) {
|
||||||
|
this.#intervals.splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#breakEdge(edge) {
|
||||||
|
const [x, y1, y2] = edge;
|
||||||
|
const results = [[x, y1, y2]];
|
||||||
|
const index = this.#binarySearch(y2);
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
const [start, end] = this.#intervals[i];
|
||||||
|
for (let j = 0, jj = results.length; j < jj; j++) {
|
||||||
|
const [, y3, y4] = results[j];
|
||||||
|
if (end <= y3 || y4 <= start) {
|
||||||
|
// There is no intersection between the interval and the edge, hence
|
||||||
|
// we keep it as is.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (y3 >= start) {
|
||||||
|
if (y4 > end) {
|
||||||
|
results[j][1] = end;
|
||||||
|
} else {
|
||||||
|
if (jj === 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// The edge is included in the interval, hence we remove it.
|
||||||
|
results.splice(j, 1);
|
||||||
|
j--;
|
||||||
|
jj--;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results[j][2] = start;
|
||||||
|
if (y4 > end) {
|
||||||
|
results.push([x, end, y4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HighlightOutline extends Outline {
|
||||||
|
#box;
|
||||||
|
|
||||||
|
#outlines;
|
||||||
|
|
||||||
|
constructor(outlines, box) {
|
||||||
|
super();
|
||||||
|
this.#outlines = outlines;
|
||||||
|
this.#box = box;
|
||||||
|
}
|
||||||
|
|
||||||
|
toSVGPath() {
|
||||||
|
const buffer = [];
|
||||||
|
for (const polygon of this.#outlines) {
|
||||||
|
let [prevX, prevY] = polygon;
|
||||||
|
buffer.push(`M${prevX} ${prevY}`);
|
||||||
|
for (let i = 2; i < polygon.length; i += 2) {
|
||||||
|
const x = polygon[i];
|
||||||
|
const y = polygon[i + 1];
|
||||||
|
if (x === prevX) {
|
||||||
|
buffer.push(`V${y}`);
|
||||||
|
prevY = y;
|
||||||
|
} else if (y === prevY) {
|
||||||
|
buffer.push(`H${x}`);
|
||||||
|
prevX = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.push("Z");
|
||||||
|
}
|
||||||
|
return buffer.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the outlines into the PDF page coordinate system.
|
||||||
|
* @param {Array<number>} _bbox - the bounding box of the annotation.
|
||||||
|
* @param {number} _rotation - the rotation of the annotation.
|
||||||
|
* @returns {Array<Array<number>>}
|
||||||
|
*/
|
||||||
|
serialize([blX, blY, trX, trY], _rotation) {
|
||||||
|
const outlines = [];
|
||||||
|
const width = trX - blX;
|
||||||
|
const height = trY - blY;
|
||||||
|
for (const outline of this.#outlines) {
|
||||||
|
const points = new Array(outline.length);
|
||||||
|
for (let i = 0; i < outline.length; i += 2) {
|
||||||
|
points[i] = blX + outline[i] * width;
|
||||||
|
points[i + 1] = trY - outline[i + 1] * height;
|
||||||
|
}
|
||||||
|
outlines.push(points);
|
||||||
|
}
|
||||||
|
return outlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
get box() {
|
||||||
|
return this.#box;
|
||||||
|
}
|
||||||
|
|
||||||
|
get classNamesForDrawing() {
|
||||||
|
return ["highlight"];
|
||||||
|
}
|
||||||
|
|
||||||
|
get classNamesForOutlining() {
|
||||||
|
return ["highlightOutline"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FreeHighlightOutliner extends FreeDrawOutliner {
|
||||||
|
newFreeDrawOutline(outline, points, box, scaleFactor, innerMargin, isLTR) {
|
||||||
|
return new FreeHighlightOutline(
|
||||||
|
outline,
|
||||||
|
points,
|
||||||
|
box,
|
||||||
|
scaleFactor,
|
||||||
|
innerMargin,
|
||||||
|
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,
|
||||||
|
box,
|
||||||
|
scaleFactor,
|
||||||
|
thickness,
|
||||||
|
isLTR,
|
||||||
|
innerMargin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FreeHighlightOutliner, HighlightOutliner };
|
||||||
55
src/display/editor/drawers/outline.js
Normal file
55
src/display/editor/drawers/outline.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/* Copyright 2023 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 { unreachable } from "../../../shared/util.js";
|
||||||
|
|
||||||
|
class Outline {
|
||||||
|
/**
|
||||||
|
* @returns {string} The SVG path of the outline.
|
||||||
|
*/
|
||||||
|
toSVGPath() {
|
||||||
|
unreachable("Abstract method `toSVGPath` must be implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Object|null} The bounding box of the outline.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line getter-return
|
||||||
|
get box() {
|
||||||
|
unreachable("Abstract getter `box` must be implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(_bbox, _rotation) {
|
||||||
|
unreachable("Abstract method `serialize` must be implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line getter-return
|
||||||
|
get classNamesForDrawing() {
|
||||||
|
unreachable("Abstract getter `classNamesForDrawing` must be implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line getter-return
|
||||||
|
get classNamesForOutlining() {
|
||||||
|
unreachable(
|
||||||
|
"Abstract getter `classNamesForOutlining` must be implemented."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mustRemoveSelfIntersections() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Outline };
|
||||||
@ -20,7 +20,10 @@ import {
|
|||||||
Util,
|
Util,
|
||||||
} from "../../shared/util.js";
|
} from "../../shared/util.js";
|
||||||
import { bindEvents, KeyboardManager } from "./tools.js";
|
import { bindEvents, KeyboardManager } from "./tools.js";
|
||||||
import { FreeOutliner, Outliner } from "./outliner.js";
|
import {
|
||||||
|
FreeHighlightOutliner,
|
||||||
|
HighlightOutliner,
|
||||||
|
} from "./drawers/highlight.js";
|
||||||
import {
|
import {
|
||||||
HighlightAnnotationElement,
|
HighlightAnnotationElement,
|
||||||
InkAnnotationElement,
|
InkAnnotationElement,
|
||||||
@ -149,7 +152,10 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#createOutlines() {
|
#createOutlines() {
|
||||||
const outliner = new Outliner(this.#boxes, /* borderWidth = */ 0.001);
|
const outliner = new HighlightOutliner(
|
||||||
|
this.#boxes,
|
||||||
|
/* borderWidth = */ 0.001
|
||||||
|
);
|
||||||
this.#highlightOutlines = outliner.getOutlines();
|
this.#highlightOutlines = outliner.getOutlines();
|
||||||
({
|
({
|
||||||
x: this.x,
|
x: this.x,
|
||||||
@ -158,7 +164,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
height: this.height,
|
height: this.height,
|
||||||
} = this.#highlightOutlines.box);
|
} = this.#highlightOutlines.box);
|
||||||
|
|
||||||
const outlinerForOutline = new Outliner(
|
const outlinerForOutline = new HighlightOutliner(
|
||||||
this.#boxes,
|
this.#boxes,
|
||||||
/* borderWidth = */ 0.0025,
|
/* borderWidth = */ 0.0025,
|
||||||
/* innerMargin = */ 0.001,
|
/* innerMargin = */ 0.001,
|
||||||
@ -190,9 +196,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
// We need to redraw the highlight because we change the coordinates to be
|
// We need to redraw the highlight because we change the coordinates to be
|
||||||
// in the box coordinate system.
|
// in the box coordinate system.
|
||||||
this.parent.drawLayer.finalizeLine(highlightId, highlightOutlines);
|
this.parent.drawLayer.finalizeLine(highlightId, highlightOutlines);
|
||||||
this.#outlineId = this.parent.drawLayer.highlightOutline(
|
this.#outlineId = this.parent.drawLayer.drawOutline(this.#focusOutlines);
|
||||||
this.#focusOutlines
|
|
||||||
);
|
|
||||||
} else if (this.parent) {
|
} else if (this.parent) {
|
||||||
const angle = this.parent.viewport.rotation;
|
const angle = this.parent.viewport.rotation;
|
||||||
this.parent.drawLayer.updateLine(this.#id, highlightOutlines);
|
this.parent.drawLayer.updateLine(this.#id, highlightOutlines);
|
||||||
@ -498,13 +502,12 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
if (this.#id !== null) {
|
if (this.#id !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
({ id: this.#id, clipPathId: this.#clipPathId } =
|
({ id: this.#id, clipPathId: this.#clipPathId } = parent.drawLayer.draw(
|
||||||
parent.drawLayer.highlight(
|
this.#highlightOutlines,
|
||||||
this.#highlightOutlines,
|
this.color,
|
||||||
this.color,
|
this.#opacity
|
||||||
this.#opacity
|
));
|
||||||
));
|
this.#outlineId = parent.drawLayer.drawOutline(this.#focusOutlines);
|
||||||
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
|
|
||||||
if (this.#highlightDiv) {
|
if (this.#highlightDiv) {
|
||||||
this.#highlightDiv.style.clipPath = this.#clipPathId;
|
this.#highlightDiv.style.clipPath = this.#clipPathId;
|
||||||
}
|
}
|
||||||
@ -742,7 +745,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
this.#highlightMove.bind(this, parent),
|
this.#highlightMove.bind(this, parent),
|
||||||
{ signal }
|
{ signal }
|
||||||
);
|
);
|
||||||
this._freeHighlight = new FreeOutliner(
|
this._freeHighlight = new FreeHighlightOutliner(
|
||||||
{ x, y },
|
{ x, y },
|
||||||
[layerX, layerY, parentWidth, parentHeight],
|
[layerX, layerY, parentWidth, parentHeight],
|
||||||
parent.scale,
|
parent.scale,
|
||||||
@ -751,7 +754,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
/* innerMargin = */ 0.001
|
/* innerMargin = */ 0.001
|
||||||
);
|
);
|
||||||
({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
|
({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
|
||||||
parent.drawLayer.highlight(
|
parent.drawLayer.draw(
|
||||||
this._freeHighlight,
|
this._freeHighlight,
|
||||||
this._defaultColor,
|
this._defaultColor,
|
||||||
this._defaultOpacity,
|
this._defaultOpacity,
|
||||||
@ -775,7 +778,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
methodOfCreation: "main_toolbar",
|
methodOfCreation: "main_toolbar",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
parent.drawLayer.removeFreeHighlight(this._freeHighlightId);
|
parent.drawLayer.remove(this._freeHighlightId);
|
||||||
}
|
}
|
||||||
this._freeHighlightId = -1;
|
this._freeHighlightId = -1;
|
||||||
this._freeHighlight = null;
|
this._freeHighlight = null;
|
||||||
@ -869,7 +872,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
x: points[0] - pageX,
|
x: points[0] - pageX,
|
||||||
y: pageHeight - (points[1] - pageY),
|
y: pageHeight - (points[1] - pageY),
|
||||||
};
|
};
|
||||||
const outliner = new FreeOutliner(
|
const outliner = new FreeHighlightOutliner(
|
||||||
point,
|
point,
|
||||||
[0, 0, pageWidth, pageHeight],
|
[0, 0, pageWidth, pageHeight],
|
||||||
1,
|
1,
|
||||||
@ -882,7 +885,7 @@ class HighlightEditor extends AnnotationEditor {
|
|||||||
point.y = pageHeight - (points[i + 1] - pageY);
|
point.y = pageHeight - (points[i + 1] - pageY);
|
||||||
outliner.add(point);
|
outliner.add(point);
|
||||||
}
|
}
|
||||||
const { id, clipPathId } = parent.drawLayer.highlight(
|
const { id, clipPathId } = parent.drawLayer.draw(
|
||||||
outliner,
|
outliner,
|
||||||
editor.color,
|
editor.color,
|
||||||
editor._defaultOpacity,
|
editor._defaultOpacity,
|
||||||
|
|||||||
@ -69,7 +69,7 @@ import { AnnotationLayer } from "./display/annotation_layer.js";
|
|||||||
import { ColorPicker } from "./display/editor/color_picker.js";
|
import { ColorPicker } from "./display/editor/color_picker.js";
|
||||||
import { DrawLayer } from "./display/draw_layer.js";
|
import { DrawLayer } from "./display/draw_layer.js";
|
||||||
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
import { GlobalWorkerOptions } from "./display/worker_options.js";
|
||||||
import { Outliner } from "./display/editor/outliner.js";
|
import { HighlightOutliner } from "./display/editor/drawers/highlight.js";
|
||||||
import { TextLayer } from "./display/text_layer.js";
|
import { TextLayer } from "./display/text_layer.js";
|
||||||
import { XfaLayer } from "./display/xfa_layer.js";
|
import { XfaLayer } from "./display/xfa_layer.js";
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ const pdfjsBuild =
|
|||||||
|
|
||||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
globalThis.pdfjsTestingUtils = {
|
globalThis.pdfjsTestingUtils = {
|
||||||
Outliner,
|
HighlightOutliner,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const {
|
|||||||
TextLayer,
|
TextLayer,
|
||||||
XfaLayer,
|
XfaLayer,
|
||||||
} = pdfjsLib;
|
} = pdfjsLib;
|
||||||
const { Outliner } = pdfjsTestingUtils;
|
const { HighlightOutliner } = pdfjsTestingUtils;
|
||||||
const { GenericL10n, parseQueryString, SimpleLinkService } = pdfjsViewer;
|
const { GenericL10n, parseQueryString, SimpleLinkService } = pdfjsViewer;
|
||||||
|
|
||||||
const WAITING_TIME = 100; // ms
|
const WAITING_TIME = 100; // ms
|
||||||
@ -370,19 +370,19 @@ class Rasterize {
|
|||||||
}
|
}
|
||||||
// We set the borderWidth to 0.001 to slighly increase the size of the
|
// We set the borderWidth to 0.001 to slighly increase the size of the
|
||||||
// boxes so that they can be merged together.
|
// boxes so that they can be merged together.
|
||||||
const outliner = new Outliner(boxes, /* borderWidth = */ 0.001);
|
const outliner = new HighlightOutliner(boxes, /* borderWidth = */ 0.001);
|
||||||
// We set the borderWidth to 0.0025 in order to have an outline which is
|
// We set the borderWidth to 0.0025 in order to have an outline which is
|
||||||
// slightly bigger than the highlight itself.
|
// slightly bigger than the highlight itself.
|
||||||
// We must add an inner margin to avoid to have a partial outline.
|
// We must add an inner margin to avoid to have a partial outline.
|
||||||
const outlinerForOutline = new Outliner(
|
const outlinerForOutline = new HighlightOutliner(
|
||||||
boxes,
|
boxes,
|
||||||
/* borderWidth = */ 0.0025,
|
/* borderWidth = */ 0.0025,
|
||||||
/* innerMargin = */ 0.001
|
/* innerMargin = */ 0.001
|
||||||
);
|
);
|
||||||
const drawLayer = new DrawLayer({ pageIndex: 0 });
|
const drawLayer = new DrawLayer({ pageIndex: 0 });
|
||||||
drawLayer.setParent(div);
|
drawLayer.setParent(div);
|
||||||
drawLayer.highlight(outliner.getOutlines(), "orange", 0.4);
|
drawLayer.draw(outliner.getOutlines(), "orange", 0.4);
|
||||||
drawLayer.highlightOutline(outlinerForOutline.getOutlines());
|
drawLayer.drawOutline(outlinerForOutline.getOutlines());
|
||||||
|
|
||||||
svg.append(foreignObject);
|
svg.append(foreignObject);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user