Source: svgcanvas/path-actions.js

/**
 * Path functionality.
 * @module path
 * @license MIT
 *
 * @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
 */

import { NS } from './namespaces.js'
import { shortFloat } from '../common/units.js'
import { ChangeElementCommand, BatchCommand } from './history.js'
import {
  transformPoint, snapToAngle, rectsIntersect,
  transformListToTransform
} from './math.js'
import {
  assignAttributes, getElement, getRotationAngle, snapToGrid,
  getBBox
} from './utilities.js'

let svgCanvas = null
let path = null

/**
* @function module:path-actions.init
* @param {module:path-actions.svgCanvas} pathActionsContext
* @returns {void}
*/
export const init = (canvas) => {
  svgCanvas = canvas
}

/**
 * Convert a path to one with only absolute or relative values.
 * @todo move to pathActions.js
 * @function module:path.convertPath
 * @param {SVGPathElement} pth - the path to convert
 * @param {boolean} toRel - true of convert to relative
 * @returns {string}
 */
export const convertPath = function (pth, toRel) {
  const { pathSegList } = pth
  const len = pathSegList.numberOfItems
  let curx = 0; let cury = 0
  let d = ''
  let lastM = null

  for (let i = 0; i < len; ++i) {
    const seg = pathSegList.getItem(i)
    // if these properties are not in the segment, set them to zero
    let x = seg.x || 0
    let y = seg.y || 0
    let x1 = seg.x1 || 0
    let y1 = seg.y1 || 0
    let x2 = seg.x2 || 0
    let y2 = seg.y2 || 0

    // const type = seg.pathSegType;
    // const pathMap = svgCanvas.getPathMap();
    // let letter = pathMap[type][toRel ? 'toLowerCase' : 'toUpperCase']();
    let letter = seg.pathSegTypeAsLetter

    switch (letter) {
      case 'z': // z,Z closepath (Z/z)
      case 'Z':
        d += 'z'
        if (lastM && !toRel) {
          curx = lastM[0]
          cury = lastM[1]
        }
        break
      case 'H': // absolute horizontal line (H)
        x -= curx
      // Fallthrough
      case 'h': // relative horizontal line (h)
        if (toRel) {
          y = 0
          curx += x
          letter = 'l'
        } else {
          y = cury
          x += curx
          curx = x
          letter = 'L'
        }
        // Convert to "line" for easier editing
        d += pathDSegment(letter, [[x, y]])
        break
      case 'V': // absolute vertical line (V)
        y -= cury
      // Fallthrough
      case 'v': // relative vertical line (v)
        if (toRel) {
          x = 0
          cury += y
          letter = 'l'
        } else {
          x = curx
          y += cury
          cury = y
          letter = 'L'
        }
        // Convert to "line" for easier editing
        d += pathDSegment(letter, [[x, y]])
        break
      case 'M': // absolute move (M)
      case 'L': // absolute line (L)
      case 'T': // absolute smooth quad (T)
        x -= curx
        y -= cury
      // Fallthrough
      case 'l': // relative line (l)
      case 'm': // relative move (m)
      case 't': // relative smooth quad (t)
        if (toRel) {
          curx += x
          cury += y
          letter = letter.toLowerCase()
        } else {
          x += curx
          y += cury
          curx = x
          cury = y
          letter = letter.toUpperCase()
        }
        if (letter === 'm' || letter === 'M') { lastM = [curx, cury] }

        d += pathDSegment(letter, [[x, y]])
        break
      case 'C': // absolute cubic (C)
        x -= curx; x1 -= curx; x2 -= curx
        y -= cury; y1 -= cury; y2 -= cury
      // Fallthrough
      case 'c': // relative cubic (c)
        if (toRel) {
          curx += x
          cury += y
          letter = 'c'
        } else {
          x += curx; x1 += curx; x2 += curx
          y += cury; y1 += cury; y2 += cury
          curx = x
          cury = y
          letter = 'C'
        }
        d += pathDSegment(letter, [[x1, y1], [x2, y2], [x, y]])
        break
      case 'Q': // absolute quad (Q)
        x -= curx; x1 -= curx
        y -= cury; y1 -= cury
      // Fallthrough
      case 'q': // relative quad (q)
        if (toRel) {
          curx += x
          cury += y
          letter = 'q'
        } else {
          x += curx; x1 += curx
          y += cury; y1 += cury
          curx = x
          cury = y
          letter = 'Q'
        }
        d += pathDSegment(letter, [[x1, y1], [x, y]])
        break
      case 'A':
        x -= curx
        y -= cury
      // fallthrough
      case 'a': // relative elliptical arc (a)
        if (toRel) {
          curx += x
          cury += y
          letter = 'a'
        } else {
          x += curx
          y += cury
          curx = x
          cury = y
          letter = 'A'
        }
        d += pathDSegment(letter, [[seg.r1, seg.r2]], [
          seg.angle,
          (seg.largeArcFlag ? 1 : 0),
          (seg.sweepFlag ? 1 : 0)
        ], [x, y])
        break
      case 'S': // absolute smooth cubic (S)
        x -= curx; x2 -= curx
        y -= cury; y2 -= cury
      // Fallthrough
      case 's': // relative smooth cubic (s)
        if (toRel) {
          curx += x
          cury += y
          letter = 's'
        } else {
          x += curx; x2 += curx
          y += cury; y2 += cury
          curx = x
          cury = y
          letter = 'S'
        }
        d += pathDSegment(letter, [[x2, y2], [x, y]])
        break
    } // switch on path segment type
  } // for each segment
  return d
}

