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

  const DEFAULT_SETTINGS = Object.freeze({
    anchors: true,
    segments: true,
    intersections: true,
    midpoints: true,
    centers: true,
    tangents: true,
    connections: true
  });
  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const distance = (a, b) => Math.hypot(finite(a?.x) - finite(b?.x), finite(a?.y) - finite(b?.y));
  const point = (value) => ({ x: finite(value?.x ?? value?.[0]), y: finite(value?.y ?? value?.[1]) });

  function normalizeSettings(value = {}) {
    return Object.fromEntries(Object.keys(DEFAULT_SETTINGS).map((key) => [key, value[key] !== false]));
  }

  function closestPointOnSegment(value, startValue, endValue) {
    const probe = point(value); const start = point(startValue); const end = point(endValue);
    const dx = end.x - start.x; const dy = end.y - start.y; const lengthSquared = dx * dx + dy * dy;
    const t = lengthSquared > 1e-12 ? Math.max(0, Math.min(1, ((probe.x - start.x) * dx + (probe.y - start.y) * dy) / lengthSquared)) : 0;
    return { x: start.x + dx * t, y: start.y + dy * t, t };
  }

  function segmentIntersection(leftValue, rightValue) {
    const a = point(leftValue?.a); const b = point(leftValue?.b); const c = point(rightValue?.a); const d = point(rightValue?.b);
    const abX = b.x - a.x; const abY = b.y - a.y; const cdX = d.x - c.x; const cdY = d.y - c.y;
    const denominator = abX * cdY - abY * cdX; if (Math.abs(denominator) < 1e-10) return null;
    const acX = c.x - a.x; const acY = c.y - a.y;
    const t = (acX * cdY - acY * cdX) / denominator; const u = (acX * abY - acY * abX) / denominator;
    if (t < -1e-8 || t > 1 + 1e-8 || u < -1e-8 || u > 1 + 1e-8) return null;
    return { x: a.x + abX * t, y: a.y + abY * t, t, u };
  }

  function segmentBoundsNear(segment, probe, threshold) {
    const a = point(segment.a); const b = point(segment.b);
    return probe.x >= Math.min(a.x, b.x) - threshold && probe.x <= Math.max(a.x, b.x) + threshold
      && probe.y >= Math.min(a.y, b.y) - threshold && probe.y <= Math.max(a.y, b.y) + threshold;
  }

  function tangentCandidate(segment, probe, origin, threshold) {
    const a = point(segment.a); const b = point(segment.b); const dx = b.x - a.x; const dy = b.y - a.y;
    const length = Math.hypot(dx, dy); if (length < 1e-8) return null;
    // Eine abgeflachte Kurvensehne ist tangential, wenn ihre verlängerte Gerade
    // den Ausgangspunkt schneidet. Die Toleranz skaliert mit der Sehnenlänge.
    const lineDistance = Math.abs((origin.x - a.x) * dy - (origin.y - a.y) * dx) / length;
    if (lineDistance > Math.max(1.25, Math.min(threshold * .55, length * .22))) return null;
    const candidate = closestPointOnSegment(probe, a, b);
    if (distance(candidate, probe) > threshold) return null;
    return { ...candidate, angle: Math.atan2(dy, dx) * 180 / Math.PI };
  }

  const kindSetting = Object.freeze({
    anchor: 'anchors', segment: 'segments', intersection: 'intersections', midpoint: 'midpoints', center: 'centers', tangent: 'tangents', connection: 'connections'
  });
  const priority = Object.freeze({ connection: 0, anchor: 1, intersection: 2, midpoint: 3, tangent: 4, segment: 5, center: 6 });

  function snapPoint(value, scene = {}, options = {}) {
    const probe = point(value); const threshold = Math.max(.01, finite(options.threshold, 7)); const settings = normalizeSettings(options.settings);
    const excluded = new Set(options.excludeObjectIds || []); const candidates = [];
    const add = (candidate) => {
      if (!candidate || excluded.has(candidate.objectId)) return;
      const setting = kindSetting[candidate.kind]; if (setting && !settings[setting]) return;
      const measured = distance(probe, candidate); if (measured > threshold) return;
      candidates.push({ ...candidate, x: finite(candidate.x), y: finite(candidate.y), distance: measured });
    };

    (scene.points || []).forEach((target) => add(target));
    const segments = (scene.segments || []).filter((segment) => !excluded.has(segment.objectId));
    if (settings.segments) segments.forEach((segment) => {
      const projected = closestPointOnSegment(probe, segment.a, segment.b);
      if (projected.t > 1e-5 && projected.t < 1 - 1e-5) add({ ...projected, kind: 'segment', objectId: segment.objectId, pathId: segment.pathId, segmentIndex: segment.segmentIndex });
    });

    if (settings.intersections) {
      const nearby = segments.filter((segment) => segmentBoundsNear(segment, probe, threshold));
      for (let left = 0; left < nearby.length; left += 1) for (let right = left + 1; right < nearby.length; right += 1) {
        const first = nearby[left]; const second = nearby[right];
        if (first.pathId === second.pathId) continue;
        const intersection = segmentIntersection(first, second);
        if (intersection) add({ ...intersection, kind: 'intersection', objectId: `${first.objectId}:${second.objectId}`, objectIds: [first.objectId, second.objectId] });
      }
    }

    if (settings.tangents && options.tangentOrigin) {
      const origin = point(options.tangentOrigin);
      segments.forEach((segment) => {
        const candidate = tangentCandidate(segment, probe, origin, threshold);
        if (candidate) add({ ...candidate, kind: 'tangent', objectId: segment.objectId, pathId: segment.pathId, angle: candidate.angle, origin });
      });
    }

    // Spezifische Ziele gewinnen innerhalb der gemeinsamen Fangtoleranz vor
    // allgemeinen Projektionen. Sonst würde etwa das mathematisch etwas näher
    // liegende Segment seinen eigenen Schnittpunkt praktisch unerreichbar machen.
    candidates.sort((left, right) => (priority[left.kind] ?? 99) - (priority[right.kind] ?? 99) || left.distance - right.distance);
    const target = candidates[0] || null;
    return target ? { x: target.x, y: target.y, target, distance: target.distance } : { x: probe.x, y: probe.y, target: null, distance: Infinity };
  }

  const api = Object.freeze({ DEFAULT_SETTINGS, normalizeSettings, closestPointOnSegment, segmentIntersection, snapPoint });
  root.PyroSnap = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
