[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()); 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);
}
} }
} }