src_tools_arrow.js

import {Point2D} from '../math/point';
import {Line, getPerpendicularLine, getAngle} from '../math/line';
import {defaults} from '../app/defaults';
import {getDefaultAnchor} from './editor';
// external
import Konva from 'konva';

// doc imports
/* eslint-disable no-unused-vars */
import {ViewController} from '../app/viewController';
import {Style} from '../gui/style';
/* eslint-enable no-unused-vars */

/**
 * Arrow factory.
 */
export class ArrowFactory {
  /**
   * Get the name of the shape group.
   *
   * @returns {string} The name.
   */
  getGroupName() {
    return 'line-group';
  }

  /**
   * Get the number of points needed to build the shape.
   *
   * @returns {number} The number of points.
   */
  getNPoints() {
    return 2;
  }

  /**
   * Get the timeout between point storage.
   *
   * @returns {number} The timeout in milliseconds.
   */
  getTimeout() {
    return 0;
  }

  /**
   * Is the input group a group of this factory?
   *
   * @param {Konva.Group} group The group to test.
   * @returns {boolean} True if the group is from this fcatory.
   */
  isFactoryGroup(group) {
    return this.getGroupName() === group.name();
  }

  /**
   * Create an arrow shape to be displayed.
   *
   * @param {Point2D[]} points The points from which to extract the line.
   * @param {Style} style The drawing style.
   * @param {ViewController} viewController The associated view controller.
   * @returns {Konva.Group} The Konva group.
   */
  create(points, style, viewController) {
    // physical shape
    const line = new Line(points[0], points[1]);
    // draw shape
    const kshape = new Konva.Line({
      points: [line.getBegin().getX(),
        line.getBegin().getY(),
        line.getEnd().getX(),
        line.getEnd().getY()],
      stroke: style.getLineColour(),
      strokeWidth: style.getStrokeWidth(),
      strokeScaleEnabled: false,
      name: 'shape'
    });
    // larger hitfunc
    const tickLen = style.applyZoomScale(10).x;
    const linePerp0 = getPerpendicularLine(line, points[0], tickLen);
    const linePerp1 = getPerpendicularLine(line, points[1], tickLen);
    kshape.hitFunc(function (context) {
      context.beginPath();
      context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
      context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
      context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
      context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
      context.closePath();
      context.fillStrokeShape(kshape);
    });
    // triangle
    const beginTy = new Point2D(
      line.getBegin().getX(),
      line.getBegin().getY() - 10);
    const verticalLine = new Line(line.getBegin(), beginTy);
    const angle = getAngle(line, verticalLine);
    const angleRad = angle * Math.PI / 180;
    const radius = Math.abs(style.applyZoomScale(8).x);
    const kpoly = new Konva.RegularPolygon({
      x: line.getBegin().getX() + radius * Math.sin(angleRad),
      y: line.getBegin().getY() + radius * Math.cos(angleRad),
      sides: 3,
      radius: radius,
      rotation: -angle,
      fill: style.getLineColour(),
      strokeWidth: style.getStrokeWidth(),
      strokeScaleEnabled: false,
      name: 'shape-triangle'
    });
    // quantification
    const ktext = new Konva.Text({
      fontSize: style.getFontSize(),
      fontFamily: style.getFontFamily(),
      fill: style.getLineColour(),
      padding: style.getTextPadding(),
      shadowColor: style.getShadowLineColour(),
      shadowOffset: style.getShadowOffset(),
      name: 'text'
    });
    let textExpr = '';
    const modality = viewController.getModality();
    if (typeof defaults.labelText.arrow[modality] !== 'undefined') {
      textExpr = defaults.labelText.arrow[modality];
    } else {
      textExpr = defaults.labelText.arrow['*'];
    }
    ktext.setText(textExpr);
    // augment text with meta data
    // @ts-ignore
    ktext.meta = {
      textExpr: textExpr,
      quantification: {}
    };
    // label
    const dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
    const dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0;
    const klabel = new Konva.Label({
      x: line.getEnd().getX() + dX * ktext.width(),
      y: line.getEnd().getY() + dY * style.applyZoomScale(15).y,
      scale: style.applyZoomScale(1),
      visible: textExpr.length !== 0,
      name: 'label'
    });
    klabel.add(ktext);
    klabel.add(new Konva.Tag({
      fill: style.getLineColour(),
      opacity: style.getTagOpacity()
    }));

    // return group
    const group = new Konva.Group();
    group.name(this.getGroupName());
    group.add(klabel);
    group.add(kpoly);
    group.add(kshape);
    group.visible(true); // dont inherit
    return group;
  }

