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()); 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; const { style } = element;
style.top = `${100 * y}%`; style.top = `${100 * y}%`;
style.left = `${100 * x}%`; style.left = `${100 * x}%`;
@ -63,11 +63,10 @@ class DrawLayer {
style.height = `${100 * height}%`; style.height = `${100 * height}%`;
} }
#createSVG(box) { #createSVG() {
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true); const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
this.#parent.append(svg); this.#parent.append(svg);
svg.setAttribute("aria-hidden", true); svg.setAttribute("aria-hidden", true);
DrawLayer.#setBox(svg, box);
return svg; return svg;
} }
@ -86,10 +85,19 @@ class DrawLayer {
return clipPathId; 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 id = this.#id++;
const root = this.#createSVG(outlines.box); const root = this.#createSVG();
root.classList.add(...outlines.classNamesForDrawing);
const defs = DrawLayer._svgFactory.createElement("defs"); const defs = DrawLayer._svgFactory.createElement("defs");
root.append(defs); root.append(defs);
@ -97,45 +105,42 @@ class DrawLayer {
defs.append(path); defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`; const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId); path.setAttribute("id", pathId);
path.setAttribute("d", outlines.toSVGPath()); path.setAttribute("vector-effect", "non-scaling-stroke");
if (isPathUpdatable) { if (isPathUpdatable) {
this.#toUpdate.set(id, path); this.#toUpdate.set(id, path);
} }
// Create the clipping path for the editor div. // 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"); const use = DrawLayer._svgFactory.createElement("use");
root.append(use); root.append(use);
root.setAttribute("fill", color);
root.setAttribute("fill-opacity", opacity);
use.setAttribute("href", `#${pathId}`); use.setAttribute("href", `#${pathId}`);
this.updateProperties(root, properties);
this.#mapping.set(id, root); this.#mapping.set(id, root);
return { id, clipPathId: `url(#${clipPathId})` }; return { id, clipPathId: `url(#${clipPathId})` };
} }
drawOutline(outlines) { drawOutline(properties, mustRemoveSelfIntersections) {
// 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();
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");
defs.append(path); defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`; const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId); path.setAttribute("id", pathId);
path.setAttribute("d", outlines.toSVGPath());
path.setAttribute("vector-effect", "non-scaling-stroke"); path.setAttribute("vector-effect", "non-scaling-stroke");
let maskId; let maskId;
if (outlines.mustRemoveSelfIntersections) { if (mustRemoveSelfIntersections) {
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}`;
@ -166,59 +171,40 @@ class DrawLayer {
use1.classList.add("mainOutline"); use1.classList.add("mainOutline");
use2.classList.add("secondaryOutline"); use2.classList.add("secondaryOutline");
this.updateProperties(root, properties);
this.#mapping.set(id, root); this.#mapping.set(id, root);
return id; return id;
} }
finalizeLine(id, line) { finalizeDraw(id, properties) {
const path = this.#toUpdate.get(id);
this.#toUpdate.delete(id); this.#toUpdate.delete(id);
this.updateBox(id, line.box); this.updateProperties(id, properties);
path.setAttribute("d", line.toSVGPath());
} }
updateLine(id, line) { updateProperties(elementOrId, { root, bbox, rootClass, path }) {
const root = this.#mapping.get(id); const element =
const defs = root.firstChild; typeof elementOrId === "number"
const path = defs.firstChild; ? this.#mapping.get(elementOrId)
path.setAttribute("d", line.toSVGPath()); : elementOrId;
} if (root) {
this.#updateProperties(element, root);
updatePath(id, line) { }
this.#toUpdate.get(id).setAttribute("d", line.toSVGPath()); if (bbox) {
} DrawLayer.#setBox(element, bbox);
}
updateBox(id, box) { if (rootClass) {
DrawLayer.#setBox(this.#mapping.get(id), box); const { classList } = element;
} for (const [className, value] of Object.entries(rootClass)) {
classList.toggle(className, value);
show(id, visible) { }
this.#mapping.get(id).classList.toggle("hidden", !visible); }
} if (path) {
const defs = element.firstChild;
rotate(id, angle) { const pathElement = defs.firstChild;
this.#mapping.get(id).setAttribute("data-main-rotation", angle); this.#updateProperties(pathElement, path);
} }
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);
} }
remove(id) { remove(id) {

View File

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

View File

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

View File

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

View File

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

View File

@ -381,8 +381,40 @@ class Rasterize {
); );
const drawLayer = new DrawLayer({ pageIndex: 0 }); const drawLayer = new DrawLayer({ pageIndex: 0 });
drawLayer.setParent(div); drawLayer.setParent(div);
drawLayer.draw(outliner.getOutlines(), "orange", 0.4); const outlines = outliner.getOutlines();
drawLayer.drawOutline(outlinerForOutline.getOutlines()); drawLayer.draw(
{
bbox: outlines.box,
root: {
viewBox: "0 0 1 1",
fill: "orange",
"fill-opacity": 0.4,
},
rootClass: {
highlight: true,
free: false,
},
path: {
d: outlines.toSVGPath(),
},
},
/* isPathUpdatable = */ false,
/* hasClip = */ true
);
const focusLine = outlinerForOutline.getOutlines();
drawLayer.drawOutline(
{
rootClass: {
highlightOutline: true,
free: false,
},
bbox: focusLine.box,
path: {
d: focusLine.toSVGPath(),
},
},
/* mustRemoveSelfIntersections = */ false
);
svg.append(foreignObject); svg.append(foreignObject);

View File

@ -17,24 +17,27 @@
svg { svg {
transform: none; transform: none;
&[data-main-rotation="90"] { &.highlight,
mask, &.highlightOutline {
use:not(.clip, .mask) { &[data-main-rotation="90"] {
transform: matrix(0, 1, -1, 0, 1, 0); mask,
use:not(.clip, .mask) {
transform: matrix(0, 1, -1, 0, 1, 0);
}
} }
}
&[data-main-rotation="180"] { &[data-main-rotation="180"] {
mask, mask,
use:not(.clip, .mask) { use:not(.clip, .mask) {
transform: matrix(-1, 0, 0, -1, 1, 1); transform: matrix(-1, 0, 0, -1, 1, 1);
}
} }
}
&[data-main-rotation="270"] { &[data-main-rotation="270"] {
mask, mask,
use:not(.clip, .mask) { use:not(.clip, .mask) {
transform: matrix(0, -1, 1, 0, 0, 1); transform: matrix(0, -1, 1, 0, 0, 1);
}
} }
} }