# model/Symbol.js

    function mergeBounds (boundsA, boundsB) {
      return {
        minX: Math.min(boundsA.minX, boundsB.minX),
        maxX: Math.max(boundsA.maxX, boundsB.maxX),
        minY: Math.min(boundsA.minY, boundsB.minY),
        maxY: Math.max(boundsA.maxY, boundsB.maxY)
      }
    }

    function getLineBounds (line) {
      return {
        minX: Math.min(line.firstPoint.x, line.lastPoint.x),
        maxX: Math.max(line.firstPoint.x, line.lastPoint.x),
        minY: Math.min(line.firstPoint.y, line.lastPoint.y),
        maxY: Math.max(line.firstPoint.y, line.lastPoint.y)
      }
    }

    function getEllipseBounds (ellipse) {
      const angleStep = 0.02 // angle delta between interpolated points on the arc, in radian

      let z1 = Math.cos(ellipse.orientation)
      let z3 = Math.sin(ellipse.orientation)
      let z2 = z1
      let z4 = z3
      z1 *= ellipse.maxRadius
      z2 *= ellipse.minRadius
      z3 *= ellipse.maxRadius
      z4 *= ellipse.minRadius

      const n = Math.abs(ellipse.sweepAngle) / angleStep

      const x = []
      const y = []

      for (let i = 0; i <= n; i++) {
        const angle = ellipse.startAngle + ((i / n) * ellipse.sweepAngle)
        const alpha = Math.atan2(Math.sin(angle) / ellipse.minRadius, Math.cos(angle) / ellipse.maxRadius)

        const cosAlpha = Math.cos(alpha)
        const sinAlpha = Math.sin(alpha)

        x.push(ellipse.center.x + ((z1 * cosAlpha) - (z4 * sinAlpha)))
        y.push(ellipse.center.y + ((z2 * sinAlpha) + (z3 * cosAlpha)))
      }

      return {
        minX: Math.min(...x),
        maxX: Math.max(...x),
        minY: Math.min(...y),
        maxY: Math.max(...y)
      }
    }

    function getTextLineBounds (textLine) {
      return {
        minX: textLine.data.topLeftPoint.x,
        maxX: textLine.data.topLeftPoint.x + textLine.data.width,
        minY: textLine.data.topLeftPoint.y,
        maxY: textLine.data.topLeftPoint.y + textLine.data.height
      }
    }

    function getClefBounds (clef) {
      return {
        minX: clef.boundingBox.x,
        maxX: clef.boundingBox.x + clef.boundingBox.width,
        minY: clef.boundingBox.y,
        maxY: clef.boundingBox.y + clef.boundingBox.height
      }
    }

    function getStrokeBounds (stroke) {
      return {
        minX: Math.min(...stroke.x),
        maxX: Math.max(...stroke.x),
        minY: Math.min(...stroke.y),
        maxY: Math.max(...stroke.y)
      }
    }

    /**
     * Get the box enclosing the given symbols
     * @param {Array} symbols Symbols to extract bounds from
     * @param {Bounds} [bounds] Starting bounds for recursion
     * @return {Bounds} Bounding box enclosing symbols
     */
    export function getSymbolsBounds (symbols, bounds = { minX: Number.MAX_VALUE, maxX: Number.MIN_VALUE, minY: Number.MAX_VALUE, maxY: Number.MIN_VALUE }) {
      let boundsRef = bounds
      boundsRef = symbols
        .filter(symbol => symbol.type === 'stroke')
        .map(getStrokeBounds)
        .reduce(mergeBounds, boundsRef)
      boundsRef = symbols
        .filter(symbol => symbol.type === 'clef')
        .map(getClefBounds)
        .reduce(mergeBounds, boundsRef)
      boundsRef = symbols
        .filter(symbol => symbol.type === 'line')
        .map(getLineBounds)
        .reduce(mergeBounds, boundsRef)
      boundsRef = symbols
        .filter(symbol => symbol.type === 'ellipse')
        .map(getEllipseBounds)
        .reduce(mergeBounds, boundsRef)
      boundsRef = symbols
        .filter(symbol => symbol.type === 'textLine')
        .map(getTextLineBounds)
        .reduce(mergeBounds, boundsRef)
      return boundsRef
    }