Merge pull request #17506 from calixteman/editor_free_highlight
[Editor] Add the ability to make a free highlight (i.e. without having to select some text) (bug 1856218)
This commit is contained in:
		
						commit
						1cdbcfef82
					
				@ -28,6 +28,8 @@ class DrawLayer {
 | 
			
		||||
 | 
			
		||||
  #mapping = new Map();
 | 
			
		||||
 | 
			
		||||
  #toUpdate = new Map();
 | 
			
		||||
 | 
			
		||||
  constructor({ pageIndex }) {
 | 
			
		||||
    this.pageIndex = pageIndex;
 | 
			
		||||
  }
 | 
			
		||||
@ -53,7 +55,7 @@ class DrawLayer {
 | 
			
		||||
    return shadow(this, "_svgFactory", new DOMSVGFactory());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static #setBox(element, { x, y, width, height }) {
 | 
			
		||||
  static #setBox(element, { x = 0, y = 0, width = 1, height = 1 } = {}) {
 | 
			
		||||
    const { style } = element;
 | 
			
		||||
    style.top = `${100 * y}%`;
 | 
			
		||||
    style.left = `${100 * x}%`;
 | 
			
		||||
@ -83,10 +85,13 @@ class DrawLayer {
 | 
			
		||||
    return clipPathId;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  highlight(outlines, color, opacity) {
 | 
			
		||||
  highlight(outlines, color, opacity, isPathUpdatable = false) {
 | 
			
		||||
    const id = this.#id++;
 | 
			
		||||
    const root = this.#createSVG(outlines.box);
 | 
			
		||||
    root.classList.add("highlight");
 | 
			
		||||
    if (outlines.free) {
 | 
			
		||||
      root.classList.add("free");
 | 
			
		||||
    }
 | 
			
		||||
    const defs = DrawLayer._svgFactory.createElement("defs");
 | 
			
		||||
    root.append(defs);
 | 
			
		||||
    const path = DrawLayer._svgFactory.createElement("path");
 | 
			
		||||
@ -95,6 +100,10 @@ class DrawLayer {
 | 
			
		||||
    path.setAttribute("id", pathId);
 | 
			
		||||
    path.setAttribute("d", outlines.toSVGPath());
 | 
			
		||||
 | 
			
		||||
    if (isPathUpdatable) {
 | 
			
		||||
      this.#toUpdate.set(id, path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create the clipping path for the editor div.
 | 
			
		||||
    const clipPathId = this.#createClipPath(defs, pathId);
 | 
			
		||||
 | 
			
		||||
@ -139,6 +148,22 @@ class DrawLayer {
 | 
			
		||||
    return id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  finalizeLine(id, line) {
 | 
			
		||||
    const path = this.#toUpdate.get(id);
 | 
			
		||||
    this.#toUpdate.delete(id);
 | 
			
		||||
    this.updateBox(id, line.box);
 | 
			
		||||
    path.setAttribute("d", line.toSVGPath());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeFreeHighlight(id) {
 | 
			
		||||
    this.remove(id);
 | 
			
		||||
    this.#toUpdate.delete(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updatePath(id, line) {
 | 
			
		||||
    this.#toUpdate.get(id).setAttribute("d", line.toSVGPath());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateBox(id, box) {
 | 
			
		||||
    DrawLayer.#setBox(this.#mapping.get(id), box);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,8 @@ class AnnotationEditorLayer {
 | 
			
		||||
 | 
			
		||||
  #boundPointerdown = this.pointerdown.bind(this);
 | 
			
		||||
 | 
			
		||||
  #boundTextLayerPointerDown = this.#textLayerPointerDown.bind(this);
 | 
			
		||||
 | 
			
		||||
  #editorFocusTimeoutId = null;
 | 
			
		||||
 | 
			
		||||
  #boundSelectionStart = this.selectionStart.bind(this);
 | 
			
		||||
@ -199,7 +201,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const editor = this.#createAndAddNewEditor(
 | 
			
		||||
    const editor = this.createAndAddNewEditor(
 | 
			
		||||
      { offsetX: 0, offsetY: 0 },
 | 
			
		||||
      /* isCentered = */ false
 | 
			
		||||
    );
 | 
			
		||||
@ -328,12 +330,34 @@ class AnnotationEditorLayer {
 | 
			
		||||
  enableTextSelection() {
 | 
			
		||||
    if (this.#textLayer?.div) {
 | 
			
		||||
      document.addEventListener("selectstart", this.#boundSelectionStart);
 | 
			
		||||
      this.#textLayer.div.addEventListener(
 | 
			
		||||
        "pointerdown",
 | 
			
		||||
        this.#boundTextLayerPointerDown
 | 
			
		||||
      );
 | 
			
		||||
      this.#textLayer.div.classList.add("drawing");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  disableTextSelection() {
 | 
			
		||||
    if (this.#textLayer?.div) {
 | 
			
		||||
      document.removeEventListener("selectstart", this.#boundSelectionStart);
 | 
			
		||||
      this.#textLayer.div.removeEventListener(
 | 
			
		||||
        "pointerdown",
 | 
			
		||||
        this.#boundTextLayerPointerDown
 | 
			
		||||
      );
 | 
			
		||||
      this.#textLayer.div.classList.remove("drawing");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #textLayerPointerDown(event) {
 | 
			
		||||
    if (event.target === this.#textLayer.div) {
 | 
			
		||||
      const { isMac } = FeatureTest.platform;
 | 
			
		||||
      if (event.button !== 0 || (event.ctrlKey && isMac)) {
 | 
			
		||||
        // Do nothing on right click.
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      HighlightEditor.startHighlighting(this, event);
 | 
			
		||||
      event.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -565,7 +589,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
   * @param [Object] data
 | 
			
		||||
   * @returns {AnnotationEditor}
 | 
			
		||||
   */
 | 
			
		||||
  #createAndAddNewEditor(event, isCentered, data = {}) {
 | 
			
		||||
  createAndAddNewEditor(event, isCentered, data = {}) {
 | 
			
		||||
    const id = this.getNextId();
 | 
			
		||||
    const editor = this.#createNewEditor({
 | 
			
		||||
      parent: this,
 | 
			
		||||
@ -603,10 +627,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
   * Create and add a new editor.
 | 
			
		||||
   */
 | 
			
		||||
  addNewEditor() {
 | 
			
		||||
    this.#createAndAddNewEditor(
 | 
			
		||||
      this.#getCenterPoint(),
 | 
			
		||||
      /* isCentered = */ true
 | 
			
		||||
    );
 | 
			
		||||
    this.createAndAddNewEditor(this.#getCenterPoint(), /* isCentered = */ true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -726,7 +747,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
      boxes.push(rotator(x, y, width, height));
 | 
			
		||||
    }
 | 
			
		||||
    if (boxes.length !== 0) {
 | 
			
		||||
      this.#createAndAddNewEditor(event, false, {
 | 
			
		||||
      this.createAndAddNewEditor(event, false, {
 | 
			
		||||
        boxes,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
@ -767,7 +788,7 @@ class AnnotationEditorLayer {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.#createAndAddNewEditor(event, /* isCentered = */ false);
 | 
			
		||||
    this.createAndAddNewEditor(event, /* isCentered = */ false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -901,6 +922,10 @@ class AnnotationEditorLayer {
 | 
			
		||||
    const { pageWidth, pageHeight } = this.viewport.rawDims;
 | 
			
		||||
    return [pageWidth, pageHeight];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get scale() {
 | 
			
		||||
    return this.#uiManager.viewParameters.realScale;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { AnnotationEditorLayer };
 | 
			
		||||
 | 
			
		||||
@ -18,10 +18,11 @@ import {
 | 
			
		||||
  AnnotationEditorType,
 | 
			
		||||
  Util,
 | 
			
		||||
} from "../../shared/util.js";
 | 
			
		||||
import { FreeOutliner, Outliner } from "./outliner.js";
 | 
			
		||||
import { AnnotationEditor } from "./editor.js";
 | 
			
		||||
import { bindEvents } from "./tools.js";
 | 
			
		||||
import { ColorPicker } from "./color_picker.js";
 | 
			
		||||
import { Outliner } from "./outliner.js";
 | 
			
		||||
import { noContextMenu } from "../display_utils.js";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Basic draw editor in order to generate an Highlight annotation.
 | 
			
		||||
@ -41,6 +42,8 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  #id = null;
 | 
			
		||||
 | 
			
		||||
  #isFreeHighlight = false;
 | 
			
		||||
 | 
			
		||||
  #lastPoint = null;
 | 
			
		||||
 | 
			
		||||
  #opacity;
 | 
			
		||||
@ -51,12 +54,20 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  static _defaultOpacity = 1;
 | 
			
		||||
 | 
			
		||||
  static _defaultThickness = 10;
 | 
			
		||||
 | 
			
		||||
  static _l10nPromise;
 | 
			
		||||
 | 
			
		||||
  static _type = "highlight";
 | 
			
		||||
 | 
			
		||||
  static _editorType = AnnotationEditorType.HIGHLIGHT;
 | 
			
		||||
 | 
			
		||||
  static _freeHighlightId = -1;
 | 
			
		||||
 | 
			
		||||
  static _freeHighlight = null;
 | 
			
		||||
 | 
			
		||||
  static _freeHighlightClipId = "";
 | 
			
		||||
 | 
			
		||||
  constructor(params) {
 | 
			
		||||
    super({ ...params, name: "highlightEditor" });
 | 
			
		||||
    this.color = params.color || HighlightEditor._defaultColor;
 | 
			
		||||
@ -64,9 +75,15 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
    this.#boxes = params.boxes || null;
 | 
			
		||||
    this._isDraggable = false;
 | 
			
		||||
 | 
			
		||||
    this.#createOutlines();
 | 
			
		||||
    this.#addToDrawLayer();
 | 
			
		||||
    this.rotate(this.rotation);
 | 
			
		||||
    if (params.highlightId > -1) {
 | 
			
		||||
      this.#isFreeHighlight = true;
 | 
			
		||||
      this.#createFreeOutlines(params);
 | 
			
		||||
      this.#addToDrawLayer();
 | 
			
		||||
    } else {
 | 
			
		||||
      this.#createOutlines();
 | 
			
		||||
      this.#addToDrawLayer();
 | 
			
		||||
      this.rotate(this.rotation);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #createOutlines() {
 | 
			
		||||
@ -95,6 +112,60 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #createFreeOutlines({ highlight, highlightId, clipPathId }) {
 | 
			
		||||
    this.#highlightOutlines = highlight.getOutlines(
 | 
			
		||||
      this._uiManager.direction === "ltr"
 | 
			
		||||
    );
 | 
			
		||||
    this.#id = highlightId;
 | 
			
		||||
    this.#clipPathId = clipPathId;
 | 
			
		||||
    const { x, y, width, height, lastPoint } = this.#highlightOutlines.box;
 | 
			
		||||
 | 
			
		||||
    // We need to redraw the highlight because we change the coordinates to be
 | 
			
		||||
    // in the box coordinate system.
 | 
			
		||||
    this.parent.drawLayer.finalizeLine(this.#id, this.#highlightOutlines);
 | 
			
		||||
    switch (this.rotation) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        this.x = x;
 | 
			
		||||
        this.y = y;
 | 
			
		||||
        this.width = width;
 | 
			
		||||
        this.height = height;
 | 
			
		||||
        break;
 | 
			
		||||
      case 90: {
 | 
			
		||||
        const [pageWidth, pageHeight] = this.parentDimensions;
 | 
			
		||||
        this.x = y;
 | 
			
		||||
        this.y = 1 - x;
 | 
			
		||||
        this.width = (width * pageHeight) / pageWidth;
 | 
			
		||||
        this.height = (height * pageWidth) / pageHeight;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 180:
 | 
			
		||||
        this.x = 1 - x;
 | 
			
		||||
        this.y = 1 - y;
 | 
			
		||||
        this.width = width;
 | 
			
		||||
        this.height = height;
 | 
			
		||||
        break;
 | 
			
		||||
      case 270: {
 | 
			
		||||
        const [pageWidth, pageHeight] = this.parentDimensions;
 | 
			
		||||
        this.x = 1 - y;
 | 
			
		||||
        this.y = x;
 | 
			
		||||
        this.width = (width * pageHeight) / pageWidth;
 | 
			
		||||
        this.height = (height * pageWidth) / pageHeight;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const innerMargin = 1.5;
 | 
			
		||||
    this.#focusOutlines = highlight.getFocusOutline(
 | 
			
		||||
      /* Slightly bigger than the highlight in order to have a little
 | 
			
		||||
         space between the highlight and the outline. */
 | 
			
		||||
      HighlightEditor._defaultThickness + innerMargin
 | 
			
		||||
    );
 | 
			
		||||
    this.#outlineId = this.parent.drawLayer.highlightOutline(
 | 
			
		||||
      this.#focusOutlines
 | 
			
		||||
    );
 | 
			
		||||
    this.#lastPoint = lastPoint;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static initialize(l10n, uiManager) {
 | 
			
		||||
    AnnotationEditor.initialize(l10n, uiManager);
 | 
			
		||||
    HighlightEditor._defaultColor ||=
 | 
			
		||||
@ -196,12 +267,12 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  fixAndSetPosition() {
 | 
			
		||||
    return super.fixAndSetPosition(0);
 | 
			
		||||
    return super.fixAndSetPosition(this.#getRotation());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  getRect(tx, ty) {
 | 
			
		||||
    return super.getRect(tx, ty, 0);
 | 
			
		||||
    return super.getRect(tx, ty, this.#getRotation());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
@ -229,7 +300,7 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
    this.#addToDrawLayer();
 | 
			
		||||
 | 
			
		||||
    if (!this.isAttachedToDOM) {
 | 
			
		||||
      // At some point this editor was removed and we're rebuilting it,
 | 
			
		||||
      // At some point this editor was removed and we're rebuilding it,
 | 
			
		||||
      // hence we must add it to its parent.
 | 
			
		||||
      this.parent.add(this);
 | 
			
		||||
    }
 | 
			
		||||
@ -273,10 +344,10 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
        this.color,
 | 
			
		||||
        this.#opacity
 | 
			
		||||
      ));
 | 
			
		||||
    this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
 | 
			
		||||
    if (this.#highlightDiv) {
 | 
			
		||||
      this.#highlightDiv.style.clipPath = this.#clipPathId;
 | 
			
		||||
    }
 | 
			
		||||
    this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static #rotateBbox({ x, y, width, height }, angle) {
 | 
			
		||||
@ -313,10 +384,19 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
  rotate(angle) {
 | 
			
		||||
    // We need to rotate the svgs because of the coordinates system.
 | 
			
		||||
    const { drawLayer } = this.parent;
 | 
			
		||||
    let box;
 | 
			
		||||
    if (this.#isFreeHighlight) {
 | 
			
		||||
      angle = (angle - this.rotation + 360) % 360;
 | 
			
		||||
      box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle);
 | 
			
		||||
    } else {
 | 
			
		||||
      // An highlight annotation is always drawn horizontally.
 | 
			
		||||
      box = HighlightEditor.#rotateBbox(this, angle);
 | 
			
		||||
    }
 | 
			
		||||
    drawLayer.rotate(this.#id, angle);
 | 
			
		||||
    drawLayer.rotate(this.#outlineId, angle);
 | 
			
		||||
    drawLayer.updateBox(this.#id, HighlightEditor.#rotateBbox(this, angle));
 | 
			
		||||
    drawLayer.updateBox(this.#id, box);
 | 
			
		||||
    drawLayer.updateBox(
 | 
			
		||||
      this.#outlineId,
 | 
			
		||||
      HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
 | 
			
		||||
@ -330,6 +410,9 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const div = super.render();
 | 
			
		||||
    if (this.#isFreeHighlight) {
 | 
			
		||||
      div.classList.add("free");
 | 
			
		||||
    }
 | 
			
		||||
    const highlightDiv = (this.#highlightDiv = document.createElement("div"));
 | 
			
		||||
    div.append(highlightDiv);
 | 
			
		||||
    highlightDiv.className = "internal";
 | 
			
		||||
@ -364,7 +447,16 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
    this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #getRotation() {
 | 
			
		||||
    // Highlight annotations are always drawn horizontally but if
 | 
			
		||||
    // a free highlight annotation can be rotated.
 | 
			
		||||
    return this.#isFreeHighlight ? this.rotation : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #serializeBoxes(rect) {
 | 
			
		||||
    if (this.#isFreeHighlight) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    const [pageWidth, pageHeight] = this.pageDimensions;
 | 
			
		||||
    const boxes = this.#boxes;
 | 
			
		||||
    const quadPoints = new Array(boxes.length * 8);
 | 
			
		||||
@ -387,7 +479,79 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #serializeOutlines(rect) {
 | 
			
		||||
    return this.#highlightOutlines.serialize(rect, 0);
 | 
			
		||||
    return this.#highlightOutlines.serialize(rect, this.#getRotation());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static startHighlighting(parent, { target: textLayer, x, y }) {
 | 
			
		||||
    const {
 | 
			
		||||
      x: layerX,
 | 
			
		||||
      y: layerY,
 | 
			
		||||
      width: parentWidth,
 | 
			
		||||
      height: parentHeight,
 | 
			
		||||
    } = textLayer.getBoundingClientRect();
 | 
			
		||||
    const pointerMove = e => {
 | 
			
		||||
      this.#highlightMove(parent, e);
 | 
			
		||||
    };
 | 
			
		||||
    const pointerDownOptions = { capture: true, passive: false };
 | 
			
		||||
    const pointerDown = e => {
 | 
			
		||||
      // Avoid to have undesired clicks during the drawing.
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
    };
 | 
			
		||||
    const pointerUpCallback = e => {
 | 
			
		||||
      textLayer.removeEventListener("pointermove", pointerMove);
 | 
			
		||||
      window.removeEventListener("blur", pointerUpCallback);
 | 
			
		||||
      window.removeEventListener("pointerup", pointerUpCallback);
 | 
			
		||||
      window.removeEventListener(
 | 
			
		||||
        "pointerdown",
 | 
			
		||||
        pointerDown,
 | 
			
		||||
        pointerDownOptions
 | 
			
		||||
      );
 | 
			
		||||
      window.removeEventListener("contextmenu", noContextMenu);
 | 
			
		||||
      this.#endHighlight(parent, e);
 | 
			
		||||
    };
 | 
			
		||||
    window.addEventListener("blur", pointerUpCallback);
 | 
			
		||||
    window.addEventListener("pointerup", pointerUpCallback);
 | 
			
		||||
    window.addEventListener("pointerdown", pointerDown, pointerDownOptions);
 | 
			
		||||
    window.addEventListener("contextmenu", noContextMenu);
 | 
			
		||||
 | 
			
		||||
    textLayer.addEventListener("pointermove", pointerMove);
 | 
			
		||||
    this._freeHighlight = new FreeOutliner(
 | 
			
		||||
      { x, y },
 | 
			
		||||
      [layerX, layerY, parentWidth, parentHeight],
 | 
			
		||||
      parent.scale,
 | 
			
		||||
      this._defaultThickness,
 | 
			
		||||
      /* innerMargin = */ 0.001
 | 
			
		||||
    );
 | 
			
		||||
    ({ id: this._freeHighlightId, clipPathId: this._freeHighlightClipId } =
 | 
			
		||||
      parent.drawLayer.highlight(
 | 
			
		||||
        this._freeHighlight,
 | 
			
		||||
        this._defaultColor,
 | 
			
		||||
        this._defaultOpacity,
 | 
			
		||||
        /* isPathUpdatable = */ true
 | 
			
		||||
      ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static #highlightMove(parent, event) {
 | 
			
		||||
    if (this._freeHighlight.add(event)) {
 | 
			
		||||
      // Redraw only if the point has been added.
 | 
			
		||||
      parent.drawLayer.updatePath(this._freeHighlightId, this._freeHighlight);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static #endHighlight(parent, event) {
 | 
			
		||||
    if (!this._freeHighlight.isEmpty()) {
 | 
			
		||||
      parent.createAndAddNewEditor(event, false, {
 | 
			
		||||
        highlightId: this._freeHighlightId,
 | 
			
		||||
        highlight: this._freeHighlight,
 | 
			
		||||
        clipPathId: this._freeHighlightClipId,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      parent.drawLayer.removeFreeHighlight(this._freeHighlightId);
 | 
			
		||||
    }
 | 
			
		||||
    this._freeHighlightId = -1;
 | 
			
		||||
    this._freeHighlight = null;
 | 
			
		||||
    this._freeHighlightClipId = "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @inheritdoc */
 | 
			
		||||
@ -437,7 +601,7 @@ class HighlightEditor extends AnnotationEditor {
 | 
			
		||||
      outlines: this.#serializeOutlines(rect),
 | 
			
		||||
      pageIndex: this.pageIndex,
 | 
			
		||||
      rect,
 | 
			
		||||
      rotation: 0,
 | 
			
		||||
      rotation: this.#getRotation(),
 | 
			
		||||
      structTreeParentId: this._structTreeParentId,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,8 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Util } from "../../shared/util.js";
 | 
			
		||||
 | 
			
		||||
class Outliner {
 | 
			
		||||
  #box;
 | 
			
		||||
 | 
			
		||||
@ -260,10 +262,16 @@ class Outliner {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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.");
 | 
			
		||||
  }
 | 
			
		||||
@ -271,6 +279,10 @@ class Outline {
 | 
			
		||||
  serialize(_bbox, _rotation) {
 | 
			
		||||
    throw new Error("Abstract method `serialize` must be implemented.");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get free() {
 | 
			
		||||
    return this instanceof FreeHighlightOutline;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HighlightOutline extends Outline {
 | 
			
		||||
@ -331,4 +343,469 @@ class HighlightOutline extends Outline {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Outliner };
 | 
			
		||||
class FreeOutliner {
 | 
			
		||||
  #box;
 | 
			
		||||
 | 
			
		||||
  #bottom = [];
 | 
			
		||||
 | 
			
		||||
  #innerMargin;
 | 
			
		||||
 | 
			
		||||
  #top = [];
 | 
			
		||||
 | 
			
		||||
  // The first 6 elements are the last 3 points of the top part of the outline.
 | 
			
		||||
  // The next 6 elements are the last 3 points of the line.
 | 
			
		||||
  // The next 6 elements are the last 3 points of the bottom part of the
 | 
			
		||||
  // outline.
 | 
			
		||||
  // We track the last 3 points in order to be able to:
 | 
			
		||||
  //  - compute the normal of the line,
 | 
			
		||||
  //  - compute the control points of the quadratic Bézier curve.
 | 
			
		||||
  #last = new Float64Array(18);
 | 
			
		||||
 | 
			
		||||
  #min;
 | 
			
		||||
 | 
			
		||||
  #min_dist;
 | 
			
		||||
 | 
			
		||||
  #scaleFactor;
 | 
			
		||||
 | 
			
		||||
  #thickness;
 | 
			
		||||
 | 
			
		||||
  #points = [];
 | 
			
		||||
 | 
			
		||||
  static #MIN_DIST = 8;
 | 
			
		||||
 | 
			
		||||
  static #MIN_DIFF = 2;
 | 
			
		||||
 | 
			
		||||
  static #MIN = FreeOutliner.#MIN_DIST + FreeOutliner.#MIN_DIFF;
 | 
			
		||||
 | 
			
		||||
  constructor({ x, y }, box, scaleFactor, thickness, innerMargin = 0) {
 | 
			
		||||
    this.#box = box;
 | 
			
		||||
    this.#thickness = thickness * scaleFactor;
 | 
			
		||||
    this.#last.set([NaN, NaN, NaN, NaN, x, y], 6);
 | 
			
		||||
    this.#innerMargin = innerMargin;
 | 
			
		||||
    this.#min_dist = FreeOutliner.#MIN_DIST * scaleFactor;
 | 
			
		||||
    this.#min = FreeOutliner.#MIN * scaleFactor;
 | 
			
		||||
    this.#scaleFactor = scaleFactor;
 | 
			
		||||
    this.#points.push(x, y);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get free() {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isEmpty() {
 | 
			
		||||
    // 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
 | 
			
		||||
    // at index 8 means that we've only one point.
 | 
			
		||||
    return isNaN(this.#last[8]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  add({ x, y }) {
 | 
			
		||||
    const [layerX, layerY, layerWidth, layerHeight] = this.#box;
 | 
			
		||||
    let [x1, y1, x2, y2] = this.#last.subarray(8, 12);
 | 
			
		||||
    const diffX = x - x2;
 | 
			
		||||
    const diffY = y - y2;
 | 
			
		||||
    const d = Math.hypot(diffX, diffY);
 | 
			
		||||
    if (d < this.#min) {
 | 
			
		||||
      // The idea is to avoid garbage points around the last point.
 | 
			
		||||
      // When the points are too close, it just leads to bad normal vectors and
 | 
			
		||||
      // control points.
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const diffD = d - this.#min_dist;
 | 
			
		||||
    const K = diffD / d;
 | 
			
		||||
    const shiftX = K * diffX;
 | 
			
		||||
    const shiftY = K * diffY;
 | 
			
		||||
 | 
			
		||||
    // We update the last 3 points of the line.
 | 
			
		||||
    let x0 = x1;
 | 
			
		||||
    let y0 = y1;
 | 
			
		||||
    x1 = x2;
 | 
			
		||||
    y1 = y2;
 | 
			
		||||
    x2 += shiftX;
 | 
			
		||||
    y2 += shiftY;
 | 
			
		||||
 | 
			
		||||
    // We keep track of the points in order to be able to compute the focus
 | 
			
		||||
    // outline.
 | 
			
		||||
    this.#points?.push(x, y);
 | 
			
		||||
 | 
			
		||||
    // Create the normal unit vector.
 | 
			
		||||
    // |(shiftX, shiftY)| = |K| * |(diffX, diffY)| = |K| * d = diffD.
 | 
			
		||||
    const nX = -shiftY / diffD;
 | 
			
		||||
    const nY = shiftX / diffD;
 | 
			
		||||
    const thX = nX * this.#thickness;
 | 
			
		||||
    const thY = nY * this.#thickness;
 | 
			
		||||
    this.#last.set(this.#last.subarray(2, 8), 0);
 | 
			
		||||
    this.#last.set([x2 + thX, y2 + thY], 4);
 | 
			
		||||
    this.#last.set(this.#last.subarray(14, 18), 12);
 | 
			
		||||
    this.#last.set([x2 - thX, y2 - thY], 16);
 | 
			
		||||
 | 
			
		||||
    if (isNaN(this.#last[6])) {
 | 
			
		||||
      if (this.#top.length === 0) {
 | 
			
		||||
        this.#last.set([x1 + thX, y1 + thY], 2);
 | 
			
		||||
        this.#top.push(
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          (x1 + thX - layerX) / layerWidth,
 | 
			
		||||
          (y1 + thY - layerY) / layerHeight
 | 
			
		||||
        );
 | 
			
		||||
        this.#last.set([x1 - thX, y1 - thY], 14);
 | 
			
		||||
        this.#bottom.push(
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          NaN,
 | 
			
		||||
          (x1 - thX - layerX) / layerWidth,
 | 
			
		||||
          (y1 - thY - layerY) / layerHeight
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      this.#last.set([x0, y0, x1, y1, x2, y2], 6);
 | 
			
		||||
      return !this.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.#last.set([x0, y0, x1, y1, x2, y2], 6);
 | 
			
		||||
 | 
			
		||||
    const angle = Math.abs(
 | 
			
		||||
      Math.atan2(y0 - y1, x0 - x1) - Math.atan2(shiftY, shiftX)
 | 
			
		||||
    );
 | 
			
		||||
    if (angle < Math.PI / 2) {
 | 
			
		||||
      // In order to avoid some possible artifacts, we're going to use the a
 | 
			
		||||
      // straight line instead of a quadratic Bézier curve.
 | 
			
		||||
      [x1, y1, x2, y2] = this.#last.subarray(2, 6);
 | 
			
		||||
      this.#top.push(
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        ((x1 + x2) / 2 - layerX) / layerWidth,
 | 
			
		||||
        ((y1 + y2) / 2 - layerY) / layerHeight
 | 
			
		||||
      );
 | 
			
		||||
      [x1, y1, x0, y0] = this.#last.subarray(14, 18);
 | 
			
		||||
      this.#bottom.push(
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        ((x0 + x1) / 2 - layerX) / layerWidth,
 | 
			
		||||
        ((y0 + y1) / 2 - layerY) / layerHeight
 | 
			
		||||
      );
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Control points and the final point for the quadratic Bézier curve.
 | 
			
		||||
    [x0, y0, x1, y1, x2, y2] = this.#last.subarray(0, 6);
 | 
			
		||||
    this.#top.push(
 | 
			
		||||
      ((x0 + 5 * x1) / 6 - layerX) / layerWidth,
 | 
			
		||||
      ((y0 + 5 * y1) / 6 - layerY) / layerHeight,
 | 
			
		||||
      ((5 * x1 + x2) / 6 - layerX) / layerWidth,
 | 
			
		||||
      ((5 * y1 + y2) / 6 - layerY) / layerHeight,
 | 
			
		||||
      ((x1 + x2) / 2 - layerX) / layerWidth,
 | 
			
		||||
      ((y1 + y2) / 2 - layerY) / layerHeight
 | 
			
		||||
    );
 | 
			
		||||
    [x2, y2, x1, y1, x0, y0] = this.#last.subarray(12, 18);
 | 
			
		||||
    this.#bottom.push(
 | 
			
		||||
      ((x0 + 5 * x1) / 6 - layerX) / layerWidth,
 | 
			
		||||
      ((y0 + 5 * y1) / 6 - layerY) / layerHeight,
 | 
			
		||||
      ((5 * x1 + x2) / 6 - layerX) / layerWidth,
 | 
			
		||||
      ((5 * y1 + y2) / 6 - layerY) / layerHeight,
 | 
			
		||||
      ((x1 + x2) / 2 - layerX) / layerWidth,
 | 
			
		||||
      ((y1 + y2) / 2 - layerY) / layerHeight
 | 
			
		||||
    );
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toSVGPath() {
 | 
			
		||||
    if (this.isEmpty()) {
 | 
			
		||||
      // We've only one point.
 | 
			
		||||
      return "";
 | 
			
		||||
    }
 | 
			
		||||
    const top = this.#top;
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
    if (isNaN(this.#last[6]) && !this.isEmpty()) {
 | 
			
		||||
      // We've only two points.
 | 
			
		||||
      return `M${(this.#last[2] - x) / width} ${
 | 
			
		||||
        (this.#last[3] - y) / height
 | 
			
		||||
      } L${(this.#last[4] - x) / width} ${(this.#last[5] - y) / height} 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 = [];
 | 
			
		||||
    buffer.push(`M${top[4]} ${top[5]}`);
 | 
			
		||||
    for (let i = 6; i < top.length; i += 6) {
 | 
			
		||||
      if (isNaN(top[i])) {
 | 
			
		||||
        buffer.push(`L${top[i + 4]} ${top[i + 5]}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        buffer.push(
 | 
			
		||||
          `C${top[i]} ${top[i + 1]} ${top[i + 2]} ${top[i + 3]} ${top[i + 4]} ${
 | 
			
		||||
            top[i + 5]
 | 
			
		||||
          }`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    buffer.push(
 | 
			
		||||
      `L${(lastTop[0] - x) / width} ${(lastTop[1] - y) / height} L${
 | 
			
		||||
        (lastBottom[0] - x) / width
 | 
			
		||||
      } ${(lastBottom[1] - y) / height}`
 | 
			
		||||
    );
 | 
			
		||||
    for (let i = bottom.length - 6; i >= 6; i -= 6) {
 | 
			
		||||
      if (isNaN(bottom[i])) {
 | 
			
		||||
        buffer.push(`L${bottom[i + 4]} ${bottom[i + 5]}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        buffer.push(
 | 
			
		||||
          `C${bottom[i]} ${bottom[i + 1]} ${bottom[i + 2]} ${bottom[i + 3]} ${
 | 
			
		||||
            bottom[i + 4]
 | 
			
		||||
          } ${bottom[i + 5]}`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    buffer.push(`L${bottom[4]} ${bottom[5]} Z`);
 | 
			
		||||
 | 
			
		||||
    return buffer.join(" ");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getFocusOutline(thickness) {
 | 
			
		||||
    // Build the outline of the highlight to use as the focus outline.
 | 
			
		||||
    const [x, y] = this.#points;
 | 
			
		||||
    const outliner = new FreeOutliner(
 | 
			
		||||
      { x, y },
 | 
			
		||||
      this.#box,
 | 
			
		||||
      this.#scaleFactor,
 | 
			
		||||
      thickness,
 | 
			
		||||
      /* innerMargin = */ 0.0025
 | 
			
		||||
    );
 | 
			
		||||
    outliner.#points = null;
 | 
			
		||||
    for (let i = 2; i < this.#points.length; i += 2) {
 | 
			
		||||
      outliner.add({ x: this.#points[i], y: this.#points[i + 1] });
 | 
			
		||||
    }
 | 
			
		||||
    return outliner.getOutlines();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOutlines(isLTR) {
 | 
			
		||||
    const top = this.#top;
 | 
			
		||||
    const bottom = this.#bottom;
 | 
			
		||||
    const last = this.#last;
 | 
			
		||||
    const lastTop = last.subarray(4, 6);
 | 
			
		||||
    const lastBottom = last.subarray(16, 18);
 | 
			
		||||
    const [layerX, layerY, layerWidth, layerHeight] = this.#box;
 | 
			
		||||
 | 
			
		||||
    if (isNaN(last[6]) && !this.isEmpty()) {
 | 
			
		||||
      // We've only two points.
 | 
			
		||||
      const outline = new Float64Array(24);
 | 
			
		||||
      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,
 | 
			
		||||
          (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, this.#innerMargin, isLTR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const outline = new Float64Array(
 | 
			
		||||
      this.#top.length + 12 + this.#bottom.length
 | 
			
		||||
    );
 | 
			
		||||
    let N = top.length;
 | 
			
		||||
    for (let i = 0; i < N; i += 2) {
 | 
			
		||||
      if (isNaN(top[i])) {
 | 
			
		||||
        outline[i] = outline[i + 1] = NaN;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      outline[i] = top[i];
 | 
			
		||||
      outline[i + 1] = top[i + 1];
 | 
			
		||||
    }
 | 
			
		||||
    outline.set(
 | 
			
		||||
      [
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        (lastTop[0] - layerX) / layerWidth,
 | 
			
		||||
        (lastTop[1] - layerY) / layerHeight,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        NaN,
 | 
			
		||||
        (lastBottom[0] - layerX) / layerWidth,
 | 
			
		||||
        (lastBottom[1] - layerY) / layerHeight,
 | 
			
		||||
      ],
 | 
			
		||||
      N
 | 
			
		||||
    );
 | 
			
		||||
    N += 12;
 | 
			
		||||
 | 
			
		||||
    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, this.#innerMargin, isLTR);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FreeHighlightOutline extends Outline {
 | 
			
		||||
  #bbox = null;
 | 
			
		||||
 | 
			
		||||
  #innerMargin;
 | 
			
		||||
 | 
			
		||||
  #outline;
 | 
			
		||||
 | 
			
		||||
  constructor(outline, innerMargin, isLTR) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.#outline = outline;
 | 
			
		||||
    this.#innerMargin = innerMargin;
 | 
			
		||||
    this.#computeMinMax(isLTR);
 | 
			
		||||
 | 
			
		||||
    const { x, y, width, height } = this.#bbox;
 | 
			
		||||
    for (let i = 0, ii = outline.length; i < ii; i += 2) {
 | 
			
		||||
      outline[i] = (outline[i] - x) / width;
 | 
			
		||||
      outline[i + 1] = (outline[i + 1] - y) / height;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toSVGPath() {
 | 
			
		||||
    const buffer = [`M${this.#outline[4]} ${this.#outline[5]}`];
 | 
			
		||||
    for (let i = 6, ii = this.#outline.length; i < ii; i += 6) {
 | 
			
		||||
      if (isNaN(this.#outline[i])) {
 | 
			
		||||
        buffer.push(`L${this.#outline[i + 4]} ${this.#outline[i + 5]}`);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      buffer.push(
 | 
			
		||||
        `C${this.#outline[i]} ${this.#outline[i + 1]} ${this.#outline[i + 2]} ${
 | 
			
		||||
          this.#outline[i + 3]
 | 
			
		||||
        } ${this.#outline[i + 4]} ${this.#outline[i + 5]}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    buffer.push("Z");
 | 
			
		||||
    return buffer.join(" ");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  serialize([blX, blY, trX, trY], rotation) {
 | 
			
		||||
    const src = this.#outline;
 | 
			
		||||
    const outline = new Float64Array(src.length);
 | 
			
		||||
    const width = trX - blX;
 | 
			
		||||
    const height = trY - blY;
 | 
			
		||||
    switch (rotation) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        for (let i = 0, ii = src.length; i < ii; i += 2) {
 | 
			
		||||
          outline[i] = blX + src[i] * width;
 | 
			
		||||
          outline[i + 1] = trY - src[i + 1] * height;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 90:
 | 
			
		||||
        for (let i = 0, ii = src.length; i < ii; i += 2) {
 | 
			
		||||
          outline[i] = blX + src[i + 1] * width;
 | 
			
		||||
          outline[i + 1] = blY + src[i] * height;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 180:
 | 
			
		||||
        for (let i = 0, ii = src.length; i < ii; i += 2) {
 | 
			
		||||
          outline[i] = trX - src[i] * width;
 | 
			
		||||
          outline[i + 1] = blY + src[i + 1] * height;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 270:
 | 
			
		||||
        for (let i = 0, ii = src.length; i < ii; i += 2) {
 | 
			
		||||
          outline[i] = trX - src[i + 1] * width;
 | 
			
		||||
          outline[i + 1] = trY - src[i] * height;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return outline;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #computeMinMax(isLTR) {
 | 
			
		||||
    const outline = this.#outline;
 | 
			
		||||
    let lastX = outline[4];
 | 
			
		||||
    let lastY = outline[5];
 | 
			
		||||
    let minX = lastX;
 | 
			
		||||
    let minY = lastY;
 | 
			
		||||
    let maxX = lastX;
 | 
			
		||||
    let maxY = lastY;
 | 
			
		||||
    let lastPointX = lastX;
 | 
			
		||||
    let lastPointY = lastY;
 | 
			
		||||
    const ltrCallback = isLTR ? Math.max : Math.min;
 | 
			
		||||
 | 
			
		||||
    for (let i = 6, ii = outline.length; i < ii; i += 6) {
 | 
			
		||||
      if (isNaN(outline[i])) {
 | 
			
		||||
        minX = Math.min(minX, outline[i + 4]);
 | 
			
		||||
        minY = Math.min(minY, outline[i + 5]);
 | 
			
		||||
        maxX = Math.max(maxX, outline[i + 4]);
 | 
			
		||||
        maxY = Math.max(maxY, outline[i + 5]);
 | 
			
		||||
        if (lastPointY < outline[i + 5]) {
 | 
			
		||||
          lastPointX = outline[i + 4];
 | 
			
		||||
          lastPointY = outline[i + 5];
 | 
			
		||||
        } else if (lastPointY === outline[i + 5]) {
 | 
			
		||||
          lastPointX = ltrCallback(lastPointX, outline[i + 4]);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        const bbox = Util.bezierBoundingBox(
 | 
			
		||||
          lastX,
 | 
			
		||||
          lastY,
 | 
			
		||||
          ...outline.slice(i, i + 6)
 | 
			
		||||
        );
 | 
			
		||||
        minX = Math.min(minX, bbox[0]);
 | 
			
		||||
        minY = Math.min(minY, bbox[1]);
 | 
			
		||||
        maxX = Math.max(maxX, bbox[2]);
 | 
			
		||||
        maxY = Math.max(maxY, bbox[3]);
 | 
			
		||||
        if (lastPointY < bbox[3]) {
 | 
			
		||||
          lastPointX = bbox[2];
 | 
			
		||||
          lastPointY = bbox[3];
 | 
			
		||||
        } else if (lastPointY === bbox[3]) {
 | 
			
		||||
          lastPointX = ltrCallback(lastPointX, bbox[2]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      lastX = outline[i + 4];
 | 
			
		||||
      lastY = outline[i + 5];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const x = minX - this.#innerMargin,
 | 
			
		||||
      y = minY - this.#innerMargin,
 | 
			
		||||
      width = maxX - minX + 2 * this.#innerMargin,
 | 
			
		||||
      height = maxY - minY + 2 * this.#innerMargin;
 | 
			
		||||
    lastPointX = (lastPointX - x) / width;
 | 
			
		||||
    lastPointY = (lastPointY - y) / height;
 | 
			
		||||
    this.#bbox = { x, y, width, height, lastPoint: [lastPointX, lastPointY] };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get box() {
 | 
			
		||||
    return this.#bbox;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { FreeOutliner, Outliner };
 | 
			
		||||
 | 
			
		||||
@ -942,25 +942,25 @@
 | 
			
		||||
 | 
			
		||||
.annotationEditorLayer {
 | 
			
		||||
  &[data-main-rotation="0"] {
 | 
			
		||||
    .highlightEditor > .editToolbar {
 | 
			
		||||
    .highlightEditor:not(.free) > .editToolbar {
 | 
			
		||||
      rotate: 0deg;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[data-main-rotation="90"] {
 | 
			
		||||
    .highlightEditor > .editToolbar {
 | 
			
		||||
    .highlightEditor:not(.free) > .editToolbar {
 | 
			
		||||
      rotate: 270deg;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[data-main-rotation="180"] {
 | 
			
		||||
    .highlightEditor > .editToolbar {
 | 
			
		||||
    .highlightEditor:not(.free) > .editToolbar {
 | 
			
		||||
      rotate: 180deg;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &[data-main-rotation="270"] {
 | 
			
		||||
    .highlightEditor > .editToolbar {
 | 
			
		||||
    .highlightEditor:not(.free) > .editToolbar {
 | 
			
		||||
      rotate: 90deg;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -969,14 +969,17 @@
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    transform-origin: 0 0;
 | 
			
		||||
    cursor: auto;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    max-height: 100%;
 | 
			
		||||
    border: none;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    transform: none;
 | 
			
		||||
    transform-origin: 0 0;
 | 
			
		||||
 | 
			
		||||
    &:not(.free) {
 | 
			
		||||
      transform: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .internal {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,10 @@
 | 
			
		||||
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      mix-blend-mode: var(--blend-mode);
 | 
			
		||||
      fill-rule: evenodd;
 | 
			
		||||
 | 
			
		||||
      &:not(.free) {
 | 
			
		||||
        fill-rule: evenodd;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.highlightOutline {
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,10 @@
 | 
			
		||||
  transform-origin: 0 0;
 | 
			
		||||
  z-index: 2;
 | 
			
		||||
 | 
			
		||||
  &.drawing {
 | 
			
		||||
    touch-action: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :is(span, br) {
 | 
			
		||||
    color: transparent;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user