  /**
   * Get anchors to update an arrow shape.
   *
   * @param {Konva.Line} shape The associated shape.
   * @param {Style} style The application style.
   * @returns {Konva.Ellipse[]} A list of anchors.
   */
  getAnchors(shape, style) {
    const points = shape.points();

    const anchors = [];
    anchors.push(getDefaultAnchor(
      points[0] + shape.x(), points[1] + shape.y(), 'begin', style
    ));
    anchors.push(getDefaultAnchor(
      points[2] + shape.x(), points[3] + shape.y(), 'end', style
    ));
    return anchors;
  }

  /**
   * Update an arrow shape.
   *
   * @param {Konva.Ellipse} anchor The active anchor.
   * @param {Style} style The app style.
   * @param {ViewController} _viewController The associated view controller.
   */
  update(anchor, style, _viewController) {
    // parent group
    const group = anchor.getParent();
    // associated shape
    const kline = group.getChildren(function (node) {
      return node.name() === 'shape';
    })[0];
    if (!(kline instanceof Konva.Line)) {
      return;
    }
    // associated triangle shape
    const ktriangle = group.getChildren(function (node) {
      return node.name() === 'shape-triangle';
    })[0];
    if (!(ktriangle instanceof Konva.RegularPolygon)) {
      return;
    }
    // associated label
    const klabel = group.getChildren(function (node) {
      return node.name() === 'label';
    })[0];
    if (!(klabel instanceof Konva.Label)) {
      return;
    }
    // find special points
    const begin = group.getChildren(function (node) {
      return node.id() === 'begin';
    })[0];
    const end = group.getChildren(function (node) {
      return node.id() === 'end';
    })[0];
    // update special points
    switch (anchor.id()) {
    case 'begin':
      begin.x(anchor.x());
      begin.y(anchor.y());
      break;
    case 'end':
      end.x(anchor.x());
      end.y(anchor.y());
      break;
    }
    // update shape and compensate for possible drag
    // note: shape.position() and shape.size() won't work...
    const bx = begin.x() - kline.x();
    const by = begin.y() - kline.y();
    const ex = end.x() - kline.x();
    const ey = end.y() - kline.y();
    kline.points([bx, by, ex, ey]);
    // new line
    const p2d0 = new Point2D(begin.x(), begin.y());
    const p2d1 = new Point2D(end.x(), end.y());
    const line = new Line(p2d0, p2d1);
    // larger hitfunc
    const p2b = new Point2D(bx, by);
    const p2e = new Point2D(ex, ey);
    const tickLen = style.applyZoomScale(10).x;
    const linePerp0 = getPerpendicularLine(line, p2b, tickLen);
    const linePerp1 = getPerpendicularLine(line, p2e, tickLen);
    kline.hitFunc(function (context) {
      context.beginPath();
      context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
      context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
      context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
      context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
      context.closePath();
      context.fillStrokeShape(kline);
    });
    // udate triangle
    const beginTy = new Point2D(
      line.getBegin().getX(),
      line.getBegin().getY() - 10);
    const verticalLine = new Line(line.getBegin(), beginTy);
    const angle = getAngle(line, verticalLine);
    const angleRad = angle * Math.PI / 180;
    ktriangle.x(
      line.getBegin().getX() + ktriangle.radius() * Math.sin(angleRad));
    ktriangle.y(
      line.getBegin().getY() + ktriangle.radius() * Math.cos(angleRad));
    ktriangle.rotation(-angle);

    // update text
    const ktext = klabel.getText();
    // @ts-expect-error
    ktext.setText(ktext.meta.textExpr);
    // update position
    const dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
    const dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0;
    const textPos = {
      x: line.getEnd().getX() + dX * ktext.width(),
      y: line.getEnd().getY() + dY * style.applyZoomScale(15).y
    };
    klabel.position(textPos);
  }

} // class ArrowFactory