(function (root, factory) {
  "use strict";
  const api = factory();
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.JianglaiImpositionEngine = Object.freeze(api);
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
  "use strict";

  const MICRONS_PER_MM = 1000;
  const MAX_DIMENSION_MM = 50000;
  const MAX_SKUS = 80;
  const MAX_QUANTITY = 100000000;
  const MAX_PATTERN_PLACEMENTS = 20000;
  const MAX_PATTERN_GROUPS = 2000;
  const MAX_FREE_RECTS = 30000;
  const ENGINE_VERSION = "1.0.0";

  const HEURISTICS = Object.freeze({
    best_area_fit: Object.freeze({
      id: "best_area_fit",
      label: "最小剩余面积",
      description: "优先放入最贴合当前空白区域的产品，通常适合尺寸差异较大的混拼。"
    }),
    best_short_side_fit: Object.freeze({
      id: "best_short_side_fit",
      label: "最短边贴合",
      description: "优先缩小短边余量，通常能减少狭长且无法再利用的边角料。"
    }),
    largest_first: Object.freeze({
      id: "largest_first",
      label: "大件优先",
      description: "先安排占纸面积大的产品，再用小件填充剩余区域。"
    }),
    demand_balanced: Object.freeze({
      id: "demand_balanced",
      label: "交期需求均衡",
      description: "优先安排剩余需求比例较高的产品，降低某个订单在最后集中欠产的风险。"
    })
  });

  const DEFAULT_HEURISTICS = Object.freeze([
    "best_area_fit",
    "best_short_side_fit",
    "largest_first",
    "demand_balanced"
  ]);

  function numeric(value, label, options) {
    const settings = options || {};
    if (value === null || value === "" || typeof value === "boolean") {
      throw new TypeError(`${label}必须填写数字`);
    }
    const parsed = Number(value);
    const minimum = settings.allowZero ? 0 : (settings.minimum || 0.001);
    if (!Number.isFinite(parsed)) throw new TypeError(`${label}必须是有限数字`);
    if (parsed < minimum) {
      throw new RangeError(`${label}${settings.allowZero ? "不能小于0" : "必须大于0"}`);
    }
    if (!settings.noMaximum && parsed > MAX_DIMENSION_MM) {
      throw new RangeError(`${label}不能超过${MAX_DIMENSION_MM}mm`);
    }
    return parsed;
  }

  function positiveInteger(value, label, options) {
    const settings = options || {};
    if (value === null || value === "" || typeof value === "boolean") {
      throw new TypeError(`${label}必须填写正整数`);
    }
    const parsed = Number(value);
    const minimum = settings.allowZero ? 0 : 1;
    if (!Number.isInteger(parsed) || parsed < minimum) {
      throw new RangeError(`${label}必须是${settings.allowZero ? "非负" : "正"}整数`);
    }
    const maximum = settings.maximum || MAX_QUANTITY;
    if (parsed > maximum) throw new RangeError(`${label}不能超过${maximum}`);
    return parsed;
  }

  function toMicrons(value, label, options) {
    return Math.round(numeric(value, label, options) * MICRONS_PER_MM);
  }

  function toMm(value) {
    return Number((value / MICRONS_PER_MM).toFixed(3));
  }

  function round(value, digits) {
    const factor = 10 ** (digits || 6);
    return Math.round((Number(value) + Number.EPSILON) * factor) / factor;
  }

  function sideValues(value, label) {
    if (value === undefined || value === null || value === "") {
      return { top: 0, right: 0, bottom: 0, left: 0 };
    }
    if (typeof value === "number" || typeof value === "string") {
      const size = toMicrons(value, label, { allowZero: true });
      return { top: size, right: size, bottom: size, left: size };
    }
    if (typeof value !== "object" || Array.isArray(value)) {
      throw new TypeError(`${label}必须是数字或包含上、右、下、左的对象`);
    }
    return {
      top: toMicrons(value.top ?? 0, `${label}上边`, { allowZero: true }),
      right: toMicrons(value.right ?? 0, `${label}右边`, { allowZero: true }),
      bottom: toMicrons(value.bottom ?? 0, `${label}下边`, { allowZero: true }),
      left: toMicrons(value.left ?? 0, `${label}左边`, { allowZero: true })
    };
  }

  function gapValues(value) {
    if (value === undefined || value === null || value === "") return { x: 0, y: 0 };
    if (typeof value === "number" || typeof value === "string") {
      const gap = toMicrons(value, "拼版间隙", { allowZero: true });
      return { x: gap, y: gap };
    }
    if (typeof value !== "object" || Array.isArray(value)) {
      throw new TypeError("拼版间隙必须是数字或包含x、y的对象");
    }
    return {
      x: toMicrons(value.x ?? value.horizontal ?? 0, "横向拼版间隙", { allowZero: true }),
      y: toMicrons(value.y ?? value.vertical ?? 0, "纵向拼版间隙", { allowZero: true })
    };
  }

  function normalizedDirection(value, label, accepted, fallback) {
    const raw = String(value ?? fallback).trim().toLowerCase();
    const aliases = {
      none: "free",
      any: "free",
      unrestricted: "free",
      horizontal: "width",
      x: "width",
      vertical: "height",
      y: "height",
      locked: "fixed"
    };
    const normalized = aliases[raw] || raw;
    if (!accepted.includes(normalized)) {
      throw new RangeError(`${label}只支持${accepted.join("、")}`);
    }
    return normalized;
  }

  function normalizeInput(input) {
    if (!input || typeof input !== "object" || Array.isArray(input)) {
      throw new TypeError("拼版参数必须是对象");
    }
    const sheet = input.sheet && typeof input.sheet === "object" ? input.sheet : input;
    const sheetWidth = toMicrons(
      sheet.widthMm ?? sheet.sheetWidthMm ?? input.sheetWidthMm,
      "上机纸宽度"
    );
    const sheetHeight = toMicrons(
      sheet.heightMm ?? sheet.sheetHeightMm ?? input.sheetHeightMm,
      "上机纸高度"
    );
    const margins = sideValues(input.marginsMm ?? sheet.marginsMm, "纸张安全边距");
    const unprintable = sideValues(
      input.unprintableMm ?? sheet.unprintableMm,
      "不可印区域"
    );
    const gripper = toMicrons(
      input.gripperMm ?? sheet.gripperMm ?? 0,
      "咬口",
      { allowZero: true }
    );
    const gripperEdge = normalizedDirection(
      input.gripperEdge ?? sheet.gripperEdge,
      "咬口方向",
      ["top", "right", "bottom", "left"],
      "top"
    );
    const effectiveMargins = {
      top: margins.top + unprintable.top,
      right: margins.right + unprintable.right,
      bottom: margins.bottom + unprintable.bottom,
      left: margins.left + unprintable.left
    };
    effectiveMargins[gripperEdge] += gripper;
    const printableWidth = sheetWidth - effectiveMargins.left - effectiveMargins.right;
    const printableHeight = sheetHeight - effectiveMargins.top - effectiveMargins.bottom;
    if (printableWidth <= 0 || printableHeight <= 0) {
      throw new RangeError("安全边距、不可印区域和咬口已经占满上机纸");
    }

    const gap = gapValues(input.gapMm ?? sheet.gapMm);
    const rawSkus = input.skus;
    if (!Array.isArray(rawSkus) || rawSkus.length === 0) {
      throw new TypeError("至少需要一个SKU才能拼版");
    }
    if (rawSkus.length > MAX_SKUS) throw new RangeError(`单次拼版最多支持${MAX_SKUS}个SKU`);
    const knownIds = new Set();
    const skus = rawSkus.map((rawSku, index) => {
      if (!rawSku || typeof rawSku !== "object" || Array.isArray(rawSku)) {
        throw new TypeError(`第${index + 1}个SKU必须是对象`);
      }
      const id = String(rawSku.id ?? rawSku.skuId ?? rawSku.code ?? `sku-${index + 1}`).trim();
      if (!id) throw new TypeError(`第${index + 1}个SKU编号不能为空`);
      if (knownIds.has(id)) throw new RangeError(`SKU编号不能重复：${id}`);
      knownIds.add(id);
      const width = toMicrons(rawSku.widthMm ?? rawSku.width, `${id}宽度`);
      const height = toMicrons(rawSku.heightMm ?? rawSku.height, `${id}高度`);
      const quantity = positiveInteger(rawSku.quantity, `${id}数量`);
      const allowRotate = rawSku.allowRotate !== false;
      const grainDirection = normalizedDirection(
        rawSku.grainDirection,
        `${id}纹向`,
        ["free", "fixed", "width", "height"],
        "free"
      );
      return {
        id,
        name: String(rawSku.name || id),
        width,
        height,
        quantity,
        allowRotate,
        grainDirection,
        priority: numeric(rawSku.priority ?? 1, `${id}优先级`, {
          allowZero: true,
          noMaximum: true
        }),
        sourceIndex: index
      };
    });

    const sheetGrainDirection = normalizedDirection(
      input.sheetGrainDirection ?? sheet.grainDirection,
      "上机纸纹向",
      ["free", "width", "height"],
      "free"
    );
    const heuristics = input.heuristics === undefined
      ? DEFAULT_HEURISTICS.slice()
      : Array.from(new Set(
        (Array.isArray(input.heuristics) ? input.heuristics : [input.heuristics])
          .map(item => String(item || "").trim())
          .filter(Boolean)
      ));
    if (heuristics.length === 0) throw new TypeError("至少需要一种拼版策略");
    heuristics.forEach(item => {
      if (!HEURISTICS[item]) throw new RangeError(`不支持的拼版策略：${item}`);
    });

    const maxSheets = input.maxSheets === undefined || input.maxSheets === null || input.maxSheets === ""
      ? null
      : positiveInteger(input.maxSheets, "最大生产张数");
    return {
      sheetWidth,
      sheetHeight,
      margins,
      unprintable,
      effectiveMargins,
      gripper,
      gripperEdge,
      printable: {
        x: effectiveMargins.left,
        y: effectiveMargins.top,
        width: printableWidth,
        height: printableHeight
      },
      gap,
      sheetGrainDirection,
      skus,
      heuristics,
      maxSheets
    };
  }

  function allowedOrientations(sku, sheetGrainDirection) {
    if (!sku.allowRotate || sku.grainDirection === "fixed") return [0];
    if (sku.grainDirection === "free") {
      return sku.width === sku.height ? [0] : [0, 90];
    }
    if (sheetGrainDirection === "free") return [0];
    const sameAxis = sku.grainDirection === sheetGrainDirection;
    return sameAxis ? [0] : [90];
  }

  function orientedSize(sku, rotation) {
    return rotation === 90
      ? { width: sku.height, height: sku.width }
      : { width: sku.width, height: sku.height };
  }

  function orientationFits(sku, context, rotation) {
    const size = orientedSize(sku, rotation);
    return size.width <= context.printable.width && size.height <= context.printable.height;
  }

  function ensureEverySkuFits(context) {
    context.skus.forEach(sku => {
      const orientations = allowedOrientations(sku, context.sheetGrainDirection);
      if (!orientations.some(rotation => orientationFits(sku, context, rotation))) {
        const grainHint = sku.grainDirection !== "free"
          ? "；当前纹向限制不允许改用另一个方向"
          : "";
        throw new RangeError(
          `${sku.id}（${toMm(sku.width)}×${toMm(sku.height)}mm）无法放入可印区域` +
          `（${toMm(context.printable.width)}×${toMm(context.printable.height)}mm）${grainHint}`
        );
      }
    });
  }

  function intersects(a, b) {
    return a.x < b.x + b.width &&
      a.x + a.width > b.x &&
      a.y < b.y + b.height &&
      a.y + a.height > b.y;
  }

  function containedBy(a, b) {
    return a.x >= b.x && a.y >= b.y &&
      a.x + a.width <= b.x + b.width &&
      a.y + a.height <= b.y + b.height;
  }

  function pruneFreeRectangles(rectangles) {
    const unique = [];
    const keys = new Set();
    rectangles.forEach(rect => {
      if (!(rect.width > 0 && rect.height > 0)) return;
      const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`;
      if (keys.has(key)) return;
      keys.add(key);
      unique.push(rect);
    });
    const kept = unique.filter((rect, index) => !unique.some(
      (other, otherIndex) => index !== otherIndex && containedBy(rect, other)
    ));
    if (kept.length > MAX_FREE_RECTS) {
      throw new RangeError("当前尺寸组合产生了过多空白区域，请增大间隙或拆分订单后重试");
    }
    return kept.sort((a, b) => a.y - b.y || a.x - b.x || a.width - b.width || a.height - b.height);
  }

  function splitFreeRectangles(rectangles, occupied) {
    const next = [];
    rectangles.forEach(free => {
      if (!intersects(free, occupied)) {
        next.push(free);
        return;
      }
      if (occupied.x > free.x) {
        next.push({
          x: free.x,
          y: free.y,
          width: occupied.x - free.x,
          height: free.height
        });
      }
      if (occupied.x + occupied.width < free.x + free.width) {
        next.push({
          x: occupied.x + occupied.width,
          y: free.y,
          width: free.x + free.width - occupied.x - occupied.width,
          height: free.height
        });
      }
      if (occupied.y > free.y) {
        next.push({
          x: free.x,
          y: free.y,
          width: free.width,
          height: occupied.y - free.y
        });
      }
      if (occupied.y + occupied.height < free.y + free.height) {
        next.push({
          x: free.x,
          y: occupied.y + occupied.height,
          width: free.width,
          height: free.y + free.height - occupied.y - occupied.height
        });
      }
    });
    return pruneFreeRectangles(next);
  }

  function lexicographicCompare(left, right) {
    const length = Math.max(left.length, right.length);
    for (let index = 0; index < length; index += 1) {
      const a = left[index] ?? 0;
      const b = right[index] ?? 0;
      if (a < b) return -1;
      if (a > b) return 1;
    }
    return 0;
  }

  function placementScore(option, heuristic, remaining, sku, patternCount) {
    const shortSide = Math.min(option.free.width - option.footprintWidth, option.free.height - option.footprintHeight);
    const longSide = Math.max(option.free.width - option.footprintWidth, option.free.height - option.footprintHeight);
    const unusedArea = option.free.width * option.free.height - option.footprintWidth * option.footprintHeight;
    const itemArea = option.width * option.height;
    const stillNeeded = remaining[sku.sourceIndex] - patternCount;
    const demandRatio = stillNeeded / sku.quantity;
    const priority = sku.priority;
    let score;
    if (heuristic === "best_short_side_fit") {
      score = [shortSide, longSide, unusedArea, -stillNeeded * priority];
    } else if (heuristic === "largest_first") {
      score = [-itemArea, unusedArea, shortSide, -stillNeeded * priority];
    } else if (heuristic === "demand_balanced") {
      score = [-Math.round(demandRatio * priority * 1000000), unusedArea, shortSide, -itemArea];
    } else {
      score = [unusedArea, shortSide, longSide, -stillNeeded * priority];
    }
    score.push(
      sku.sourceIndex,
      option.rotation,
      option.free.y,
      option.free.x
    );
    return score;
  }

  function findBestPlacement(context, remaining, counts, freeRects, heuristic) {
    let best = null;
    context.skus.forEach(sku => {
      const alreadyPlaced = counts[sku.sourceIndex] || 0;
      if (alreadyPlaced >= remaining[sku.sourceIndex]) return;
      allowedOrientations(sku, context.sheetGrainDirection).forEach(rotation => {
        const size = orientedSize(sku, rotation);
        const footprintWidth = size.width + context.gap.x;
        const footprintHeight = size.height + context.gap.y;
        freeRects.forEach(free => {
          if (footprintWidth > free.width || footprintHeight > free.height) return;
          const option = {
            sku,
            rotation,
            width: size.width,
            height: size.height,
            footprintWidth,
            footprintHeight,
            free
          };
          option.score = placementScore(option, heuristic, remaining, sku, alreadyPlaced);
          if (!best || lexicographicCompare(option.score, best.score) < 0) best = option;
        });
      });
    });
    return best;
  }

  function createPattern(context, remaining, heuristic) {
    const virtualWidth = context.printable.width + context.gap.x;
    const virtualHeight = context.printable.height + context.gap.y;
    let freeRects = [{ x: 0, y: 0, width: virtualWidth, height: virtualHeight }];
    const counts = context.skus.map(() => 0);
    const placements = [];
    let usedArea = 0;
    while (placements.length < MAX_PATTERN_PLACEMENTS) {
      const best = findBestPlacement(context, remaining, counts, freeRects, heuristic);
      if (!best) break;
      const occupied = {
        x: best.free.x,
        y: best.free.y,
        width: best.footprintWidth,
        height: best.footprintHeight
      };
      placements.push({
        skuId: best.sku.id,
        skuName: best.sku.name,
        x: context.printable.x + occupied.x,
        y: context.printable.y + occupied.y,
        width: best.width,
        height: best.height,
        rotation: best.rotation
      });
      counts[best.sku.sourceIndex] += 1;
      usedArea += best.width * best.height;
      freeRects = splitFreeRectangles(freeRects, occupied);
    }
    if (placements.length === MAX_PATTERN_PLACEMENTS &&
        findBestPlacement(context, remaining, counts, freeRects, heuristic)) {
      throw new RangeError(`单张版面超过${MAX_PATTERN_PLACEMENTS}个成品，已触发安全上限`);
    }
    return { placements, counts, usedArea };
  }

  function patternRepeat(counts, remaining, availableSheets) {
    let repeat = Number.POSITIVE_INFINITY;
    counts.forEach((count, index) => {
      if (count > 0) repeat = Math.min(repeat, Math.floor(remaining[index] / count));
    });
    if (!Number.isFinite(repeat) || repeat < 1) repeat = 1;
    if (availableSheets !== null) repeat = Math.min(repeat, availableSheets);
    return repeat;
  }

  function publicPlacement(placement) {
    return {
      skuId: placement.skuId,
      skuName: placement.skuName,
      xMm: toMm(placement.x),
      yMm: toMm(placement.y),
      widthMm: toMm(placement.width),
      heightMm: toMm(placement.height),
      rotation: placement.rotation
    };
  }

  function buildCandidate(context, heuristic, heuristicIndex) {
    const remaining = context.skus.map(sku => sku.quantity);
    const produced = context.skus.map(() => 0);
    const groups = [];
    let productionSheets = 0;
    let usedArea = 0;
    while (remaining.some(value => value > 0)) {
      if (groups.length >= MAX_PATTERN_GROUPS) {
        throw new RangeError(`拼版方案超过${MAX_PATTERN_GROUPS}种版式，请拆分订单或减少SKU`);
      }
      const availableSheets = context.maxSheets === null
        ? null
        : context.maxSheets - productionSheets;
      if (availableSheets !== null && availableSheets <= 0) break;
      const pattern = createPattern(context, remaining, heuristic);
      if (pattern.placements.length === 0) break;
      const repeat = patternRepeat(pattern.counts, remaining, availableSheets);
      if (repeat <= 0) break;
      const countsBySku = {};
      pattern.counts.forEach((count, index) => {
        if (!count) return;
        const output = count * repeat;
        remaining[index] -= output;
        produced[index] += output;
        countsBySku[context.skus[index].id] = count;
      });
      productionSheets += repeat;
      usedArea += pattern.usedArea * repeat;
      const patternUtilization = pattern.usedArea /
        (context.printable.width * context.printable.height);
      groups.push({
        patternId: `${heuristic}-P${String(groups.length + 1).padStart(2, "0")}`,
        repeat,
        countsBySku,
        piecesPerSheet: pattern.placements.length,
        utilizationRate: round(patternUtilization, 6),
        placements: pattern.placements.map(publicPlacement),
        explanation: ""
      });
    }

    groups.forEach(group => {
      const composition = Object.keys(group.countsBySku)
        .map(id => `${id} ${group.countsBySku[id]}件`)
        .join("、");
      group.explanation = `${group.patternId}生产${group.repeat}张：每张${composition}；版面利用率${round(group.utilizationRate * 100, 2)}%`;
    });

    const targetTotal = context.skus.reduce((sum, sku) => sum + sku.quantity, 0);
    const producedTotal = produced.reduce((sum, value) => sum + value, 0);
    const printableAreaTotal = context.printable.width * context.printable.height * productionSheets;
    const utilizationRate = printableAreaTotal > 0 ? usedArea / printableAreaTotal : 0;
    const wasteArea = Math.max(0, printableAreaTotal - usedArea);
    const perSku = context.skus.map((sku, index) => ({
      skuId: sku.id,
      skuName: sku.name,
      targetQuantity: sku.quantity,
      plannedOutput: produced[index],
      shortage: Math.max(0, sku.quantity - produced[index]),
      overrun: Math.max(0, produced[index] - sku.quantity),
      completionRate: round(Math.min(1, produced[index] / sku.quantity), 6)
    }));
    const mixedSheetCount = groups.reduce(
      (sum, group) => sum + (Object.keys(group.countsBySku).length > 1 ? group.repeat : 0),
      0
    );
    const explanation = [
      `采用“${HEURISTICS[heuristic].label}”策略生成${groups.length}种版式，共生产${productionSheets}张。`,
      `完成${producedTotal}/${targetTotal}件，综合版面利用率${round(utilizationRate * 100, 2)}%，空白损耗${round((1 - utilizationRate) * 100, 2)}%。`,
      mixedSheetCount > 0
        ? `其中${mixedSheetCount}张为跨SKU混拼，可减少单独开版造成的空白。`
        : "本方案没有形成跨SKU混拼，通常是尺寸、纹向或数量不适合混放。"
    ];
    if (perSku.some(item => item.shortage > 0)) {
      explanation.push("受最大生产张数限制，方案存在欠产；请增加允许张数或拆分订单。");
    }
    return {
      candidateId: `candidate-${heuristic}`,
      heuristic,
      heuristicLabel: HEURISTICS[heuristic].label,
      heuristicIndex,
      productionSheets,
      patternCount: groups.length,
      mixedSheetCount,
      utilizationRate: round(utilizationRate, 6),
      wasteRate: round(productionSheets > 0 ? 1 - utilizationRate : 0, 6),
      usedAreaM2: round(usedArea / 1000000000000, 6),
      wasteAreaM2: round(wasteArea / 1000000000000, 6),
      targetTotal,
      producedTotal,
      shortageTotal: Math.max(0, targetTotal - producedTotal),
      perSku,
      sheetGroups: groups,
      explanation
    };
  }

  function candidateComparator(left, right) {
    const leftCoverage = left.targetTotal ? left.producedTotal / left.targetTotal : 0;
    const rightCoverage = right.targetTotal ? right.producedTotal / right.targetTotal : 0;
    if (leftCoverage !== rightCoverage) return rightCoverage - leftCoverage;
    if (left.productionSheets !== right.productionSheets) {
      return left.productionSheets - right.productionSheets;
    }
    if (left.utilizationRate !== right.utilizationRate) {
      return right.utilizationRate - left.utilizationRate;
    }
    if (left.patternCount !== right.patternCount) return left.patternCount - right.patternCount;
    return left.heuristicIndex - right.heuristicIndex;
  }

  function separateBaseline(context) {
    let productionSheets = 0;
    let usedArea = 0;
    const perSku = [];
    context.skus.forEach(sourceSku => {
      const sku = { ...sourceSku, sourceIndex: 0 };
      const singleContext = { ...context, skus: [sku], maxSheets: null };
      const remaining = [sku.quantity];
      const pattern = createPattern(singleContext, remaining, "best_area_fit");
      if (!pattern.placements.length) {
        throw new RangeError(`${sku.id}无法独立拼版`);
      }
      const capacity = pattern.counts[0];
      const sheets = Math.ceil(sku.quantity / capacity);
      productionSheets += sheets;
      usedArea += sku.width * sku.height * sku.quantity;
      perSku.push({
        skuId: sku.id,
        capacityPerSheet: capacity,
        productionSheets: sheets
      });
    });
    const totalArea = context.printable.width * context.printable.height * productionSheets;
    return {
      productionSheets,
      utilizationRate: round(totalArea > 0 ? usedArea / totalArea : 0, 6),
      perSku
    };
  }

  function publicInput(context) {
    return {
      sheet: {
        widthMm: toMm(context.sheetWidth),
        heightMm: toMm(context.sheetHeight),
        printableXmm: toMm(context.printable.x),
        printableYmm: toMm(context.printable.y),
        printableWidthMm: toMm(context.printable.width),
        printableHeightMm: toMm(context.printable.height),
        marginsMm: Object.fromEntries(
          Object.entries(context.margins).map(([key, value]) => [key, toMm(value)])
        ),
        unprintableMm: Object.fromEntries(
          Object.entries(context.unprintable).map(([key, value]) => [key, toMm(value)])
        ),
        effectiveMarginsMm: Object.fromEntries(
          Object.entries(context.effectiveMargins).map(([key, value]) => [key, toMm(value)])
        ),
        gripperMm: toMm(context.gripper),
        gripperEdge: context.gripperEdge,
        grainDirection: context.sheetGrainDirection
      },
      gapMm: { x: toMm(context.gap.x), y: toMm(context.gap.y) },
      maxSheets: context.maxSheets,
      skus: context.skus.map(sku => ({
        id: sku.id,
        name: sku.name,
        widthMm: toMm(sku.width),
        heightMm: toMm(sku.height),
        quantity: sku.quantity,
        allowRotate: sku.allowRotate,
        grainDirection: sku.grainDirection,
        priority: sku.priority
      }))
    };
  }

  function calculateMultiSkuImposition(input) {
    const context = normalizeInput(input);
    ensureEverySkuFits(context);
    const candidates = context.heuristics.map((heuristic, index) =>
      buildCandidate(context, heuristic, index)
    );
    const ranked = candidates.slice().sort(candidateComparator);
    ranked.forEach((candidate, index) => {
      candidate.rank = index + 1;
      candidate.recommended = index === 0;
    });
    const recommended = ranked[0];
    const baseline = separateBaseline(context);
    const comparison = ranked.map(candidate => ({
      rank: candidate.rank,
      candidateId: candidate.candidateId,
      heuristic: candidate.heuristic,
      heuristicLabel: candidate.heuristicLabel,
      productionSheets: candidate.productionSheets,
      utilizationRate: candidate.utilizationRate,
      wasteRate: candidate.wasteRate,
      shortageTotal: candidate.shortageTotal,
      patternCount: candidate.patternCount,
      recommended: candidate.recommended,
      reason: candidate.recommended
        ? "在订单完成率、生产张数、利用率和版式数量的综合排序中最优"
        : `比推荐方案${candidate.productionSheets - recommended.productionSheets >= 0 ? "多" : "少"}${Math.abs(candidate.productionSheets - recommended.productionSheets)}张，利用率${round((candidate.utilizationRate - recommended.utilizationRate) * 100, 2)}个百分点`
    }));
    return {
      engine: "JianglaiImpositionEngine",
      version: ENGINE_VERSION,
      input: publicInput(context),
      recommended,
      candidates: ranked,
      comparison,
      separateBaseline: {
        ...baseline,
        sheetSavings: Math.max(0, baseline.productionSheets - recommended.productionSheets),
        savingsRate: baseline.productionSheets > 0
          ? round(Math.max(0, baseline.productionSheets - recommended.productionSheets) / baseline.productionSheets, 6)
          : 0
      }
    };
  }

  function verifyPlan(plan) {
    const errors = [];
    if (!plan || typeof plan !== "object") {
      return { valid: false, errors: ["方案必须是对象"] };
    }
    const input = plan.input || {};
    const sheet = input.sheet || {};
    const recommended = plan.recommended || plan;
    const groups = recommended.sheetGroups;
    if (!Array.isArray(groups)) return { valid: false, errors: ["方案缺少版式列表"] };
    const left = Number(sheet.printableXmm);
    const top = Number(sheet.printableYmm);
    const right = left + Number(sheet.printableWidthMm);
    const bottom = top + Number(sheet.printableHeightMm);
    const gapX = Number(input.gapMm && input.gapMm.x || 0);
    const gapY = Number(input.gapMm && input.gapMm.y || 0);
    groups.forEach(group => {
      const placements = Array.isArray(group.placements) ? group.placements : [];
      placements.forEach((placement, index) => {
        const x = Number(placement.xMm);
        const y = Number(placement.yMm);
        const width = Number(placement.widthMm);
        const height = Number(placement.heightMm);
        if (![x, y, width, height].every(Number.isFinite) || width <= 0 || height <= 0) {
          errors.push(`${group.patternId}第${index + 1}个位置数据无效`);
          return;
        }
        if (x < left - 0.001 || y < top - 0.001 ||
            x + width > right + 0.001 || y + height > bottom + 0.001) {
          errors.push(`${group.patternId}第${index + 1}个位置超出可印区域`);
        }
      });
      for (let first = 0; first < placements.length; first += 1) {
        for (let second = first + 1; second < placements.length; second += 1) {
          const a = placements[first];
          const b = placements[second];
          const separated = a.xMm + a.widthMm + gapX <= b.xMm + 0.001 ||
            b.xMm + b.widthMm + gapX <= a.xMm + 0.001 ||
            a.yMm + a.heightMm + gapY <= b.yMm + 0.001 ||
            b.yMm + b.heightMm + gapY <= a.yMm + 0.001;
          if (!separated) {
            errors.push(`${group.patternId}第${first + 1}和第${second + 1}个位置重叠或间隙不足`);
          }
        }
      }
    });
    return { valid: errors.length === 0, errors };
  }

  return {
    ENGINE_VERSION,
    HEURISTICS,
    calculateMultiSkuImposition,
    calculateImposition: calculateMultiSkuImposition,
    optimize: calculateMultiSkuImposition,
    verifyPlan
  };
});
