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

  const PROJECT_FORMAT = 'pyro';
  const PROJECT_VERSION = 4;
  const Catalog = root.PyroToolCatalog || (typeof require === 'function' ? require('./src/tools/catalog.js') : null);
  const AutoShapes = root.PyroAutoShapes || (typeof require === 'function' ? require('./src/tools/auto-shapes.js') : null);
  const BitmapModel = root.PyroBitmapModel || (typeof require === 'function' ? require('./src/bitmap/model.js') : null);
  const FillPaint = root.PyroFillPaint || (typeof require === 'function' ? require('./src/fills/paint.js') : null);
  const StrokeStyle = root.PyroStrokeStyle || (typeof require === 'function' ? require('./src/strokes/style.js') : null);
  const FreeTransform = root.PyroFreeTransform || (typeof require === 'function' ? require('./src/geometry/free-transform.js') : null);
  const Effects = root.PyroEffects || (typeof require === 'function' ? require('./src/effects/stack.js') : null);
  const ReusableAssets = root.PyroReusableAssets || (typeof require === 'function' ? require('./src/model/reusable-assets.js') : null);
  const Security = root.PyroSecurityPolicy || (typeof require === 'function' ? require('./src/security/policy.js') : null);
  const DEFAULT_LIMITS = Object.freeze({
    canvasSize: Security?.LIMITS?.canvasSide || 10000,
    dpi: 2400,
    layers: 512,
    objects: 50000,
    groupDepth: 64,
    points: 250000,
    values: 1000000,
    fieldLength: 1000000,
    hrefLength: Security?.LIMITS?.embeddedImageCharacters || 48 * 1024 * 1024
  });

  class ProjectValidationError extends Error {
    constructor(message, code = 'INVALID_PROJECT') {
      super(message);
      this.name = 'ProjectValidationError';
      this.code = code;
    }
  }

  const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
  const round = (value, digits = 4) => Number(finite(value).toFixed(digits));
  const safeText = (value, fallback, maximum = 128) => {
    const text = String(value ?? '').replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, maximum);
    return text || fallback;
  };
  const isPlainObject = (value) => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
    const prototype = Object.getPrototypeOf(value);
    return prototype === null || prototype === Object.prototype;
  };
  const blockedKeys = Security?.BLOCKED_OBJECT_KEYS || new Set(['__proto__', 'prototype', 'constructor']);

  function mergeLimits(overrides) {
    const limits = { ...DEFAULT_LIMITS };
    if (!isPlainObject(overrides)) return limits;
    Object.keys(limits).forEach((key) => {
      if (Number.isFinite(Number(overrides[key])) && Number(overrides[key]) > 0) limits[key] = Number(overrides[key]);
    });
    return limits;
  }

  function copySerializable(value, context, depth = 0, skipChildren = false) {
    context.valueCount += 1;
    if (context.valueCount > context.limits.values) throw new ProjectValidationError('Das Projekt enthält zu viele Datenwerte', 'PROJECT_TOO_COMPLEX');
    if (value === null || typeof value === 'boolean') return value;
    if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
    if (typeof value === 'string') return value.slice(0, context.limits.fieldLength);
    if (Array.isArray(value)) {
      if (depth > context.limits.groupDepth + 8) throw new ProjectValidationError('Das Projekt ist zu tief verschachtelt', 'PROJECT_TOO_DEEP');
      return value.map((item) => copySerializable(item, context, depth + 1));
    }
    if (!isPlainObject(value)) return undefined;
    if (depth > context.limits.groupDepth + 8) throw new ProjectValidationError('Das Projekt ist zu tief verschachtelt', 'PROJECT_TOO_DEEP');
    const copy = {};
    Object.entries(value).forEach(([key, item]) => {
      if (skipChildren && key === 'children') return;
      if (blockedKeys.has(key) || typeof item === 'function' || typeof item === 'symbol' || item === undefined) return;
      const copied = copySerializable(item, context, depth + 1);
      if (copied !== undefined) copy[key] = copied;
    });
    return copy;
  }

  function safeImageHref(value, maximum) {
    if (Security?.safeEmbeddedImageHref) return Security.safeEmbeddedImageHref(value, maximum);
    const href = typeof value === 'string' && value.length <= maximum ? value : '';
    return /^data:image\/(?:png|jpe?g|webp|gif|svg\+xml)(?:;[^,]*)?,/i.test(href) ? href : '';
  }

  function sanitizeTextTexture(value, context) {
    if (!isPlainObject(value)) return null;
    const modes = new Set(['none', 'grain', 'waves', 'paper', 'canvas', 'wood', 'grunge', 'distressed', 'cracks', 'washed', 'outline', 'pixel']);
    const mode = modes.has(value.mode) ? value.mode : 'none';
    if (mode === 'none') return null;
    return {
      mode,
      scale: clamp(round(value.scale || 100), 20, 400),
      distortion: clamp(round(value.distortion), 0, 40),
      distribution: clamp(round(value.distribution), 0, 100),
      pixelSize: clamp(round(value.pixelSize == null ? 6 : value.pixelSize), 1, 128),
      gap: clamp(round(value.gap == null ? 1 : value.gap), 0, 64),
      pixelShape: value.pixelShape === 'round' ? 'round' : 'square',
      pixelFullCells: Boolean(value.pixelFullCells),
      amount: clamp(round(value.amount == null ? 45 : value.amount), 0, 100),
      seed: clamp(Math.round(finite(value.seed, 1)), 1, 9999),
      transparent: ['grain', 'waves', 'paper', 'canvas', 'wood'].includes(mode) && Boolean(value.transparent)
    };
  }

  function legacyTextTextureFillPaint(value, context) {
    if (!FillPaint || !isPlainObject(value)) return null;
    const href = safeImageHref(value.href, context.limits.hrefLength);
    const legacyMode = value.mode === 'image-tile' ? 'tile' : value.mode === 'image-cover' ? 'cover' : null;
    const explicitMode = ['none', 'cover', 'tile'].includes(value.imageMode) ? value.imageMode : null;
    const imageMode = explicitMode || legacyMode || (href ? 'cover' : 'none');
    if (!href || imageMode === 'none') return null;
    return FillPaint.normalizePaint({
      type: imageMode === 'tile' ? 'pattern' : 'image',
      href,
      scale: clamp(round(value.imageScale == null ? value.scale || 100 : value.imageScale), imageMode === 'cover' ? 100 : 20, 400),
      rotation: 0,
      offsetX: clamp(round(value.imageOffsetX), -10000, 10000),
      offsetY: clamp(round(value.imageOffsetY), -10000, 10000),
      imageWidth: value.imageWidth,
      imageHeight: value.imageHeight
    }, { fallbackColor: '#4f6df5', href });
  }

  function supportsObjectTexture(object) {
    if (!object) return false;
    if (object.type === 'text' || object.type === 'text-outline') return true;
    if (object.type === 'pen' || object.type === 'polyline') return object.closed === true;
    if (object.type === 'spiral') return object.autoShape?.closed === true;
    return Boolean(Catalog?.fillableVectorShapeTypes?.has(object.type));
  }

  function sanitizeTextPathBinding(value) {
    if (!isPlainObject(value)) return null;
    const targetId = safeText(value.targetId, '', 128);
    if (!targetId) return null;
    return {
      targetId,
      placement: value.placement === 'inside' ? 'inside' : 'outside',
      distance: clamp(round(value.distance), 0, 1000),
      offset: clamp(round(value.offset), -360, 360),
      orientation: value.orientation === 'horizontal' ? 'horizontal' : 'vertical',
      spacing: clamp(round(value.spacing), -100, 500),
      reversed: Boolean(value.reversed),
      targetVisible: value.targetVisible !== false
    };
  }

  function createSanitizeContext(options = {}) {
    return {
      limits: mergeLimits(options.limits),
      createId: typeof options.createId === 'function'
        ? options.createId
        : (prefix) => `${prefix}-${Math.random().toString(36).slice(2, 10)}`,
      ids: new Set(),
      objectCount: 0,
      pointCount: 0,
      valueCount: 0,
      validSymbolIds: null,
      warnings: []
    };
  }

  function uniqueId(value, prefix, context) {
    let id = safeText(value, '', 128).replace(/\s+/g, '-');
    if (!id || context.ids.has(id)) {
      if (id) context.warnings.push(`Doppelte ID „${id}“ wurde ersetzt`);
      do { id = safeText(context.createId(prefix), `${prefix}-${context.ids.size + 1}`, 128); } while (context.ids.has(id));
    }
    context.ids.add(id);
    return id;
  }

  function sanitizePointList(value, context) {
    if (!Array.isArray(value)) return [];
    const remaining = context.limits.points - context.pointCount;
    if (remaining <= 0) throw new ProjectValidationError('Das Projekt enthält zu viele Vektorpunkte', 'TOO_MANY_POINTS');
    const points = value.slice(0, remaining).filter((point) => Array.isArray(point) && point.length >= 2)
      .map((point) => [clamp(round(point[0]), -1000000, 1000000), clamp(round(point[1]), -1000000, 1000000)]);
    context.pointCount += points.length;
    if (value.length > points.length && context.pointCount >= context.limits.points) throw new ProjectValidationError('Das Projekt enthält zu viele Vektorpunkte', 'TOO_MANY_POINTS');
    return points;
  }

  function sanitizeBezierSegments(value, context) {
    if (!Array.isArray(value)) return [];
    return value.slice(0, context.limits.points).map((segment) => {
      if (!isPlainObject(segment) || !Array.isArray(segment.c1) || !Array.isArray(segment.c2)) return null;
      const controls = sanitizePointList([segment.c1, segment.c2], context);
      return controls.length === 2 ? { c1: controls[0], c2: controls[1] } : null;
    });
  }

  function sanitizeObjectWithContext(raw, context, depth = 0, parentType = '') {
    if (!isPlainObject(raw)) throw new ProjectValidationError('Ein Objekt im Projekt ist ungültig', 'INVALID_OBJECT');
    if (depth > context.limits.groupDepth) throw new ProjectValidationError('Gruppen sind zu tief verschachtelt', 'GROUP_TOO_DEEP');
    context.objectCount += 1;
    if (context.objectCount > context.limits.objects) throw new ProjectValidationError('Das Projekt enthält zu viele Objekte', 'TOO_MANY_OBJECTS');

    // Gruppeninhalte werden weiter unten rekursiv sanitisiert. Beim generischen
    // Kopieren wuerde derselbe Teilbaum sonst vorab komplett kopiert und gegen
    // das Wertebudget gezaehlt.
    const copy = copySerializable(raw, context, 0, true);
    copy.id = uniqueId(raw.id, 'object', context);
    copy.type = safeText(raw.type, 'rectangle', 64).toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'rectangle';
    copy.name = safeText(raw.name, copy.type, 256);
    const symbolId = safeText(raw.symbolId, '', 128);
    const symbolMasterId = safeText(raw.symbolMasterId, '', 128);
    if (copy.type === 'symbol-instance') {
      if (symbolId && (!context.validSymbolIds || context.validSymbolIds.has(symbolId))) copy.symbolId = symbolId; else delete copy.symbolId;
      delete copy.symbolMasterId;
    } else {
      delete copy.symbolId;
      if (symbolMasterId && (!context.validSymbolIds || context.validSymbolIds.has(symbolMasterId))) copy.symbolMasterId = symbolMasterId; else delete copy.symbolMasterId;
    }
    if (copy.symbolMasterId) {
      copy.symbolMasterScaleX = clamp(finite(raw.symbolMasterScaleX, 1), .0001, 10000);
      copy.symbolMasterScaleY = clamp(finite(raw.symbolMasterScaleY, 1), .0001, 10000);
    } else { delete copy.symbolMasterScaleX; delete copy.symbolMasterScaleY; }
    if (copy.type === 'text') {
      copy.text = typeof raw.text === 'string' ? raw.text.slice(0, context.limits.fieldLength) : '';
      const automaticName = safeText(raw.text, 'Text', 256);
      const legacyAutomaticName = raw.name == null || ['Text', 'text'].includes(copy.name) || copy.name === automaticName;
      copy.nameManuallyEdited = Object.prototype.hasOwnProperty.call(raw, 'nameManuallyEdited')
        ? Boolean(raw.nameManuallyEdited)
        : !legacyAutomaticName;
      if (!copy.nameManuallyEdited) copy.name = automaticName;
      const pathBinding = sanitizeTextPathBinding(raw.pathBinding);
      if (pathBinding) copy.pathBinding = pathBinding; else delete copy.pathBinding;
      if (Number.isFinite(Number(raw.svgInlineSize)) && Number(raw.svgInlineSize) > 0) copy.svgInlineSize = clamp(finite(raw.svgInlineSize), .0001, 1000000);
      else delete copy.svgInlineSize;
      if (['vertical-rl', 'vertical-lr', 'sideways-rl', 'sideways-lr'].includes(String(raw.svgWritingMode || '').toLowerCase())) copy.svgWritingMode = String(raw.svgWritingMode).toLowerCase();
      else delete copy.svgWritingMode;
      if (raw.svgDirection === 'rtl') copy.svgDirection = 'rtl'; else delete copy.svgDirection;
      if (['upright', 'sideways', 'sideways-right'].includes(String(raw.svgTextOrientation || '').toLowerCase())) copy.svgTextOrientation = String(raw.svgTextOrientation).toLowerCase();
      else delete copy.svgTextOrientation;
      if (['end', 'left', 'right', 'center', 'justify', 'match-parent'].includes(String(raw.svgTextAlign || '').toLowerCase())) copy.svgTextAlign = String(raw.svgTextAlign).toLowerCase();
      else delete copy.svgTextAlign;
      const fontSizeAdjust = safeText(raw.svgFontSizeAdjust, '', 64);
      if (fontSizeAdjust && fontSizeAdjust !== 'none') copy.svgFontSizeAdjust = fontSizeAdjust; else delete copy.svgFontSizeAdjust;
      if (Number.isFinite(Number(raw.svgImplicitFontSize)) && Number(raw.svgImplicitFontSize) > 0) copy.svgImplicitFontSize = clamp(finite(raw.svgImplicitFontSize), 1, 1000000);
      else delete copy.svgImplicitFontSize;
      if (Number.isFinite(Number(raw.svgTextLength)) && Number(raw.svgTextLength) >= 0) {
        copy.svgTextLength = clamp(finite(raw.svgTextLength), 0, 1000000);
        copy.svgLengthAdjust = raw.svgLengthAdjust === 'spacingAndGlyphs' ? 'spacingAndGlyphs' : 'spacing';
      } else { delete copy.svgTextLength; delete copy.svgLengthAdjust; }
      if (raw.svgXmlSpace === 'preserve') copy.svgXmlSpace = 'preserve'; else delete copy.svgXmlSpace;
      const textRuns = Array.isArray(raw.svgTextRuns) ? raw.svgTextRuns.slice(0, 512).filter(isPlainObject).map((run) => {
        const text = String(run.text || '').replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '').slice(0, context.limits.fieldLength);
        const fill = safeText(run.fill, '', 64); const fontFamily = safeText(run.fontFamily, '', 256);
        return {
          text,
          opacity: clamp(finite(run.opacity, 100), 0, 100),
          fill: /^(?:#[0-9a-f]{3,8}|rgba?\([^)]{1,80}\)|[a-z][a-z0-9-]{0,48})$/i.test(fill) ? fill : '',
          fontFamily,
          fontSize: Number(run.fontSize) > 0 ? clamp(finite(run.fontSize), 1, 1000000) : 0,
          lineHeight: Number.isFinite(Number(run.lineHeight)) ? clamp(finite(run.lineHeight), 0, 1000000) : 0,
          lineHeightExplicit: Boolean(run.lineHeightExplicit),
          bold: Boolean(run.bold), italic: Boolean(run.italic), direction: run.direction === 'rtl' ? 'rtl' : ''
        };
      }).filter((run) => run.text) : [];
      const textRunsSource = typeof raw.svgTextRunsSource === 'string' ? raw.svgTextRunsSource.slice(0, context.limits.fieldLength) : '';
      if (textRuns.length > 1 && textRunsSource === copy.text && textRuns.map((run) => run.text).join('') === copy.text) {
        copy.svgTextRuns = textRuns; copy.svgTextRunsSource = textRunsSource;
      } else { delete copy.svgTextRuns; delete copy.svgTextRunsSource; }
      if (['use-script', 'no-change', 'reset-size', 'ideographic', 'alphabetic', 'hanging', 'mathematical', 'central', 'middle', 'text-after-edge', 'text-before-edge', 'text-top', 'text-bottom'].includes(String(raw.svgDominantBaseline || '').toLowerCase())) {
        copy.svgDominantBaseline = String(raw.svgDominantBaseline).toLowerCase();
      } else delete copy.svgDominantBaseline;
    } else {
      delete copy.text;
      delete copy.nameManuallyEdited;
      delete copy.pathBinding;
      delete copy.svgInlineSize;
      delete copy.svgWritingMode;
      delete copy.svgDirection;
      delete copy.svgTextOrientation;
      delete copy.svgTextAlign;
      delete copy.svgFontSizeAdjust;
      delete copy.svgImplicitFontSize;
      delete copy.svgTextLength;
      delete copy.svgLengthAdjust;
      delete copy.svgXmlSpace;
      delete copy.svgTextRuns;
      delete copy.svgTextRunsSource;
      delete copy.svgDominantBaseline;
    }
    copy.x = clamp(finite(raw.x), -1000000, 1000000);
    copy.y = clamp(finite(raw.y), -1000000, 1000000);
    copy.width = clamp(Math.abs(finite(raw.width, 1)), .0001, 1000000);
    copy.height = clamp(Math.abs(finite(raw.height, 1)), .0001, 1000000);
    const svgTransformMatrix = Array.isArray(raw.svgTransformMatrix) ? raw.svgTransformMatrix.slice(0, 6).map(Number) : [];
    if (svgTransformMatrix.length === 6 && svgTransformMatrix.every(Number.isFinite)) {
      copy.svgTransformMatrix = svgTransformMatrix.map((value) => clamp(round(value, 8), -1000000, 1000000));
    } else delete copy.svgTransformMatrix;
    if (['auto', 'optimizespeed', 'crispedges', 'geometricprecision'].includes(String(raw.svgShapeRendering || '').toLowerCase())) {
      copy.svgShapeRendering = String(raw.svgShapeRendering).toLowerCase();
    } else delete copy.svgShapeRendering;
    if (Number.isFinite(Number(raw.svgPathLength)) && Number(raw.svgPathLength) >= 0) copy.svgPathLength = clamp(finite(raw.svgPathLength), 0, 1000000000);
    else delete copy.svgPathLength;
    if (['nonzero', 'evenodd'].includes(String(raw.fillRule || '').toLowerCase())) copy.fillRule = String(raw.fillRule).toLowerCase();
    else delete copy.fillRule;
    if (['pen', 'polyline'].includes(copy.type) && raw.svgOpenFill === true) copy.svgOpenFill = true;
    else delete copy.svgOpenFill;
    if (typeof raw.svgPaintOrder === 'string') {
      const requested = raw.svgPaintOrder.trim().toLowerCase().split(/\s+/).filter((value, index, values) => ['fill', 'stroke', 'markers'].includes(value) && values.indexOf(value) === index);
      if (requested.length) copy.svgPaintOrder = [...requested, ...['fill', 'stroke', 'markers'].filter((value) => !requested.includes(value))].join(' ');
      else delete copy.svgPaintOrder;
    } else delete copy.svgPaintOrder;
    if (raw.svgIsolation === 'isolate') copy.svgIsolation = 'isolate'; else delete copy.svgIsolation;
    copy.svgFillOpacity = clamp(finite(raw.svgFillOpacity, 1), 0, 1);
    copy.svgStrokeOpacity = clamp(finite(raw.svgStrokeOpacity, 1), 0, 1);
    if (copy.svgFillOpacity === 1) delete copy.svgFillOpacity;
    if (copy.svgStrokeOpacity === 1) delete copy.svgStrokeOpacity;
    if (copy.type === 'ellipse' && raw.svgPrimitiveType === 'circle') copy.svgPrimitiveType = 'circle';
    else delete copy.svgPrimitiveType;
    if (copy.type === 'rectangle') {
      copy.svgRx = clamp(finite(raw.svgRx), 0, copy.width / 2);
      copy.svgRy = clamp(finite(raw.svgRy), 0, copy.height / 2);
      if (!copy.svgRx && !copy.svgRy) { delete copy.svgRx; delete copy.svgRy; }
    } else { delete copy.svgRx; delete copy.svgRy; }
    if (copy.type === 'image') {
      copy.cornerRadius = clamp(round(raw.cornerRadius), 0, Math.min(copy.width, copy.height) / 2);
      if (!copy.cornerRadius) delete copy.cornerRadius;
    } else delete copy.cornerRadius;
    copy.rotation = ((finite(raw.rotation) % 360) + 360) % 360;
    copy.opacity = clamp(finite(raw.opacity, 100), 0, 100);
    copy.blendMode = Catalog?.normalizeBlendMode?.(raw.blendMode) || 'normal';
    copy.visible = raw.visible !== false;
    copy.locked = Boolean(raw.locked);
    copy.flipX = Boolean(raw.flipX);
    copy.flipY = Boolean(raw.flipY);
    if (FreeTransform && raw.freeTransform && typeof raw.freeTransform === 'object') {
      const transform = FreeTransform.normalize(raw.freeTransform);
      if (!FreeTransform.isIdentity(transform)) copy.freeTransform = transform; else delete copy.freeTransform;
    } else delete copy.freeTransform;
    if (raw.transformPivot && typeof raw.transformPivot === 'object') {
      copy.transformPivot = {
        x: clamp(round(raw.transformPivot.x == null ? raw.transformPivot[0] : raw.transformPivot.x), -20, 20),
        y: clamp(round(raw.transformPivot.y == null ? raw.transformPivot[1] : raw.transformPivot.y), -20, 20)
      };
    } else delete copy.transformPivot;
    copy.strokeWidth = clamp(finite(raw.strokeWidth, 0), 0, 10000);
    if (AutoShapes?.isAutoShape(copy.type)) copy.autoShape = AutoShapes.sanitize(copy.type, raw.autoShape, copy);
    else delete copy.autoShape;
    const supportsStrokeType = copy.type === 'text' || copy.type === 'text-outline' || Boolean(Catalog?.colorableVectorTypes?.has(copy.type));
    if (supportsStrokeType) copy.strokeType = Catalog?.normalizeStrokeType?.(raw.strokeType) || 'basic-soft-rounded';
    else delete copy.strokeType;
    if (supportsStrokeType && StrokeStyle) {
      const fallback = StrokeStyle.legacyStyle({ ...copy, ...raw, type: copy.type, strokeType: copy.strokeType });
      const stack = StrokeStyle.normalizeStack(raw.strokeStack, { fallback, createId: context.createId });
      if (stack.length) {
        copy.strokeStack = stack;
        StrokeStyle.syncLegacy(copy);
      } else delete copy.strokeStack;
    } else delete copy.strokeStack;
    if ('pixelWidth' in raw) copy.pixelWidth = clamp(Math.round(finite(raw.pixelWidth, copy.width)), 1, 100000);
    if ('pixelHeight' in raw) copy.pixelHeight = clamp(Math.round(finite(raw.pixelHeight, copy.height)), 1, 100000);
    if ('href' in raw) copy.href = safeImageHref(raw.href, context.limits.hrefLength);
    if (copy.type === 'image' && BitmapModel) {
      Object.assign(copy, BitmapModel.normalizedFields(copy));
      const sourceOrientation = Math.round(finite(raw.sourceOrientation));
      if (sourceOrientation >= 1 && sourceOrientation <= 8) copy.sourceOrientation = sourceOrientation;
      else delete copy.sourceOrientation;
      if (['icc', 'srgb', 'gamma'].includes(raw.sourceColorProfile)) copy.sourceColorProfile = raw.sourceColorProfile;
      else delete copy.sourceColorProfile;
      if (isPlainObject(raw.svgImageSettings)) {
        copy.svgImageSettings = { preserveAspectRatio: safeText(raw.svgImageSettings.preserveAspectRatio, 'xMidYMid meet', 128) };
        const viewport = raw.svgImageSettings.importedViewport; const matrix = Array.isArray(viewport?.matrix) ? viewport.matrix.slice(0, 6).map(Number) : [];
        if (isPlainObject(viewport) && matrix.length === 6 && matrix.every(Number.isFinite) && finite(viewport.width) > 0 && finite(viewport.height) > 0 && finite(viewport.baseWidth) > 0 && finite(viewport.baseHeight) > 0) {
          copy.svgImageSettings.importedViewport = {
            x: clamp(round(viewport.x), -1000000, 1000000), y: clamp(round(viewport.y), -1000000, 1000000),
            width: clamp(round(viewport.width), .0001, 2000000), height: clamp(round(viewport.height), .0001, 2000000),
            baseWidth: clamp(round(viewport.baseWidth), .0001, 2000000), baseHeight: clamp(round(viewport.baseHeight), .0001, 2000000),
            matrix: matrix.map((value) => clamp(round(value, 8), -1000000, 1000000))
          };
        }
      }
      else delete copy.svgImageSettings;
    }
    else {
      delete copy.bitmapMode; delete copy.bitmapInterpolation; delete copy.sourceDpi; delete copy.sourceDpiX; delete copy.sourceDpiY;
      delete copy.sourceOrientation; delete copy.sourceColorProfile;
      delete copy.svgImageSettings;
    }
    if (copy.type === 'resource-placeholder') {
      copy.resourceHref = safeText(raw.resourceHref, '', 2048); copy.resourceKind = raw.resourceKind === 'blocked' ? 'blocked' : 'missing'; delete copy.href;
    } else { delete copy.resourceHref; delete copy.resourceKind; }
    if (FillPaint) {
      const legacyTextureFill = legacyTextTextureFillPaint(raw.textTexture, context);
      const fillSource = legacyTextureFill || raw.fillPaint;
      const fillPaint = FillPaint.normalizePaint(fillSource, {
        fallbackColor: /^#[0-9a-f]{6}$/i.test(String(raw.fill || '')) ? raw.fill : '#4f6df5',
        href: safeImageHref(fillSource?.href, context.limits.hrefLength)
      });
      if (fillPaint) copy.fillPaint = fillPaint; else delete copy.fillPaint;
      const rawStrokeColor = copy.type === 'text' ? raw.textStroke : raw.stroke;
      const strokePaint = supportsStrokeType ? FillPaint.normalizePaint(raw.strokePaint, {
        fallbackColor: /^#[0-9a-f]{6}$/i.test(String(rawStrokeColor || '')) ? rawStrokeColor : '#172033',
        href: safeImageHref(raw.strokePaint?.href, context.limits.hrefLength)
      }) : null;
      if (strokePaint) copy.strokePaint = strokePaint; else delete copy.strokePaint;
    } else { delete copy.fillPaint; delete copy.strokePaint; }
    if ('points' in raw) copy.points = sanitizePointList(raw.points, context);
    if ('rawPoints' in raw) copy.rawPoints = sanitizePointList(raw.rawPoints, context);
    if (['pencil', 'brush', 'line', 'polyline', 'pen'].includes(copy.type) && Array.isArray(raw.strokeProfile) && copy.points?.length === raw.strokeProfile.length) {
      copy.strokeProfile = raw.strokeProfile.slice(0, context.limits.points).map((value) => clamp(finite(value, 1), .05, 4));
    } else delete copy.strokeProfile;
    if (Array.isArray(raw.bezierSegments)) copy.bezierSegments = sanitizeBezierSegments(raw.bezierSegments, context);
    if (Array.isArray(raw.pointModes)) copy.pointModes = raw.pointModes.slice(0, context.limits.points).map((mode) => ['corner', 'smooth', 'symmetric'].includes(mode) ? mode : 'corner');
    if ('closed' in raw) copy.closed = Boolean(raw.closed);
    if (supportsObjectTexture(copy)) {
      const textTexture = sanitizeTextTexture(raw.textTexture, context);
      if (textTexture) {
        if (copy.type !== 'text') textTexture.pixelFullCells = false;
        copy.textTexture = textTexture;
      } else delete copy.textTexture;
    } else delete copy.textTexture;
    if (isPlainObject(raw.svgMarkerSettings)) {
      const markerSettings = {}; ['start', 'end'].forEach((edge) => {
        const value = raw.svgMarkerSettings[edge]; if (!isPlainObject(value)) return;
        markerSettings[edge] = {
          refX: clamp(round(value.refX), -1000, 1000), refY: clamp(round(value.refY), -1000, 1000),
          orient: safeText(value.orient, 'auto-start-reverse', 64), markerUnits: value.markerUnits === 'userSpaceOnUse' ? 'userSpaceOnUse' : 'strokeWidth'
        };
      });
      if (Object.keys(markerSettings).length) copy.svgMarkerSettings = markerSettings; else delete copy.svgMarkerSettings;
    } else delete copy.svgMarkerSettings;
    if ('maskInverted' in raw) copy.maskInverted = Boolean(raw.maskInverted);
    if (copy.type === 'mask-group') {
      copy.maskType = raw.maskType === 'alpha' ? 'alpha' : 'luminance';
      copy.maskUsesAppearance = Boolean(raw.maskUsesAppearance);
      if (isPlainObject(raw.maskRegion) && finite(raw.maskRegion.width) > 0 && finite(raw.maskRegion.height) > 0) copy.maskRegion = {
        x: clamp(round(raw.maskRegion.x), -1000000, 1000000), y: clamp(round(raw.maskRegion.y), -1000000, 1000000),
        width: clamp(round(raw.maskRegion.width), .0001, 2000000), height: clamp(round(raw.maskRegion.height), .0001, 2000000)
      };
      else delete copy.maskRegion;
    } else {
      delete copy.maskType; delete copy.maskUsesAppearance; delete copy.maskRegion;
    }
    if (raw.maskRole === 'mask') copy.maskRole = 'mask'; else delete copy.maskRole;
    if (parentType === 'clip-group' && raw.clipRole === 'clip') copy.clipRole = 'clip'; else delete copy.clipRole;
    delete copy.maskMode;
    if (Effects) {
      const effects = Effects.normalizeStack(raw.effects, { createId: context.createId })
        .filter((effect) => Effects.isAllowed(effect.type, copy.type));
      const legacyFillDither = Effects.fromLegacyFillDither(raw.fillPaint, { createId: context.createId });
      if (legacyFillDither && !effects.some((effect) => effect.type === 'dither')) effects.unshift(legacyFillDither);
      const legacyGlow = Effects.fromLegacyGlow(raw.glow, { createId: context.createId });
      if (legacyGlow && !effects.some((effect) => effect.type === 'outer-glow')) effects.push(legacyGlow);
      copy.effects = Effects.normalizeStack(effects, { createId: context.createId });
      if (!copy.effects.length) delete copy.effects;
    } else delete copy.effects;
    if (copy.effects?.length && isPlainObject(raw.svgFilterSettings) && isPlainObject(raw.svgFilterSettings.region)) {
      const region = raw.svgFilterSettings.region;
      copy.svgFilterSettings = {
        filterUnits: raw.svgFilterSettings.filterUnits === 'userSpaceOnUse' ? 'userSpaceOnUse' : 'objectBoundingBox',
        primitiveUnits: raw.svgFilterSettings.primitiveUnits === 'objectBoundingBox' ? 'objectBoundingBox' : 'userSpaceOnUse',
        colorInterpolationFilters: raw.svgFilterSettings.colorInterpolationFilters === 'sRGB' ? 'sRGB' : 'linearRGB',
        region: {
          x: clamp(round(region.x), -1000000, 1000000), y: clamp(round(region.y), -1000000, 1000000),
          width: clamp(round(region.width), .0001, 2000000), height: clamp(round(region.height), .0001, 2000000)
        }
      };
    } else delete copy.svgFilterSettings;
    delete copy.glow;
    if (Array.isArray(raw.outlineContours)) copy.outlineContours = raw.outlineContours.map((contour) => sanitizePointList(contour, context)).filter((contour) => contour.length >= 3);
    if (copy.outlineContours?.length && Array.isArray(raw.outlineBezierContours)) {
      copy.outlineBezierContours = copy.outlineContours.map((contour, contourIndex) => {
        const segments = sanitizeBezierSegments(raw.outlineBezierContours[contourIndex], context);
        return Array.from({ length: contour.length }, (_, index) => segments[index] || null);
      });
      if (!copy.outlineBezierContours.some((segments) => segments.some(Boolean))) delete copy.outlineBezierContours;
    } else delete copy.outlineBezierContours;
    copy.children = Array.isArray(raw.children)
      ? raw.children.map((child) => sanitizeObjectWithContext(child, context, depth + 1, copy.type))
      : undefined;
    if (!copy.children?.length) delete copy.children;
    return copy;
  }

  function sanitizeAppearanceStyle(raw, context) {
    if (!isPlainObject(raw)) return {};
    const style = copySerializable(raw, context); const output = {};
    const color = (value, fallback = 'none') => value === 'none' || value === 'transparent' || /^#[0-9a-f]{6}$/i.test(String(value || '')) ? String(value).toLowerCase() : fallback;
    if ('fill' in style) output.fill = color(style.fill);
    if ('stroke' in style) output.stroke = color(style.stroke);
    if ('textStroke' in style) output.textStroke = color(style.textStroke);
    if ('strokeWidth' in style) output.strokeWidth = clamp(finite(style.strokeWidth), 0, 10000);
    if ('textStrokeWidth' in style) output.textStrokeWidth = clamp(finite(style.textStrokeWidth), 0, 10000);
    if ('strokeType' in style) output.strokeType = Catalog?.normalizeStrokeType?.(style.strokeType) || 'basic-soft-rounded';
    if (FillPaint && style.fillPaint) {
      const paint = FillPaint.normalizePaint(style.fillPaint, { fallbackColor: output.fill || '#4f6df5', href: safeImageHref(style.fillPaint.href, context.limits.hrefLength) });
      if (paint) output.fillPaint = paint;
    }
    if (FillPaint && style.strokePaint) {
      const paint = FillPaint.normalizePaint(style.strokePaint, { fallbackColor: output.textStroke || output.stroke || '#172033', href: safeImageHref(style.strokePaint.href, context.limits.hrefLength) });
      if (paint) output.strokePaint = paint;
    }
    if (StrokeStyle && Array.isArray(style.strokeStack)) output.strokeStack = StrokeStyle.normalizeStack(style.strokeStack, { fallback: StrokeStyle.legacyStyle(style), createId: context.createId });
    if (Effects) {
      const effects = Effects.normalizeStack(style.effects, { createId: context.createId });
      const legacyFillDither = Effects.fromLegacyFillDither(style.fillPaint, { createId: context.createId });
      if (legacyFillDither && !effects.some((effect) => effect.type === 'dither')) effects.unshift(legacyFillDither);
      output.effects = Effects.normalizeStack(effects, { createId: context.createId });
      if (!output.effects.length) delete output.effects;
    }
    return output;
  }

  function sanitizeResources(raw, context) {
    const source = isPlainObject(raw) ? raw : {}; const resources = ReusableAssets?.emptyLibrary?.() || { symbols: [], objectStyles: [], fillPresets: [], strokePresets: [] };
    const used = new Set();
    const resourceId = (value, prefix) => {
      let id = safeText(value, '', 128).replace(/\s+/g, '-');
      if (!id || used.has(id)) do { id = safeText(context.createId(prefix), `${prefix}-${used.size + 1}`, 128); } while (used.has(id));
      used.add(id); return id;
    };
    const symbolStubs = (Array.isArray(source.symbols) ? source.symbols : []).slice(0, 10000).filter(isPlainObject).map((entry) => ({
      raw: entry, id: resourceId(entry.id, 'symbol'), name: safeText(entry.name, 'Symbol', 256)
    }));
    context.validSymbolIds = new Set(symbolStubs.map((entry) => entry.id));
    symbolStubs.forEach((entry) => {
      if (!isPlainObject(entry.raw.object)) return;
      const object = sanitizeObjectWithContext(entry.raw.object, context); delete object.symbolMasterId;
      if (object.type === 'symbol-instance') return;
      object.x = 0; object.y = 0; object.rotation = 0; object.flipX = false; object.flipY = false; object.visible = true; object.locked = false;
      resources.symbols.push({ id: entry.id, name: entry.name, object });
    });
    (Array.isArray(source.objectStyles) ? source.objectStyles : []).slice(0, 10000).filter(isPlainObject).forEach((entry) => {
      resources.objectStyles.push({ id: resourceId(entry.id, 'object-style'), name: safeText(entry.name, 'Objektstil', 256), style: sanitizeAppearanceStyle(entry.style, context) });
    });
    (Array.isArray(source.fillPresets) ? source.fillPresets : []).slice(0, 10000).filter(isPlainObject).forEach((entry) => {
      if (!FillPaint) return;
      const paint = FillPaint.normalizePaint(entry.paint, { fallbackColor: '#4f6df5', href: safeImageHref(entry.paint?.href, context.limits.hrefLength) });
      if (!paint || (!FillPaint.isGradient(paint.type) && !FillPaint.isImage(paint.type))) return;
      resources.fillPresets.push({ id: resourceId(entry.id, 'fill-preset'), name: safeText(entry.name, FillPaint.typeLabel(paint.type), 256), paint });
    });
    (Array.isArray(source.strokePresets) ? source.strokePresets : []).slice(0, 10000).filter(isPlainObject).forEach((entry) => {
      if (!StrokeStyle) return;
      const stack = StrokeStyle.normalizeStack(entry.stack, { fallback: StrokeStyle.legacyStyle({}), createId: context.createId });
      if (stack.length) resources.strokePresets.push({ id: resourceId(entry.id, 'stroke-preset'), name: safeText(entry.name, 'Konturvorgabe', 256), stack });
    });
    return resources;
  }

  function sanitizeObject(raw, options = {}) {
    const context = createSanitizeContext(options);
    const object = sanitizeObjectWithContext(raw, context);
    return { object, warnings: context.warnings };
  }

  function sanitizeDocument(raw, options = {}) {
    const source = raw?.document || raw?.doc || raw;
    if (!isPlainObject(source)) throw new ProjectValidationError('Die Projektdatei enthält kein Dokument', 'MISSING_DOCUMENT');
    const context = createSanitizeContext(options);
    const resources = sanitizeResources(source.resources, context);
    const width = clamp(Math.round(finite(source.width, 960)), 1, context.limits.canvasSize);
    const height = clamp(Math.round(finite(source.height, 640)), 1, context.limits.canvasSize);
    const sourceLayers = Array.isArray(source.layers) ? source.layers : [];
    if (sourceLayers.length > context.limits.layers) throw new ProjectValidationError('Das Projekt enthält zu viele Ebenen', 'TOO_MANY_LAYERS');
    const guideList = (values, maximum) => [...new Set((Array.isArray(values) ? values.slice(0, 10000) : [])
      .map((value) => round(finite(value, -1)))
      .filter((value) => value >= 0 && value <= maximum))].slice(0, 10000);
    const layers = sourceLayers.map((rawLayer, index) => {
      if (!isPlainObject(rawLayer)) throw new ProjectValidationError('Eine Ebene im Projekt ist ungültig', 'INVALID_LAYER');
      return {
        id: uniqueId(rawLayer.id, 'layer', context),
        name: safeText(rawLayer.name, `Ebene ${index + 1}`, 256),
        visible: rawLayer.visible !== false,
        locked: Boolean(rawLayer.locked),
        blendMode: Catalog?.normalizeBlendMode?.(rawLayer.blendMode) || 'normal',
        objects: Array.isArray(rawLayer.objects) ? rawLayer.objects.map((object) => sanitizeObjectWithContext(object, context)) : []
      };
    });
    if (!layers.length) layers.push({ id: uniqueId('', 'layer', context), name: 'Ebene 1', visible: true, locked: false, blendMode: 'normal', objects: [] });
    const requestedActiveLayer = safeText(source.activeLayerId, '', 128);
    const activeLayerId = layers.some((layer) => layer.id === requestedActiveLayer) ? requestedActiveLayer : layers[layers.length - 1].id;
    const bg = source.bg === 'transparent' || /^#[0-9a-f]{6}$/i.test(String(source.bg || '')) ? source.bg : '#ffffff';
    return {
      document: {
        width,
        height,
        dpi: clamp(Math.round(finite(source.dpi, 72)), 1, context.limits.dpi),
        bg,
        layers,
        activeLayerId,
        guides: {
          horizontal: guideList(source.guides?.horizontal, height),
          vertical: guideList(source.guides?.vertical, width),
          locked: Boolean(source.guides?.locked)
        },
        grid: {
          visible: Boolean(source.grid?.visible),
          snap: Boolean(source.grid?.snap),
          size: clamp(Math.round(finite(source.grid?.size, 10)), 1, 10000)
        },
        resources
      },
      warnings: context.warnings,
      stats: { layers: layers.length, objects: context.objectCount, points: context.pointCount }
    };
  }

  function normalizeTitle(value, fallback = 'Unbenannt') {
    return safeText(value, fallback, 80).replace(/\s+/g, ' ');
  }

  function parseProject(raw, options = {}) {
    if (!isPlainObject(raw)) throw new ProjectValidationError('Die Projektdatei ist kein gültiges JSON-Objekt');
    const declaredFormat = raw.format == null ? '' : String(raw.format).toLowerCase();
    if (declaredFormat && declaredFormat !== PROJECT_FORMAT) throw new ProjectValidationError(`Unbekanntes Projektformat „${declaredFormat}“`, 'UNSUPPORTED_FORMAT');
    const sourceVersion = raw.version == null ? 1 : Math.round(finite(raw.version, NaN));
    if (!Number.isFinite(sourceVersion) || sourceVersion < 1) throw new ProjectValidationError('Die Projektversion ist ungültig', 'INVALID_VERSION');
    if (sourceVersion > PROJECT_VERSION) throw new ProjectValidationError(`Projektversion ${sourceVersion} wird noch nicht unterstützt`, 'NEWER_VERSION');
    const cleaned = sanitizeDocument(raw, options);
    return {
      format: PROJECT_FORMAT,
      version: PROJECT_VERSION,
      sourceVersion,
      migrated: sourceVersion < PROJECT_VERSION || declaredFormat !== PROJECT_FORMAT,
      title: normalizeTitle(raw.title, options.fallbackTitle || 'Unbenannt'),
      savedAt: typeof raw.savedAt === 'string' ? raw.savedAt : null,
      document: cleaned.document,
      warnings: cleaned.warnings,
      stats: cleaned.stats
    };
  }

  function createProjectPayload({ title = 'Unbenannt', document, savedAt = new Date().toISOString() } = {}, options = {}) {
    const cleaned = sanitizeDocument(document, options);
    return {
      format: PROJECT_FORMAT,
      version: PROJECT_VERSION,
      generator: { name: 'PYRO', schema: PROJECT_VERSION },
      savedAt,
      title: normalizeTitle(title),
      document: cleaned.document
    };
  }

  // 2D-Matrizen verwenden die SVG/DOMMatrix-Anordnung [a, b, c, d, e, f].
  const identityMatrix = () => [1, 0, 0, 1, 0, 0];
  const translationMatrix = (x, y) => [1, 0, 0, 1, finite(x), finite(y)];
  const rotationMatrix = (degrees) => {
    const radians = finite(degrees) * Math.PI / 180;
    return [Math.cos(radians), Math.sin(radians), -Math.sin(radians), Math.cos(radians), 0, 0];
  };
  const scaleMatrix = (x, y = x) => [finite(x, 1), 0, 0, finite(y, 1), 0, 0];
  function multiplyMatrices(left, right) {
    const [a1, b1, c1, d1, e1, f1] = left; const [a2, b2, c2, d2, e2, f2] = right;
    return [
      a1 * a2 + c1 * b2,
      b1 * a2 + d1 * b2,
      a1 * c2 + c1 * d2,
      b1 * c2 + d1 * d2,
      a1 * e2 + c1 * f2 + e1,
      b1 * e2 + d1 * f2 + f1
    ];
  }
  function invertMatrix(matrix) {
    const [a, b, c, d, e, f] = matrix; const determinant = a * d - b * c;
    if (Math.abs(determinant) < 1e-12) return null;
    return [d / determinant, -b / determinant, -c / determinant, a / determinant, (c * f - d * e) / determinant, (b * e - a * f) / determinant];
  }
  function applyMatrix(matrix, point) {
    const [a, b, c, d, e, f] = matrix; const x = finite(point?.x ?? point?.[0]); const y = finite(point?.y ?? point?.[1]);
    return { x: a * x + c * y + e, y: b * x + d * y + f };
  }
  function objectMatrix(object) {
    const x = finite(object?.x); const y = finite(object?.y); const width = Math.max(.0001, finite(object?.width, 1)); const height = Math.max(.0001, finite(object?.height, 1));
    const cx = width / 2; const cy = height / 2;
    const base = multiplyMatrices(
      translationMatrix(x + cx, y + cy),
      multiplyMatrices(rotationMatrix(object?.rotation), multiplyMatrices(scaleMatrix(object?.flipX ? -1 : 1, object?.flipY ? -1 : 1), translationMatrix(-cx, -cy)))
    );
    const imported = Array.isArray(object?.svgTransformMatrix) && object.svgTransformMatrix.length >= 6
      && object.svgTransformMatrix.slice(0, 6).every(Number.isFinite) ? object.svgTransformMatrix.slice(0, 6) : null;
    return imported ? multiplyMatrices(imported, base) : base;
  }
  function pointToObjectLocal(object, point) {
    const inverse = invertMatrix(objectMatrix(object));
    return inverse ? applyMatrix(inverse, point) : { x: 0, y: 0 };
  }
  function transformedBounds(object, outset = 0) {
    const width = Math.max(.0001, finite(object?.width, 1)); const height = Math.max(.0001, finite(object?.height, 1)); const matrix = objectMatrix(object);
    const corners = [[0, 0], [width, 0], [width, height], [0, height]].map(([x, y]) => applyMatrix(matrix, { x, y }));
    const padding = Math.max(0, finite(outset));
    const left = Math.min(...corners.map((point) => point.x)) - padding;
    const top = Math.min(...corners.map((point) => point.y)) - padding;
    const right = Math.max(...corners.map((point) => point.x)) + padding;
    const bottom = Math.max(...corners.map((point) => point.y)) + padding;
    return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
  }
  function unionBounds(bounds) {
    if (!bounds.length) return { x: 0, y: 0, width: 0, height: 0, right: 0, bottom: 0, cx: 0, cy: 0 };
    const left = Math.min(...bounds.map((item) => item.x)); const top = Math.min(...bounds.map((item) => item.y));
    const right = Math.max(...bounds.map((item) => item.right)); const bottom = Math.max(...bounds.map((item) => item.bottom));
    return { x: left, y: top, width: right - left, height: bottom - top, right, bottom, cx: (left + right) / 2, cy: (top + bottom) / 2 };
  }
  function boundsOf(objects, outsetFor = () => 0) {
    return unionBounds((objects || []).map((object) => transformedBounds(object, outsetFor(object))));
  }
  function boxContainsPoint(object, point, padding = 0) {
    const local = pointToObjectLocal(object, point); const width = Math.max(.0001, finite(object?.width, 1)); const height = Math.max(.0001, finite(object?.height, 1));
    return local.x >= -padding && local.x <= width + padding && local.y >= -padding && local.y <= height + padding;
  }
  function pointInPolygon(point, points) {
    let inside = false;
    for (let index = 0, previous = points.length - 1; index < points.length; previous = index, index += 1) {
      const currentPoint = points[index]; const previousPoint = points[previous];
      const intersects = (finite(currentPoint[1]) > point.y) !== (finite(previousPoint[1]) > point.y)
        && point.x < (finite(previousPoint[0]) - finite(currentPoint[0])) * (point.y - finite(currentPoint[1])) / ((finite(previousPoint[1]) - finite(currentPoint[1])) || Number.EPSILON) + finite(currentPoint[0]);
      if (intersects) inside = !inside;
    }
    return inside;
  }
  function composeBoxTransforms(parent, child) {
    const width = Math.max(.0001, finite(child?.width, 1)); const height = Math.max(.0001, finite(child?.height, 1));
    const matrix = multiplyMatrices(objectMatrix(parent), objectMatrix(child));
    if (parent?.svgTransformMatrix || child?.svgTransformMatrix) return {
      x: 0, y: 0, rotation: 0, flipX: false, flipY: false, svgTransformMatrix: matrix
    };
    const [a, b, c, d] = matrix; const determinant = a * d - b * c;
    const center = applyMatrix(matrix, { x: width / 2, y: height / 2 });
    const rotation = ((Math.atan2(b, a) * 180 / Math.PI) % 360 + 360) % 360;
    return {
      x: center.x - width / 2,
      y: center.y - height / 2,
      rotation,
      flipX: false,
      flipY: determinant < 0
    };
  }

  const api = Object.freeze({
    PROJECT_FORMAT,
    PROJECT_VERSION,
    DEFAULT_LIMITS,
    ProjectValidationError,
    sanitizeObject,
    sanitizeDocument,
    parseProject,
    createProjectPayload,
    identityMatrix,
    translationMatrix,
    rotationMatrix,
    scaleMatrix,
    multiplyMatrices,
    invertMatrix,
    applyMatrix,
    objectMatrix,
    pointToObjectLocal,
    transformedBounds,
    unionBounds,
    boundsOf,
    boxContainsPoint,
    pointInPolygon,
    composeBoxTransforms
  });

  root.PyroCore = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
