Source: utils/geometry.js

/**
 * Geometry helpers for computing node port positions and converting between
 * screen and canvas coordinate spaces (accounting for zoom and pan).
 *
 * @module utils/geometry
 */

// Allowed zoom range, shared with the reducer's SET_ZOOM clamp so fit-to-screen
// never computes a zoom the store would silently reject.
export const ZOOM_MIN = 0.25
export const ZOOM_MAX = 2

/**
 * Return the rendered pixel size of a node, which depends on its type: filter
 * diamonds and start/end control nodes are smaller than regular components.
 *
 * @param {Object} node - A workflow node with a `type` field.
 * @returns {{width: number, height: number}} The node's width and height in canvas pixels.
 */
export function getNodeSize(node) {
  if (node.type === 'filter_point') return { width: 90, height: 90 }
  if (node.type === 'start_point' || node.type === 'end_point') return { width: 100, height: 80 }
  return { width: 160, height: 80 }
}

/**
 * Compute the zoom and pan that fit every node within the viewport, centered and
 * with a margin. Zoom is clamped to [ZOOM_MIN, ZOOM_MAX]; pan is derived from the
 * canvas mapping `screen = (canvas + pan) * zoom` so the node bounding box's
 * center lands at the viewport center.
 *
 * @param {Object[]} nodes - Workflow nodes with numeric `x`, `y` and a `type`.
 * @param {{width: number, height: number}} viewport - Visible canvas size in screen pixels.
 * @param {number} [padding=60] - Screen-pixel margin to leave on every side.
 * @returns {{zoom: number, pan: {x: number, y: number}}|null} The fit transform, or null when there are no nodes.
 */
export function computeFitToScreen(nodes, viewport, padding = 60) {
  if (!nodes || nodes.length === 0) return null

  // Bounding box of all nodes in canvas coordinates
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
  nodes.forEach(node => {
    const { width, height } = getNodeSize(node)
    minX = Math.min(minX, node.x)
    minY = Math.min(minY, node.y)
    maxX = Math.max(maxX, node.x + width)
    maxY = Math.max(maxY, node.y + height)
  })

  const boxWidth = maxX - minX
  const boxHeight = maxY - minY

  // Zoom so the padded box fits both dimensions; clamp to the allowed range.
  const zoomForWidth = (viewport.width - 2 * padding) / boxWidth
  const zoomForHeight = (viewport.height - 2 * padding) / boxHeight
  const zoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.min(zoomForWidth, zoomForHeight)))

  // Pan so the box center lands at the viewport center.
  // screen = (canvas + pan) * zoom  =>  pan = (screen / zoom) - canvas
  const boxCenterX = (minX + maxX) / 2
  const boxCenterY = (minY + maxY) / 2
  const pan = {
    x: (viewport.width / 2) / zoom - boxCenterX,
    y: (viewport.height / 2) / zoom - boxCenterY
  }

  return { zoom, pan }
}

/**
 * Compute the canvas coordinates of a node's input or output port.
 *
 * @param {Object} node - Node with numeric `x` and `y` top-left canvas coordinates.
 * @param {string} portType - Which port to locate; `'output'` for the right edge, otherwise the left edge.
 * @param {number} [nodeWidth=160] - Node width in canvas units.
 * @param {number} [nodeHeight=80] - Node height in canvas units.
 * @returns {Object} Point `{ x, y }` in canvas coordinates at the vertical center of the node's edge.
 */
export function getPortPosition(node, portType, nodeWidth = 160, nodeHeight = 80) {
  if (portType === 'output') {
    return {
      x: node.x + nodeWidth,
      y: node.y + nodeHeight / 2
    }
  }
  return {
    x: node.x,
    y: node.y + nodeHeight / 2
  }
}

/**
 * Convert a screen (viewport) point into canvas coordinates.
 *
 * @param {number} screenX - Horizontal screen coordinate (e.g. from a mouse event).
 * @param {number} screenY - Vertical screen coordinate.
 * @param {Object} canvasRect - Canvas bounding rectangle with `left` and `top` offsets.
 * @param {number} zoom - Current zoom factor.
 * @param {Object} pan - Current pan offset with `x` and `y` in canvas units.
 * @returns {Object} Point `{ x, y }` in canvas coordinates.
 */
export function screenToCanvas(screenX, screenY, canvasRect, zoom, pan) {
  return {
    x: (screenX - canvasRect.left) / zoom - pan.x,
    y: (screenY - canvasRect.top) / zoom - pan.y
  }
}

/**
 * Convert a canvas point into screen (viewport) coordinates.
 *
 * @param {number} canvasX - Horizontal canvas coordinate.
 * @param {number} canvasY - Vertical canvas coordinate.
 * @param {Object} canvasRect - Canvas bounding rectangle with `left` and `top` offsets.
 * @param {number} zoom - Current zoom factor.
 * @param {Object} pan - Current pan offset with `x` and `y` in canvas units.
 * @returns {Object} Point `{ x, y }` in screen coordinates.
 */
export function canvasToScreen(canvasX, canvasY, canvasRect, zoom, pan) {
  return {
    x: (canvasX + pan.x) * zoom + canvasRect.left,
    y: (canvasY + pan.y) * zoom + canvasRect.top
  }
}