(function attachPyroPathMetrics(root) {
  'use strict';

  const EPSILON = 1e-9;
  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const cleanZero = (value) => Object.is(value, -0) || Math.abs(value) <= EPSILON ? 0 : value;
  const point = (value) => Array.isArray(value)
    ? [finite(value[0]), finite(value[1])]
    : [finite(value?.x), finite(value?.y)];
  const distance = (left, right) => Math.hypot(right[0] - left[0], right[1] - left[1]);
  const isCubic = (value) => Array.isArray(value?.c1) && Array.isArray(value?.c2);

  function transformPoint(value, transform = null) {
    const source = point(value);
    if (typeof transform === 'function') return point(transform(source));
    if (!transform) return source;
    const matrix = Array.isArray(transform)
      ? { a: transform[0], b: transform[1], c: transform[2], d: transform[3], e: transform[4], f: transform[5] }
      : transform;
    const a = finite(matrix?.a, 1); const b = finite(matrix?.b); const c = finite(matrix?.c);
    const d = finite(matrix?.d, 1); const e = finite(matrix?.e); const f = finite(matrix?.f);
    return [a * source[0] + c * source[1] + e, b * source[0] + d * source[1] + f];
  }

  function cubicPoint(startValue, control1Value, control2Value, endValue, progress) {
    const start = point(startValue); const control1 = point(control1Value); const control2 = point(control2Value); const end = point(endValue);
    const t = Math.max(0, Math.min(1, finite(progress))); const inverse = 1 - t;
    return [
      inverse ** 3 * start[0] + 3 * inverse ** 2 * t * control1[0] + 3 * inverse * t ** 2 * control2[0] + t ** 3 * end[0],
      inverse ** 3 * start[1] + 3 * inverse ** 2 * t * control1[1] + 3 * inverse * t ** 2 * control2[1] + t ** 3 * end[1]
    ];
  }

  function splitCubic(startValue, control1Value, control2Value, endValue) {
    const start = point(startValue); const control1 = point(control1Value); const control2 = point(control2Value); const end = point(endValue);
    const a = [(start[0] + control1[0]) / 2, (start[1] + control1[1]) / 2];
    const b = [(control1[0] + control2[0]) / 2, (control1[1] + control2[1]) / 2];
    const c = [(control2[0] + end[0]) / 2, (control2[1] + end[1]) / 2];
    const d = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; const e = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2];
    const middle = [(d[0] + e[0]) / 2, (d[1] + e[1]) / 2];
    return [[start, a, d, middle], [middle, e, c, end]];
  }

  function distanceToLine(value, start, end) {
    const dx = end[0] - start[0]; const dy = end[1] - start[1]; const length = Math.hypot(dx, dy);
    if (length <= EPSILON) return distance(value, start);
    return Math.abs(dy * value[0] - dx * value[1] + end[0] * start[1] - end[1] * start[0]) / length;
  }

  function cubicFlatness(curve) {
    const [start, control1, control2, end] = curve; const chord = distance(start, end);
    const polygon = distance(start, control1) + distance(control1, control2) + distance(control2, end);
    return Math.max(distanceToLine(control1, start, end), distanceToLine(control2, start, end), (polygon - chord) / 2);
  }

  function flattenCubic(start, control1, control2, end, options = {}) {
    const tolerance = Math.max(1e-7, finite(options.tolerance, .25)); const maxDepth = Math.max(1, Math.min(24, Math.round(finite(options.maxDepth, 14))));
    const output = [point(start)];
    const visit = (curve, depth) => {
      if (depth >= maxDepth || cubicFlatness(curve) <= tolerance) { output.push(point(curve[3])); return; }
      const [left, right] = splitCubic(...curve); visit(left, depth + 1); visit(right, depth + 1);
    };
    visit([point(start), point(control1), point(control2), point(end)], 0); return output;
  }

  function flattenPath(value = {}, options = {}) {
    const sourcePoints = (Array.isArray(value.points) ? value.points : []).map((entry) => transformPoint(entry, options.transform));
    const closed = Boolean(value.closed && sourcePoints.length >= 2);
    if (sourcePoints.length < 2) return { points: sourcePoints, sourceSegments: [], closed: false };
    const output = [sourcePoints[0]]; const sourceSegments = [];
    const append = (next, sourceIndex) => {
      const target = point(next);
      if (distance(output.at(-1), target) <= EPSILON) return;
      output.push(target); sourceSegments.push(sourceIndex);
    };
    const count = closed ? sourcePoints.length : sourcePoints.length - 1;
    for (let index = 0; index < count; index += 1) {
      const start = sourcePoints[index]; const end = sourcePoints[(index + 1) % sourcePoints.length]; const segment = value.segments?.[index];
      if (isCubic(segment)) {
        const control1 = transformPoint(segment.c1, options.transform); const control2 = transformPoint(segment.c2, options.transform);
        flattenCubic(start, control1, control2, end, options).slice(1).forEach((entry) => append(entry, index));
      } else append(end, index);
    }
    return { points: output, sourceSegments, closed };
  }

  function metricsFromFlattened(flattened) {
    const points = flattened.points.map(point); const cumulative = [0]; const segments = [];
    for (let index = 0; index < points.length - 1; index += 1) {
      const length = distance(points[index], points[index + 1]);
      if (length <= EPSILON) continue;
      const from = cumulative.at(-1); const to = from + length;
      segments.push({ start: points[index], end: points[index + 1], length, from, to, sourceSegment: flattened.sourceSegments[index] ?? index });
      cumulative.push(to);
    }
    const normalizedPoints = segments.length ? [segments[0].start, ...segments.map((segment) => segment.end)] : points.slice(0, 1);
    return { points: normalizedPoints, segments, cumulative, length: cumulative.at(-1) || 0, closed: Boolean(flattened.closed) };
  }

  function normalizedOffset(value, closed) {
    const raw = finite(value);
    return closed ? ((raw % 1) + 1) % 1 : Math.max(0, Math.min(1, raw));
  }

  function sampleAtDistance(metrics, rawDistance, rawOffset = null) {
    if (!metrics.segments.length || metrics.length <= EPSILON) {
      const first = metrics.points[0] || [0, 0];
      return { x: first[0], y: first[1], offset: 0, pathDistance: 0, tangent: { x: 1, y: 0, angle: 0 }, normal: { x: 0, y: 1 }, segmentIndex: -1, sourceSegment: -1 };
    }
    const pathDistance = Math.max(0, Math.min(metrics.length, finite(rawDistance))); let low = 0; let high = metrics.segments.length - 1;
    while (low < high) {
      const middle = Math.floor((low + high) / 2);
      if (pathDistance <= metrics.segments[middle].to) high = middle; else low = middle + 1;
    }
    const segment = metrics.segments[low]; const progress = segment.length > EPSILON ? Math.max(0, Math.min(1, (pathDistance - segment.from) / segment.length)) : 0;
    const tangentX = cleanZero((segment.end[0] - segment.start[0]) / segment.length); const tangentY = cleanZero((segment.end[1] - segment.start[1]) / segment.length);
    return {
      x: segment.start[0] + (segment.end[0] - segment.start[0]) * progress,
      y: segment.start[1] + (segment.end[1] - segment.start[1]) * progress,
      offset: rawOffset == null ? pathDistance / metrics.length : rawOffset, pathDistance,
      tangent: { x: tangentX, y: tangentY, angle: Math.atan2(tangentY, tangentX) * 180 / Math.PI },
      normal: { x: cleanZero(-tangentY), y: tangentX }, segmentIndex: low, sourceSegment: segment.sourceSegment
    };
  }

  function pointAtOffset(metrics, offset) {
    const normalized = normalizedOffset(offset, metrics.closed);
    return sampleAtDistance(metrics, normalized * metrics.length, normalized);
  }

  function nearestOffset(metrics, value) {
    const target = point(value);
    if (!metrics.segments.length || metrics.length <= EPSILON) {
      const sample = pointAtOffset(metrics, 0); return { ...sample, distance: distance([sample.x, sample.y], target) };
    }
    let best = null;
    metrics.segments.forEach((segment, index) => {
      const dx = segment.end[0] - segment.start[0]; const dy = segment.end[1] - segment.start[1];
      const progress = Math.max(0, Math.min(1, ((target[0] - segment.start[0]) * dx + (target[1] - segment.start[1]) * dy) / (segment.length * segment.length)));
      const x = segment.start[0] + dx * progress; const y = segment.start[1] + dy * progress; const squared = (target[0] - x) ** 2 + (target[1] - y) ** 2;
      if (!best || squared < best.squared) best = { squared, x, y, index, pathDistance: segment.from + segment.length * progress };
    });
    const offset = metrics.length > EPSILON ? best.pathDistance / metrics.length : 0;
    const sample = sampleAtDistance(metrics, best.pathDistance, metrics.closed ? normalizedOffset(offset, true) : offset);
    return { ...sample, x: best.x, y: best.y, distance: Math.sqrt(best.squared) };
  }

  function signedArea(value) {
    const points = Array.isArray(value) ? value.map(point) : value?.points?.map(point) || [];
    const source = points.length > 2 && distance(points[0], points.at(-1)) <= EPSILON ? points.slice(0, -1) : points;
    return source.reduce((sum, current, index) => {
      const next = source[(index + 1) % source.length]; return sum + current[0] * next[1] - next[0] * current[1];
    }, 0) / 2;
  }

  function measurePath(path, options = {}) {
    const metrics = metricsFromFlattened(flattenPath(path, options));
    metrics.area = metrics.closed ? signedArea(metrics.points) : 0;
    metrics.at = (offset) => pointAtOffset(metrics, offset);
    metrics.nearestOffset = (value) => nearestOffset(metrics, value);
    return metrics;
  }

  const api = Object.freeze({
    transformPoint, cubicPoint, splitCubic, flattenCubic, flattenPath, measurePath,
    pointAtOffset, nearestOffset, signedArea
  });
  root.PyroPathMetrics = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : self);