/**
 * TODO: refactor callers in `convertPath` to use `getPathDFromSegments` instead of this function.
 * Legacy code refactored from `svgcanvas.pathActions.convertPath`.
 * @param {string} letter - path segment command (letter in potentially either case from {@link module:path.pathMap}; see [SVGPathSeg#pathSegTypeAsLetter]{@link https://www.w3.org/TR/SVG/single-page.html#paths-__svg__SVGPathSeg__pathSegTypeAsLetter})
 * @param {GenericArray<GenericArray<Integer>>} points - x,y points
 * @param {GenericArray<GenericArray<Integer>>} [morePoints] - x,y points
 * @param {Integer[]} [lastPoint] - x,y point
 * @returns {string}
 */
function pathDSegment (letter, points, morePoints, lastPoint) {
  points.forEach(function (pnt, i) {
    points[i] = shortFloat(pnt)
  })
  let segment = letter + points.join(' ')
  if (morePoints) {
    segment += ' ' + morePoints.join(' ')
  }
  if (lastPoint) {
    segment += ' ' + shortFloat(lastPoint)
  }
  return segment
}

/**
* Group: Path edit functions.
* Functions relating to editing path elements.
* @namespace {PlainObject} pathActions
* @memberof module:path
*/
export const pathActionsMethod = (function () {
  let subpath = false
  let newPoint; let firstCtrl

  let currentPath = null
  let hasMoved = false
  // No `svgCanvas` yet but should be ok as is `null` by default
  // svgCanvas.setDrawnPath(null);

  /**
  * This function converts a polyline (created by the fh_path tool) into
  * a path element and coverts every three line segments into a single bezier
  * curve in an attempt to smooth out the free-hand.
  * @function smoothPolylineIntoPath
  * @param {Element} element
  * @returns {Element}
  */
  const smoothPolylineIntoPath = function (element) {
    let i
    const { points } = element
    const N = points.numberOfItems
    if (N >= 4) {
      // loop through every 3 points and convert to a cubic bezier curve segment
      //
      // NOTE: this is cheating, it means that every 3 points has the potential to
      // be a corner instead of treating each point in an equal manner. In general,
      // this technique does not look that good.
      //
      // I am open to better ideas!
      //
      // Reading:
      // - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm
      // - https://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963
      // - https://www.ian-ko.com/ET_GeoWizards/UserGuide/smooth.htm
      // - https://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html
      let curpos = points.getItem(0); let prevCtlPt = null
      let d = []
      d.push(['M', curpos.x, ',', curpos.y, ' C'].join(''))
      for (i = 1; i <= (N - 4); i += 3) {
        let ct1 = points.getItem(i)
        const ct2 = points.getItem(i + 1)
        const end = points.getItem(i + 2)

        // if the previous segment had a control point, we want to smooth out
        // the control points on both sides
        if (prevCtlPt) {
          const newpts = svgCanvas.smoothControlPoints(prevCtlPt, ct1, curpos)
          if (newpts?.length === 2) {
            const prevArr = d[d.length - 1].split(',')
            prevArr[2] = newpts[0].x
            prevArr[3] = newpts[0].y
            d[d.length - 1] = prevArr.join(',')
            ct1 = newpts[1]
          }
        }

        d.push([ct1.x, ct1.y, ct2.x, ct2.y, end.x, end.y].join(','))

        curpos = end
        prevCtlPt = ct2
      }
      // handle remaining line segments
      d.push('L')
      while (i < N) {
        const pt = points.getItem(i)
        d.push([pt.x, pt.y].join(','))
        i++
      }
      d = d.join(' ')

      element = svgCanvas.addSVGElementsFromJson({
        element: 'path',
        curStyles: true,
        attr: {
          id: svgCanvas.getId(),
          d,
          fill: 'none'
        }
      })
      // No need to call "changed", as this is already done under mouseUp
    }
    return element
  }

  return (/** @lends module:path.pathActions */ {
    /**
    * @param {MouseEvent} evt
    * @param {Element} mouseTarget
    * @param {Float} startX
    * @param {Float} startY
    * @returns {boolean|void}
    */
    mouseDown (evt, mouseTarget, startX, startY) {
      let id
      if (svgCanvas.getCurrentMode() === 'path') {
        let mouseX = startX // Was this meant to work with the other `mouseX`? (was defined globally so adding `let` to at least avoid a global)
        let mouseY = startY // Was this meant to work with the other `mouseY`? (was defined globally so adding `let` to at least avoid a global)

        const zoom = svgCanvas.getZoom()
        let x = mouseX / zoom
        let y = mouseY / zoom
        let stretchy = getElement('path_stretch_line')
        newPoint = [x, y]

        if (svgCanvas.getGridSnapping()) {
          x = snapToGrid(x)
          y = snapToGrid(y)
          mouseX = snapToGrid(mouseX)
          mouseY = snapToGrid(mouseY)
        }

        if (!stretchy) {
          stretchy = document.createElementNS(NS.SVG, 'path')
          assignAttributes(stretchy, {
            id: 'path_stretch_line',
            stroke: '#22C',
            'stroke-width': '0.5',
            fill: 'none'
          })
          getElement('selectorParentGroup').append(stretchy)
        }
        stretchy.setAttribute('display', 'inline')

        let keep = null
        let index
        // if pts array is empty, create path element with M at current point
        const drawnPath = svgCanvas.getDrawnPath()
        if (!drawnPath) {
          const dAttr = 'M' + x + ',' + y + ' ' // Was this meant to work with the other `dAttr`? (was defined globally so adding `var` to at least avoid a global)
          /* drawnPath = */ svgCanvas.setDrawnPath(svgCanvas.addSVGElementsFromJson({
            element: 'path',
            curStyles: true,
            attr: {
              d: dAttr,
              id: svgCanvas.getNextId(),
              opacity: svgCanvas.getOpacity() / 2
            }
          }))
          // set stretchy line to first point
          stretchy.setAttribute('d', ['M', mouseX, mouseY, mouseX, mouseY].join(' '))
          index = subpath ? path.segs.length : 0
          svgCanvas.addPointGrip(index, mouseX, mouseY)
        } else {
          // determine if we clicked on an existing point
          const seglist = drawnPath.pathSegList
          let i = seglist.numberOfItems
          const FUZZ = 6 / zoom
          let clickOnPoint = false
          while (i) {
            i--
            const item = seglist.getItem(i)
            const px = item.x; const py = item.y
            // found a matching point
            if (x >= (px - FUZZ) && x <= (px + FUZZ) &&
              y >= (py - FUZZ) && y <= (py + FUZZ)
            ) {
              clickOnPoint = true
              break
            }
          }

          // get path element that we are in the process of creating
          id = svgCanvas.getId()

          // Remove previous path object if previously created
          svgCanvas.removePath_(id)

          const newpath = getElement(id)
          let newseg
          let sSeg
          const len = seglist.numberOfItems
          // if we clicked on an existing point, then we are done this path, commit it
          // (i, i+1) are the x,y that were clicked on
          if (clickOnPoint) {
            // if clicked on any other point but the first OR
            // the first point was clicked on and there are less than 3 points
            // then leave the path open
            // otherwise, close the path
            if (i <= 1 && len >= 2) {
              // Create end segment
              const absX = seglist.getItem(0).x
              const absY = seglist.getItem(0).y

              sSeg = stretchy.pathSegList.getItem(1)
              newseg = sSeg.pathSegType === 4
                ? drawnPath.createSVGPathSegLinetoAbs(absX, absY)
                : drawnPath.createSVGPathSegCurvetoCubicAbs(absX, absY, sSeg.x1 / zoom, sSeg.y1 / zoom, absX, absY)

              const endseg = drawnPath.createSVGPathSegClosePath()
              seglist.appendItem(newseg)
              seglist.appendItem(endseg)
            } else if (len < 3) {
              keep = false
              return keep
            }
            stretchy.remove()

            // This will signal to commit the path
            // const element = newpath; // Other event handlers define own `element`, so this was probably not meant to interact with them or one which shares state (as there were none); I therefore adding a missing `var` to avoid a global
            /* drawnPath = */ svgCanvas.setDrawnPath(null)
            svgCanvas.setStarted(false)

            if (subpath) {
              if (path.matrix) {
                svgCanvas.remapElement(newpath, {}, path.matrix.inverse())
              }

              const newD = newpath.getAttribute('d')
              const origD = path.elem.getAttribute('d')
              path.elem.setAttribute('d', origD + newD)
              newpath.parentNode.removeChild(newpath)
              if (path.matrix) {
                svgCanvas.recalcRotatedPath()
              }
              pathActionsMethod.toEditMode(path.elem)
              path.selectPt()
              return false
            }
          // else, create a new point, update path element
          } else {
            // Checks if current target or parents are #svgcontent
            if (!(svgCanvas.getContainer() !== svgCanvas.getMouseTarget(evt) && svgCanvas.getContainer().contains(
              svgCanvas.getMouseTarget(evt)
            ))) {
              // Clicked outside canvas, so don't make point
              return false
            }

            const num = drawnPath.pathSegList.numberOfItems
            const last = drawnPath.pathSegList.getItem(num - 1)
            const lastx = last.x; const lasty = last.y

            if (evt.shiftKey) {
              const xya = snapToAngle(lastx, lasty, x, y);
              ({ x, y } = xya)
            }

            // Use the segment defined by stretchy
            sSeg = stretchy.pathSegList.getItem(1)
            newseg = sSeg.pathSegType === 4
              ? drawnPath.createSVGPathSegLinetoAbs(svgCanvas.round(x), svgCanvas.round(y))
              : drawnPath.createSVGPathSegCurvetoCubicAbs(
                svgCanvas.round(x),
                svgCanvas.round(y),
                sSeg.x1 / zoom,
                sSeg.y1 / zoom,
                sSeg.x2 / zoom,
                sSeg.y2 / zoom
              )

            drawnPath.pathSegList.appendItem(newseg)

            x *= zoom
            y *= zoom

            // set stretchy line to latest point
            stretchy.setAttribute('d', ['M', x, y, x, y].join(' '))
            index = num
            if (subpath) { index += path.segs.length }
            svgCanvas.addPointGrip(index, x, y)
          }
          // keep = true;
        }

        return undefined
      }

      // TODO: Make sure currentPath isn't null at this point
      if (!path) { return undefined }

      path.storeD();

      ({ id } = evt.target)
      let curPt
      if (id.substr(0, 14) === 'pathpointgrip_') {
        // Select this point
        curPt = path.cur_pt = Number.parseInt(id.substr(14))
        path.dragging = [startX, startY]
        const seg = path.segs[curPt]

        // only clear selection if shift is not pressed (otherwise, add
        // node to selection)
        if (!evt.shiftKey) {
          if (path.selected_pts.length <= 1 || !seg.selected) {
            path.clearSelection()
          }
          path.addPtsToSelection(curPt)
        } else if (seg.selected) {
          path.removePtFromSelection(curPt)
        } else {
          path.addPtsToSelection(curPt)
        }
      } else if (id.startsWith('ctrlpointgrip_')) {
        path.dragging = [startX, startY]

        const parts = id.split('_')[1].split('c')
        curPt = Number(parts[0])
        const ctrlNum = Number(parts[1])
        path.selectPt(curPt, ctrlNum)
      }

      // Start selection box
      if (!path.dragging) {
        let rubberBox = svgCanvas.getRubberBox()
        if (!rubberBox) {
          rubberBox = svgCanvas.setRubberBox(
            svgCanvas.selectorManager.getRubberBandBox()
          )
        }
        const zoom = svgCanvas.getZoom()
        assignAttributes(rubberBox, {
          x: startX * zoom,
          y: startY * zoom,
          width: 0,
          height: 0,
          display: 'inline'
        }, 100)
      }
      return undefined
    },
    /**
    * @param {Float} mouseX
    * @param {Float} mouseY
    * @returns {void}
    */
    mouseMove (mouseX, mouseY) {
      const zoom = svgCanvas.getZoom()
      hasMoved = true
      const drawnPath = svgCanvas.getDrawnPath()
      if (svgCanvas.getCurrentMode() === 'path') {
        if (!drawnPath) { return }
        const seglist = drawnPath.pathSegList
        const index = seglist.numberOfItems - 1

        if (newPoint) {
          // First point
          // if (!index) { return; }

          // Set control points
          const pointGrip1 = svgCanvas.addCtrlGrip('1c1')
          const pointGrip2 = svgCanvas.addCtrlGrip('0c2')

          // dragging pointGrip1
          pointGrip1.setAttribute('cx', mouseX)
          pointGrip1.setAttribute('cy', mouseY)
          pointGrip1.setAttribute('display', 'inline')

          const ptX = newPoint[0]
          const ptY = newPoint[1]

          // set curve
          // const seg = seglist.getItem(index);
          const curX = mouseX / zoom
          const curY = mouseY / zoom
          const altX = (ptX + (ptX - curX))
          const altY = (ptY + (ptY - curY))

          pointGrip2.setAttribute('cx', altX * zoom)
          pointGrip2.setAttribute('cy', altY * zoom)
          pointGrip2.setAttribute('display', 'inline')

          const ctrlLine = svgCanvas.getCtrlLine(1)
          assignAttributes(ctrlLine, {
            x1: mouseX,
            y1: mouseY,
            x2: altX * zoom,
            y2: altY * zoom,
            display: 'inline'
          })

          if (index === 0) {
            firstCtrl = [mouseX, mouseY]
          } else {
            const last = seglist.getItem(index - 1)
            let lastX = last.x
            let lastY = last.y

            if (last.pathSegType === 6) {
              lastX += (lastX - last.x2)
              lastY += (lastY - last.y2)
            } else if (firstCtrl) {
              lastX = firstCtrl[0] / zoom
              lastY = firstCtrl[1] / zoom
            }
            svgCanvas.replacePathSeg(6, index, [ptX, ptY, lastX, lastY, altX, altY], drawnPath)
          }
        } else {
          const stretchy = getElement('path_stretch_line')
          if (stretchy) {
            const prev = seglist.getItem(index)
            if (prev.pathSegType === 6) {
              const prevX = prev.x + (prev.x - prev.x2)
              const prevY = prev.y + (prev.y - prev.y2)
              svgCanvas.replacePathSeg(
                6,
                1,
                [mouseX, mouseY, prevX * zoom, prevY * zoom, mouseX, mouseY],
                stretchy
              )
            } else if (firstCtrl) {
              svgCanvas.replacePathSeg(6, 1, [mouseX, mouseY, firstCtrl[0], firstCtrl[1], mouseX, mouseY], stretchy)
            } else {
              svgCanvas.replacePathSeg(4, 1, [mouseX, mouseY], stretchy)
            }
          }
        }
        return
      }
      // if we are dragging a point, let's move it
      if (path.dragging) {
        const pt = svgCanvas.getPointFromGrip({
          x: path.dragging[0],
          y: path.dragging[1]
        }, path)
        const mpt = svgCanvas.getPointFromGrip({
          x: mouseX,
          y: mouseY
        }, path)
        const diffX = mpt.x - pt.x
        const diffY = mpt.y - pt.y
        path.dragging = [mouseX, mouseY]

        if (path.dragctrl) {
          path.moveCtrl(diffX, diffY)
        } else {
          path.movePts(diffX, diffY)
        }
      } else {
        path.selected_pts = []
        path.eachSeg(function (_i) {
          const seg = this
          if (!seg.next && !seg.prev) { return }

          // const {item} = seg;
          const rubberBox = svgCanvas.getRubberBox()
          const rbb = getBBox(rubberBox)

          const pt = svgCanvas.getGripPt(seg)
          const ptBb = {
            x: pt.x,
            y: pt.y,
            width: 0,
            height: 0
          }

          const sel = rectsIntersect(rbb, ptBb)

          this.select(sel)
          // Note that addPtsToSelection is not being run
          if (sel) { path.selected_pts.push(seg.index) }
        })
      }
    },
    /**
     * @typedef module:path.keepElement
     * @type {PlainObject}
     * @property {boolean} keep
     * @property {Element} element
     */
    /**
    * @param {Event} evt
    * @param {Element} element
    * @param {Float} _mouseX
    * @param {Float} _mouseY
    * @returns {module:path.keepElement|void}
    */
    mouseUp (evt, element, _mouseX, _mouseY) {
      const drawnPath = svgCanvas.getDrawnPath()
      // Create mode
      if (svgCanvas.getCurrentMode() === 'path') {
        newPoint = null
        if (!drawnPath) {
          element = getElement(svgCanvas.getId())
          svgCanvas.setStarted(false)
          firstCtrl = null
        }

        return {
          keep: true,
          element
        }
      }

      // Edit mode
      const rubberBox = svgCanvas.getRubberBox()
      if (path.dragging) {
        const lastPt = path.cur_pt

        path.dragging = false
        path.dragctrl = false
        path.update()

        if (hasMoved) {
          path.endChanges('Move path point(s)')
        }

        if (!evt.shiftKey && !hasMoved) {
          path.selectPt(lastPt)
        }
      } else if (rubberBox?.getAttribute('display') !== 'none') {
        // Done with multi-node-select
        rubberBox.setAttribute('display', 'none')

        if (rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) {
          pathActionsMethod.toSelectMode(evt.target)
        }

      // else, move back to select mode
      } else {
        pathActionsMethod.toSelectMode(evt.target)
      }
      hasMoved = false
      return undefined
    },
    /**
    * @param {Element} element
    * @returns {void}
    */
    toEditMode (element) {
      path = svgCanvas.getPath_(element)
      svgCanvas.setCurrentMode('pathedit')
      svgCanvas.clearSelection()
      path.setPathContext()
      path.show(true).update()
      path.oldbbox = getBBox(path.elem)
      subpath = false
    },
    /**
    * @param {Element} elem
    * @fires module:svgcanvas.SvgCanvas#event:selected
    * @returns {void}
    */
    toSelectMode (elem) {
      const selPath = (elem === path.elem)
      svgCanvas.setCurrentMode('select')
      path.setPathContext()
      path.show(false)
      currentPath = false
      svgCanvas.clearSelection()

      if (path.matrix) {
        // Rotated, so may need to re-calculate the center
        svgCanvas.recalcRotatedPath()
      }

      if (selPath) {
        svgCanvas.call('selected', [elem])
        svgCanvas.addToSelection([elem], true)
      }
    },
    /**
    * @param {boolean} on
    * @returns {void}
    */
    addSubPath (on) {
      if (on) {
        // Internally we go into "path" mode, but in the UI it will
        // still appear as if in "pathedit" mode.
        svgCanvas.setCurrentMode('path')
        subpath = true
      } else {
        pathActionsMethod.clear(true)
        pathActionsMethod.toEditMode(path.elem)
      }
    },
    /**
    * @param {Element} target
    * @returns {void}
    */
    select (target) {
      if (currentPath === target) {
        pathActionsMethod.toEditMode(target)
        svgCanvas.setCurrentMode('pathedit')
      // going into pathedit mode
      } else {
        currentPath = target
      }
    },
    /**
    * @fires module:svgcanvas.SvgCanvas#event:changed
    * @returns {void}
    */
    reorient () {
      const elem = svgCanvas.getSelectedElements()[0]
      if (!elem) { return }
      const angl = getRotationAngle(elem)
      if (angl === 0) { return }

      const batchCmd = new BatchCommand('Reorient path')
      const changes = {
        d: elem.getAttribute('d'),
        transform: elem.getAttribute('transform')
      }
      batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
      svgCanvas.clearSelection()
      this.resetOrientation(elem)

      svgCanvas.addCommandToHistory(batchCmd)

      // Set matrix to null
      svgCanvas.getPath_(elem).show(false).matrix = null

      this.clear()

      svgCanvas.addToSelection([elem], true)
      svgCanvas.call('changed', svgCanvas.getSelectedElements())
    },

    /**
    * @param {boolean} remove Not in use
    * @returns {void}
    */
    clear () {
      const drawnPath = svgCanvas.getDrawnPath()
      currentPath = null
      if (drawnPath) {
        const elem = getElement(svgCanvas.getId())
        const psl = getElement('path_stretch_line')
        psl.parentNode.removeChild(psl)
        elem.parentNode.removeChild(elem)
        const pathpointgripContainer = getElement('pathpointgrip_container')
        const elements = pathpointgripContainer.querySelectorAll('*')
        Array.prototype.forEach.call(elements, function (el) {
          el.setAttribute('display', 'none')
        })
        firstCtrl = null
        svgCanvas.setDrawnPath(null)
        svgCanvas.setStarted(false)
      } else if (svgCanvas.getCurrentMode() === 'pathedit') {
        this.toSelectMode()
      }
      if (path) { path.init().show(false) }
    },
    /**
    * @param {?(Element|SVGPathElement)} pth
    * @returns {false|void}
    */
    resetOrientation (pth) {
      if (pth?.nodeName !== 'path') { return false }
      const tlist = pth.transform.baseVal
      const m = transformListToTransform(tlist).matrix
      tlist.clear()
      pth.removeAttribute('transform')
      const segList = pth.pathSegList

      // Opera/win/non-EN throws an error here.
      // TODO: Find out why!
      // Presumed fixed in Opera 10.5, so commented out for now

      // try {
      const len = segList.numberOfItems
      // } catch(err) {
      //   const fixed_d = pathActions.convertPath(pth);
      //   pth.setAttribute('d', fixed_d);
      //   segList = pth.pathSegList;
      //   const len = segList.numberOfItems;
      // }
      // let lastX, lastY;
      for (let i = 0; i < len; ++i) {
        const seg = segList.getItem(i)
        const type = seg.pathSegType
        if (type === 1) { continue }
        const pts = [];
        ['', 1, 2].forEach(function (n) {
          const x = seg['x' + n]; const y = seg['y' + n]
          if (x !== undefined && y !== undefined) {
            const pt = transformPoint(x, y, m)
            pts.splice(pts.length, 0, pt.x, pt.y)
          }
        })
        svgCanvas.replacePathSeg(type, i, pts, pth)
      }

      svgCanvas.reorientGrads(pth, m)
      return undefined
    },
    /**
    * @returns {void}
    */
    zoomChange () {
      if (svgCanvas.getCurrentMode() === 'pathedit') {
        path.update()
      }
    },
    /**
    * @typedef {PlainObject} module:path.NodePoint
    * @property {Float} x
    * @property {Float} y
    * @property {Integer} type
    */
    /**
    * @returns {module:path.NodePoint}
    */
    getNodePoint () {
      const selPt = path.selected_pts.length ? path.selected_pts[0] : 1

      const seg = path.segs[selPt]
      return {
        x: seg.item.x,
        y: seg.item.y,
        type: seg.type
      }
    },
    /**
    * @param {boolean} linkPoints
    * @returns {void}
    */
    linkControlPoints (linkPoints) {
      svgCanvas.setLinkControlPoints(linkPoints)
    },
    /**
    * @returns {void}
    */
    clonePathNode () {
      path.storeD()

      const selPts = path.selected_pts
      // const {segs} = path;

      let i = selPts.length
      const nums = []

      while (i--) {
        const pt = selPts[i]
        path.addSeg(pt)

        nums.push(pt + i)
        nums.push(pt + i + 1)
      }
      path.init().addPtsToSelection(nums)

      path.endChanges('Clone path node(s)')
    },
    /**
    * @returns {void}
    */
    opencloseSubPath () {
      const selPts = path.selected_pts
      // Only allow one selected node for now
      if (selPts.length !== 1) { return }

      const { elem } = path
      const list = elem.pathSegList

      // const len = list.numberOfItems;

      const index = selPts[0]

      let openPt = null
      let startItem = null

      // Check if subpath is already open
      path.eachSeg(function (i) {
        if (this.type === 2 && i <= index) {
          startItem = this.item
        }
        if (i <= index) { return true }
        if (this.type === 2) {
          // Found M first, so open
          openPt = i
          return false
        }
        if (this.type === 1) {
          // Found Z first, so closed
          openPt = false
          return false
        }
        return true
      })

      if (!openPt) {
        // Single path, so close last seg
        openPt = path.segs.length - 1
      }

      if (openPt !== false) {
        // Close this path

        // Create a line going to the previous "M"
        const newseg = elem.createSVGPathSegLinetoAbs(startItem.x, startItem.y)

        const closer = elem.createSVGPathSegClosePath()
        if (openPt === path.segs.length - 1) {
          list.appendItem(newseg)
          list.appendItem(closer)
        } else {
          list.insertItemBefore(closer, openPt)
          list.insertItemBefore(newseg, openPt)
        }

        path.init().selectPt(openPt + 1)
        return
      }

      // M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2
      // M 2,2 L 3,3 L 1,1

      // M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z
      // M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z

      const seg = path.segs[index]

      if (seg.mate) {
        list.removeItem(index) // Removes last "L"
        list.removeItem(index) // Removes the "Z"
        path.init().selectPt(index - 1)
        return
      }

      let lastM; let zSeg

      // Find this sub-path's closing point and remove
      for (let i = 0; i < list.numberOfItems; i++) {
        const item = list.getItem(i)

        if (item.pathSegType === 2) {
          // Find the preceding M
          lastM = i
        } else if (i === index) {
          // Remove it
          list.removeItem(lastM)
          // index--;
        } else if (item.pathSegType === 1 && index < i) {
          // Remove the closing seg of this subpath
          zSeg = i - 1
          list.removeItem(i)
          break
        }
      }

      let num = (index - lastM) - 1

      while (num--) {
        list.insertItemBefore(list.getItem(lastM), zSeg)
      }

      const pt = list.getItem(lastM)

      // Make this point the new "M"
      svgCanvas.replacePathSeg(2, lastM, [pt.x, pt.y])

      // i = index; // i is local here, so has no effect; what was the intent for this?

      path.init().selectPt(0)
    },
    /**
    * @returns {void}
    */
    deletePathNode () {
      if (!pathActionsMethod.canDeleteNodes) { return }
      path.storeD()

      const selPts = path.selected_pts

      let i = selPts.length
      while (i--) {
        const pt = selPts[i]
        path.deleteSeg(pt)
      }

      // Cleanup
      const cleanup = function () {
        const segList = path.elem.pathSegList
        let len = segList.numberOfItems

        const remItems = function (pos, count) {
          while (count--) {
            segList.removeItem(pos)
          }
        }

        if (len <= 1) { return true }

        while (len--) {
          const item = segList.getItem(len)
          if (item.pathSegType === 1) {
            const prev = segList.getItem(len - 1)
            const nprev = segList.getItem(len - 2)
            if (prev.pathSegType === 2) {
              remItems(len - 1, 2)
              cleanup()
              break
            } else if (nprev.pathSegType === 2) {
              remItems(len - 2, 3)
              cleanup()
              break
            }
          } else if (item.pathSegType === 2 && len > 0) {
            const prevType = segList.getItem(len - 1).pathSegType
            // Path has M M
            if (prevType === 2) {
              remItems(len - 1, 1)
              cleanup()
              break
              // Entire path ends with Z M
            } else if (prevType === 1 && segList.numberOfItems - 1 === len) {
              remItems(len, 1)
              cleanup()
              break
            }
          }
        }
        return false
      }

      cleanup()

      // Completely delete a path with 1 or 0 segments
      if (path.elem.pathSegList.numberOfItems <= 1) {
        pathActionsMethod.toSelectMode(path.elem)
        svgCanvas.canvas.deleteSelectedElements()
        return
      }

      path.init()
      path.clearSelection()

      // TODO: Find right way to select point now
      // path.selectPt(selPt);
      if (window.opera) { // Opera repaints incorrectly
        path.elem.setAttribute('d', path.elem.getAttribute('d'))
      }
      path.endChanges('Delete path node(s)')
    },
    // Can't seem to use `@borrows` here, so using `@see`
    /**
    * Smooth polyline into path.
    * @function module:path.pathActions.smoothPolylineIntoPath
    * @see module:path~smoothPolylineIntoPath
    */
    smoothPolylineIntoPath,
    /* eslint-enable  */
    /**
    * @param {?Integer} v See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
    * @returns {void}
    */
    setSegType (v) {
      path?.setSegType(v)
    },
    /**
    * @param {string} attr
    * @param {Float} newValue
    * @returns {void}
    */
    moveNode (attr, newValue) {
      const selPts = path.selected_pts
      if (!selPts.length) { return }

      path.storeD()

      // Get first selected point
      const seg = path.segs[selPts[0]]
      const diff = { x: 0, y: 0 }
      diff[attr] = newValue - seg.item[attr]

      seg.move(diff.x, diff.y)
      path.endChanges('Move path point')
    },
    /**
    * @param {Element} elem
    * @returns {void}
    */
    fixEnd (elem) {
      // Adds an extra segment if the last seg before a Z doesn't end
      // at its M point
      // M0,0 L0,100 L100,100 z
      const segList = elem.pathSegList
      const len = segList.numberOfItems
      let lastM
      for (let i = 0; i < len; ++i) {
        const item = segList.getItem(i)
        if (item.pathSegType === 2) { // 2 => M segment type (move to)
          lastM = item
        }

        if (item.pathSegType === 1) { // 1 => Z segment type (close path)
          const prev = segList.getItem(i - 1)
          if (prev.x !== lastM.x || prev.y !== lastM.y) {
            // Add an L segment here
            const newseg = elem.createSVGPathSegLinetoAbs(lastM.x, lastM.y)
            segList.insertItemBefore(newseg, i)
            // Can this be done better?
            pathActionsMethod.fixEnd(elem)
            break
          }
        }
      }
    },
    // Can't seem to use `@borrows` here, so using `@see`
    /**
    * Convert a path to one with only absolute or relative values.
    * @function module:path.pathActions.convertPath
    * @see module:path.convertPath
    */
    convertPath
  })
})()
// end pathActions