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

  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clonePoint = (point) => [finite(point?.[0]), finite(point?.[1])];
  const distance = (left, right) => Math.hypot(finite(left?.[0]) - finite(right?.[0]), finite(left?.[1]) - finite(right?.[1]));
  const isBezier = (segment) => Array.isArray(segment?.c1) && Array.isArray(segment?.c2)
    && segment.c1.length >= 2 && segment.c2.length >= 2
    && segment.c1.every((value) => Number.isFinite(Number(value)))
    && segment.c2.every((value) => Number.isFinite(Number(value)));
  const cloneSegment = (segment) => isBezier(segment) ? { c1: clonePoint(segment.c1), c2: clonePoint(segment.c2) } : null;
  const allowedModes = new Set(['corner', 'smooth', 'symmetric']);
  const cloneMode = (mode) => allowedModes.has(mode) ? mode : 'corner';
  const segmentCount = (path) => path.closed ? path.points.length : Math.max(0, path.points.length - 1);
  const nextIndex = (path, index) => index + 1 < path.points.length ? index + 1 : path.closed ? 0 : -1;
  const previousIndex = (path, index) => index > 0 ? index - 1 : path.closed ? path.points.length - 1 : -1;

  function normalizePath(value = {}) {
    const points = (Array.isArray(value.points) ? value.points : [])
      .filter((point) => Array.isArray(point) && point.length >= 2)
      .map(clonePoint);
    const closed = Boolean(value.closed && points.length >= 3);
    const count = closed ? points.length : Math.max(0, points.length - 1);
    return {
      points,
      closed,
      segments: Array.from({ length: count }, (_, index) => cloneSegment(value.segments?.[index] ?? value.bezierSegments?.[index])),
      pointModes: Array.from({ length: points.length }, (_, index) => cloneMode(value.pointModes?.[index]))
    };
  }

  function defaultControls(path, index) {
    const endIndex = nextIndex(path, index);
    if (endIndex < 0) return null;
    const start = path.points[index]; const end = path.points[endIndex];
    return {
      c1: [start[0] + (end[0] - start[0]) / 3, start[1] + (end[1] - start[1]) / 3],
      c2: [start[0] + (end[0] - start[0]) * 2 / 3, start[1] + (end[1] - start[1]) * 2 / 3]
    };
  }

  function ensureSegment(path, index) {
    if (index < 0 || index >= segmentCount(path)) return null;
    if (!isBezier(path.segments[index])) path.segments[index] = defaultControls(path, index);
    return path.segments[index];
  }

  function coupledPoint(anchor, moved, existing, symmetric) {
    const dx = moved[0] - anchor[0]; const dy = moved[1] - anchor[1]; const movedLength = Math.hypot(dx, dy);
    if (movedLength < 1e-8) return clonePoint(anchor);
    const existingLength = symmetric ? movedLength : Math.max(1e-8, distance(anchor, existing));
    return [anchor[0] - dx / movedLength * existingLength, anchor[1] - dy / movedLength * existingLength];
  }

  function moveHandle(value, segmentIndex, control, point) {
    const path = normalizePath(value); const segment = ensureSegment(path, segmentIndex);
    if (!segment || !['c1', 'c2'].includes(control)) return path;
    const moved = clonePoint(point); segment[control] = moved;
    const anchorIndex = control === 'c1' ? segmentIndex : nextIndex(path, segmentIndex);
    if (anchorIndex < 0) return path;
    const mode = path.pointModes[anchorIndex] || 'corner';
    if (mode === 'corner') return path;
    const oppositeSegmentIndex = control === 'c1' ? previousIndex(path, anchorIndex) : anchorIndex;
    const oppositeControl = control === 'c1' ? 'c2' : 'c1';
    const oppositeSegment = ensureSegment(path, oppositeSegmentIndex);
    if (!oppositeSegment || oppositeSegment === segment) return path;
    oppositeSegment[oppositeControl] = coupledPoint(path.points[anchorIndex], moved, oppositeSegment[oppositeControl], mode === 'symmetric');
    return path;
  }

  function setPointModes(value, indices, mode) {
    let path = normalizePath(value); const nextMode = cloneMode(mode);
    const unique = [...new Set((Array.isArray(indices) ? indices : []).filter((index) => Number.isInteger(index) && path.points[index]))];
    unique.forEach((index) => { path.pointModes[index] = nextMode; });
    if (nextMode === 'corner') return path;
    unique.forEach((index) => {
      const outgoingIndex = index < segmentCount(path) ? index : -1;
      const incomingIndex = previousIndex(path, index);
      if (outgoingIndex >= 0 && incomingIndex >= 0) {
        const outgoing = ensureSegment(path, outgoingIndex);
        ensureSegment(path, incomingIndex);
        path = moveHandle(path, outgoingIndex, 'c1', outgoing.c1);
      }
    });
    return path;
  }

  function reversePath(value) {
    const path = normalizePath(value); const count = path.points.length;
    if (count < 2) return path;
    const points = [...path.points].reverse().map(clonePoint);
    const pointModes = [...path.pointModes].reverse();
    const segments = Array.from({ length: segmentCount(path) }, (_, index) => {
      const oldIndex = path.closed ? (count - 2 - index + count) % count : count - 2 - index;
      const old = path.segments[oldIndex];
      return isBezier(old) ? { c1: clonePoint(old.c2), c2: clonePoint(old.c1) } : null;
    });
    return { points, pointModes, segments, closed: path.closed };
  }

  function openPathAt(value, anchorIndex) {
    const path = normalizePath(value); const count = path.points.length;
    if (!path.closed || count < 3 || !Number.isInteger(anchorIndex) || !path.points[anchorIndex]) return path;
    const order = Array.from({ length: count }, (_, offset) => (anchorIndex + offset) % count);
    return {
      points: order.map((index) => clonePoint(path.points[index])),
      pointModes: order.map((index) => path.pointModes[index]),
      segments: order.slice(0, -1).map((index) => cloneSegment(path.segments[index])),
      closed: false
    };
  }

  function closePath(value) {
    const path = normalizePath(value);
    if (path.points.length < 3) return path;
    path.closed = true;
    path.segments = Array.from({ length: path.points.length }, (_, index) => cloneSegment(path.segments[index]));
    return path;
  }

  function orientForJoin(value, endpointIndex, asStart) {
    let path = normalizePath(value);
    if (path.closed || path.points.length < 2) return path;
    const isStart = endpointIndex === 0; const isEnd = endpointIndex === path.points.length - 1;
    if (!isStart && !isEnd) return path;
    if ((asStart && isEnd) || (!asStart && isStart)) path = reversePath(path);
    return path;
  }

  function joinPaths(leftValue, leftEndpoint, rightValue, rightEndpoint, epsilon = .01) {
    const left = orientForJoin(leftValue, leftEndpoint, false);
    const right = orientForJoin(rightValue, rightEndpoint, true);
    if (left.closed || right.closed || left.points.length < 2 || right.points.length < 2) return null;
    const mergedEndpoint = distance(left.points.at(-1), right.points[0]) <= Math.max(0, finite(epsilon, .01));
    const points = [...left.points.map(clonePoint), ...right.points.slice(mergedEndpoint ? 1 : 0).map(clonePoint)];
    const pointModes = [...left.pointModes, ...right.pointModes.slice(mergedEndpoint ? 1 : 0)];
    const segments = mergedEndpoint
      ? [...left.segments.map(cloneSegment), ...right.segments.map(cloneSegment)]
      : [...left.segments.map(cloneSegment), null, ...right.segments.map(cloneSegment)];
    return { points, pointModes, segments, closed: false };
  }

  function perpendicularDistance(point, start, end) {
    const dx = end[0] - start[0]; const dy = end[1] - start[1]; const lengthSquared = dx * dx + dy * dy;
    if (lengthSquared < 1e-12) return distance(point, start);
    const t = Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / lengthSquared));
    return distance(point, [start[0] + dx * t, start[1] + dy * t]);
  }

  function rdp(points, tolerance) {
    if (points.length <= 2) return points.map(clonePoint);
    let maximum = -1; let split = -1;
    for (let index = 1; index < points.length - 1; index += 1) {
      const current = perpendicularDistance(points[index], points[0], points.at(-1));
      if (current > maximum) { maximum = current; split = index; }
    }
    if (maximum <= tolerance || split < 0) return [clonePoint(points[0]), clonePoint(points.at(-1))];
    return [...rdp(points.slice(0, split + 1), tolerance).slice(0, -1), ...rdp(points.slice(split), tolerance)];
  }

  function simplifyPath(value, tolerance = 1) {
    const path = normalizePath(value); const limit = Math.max(0, finite(tolerance, 1));
    if (path.points.length <= (path.closed ? 3 : 2) || limit <= 0) return path;
    let points;
    if (path.closed) {
      // Der entfernteste Punkt vom ersten Punkt bildet eine stabile zweite
      // Teilgrenze; so kollabiert ein geschlossener Ring nicht an der Naht.
      let opposite = 1; let farthest = 0;
      for (let index = 1; index < path.points.length; index += 1) {
        const current = distance(path.points[0], path.points[index]);
        if (current > farthest) { farthest = current; opposite = index; }
      }
      const first = rdp(path.points.slice(0, opposite + 1), limit);
      const second = rdp([...path.points.slice(opposite), path.points[0]], limit);
      points = [...first.slice(0, -1), ...second.slice(0, -1)];
      if (points.length < 3) points = path.points.slice(0, 3).map(clonePoint);
    } else points = rdp(path.points, limit);
    return {
      points,
      closed: path.closed,
      segments: Array.from({ length: path.closed ? points.length : Math.max(0, points.length - 1) }, () => null),
      pointModes: Array.from({ length: points.length }, () => 'corner')
    };
  }

  function smoothPath(value, strength = 50, selectedSegments = null) {
    const path = normalizePath(value); const amount = Math.max(0, Math.min(100, finite(strength, 50))) / 100;
    const chosen = Array.isArray(selectedSegments) && selectedSegments.length
      ? new Set(selectedSegments.filter((index) => Number.isInteger(index)))
      : new Set(Array.from({ length: segmentCount(path) }, (_, index) => index));
    chosen.forEach((index) => {
      const following = nextIndex(path, index); if (following < 0) return;
      if (amount <= 0) { path.segments[index] = null; return; }
      const previous = previousIndex(path, index); const after = nextIndex(path, following);
      const p0 = path.points[previous >= 0 ? previous : index]; const p1 = path.points[index];
      const p2 = path.points[following]; const p3 = path.points[after >= 0 ? after : following];
      path.segments[index] = {
        c1: [p1[0] + (p2[0] - p0[0]) * amount / 6, p1[1] + (p2[1] - p0[1]) * amount / 6],
        c2: [p2[0] - (p3[0] - p1[0]) * amount / 6, p2[1] - (p3[1] - p1[1]) * amount / 6]
      };
      path.pointModes[index] = 'smooth'; path.pointModes[following] = 'smooth';
    });
    return path;
  }

  function deleteSegment(value, index) {
    const path = normalizePath(value); const count = segmentCount(path);
    if (!Number.isInteger(index) || index < 0 || index >= count) return [];
    if (path.closed) return [openPathAt(path, nextIndex(path, index))];
    const leftPoints = path.points.slice(0, index + 1); const rightPoints = path.points.slice(index + 1);
    const make = (points, modes, segments) => points.length >= 2 ? {
      points: points.map(clonePoint), pointModes: modes.map(cloneMode), segments: segments.map(cloneSegment), closed: false
    } : null;
    return [
      make(leftPoints, path.pointModes.slice(0, index + 1), path.segments.slice(0, Math.max(0, index))),
      make(rightPoints, path.pointModes.slice(index + 1), path.segments.slice(index + 1))
    ].filter(Boolean);
  }

  const api = Object.freeze({
    normalizePath, reversePath, openPathAt, closePath, joinPaths, simplifyPath,
    smoothPath, deleteSegment, moveHandle, setPointModes, isBezier
  });
  root.PyroPathWorkbench = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : self);
