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

  const MICRONS_PER_MM = 1000;
  const MAX_SLOTS = 20000;
  const MAX_DIMENSION_MM = 100000;
  const MAX_UPLOAD_BYTES = 30 * 1024 * 1024;
  const SVG_POINTS_PER_MM = 72 / 25.4;

  const roundMm = value => Number((value / MICRONS_PER_MM).toFixed(3));
  const mmToMicrons = (value, label, options = {}) => {
    if (value === null || value === "" || typeof value === "boolean") throw new TypeError(`${label}必须填写数字`);
    const number = Number(value);
    if (!Number.isFinite(number)) throw new TypeError(`${label}必须是有限数字`);
    const minimum = options.allowZero ? 0 : (options.minimum ?? 0.1);
    if (number < minimum) throw new RangeError(`${label}${options.allowZero ? "不能小于 0" : "必须大于 0"}`);
    if (number > MAX_DIMENSION_MM) throw new RangeError(`${label}不能超过 ${MAX_DIMENSION_MM} mm`);
    return Math.round(number * MICRONS_PER_MM);
  };

  const positiveInteger = (value, label) => {
    if (value === null || value === "" || typeof value === "boolean") throw new TypeError(`${label}必须填写正整数`);
    const number = Number(value);
    if (!Number.isInteger(number) || number <= 0) throw new RangeError(`${label}必须是正整数`);
    if (number > 100000000) throw new RangeError(`${label}不能超过 100,000,000`);
    return number;
  };

  function parsePhysicalLength(value) {
    const match = String(value ?? "").trim().match(/^([+]?(?:\d+(?:\.\d*)?|\.\d+))\s*(mm|cm|in|pt|pc|px)$/i);
    if (!match) return null;
    const number = Number(match[1]);
    if (!(number > 0)) return null;
    const factor = { mm:1, cm:10, in:25.4, pt:25.4 / 72, pc:25.4 / 6, px:25.4 / 96 }[match[2].toLowerCase()];
    return Number((number * factor).toFixed(3));
  }

  function parsePdfPageSize(input) {
    const bytes = input instanceof Uint8Array ? input : new Uint8Array(input || []);
    let source = "";
    for (let offset = 0; offset < Math.min(bytes.length, 5 * 1024 * 1024); offset += 32768) {
      source += String.fromCharCode(...bytes.subarray(offset, Math.min(bytes.length, offset + 32768)));
    }
    if (!/^%PDF-/i.test(source.slice(0, 16))) throw new TypeError("文件不是有效的 PDF 或 PDF 兼容 AI");
    const boxes = [...source.matchAll(/\/(?:MediaBox|CropBox)\s*\[\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\]/gi)];
    const first = boxes.find(match => Number(match[3]) > Number(match[1]) && Number(match[4]) > Number(match[2]));
    if (!first) throw new TypeError("未能读取 PDF 页面尺寸，请补填刀板图实际宽高");
    return {
      widthMm:Number(((Number(first[3]) - Number(first[1])) * 25.4 / 72).toFixed(3)),
      heightMm:Number(((Number(first[4]) - Number(first[2])) * 25.4 / 72).toFixed(3)),
      pageCount:Math.max(1, (source.match(/\/Type\s*\/Page\b/g) || []).length)
    };
  }

  function parseRasterDpi(input) {
    const bytes = input instanceof Uint8Array ? input : new Uint8Array(input || []);
    if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
      for (let offset = 8; offset + 12 <= bytes.length;) {
        const length = ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0;
        const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
        if (type === "pHYs" && length >= 9 && offset + 17 <= bytes.length && bytes[offset + 16] === 1) {
          const xPpm = ((bytes[offset + 8] << 24) | (bytes[offset + 9] << 16) | (bytes[offset + 10] << 8) | bytes[offset + 11]) >>> 0;
          const yPpm = ((bytes[offset + 12] << 24) | (bytes[offset + 13] << 16) | (bytes[offset + 14] << 8) | bytes[offset + 15]) >>> 0;
          if (xPpm > 0 && yPpm > 0) return { x:Number((xPpm * .0254).toFixed(3)), y:Number((yPpm * .0254).toFixed(3)), source:"PNG pHYs" };
        }
        offset += 12 + length;
      }
    }
    if (bytes.length >= 16 && bytes[0] === 0xff && bytes[1] === 0xd8) {
      for (let offset = 2; offset + 4 < bytes.length;) {
        if (bytes[offset] !== 0xff) { offset += 1; continue; }
        const marker = bytes[offset + 1];
        if (marker === 0xda || marker === 0xd9) break;
        if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { offset += 2; continue; }
        const length = (bytes[offset + 2] << 8) | bytes[offset + 3];
        if (length < 2 || offset + 2 + length > bytes.length) break;
        const data = offset + 4;
        if (marker === 0xe0 && length >= 16 && String.fromCharCode(...bytes.subarray(data, data + 5)) === "JFIF\0") {
          const units = bytes[data + 7];
          const xDensity = (bytes[data + 8] << 8) | bytes[data + 9];
          const yDensity = (bytes[data + 10] << 8) | bytes[data + 11];
          if (xDensity > 0 && yDensity > 0 && (units === 1 || units === 2)) {
            const factor = units === 1 ? 1 : 2.54;
            return { x:Number((xDensity * factor).toFixed(3)), y:Number((yDensity * factor).toFixed(3)), source:"JPEG JFIF" };
          }
        }
        offset += 2 + length;
      }
    }
    return null;
  }

  function detectRasterDielineBounds(image, dpi) {
    if (!image || !(image.naturalWidth > 0) || !(image.naturalHeight > 0)) return null;
    if (typeof document === "undefined" || !document.createElement) return null;
    const maximum = 1200;
    const scale = Math.min(1, maximum / Math.max(image.naturalWidth, image.naturalHeight));
    const width = Math.max(1, Math.round(image.naturalWidth * scale));
    const height = Math.max(1, Math.round(image.naturalHeight * scale));
    const canvas = document.createElement("canvas");
    canvas.width = width; canvas.height = height;
    const context = canvas.getContext("2d", { willReadFrequently:true });
    if (!context) return null;
    context.fillStyle = "#fff"; context.fillRect(0, 0, width, height);
    context.drawImage(image, 0, 0, width, height);
    const pixels = context.getImageData(0, 0, width, height).data;
    const mask = new Uint8Array(width * height);
    for (let index = 0, pixel = 0; index < mask.length; index += 1, pixel += 4) {
      const red = pixels[pixel], green = pixels[pixel + 1], blue = pixels[pixel + 2];
      const darkLine = red < 115 && green < 115 && blue < 115;
      const redCrease = red > 155 && green < 115 && blue < 115 && red > green * 1.35;
      if (darkLine || redCrease) mask[index] = 1;
    }
    const originalMask = mask.slice();
    for (let y = 1; y < height - 1; y += 1) {
      for (let x = 1; x < width - 1; x += 1) {
        const index = y * width + x;
        if (!originalMask[index]) continue;
        mask[index - 1] = 1; mask[index + 1] = 1; mask[index - width] = 1; mask[index + width] = 1;
      }
    }
    const components = [];
    const stack = [];
    for (let start = 0; start < mask.length; start += 1) {
      if (mask[start] !== 1) continue;
      mask[start] = 2; stack.push(start);
      let count = 0, minX = width, minY = height, maxX = 0, maxY = 0;
      while (stack.length) {
        const current = stack.pop();
        const y = Math.floor(current / width), x = current - y * width;
        count += 1; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y;
        if (x > 0 && mask[current - 1] === 1) { mask[current - 1] = 2; stack.push(current - 1); }
        if (x + 1 < width && mask[current + 1] === 1) { mask[current + 1] = 2; stack.push(current + 1); }
        if (y > 0 && mask[current - width] === 1) { mask[current - width] = 2; stack.push(current - width); }
        if (y + 1 < height && mask[current + width] === 1) { mask[current + width] = 2; stack.push(current + width); }
      }
      const boxWidth = maxX - minX + 1, boxHeight = maxY - minY + 1;
      if (boxWidth >= width * .15 && boxHeight >= height * .15 && boxWidth <= width * .85 && boxHeight <= height * .85 && count >= 150) {
        components.push({ count, x:minX, y:minY, width:boxWidth, height:boxHeight });
      }
    }
    if (!components.length) return null;
    components.sort((a, b) => b.count - a.count);
    const primary = components[0];
    const similar = components.filter(item => Math.abs(item.width / primary.width - 1) <= .08 && Math.abs(item.height / primary.height - 1) <= .08);
    const average = key => similar.reduce((sum, item) => sum + item[key], 0) / similar.length;
    const pixelWidth = average("width") / scale;
    const pixelHeight = average("height") / scale;
    const result = { pixelWidth:Number(pixelWidth.toFixed(1)), pixelHeight:Number(pixelHeight.toFixed(1)), repeatedCount:similar.length, confidence:similar.length >= 2 ? "high" : "medium" };
    if (dpi?.x > 0 && dpi?.y > 0) {
      result.widthMm = Number((pixelWidth / dpi.x * 25.4).toFixed(1));
      result.heightMm = Number((pixelHeight / dpi.y * 25.4).toFixed(1));
    }
    return result;
  }

  function inspectSvgText(text, parser) {
    if (!parser) throw new Error("当前浏览器不能读取 SVG，请改用 PDF");
    const documentResult = parser.parseFromString(String(text || ""), "image/svg+xml");
    if (documentResult.querySelector("parsererror") || documentResult.documentElement?.localName !== "svg") throw new TypeError("SVG 文件结构无效");
    const rootSvg = documentResult.documentElement;
    const viewBoxValues = String(rootSvg.getAttribute("viewBox") || "").trim().split(/[\s,]+/).map(Number);
    const validViewBox = viewBoxValues.length === 4 && viewBoxValues.every(Number.isFinite) && viewBoxValues[2] > 0 && viewBoxValues[3] > 0;
    let widthMm = parsePhysicalLength(rootSvg.getAttribute("width"));
    let heightMm = parsePhysicalLength(rootSvg.getAttribute("height"));
    if (validViewBox && widthMm && !heightMm) heightMm = Number((widthMm * viewBoxValues[3] / viewBoxValues[2]).toFixed(3));
    if (validViewBox && heightMm && !widthMm) widthMm = Number((heightMm * viewBoxValues[2] / viewBoxValues[3]).toFixed(3));
    const viewBox = validViewBox ? viewBoxValues : [0, 0, widthMm || 1, heightMm || 1];
    const forbidden = new Set(["script", "foreignObject", "iframe", "object", "embed", "image", "audio", "video", "style", "use", "a", "set", "animate", "animateTransform", "animateMotion"]);
    const allowed = new Set(["svg", "g", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "text", "tspan"]);
    const allowedAttributes = new Set(["d", "x", "y", "x1", "y1", "x2", "y2", "cx", "cy", "r", "rx", "ry", "width", "height", "points", "transform", "fill", "fill-rule", "fill-opacity", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-dasharray", "stroke-dashoffset", "stroke-opacity", "opacity", "font-family", "font-size", "font-weight", "text-anchor", "dominant-baseline"]);
    [...rootSvg.querySelectorAll("*")].forEach(node => {
      const name = node.localName;
      if (forbidden.has(name) || !allowed.has(name)) { node.remove(); return; }
      [...node.attributes].forEach(attribute => {
        const value = String(attribute.value || "");
        if (!allowedAttributes.has(attribute.name) || /^on/i.test(attribute.name) || /(?:url\s*\(|javascript:|data:|https?:|file:)/i.test(value)) node.removeAttribute(attribute.name);
      });
    });
    [...rootSvg.attributes].forEach(attribute => rootSvg.removeAttribute(attribute.name));
    const serializer = typeof XMLSerializer !== "undefined" ? new XMLSerializer() : null;
    if (!serializer) throw new Error("当前浏览器不能安全处理 SVG");
    const markup = [...rootSvg.childNodes].map(node => serializer.serializeToString(node)).join("");
    if (!markup.trim()) throw new TypeError("SVG 中没有可用于排版的刀线或图形");
    return {
      kind:"svg", widthMm, heightMm, viewBox, markup,
      dimensionReliable:Boolean(widthMm && heightMm),
      note:widthMm && heightMm ? "已读取 SVG 毫米尺寸并保留真实刀线" : "SVG 没有可靠毫米单位，请补填实际宽高"
    };
  }

  async function inspectDieFile(file) {
    if (!file) throw new TypeError("请选择客户刀板文件");
    if (Number(file.size) > MAX_UPLOAD_BYTES) throw new RangeError("刀板文件不能超过 30MB");
    const name = String(file.name || "客户刀板");
    const extension = (name.split(".").pop() || "").toLowerCase();
    if (extension === "cdr") throw new TypeError("CDR 请先另存为 PDF 或 SVG 再排版");
    if (extension === "svg" || file.type === "image/svg+xml") {
      const result = inspectSvgText(await file.text(), typeof DOMParser !== "undefined" ? new DOMParser() : null);
      return { ...result, name, size:Number(file.size) || 0 };
    }
    if (["pdf", "ai"].includes(extension) || file.type === "application/pdf") {
      const bytes = new Uint8Array(await file.arrayBuffer());
      let pageDimensions = null;
      try { pageDimensions = parsePdfPageSize(bytes); } catch (_) { /* server parser gives the actionable error */ }
      if (typeof fetch !== "function") throw new Error("当前环境不能解析 PDF 真实刀线，请使用桌面软件最新版");
      const authToken = typeof window !== "undefined" && typeof window.__currentFormalApiToken === "function"
        ? String(window.__currentFormalApiToken() || "")
        : "";
      const headers = { "Content-Type":"application/pdf" };
      if (authToken) headers.Authorization = `Bearer ${authToken}`;
      const response = await fetch("/api/dieline/inspect", {
        method:"POST",
        credentials:"same-origin",
        headers,
        body:file
      });
      let payload = {};
      try { payload = await response.json(); } catch (_) { payload = {}; }
      if (!response.ok || !payload?.ok || !payload?.exactContour) {
        const pageHint = pageDimensions ? `（PDF页面是${pageDimensions.widthMm}×${pageDimensions.heightMm}mm，不能把页面误当刀板）` : "";
        throw new TypeError(`${payload?.error || "没有从 PDF 中识别到闭合矢量刀线"}${pageHint}`);
      }
      const widthMm = Number(payload.widthMm);
      const heightMm = Number(payload.heightMm);
      if (!(widthMm > 0 && heightMm > 0) || !Array.isArray(payload.outline) || payload.outline.length < 4) {
        throw new TypeError("PDF 刀线轮廓不完整，请另存为保留矢量刀线的 PDF 或 SVG");
      }
      const pageWidth = Number(payload.pageWidthMm || pageDimensions?.widthMm || 0);
      const pageHeight = Number(payload.pageHeightMm || pageDimensions?.heightMm || 0);
      const pageText = pageWidth > 0 && pageHeight > 0 ? `，已排除${pageWidth}×${pageHeight}mm页面空白` : "";
      return {
        kind:"pdf", name, size:Number(file.size) || bytes.length,
        widthMm, heightMm, pageWidthMm:pageWidth, pageHeightMm:pageHeight,
        pageCount:Number(payload.pageCount || pageDimensions?.pageCount || 1),
        outline:payload.outline, segments:Array.isArray(payload.segments) ? payload.segments : [],
        materialAreaMm2:Number(payload.materialAreaMm2 || 0), rectCount:Number(payload.rectCount || 0),
        exactContour:true, dimensionReliable:true, contourSource:String(payload.source || "pdf-vector"),
        note:`已提取 PDF 真实刀线外框 ${widthMm}×${heightMm}mm${pageText}`
      };
    }
    if (["png", "jpg", "jpeg", "webp"].includes(extension) || /^image\/(png|jpeg|webp)$/i.test(file.type || "")) {
      const bytes = new Uint8Array(await file.arrayBuffer());
      const dpi = parseRasterDpi(bytes);
      const url = URL.createObjectURL(file);
      try {
        const loaded = await new Promise((resolve, reject) => {
          const image = new Image();
          image.onload = () => resolve(image);
          image.onerror = () => reject(new TypeError("图片无法读取，请换用 PDF 或 SVG"));
          image.src = url;
        });
        const pixelWidth = loaded.naturalWidth, pixelHeight = loaded.naturalHeight;
        const dielineBounds = detectRasterDielineBounds(loaded, dpi);
        if (dpi && dielineBounds?.widthMm > 0 && dielineBounds?.heightMm > 0) {
          const repeatText = dielineBounds.repeatedCount > 1 ? `，检测到${dielineBounds.repeatedCount}套相同刀板` : "";
          return {
            kind:"raster", name, size:Number(file.size) || bytes.length, pixelWidth, pixelHeight, dpi, dielineBounds,
            widthMm:dielineBounds.widthMm, heightMm:dielineBounds.heightMm, dimensionReliable:true,
            note:`已按${dpi.x}DPI识别单套刀线外框${dielineBounds.widthMm}×${dielineBounds.heightMm}mm${repeatText}`
          };
        }
        const dpiText = dpi ? `；已读取${dpi.x}DPI，但未能分离单套刀线` : "；图片没有可靠DPI";
        return { kind:"raster", name, size:Number(file.size) || bytes.length, pixelWidth, pixelHeight, dpi, dielineBounds, dimensionReliable:false, note:`已读取 ${pixelWidth}×${pixelHeight}px${dpiText}，请补填单套刀板实际宽高` };
      } finally { URL.revokeObjectURL(url); }
    }
    throw new TypeError("暂支持 SVG、PDF、PDF兼容AI、PNG、JPG 和 WebP");
  }

  function normalizeMachineOptions(input = {}) {
    if (!input || typeof input !== "object" || Array.isArray(input)) throw new TypeError("排版参数必须是对象");
    const margins = input.marginsMm && typeof input.marginsMm === "object" ? input.marginsMm : {};
    const gaps = input.gapMm && typeof input.gapMm === "object" ? input.gapMm : {};
    const rotation = ["forbid", "allow", "force"].includes(input.rotation) ? input.rotation : "allow";
    const align = ["center", "top-left"].includes(input.align) ? input.align : "center";
    const strategy = ["balanced", "min-sheets", "min-overrun"].includes(input.strategy) ? input.strategy : "balanced";
    const gripperEdge = ["top", "right", "bottom", "left"].includes(input.gripperEdge) ? input.gripperEdge : "top";
    return {
      boardWidth: mmToMicrons(input.boardWidthMm, "刀板宽度"),
      boardHeight: mmToMicrons(input.boardHeightMm, "刀板高度"),
      margins: {
        top: mmToMicrons(margins.top ?? 0, "上边距", { allowZero:true }),
        right: mmToMicrons(margins.right ?? 0, "右边距", { allowZero:true }),
        bottom: mmToMicrons(margins.bottom ?? 0, "下边距", { allowZero:true }),
        left: mmToMicrons(margins.left ?? 0, "左边距", { allowZero:true })
      },
      gap: {
        x: mmToMicrons(gaps.x ?? 0, "横向刀间距", { allowZero:true }),
        y: mmToMicrons(gaps.y ?? 0, "纵向刀间距", { allowZero:true })
      },
      gripper: mmToMicrons(input.gripperMm ?? 0, "咬口预留", { allowZero:true }),
      gripperEdge,
      rotation,
      align,
      strategy
    };
  }

  function normalizeOptions(input = {}) {
    if (!input || typeof input !== "object" || Array.isArray(input)) throw new TypeError("排版参数必须是对象");
    return {
      projectName: String(input.projectName || "刀板排版").trim().slice(0, 80) || "刀板排版",
      ...normalizeMachineOptions(input),
      partWidth: mmToMicrons(input.partWidthMm, "单模宽度"),
      partHeight: mmToMicrons(input.partHeightMm, "单模高度"),
      quantity: positiveInteger(input.quantity, "订单数量")
    };
  }

  function fitCount(available, item, gap) {
    if (available < item) return 0;
    return Math.floor((available + gap) / (item + gap));
  }

  function effectiveBoard(options) {
    const reserves = { ...options.margins };
    reserves[options.gripperEdge] += options.gripper;
    const width = options.boardWidth - reserves.left - reserves.right;
    const height = options.boardHeight - reserves.top - reserves.bottom;
    if (width <= 0 || height <= 0) throw new RangeError("边距和咬口已经占满刀板，请减小预留尺寸");
    return { x:reserves.left, y:reserves.top, width, height, reserves };
  }

  function buildCandidate(options, effective, orientation) {
    const width = orientation === 90 ? options.partHeight : options.partWidth;
    const height = orientation === 90 ? options.partWidth : options.partHeight;
    const columns = fitCount(effective.width, width, options.gap.x);
    const rows = fitCount(effective.height, height, options.gap.y);
    const capacity = columns * rows;
    if (!capacity) return {
      fits:false,
      orientation,
      columns,
      rows,
      capacityPerSheet:0,
      reason:`${orientation}°方向的单模尺寸超出有效幅面`
    };
    if (capacity > MAX_SLOTS) return {
      fits:false,
      orientation,
      columns,
      rows,
      capacityPerSheet:capacity,
      exceedsSafeLimit:true,
      reason:`${orientation}°方向将产生 ${capacity.toLocaleString("zh-CN")} 个刀位，超过安全上限 ${MAX_SLOTS.toLocaleString("zh-CN")}；请检查单位或尺寸`
    };
    const usedWidth = columns * width + Math.max(0, columns - 1) * options.gap.x;
    const usedHeight = rows * height + Math.max(0, rows - 1) * options.gap.y;
    const startX = effective.x + (options.align === "center" ? Math.floor((effective.width - usedWidth) / 2) : 0);
    const startY = effective.y + (options.align === "center" ? Math.floor((effective.height - usedHeight) / 2) : 0);
    const placements = [];
    for (let row = 0; row < rows; row += 1) {
      for (let column = 0; column < columns; column += 1) {
        placements.push({
          index:placements.length + 1,
          row:row + 1,
          column:column + 1,
          x:roundMm(startX + column * (width + options.gap.x)),
          y:roundMm(startY + row * (height + options.gap.y)),
          width:roundMm(width),
          height:roundMm(height),
          rotation:orientation
        });
      }
    }
    const sheetsRequired = Math.ceil(options.quantity / capacity);
    const lastSheetCount = options.quantity - (sheetsRequired - 1) * capacity;
    const overrun = sheetsRequired * capacity - options.quantity;
    const partArea = options.partWidth * options.partHeight;
    const totalBoardArea = options.boardWidth * options.boardHeight;
    const effectiveArea = effective.width * effective.height;
    const totalUsedArea = capacity * partArea;
    return {
      fits:true,
      id:`orientation-${orientation}`,
      orientation,
      columns,
      rows,
      capacityPerSheet:capacity,
      sheetsRequired,
      lastSheetCount,
      overrun,
      usedBounds:{ x:roundMm(startX), y:roundMm(startY), width:roundMm(usedWidth), height:roundMm(usedHeight) },
      utilization:{
        totalBoardPercent:Number((totalUsedArea / totalBoardArea * 100).toFixed(1)),
        effectiveBoardPercent:Number((totalUsedArea / effectiveArea * 100).toFixed(1))
      },
      placements,
      warnings:orientation === 90 ? ["当前方案旋转了90°，请确认纸张纹向和图文方向允许旋转。"] : []
    };
  }

  function normalizeContour(input, options) {
    const source = input?.contour && typeof input.contour === "object" ? input.contour : null;
    if (!source?.exactContour || !Array.isArray(source.outline) || source.outline.length < 4) return null;
    const points = source.outline.map((point, index) => ({
      x:mmToMicrons(point?.x, `刀线第${index + 1}个点X`, { allowZero:true }),
      y:mmToMicrons(point?.y, `刀线第${index + 1}个点Y`, { allowZero:true })
    }));
    const minX = Math.min(...points.map(point => point.x));
    const minY = Math.min(...points.map(point => point.y));
    points.forEach(point => { point.x -= minX; point.y -= minY; });
    const width = Math.max(...points.map(point => point.x));
    const height = Math.max(...points.map(point => point.y));
    if (!(width > 0 && height > 0)) throw new RangeError("刀线轮廓尺寸无效");
    const sourceArea = Number(source.materialAreaMm2);
    const polygonArea = Math.abs(points.reduce((sum, point, index) => {
      const next = points[(index + 1) % points.length];
      return sum + point.x * next.y - next.x * point.y;
    }, 0) / 2);
    return {
      points,
      width,
      height,
      materialArea:Number.isFinite(sourceArea) && sourceArea > 0 ? Math.round(sourceArea * MICRONS_PER_MM * MICRONS_PER_MM) : polygonArea,
      source
    };
  }

  function rotatedContour(contour, rotation) {
    const width = contour.width, height = contour.height;
    const points = contour.points.map(point => {
      if (rotation === 90) return { x:height - point.y, y:point.x };
      if (rotation === 180) return { x:width - point.x, y:height - point.y };
      if (rotation === 270) return { x:point.y, y:width - point.x };
      return { x:point.x, y:point.y };
    });
    return { rotation, points, width:rotation % 180 ? height : width, height:rotation % 180 ? width : height };
  }

  const crossProduct = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
  const onSegment = (a, b, point, epsilon = 1) => Math.abs(crossProduct(a, b, point)) <= epsilon
    && point.x >= Math.min(a.x, b.x) - epsilon && point.x <= Math.max(a.x, b.x) + epsilon
    && point.y >= Math.min(a.y, b.y) - epsilon && point.y <= Math.max(a.y, b.y) + epsilon;

  function pointInPolygon(point, polygon, strict = true) {
    let inside = false;
    for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index++) {
      const a = polygon[previous], b = polygon[index];
      if (onSegment(a, b, point)) return !strict;
      if ((a.y > point.y) !== (b.y > point.y)) {
        const crossingX = (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x;
        if (point.x < crossingX) inside = !inside;
      }
    }
    return inside;
  }

  function properSegmentIntersection(a, b, c, d) {
    const abC = crossProduct(a, b, c), abD = crossProduct(a, b, d);
    const cdA = crossProduct(c, d, a), cdB = crossProduct(c, d, b);
    return ((abC > 1 && abD < -1) || (abC < -1 && abD > 1))
      && ((cdA > 1 && cdB < -1) || (cdA < -1 && cdB > 1));
  }

  function pointSegmentDistanceSquared(point, a, b) {
    const dx = b.x - a.x, dy = b.y - a.y;
    if (dx === 0 && dy === 0) return (point.x - a.x) ** 2 + (point.y - a.y) ** 2;
    const ratio = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / (dx * dx + dy * dy)));
    const x = a.x + ratio * dx, y = a.y + ratio * dy;
    return (point.x - x) ** 2 + (point.y - y) ** 2;
  }

  function segmentDistanceSquared(a, b, c, d) {
    if (properSegmentIntersection(a, b, c, d)) return 0;
    return Math.min(
      pointSegmentDistanceSquared(a, c, d), pointSegmentDistanceSquared(b, c, d),
      pointSegmentDistanceSquared(c, a, b), pointSegmentDistanceSquared(d, a, b)
    );
  }

  function polygonBounds(points) {
    const xs = points.map(point => point.x), ys = points.map(point => point.y);
    return { minX:Math.min(...xs), minY:Math.min(...ys), maxX:Math.max(...xs), maxY:Math.max(...ys) };
  }

  function polygonsConflict(first, second, clearance) {
    const a = first.bounds, b = second.bounds;
    if (a.maxX + clearance <= b.minX || b.maxX + clearance <= a.minX || a.maxY + clearance <= b.minY || b.maxY + clearance <= a.minY) return false;
    const one = first.points, two = second.points;
    for (let i = 0; i < one.length; i += 1) {
      const a1 = one[i], a2 = one[(i + 1) % one.length];
      for (let j = 0; j < two.length; j += 1) {
        const b1 = two[j], b2 = two[(j + 1) % two.length];
        if (properSegmentIntersection(a1, a2, b1, b2)) return true;
        if (clearance > 0 && segmentDistanceSquared(a1, a2, b1, b2) < clearance * clearance - 1) return true;
      }
    }
    const samplesInside = (source, target) => source.some((point, index) => {
      if (pointInPolygon(point, target, true)) return true;
      const next = source[(index + 1) % source.length];
      return pointInPolygon({ x:(point.x + next.x) / 2, y:(point.y + next.y) / 2 }, target, true);
    });
    return samplesInside(one, two) || samplesInside(two, one);
  }

  function contourScanStep(contour) {
    const coordinates = [...contour.points.map(point => point.x), ...contour.points.map(point => point.y)].sort((a, b) => a - b);
    let smallest = Infinity;
    for (let index = 1; index < coordinates.length; index += 1) {
      const difference = coordinates[index] - coordinates[index - 1];
      if (difference >= 1000 && difference < smallest) smallest = difference;
    }
    return Math.max(1000, Math.min(5000, Number.isFinite(smallest) ? smallest : 2000));
  }

  function buildContourCandidate(options, effective, contour, rotations, id) {
    const variants = rotations.map(rotation => rotatedContour(contour, rotation));
    const clearance = Math.max(options.gap.x, options.gap.y);
    const step = contourScanStep(contour);
    const placements = [];
    const yStop = effective.y + effective.height;
    const xStop = effective.x + effective.width;
    for (let y = effective.y; y <= yStop; y += step) {
      for (let x = effective.x; x <= xStop; x += step) {
        const variantOrder = ((Math.round((x - effective.x) / step) + Math.round((y - effective.y) / step)) % 2)
          ? [...variants].reverse() : variants;
        for (const variant of variantOrder) {
          if (x + variant.width > xStop || y + variant.height > yStop) continue;
          const points = variant.points.map(point => ({ x:point.x + x, y:point.y + y }));
          const shape = { points, bounds:polygonBounds(points) };
          if (placements.some(item => polygonsConflict(shape, item._shape, clearance))) continue;
          placements.push({
            index:placements.length + 1,
            row:0,
            column:0,
            x:roundMm(x), y:roundMm(y), width:roundMm(variant.width), height:roundMm(variant.height),
            rotation:variant.rotation,
            points:points.map(point => ({ x:roundMm(point.x), y:roundMm(point.y) })),
            _shape:shape
          });
          break;
        }
        if (placements.length >= MAX_SLOTS) break;
      }
      if (placements.length >= MAX_SLOTS) break;
    }
    if (!placements.length) return { fits:false, id, contourMode:true, reason:"真实刀线轮廓无法放入当前有效幅面" };
    const publicPlacements = placements.map(({ _shape, ...placement }) => placement);
    const capacity = publicPlacements.length;
    const sheetsRequired = Math.ceil(options.quantity / capacity);
    const lastSheetCount = options.quantity - (sheetsRequired - 1) * capacity;
    const overrun = sheetsRequired * capacity - options.quantity;
    const bounds = polygonBounds(placements.flatMap(item => item._shape.points));
    const totalUsedArea = capacity * contour.materialArea;
    const distinctRotations = [...new Set(publicPlacements.map(item => item.rotation))].sort((a, b) => a - b);
    return {
      fits:true, id, contourMode:true, orientation:distinctRotations[0] || 0, rotations:distinctRotations,
      columns:null, rows:null, scanStepMm:roundMm(step), capacityPerSheet:capacity,
      sheetsRequired, lastSheetCount, overrun,
      usedBounds:{ x:roundMm(bounds.minX), y:roundMm(bounds.minY), width:roundMm(bounds.maxX - bounds.minX), height:roundMm(bounds.maxY - bounds.minY) },
      utilization:{
        totalBoardPercent:Number((totalUsedArea / (options.boardWidth * options.boardHeight) * 100).toFixed(1)),
        effectiveBoardPercent:Number((totalUsedArea / (effective.width * effective.height) * 100).toFixed(1))
      },
      placements:publicPlacements,
      warnings:["当前方案按真实凹凸刀线做轮廓碰撞和交叉咬合；上机前仍需刀模师确认桥位、清废和共刀条件。"]
    };
  }

  function candidateComparator(strategy) {
    return (a, b) => {
      if (strategy === "min-overrun") {
        return a.overrun - b.overrun || a.sheetsRequired - b.sheetsRequired || b.utilization.effectiveBoardPercent - a.utilization.effectiveBoardPercent || a.orientation - b.orientation;
      }
      if (strategy === "min-sheets") {
        return a.sheetsRequired - b.sheetsRequired || b.capacityPerSheet - a.capacityPerSheet || a.overrun - b.overrun || a.orientation - b.orientation;
      }
      return a.sheetsRequired - b.sheetsRequired || a.overrun - b.overrun || b.utilization.effectiveBoardPercent - a.utilization.effectiveBoardPercent || a.orientation - b.orientation;
    };
  }

  function publicMachineOptions(options) {
    return {
      boardWidthMm:roundMm(options.boardWidth),
      boardHeightMm:roundMm(options.boardHeight),
      marginsMm:Object.fromEntries(Object.entries(options.margins).map(([key, value]) => [key, roundMm(value)])),
      gapMm:{ x:roundMm(options.gap.x), y:roundMm(options.gap.y) },
      gripperMm:roundMm(options.gripper),
      gripperEdge:options.gripperEdge,
      rotation:options.rotation,
      align:options.align,
      strategy:options.strategy,
      sourceWorkflowVersion:2
    };
  }

  function publicOptions(options) {
    return {
      projectName:options.projectName,
      ...publicMachineOptions(options),
      partWidthMm:roundMm(options.partWidth),
      partHeightMm:roundMm(options.partHeight),
      quantity:options.quantity
    };
  }

  function calculateLayout(input) {
    const options = normalizeOptions(input);
    const effective = effectiveBoard(options);
    const contour = normalizeContour(input, options);
    let attempted;
    if (contour) {
      const schedules = options.rotation === "force"
        ? [[[90, 270], "contour-90-270"]]
        : options.rotation === "allow"
          ? [[[0, 180], "contour-grain"], [[90, 270], "contour-rotated"], [[0, 180, 90, 270], "contour-mixed"]]
          : [[[0, 180], "contour-grain"]];
      attempted = schedules.map(([rotations, id]) => buildContourCandidate(options, effective, contour, rotations, id));
    } else {
      const orientations = options.rotation === "force" ? [90] : options.rotation === "allow" ? [0, 90] : [0];
      attempted = orientations.map(orientation => buildCandidate(options, effective, orientation));
    }
    const candidates = attempted.filter(candidate => candidate.fits).sort(candidateComparator(options.strategy));
    if (!candidates.length) {
      const oversized = attempted.find(candidate => candidate.exceedsSafeLimit);
      const error = new RangeError(oversized?.reason || "单模尺寸无法放入当前有效刀板，请检查刀板、边距、咬口或旋转设置");
      error.attempted = attempted;
      throw error;
    }
    const recommended = candidates[0];
    const sharedWarnings = [];
    if (options.gap.x === 0 || options.gap.y === 0) sharedWarnings.push("刀间距为0只表示紧贴预排，不代表已经验证共刀、桥位和清废条件。");
    sharedWarnings.push(contour
      ? "系统已按真实闭合刀线计算相交与间距；共刀、桥位、刀线补偿和清废结构仍须由刀模师复核后生产。"
      : "系统按上传刀板图的实际外接尺寸比较方向；异形、共刀、桥位、刀线补偿、互嵌和清废结构仍须由刀模师复核后生产。");
    return {
      schemaVersion:1,
      ...recommended,
      input:publicOptions(options),
      effectiveBoard:{
        x:roundMm(effective.x), y:roundMm(effective.y), width:roundMm(effective.width), height:roundMm(effective.height),
        reservesMm:Object.fromEntries(Object.entries(effective.reserves).map(([key, value]) => [key, roundMm(value)]))
      },
      contourMode:Boolean(contour),
      candidates,
      alternatives:candidates.slice(1),
      sharedWarnings,
      warnings:[...recommended.warnings, ...sharedWarnings]
    };
  }

  function escapeXml(value) {
    return String(value ?? "").replace(/[&<>"']/g, character => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", "\"":"&quot;", "'":"&apos;" })[character]);
  }

  function transformSourcePoint(point, slot, source) {
    const x = Number(point?.x) || 0, y = Number(point?.y) || 0;
    const width = Number(source?.widthMm) || Number(slot.width) || 0;
    const height = Number(source?.heightMm) || Number(slot.height) || 0;
    if (slot.rotation === 90) return { x:slot.x + height - y, y:slot.y + x };
    if (slot.rotation === 180) return { x:slot.x + width - x, y:slot.y + height - y };
    if (slot.rotation === 270) return { x:slot.x + y, y:slot.y + width - x };
    return { x:slot.x + x, y:slot.y + y };
  }

  function renderSvg(layout, options = {}) {
    if (!layout?.fits || !Array.isArray(layout.placements)) throw new TypeError("请先生成有效排版方案");
    const width = Number(layout.input?.boardWidthMm);
    const height = Number(layout.input?.boardHeightMm);
    if (!(width > 0 && height > 0)) throw new TypeError("排版缺少刀板尺寸");
    const title = escapeXml(options.title || layout.input.projectName || "刀板排版");
    const showNumbers = options.showNumbers !== false;
    const stroke = Math.max(0.05, Math.min(2, Number(options.strokeWidthMm) || 0.25));
    const effective = layout.effectiveBoard;
    const dieSource = options.dieSource && typeof options.dieSource === "object" ? options.dieSource : null;
    const exactSvgSource = dieSource?.kind === "svg" && dieSource.markup && Array.isArray(dieSource.viewBox);
    const exactContourSource = Boolean(dieSource?.exactContour && Array.isArray(dieSource.outline) && dieSource.outline.length >= 4);
    const exactSource = exactSvgSource || exactContourSource;
    const slots = layout.placements.map(slot => {
      const label = showNumbers && slot.width >= 12 && slot.height >= 8
        ? `<text x="${slot.x + slot.width / 2}" y="${slot.y + slot.height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="${Math.max(2.5, Math.min(8, Math.min(slot.width, slot.height) * .12))}" fill="#7f1d1d">${slot.index}</text>`
        : "";
      let artwork = "";
      if (exactSvgSource) {
        const viewBox = dieSource.viewBox.join(" ");
        artwork = slot.rotation === 90
          ? `<g transform="translate(${slot.x + slot.width} ${slot.y}) rotate(90)"><svg x="0" y="0" width="${slot.height}" height="${slot.width}" viewBox="${viewBox}" preserveAspectRatio="none"><g>${dieSource.markup}</g></svg></g>`
          : `<svg x="${slot.x}" y="${slot.y}" width="${slot.width}" height="${slot.height}" viewBox="${viewBox}" preserveAspectRatio="none"><g>${dieSource.markup}</g></svg>`;
      }
      if (exactContourSource && Array.isArray(dieSource.segments) && dieSource.segments.length) {
        artwork = dieSource.segments.map(segment => {
          const start = transformSourcePoint(segment?.[0], slot, dieSource);
          const end = transformSourcePoint(segment?.[1], slot, dieSource);
          return `<line x1="${start.x}" y1="${start.y}" x2="${end.x}" y2="${end.y}" fill="none" stroke="#65a30d" stroke-width="${Math.max(.08, stroke * .55)}"/>`;
        }).join("");
      }
      const guide = exactContourSource
        ? `<polygon points="${(slot.points || dieSource.outline.map(point => transformSourcePoint(point, slot, dieSource))).map(point => `${point.x},${point.y}`).join(" ")}" fill="none" stroke="#2563eb" stroke-width="${Math.max(.06, stroke * .45)}" stroke-dasharray="2 1" opacity=".6"/>`
        : `<rect x="${slot.x}" y="${slot.y}" width="${slot.width}" height="${slot.height}" fill="none" stroke="${exactSource ? "#7da6c9" : "#d92d20"}" stroke-width="${exactSource ? Math.max(.06, stroke * .45) : stroke}" ${exactSource ? 'stroke-dasharray="2 1" opacity=".55"' : ""}/>`;
      return `<g data-slot="${slot.index}" data-row="${slot.row}" data-column="${slot.column}" data-rotation="${slot.rotation}">${artwork}${guide}${label}</g>`;
    }).join("");
    return `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${width}mm" height="${height}mm" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="die-title die-desc"><title id="die-title">${title}</title><desc id="die-desc">${exactSource ? "客户真实刀线" : "刀板外接框"}智能排版，共${layout.capacityPerSheet}个刀位</desc><rect width="${width}" height="${height}" fill="#ffffff" stroke="#263442" stroke-width="${Math.max(.1, stroke)}"/><rect x="${effective.x}" y="${effective.y}" width="${effective.width}" height="${effective.height}" fill="#f6f9fc" stroke="#4f718f" stroke-width="${Math.max(.1, stroke * .7)}" stroke-dasharray="3 2"/>${slots}</svg>`;
  }

  const rectanglePoints = placement => [
    { x:placement.x, y:placement.y },
    { x:placement.x + placement.width, y:placement.y },
    { x:placement.x + placement.width, y:placement.y + placement.height },
    { x:placement.x, y:placement.y + placement.height }
  ];
  const closedStroke = points => ({ points:[...points, points[0]] });

  function toVectorDocument(layout, options = {}) {
    if (!layout?.fits || !Array.isArray(layout.placements)) throw new TypeError("请先生成有效排版方案");
    const boardWidth = layout.input.boardWidthMm;
    const boardHeight = layout.input.boardHeightMm;
    const boardPoints = rectanglePoints({ x:0, y:0, width:boardWidth, height:boardHeight });
    const effectivePoints = rectanglePoints(layout.effectiveBoard);
    const dieSource = options.dieSource && typeof options.dieSource === "object" ? options.dieSource : null;
    const exactContour = Boolean(dieSource?.exactContour && Array.isArray(dieSource.outline) && dieSource.outline.length >= 4);
    const dielines = exactContour
      ? layout.placements.map(slot => dieSource.outline.map(point => transformSourcePoint(point, slot, dieSource)))
      : layout.placements.map(rectanglePoints);
    const internalStrokes = exactContour && Array.isArray(dieSource.segments)
      ? layout.placements.flatMap(slot => dieSource.segments.map(segment => ({
        points:segment.map(point => transformSourcePoint(point, slot, dieSource))
      })))
      : [];
    const dxfContours = options.forDxf === true;
    return {
      width:boardWidth,
      height:boardHeight,
      options:{ smoothing:0 },
      layers:[
        { id:"board-guide", name:"BOARD_GUIDE", role:"guide", color:"#7f8c99", cmyk:[0, 0, 0, .45], contours:dxfContours ? [{ points:boardPoints }] : [], strokes:dxfContours ? [] : [closedStroke(boardPoints)], strokeWidthMm:.15 },
        { id:"usable-area", name:"USABLE_AREA", role:"guide", color:"#4f718f", cmyk:[.45, .15, 0, .4], contours:dxfContours ? [{ points:effectivePoints }] : [], strokes:dxfContours ? [] : [closedStroke(effectivePoints)], strokeWidthMm:.15 },
        {
          id:"dieline", name:"DIELINE", role:"dieline", color:"#fff200", cmyk:[0, 0, 1, 0], spotName:"DIELINE",
          contours:dxfContours && !internalStrokes.length ? dielines.map(points => ({ points })) : [],
          strokes:internalStrokes.length ? internalStrokes : (dxfContours ? [] : dielines.map(closedStroke)),
          strokeWidthMm:.25
        }
      ]
    };
  }

  function layoutToCsv(layout) {
    if (!layout?.fits || !Array.isArray(layout.placements)) throw new TypeError("请先生成有效排版方案");
    const rows = [
      ["项目", layout.input.projectName],
      ["刀板尺寸(mm)", `${layout.input.boardWidthMm}×${layout.input.boardHeightMm}`],
      ["有效幅面(mm)", `${layout.effectiveBoard.width}×${layout.effectiveBoard.height}`],
      ["排版", `${layout.columns}列×${layout.rows}行`],
      ["单板数量", layout.capacityPerSheet],
      ["订单数量", layout.input.quantity],
      ["所需生产张数", layout.sheetsRequired],
      ["超产数量", layout.overrun],
      [],
      ["刀位", "行", "列", "X(mm)", "Y(mm)", "宽(mm)", "高(mm)", "旋转"]
    ];
    layout.placements.forEach(slot => rows.push([slot.index, slot.row, slot.column, slot.x, slot.y, slot.width, slot.height, `${slot.rotation}°`]));
    const spreadsheetSafe = value => {
      const source = String(value ?? "");
      return /^[\u0000-\u0020]*[=+\-@]/.test(source) ? `'${source}` : source;
    };
    const encode = value => `"${spreadsheetSafe(value).replace(/"/g, '""')}"`;
    return `\uFEFF${rows.map(row => row.map(encode).join(",")).join("\r\n")}`;
  }

  function safeFilename(value) {
    return String(value || "刀板排版").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").replace(/^\.+|\.+$/g, "").trim().slice(0, 80) || "刀板排版";
  }

  const boxTemplate = definition => {
    const template = {
      family:"folding-carton",
      topStyle:"tuck",
      productionTopVariant:"tuck",
      bottomStyle:"tuck",
      closureSide:"same",
      reverse:false,
      lockSlot:false,
      /*
       * Only structures whose 2D cut/fold topology has been checked against a
       * recognised ECMA/FEFCO construction or a print-ready dieline reference
       * are shown for new projects.  Older definitions remain addressable by
       * id so saved jobs do not lose data.
       */
      production2DVerified:false,
      dimensionRule:"length-gte-width",
      preset:[120, 60, 180, "300g白卡"],
      ...definition
    };
    if (
      template.category === "卡盒"
      && String(template.production2DStandard || "").startsWith("shengda-")
    ) {
      template.referenceProvider = "标准结构库";
      template.referenceTemplate = template.name;
      template.dimensionBasis = "manufacturing";
    }
    return Object.freeze(template);
  };

  /*
   * The reference engine exposes eight snap-bottom constructions: ordinary/reverse bottom,
   * same/opposite top direction, with/without tuck shoulder slits.
   */
  const LOCK_BOTTOM_VARIANTS = Object.freeze([
    { suffix:"standard-opposite", name:"普通扣底盒(异侧)", reverse:false, closureSide:"opposite", lockSlot:false, shengdaType:"ButtonBottomBox", wideShengdaType:"LargeWidthButtonBottomBox" },
    { suffix:"standard-same", name:"普通扣底盒(同侧)", reverse:false, closureSide:"same", lockSlot:false, shengdaType:"SameButtonBottomBox", wideShengdaType:"LargeWidthSameButtonBottomBox" },
    { suffix:"reverse-opposite", name:"反向扣底盒(异侧)", reverse:true, closureSide:"opposite", lockSlot:false, shengdaType:"ReverseButtonBottomBox", wideShengdaType:"LargeWidthReverseButtonBottomBox" },
    { suffix:"reverse-same", name:"反向扣底盒(同侧)", reverse:true, closureSide:"same", lockSlot:false, shengdaType:"ReverseSameButtonBottomBox", wideShengdaType:"LargeWidthReverseSameButtonBottomBox" },
    { suffix:"standard-opposite-slot", name:"普通扣底盒(异侧) + 插扣", reverse:false, closureSide:"opposite", lockSlot:true, shengdaType:"BuckleButtonBottomBox", wideShengdaType:"BuckleLargeWidthButtonBottomBox" },
    { suffix:"standard-same-slot", name:"普通扣底盒(同侧) + 插扣", reverse:false, closureSide:"same", lockSlot:true, shengdaType:"BuckleSameButtonBottomBox", wideShengdaType:"BuckleLargeWidthSameButtonBottomBox" },
    { suffix:"reverse-opposite-slot", name:"反向扣底盒(异侧) + 插扣", reverse:true, closureSide:"opposite", lockSlot:true, shengdaType:"BuckleReverseButtonBottomBox", wideShengdaType:"BuckleLargeWidthReverseButtonBottomBox" },
    { suffix:"reverse-same-slot", name:"反向扣底盒(同侧) + 插扣", reverse:true, closureSide:"same", lockSlot:true, shengdaType:"BuckleReverseSameButtonBottomBox", wideShengdaType:"BuckleLargeWidthReverseSameButtonBottomBox" }
  ]);

  const makeLockBottomDefinitions = (wide = false) => LOCK_BOTTOM_VARIANTS.map((variant, index) => {
    const { suffix, name, reverse, closureSide, lockSlot, shengdaType, wideShengdaType } = variant;
    const prefix = wide ? "wide-lock" : "lock";
    const code = wide ? 28 + index : 5 + index;
    const wideName = name
      .replace("盒(", `盒(宽大于长、`)
      .replace(") +", ") +");
    return boxTemplate({
      id:`${prefix}-${suffix}`,
      code:`JL-C${String(code).padStart(2, "0")}`,
      category:"卡盒",
      subgroup:wide ? "扣底盒（宽大于长）" : "扣底盒",
      name:wide ? wideName : name,
      description:`标准四片扣底结构；普通/反向底翼、顶盖同异侧方向${lockSlot ? "及插扣刀口" : ""}按参数真实联动`,
      production2DVerified:true,
      production2DStandard:wide ? "shengda-snap-lock-wide-parametric" : "shengda-snap-lock-parametric",
      bottomStyle:"lock",
      topStyle:"tuck",
      productionTopVariant:"tuck",
      reverse,
      closureSide,
      lockSlot,
      shengdaType:wide ? wideShengdaType : shengdaType,
      presetGlueFlapMm:15,
      referenceThicknessMm:.5,
      dimensionRule:wide ? "width-gt-length" : "length-gte-width",
      preset:wide ? [70, 120, 180, "350g白卡"] : [120, 60, 180, "350g白卡"]
    });
  });

  const BOX_TEMPLATE_DEFINITIONS = Object.freeze([
    boxTemplate({ id:"straight-tuck", code:"JL-C01", category:"卡盒", subgroup:"双插盒", name:"普通双插盒(异侧)", description:"上下插舌位于相反主面；含插舌圆角、左右尘翼、糊口和完整压线", production2DVerified:true, production2DStandard:"shengda-double-tuck-opposite", shengdaType:"ReversePluginLockBox", closureSide:"opposite", preset:[120,60,160,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"straight-tuck-same", code:"JL-C02", category:"卡盒", subgroup:"双插盒", name:"普通双插盒(同侧)", description:"上下插舌位于同一主面；含插舌圆角、左右尘翼、糊口和完整压线", production2DVerified:true, production2DStandard:"shengda-double-tuck-same", shengdaType:"SameReversePluginLockBox", closureSide:"same", preset:[120,60,160,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"reverse-tuck", code:"JL-C03", category:"卡盒", subgroup:"双插盒", name:"普通双插盒(异侧) + 插扣", description:"异侧双插盒增加上下插扣刀口", production2DVerified:true, production2DStandard:"shengda-double-tuck-opposite-slot", shengdaType:"BuckleReversePluginLockBox", closureSide:"opposite", lockSlot:true, preset:[120,60,160,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"reverse-tuck-same", code:"JL-C04", category:"卡盒", subgroup:"双插盒", name:"普通双插盒(同侧) + 插扣", description:"同侧双插盒增加上下插扣刀口", production2DVerified:true, production2DStandard:"shengda-double-tuck-same-slot", shengdaType:"BuckleSameReversePluginLockBox", closureSide:"same", lockSlot:true, preset:[120,60,160,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    ...makeLockBottomDefinitions(false),
    ...makeLockBottomDefinitions(true),
    boxTemplate({ id:"hanger", code:"JL-C36", category:"卡盒", subgroup:"挂耳盒", name:"挂耳双插盒", description:"一体式挂耳、欧式挂孔与双插封口", production2DVerified:true, production2DStandard:"shengda-hanger-double-tuck", shengdaType:"EarHangingPluginLockBox", topStyle:"hanger", productionTopVariant:"hanger", hangerHoleStyle:"euro", preset:[100,45,150,"300g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"hanger-reverse", code:"JL-C37", category:"卡盒", subgroup:"挂耳盒", name:"挂耳双插盒 + 插扣", description:"挂耳双插盒增加上下插扣刀口", production2DVerified:true, production2DStandard:"shengda-hanger-double-tuck-slot", shengdaType:"BuckleEarHangingPluginLockBox", topStyle:"hanger", productionTopVariant:"hanger", hangerHoleStyle:"euro", closureSide:"opposite", lockSlot:true, preset:[100,45,150,"300g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"hanger-lock", code:"JL-C38", category:"卡盒", subgroup:"挂耳盒", name:"挂耳扣底盒", description:"一体式欧式挂耳与四片扣底组合", production2DVerified:true, production2DStandard:"shengda-hanger-snap-lock", shengdaType:"EarHangingButtonBottomBox", topStyle:"hanger", productionTopVariant:"hanger", hangerHoleStyle:"euro", bottomStyle:"lock", preset:[100,45,150,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({
      id:"auto-lock",
      code:"JL-C39",
      category:"卡盒",
      subgroup:"自动底盒",
      name:"普通自锁底盒(同侧)",
      description:"四片预糊自锁底翼；顶盖与自锁主翼同侧",
      production2DVerified:true,
      bottomStyle:"auto",
      closureSide:"same",
      production2DStandard:"shengda-auto-bottom-same",
      shengdaType:"SameSelfLockingBox",
      preset:[120,60,180,"350g白卡"],
      presetGlueFlapMm:15,
      referenceThicknessMm:.5
    }),
    boxTemplate({ id:"auto-lock-same", code:"JL-C40", category:"卡盒", subgroup:"自动底盒", name:"普通自锁底盒(异侧)", description:"四片预糊自锁底翼；顶盖与自锁主翼异侧", production2DVerified:true, production2DStandard:"shengda-auto-bottom-opposite", shengdaType:"SelfLockingBox", bottomStyle:"auto", closureSide:"opposite", preset:[120,60,180,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"auto-lock-reverse", code:"JL-C41", category:"卡盒", subgroup:"自动底盒", name:"普通自锁底盒(异侧) + 插扣", description:"异侧自锁底盒增加顶盖插扣刀口", production2DVerified:true, production2DStandard:"shengda-auto-bottom-opposite-slot", shengdaType:"BuckleSelfLockingBox", bottomStyle:"auto", lockSlot:true, closureSide:"opposite", preset:[120,60,180,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"auto-lock-slot", code:"JL-C42", category:"卡盒", subgroup:"自动底盒", name:"普通自锁底盒(同侧) + 插扣", description:"同侧自锁底盒增加顶盖插扣刀口", production2DVerified:true, production2DStandard:"shengda-auto-bottom-same-slot", shengdaType:"BuckleSameSelfLockingBox", bottomStyle:"auto", lockSlot:true, closureSide:"same", preset:[120,60,180,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"sleeve", code:"JL-C43", category:"卡盒", subgroup:"其他", name:"腰封", description:"四面围合平粘结构，无上下封口", family:"sleeve", production2DVerified:true, production2DStandard:"shengda-belly-band", shengdaType:"Envelope", dimensionRule:"any", preset:[160,110,35,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"flat-envelope", code:"JL-C44", category:"卡盒", subgroup:"平粘盒", name:"平粘盒", description:"四边折翼平粘结构，适合卡片和薄件", family:"envelope", production2DVerified:true, production2DStandard:"shengda-flat-glue-box", shengdaType:"FlatAdhesiveBox", dimensionRule:"any", preset:[180,110,18,"300g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"pillow", code:"JL-C45", category:"卡盒", subgroup:"平粘盒", name:"平粘盒(带易撕线)", description:"平粘结构增加贯穿式易撕虚线", family:"envelope", tearLine:true, production2DVerified:true, production2DStandard:"shengda-flat-glue-box-tear", shengdaType:"WithTearLineFlatAdhesiveBox", dimensionRule:"any", preset:[180,110,18,"300g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"gable", code:"JL-C46", category:"卡盒", subgroup:"特殊盒型", name:"手提盒", description:"顶部对称屋脊、锁合提手孔与底部封口", family:"gable", production2DVerified:true, production2DStandard:"shengda-carry-box", shengdaType:"HandheldBox", dimensionRule:"any", preset:[120,70,160,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"display", code:"JL-C47", category:"卡盒", subgroup:"特殊盒型", name:"清洁盒", description:"双插盒体增加两道清洁取用易撕线", production2DVerified:true, production2DStandard:"shengda-cleaning-box", shengdaType:"CleaningBox", family:"folding-carton", closureSide:"opposite", cleaningTear:true, preset:[120,60,180,"300g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"hexagon", code:"JL-C48", category:"卡盒", subgroup:"特殊盒型", name:"飞机盒", description:"一片成型翻盖、卷边侧墙与前墙锁舌结构", family:"mailer", cardMailer:true, production2DVerified:true, production2DStandard:"shengda-card-mailer", shengdaType:"AircraftBox", dimensionRule:"any", preset:[180,120,45,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"other-open-tray", code:"JL-C49", category:"卡盒", subgroup:"特殊盒型", name:"天地盖盒（上盖、盒底）", description:"独立上盖与盒底两片式十字结构", family:"lid-base", cardLidBase:true, production2DVerified:true, production2DStandard:"shengda-card-lid-base", shengdaType:"SkyAndEarthBox", dimensionRule:"any", preset:[180,120,35,"350g白卡"], presetGlueFlapMm:15, referenceThicknessMm:.5 }),
    boxTemplate({ id:"mailer", code:"JL-W01", category:"瓦楞盒", subgroup:"邮寄盒", name:"飞机盒（FEFCO 0427）", description:"经 FEFCO 0427 对照的一片成型翻盖、卷边侧墙与前墙锁舌结构", family:"mailer", production2DVerified:true, production2DStandard:"fefco-0427", dimensionRule:"any", preset:[220,160,55,"E瓦楞"] }),
    boxTemplate({ id:"mailer-lock", code:"JL-W02", category:"瓦楞盒", subgroup:"邮寄盒", name:"双耳锁口飞机盒（0427 变体）", description:"FEFCO 0427 基础结构增加前墙双耳承口，适合反复开合", production2DVerified:true, production2DStandard:"fefco-0427-double-lock", family:"mailer", lockSlot:true, dimensionRule:"any", preset:[220,160,55,"E瓦楞"] }),
    boxTemplate({ id:"corrugated-lid-base", code:"JL-W03", category:"瓦楞盒", subgroup:"天地盒", name:"瓦楞天地盖", description:"独立上盖与盒底两片式十字结构，并加入纸厚和装配放量", production2DVerified:true, production2DStandard:"fefco-lid-base", family:"lid-base", dimensionRule:"any", preset:[240,180,55,"E瓦楞"] }),
    boxTemplate({ id:"corrugated-sleeve", code:"JL-W04", category:"瓦楞盒", subgroup:"套筒盒", name:"瓦楞抽屉套筒", description:"经核对的瓦楞四面套筒，适合抽屉盒外套", family:"sleeve", production2DVerified:true, production2DStandard:"fefco-sleeve", dimensionRule:"any", preset:[220,160,55,"E瓦楞"] }),
    boxTemplate({ id:"lid-base", code:"JL-M01", category:"白卡对裱盒", subgroup:"天地盖", name:"天地盖盒", description:"上盖与盒底两套十字灰板/卡纸刀线，盖底按装配放量联动", family:"lid-base", production2DVerified:true, production2DStandard:"rigid-lid-base", dimensionRule:"any", preset:[110,110,25,"1.5mm灰板"] }),
    boxTemplate({ id:"mounted-tray", code:"JL-M02", category:"白卡对裱盒", subgroup:"托盘", name:"对裱四角托盘", description:"一片式底板、四周立墙和适合厚卡对裱的四角糊耳", family:"tray", production2DVerified:true, production2DStandard:"rigid-four-corner-tray", dimensionRule:"any", preset:[180,120,35,"1.5mm灰板"] }),
    boxTemplate({ id:"paper-bag", code:"JL-B01", category:"手提袋", subgroup:"纸袋", name:"模切提手白卡袋", description:"含侧风琴、袋底折线、顶部反折和两面模切提手孔", family:"paper-bag", production2DVerified:true, production2DStandard:"paper-bag-die-cut-handle", dimensionRule:"any", preset:[220,100,280,"250g白卡"] }),
    boxTemplate({ id:"kraft-bag", code:"JL-B02", category:"手提袋", subgroup:"纸袋", name:"穿绳牛皮纸手提袋", description:"含侧风琴、袋底折线、顶部反折和两面穿绳孔", family:"paper-bag", production2DVerified:true, production2DStandard:"paper-bag-rope-handle", dimensionRule:"any", preset:[220,100,280,"250g白卡"], handleStyle:"rope" })
  ]);

  /*
   * Keep the full historical catalogue discoverable.  Verification is a
   * production-safety state, not a visibility switch: hiding every structure
   * that still needs calibration made the catalogue look as if customer data
   * had been deleted.  Unverified structures remain previewable, but are
   * clearly marked and cannot be exported as production files.
   */
  const public2DTemplates = BOX_TEMPLATE_DEFINITIONS;
  const BOX_TEMPLATE_CATALOG = Object.freeze(
    ["卡盒", "瓦楞盒", "白卡对裱盒", "手提袋"]
      .map(category => ({
        category,
        groups:[...new Set(public2DTemplates.filter(item => item.category === category).map(item => item.subgroup))].map(subgroup => Object.freeze({
          subgroup,
          templates:Object.freeze(public2DTemplates.filter(item => item.category === category && item.subgroup === subgroup))
        }))
      }))
      .filter(item => item.groups.length)
      .map(item => Object.freeze({ category:item.category, groups:Object.freeze(item.groups) }))
  );

  const BOX_MATERIAL_THICKNESS = Object.freeze({
    "250g白卡":0.32,
    "300g白卡":0.38,
    "350g白卡":0.46,
    "400g白卡":0.55,
    "E瓦楞":1.6,
    "B瓦楞":3,
    "1.5mm灰板":1.5,
    "2mm灰板":2
  });

  const boxPoint = (x, y) => ({ x:Number(x), y:Number(y) });
  const boxLine = (x1, y1, x2, y2) => [boxPoint(x1, y1), boxPoint(x2, y2)];
  const closeBoxPath = points => {
    /*
     * Adjacent production flap helpers deliberately include both hinge
     * endpoints.  When several helpers are concatenated those endpoints
     * repeat; leaving them in the exported polyline creates zero-length knife
     * segments and makes CAD preflight report false self intersections.
     * Canonicalise every closed contour here so all box families benefit.
     */
    const output = points
      .map(point => boxPoint(point.x, point.y))
      .filter((point, index, source) => index === 0 || Math.hypot(
        point.x - source[index - 1].x,
        point.y - source[index - 1].y
      ) > 1e-7);
    const first = output[0];
    const last = output[output.length - 1];
    if (first && last && (first.x !== last.x || first.y !== last.y)) output.push(boxPoint(first.x, first.y));
    return output;
  };

  const TOPOLOGY_EPSILON = 1e-4;
  const sameBoxPoint = (first, second, epsilon = TOPOLOGY_EPSILON) =>
    Math.hypot(Number(first?.x) - Number(second?.x), Number(first?.y) - Number(second?.y)) <= epsilon;

  function openClosedBoxPath(path) {
    if (!Array.isArray(path) || path.length < 4 || !sameBoxPoint(path[0], path[path.length - 1])) return [];
    const points = path.slice(0, -1).map(point => boxPoint(point.x, point.y));
    return points.filter((point, index) => index === 0 || !sameBoxPoint(point, points[index - 1]));
  }

  function boxPolygonArea(points) {
    return points.reduce((sum, point, index) => {
      const next = points[(index + 1) % points.length];
      return sum + point.x * next.y - next.x * point.y;
    }, 0) / 2;
  }

  function boxPointInPolygon(point, polygon) {
    let inside = false;
    for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index, index += 1) {
      const a = polygon[index];
      const b = polygon[previous];
      const crosses = ((a.y > point.y) !== (b.y > point.y))
        && point.x < (b.x - a.x) * (point.y - a.y) / ((b.y - a.y) || Number.EPSILON) + a.x;
      if (crosses) inside = !inside;
    }
    return inside;
  }

  function boxPointOnSegment(point, start, end, epsilon = TOPOLOGY_EPSILON) {
    const length = Math.hypot(end.x - start.x, end.y - start.y);
    if (length <= epsilon) return false;
    const cross = Math.abs((point.x - start.x) * (end.y - start.y) - (point.y - start.y) * (end.x - start.x));
    if (cross > epsilon * Math.max(1, length)) return false;
    const dot = (point.x - start.x) * (end.x - start.x) + (point.y - start.y) * (end.y - start.y);
    return dot >= -epsilon && dot <= length * length + epsilon;
  }

  function insertBoxBoundaryPoint(polygon, point) {
    const existing = polygon.findIndex(candidate => sameBoxPoint(candidate, point));
    if (existing >= 0) return { polygon:polygon.slice(), index:existing };
    for (let index = 0; index < polygon.length; index += 1) {
      const nextIndex = (index + 1) % polygon.length;
      if (!boxPointOnSegment(point, polygon[index], polygon[nextIndex])) continue;
      const output = polygon.slice();
      output.splice(index + 1, 0, boxPoint(point.x, point.y));
      return { polygon:output, index:index + 1 };
    }
    return null;
  }

  function splitBoxPolygon(polygon, start, end) {
    if (sameBoxPoint(start, end)) return null;
    const firstInsert = insertBoxBoundaryPoint(polygon, start);
    if (!firstInsert) return null;
    const secondInsert = insertBoxBoundaryPoint(firstInsert.polygon, end);
    if (!secondInsert) return null;
    const points = secondInsert.polygon;
    const firstIndex = points.findIndex(point => sameBoxPoint(point, start));
    const secondIndex = points.findIndex(point => sameBoxPoint(point, end));
    if (firstIndex < 0 || secondIndex < 0 || firstIndex === secondIndex) return null;
    const forward = [];
    for (let index = firstIndex; ; index = (index + 1) % points.length) {
      forward.push(points[index]);
      if (index === secondIndex) break;
    }
    const backward = [];
    for (let index = secondIndex; ; index = (index + 1) % points.length) {
      backward.push(points[index]);
      if (index === firstIndex) break;
    }
    if (forward.length < 3 || backward.length < 3) return null;
    if (Math.abs(boxPolygonArea(forward)) <= TOPOLOGY_EPSILON || Math.abs(boxPolygonArea(backward)) <= TOPOLOGY_EPSILON) return null;
    return [forward, backward];
  }

  function extendBoxFoldAcrossPolygon(polygon, start, end) {
    const dx = end.x - start.x;
    const dy = end.y - start.y;
    const denominatorFloor = TOPOLOGY_EPSILON * TOPOLOGY_EPSILON;
    const intersections = [];
    for (let index = 0; index < polygon.length; index += 1) {
      const edgeStart = polygon[index];
      const edgeEnd = polygon[(index + 1) % polygon.length];
      const ex = edgeEnd.x - edgeStart.x;
      const ey = edgeEnd.y - edgeStart.y;
      const denominator = dx * ey - dy * ex;
      if (Math.abs(denominator) <= denominatorFloor) continue;
      const offsetX = edgeStart.x - start.x;
      const offsetY = edgeStart.y - start.y;
      const lineAmount = (offsetX * ey - offsetY * ex) / denominator;
      const edgeAmount = (offsetX * dy - offsetY * dx) / denominator;
      if (edgeAmount < -TOPOLOGY_EPSILON || edgeAmount > 1 + TOPOLOGY_EPSILON) continue;
      const point = boxPoint(start.x + lineAmount * dx, start.y + lineAmount * dy);
      if (!intersections.some(item => sameBoxPoint(item.point, point))) intersections.push({ point, lineAmount });
    }
    if (intersections.length < 2) return null;
    intersections.sort((first, second) => first.lineAmount - second.lineAmount);
    const minimumAmount = intersections[0].lineAmount;
    const maximumAmount = intersections[intersections.length - 1].lineAmount;
    if (maximumAmount < -TOPOLOGY_EPSILON || minimumAmount > 1 + TOPOLOGY_EPSILON) return null;
    const before = intersections.filter(item => item.lineAmount <= TOPOLOGY_EPSILON).at(-1);
    const after = intersections.find(item => item.lineAmount >= 1 - TOPOLOGY_EPSILON);
    const first = before || intersections[0];
    const second = after || intersections[intersections.length - 1];
    if (!first || !second || sameBoxPoint(first.point, second.point)) return null;
    return splitBoxPolygon(polygon, first.point, second.point);
  }

  function buildBoxNetTopology(cutPaths, foldPaths) {
    const closedCuts = (cutPaths || [])
      .map(openClosedBoxPath)
      .filter(points => points.length >= 3 && Math.abs(boxPolygonArea(points)) > TOPOLOGY_EPSILON)
      .sort((first, second) => Math.abs(boxPolygonArea(second)) - Math.abs(boxPolygonArea(first)));
    const outerPolygons = closedCuts.filter((polygon, index) => {
      const sample = polygon[0];
      return !closedCuts.slice(0, index).some(container => boxPointInPolygon(sample, container));
    });
    const foldSegments = (foldPaths || []).flatMap(path => (path || []).slice(1).map((point, index) => ({
      start:boxPoint(path[index].x, path[index].y),
      end:boxPoint(point.x, point.y)
    }))).filter(segment => !sameBoxPoint(segment.start, segment.end));
    let faces = outerPolygons.map(polygon => polygon.slice());
    const maximumPasses = Math.max(2, foldSegments.length + 1);
    for (let pass = 0; pass < maximumPasses; pass += 1) {
      let changed = false;
      foldSegments.forEach(segment => {
        const nextFaces = [];
        faces.forEach(face => {
          const split = splitBoxPolygon(face, segment.start, segment.end);
          if (split) {
            nextFaces.push(...split);
            changed = true;
          } else {
            nextFaces.push(face);
          }
        });
        faces = nextFaces;
      });
      if (!changed) break;
    }
    foldSegments.forEach(segment => {
      const nextFaces = [];
      faces.forEach(face => {
        const split = extendBoxFoldAcrossPolygon(face, segment.start, segment.end);
        if (split) nextFaces.push(...split);
        else nextFaces.push(face);
      });
      faces = nextFaces;
    });
    const serialize = polygon => polygon.map(point => ({ x:Number(point.x.toFixed(4)), y:Number(point.y.toFixed(4)) }));
    return {
      version:1,
      source:"cut-and-fold-paths",
      outerPolygons:outerPolygons.map(serialize),
      facePolygons:faces.map(serialize),
      foldSegments:foldSegments.map(segment => ({ start:serialize([segment.start])[0], end:serialize([segment.end])[0] }))
    };
  }

  function boxDimension(value, label, options = {}) {
    if (value === null || value === "" || typeof value === "boolean") throw new TypeError(`${label}必须填写数字`);
    const number = Number(value);
    if (!Number.isFinite(number)) throw new TypeError(`${label}必须是有限数字`);
    const minimum = options.minimum ?? .1;
    const maximum = options.maximum ?? 2000;
    if (number < minimum || number > maximum) throw new RangeError(`${label}必须在${minimum}–${maximum}mm之间`);
    return Number(number.toFixed(3));
  }

  function normalizeBoxTemplateOptions(input = {}) {
    const legacyTypes = {
      "snap-lock":"lock-standard-opposite",
      "straight-tuck-slot":"reverse-tuck-same",
      "reverse-tuck-slot":"reverse-tuck"
    };
    const requestedType = String(input.type || "straight-tuck");
    const type = legacyTypes[requestedType] || requestedType;
    const definition = BOX_TEMPLATE_DEFINITIONS.find(item => item.id === type);
    if (!definition) throw new RangeError("请选择支持的盒型");
    const length = boxDimension(input.lengthMm, "长", { minimum:10 });
    const width = boxDimension(input.widthMm, "宽", { minimum:8 });
    const height = boxDimension(input.heightMm, "高", { minimum:8 });
    const thickness = boxDimension(input.thicknessMm ?? BOX_MATERIAL_THICKNESS[input.material] ?? .38, "纸张厚度", { minimum:.05, maximum:10 });
    const referenceGlueFlap = definition.dimensionBasis === "manufacturing"
      ? Number(definition.presetGlueFlapMm || 15)
      : null;
    const glueFlap = boxDimension(referenceGlueFlap ?? input.glueFlapMm ?? Math.max(12, Math.min(25, height * .18)), "糊口", { minimum:5, maximum:80 });
    const bleed = boxDimension(input.bleedMm ?? 3, "出血", { minimum:0, maximum:20 });
    const clearance = boxDimension(input.clearanceMm ?? Math.max(.5, thickness * 1.5), "装配放量", { minimum:0, maximum:15 });
    if (definition.dimensionRule === "length-gte-width" && length < width) {
      throw new RangeError("当前盒型要求长不小于宽；请交换长、宽或选择“宽大于长”结构");
    }
    if (definition.dimensionRule === "width-gt-length" && width <= length) {
      throw new RangeError("当前盒型专用于宽大于长；请调整尺寸或选择普通扣底盒");
    }
    return {
      type,
      definition,
      projectName:String(input.projectName || `${definition.name}刀板`).trim().slice(0, 80) || `${definition.name}刀板`,
      material:String(input.material || "300g白卡"),
      length,
      width,
      height,
      thickness,
      glueFlap,
      bleed,
      clearance
    };
  }

  function tuckFlapDimensions(width, depth) {
    const tongue = Math.min(20, Math.max(10, depth * .18));
    const closure = Math.max(12, depth - tongue);
    const inset = Math.min(width * .09, Math.max(5, tongue * .45));
    const radius = Math.min(6, Math.max(2.5, tongue * .32));
    return { tongue, closure, inset, radius };
  }

  function cartonFlapPoints(x0, x1, creaseY, depth, direction, style = "dust", mirror = false) {
    const width = x1 - x0;
    const outerY = creaseY + direction * depth;
    const shoulder = Math.min(width * .18, Math.max(2.2, depth * .18));
    const root = Math.min(2.5, width * .04);
    if (style === "none") return [boxPoint(x0, creaseY), boxPoint(x1, creaseY)];
    if (style === "tuck") {
      const { tongue, closure, inset, radius } = tuckFlapDimensions(width, depth);
      const tongueBaseY = creaseY + direction * closure;
      const noseY = creaseY + direction * (closure + tongue);
      const sideRelief = Math.min(5, Math.max(2, closure * .07));
      const left = x0 + inset;
      const right = x1 - inset;
      return [
        boxPoint(x0, creaseY),
        boxPoint(x0 + root, creaseY + direction * sideRelief),
        boxPoint(left, tongueBaseY),
        boxPoint(left, noseY - direction * radius),
        boxPoint(left + radius * .3, noseY - direction * radius * .45),
        boxPoint(left + radius, noseY),
        boxPoint(right - radius, noseY),
        boxPoint(right - radius * .3, noseY - direction * radius * .45),
        boxPoint(right, noseY - direction * radius),
        boxPoint(right, tongueBaseY),
        boxPoint(x1 - root, creaseY + direction * sideRelief),
        boxPoint(x1, creaseY)
      ];
    }
    if (style === "receiver") {
      const receiverDepth = depth * .48;
      const receiverY = creaseY + direction * receiverDepth;
      return [
        boxPoint(x0, creaseY), boxPoint(x0 + shoulder, receiverY),
        boxPoint(x1 - shoulder, receiverY), boxPoint(x1, creaseY)
      ];
    }
    if (style === "hanger") {
      const neck = Math.min(width * .2, Math.max(7, depth * .2));
      const ear = Math.min(width * .32, Math.max(13, depth * .34));
      const peakY = outerY + direction * Math.min(30, Math.max(20, depth * .5));
      const center = (x0 + x1) / 2;
      return [
        boxPoint(x0, creaseY), boxPoint(x0 + shoulder, outerY),
        boxPoint(center - ear, outerY), boxPoint(center - ear, peakY),
        boxPoint(center + ear, peakY), boxPoint(center + ear, outerY),
        boxPoint(x1 - shoulder, outerY), boxPoint(x1, creaseY)
      ];
    }
    if (style === "lock-major") {
      const lead = mirror ? x1 - width * .18 : x0 + width * .18;
      const heel = mirror ? x0 + width * .12 : x1 - width * .12;
      return [
        boxPoint(x0, creaseY),
        boxPoint(mirror ? x0 + root : lead, creaseY + direction * depth * .72),
        boxPoint(mirror ? lead : x0 + width * .32, outerY),
        boxPoint(mirror ? heel : x1 - width * .3, outerY),
        boxPoint(mirror ? x1 - width * .25 : heel, creaseY + direction * depth * .38),
        boxPoint(x1, creaseY)
      ];
    }
    if (style === "lock-side") {
      return [
        boxPoint(x0, creaseY), boxPoint(x0 + width * .13, outerY - direction * depth * .1),
        boxPoint(x0 + width * .38, outerY), boxPoint(x1 - width * .16, outerY),
        boxPoint(x1, creaseY + direction * depth * .28), boxPoint(x1, creaseY)
      ];
    }
    if (style === "auto-major") {
      const pivot = mirror ? x1 - width * .38 : x0 + width * .38;
      return [
        boxPoint(x0, creaseY), boxPoint(x0 + root, creaseY + direction * depth * .16),
        boxPoint(mirror ? x0 + width * .16 : pivot, outerY),
        boxPoint(mirror ? pivot : x1 - width * .16, outerY),
        boxPoint(x1 - root, creaseY + direction * depth * .16), boxPoint(x1, creaseY)
      ];
    }
    const nearShoulder = mirror ? shoulder * .58 : shoulder;
    const farShoulder = mirror ? shoulder : shoulder * .58;
    return [
      boxPoint(x0, creaseY), boxPoint(x0 + root, creaseY + direction * depth * .12),
      boxPoint(x0 + nearShoulder, outerY - direction * Math.min(3, depth * .1)),
      boxPoint(x0 + nearShoulder + Math.min(4, width * .08), outerY),
      boxPoint(x1 - farShoulder, outerY),
      boxPoint(x1 - root, creaseY + direction * depth * .12), boxPoint(x1, creaseY)
    ];
  }

  /*
   * Production 2D closure outlines.
   *
   * Do not reuse these points as 3D face profiles.  A folding-carton dieline
   * contains relief cuts, locking noses and receiver notches which are
   * mechanically important on the cutting table, but are not additional box
   * faces.  Keeping this layer separate also lets us correct the production
   * drawing without silently changing the established 3D preview.
   */
  function cartonProductionFlapPoints(x0, x1, creaseY, depth, direction, style, mirror = false) {
    const width = x1 - x0;
    const point = (ratio, outward) => boxPoint(x0 + width * ratio, creaseY + direction * depth * outward);
    const mirroredPoint = (ratio, outward) => point(mirror ? 1 - ratio : ratio, outward);
    const normalized = points => {
      const ordered = mirror ? [...points].reverse() : points;
      return ordered.map(([ratio, outward]) => mirroredPoint(ratio, outward));
    };

    if (style === "snap-base") {
      // ECMA-style 1-2-3 bottom, first/load-bearing flap.  Production samples
      // use straight shoulders and a rectangular receiver relief; the older
      // scalloped outline looked attractive on screen but was not a locking
      // construction a cutting-table operator could recognise.
      return normalized([
        [0, 0], [.04, .12], [.08, 1], [.29, 1],
        [.29, .54], [.71, .54], [.71, 1], [.92, 1],
        [.96, .12], [1, 0]
      ]);
    }
    if (style === "snap-side-a") {
      // Second flap: straight bridge with a single relief shoulder.
      return normalized([
        [0, 0], [.05, .12], [.08, .82], [.24, 1],
        [.72, 1], [.96, .58], [1, 0]
      ]);
    }
    if (style === "snap-side-b") {
      // Third flap: handed partner of the bridge.  It intentionally differs
      // from the second flap so the assembly order is visible in the net.
      return normalized([
        [0, 0], [.04, .58], [.28, 1], [.76, 1],
        [.92, .82], [.95, .12], [1, 0]
      ]);
    }
    if (style === "snap-final") {
      // Fourth/final locking flap.  The outer edge stays continuous; the
      // characteristic reverse-crease "V" is added separately below.
      return normalized([
        [0, 0], [.06, .12], [.24, .82], [.3, 1],
        [.7, 1], [.76, .82], [.94, .12], [1, 0]
      ]);
    }
    if (style === "snap-base-reinforced") {
      // A wide panel still uses the same receiver-lock principle.  The former
      // double-lobed approximation introduced a non-standard centre slot and
      // is intentionally replaced by the verified receiver relief.
      return cartonProductionFlapPoints(x0, x1, creaseY, depth, direction, "snap-base", mirror);
    }
    if (style === "auto-main-a") {
      // Crash/auto-lock bottoms are pre-glued diagonal flaps, not snap-lock
      // tongues.  Keep the outer edges straight and expose the glue fold below.
      return normalized([
        [0, 0], [.04, .12], [.3, 1], [.88, 1],
        [.96, .16], [1, 0]
      ]);
    }
    if (style === "auto-main-b") {
      return normalized([
        [0, 0], [.04, .16], [.12, 1], [.7, 1],
        [.96, .12], [1, 0]
      ]);
    }
    if (style === "auto-side-a") {
      return normalized([
        [0, 0], [.08, .16], [.3, .9], [.7, .9],
        [.88, .72], [.96, .14], [1, 0]
      ]);
    }
    if (style === "auto-side-b") {
      return normalized([
        [0, 0], [.05, .14], [.2, .72], [.32, .9],
        [.68, .9], [.84, .76], [.95, .16], [1, 0]
      ]);
    }
    if (style === "centered-tuck") {
      /*
       * Centred tuck: the closure panel is narrowed symmetrically before the
       * tongue starts.  It is a real alternative to a full-width tuck, not a
       * mirrored copy of the same outline.
       */
      return normalized([
        [0, 0], [.04, .08], [.18, .54], [.2, .72],
        [.22, .9], [.28, 1], [.72, 1], [.78, .9],
        [.8, .72], [.82, .54], [.96, .08], [1, 0]
      ]);
    }
    if (style === "full-flap") {
      /*
       * Full-overlap cover: broad straight cover with a shallow centred nose.
       * The almost full panel width is the production distinction from the
       * regular insertion tongue.
       */
      return normalized([
        [0, 0], [.025, .08], [.05, .86], [.1, .96],
        [.18, 1], [.82, 1], [.9, .96], [.95, .86],
        [.975, .08], [1, 0]
      ]);
    }
    if (style === "tab-lock") {
      /*
       * Locking cover: the centre tab enters a receiver slit on the opposite
       * body panel.  Shoulder reliefs prevent tearing at the tab root.
       */
      return normalized([
        [0, 0], [.035, .08], [.1, .74], [.22, .84],
        [.34, .84], [.34, .94], [.4, 1], [.6, 1],
        [.66, .94], [.66, .84], [.78, .84], [.9, .74],
        [.965, .08], [1, 0]
      ]);
    }
    return cartonFlapPoints(x0, x1, creaseY, depth, direction, style, mirror);
  }

  function productionBottomStyles(definition, panels, bottomPrimary) {
    if (definition.bottomStyle === "lock") {
      const styles = new Array(4);
      const base = bottomPrimary;
      const finalLock = (base + 2) % 4;
      const next = (base + 1) % 4;
      const previous = (base + 3) % 4;
      const narrowDepth = Math.min(panels[next], panels[previous]);
      styles[base] = "snap-base";
      styles[next] = "snap-side-a";
      styles[finalLock] = "snap-final";
      styles[previous] = "snap-side-b";
      return styles;
    }
    if (definition.bottomStyle === "auto") {
      const styles = new Array(4);
      const firstMain = bottomPrimary;
      styles[firstMain] = "auto-main-a";
      styles[(firstMain + 1) % 4] = "auto-side-a";
      styles[(firstMain + 2) % 4] = "auto-main-b";
      styles[(firstMain + 3) % 4] = "auto-side-b";
      return styles;
    }
    return null;
  }

  const SHENGDA_DOUBLE_TUCK_TYPES = Object.freeze({
    "shengda-double-tuck-opposite":"ReversePluginLockBox",
    "shengda-double-tuck-same":"SameReversePluginLockBox",
    "shengda-double-tuck-opposite-slot":"BuckleReversePluginLockBox",
    "shengda-double-tuck-same-slot":"BuckleSameReversePluginLockBox"
  });

  const nearlyEqual = (left, right, tolerance = .001) =>
    Math.abs(Number(left) - Number(right)) <= tolerance;

  function shengdaReferenceProfile(definition) {
    const library = root.JianglaiShengdaReferenceDielines;
    const reference = library?.types?.[definition?.shengdaType];
    const params = reference?.params || library?.params;
    if (!reference || !params) return null;
    return {
      reference,
      params,
      lengthMm:Number(params.boxLength),
      widthMm:Number(params.boxWidth),
      heightMm:Number(params.boxHeight),
      thicknessMm:Number(params.thickness),
      bleedMm:Number(params.bleedingSpace),
      glueFlapMm:Number(definition.presetGlueFlapMm || 15),
      sizeType:String(params.sizeType || "制造尺寸")
    };
  }

  function svgArcParameters(start, arc) {
    const end = boxPoint(arc.x, arc.y);
    let rx = Math.abs(Number(arc.rx));
    let ry = Math.abs(Number(arc.ry));
    if (!rx || !ry || (nearlyEqual(start.x, end.x) && nearlyEqual(start.y, end.y))) return null;
    const phi = Number(arc.rotation || 0) * Math.PI / 180;
    const cosine = Math.cos(phi);
    const sine = Math.sin(phi);
    const dx = (start.x - end.x) / 2;
    const dy = (start.y - end.y) / 2;
    const x1p = cosine * dx + sine * dy;
    const y1p = -sine * dx + cosine * dy;
    const radiusScale = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
    if (radiusScale > 1) {
      const multiplier = Math.sqrt(radiusScale);
      rx *= multiplier;
      ry *= multiplier;
    }
    const rx2 = rx * rx;
    const ry2 = ry * ry;
    const numerator = Math.max(0, rx2 * ry2 - rx2 * y1p * y1p - ry2 * x1p * x1p);
    const denominator = rx2 * y1p * y1p + ry2 * x1p * x1p;
    const sign = Boolean(arc.largeArc) === Boolean(arc.sweep) ? -1 : 1;
    const coefficient = denominator ? sign * Math.sqrt(numerator / denominator) : 0;
    const cxp = coefficient * rx * y1p / ry;
    const cyp = coefficient * -ry * x1p / rx;
    const cx = cosine * cxp - sine * cyp + (start.x + end.x) / 2;
    const cy = sine * cxp + cosine * cyp + (start.y + end.y) / 2;
    const vectorAngle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
    const ux = (x1p - cxp) / rx;
    const uy = (y1p - cyp) / ry;
    const vx = (-x1p - cxp) / rx;
    const vy = (-y1p - cyp) / ry;
    const startAngle = vectorAngle(1, 0, ux, uy);
    let deltaAngle = vectorAngle(ux, uy, vx, vy);
    if (!arc.sweep && deltaAngle > 0) deltaAngle -= Math.PI * 2;
    if (arc.sweep && deltaAngle < 0) deltaAngle += Math.PI * 2;
    return { end, rx, ry, phi, cosine, sine, cx, cy, startAngle, deltaAngle };
  }

  function svgArcPoint(parameters, theta) {
    return boxPoint(
      parameters.cosine * parameters.rx * Math.cos(theta) - parameters.sine * parameters.ry * Math.sin(theta) + parameters.cx,
      parameters.sine * parameters.rx * Math.cos(theta) + parameters.cosine * parameters.ry * Math.sin(theta) + parameters.cy
    );
  }

  function svgArcPoints(start, arc) {
    const parameters = svgArcParameters(start, arc);
    if (!parameters) return [boxPoint(arc.x, arc.y)];
    const { startAngle, deltaAngle } = parameters;
    const segmentCount = Math.max(1, Math.ceil(Math.abs(deltaAngle) / (Math.PI / 24)));
    return Array.from({ length:segmentCount }, (_unused, index) => {
      const theta = startAngle + deltaAngle * ((index + 1) / segmentCount);
      return svgArcPoint(parameters, theta);
    });
  }

  function svgPathCommands(pathData) {
    const tokens = String(pathData || "").match(/[MLAZmlaz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g) || [];
    const commands = [];
    let current = boxPoint(0, 0);
    let start = null;
    let index = 0;
    const number = () => {
      const value = Number(tokens[index++]);
      if (!Number.isFinite(value)) throw new TypeError("标准参考刀板包含无效坐标");
      return value;
    };
    while (index < tokens.length) {
      const command = tokens[index++];
      if (command === "M" || command === "m" || command === "L" || command === "l") {
        const x = number();
        const y = number();
        const end = command === command.toLowerCase()
          ? boxPoint(current.x + x, current.y + y)
          : boxPoint(x, y);
        const op = command.toUpperCase();
        commands.push({ op, start:boxPoint(current.x, current.y), end });
        current = end;
        if (op === "M") start = boxPoint(end.x, end.y);
      } else if (command === "A" || command === "a") {
        const arc = {
          rx:number(), ry:number(), rotation:number(),
          largeArc:Boolean(number()), sweep:Boolean(number()),
          x:number(), y:number()
        };
        if (command === "a") {
          arc.x += current.x;
          arc.y += current.y;
        }
        const end = boxPoint(arc.x, arc.y);
        commands.push({ op:"A", start:boxPoint(current.x, current.y), end, arc });
        current = end;
      } else if (command === "Z" || command === "z") {
        commands.push({ op:"Z", start:boxPoint(current.x, current.y), end:start ? boxPoint(start.x, start.y) : boxPoint(current.x, current.y) });
        current = start || current;
      } else {
        throw new TypeError(`标准参考刀板包含暂不支持的 SVG 命令：${command}`);
      }
    }
    return commands;
  }

  function normalizedAngle(value) {
    const circle = Math.PI * 2;
    return ((value % circle) + circle) % circle;
  }

  function angleFallsOnArc(angle, start, delta) {
    const epsilon = 1e-9;
    if (delta >= 0) return normalizedAngle(angle - start) <= delta + epsilon;
    return normalizedAngle(start - angle) <= -delta + epsilon;
  }

  function svgPathExactBounds(pathData, scale = 1, translateX = 0, translateY = 0) {
    const points = [];
    const add = point => points.push(boxPoint(point.x * scale + translateX, point.y * scale + translateY));
    for (const command of svgPathCommands(pathData)) {
      if (command.op === "M" || command.op === "L" || command.op === "Z") {
        add(command.end);
        if (command.op !== "M") add(command.start);
        continue;
      }
      const parameters = svgArcParameters(command.start, command.arc);
      if (!parameters) {
        add(command.end);
        continue;
      }
      add(command.start);
      add(command.end);
      const xExtreme = Math.atan2(-parameters.ry * parameters.sine, parameters.rx * parameters.cosine);
      const yExtreme = Math.atan2(parameters.ry * parameters.cosine, parameters.rx * parameters.sine);
      [xExtreme, xExtreme + Math.PI, yExtreme, yExtreme + Math.PI].forEach(angle => {
        if (angleFallsOnArc(angle, parameters.startAngle, parameters.deltaAngle)) add(svgArcPoint(parameters, angle));
      });
    }
    if (!points.length) return null;
    const xs = points.map(point => point.x);
    const ys = points.map(point => point.y);
    return {
      minX:Math.min(...xs), minY:Math.min(...ys),
      maxX:Math.max(...xs), maxY:Math.max(...ys)
    };
  }

  function svgPathToCubicPathData(pathData) {
    const output = [];
    for (const command of svgPathCommands(pathData)) {
      if (command.op === "M" || command.op === "L") {
        output.push(`${command.op}${command.end.x} ${command.end.y}`);
      } else if (command.op === "Z") {
        output.push("Z");
      } else {
        const parameters = svgArcParameters(command.start, command.arc);
        if (!parameters) {
          output.push(`L${command.end.x} ${command.end.y}`);
          continue;
        }
        const segments = Math.max(1, Math.ceil(Math.abs(parameters.deltaAngle) / (Math.PI / 2)));
        const delta = parameters.deltaAngle / segments;
        for (let index = 0; index < segments; index += 1) {
          const theta1 = parameters.startAngle + delta * index;
          const theta2 = theta1 + delta;
          const startPoint = svgArcPoint(parameters, theta1);
          const endPoint = svgArcPoint(parameters, theta2);
          const alpha = (4 / 3) * Math.tan(delta / 4);
          const derivative = theta => boxPoint(
            -parameters.cosine * parameters.rx * Math.sin(theta) - parameters.sine * parameters.ry * Math.cos(theta),
            -parameters.sine * parameters.rx * Math.sin(theta) + parameters.cosine * parameters.ry * Math.cos(theta)
          );
          const firstDerivative = derivative(theta1);
          const secondDerivative = derivative(theta2);
          const control1 = boxPoint(startPoint.x + alpha * firstDerivative.x, startPoint.y + alpha * firstDerivative.y);
          const control2 = boxPoint(endPoint.x - alpha * secondDerivative.x, endPoint.y - alpha * secondDerivative.y);
          output.push(`C${control1.x} ${control1.y} ${control2.x} ${control2.y} ${endPoint.x} ${endPoint.y}`);
        }
      }
    }
    return output.join(" ");
  }

  function svgPathToBoxPaths(pathData, scale = 1) {
    const tokens = String(pathData || "").match(/[MLAZmlaz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g) || [];
    const paths = [];
    let currentPath = [];
    let current = boxPoint(0, 0);
    let subpathStart = null;
    let index = 0;
    const flush = () => {
      if (currentPath.length > 1) paths.push(currentPath.map(point => boxPoint(point.x * scale, point.y * scale)));
      currentPath = [];
      subpathStart = null;
    };
    const number = () => {
      const value = Number(tokens[index++]);
      if (!Number.isFinite(value)) throw new TypeError("标准参考刀板包含无效坐标");
      return value;
    };
    while (index < tokens.length) {
      const command = tokens[index++];
      if (command === "M" || command === "m") {
        flush();
        const x = number();
        const y = number();
        current = command === "m" ? boxPoint(current.x + x, current.y + y) : boxPoint(x, y);
        subpathStart = boxPoint(current.x, current.y);
        currentPath.push(current);
      } else if (command === "L" || command === "l") {
        const x = number();
        const y = number();
        current = command === "l" ? boxPoint(current.x + x, current.y + y) : boxPoint(x, y);
        currentPath.push(current);
      } else if (command === "A" || command === "a") {
        const arc = {
          rx:number(), ry:number(), rotation:number(),
          largeArc:Boolean(number()), sweep:Boolean(number()),
          x:number(), y:number()
        };
        if (command === "a") {
          arc.x += current.x;
          arc.y += current.y;
        }
        const sampled = svgArcPoints(current, arc);
        currentPath.push(...sampled);
        current = sampled[sampled.length - 1];
      } else if (command === "Z" || command === "z") {
        if (subpathStart && (!nearlyEqual(current.x, subpathStart.x) || !nearlyEqual(current.y, subpathStart.y))) {
          currentPath.push(boxPoint(subpathStart.x, subpathStart.y));
        }
        current = subpathStart || current;
        flush();
      } else {
        throw new TypeError(`标准参考刀板包含暂不支持的 SVG 命令：${command}`);
      }
    }
    flush();
    return paths;
  }

  function isShengdaDoubleTuck(definition) {
    return /^shengda-double-tuck-(opposite|same)(-slot)?$/.test(
      String(definition?.production2DStandard || "")
    );
  }

  /*
   * 标准双插盒不是四个名称共用一张“十字形”示意图。
   *
   * 参考刀板中：
   * - 异侧结构从左糊口开始，面板顺序为 长/宽/长/宽；
   * - 同侧结构整体换向，面板顺序为 宽/长/宽/长，糊口位于右端；
   * - 插扣版在相对主面上有真正伸出的承口舌，主插舌外缘有半圆取口；
   * - 尘翼有斜肩和圆滑卸力段，主插舌由盖片、压线和圆角舌三段组成。
   *
   * 这些差异会直接影响模切和折叠，不能用通用矩形翼片换名称来模拟。
   * 本函数只服务四张已核对的双插盒，不改变其他卡盒和既有报价尺寸。
   */
  function approximateShengdaDoubleTuckGeometry(options) {
    const { length:l, width:w, height:h, glueFlap:g, thickness:t, definition } = options;
    const manufacturingBasis = definition.dimensionBasis === "manufacturing";
    const bodyHeight = manufacturingBasis ? h : h + t;
    const longPanel = manufacturingBasis ? l : l + t * 2;
    const shortPanel = manufacturingBasis ? w : w + t * 2;
    const sameSide = definition.closureSide === "same";
    const slot = Boolean(definition.lockSlot);
    const panels = sameSide
      ? [shortPanel, longPanel, shortPanel, longPanel]
      : [longPanel, shortPanel, longPanel, shortPanel];
    const glueOnLeft = !sameSide;
    const bodyStart = glueOnLeft ? g : 0;
    const xs = [bodyStart];
    panels.forEach(panel => xs.push(xs[xs.length - 1] + panel));
    const totalWidth = xs[4] + (glueOnLeft ? 0 : g);
    const tuckTongue = Math.min(20, Math.max(10, shortPanel * .22));
    const topDepth = Math.max(shortPanel + tuckTongue, 24);
    const bottomDepth = topDepth;
    const dustDepth = Math.max(14, Math.min(shortPanel * .54, longPanel * .42));
    // 标准插扣版的承口舌在缩略图中仍然清晰可见，不能退化成两条短线。
    const receiverDepth = Math.max(12, Math.min(dustDepth * .78, shortPanel * .42));
    const y0 = topDepth;
    const y1 = y0 + bodyHeight;
    const topPrimary = sameSide ? 1 : 0;
    const bottomPrimary = sameSide ? topPrimary : 2;
    const topReceiver = slot ? (topPrimary + 2) % 4 : -1;
    const bottomReceiver = slot ? (bottomPrimary + 2) % 4 : -1;

    const roundedTuckPoints = (x0, x1, creaseY, depth, direction, withNotch) => {
      const panelWidth = x1 - x0;
      const { tongue, closure, inset, radius } = tuckFlapDimensions(panelWidth, depth);
      const root = Math.min(2.4, panelWidth * .025);
      const relief = Math.min(5, Math.max(2.2, closure * .065));
      const left = x0 + inset;
      const right = x1 - inset;
      const tongueBaseY = creaseY + direction * closure;
      const noseY = creaseY + direction * (closure + tongue);
      const center = (left + right) / 2;
      const notchRadius = Math.min(4, Math.max(2.2, panelWidth * .026));
      const points = [
        boxPoint(x0, creaseY),
        boxPoint(x0 + root, creaseY + direction * relief),
        boxPoint(left, tongueBaseY),
        boxPoint(left, noseY - direction * radius),
        boxPoint(left + radius * .28, noseY - direction * radius * .48),
        boxPoint(left + radius, noseY)
      ];
      if (withNotch) {
        points.push(
          boxPoint(center - notchRadius, noseY),
          boxPoint(center - notchRadius * .92, noseY - direction * notchRadius * .38),
          boxPoint(center - notchRadius * .58, noseY - direction * notchRadius * .76),
          boxPoint(center, noseY - direction * notchRadius),
          boxPoint(center + notchRadius * .58, noseY - direction * notchRadius * .76),
          boxPoint(center + notchRadius * .92, noseY - direction * notchRadius * .38),
          boxPoint(center + notchRadius, noseY)
        );
      }
      points.push(
        boxPoint(right - radius, noseY),
        boxPoint(right - radius * .28, noseY - direction * radius * .48),
        boxPoint(right, noseY - direction * radius),
        boxPoint(right, tongueBaseY),
        boxPoint(x1 - root, creaseY + direction * relief),
        boxPoint(x1, creaseY)
      );
      return points;
    };

    const relievedDustPoints = (x0, x1, creaseY, depth, direction, mirror) => {
      const panelWidth = x1 - x0;
      const root = Math.min(2.4, panelWidth * .045);
      const nearShoulder = Math.min(panelWidth * (mirror ? .14 : .1), Math.max(4, depth * .17));
      const farShoulder = Math.min(panelWidth * (mirror ? .1 : .14), Math.max(4, depth * .2));
      const round = Math.min(3.2, Math.max(1.5, depth * .09));
      const outerY = creaseY + direction * depth;
      return [
        boxPoint(x0, creaseY),
        boxPoint(x0 + root, creaseY + direction * depth * .12),
        boxPoint(x0 + nearShoulder, outerY - direction * round),
        boxPoint(x0 + nearShoulder + round, outerY),
        boxPoint(x1 - farShoulder - round, outerY),
        boxPoint(x1 - farShoulder, outerY - direction * round * .72),
        boxPoint(x1 - root, creaseY + direction * depth * .12),
        boxPoint(x1, creaseY)
      ];
    };

    const receiverTabPoints = (x0, x1, creaseY, depth, direction) => {
      const panelWidth = x1 - x0;
      const center = (x0 + x1) / 2;
      const half = Math.min(12, Math.max(7, panelWidth * .09));
      const corner = Math.min(2, depth * .16);
      const outerY = creaseY + direction * depth;
      return [
        boxPoint(x0, creaseY),
        boxPoint(center - half, creaseY),
        boxPoint(center - half, outerY - direction * corner),
        boxPoint(center - half + corner, outerY),
        boxPoint(center + half - corner, outerY),
        boxPoint(center + half, outerY - direction * corner),
        boxPoint(center + half, creaseY),
        boxPoint(x1, creaseY)
      ];
    };

    const stylesFor = (primary, receiver) => panels.map((_panel, index) => {
      if (index === primary) return "tuck";
      if (index === receiver) return "receiver";
      return ((index - primary + 4) % 2) === 1 ? "dust" : "none";
    });
    const topStyles = stylesFor(topPrimary, topReceiver);
    const bottomStyles = stylesFor(bottomPrimary, bottomReceiver);
    const profilePoints = (index, creaseY, direction, style, isBottom) => {
      if (style === "tuck") {
        return roundedTuckPoints(
          xs[index],
          xs[index + 1],
          creaseY,
          isBottom ? bottomDepth : topDepth,
          direction,
          slot
        );
      }
      if (style === "dust") {
        const mirror = (index + (isBottom ? 1 : 0) + (sameSide ? 1 : 0)) % 2 === 1;
        return relievedDustPoints(xs[index], xs[index + 1], creaseY, dustDepth, direction, mirror);
      }
      if (style === "receiver") {
        return receiverTabPoints(xs[index], xs[index + 1], creaseY, receiverDepth, direction);
      }
      return [boxPoint(xs[index], creaseY), boxPoint(xs[index + 1], creaseY)];
    };

    const topContour = [];
    if (glueOnLeft) topContour.push(boxPoint(0, y0 + 6), boxPoint(xs[0], y0));
    else topContour.push(boxPoint(xs[0], y0));
    topStyles.forEach((style, index) => {
      topContour.push(...profilePoints(index, y0, -1, style, false));
    });
    if (!glueOnLeft) topContour.push(boxPoint(totalWidth, y0 + 6));

    const points = [...topContour];
    points.push(boxPoint(totalWidth, y1 - 6));
    if (!glueOnLeft) points.push(boxPoint(xs[4], y1));
    for (let index = panels.length - 1; index >= 0; index -= 1) {
      points.push(...profilePoints(index, y1, 1, bottomStyles[index], true).reverse());
    }
    if (glueOnLeft) points.push(boxPoint(xs[0], y1), boxPoint(0, y1 - 6));
    else points.push(boxPoint(xs[0], y1));

    const foldPaths = [];
    const bodyBoundaries = glueOnLeft
      ? [xs[0], xs[1], xs[2], xs[3]]
      : [xs[1], xs[2], xs[3], xs[4]];
    bodyBoundaries.forEach(x => foldPaths.push(boxLine(x, y0, x, y1)));
    const addClosureFolds = (styles, creaseY, direction, isBottom) => {
      styles.forEach((style, index) => {
        if (style === "none") return;
        foldPaths.push(boxLine(xs[index], creaseY, xs[index + 1], creaseY));
        if (style !== "tuck") return;
        const depth = isBottom ? bottomDepth : topDepth;
        const dimensions = tuckFlapDimensions(panels[index], depth);
        foldPaths.push(boxLine(
          xs[index] + dimensions.inset,
          creaseY + direction * dimensions.closure,
          xs[index + 1] - dimensions.inset,
          creaseY + direction * dimensions.closure
        ));
      });
    };
    addClosureFolds(topStyles, y0, -1, false);
    addClosureFolds(bottomStyles, y1, 1, true);

    const makeProfile = (style, index, creaseY, direction, isBottom) => {
      if (style === "none") return null;
      const localPoints = profilePoints(index, creaseY, direction, style, isBottom).map(point => ({
        x:Number((point.x - xs[index]).toFixed(4)),
        outward:Number(((point.y - creaseY) * direction).toFixed(4))
      }));
      return {
        style,
        widthMm:Number(panels[index].toFixed(4)),
        depthMm:Number(Math.max(...localPoints.map(point => point.outward), 0).toFixed(4)),
        points:localPoints
      };
    };
    const topProfiles = topStyles.map((style, index) => makeProfile(style, index, y0, -1, false));
    const bottomProfiles = bottomStyles.map((style, index) => makeProfile(style, index, y1, 1, true));

    return {
      cutPaths:[closeBoxPath(points)],
      foldPaths,
      partLabels:[],
      manufacturing:{ length:longPanel, width:shortPanel, height:bodyHeight },
      structureMeta:{
        kind:"folding-carton",
        exact3D:true,
        bodyPanels:panels.slice(),
        bodyHeight,
        topPrimary,
        bottomPrimary,
        topStyles:topStyles.slice(),
        bottomStyles:bottomStyles.slice(),
        topProfiles,
        bottomProfiles,
        topDepth,
        bottomDepth,
        dustDepth,
        tuckTongue,
        hangerExtra:0,
        lockSlot:slot,
        windowCut:false,
        cleaningTear:false,
        topStyle:"tuck",
        productionTopVariant:"tuck",
        bottomStyle:"tuck",
        productionTopStyles:topStyles.slice(),
        production2DStyles:bottomStyles.slice(),
        production2DReverseCreases:[],
        production2DStandard:String(definition.production2DStandard || ""),
        glueEdge:glueOnLeft ? "left" : "right",
        receiverPanels:slot ? { top:topReceiver, bottom:bottomReceiver } : null,
        paperAllowance:manufacturingBasis
          ? { long:0, short:0, height:0 }
          : { long:t * 2, short:t * 2, height:t }
      }
    };
  }

  function shengdaDoubleTuckGeometry(options) {
    const fallback = approximateShengdaDoubleTuckGeometry(options);
    const typeName = options.definition.shengdaType || SHENGDA_DOUBLE_TUCK_TYPES[String(options.definition.production2DStandard || "")];
    const profile = shengdaReferenceProfile({ ...options.definition, shengdaType:typeName });
    const reference = profile?.reference;
    const referenceParams = profile?.params || {};
    const exactReferenceSize = reference
      && nearlyEqual(options.length, profile.lengthMm)
      && nearlyEqual(options.width, profile.widthMm)
      && nearlyEqual(options.height, profile.heightMm)
      && nearlyEqual(options.glueFlap, profile.glueFlapMm);
    if (!exactReferenceSize) return fallback;
    const scale = 1 / Number(root.JianglaiShengdaReferenceDielines.svgPointsPerMillimeter || SVG_POINTS_PER_MM);
    const referencePathValues = Array.isArray(reference.paths) && reference.paths.length
      ? reference.paths
      : [reference.path].filter(Boolean);
    const cutPaths = referencePathValues.flatMap(pathValue => svgPathToBoxPaths(pathValue, scale));
    const foldPaths = (reference.folds || []).map(line => boxLine(
      Number(line.x1), Number(line.y1), Number(line.x2), Number(line.y2)
    ));
    if (!cutPaths.length) return fallback;
    return {
      ...fallback,
      cutPaths,
      foldPaths,
      manufacturing:{ length:profile.lengthMm, width:profile.widthMm, height:profile.heightMm },
      cutSvgPaths:referencePathValues.map(pathValue => ({ d:pathValue, scale })),
      structureMeta:{
        ...fallback.structureMeta,
        exact2D:true,
        referenceProvider:"标准结构库",
        shengdaType:typeName,
        referenceSha256:reference.sha256,
        exactReferenceParams:{
          lengthMm:Number(referenceParams.boxLength),
          widthMm:Number(referenceParams.boxWidth),
          heightMm:Number(referenceParams.boxHeight),
          thicknessMm:Number(referenceParams.thickness),
          glueFlapMm:profile.glueFlapMm
        }
      }
    };
  }

  function shengdaRuntimeGeometry(options, fallback) {
    const engine = root.JianglaiShengdaBoxEngine;
    const typeName = options?.definition?.shengdaType;
    if (!engine?.getBox || !typeName) return null;
    try {
      const box = engine.getBox(typeName);
      const pathApi = box?.pathApi;
      const cutMethod = pathApi?.cuttingLinePath
        || pathApi?.cuttingLineCoordinates
        || pathApi?.cuttingLineoordinates;
      const foldMethod = pathApi?.foldLinePath
        || pathApi?.foldLineCoordinates
        || pathApi?.foldLineoordinates;
      const bleedMethod = pathApi?.bleedingLinePath
        || pathApi?.previewBleedingLinePath;
      if (typeof cutMethod !== "function" || typeof foldMethod !== "function") return null;
      const params = {
        ...(engine.defaultParams || {}),
        boxLength:Number(options.length),
        boxWidth:Number(options.width),
        boxHeight:Number(options.height),
        thickness:Number(options.thickness),
        bleedingSpace:Number(options.bleed),
        material:"白卡纸350克",
        sizeType:"制造尺寸",
        advancedParams:{}
      };
      const cutResult = cutMethod.call(pathApi, params);
      const foldResult = foldMethod.call(pathApi, params);
      let bleedResult = null;
      if (options.bleed > 0 && typeof bleedMethod === "function") {
        try {
          bleedResult = bleedMethod.call(pathApi, params);
        } catch (error) {
          console.warn("标准参数引擎出血线生成失败，已保留精确切线并回退出血线", typeName, error);
        }
      }
      if (cutResult?.then || foldResult?.then || bleedResult?.then) return null;
      const referencePathValues = [
        cutResult?.returnPath,
        ...(Array.isArray(cutResult?.returnPaths) ? cutResult.returnPaths : [])
      ].flatMap(value => Array.isArray(value) ? value : [value])
        .map(value => String(value || "").trim())
        .filter(Boolean);
      if (!referencePathValues.length || referencePathValues.some(value => /(?:NaN|Infinity)/.test(value))) {
        return null;
      }
      const bleedPathValues = [
        bleedResult?.returnPath,
        ...(Array.isArray(bleedResult?.returnPaths) ? bleedResult.returnPaths : [])
      ].flatMap(value => Array.isArray(value) ? value : [value])
        .map(value => String(value || "").trim())
        .filter(Boolean)
        .filter(value => !/(?:NaN|Infinity)/.test(value));
      const scale = 1 / SVG_POINTS_PER_MM;
      const cutPaths = referencePathValues.flatMap(pathValue => svgPathToBoxPaths(pathValue, scale));
      const bleedPaths = bleedPathValues.flatMap(pathValue => {
        try {
          return svgPathToBoxPaths(pathValue, scale);
        } catch (_) {
          // Some native bleed paths use SVG shorthand that the point sampler
          // intentionally does not approximate. The exact path is retained
          // below and rendered/exported without converting it to polylines.
          return [];
        }
      });
      const foldLineObj = foldResult?.foldLineObj;
      const foldLines = Array.isArray(foldLineObj)
        ? foldLineObj
        : foldLineObj && typeof foldLineObj === "object"
          ? Object.values(foldLineObj)
          : [];
      const foldPaths = foldLines.map(line => boxLine(
        Number(line.x1), Number(line.y1), Number(line.x2), Number(line.y2)
      )).filter(line => line.flat().every(point => Number.isFinite(point.x) && Number.isFinite(point.y)));
      if (!cutPaths.length || cutPaths.flat().some(point => !Number.isFinite(point.x) || !Number.isFinite(point.y))) {
        return null;
      }
      const actionPoints = Array.isArray(cutResult?.actionArray)
        ? cutResult.actionArray.filter(point => Number.isFinite(Number(point?.x)) && Number.isFinite(Number(point?.y)))
        : [];
      const actionBounds = actionPoints.length ? {
        minX:Math.min(...actionPoints.map(point => Number(point.x))),
        minY:Math.min(...actionPoints.map(point => Number(point.y))),
        maxX:Math.max(...actionPoints.map(point => Number(point.x))),
        maxY:Math.max(...actionPoints.map(point => Number(point.y)))
      } : null;
      const reportedFlatDimensions = actionBounds ? {
        width:actionBounds.maxX - (Math.abs(actionBounds.minX) <= .25 ? 0 : actionBounds.minX),
        height:actionBounds.maxY - (Math.abs(actionBounds.minY) <= .25 ? 0 : actionBounds.minY)
      } : null;
      return {
        ...fallback,
        cutPaths,
        foldPaths,
        bleedPaths,
        cutSvgPaths:referencePathValues.map(pathValue => ({ d:pathValue, scale })),
        bleedSvgPaths:bleedPathValues.map(pathValue => ({ d:pathValue, scale })),
        outerCutPathCount:referencePathValues.length,
        reportedFlatDimensions,
        manufacturing:{
          length:Number(options.length),
          width:Number(options.width),
          height:Number(options.height)
        },
        structureMeta:{
          ...(fallback?.structureMeta || {}),
          exact2D:true,
          referenceProvider:"标准参数引擎",
          shengdaType:typeName,
          referenceSha256:engine.sourceSha256,
          exactReferenceParams:{
            lengthMm:Number(options.length),
            widthMm:Number(options.width),
            heightMm:Number(options.height),
            thicknessMm:Number(options.thickness),
            glueFlapMm:Number(options.glueFlap)
          }
        }
      };
    } catch (error) {
      console.warn("标准参数引擎生成失败，已回退内置结构", typeName, error);
      return null;
    }
  }

  /*
   * Prefer the verified local Shengda-compatible runtime for every customer
   * dimension.  The captured catalogue references remain as an offline
   * fallback when the runtime asset is unavailable.
   */
  function applyShengdaReferenceGeometry(options, fallback) {
    const runtimeGeometry = shengdaRuntimeGeometry(options, fallback);
    if (runtimeGeometry) return runtimeGeometry;
    if (fallback?.structureMeta?.exact2D) return fallback;
    const typeName = options.definition.shengdaType;
    const profile = shengdaReferenceProfile(options.definition);
    const reference = profile?.reference;
    const referenceParams = profile?.params || {};
    const exactReferenceSize = reference
      && nearlyEqual(options.length, profile.lengthMm)
      && nearlyEqual(options.width, profile.widthMm)
      && nearlyEqual(options.height, profile.heightMm)
      && nearlyEqual(options.glueFlap, profile.glueFlapMm);
    if (!exactReferenceSize) return fallback;
    const scale = 1 / Number(root.JianglaiShengdaReferenceDielines.svgPointsPerMillimeter || SVG_POINTS_PER_MM);
    const referencePathValues = Array.isArray(reference.paths) && reference.paths.length
      ? reference.paths
      : [reference.path].filter(Boolean);
    const cutPaths = referencePathValues.flatMap(pathValue => svgPathToBoxPaths(pathValue, scale));
    const foldPaths = (reference.folds || []).map(line => boxLine(
      Number(line.x1), Number(line.y1), Number(line.x2), Number(line.y2)
    ));
    if (!cutPaths.length) return fallback;
    return {
      ...fallback,
      cutPaths,
      foldPaths,
      cutSvgPaths:referencePathValues.map(pathValue => ({ d:pathValue, scale })),
      outerCutPathCount:referencePathValues.length,
      manufacturing:{ length:profile.lengthMm, width:profile.widthMm, height:profile.heightMm },
      structureMeta:{
        ...(fallback.structureMeta || {}),
        exact2D:true,
        referenceProvider:"标准结构库",
        shengdaType:typeName,
        referenceSha256:reference.sha256,
        exactReferenceParams:{
          lengthMm:Number(referenceParams.boxLength),
          widthMm:Number(referenceParams.boxWidth),
          heightMm:Number(referenceParams.boxHeight),
          thicknessMm:Number(referenceParams.thickness),
          glueFlapMm:profile.glueFlapMm
        }
      }
    };
  }

  function foldingCartonGeometry(options) {
    if (isShengdaDoubleTuck(options.definition)) return shengdaDoubleTuckGeometry(options);
    const { length:l, width:w, height:h, glueFlap:g, thickness:t, definition } = options;
    const manufacturingBasis = definition.dimensionBasis === "manufacturing";
    const bodyHeight = manufacturingBasis ? h : h + t;
    const longPanel = manufacturingBasis ? l : l + t * 2;
    const shortPanel = manufacturingBasis ? w : w + t * 2;
    const panels = [longPanel, shortPanel, longPanel, shortPanel];
    const xs = [g];
    panels.forEach(panel => xs.push(xs[xs.length - 1] + panel));
    const tuckTongue = Math.min(20, Math.max(10, shortPanel * .22));
    const topDepth = Math.max(shortPanel + tuckTongue, 24);
    const dustDepth = Math.max(14, Math.min(shortPanel * .54, longPanel * .42));
    const bottomDepth = definition.bottomStyle === "tuck" ? topDepth : Math.max(shortPanel * .84, 23);
    const productionTopVariant = String(definition.productionTopVariant || definition.topStyle || "tuck");
    const hangerExtra = productionTopVariant === "hanger" ? Math.min(30, Math.max(20, topDepth * .5)) : 0;
    const y0 = topDepth + hangerExtra;
    const y1 = y0 + bodyHeight;
    // The closing tongue must sit on the wider alternating body panel.
    // A fixed panel-0 tongue made every "wide" box structurally wrong.
    const preferredPrimary = longPanel >= shortPanel ? 0 : 1;
    const topPrimary = definition.reverse ? (preferredPrimary + 2) % 4 : preferredPrimary;
    const bottomPrimary = definition.closureSide === "same"
      ? topPrimary
      : (topPrimary + 2) % 4;
    const bottom2DStyles = productionBottomStyles(definition, panels, bottomPrimary);
    const top2DStyles = panels.map((_panel, index) => index === topPrimary
      ? productionTopVariant
      : (((index - topPrimary + 4) % 2) === 1 ? "dust" : "none"));
    const topPoints = [boxPoint(0, y0 + 5), boxPoint(g, y0)];
    for (let index = 0; index < panels.length; index += 1) {
      const style = top2DStyles[index];
      const depth = style === "dust" ? dustDepth : topDepth;
      topPoints.push(...cartonProductionFlapPoints(xs[index], xs[index + 1], y0, depth, -1, style, definition.reverse));
    }
    const points = [...topPoints, boxPoint(xs[4], y1)];
    for (let index = panels.length - 1; index >= 0; index -= 1) {
      let style = "dust";
      let depth = dustDepth;
      if (definition.bottomStyle === "tuck") {
        style = index === bottomPrimary
          ? "tuck"
          : (((index - bottomPrimary + 4) % 2) === 1 ? "dust" : "none");
        depth = style === "dust" ? dustDepth : bottomDepth;
      } else if (definition.bottomStyle === "lock") {
        style = bottom2DStyles[index];
        depth = style.startsWith("snap-side") ? bottomDepth * .82 : bottomDepth;
      } else if (definition.bottomStyle === "auto") {
        style = bottom2DStyles[index];
        depth = style.startsWith("auto-side") ? bottomDepth * .9 : bottomDepth;
      }
      const mirrored = (index + (definition.reverse ? 1 : 0) + (definition.closureSide === "same" ? 1 : 0)) % 2 === 1;
      points.push(...cartonProductionFlapPoints(xs[index], xs[index + 1], y1, depth, 1, style, mirrored).reverse());
    }
    points.push(boxPoint(g, y1), boxPoint(0, y1 - 5));
    const foldPaths = [
      boxLine(g, y0, g, y1),
      ...xs.slice(2, 5).map(x => boxLine(x, y0, x, y1))
    ];
    const production2DReverseCreases = [];
    const topStyles = panels.map((_panel, index) => index === topPrimary
      ? (definition.topStyle === "hanger" ? "hanger" : "tuck")
      : (((index - topPrimary + 4) % 2) === 1 ? "dust" : "none"));
    const bottomStyles = panels.map((_panel, index) => {
      if (definition.bottomStyle === "tuck") {
        return index === bottomPrimary
          ? "tuck"
          : (((index - bottomPrimary + 4) % 2) === 1 ? "dust" : "none");
      }
      if (definition.bottomStyle === "lock") return index % 2 === 0 ? "lock-major" : "lock-side";
      if (definition.bottomStyle === "auto") return index % 2 === 0 ? "auto-major" : "lock-side";
      return "none";
    });
    /*
     * Keep the actual flap outline beside the semantic structure metadata.
     * The 3D preview must not replace a rounded tuck tongue, relieved dust
     * flap or shaped lock bottom with a generic rectangle.  Coordinates are
     * local to the hinge: x runs across the panel and `outward` runs away
     * from the body, so the same profile can be folded onto any box face.
     */
    const closureProfile = (index, creaseY, direction, style, depth, mirror) => {
      if (!style || style === "none") return null;
      const points = cartonProductionFlapPoints(
        xs[index],
        xs[index + 1],
        creaseY,
        depth,
        direction,
        style,
        mirror
      ).map(point => ({
        x:Number((point.x - xs[index]).toFixed(4)),
        outward:Number(((point.y - creaseY) * direction).toFixed(4))
      }));
      return {
        style,
        widthMm:Number(panels[index].toFixed(4)),
        depthMm:Number(Math.max(...points.map(point => point.outward), 0).toFixed(4)),
        points
      };
    };
    const topProfiles = top2DStyles.map((style, index) => closureProfile(
      index,
      y0,
      -1,
      style,
      style === "dust" ? dustDepth : topDepth,
      definition.reverse
    ));
    const bottomProfiles = (bottom2DStyles || bottomStyles).map((style, index) => {
      const depth = style === "dust"
        ? dustDepth
        : style?.startsWith("snap-side")
          ? bottomDepth * .82
          : style?.startsWith("auto-side")
            ? bottomDepth * .9
          : bottomDepth;
      const mirror = (index + (definition.reverse ? 1 : 0) + (definition.closureSide === "same" ? 1 : 0)) % 2 === 1;
      return closureProfile(index, y1, 1, style, depth, mirror);
    });
    top2DStyles.forEach((style, index) => {
      if (style !== "none") foldPaths.push(boxLine(xs[index], y0, xs[index + 1], y0));
      if (["tuck", "centered-tuck", "full-flap", "tab-lock"].includes(style)) {
        const dimensions = tuckFlapDimensions(panels[index], topDepth);
        const closureRatio = style === "centered-tuck" ? .72 : style === "full-flap" ? .86 : style === "tab-lock" ? .84 : dimensions.closure / topDepth;
        const insetRatio = style === "centered-tuck" ? .2 : style === "full-flap" ? .05 : style === "tab-lock" ? .1 : dimensions.inset / panels[index];
        foldPaths.push(boxLine(
          xs[index] + panels[index] * insetRatio,
          y0 - topDepth * closureRatio,
          xs[index + 1] - panels[index] * insetRatio,
          y0 - topDepth * closureRatio
        ));
      }
    });
    bottomStyles.forEach((style, index) => {
      if (style !== "none") foldPaths.push(boxLine(xs[index], y1, xs[index + 1], y1));
      if (style === "tuck") {
        const closure = tuckFlapDimensions(panels[index], bottomDepth).closure;
        const inset = tuckFlapDimensions(panels[index], bottomDepth).inset;
        foldPaths.push(boxLine(xs[index] + inset, y1 + closure, xs[index + 1] - inset, y1 + closure));
      }
    });
    /*
     * The final snap-lock flap is not just a trapezoid.  Its two reverse
     * creases make the fourth flap flex over the first three and lock without
     * glue.  Omitting these lines produces a drawing that looks plausible but
     * cannot be folded as a real 1-2-3 bottom.
     */
    if (definition.bottomStyle === "lock") {
      bottom2DStyles.forEach((style, index) => {
        if (style !== "snap-final") return;
        const panelWidth = panels[index];
        const outerY = y1 + bottomDepth;
        const shoulderY = y1 + bottomDepth * .72;
        const left = boxPoint(xs[index] + panelWidth * .22, shoulderY);
        const center = boxPoint(xs[index] + panelWidth * .5, outerY);
        const right = boxPoint(xs[index] + panelWidth * .78, shoulderY);
        const first = boxLine(left.x, left.y, center.x, center.y);
        const second = boxLine(center.x, center.y, right.x, right.y);
        foldPaths.push(first, second);
        production2DReverseCreases.push(first, second);
      });
    }
    const cutPaths = [closeBoxPath(points)];
    if (productionTopVariant === "hanger") {
      const center = (xs[topPrimary] + xs[topPrimary + 1]) / 2;
      const holeWidth = Math.min(28, panels[topPrimary] * .25);
      const holeY = y0 - topDepth - hangerExtra * .58;
      if (definition.hangerHoleStyle === "round") {
        const radius = Math.min(5, holeWidth * .22);
        cutPaths.push(closeBoxPath(Array.from({ length:16 }, (_item, index) => {
          const angle = -Math.PI / 2 + index * Math.PI * 2 / 16;
          return boxPoint(center + Math.cos(angle) * radius, holeY + Math.sin(angle) * radius);
        })));
      } else {
        const half = holeWidth / 2;
        const radius = Math.min(3.5, half * .28);
        const stemHalf = Math.min(4.2, half * .34);
        cutPaths.push(closeBoxPath([
          boxPoint(center - half + radius, holeY - 2.8),
          boxPoint(center - stemHalf, holeY - 2.8),
          boxPoint(center - stemHalf, holeY - .4),
          boxPoint(center - stemHalf - radius, holeY + 2.7),
          boxPoint(center + stemHalf + radius, holeY + 2.7),
          boxPoint(center + stemHalf, holeY - .4),
          boxPoint(center + stemHalf, holeY - 2.8),
          boxPoint(center + half - radius, holeY - 2.8),
          boxPoint(center + half, holeY - 1.4),
          boxPoint(center + half, holeY + 1.4),
          boxPoint(center + half - radius, holeY + 2.8),
          boxPoint(center - half + radius, holeY + 2.8),
          boxPoint(center - half, holeY + 1.4),
          boxPoint(center - half, holeY - 1.4)
        ]));
      }
    }
    if (definition.lockSlot) {
      if (productionTopVariant === "tab-lock") {
        const receiver = (topPrimary + 2) % 4;
        const center = (xs[receiver] + xs[receiver + 1]) / 2;
        const slotWidth = Math.min(26, panels[receiver] * .28);
        const slotHalfHeight = Math.min(1.1, t * 1.4 + .35);
        const slotY = y0 + Math.max(4, t * 4);
        cutPaths.push(closeBoxPath([
          boxPoint(center - slotWidth / 2 + slotHalfHeight, slotY - slotHalfHeight),
          boxPoint(center + slotWidth / 2 - slotHalfHeight, slotY - slotHalfHeight),
          boxPoint(center + slotWidth / 2, slotY),
          boxPoint(center + slotWidth / 2 - slotHalfHeight, slotY + slotHalfHeight),
          boxPoint(center - slotWidth / 2 + slotHalfHeight, slotY + slotHalfHeight),
          boxPoint(center - slotWidth / 2, slotY)
        ]));
      } else {
        const addTuckShoulderSlits = (primary, creaseY, depth, direction) => {
          const dimensions = tuckFlapDimensions(panels[primary], depth);
          const tongueBaseY = creaseY + direction * dimensions.closure;
          const slitLength = Math.min(5, dimensions.tongue * .38);
          [xs[primary] + dimensions.inset, xs[primary + 1] - dimensions.inset].forEach(x => {
            cutPaths.push(boxLine(x, tongueBaseY - direction * slitLength, x, tongueBaseY + direction * slitLength * .2));
          });
        };
        addTuckShoulderSlits(topPrimary, y0, topDepth, -1);
        if (definition.bottomStyle === "tuck") addTuckShoulderSlits(bottomPrimary, y1, bottomDepth, 1);
      }
    }
    if (definition.bottomStyle === "auto") {
      [0, 2].forEach((index, order) => {
        const mirror = (order + (definition.reverse ? 1 : 0)) % 2 === 1;
        foldPaths.push(boxLine(
          mirror ? xs[index + 1] : xs[index],
          y1,
          mirror ? xs[index] + panels[index] * .38 : xs[index] + panels[index] * .62,
          y1 + bottomDepth * .78
        ));
      });
    }
    if (definition.windowCut) {
      const x0 = xs[0] + longPanel * .2;
      const x1 = xs[1] - longPanel * .2;
      const windowTop = y0 + bodyHeight * .2;
      const windowBottom = y1 - bodyHeight * .2;
      cutPaths.push(closeBoxPath([
        boxPoint(x0 + 5, windowTop), boxPoint(x1 - 5, windowTop),
        boxPoint(x1, windowTop + 5), boxPoint(x1, windowBottom - 5),
        boxPoint(x1 - 5, windowBottom), boxPoint(x0 + 5, windowBottom),
        boxPoint(x0, windowBottom - 5), boxPoint(x0, windowTop + 5)
      ]));
    }
    if (definition.cleaningTear) {
      /*
       * 标准“清洁盒”不是普通双插盒换名：正面有两道可撕开取用的
       * 波浪刀线。这里把它们写入真实 cutPaths，因此排版、SVG/PDF
       * 导出与展开预览使用的是同一组生产轮廓。
       */
      const panel = topPrimary;
      const inset = Math.max(7, panels[panel] * .1);
      const xStart = xs[panel] + inset;
      const xEnd = xs[panel + 1] - inset;
      const makeTear = (y, flip) => {
        const count = 12;
        const amplitude = Math.min(2.4, bodyHeight * .018);
        return Array.from({ length:count + 1 }, (_item, index) => {
          const ratio = index / count;
          const wave = index === 0 || index === count
            ? 0
            : Math.sin(ratio * Math.PI * 6) * amplitude * (flip ? -1 : 1);
          return boxPoint(xStart + (xEnd - xStart) * ratio, y + wave);
        });
      };
      cutPaths.push(
        makeTear(y0 + bodyHeight * .31, false),
        makeTear(y0 + bodyHeight * .69, true)
      );
    }
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:longPanel, width:shortPanel, height:bodyHeight },
      structureMeta:{
        kind:"folding-carton",
        exact3D:true,
        bodyPanels:panels.slice(),
        bodyHeight,
        topPrimary,
        bottomPrimary,
        topStyles:topStyles.slice(),
        bottomStyles:bottomStyles.slice(),
        topProfiles,
        bottomProfiles,
        topDepth,
        bottomDepth,
        dustDepth,
        tuckTongue,
        hangerExtra,
        lockSlot:Boolean(definition.lockSlot),
        windowCut:Boolean(definition.windowCut),
        cleaningTear:Boolean(definition.cleaningTear),
        topStyle:String(definition.topStyle || "tuck"),
        productionTopVariant,
        bottomStyle:String(definition.bottomStyle || "tuck"),
        productionTopStyles:top2DStyles.slice(),
        production2DStyles:bottom2DStyles ? bottom2DStyles.slice() : bottomStyles.slice(),
        production2DReverseCreases:production2DReverseCreases.map(path => path.map(point => ({ ...point }))),
        paperAllowance:manufacturingBasis
          ? { long:0, short:0, height:0 }
          : { long:t * 2, short:t * 2, height:t }
      }
    };
  }

  function mailerGeometry(options) {
    const { length:l0, width:w0, height:h0, thickness:t, definition } = options;
    const l = l0 + t * 2;
    const w = w0 + t * 2;
    const h = h0 + t;
    const x0 = h * 2;
    const x1 = x0 + l;
    const tuck = Math.max(18, Math.min(45, h * .7));
    const lidTop = tuck;
    const lidBottom = lidTop + w;
    const backBottom = lidBottom + h;
    const baseBottom = backBottom + w;
    const frontBottom = baseBottom + h;
    const lidWing = Math.max(12, h * .9);
    const frontEar = Math.max(10, h * .72);
    const lead = Math.max(5, Math.min(14, h * .22));
    const corner = Math.max(3, Math.min(8, h * .12));
    const lipInset = Math.max(8, Math.min(20, l * .08));
    /*
     * FEFCO 0427 production outline.
     *
     * This is a one-piece roll-end mailer: lid+tuck, rear wall, base, front
     * wall, double-thickness roll-over side walls and front locking ears.
     * The early vertical cross omitted the roll-over walls and could not close.
     * This correction is deliberately limited to the 2D cut/fold geometry;
     * the established 3D metadata below remains byte-for-byte compatible.
     */
    const cutPaths = [closeBoxPath([
      boxPoint(x0 + lipInset, 0), boxPoint(x1 - lipInset, 0),
      boxPoint(x1, corner), boxPoint(x1, lidTop),
      boxPoint(x1 + lidWing * .62, lidTop + lead),
      boxPoint(x1 + lidWing, lidTop + lidWing * .42),
      boxPoint(x1 + lidWing, lidBottom),
      boxPoint(x1, lidBottom),
      boxPoint(x1, backBottom),
      boxPoint(x1 + h * 1.72, backBottom),
      boxPoint(x1 + h * 2, backBottom + lead),
      boxPoint(x1 + h * 2, baseBottom - lead),
      boxPoint(x1 + h * 1.72, baseBottom),
      boxPoint(x1, baseBottom),
      boxPoint(x1 + frontEar, baseBottom + lead),
      boxPoint(x1 + frontEar, frontBottom - lead),
      boxPoint(x1, frontBottom),
      boxPoint(x0, frontBottom),
      boxPoint(x0 - frontEar, frontBottom - lead),
      boxPoint(x0 - frontEar, baseBottom + lead),
      boxPoint(x0, baseBottom),
      boxPoint(x0 - h * 1.72, baseBottom),
      boxPoint(x0 - h * 2, baseBottom - lead),
      boxPoint(x0 - h * 2, backBottom + lead),
      boxPoint(x0 - h * 1.72, backBottom),
      boxPoint(x0, backBottom),
      boxPoint(x0, lidBottom),
      boxPoint(x0 - lidWing, lidBottom),
      boxPoint(x0 - lidWing, lidTop + lidWing * .42),
      boxPoint(x0 - lidWing * .62, lidTop + lead),
      boxPoint(x0, lidTop),
      boxPoint(x0, corner)
    ])];
    const foldPaths = [
      boxLine(x0, lidTop, x1, lidTop),
      boxLine(x0, lidBottom, x1, lidBottom),
      boxLine(x0, backBottom, x1, backBottom),
      boxLine(x0, baseBottom, x1, baseBottom),
      boxLine(x0, lidTop, x0, lidBottom),
      boxLine(x1, lidTop, x1, lidBottom),
      boxLine(x0, backBottom, x0, baseBottom),
      boxLine(x1, backBottom, x1, baseBottom),
      boxLine(x0 - h, backBottom, x0 - h, baseBottom),
      boxLine(x1 + h, backBottom, x1 + h, baseBottom),
      boxLine(x0, baseBottom + lead, x0, frontBottom - lead),
      boxLine(x1, baseBottom + lead, x1, frontBottom - lead)
    ];
    // Assembly reliefs between rear wall and rolled side walls.
    cutPaths.push(
      [boxPoint(x0 - h, backBottom), boxPoint(x0 - h + lead, backBottom + lead)],
      [boxPoint(x1 + h, backBottom), boxPoint(x1 + h - lead, backBottom + lead)]
    );
    if (definition.lockSlot) {
      const center = (x0 + x1) / 2;
      const slotGap = Math.min(42, l * .26);
      [center - slotGap, center + slotGap].forEach(slotCenter => cutPaths.push([
        boxPoint(slotCenter - 5, baseBottom + h * .28),
        boxPoint(slotCenter - 5, baseBottom + h * .5),
        boxPoint(slotCenter + 5, baseBottom + h * .5),
        boxPoint(slotCenter + 5, baseBottom + h * .28)
      ]));
    }
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:l, width:w, height:h },
      structureMeta:{
        kind:"mailer",
        exact3D:true,
        base:{ length:l, width:w },
        wallHeight:h,
        lid:{ length:l, width:w },
        tuckDepth:tuck,
        lockSlot:Boolean(definition.lockSlot)
      }
    };
  }

  function crossPartGeometry(x, y, width, height, depth, label) {
    const cut = closeBoxPath([
      boxPoint(x, y), boxPoint(x + width, y),
      boxPoint(x + width, y + depth), boxPoint(x + width + depth, y + depth),
      boxPoint(x + width + depth, y + depth + height), boxPoint(x + width, y + depth + height),
      boxPoint(x + width, y + depth * 2 + height), boxPoint(x, y + depth * 2 + height),
      boxPoint(x, y + depth + height), boxPoint(x - depth, y + depth + height),
      boxPoint(x - depth, y + depth), boxPoint(x, y + depth)
    ]);
    const folds = [
      boxLine(x, y + depth, x + width, y + depth),
      boxLine(x, y + depth + height, x + width, y + depth + height),
      boxLine(x, y + depth, x, y + depth + height),
      boxLine(x + width, y + depth, x + width, y + depth + height)
    ];
    return { cut, folds, label:{ text:label, x:x + width / 2, y:y + depth + height / 2 } };
  }

  function lidBaseGeometry(options) {
    const { length:l, width:w, height:h, thickness:t, clearance:c } = options;
    const bottomDepth = h;
    const lidDepth = Math.max(15, Math.min(h, h * .55));
    const bottom = crossPartGeometry(bottomDepth, 0, l, w, bottomDepth, "盒底");
    const lidWidth = l + c * 2 + t * 2;
    const lidHeight = w + c * 2 + t * 2;
    const gap = 18;
    const lidX = bottomDepth * 2 + l + gap + lidDepth;
    const lid = crossPartGeometry(lidX, 0, lidWidth, lidHeight, lidDepth, "上盖");
    return {
      cutPaths:[bottom.cut, lid.cut],
      outerCutPathCount:2,
      foldPaths:[...bottom.folds, ...lid.folds],
      partLabels:[bottom.label, lid.label],
      manufacturing:{ length:l, width:w, height:h, lidLength:lidWidth, lidWidth:lidHeight, lidDepth },
      structureMeta:{
        kind:"lid-base",
        exact3D:true,
        base:{ length:l, width:w, depth:bottomDepth },
        lid:{ length:lidWidth, width:lidHeight, depth:lidDepth },
        clearance:c,
        thickness:t
      }
    };
  }

  function sleeveGeometry(options) {
    const { length:l, width:w, height:h, thickness:t, glueFlap:g } = options;
    const panels = [h + t, w + t * 2, h + t, w + t * 2];
    const total = panels.reduce((sum, item) => sum + item, g);
    const cutPaths = [closeBoxPath([boxPoint(0, 0), boxPoint(total, 0), boxPoint(total, l), boxPoint(0, l)])];
    let x = g;
    const foldPaths = [boxLine(x, 0, x, l)];
    panels.slice(0, -1).forEach(panel => { x += panel; foldPaths.push(boxLine(x, 0, x, l)); });
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:l, width:w + t * 2, height:h + t },
      structureMeta:{
        kind:"sleeve",
        exact3D:true,
        bodyPanels:panels.slice(),
        sleeveLength:l,
        glueFlap:g
      }
    };
  }

  function paperBagGeometry(options) {
    const { length:bagWidth, width:depth, height:bagHeight, glueFlap:g, thickness:t, definition } = options;
    const topFold = Math.max(25, Math.min(50, bagHeight * .12));
    const bottom = Math.max(30, Math.min(depth * .75, 90));
    const panelWidth = bagWidth + t * 2;
    const gusset = depth + t * 2;
    const panels = [panelWidth, gusset, panelWidth, gusset];
    const total = panels.reduce((sum, item) => sum + item, g);
    const bodyTop = topFold;
    const bodyBottom = bodyTop + bagHeight;
    const cutPaths = [closeBoxPath([boxPoint(0, 0), boxPoint(total, 0), boxPoint(total, bodyBottom + bottom), boxPoint(0, bodyBottom + bottom)])];
    let x = g;
    const foldPaths = [boxLine(0, bodyTop, total, bodyTop), boxLine(0, bodyBottom, total, bodyBottom), boxLine(x, 0, x, bodyBottom + bottom)];
    panels.forEach((panel, index) => {
      const previous = x;
      x += panel;
      if (index < panels.length - 1) foldPaths.push(boxLine(x, 0, x, bodyBottom + bottom));
      if (index % 2 === 1) {
        const center = previous + panel / 2;
        foldPaths.push(boxLine(center, bodyTop, center, bodyBottom), boxLine(previous, bodyBottom, center, bodyBottom + bottom), boxLine(x, bodyBottom, center, bodyBottom + bottom));
      }
    });
    const handleWidth = Math.min(50, panelWidth * .42);
    const handleY = Math.max(8, topFold * .45);
    const handleXs = [g + panelWidth / 2, g + panelWidth + gusset + panelWidth / 2];
    handleXs.forEach(center => {
      if (definition.handleStyle === "rope") {
        [-handleWidth * .34, handleWidth * .34].forEach(offset => cutPaths.push(closeBoxPath([
          boxPoint(center + offset - 2.2, handleY), boxPoint(center + offset + 2.2, handleY),
          boxPoint(center + offset + 2.2, handleY + 4.4), boxPoint(center + offset - 2.2, handleY + 4.4)
        ])));
      } else {
        cutPaths.push(closeBoxPath([
          boxPoint(center - handleWidth / 2, handleY),
          boxPoint(center + handleWidth / 2, handleY),
          boxPoint(center + handleWidth / 2, handleY + 5),
          boxPoint(center - handleWidth / 2, handleY + 5)
        ]));
      }
    });
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:bagWidth, width:depth, height:bagHeight },
      structureMeta:{
        kind:"paper-bag",
        exact3D:true,
        bodyPanels:panels.slice(),
        bagHeight,
        topFold,
        bottomDepth:bottom,
        handleStyle:String(definition.handleStyle || "die-cut"),
        handleWidth,
        glueFlap:g
      }
    };
  }

  function envelopeGeometry(options) {
    const { length:l, width:w, height:h, thickness:t, glueFlap:g, definition } = options;
    const depth = Math.max(12, h + t * 2);
    const bodyX = depth;
    const bodyY = depth;
    const cutPaths = [closeBoxPath([
      boxPoint(bodyX, 0), boxPoint(bodyX + l, 0),
      boxPoint(bodyX + l, bodyY), boxPoint(bodyX + l + depth, bodyY),
      boxPoint(bodyX + l + depth + g, bodyY + w / 2), boxPoint(bodyX + l + depth, bodyY + w),
      boxPoint(bodyX + l, bodyY + w), boxPoint(bodyX + l, bodyY + w + depth),
      boxPoint(bodyX + l / 2, bodyY + w + depth + Math.min(35, w * .24)),
      boxPoint(bodyX, bodyY + w + depth), boxPoint(bodyX, bodyY + w),
      boxPoint(0, bodyY + w), boxPoint(depth * .22, bodyY + w / 2), boxPoint(0, bodyY),
      boxPoint(bodyX, bodyY)
    ])];
    const foldPaths = [
      boxLine(bodyX, bodyY, bodyX + l, bodyY),
      boxLine(bodyX, bodyY + w, bodyX + l, bodyY + w),
      boxLine(bodyX, bodyY, bodyX, bodyY + w),
      boxLine(bodyX + l, bodyY, bodyX + l, bodyY + w)
    ];
    if (definition.tearLine) {
      /*
       * “平粘盒(带易撕线)”的易撕刀必须贯穿成品主体，而不是只存在
       * 于名称或备注。轻微锯齿让导出的刀线与普通平粘盒保持唯一。
       */
      const tearY = bodyY + w * .54;
      const inset = Math.max(8, l * .08);
      const start = bodyX + inset;
      const end = bodyX + l - inset;
      const count = 16;
      cutPaths.push(Array.from({ length:count + 1 }, (_item, index) => {
        const ratio = index / count;
        const wave = index % 2 ? Math.min(1.8, w * .018) : 0;
        return boxPoint(start + (end - start) * ratio, tearY + wave);
      }));
    }
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:l, width:w, height:depth },
      structureMeta:{
        kind:"envelope",
        exact3D:true,
        body:{ length:l, width:w, depth },
        sideFlap:depth,
        sealFlap:Math.min(35, w * .24),
        glueFlap:g,
        tearLine:Boolean(definition.tearLine)
      }
    };
  }

  function pillowGeometry(options) {
    const { length:l, width:w, height:h, glueFlap:g, thickness:t } = options;
    const panel = w + h + t * 2;
    const x0 = g;
    const x1 = x0 + panel;
    const x2 = x1 + panel;
    const bow = Math.max(12, Math.min(l * .18, panel * .44));
    const cutPaths = [closeBoxPath([
      boxPoint(0, bow), boxPoint(x0, 0), boxPoint(x2, 0),
      boxPoint(x2 + bow, l / 2), boxPoint(x2, l),
      boxPoint(x0, l), boxPoint(0, l - bow)
    ])];
    const foldPaths = [
      boxLine(x0, 0, x0, l),
      boxLine(x1, 0, x1, l),
      [boxPoint(x0, 0), boxPoint(x0 + panel * .18, bow), boxPoint(x0 + panel * .5, l / 2), boxPoint(x0 + panel * .18, l - bow), boxPoint(x0, l)],
      [boxPoint(x2, 0), boxPoint(x2 - panel * .18, bow), boxPoint(x2 - panel * .5, l / 2), boxPoint(x2 - panel * .18, l - bow), boxPoint(x2, l)]
    ];
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:l, width:w + t * 2, height:h + t },
      structureMeta:{
        kind:"pillow",
        exact3D:true,
        bodyLength:l,
        panelWidth:panel,
        finishedWidth:w + t * 2,
        finishedDepth:h + t,
        endBow:bow,
        glueFlap:g
      }
    };
  }

  function gableGeometry(options) {
    const { length:l, width:w, height:h, glueFlap:g, thickness:t } = options;
    const longPanel = l + t * 2;
    const shortPanel = w + t * 2;
    const panels = [longPanel, shortPanel, longPanel, shortPanel];
    const xs = [g];
    panels.forEach(panel => xs.push(xs[xs.length - 1] + panel));
    const ridgeRise = Math.max(24, Math.min(60, shortPanel * .62));
    const roofSlope = Math.hypot(shortPanel / 2, ridgeRise);
    const handle = Math.max(18, Math.min(38, longPanel * .3));
    const bottom = Math.max(18, shortPanel * .68);
    const y0 = roofSlope + handle;
    const y1 = y0 + h + t;
    const points = [boxPoint(0, y0 + 4), boxPoint(g, y0)];
    panels.forEach((panel, index) => {
      const center = (xs[index] + xs[index + 1]) / 2;
      if (index % 2 === 0) {
        points.push(boxPoint(xs[index], y0), boxPoint(xs[index] + panel * .18, y0 - roofSlope));
        points.push(boxPoint(center - handle / 2, y0 - roofSlope), boxPoint(center - handle / 2, 0));
        points.push(boxPoint(center + handle / 2, 0), boxPoint(center + handle / 2, y0 - roofSlope));
        points.push(boxPoint(xs[index + 1] - panel * .18, y0 - roofSlope), boxPoint(xs[index + 1], y0));
      } else {
        points.push(boxPoint(xs[index], y0), boxPoint(center, y0 - ridgeRise), boxPoint(xs[index + 1], y0));
      }
    });
    points.push(boxPoint(xs[4], y1));
    for (let index = 3; index >= 0; index -= 1) points.push(...cartonFlapPoints(xs[index], xs[index + 1], y1, bottom, 1, index % 2 ? "dust" : "tuck").reverse());
    points.push(boxPoint(g, y1), boxPoint(0, y1 - 4));
    const foldPaths = [
      boxLine(g, y0, g, y1), ...xs.slice(2,5).map(x => boxLine(x,y0,x,y1)),
      boxLine(g,y0,xs[4],y0), boxLine(g,y1,xs[4],y1),
      ...panels.map((panel,index) => {
        const topY = y0 - (index % 2 === 0 ? roofSlope : ridgeRise);
        return boxLine(xs[index] + panel * .18, topY, xs[index + 1] - panel * .18, topY);
      })
    ];
    const cutPaths = [closeBoxPath(points)];
    [0,2].forEach(index => {
      const center = (xs[index] + xs[index + 1]) / 2;
      cutPaths.push(closeBoxPath([
        boxPoint(center - handle * .28, y0 - roofSlope - handle * .65),
        boxPoint(center + handle * .28, y0 - roofSlope - handle * .65),
        boxPoint(center + handle * .28, y0 - roofSlope - handle * .42),
        boxPoint(center - handle * .28, y0 - roofSlope - handle * .42)
      ]));
    });
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:longPanel, width:shortPanel, height:h + t },
      structureMeta:{
        kind:"gable",
        exact3D:true,
        bodyPanels:panels.slice(),
        bodyHeight:h + t,
        ridgeRise,
        roofSlope,
        handleHeight:handle,
        handleWidth:handle,
        bottomDepth:bottom,
        glueFlap:g
      }
    };
  }

  function polygonCartonGeometry(options) {
    const { length:l, height:h, thickness:t, glueFlap:g, definition } = options;
    const sides = Math.max(5, Number(definition.sides) || 6);
    const panel = l + t * 2;
    const flap = Math.max(18, panel * .55);
    const bodyTop = flap;
    const bodyBottom = bodyTop + h + t;
    const total = g + panel * sides;
    const cutPaths = [closeBoxPath([
      boxPoint(0, bodyTop + 4), boxPoint(g, bodyTop),
      ...Array.from({ length:sides }, (_, index) => cartonFlapPoints(g + index * panel, g + (index + 1) * panel, bodyTop, flap, -1, index % 2 ? "dust" : "tuck")).flat(),
      boxPoint(total, bodyBottom),
      ...Array.from({ length:sides }, (_, rawIndex) => {
        const index = sides - 1 - rawIndex;
        return cartonFlapPoints(g + index * panel, g + (index + 1) * panel, bodyBottom, flap, 1, index % 2 ? "dust" : "tuck").reverse();
      }).flat(),
      boxPoint(g, bodyBottom), boxPoint(0, bodyBottom - 4)
    ])];
    const foldPaths = [
      boxLine(g, bodyTop, g, bodyBottom),
      ...Array.from({ length:sides }, (_, index) => boxLine(g + (index + 1) * panel, bodyTop, g + (index + 1) * panel, bodyBottom)),
      boxLine(g, bodyTop, total, bodyTop), boxLine(g, bodyBottom, total, bodyBottom)
    ];
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:panel, width:panel, height:h + t },
      structureMeta:{
        kind:"polygon-carton",
        exact3D:true,
        sides,
        sideWidth:panel,
        bodyHeight:h + t,
        flapDepth:flap,
        glueFlap:g
      }
    };
  }

  function trayGeometry(options) {
    const { length:l, width:w, height:h, thickness:t } = options;
    const wall = h + t;
    const x0 = wall;
    const x1 = x0 + l + t * 2;
    const y0 = wall;
    const y1 = y0 + w + t * 2;
    const ear = Math.max(10, Math.min(24, wall * .65));
    const cutPaths = [closeBoxPath([
      boxPoint(x0, 0), boxPoint(x1, 0),
      boxPoint(x1, y0 - ear), boxPoint(x1 + wall, y0 - ear), boxPoint(x1 + wall, y1 + ear),
      boxPoint(x1, y1 + ear), boxPoint(x1, y1 + wall),
      boxPoint(x0, y1 + wall), boxPoint(x0, y1 + ear),
      boxPoint(0, y1 + ear), boxPoint(0, y0 - ear),
      boxPoint(x0, y0 - ear)
    ])];
    const foldPaths = [
      boxLine(x0,y0,x1,y0), boxLine(x0,y1,x1,y1),
      boxLine(x0,y0,x0,y1), boxLine(x1,y0,x1,y1),
      boxLine(0,y0,x0,y0), boxLine(0,y1,x0,y1),
      boxLine(x1,y0,x1+wall,y0), boxLine(x1,y1,x1+wall,y1)
    ];
    return {
      cutPaths,
      foldPaths,
      partLabels:[],
      manufacturing:{ length:l + t * 2, width:w + t * 2, height:wall },
      structureMeta:{
        kind:"tray",
        exact3D:true,
        base:{ length:l + t * 2, width:w + t * 2 },
        wallHeight:wall,
        cornerEar:ear
      }
    };
  }

  function offsetClosedBoxPath(path, distance) {
    const source = (path || []).map(point => boxPoint(point.x, point.y));
    if (source.length > 1) {
      const first = source[0];
      const last = source[source.length - 1];
      if (Math.abs(first.x - last.x) < 1e-8 && Math.abs(first.y - last.y) < 1e-8) source.pop();
    }
    const points = source.filter((point, index) => {
      if (!index) return true;
      const previous = source[index - 1];
      return Math.abs(point.x - previous.x) > 1e-8 || Math.abs(point.y - previous.y) > 1e-8;
    });
    if (points.length < 3 || !(distance > 0)) return closeBoxPath(points);
    const area = points.reduce((sum, point, index) => {
      const next = points[(index + 1) % points.length];
      return sum + point.x * next.y - next.x * point.y;
    }, 0) / 2;
    const orientation = area >= 0 ? 1 : -1;
    const shifted = points.map((point, index) => {
      const next = points[(index + 1) % points.length];
      const dx = next.x - point.x;
      const dy = next.y - point.y;
      const length = Math.hypot(dx, dy) || 1;
      const nx = orientation * dy / length;
      const ny = orientation * -dx / length;
      return {
        a:boxPoint(point.x + nx * distance, point.y + ny * distance),
        b:boxPoint(next.x + nx * distance, next.y + ny * distance)
      };
    });
    const intersection = (first, second, fallback) => {
      const x1 = first.a.x, y1 = first.a.y;
      const x2 = first.b.x, y2 = first.b.y;
      const x3 = second.a.x, y3 = second.a.y;
      const x4 = second.b.x, y4 = second.b.y;
      const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
      if (Math.abs(denominator) < 1e-8) return fallback;
      const determinant1 = x1 * y2 - y1 * x2;
      const determinant2 = x3 * y4 - y3 * x4;
      const point = boxPoint(
        (determinant1 * (x3 - x4) - (x1 - x2) * determinant2) / denominator,
        (determinant1 * (y3 - y4) - (y1 - y2) * determinant2) / denominator
      );
      return Math.hypot(point.x - fallback.x, point.y - fallback.y) > distance * 6
        ? fallback
        : point;
    };
    const output = points.map((point, index) => {
      const previous = shifted[(index - 1 + shifted.length) % shifted.length];
      const current = shifted[index];
      const fallback = boxPoint(
        (previous.b.x + current.a.x) / 2,
        (previous.b.y + current.a.y) / 2
      );
      return intersection(previous, current, fallback);
    });
    return closeBoxPath(output);
  }

  function calculateBoxDieline(input) {
    const options = normalizeBoxTemplateOptions(input);
    let geometry;
    const family = options.definition.family;
    if (family === "folding-carton") geometry = foldingCartonGeometry(options);
    else if (family === "mailer") geometry = mailerGeometry(options);
    else if (family === "lid-base") geometry = lidBaseGeometry(options);
    else if (family === "sleeve") geometry = sleeveGeometry(options);
    else if (family === "paper-bag") geometry = paperBagGeometry(options);
    else if (family === "envelope") geometry = envelopeGeometry(options);
    else if (family === "pillow") geometry = pillowGeometry(options);
    else if (family === "gable") geometry = gableGeometry(options);
    else if (family === "polygon-carton") geometry = polygonCartonGeometry(options);
    else if (family === "tray") geometry = trayGeometry(options);
    else throw new RangeError("当前盒型结构算法尚未安装");
    geometry = applyShengdaReferenceGeometry(options, geometry);
    const allPoints = [...geometry.cutPaths, ...geometry.foldPaths].flat();
    const pointBounds = {
      minX:Math.min(...allPoints.map(point => point.x)),
      minY:Math.min(...allPoints.map(point => point.y)),
      maxX:Math.max(...allPoints.map(point => point.x)),
      maxY:Math.max(...allPoints.map(point => point.y))
    };
    const exactBounds = (geometry.cutSvgPaths || [])
      .map(item => svgPathExactBounds(item.d, Number(item.scale) || 1))
      .filter(Boolean);
    const minX = Math.min(pointBounds.minX, ...exactBounds.map(bounds => bounds.minX));
    const minY = Math.min(pointBounds.minY, ...exactBounds.map(bounds => bounds.minY));
    const maxX = Math.max(pointBounds.maxX, ...exactBounds.map(bounds => bounds.maxX));
    const maxY = Math.max(pointBounds.maxY, ...exactBounds.map(bounds => bounds.maxY));
    const padding = Math.max(5, options.bleed + 3);
    const translate = path => path.map(point => boxPoint(point.x - minX + padding, point.y - minY + padding));
    const widthMm = Number((maxX - minX + padding * 2).toFixed(3));
    const heightMm = Number((maxY - minY + padding * 2).toFixed(3));
    const flatWidthMm = Number((
      Number.isFinite(Number(geometry.reportedFlatDimensions?.width))
        ? Number(geometry.reportedFlatDimensions.width)
        : maxX - minX
    ).toFixed(3));
    const flatHeightMm = Number((
      Number.isFinite(Number(geometry.reportedFlatDimensions?.height))
        ? Number(geometry.reportedFlatDimensions.height)
        : maxY - minY
    ).toFixed(3));
    if (widthMm > 3000 || heightMm > 3000) throw new RangeError("展开尺寸不能超过3000×3000mm，请缩小成品尺寸或改用分体结构");
    const cutPaths = geometry.cutPaths.map(translate);
    const foldPaths = geometry.foldPaths.map(translate);
    const cutSvgPaths = (geometry.cutSvgPaths || []).map(item => ({
      d:String(item.d || ""),
      scale:Number(item.scale) || 1,
      translateX:Number((-minX + padding).toFixed(6)),
      translateY:Number((-minY + padding).toFixed(6))
    })).filter(item => item.d);
    const netTopology = buildBoxNetTopology(cutPaths, foldPaths);
    const labels = (geometry.partLabels || []).map(label => ({ ...label, x:label.x - minX + padding, y:label.y - minY + padding }));
    const outerCutPathCount = Math.max(1, Number(geometry.outerCutPathCount) || 1);
    const hasExactBleedPath = options.bleed > 0 && Boolean(geometry.bleedSvgPaths?.length);
    const nativeBleedPaths = options.bleed > 0 && Array.isArray(geometry.bleedPaths)
      ? geometry.bleedPaths.map(translate)
      : [];
    const bleedPaths = options.bleed > 0
      ? hasExactBleedPath
        ? nativeBleedPaths
        : cutPaths.slice(0, outerCutPathCount).map(path => offsetClosedBoxPath(path, options.bleed))
      : [];
    const bleedSvgPaths = options.bleed > 0
      ? (geometry.bleedSvgPaths || []).map(item => ({
          d:String(item.d || ""),
          scale:Number(item.scale) || 1,
          translateX:Number((-minX + padding).toFixed(6)),
          translateY:Number((-minY + padding).toFixed(6))
        })).filter(item => item.d)
      : [];
    const result = {
      options,
      widthMm,
      heightMm,
      flatWidthMm,
      flatHeightMm,
      cutPaths,
      cutSvgPaths,
      foldPaths,
      bleedPaths,
      bleedSvgPaths,
      labels,
      manufacturing:geometry.manufacturing,
      structureMeta:{ ...(geometry.structureMeta || {}), outerCutPathCount, netTopology },
      warnings:[
        options.definition.dimensionBasis === "manufacturing"
          ? `结构编号 ${options.definition.code}；已按标准制造尺寸生成，正式生产前必须由结构工程师按材料、纹向、糊盒设备和实物打样复核。`
          : `结构编号 ${options.definition.code}；已按成品内尺寸加入纸厚补偿，正式生产前必须由结构工程师按材料、纹向、糊盒设备和实物打样复核。`,
        "切线、折线和出血线已分层；桥位、清废孔、压痕底模和刀线补偿仍需刀模师确认。"
      ]
    };
    result.geometryBounds = boxDielineGeometryBounds(result);
    result.geometrySignature = boxDielineGeometrySignature(result);
    return result;
  }

  const svgPolyline = (path, attributes) => `<polyline points="${path.map(point => `${Number(point.x.toFixed(3))},${Number(point.y.toFixed(3))}`).join(" ")}" ${attributes}/>`;

  const geometryRound = value => Number(Number(value || 0).toFixed(4));

  function boxDielineGeometryBounds(result) {
    const paths = [
      ...(result?.cutSvgPaths?.length ? [] : (result?.cutPaths || [])),
      ...(result?.foldPaths || []),
      ...(result?.bleedSvgPaths?.length ? [] : (result?.bleedPaths || []))
    ];
    const points = paths.flat();
    const bounds = [...(result?.cutSvgPaths || []), ...(result?.bleedSvgPaths || [])]
      .map(item => {
        try {
          return svgPathExactBounds(item.d, Number(item.scale) || 1, Number(item.translateX) || 0, Number(item.translateY) || 0);
        } catch (_) {
          return null;
        }
      })
      .filter(Boolean);
    if (points.length) {
      bounds.push({
        minX:Math.min(...points.map(point => Number(point.x))),
        minY:Math.min(...points.map(point => Number(point.y))),
        maxX:Math.max(...points.map(point => Number(point.x))),
        maxY:Math.max(...points.map(point => Number(point.y)))
      });
    }
    if (!bounds.length) return { minX:0, minY:0, maxX:0, maxY:0, width:0, height:0 };
    const minX = Math.min(...bounds.map(item => item.minX));
    const minY = Math.min(...bounds.map(item => item.minY));
    const maxX = Math.max(...bounds.map(item => item.maxX));
    const maxY = Math.max(...bounds.map(item => item.maxY));
    return {
      minX:geometryRound(minX),
      minY:geometryRound(minY),
      maxX:geometryRound(maxX),
      maxY:geometryRound(maxY),
      width:geometryRound(maxX - minX),
      height:geometryRound(maxY - minY)
    };
  }

  function geometryHash(value) {
    const text = String(value || "");
    let hash = 2166136261;
    for (let index = 0; index < text.length; index += 1) {
      hash ^= text.charCodeAt(index);
      hash = Math.imul(hash, 16777619);
    }
    return (hash >>> 0).toString(16).padStart(8, "0");
  }

  function boxDielineGeometrySignature(result) {
    const normalizePaths = paths => (paths || []).map(path => path.map(point => [
      geometryRound(point.x), geometryRound(point.y)
    ]));
    return `jl-dieline-v1-${geometryHash(JSON.stringify({
      templateId:result?.options?.definition?.id || "",
      cut:normalizePaths(result?.cutPaths),
      exact:(result?.cutSvgPaths || []).map(item => [
        String(item.d || ""),
        geometryRound(item.scale),
        geometryRound(item.translateX),
        geometryRound(item.translateY)
      ]),
      fold:normalizePaths(result?.foldPaths),
      bleed:normalizePaths(result?.bleedPaths),
      exactBleed:(result?.bleedSvgPaths || []).map(item => [
        String(item.d || ""),
        geometryRound(item.scale),
        geometryRound(item.translateX),
        geometryRound(item.translateY)
      ])
    }))}`;
  }

  function boxDielinePreviewContract(result) {
    if (!result?.cutPaths?.length) throw new TypeError("请先生成盒型刀板");
    const bounds = result.geometryBounds || boxDielineGeometryBounds(result);
    const signature = result.geometrySignature || boxDielineGeometrySignature(result);
    // Preview framing must be derived from the actual structure, not from the
    // historical export canvas.  A fixed proportional margin keeps very wide
    // and very tall structures fully visible in every UI surface.
    const padding = geometryRound(Math.max(
      2,
      Math.min(14, Math.max(bounds.width, bounds.height) * .025)
    ));
    const viewBox = [
      geometryRound(bounds.minX - padding),
      geometryRound(bounds.minY - padding),
      geometryRound(bounds.width + padding * 2),
      geometryRound(bounds.height + padding * 2)
    ];
    return {
      templateId:String(result.options?.definition?.id || ""),
      signature,
      bounds,
      padding,
      viewBox,
      viewBoxText:viewBox.join(" "),
      cutPathCount:(result.cutSvgPaths?.length || result.cutPaths.length),
      nativePathCount:(result.cutSvgPaths?.length || 0),
      foldPathCount:result.foldPaths.length,
      bleedPathCount:(result.bleedSvgPaths?.length || result.bleedPaths.length),
      segmentCount:[...(result.cutPaths || []), ...(result.foldPaths || [])]
        .reduce((sum, path) => sum + Math.max(0, path.length - 1), 0)
    };
  }

  function formatDimensionMillimeters(value) {
    const rounded = Math.round(Number(value) * 10) / 10;
    return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1)} mm`;
  }

  function boxDielineDimensionGuides(result, contract) {
    const bounds = contract.bounds;
    const centerX = (bounds.minX + bounds.maxX) / 2;
    const centerY = (bounds.minY + bounds.maxY) / 2;
    const straightSegments = (result.foldPaths || []).flatMap((path, pathIndex) => (
      (path || []).slice(1).map((point, pointIndex) => {
        const previous = path[pointIndex];
        const deltaX = Number(point.x) - Number(previous.x);
        const deltaY = Number(point.y) - Number(previous.y);
        const horizontal = Math.abs(deltaY) < .08 && Math.abs(deltaX) > .5;
        const vertical = Math.abs(deltaX) < .08 && Math.abs(deltaY) > .5;
        if (!horizontal && !vertical) return null;
        return {
          id:`${pathIndex}-${pointIndex}`,
          orientation:horizontal ? "horizontal" : "vertical",
          x1:Number(previous.x),
          y1:Number(previous.y),
          x2:Number(point.x),
          y2:Number(point.y),
          length:Math.hypot(deltaX, deltaY)
        };
      })
    )).filter(Boolean);
    const requested = [
      { key:"length", label:"长", value:Number(result.options?.length), preferred:"horizontal" },
      { key:"width", label:"宽", value:Number(result.options?.width), preferred:"horizontal" },
      { key:"height", label:"高", value:Number(result.options?.height), preferred:"vertical" }
    ].filter(item => Number.isFinite(item.value) && item.value > 0);
    const used = new Set();
    const guides = requested.map((dimension, dimensionIndex) => {
      const ranked = straightSegments
        .filter(segment => !used.has(segment.id))
        .map(segment => {
          const absoluteDifference = Math.abs(segment.length - dimension.value);
          const relativeDifference = absoluteDifference / Math.max(1, dimension.value);
          const orientationPenalty = segment.orientation === dimension.preferred ? 0 : .045;
          const centerDistance = segment.orientation === "horizontal"
            ? Math.abs(((segment.x1 + segment.x2) / 2) - centerX) / Math.max(1, bounds.width)
            : Math.abs(((segment.y1 + segment.y2) / 2) - centerY) / Math.max(1, bounds.height);
          return {
            segment,
            score:relativeDifference + orientationPenalty + centerDistance * .012,
            relativeDifference,
            absoluteDifference
          };
        })
        .sort((first, second) => first.score - second.score);
      const matched = ranked.find(item => (
        item.relativeDifference <= .08
        || item.absoluteDifference <= Math.max(1.2, dimension.value * .02)
      ));
      const fontSize = Math.max(4.2, Math.min(8.5, Math.max(bounds.width, bounds.height) * .017));
      const arrowSize = Math.max(2.3, Math.min(5.2, fontSize * .58));
      if (!matched) {
        const vertical = dimension.preferred === "vertical" && dimension.value <= bounds.height * .84;
        if (vertical) {
          const x = bounds.minX + bounds.width * (.6 + dimensionIndex * .08);
          return {
            ...dimension,
            measured:dimension.value,
            orientation:"vertical",
            x1:x,
            y1:centerY - dimension.value / 2,
            x2:x,
            y2:centerY + dimension.value / 2,
            textX:x,
            textY:centerY,
            fontSize,
            arrowSize
          };
        }
        const displayLength = Math.min(dimension.value, bounds.width * .84);
        const y = bounds.minY + bounds.height * (.36 + dimensionIndex * .17);
        return {
          ...dimension,
          measured:dimension.value,
          orientation:"horizontal",
          x1:centerX - displayLength / 2,
          y1:y,
          x2:centerX + displayLength / 2,
          y2:y,
          textX:centerX,
          textY:y - fontSize * .62,
          fontSize,
          arrowSize
        };
      }
      const segment = matched.segment;
      used.add(segment.id);
      const inwardFactor = .44 + dimensionIndex * .11;
      if (segment.orientation === "horizontal") {
        const direction = centerY >= segment.y1 ? 1 : -1;
        const offset = Math.min(
          Math.max(7, bounds.height * .075 * inwardFactor),
          Math.max(5, Math.abs(centerY - segment.y1) * .72)
        );
        const y = segment.y1 + direction * offset;
        const x1 = Math.min(segment.x1, segment.x2);
        const x2 = Math.max(segment.x1, segment.x2);
        return {
          ...dimension,
          measured:segment.length,
          orientation:"horizontal",
          x1, y1:y, x2, y2:y,
          textX:(x1 + x2) / 2,
          textY:y - fontSize * .62,
          fontSize,
          arrowSize
        };
      }
      const direction = centerX >= segment.x1 ? 1 : -1;
      const offset = Math.min(
        Math.max(7, bounds.width * .075 * inwardFactor),
        Math.max(5, Math.abs(centerX - segment.x1) * .72)
      );
      const x = segment.x1 + direction * offset;
      const y1 = Math.min(segment.y1, segment.y2);
      const y2 = Math.max(segment.y1, segment.y2);
      return {
        ...dimension,
        measured:segment.length,
        orientation:"vertical",
        x1:x, y1, x2:x, y2,
        textX:x,
        textY:(y1 + y2) / 2,
        fontSize,
        arrowSize
      };
    }).filter(Boolean);
    const markup = guides.map(guide => {
      const text = `${guide.label} ${formatDimensionMillimeters(guide.measured)}`;
      if (guide.orientation === "horizontal") {
        return `<g data-role="dimension" data-dimension="${guide.key}" aria-label="${escapeXml(text)}"><line x1="${geometryRound(guide.x1)}" y1="${geometryRound(guide.y1)}" x2="${geometryRound(guide.x2)}" y2="${geometryRound(guide.y2)}" stroke="#087cff" stroke-width=".7" vector-effect="non-scaling-stroke"/><path d="M ${geometryRound(guide.x1)} ${geometryRound(guide.y1)} l ${geometryRound(guide.arrowSize)} ${geometryRound(-guide.arrowSize * .48)} v ${geometryRound(guide.arrowSize * .96)} z M ${geometryRound(guide.x2)} ${geometryRound(guide.y2)} l ${geometryRound(-guide.arrowSize)} ${geometryRound(-guide.arrowSize * .48)} v ${geometryRound(guide.arrowSize * .96)} z" fill="#087cff"/><text x="${geometryRound(guide.textX)}" y="${geometryRound(guide.textY)}" text-anchor="middle" fill="#087cff" stroke="#fff" stroke-width="${geometryRound(guide.fontSize * .34)}" paint-order="stroke" stroke-linejoin="round" font-size="${geometryRound(guide.fontSize)}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-weight="700">${escapeXml(text)}</text></g>`;
      }
      return `<g data-role="dimension" data-dimension="${guide.key}" aria-label="${escapeXml(text)}"><line x1="${geometryRound(guide.x1)}" y1="${geometryRound(guide.y1)}" x2="${geometryRound(guide.x2)}" y2="${geometryRound(guide.y2)}" stroke="#087cff" stroke-width=".7" vector-effect="non-scaling-stroke"/><path d="M ${geometryRound(guide.x1)} ${geometryRound(guide.y1)} l ${geometryRound(-guide.arrowSize * .48)} ${geometryRound(guide.arrowSize)} h ${geometryRound(guide.arrowSize * .96)} z M ${geometryRound(guide.x2)} ${geometryRound(guide.y2)} l ${geometryRound(-guide.arrowSize * .48)} ${geometryRound(-guide.arrowSize)} h ${geometryRound(guide.arrowSize * .96)} z" fill="#087cff"/><text x="${geometryRound(guide.textX)}" y="${geometryRound(guide.textY)}" text-anchor="middle" dominant-baseline="middle" fill="#087cff" stroke="#fff" stroke-width="${geometryRound(guide.fontSize * .34)}" paint-order="stroke" stroke-linejoin="round" font-size="${geometryRound(guide.fontSize)}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-weight="700">${escapeXml(text)}</text></g>`;
    }).join("");
    return { guides, markup };
  }

  function renderBoxDielineSvg(result, options = {}) {
    if (!result?.cutPaths?.length) throw new TypeError("请先生成盒型刀板");
    const contract = boxDielinePreviewContract(result);
    const stroke = Math.max(.15, Math.min(.5, Math.max(result.widthMm, result.heightMm) / 1800));
    const labels = options.showLabels === false ? "" : result.labels.map(label => `<text x="${label.x}" y="${label.y}" text-anchor="middle" dominant-baseline="middle" fill="#4b5563" font-size="${Math.max(4, Math.min(12, result.widthMm / 55))}">${escapeXml(label.text)}</text>`).join("");
    const accessibleName = `${result.options.projectName} · ${result.options.definition.name}参数化刀板`;
    const cutMarkup = result.cutSvgPaths?.length
      ? result.cutSvgPaths.map(item => `<path d="${escapeXml(item.d)}" transform="matrix(${item.scale} 0 0 ${item.scale} ${item.translateX} ${item.translateY})" fill="none" stroke="#111827" stroke-width="${stroke}" stroke-linejoin="round" vector-effect="non-scaling-stroke" data-role="cut"/>`).join("")
      : result.cutPaths.map(path => svgPolyline(path, `fill="none" stroke="#111827" stroke-width="${stroke}" stroke-linejoin="round" vector-effect="non-scaling-stroke" data-role="cut"`)).join("");
    const bleedMarkup = result.bleedSvgPaths?.length
      ? result.bleedSvgPaths.map(item => `<path d="${escapeXml(item.d)}" transform="matrix(${item.scale} 0 0 ${item.scale} ${item.translateX} ${item.translateY})" fill="none" stroke="#3659d9" stroke-width="${stroke}" vector-effect="non-scaling-stroke" data-role="bleed"/>`).join("")
      : result.bleedPaths.map(path => svgPolyline(path, `fill="none" stroke="#3659d9" stroke-width="${stroke}" stroke-dasharray="3 2" data-role="bleed"`)).join("");
    const responsive = options.responsive === true;
    const dimensionsVisible = options.showDimensions !== false;
    const dimensionContract = dimensionsVisible
      ? boxDielineDimensionGuides(result, contract)
      : { guides:[], markup:"" };
    const dimensionMarkup = dimensionContract.markup;
    const dimensions = responsive
      ? 'width="100%" height="100%"'
      : `width="${contract.viewBox[2]}mm" height="${contract.viewBox[3]}mm"`;
    return `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" ${dimensions} viewBox="${contract.viewBoxText}" preserveAspectRatio="xMidYMid meet" data-template-id="${escapeXml(contract.templateId)}" data-geometry-signature="${contract.signature}" data-geometry-bounds="${escapeXml(JSON.stringify(contract.bounds))}" data-cut-path-count="${contract.cutPathCount}" data-native-path-count="${contract.nativePathCount}" data-fold-path-count="${contract.foldPathCount}" data-bleed-path-count="${contract.bleedPathCount}" data-dimension-count="${dimensionContract.guides.length}" role="img" aria-label="${escapeXml(accessibleName)}"><title>${escapeXml(result.options.projectName)}</title><desc>${escapeXml(result.options.definition.name)}参数化刀板，黑色切线、红色折线、蓝色出血线及毫米尺寸标注</desc><rect x="${contract.viewBox[0]}" y="${contract.viewBox[1]}" width="${contract.viewBox[2]}" height="${contract.viewBox[3]}" fill="#fff"/>${bleedMarkup}${cutMarkup}${result.foldPaths.map(path => svgPolyline(path, `fill="none" stroke="#dc2626" stroke-width="${stroke}" stroke-dasharray="4 2" data-role="fold"`)).join("")}${dimensionMarkup}${labels}</svg>`;
  }

  function renderBoxDielinePreviewSvg(result, options = {}) {
    return renderBoxDielineSvg(result, {
      ...options,
      responsive:true,
      showLabels:options.showLabels === true
    });
  }

  function boxDielineToVectorDocument(result) {
    if (!result?.cutPaths?.length) throw new TypeError("请先生成盒型刀板");
    const contract = boxDielinePreviewContract(result);
    const shiftX = -contract.viewBox[0];
    const shiftY = -contract.viewBox[1];
    const shifted = paths => paths.map(points => ({
      points:points.map(point => boxPoint(point.x + shiftX, point.y + shiftY))
    }));
    return {
      width:contract.viewBox[2],
      height:contract.viewBox[3],
      options:{ smoothing:0 },
      geometryContract:{
        templateId:contract.templateId,
        geometrySignature:contract.signature,
        geometryBounds:contract.bounds,
        viewBox:contract.viewBox,
        cutPathCount:contract.cutPathCount,
        nativePathCount:contract.nativePathCount,
        foldPathCount:contract.foldPathCount
      },
      layers:[
        {
          id:"dieline", name:"DIELINE", role:"dieline", color:"#fff200", cmyk:[0, 0, 1, 0], spotName:"DIELINE",
          contours:[],
          strokes:result.cutSvgPaths?.length ? [] : result.cutPaths.map(points => ({ points })),
          nativeStrokePaths:(result.cutSvgPaths || []).map(item => ({
            d:svgPathToCubicPathData(item.d),
            transform:[item.scale, 0, 0, item.scale, item.translateX + shiftX, item.translateY + shiftY]
          })),
          strokeWidthMm:.25
        },
        {
          id:"crease", name:"CREASE", role:"guide", color:"#dc2626", cmyk:[0, 1, 1, 0],
          contours:[], strokes:shifted(result.foldPaths), strokeWidthMm:.25
        },
        {
          id:"bleed", name:"BLEED", role:"guide", color:"#3659d9", cmyk:[.75, .55, 0, 0],
          contours:[],
          strokes:result.bleedSvgPaths?.length ? [] : shifted(result.bleedPaths),
          nativeStrokePaths:(result.bleedSvgPaths || []).map(item => ({
            d:svgPathToCubicPathData(item.d),
            transform:[item.scale, 0, 0, item.scale, item.translateX + shiftX, item.translateY + shiftY]
          })),
          strokeWidthMm:.15
        }
      ]
    };
  }

  function verifyBoxTemplateGeometryContracts(dimensionProfiles = []) {
    const profiles = dimensionProfiles.length ? dimensionProfiles : [
      { lengthScale:1.17, widthScale:.91, heightScale:1.08 },
      { lengthScale:.83, widthScale:1.24, heightScale:.76 }
    ];
    return BOX_TEMPLATE_DEFINITIONS.flatMap(definition => profiles.map((profile, profileIndex) => {
      const [baseLength, baseWidth, baseHeight, material] = definition.preset;
      const lengthMm = Math.max(12, Number((baseLength * profile.lengthScale).toFixed(3)));
      const widthMm = Math.max(8, Number((baseWidth * profile.widthScale).toFixed(3)));
      const heightMm = Math.max(8, Number((baseHeight * profile.heightScale).toFixed(3)));
      const thicknessMm = definition.referenceThicknessMm || BOX_MATERIAL_THICKNESS[material] || .38;
      const result = calculateBoxDieline({
        type:definition.id,
        lengthMm,
        widthMm,
        heightMm,
        material,
        thicknessMm,
        glueFlapMm:definition.presetGlueFlapMm || Math.max(12, Math.min(22, heightMm * .15)),
        bleedMm:3,
        clearanceMm:Math.max(.5, thicknessMm * 1.5),
        projectName:`${definition.name}-${profileIndex + 1}`
      });
      return {
        id:definition.id,
        profileIndex,
        result,
        contract:boxDielinePreviewContract(result),
        vector:boxDielineToVectorDocument(result)
      };
    }));
  }

  function browserController() {
    const boxPreviewCache = new Map();
    const localState = {
      layout:null, selected:null, signature:"", customerSignature:"", lastError:null, initialized:false,
      dieSource:null, boxDieline:null, boxViewer:null, pagehideBound:false,
      boxCatalogCategory:"卡盒", boxCatalogGroup:"双插盒", boxCatalogSearch:""
    };
    const byId = id => typeof document === "undefined" ? null : document.getElementById(id);
    const numberValue = id => byId(id)?.value;
    const selectValue = id => String(byId(id)?.value || "");
    const defaultSettings = {
      boardWidthMm:1050, boardHeightMm:750,
      marginsMm:{ top:17, right:17, bottom:17, left:17 },
      gripperMm:12, gripperEdge:"top", gapMm:{ x:5, y:5 },
      rotation:"allow", align:"center", strategy:"balanced"
    };

    function readSettings() {
      const saved = typeof root.__getDieLayoutSettings === "function" ? root.__getDieLayoutSettings() : {};
      const normalizedSaved = saved && typeof saved === "object" ? saved : {};
      const savedRotation = Number(normalizedSaved.sourceWorkflowVersion) >= 2 ? normalizedSaved.rotation : "allow";
      return { ...defaultSettings, ...normalizedSaved, rotation:savedRotation || "allow", marginsMm:{ ...defaultSettings.marginsMm, ...(normalizedSaved.marginsMm || {}) }, gapMm:{ ...defaultSettings.gapMm, ...(normalizedSaved.gapMm || {}) } };
    }

    function setValue(id, value) { const element = byId(id); if (element && value !== undefined && value !== null) element.value = value; }
    function setText(id, value) { const element = byId(id); if (element) element.textContent = String(value ?? ""); }
    function fillSettings(settings) {
      setValue("dieBoardWidth", settings.boardWidthMm); setValue("dieBoardHeight", settings.boardHeightMm);
      setValue("dieMarginTop", settings.marginsMm.top); setValue("dieMarginRight", settings.marginsMm.right);
      setValue("dieMarginBottom", settings.marginsMm.bottom); setValue("dieMarginLeft", settings.marginsMm.left);
      setValue("dieGripper", settings.gripperMm); setValue("dieGripperEdge", settings.gripperEdge);
      setValue("dieGapX", settings.gapMm.x); setValue("dieGapY", settings.gapMm.y);
      setValue("dieRotation", settings.rotation); setValue("dieAlign", settings.align); setValue("dieStrategy", settings.strategy);
    }

    function readForm() {
      return {
        projectName:byId("dieProjectName")?.value,
        boardWidthMm:numberValue("dieBoardWidth"), boardHeightMm:numberValue("dieBoardHeight"),
        partWidthMm:numberValue("diePartWidth"), partHeightMm:numberValue("diePartHeight"), quantity:numberValue("dieQuantity"),
        marginsMm:{ top:numberValue("dieMarginTop"), right:numberValue("dieMarginRight"), bottom:numberValue("dieMarginBottom"), left:numberValue("dieMarginLeft") },
        gripperMm:numberValue("dieGripper"), gripperEdge:selectValue("dieGripperEdge"),
        gapMm:{ x:numberValue("dieGapX"), y:numberValue("dieGapY") },
        rotation:selectValue("dieRotation"), align:selectValue("dieAlign"), strategy:selectValue("dieStrategy")
      };
    }

    function customerContextValues() {
      if (typeof root.__getDieLayoutCustomerContext !== "function") return null;
      try {
        const source = root.__getDieLayoutCustomerContext();
        if (!source || typeof source !== "object" || Array.isArray(source) || source instanceof Promise) return null;
        const projectName = String(source.projectName || "").trim();
        const identity = String(source.contextId || source.customerId || projectName || "").trim();
        const sourceLabel = String(source.sourceLabel || "当前客户资料").trim() || "当前客户资料";
        const missing = [];
        if (!projectName) missing.push("项目名称");
        if (!(Number(source.partWidthMm) > 0) || !(Number(source.partHeightMm) > 0)) missing.push("单模展开尺寸");
        if (!Number.isInteger(Number(source.quantity)) || Number(source.quantity) <= 0) missing.push("订单数量");
        if (missing.length) return { identity, projectName, sourceLabel, missing, ready:false };
        const normalized = normalizeOptions({
          ...readForm(),
          projectName,
          partWidthMm:source.partWidthMm,
          partHeightMm:source.partHeightMm,
          quantity:source.quantity
        });
        return {
          identity,
          sourceLabel,
          missing:[],
          ready:true,
          projectName:normalized.projectName,
          partWidthMm:roundMm(normalized.partWidth),
          partHeightMm:roundMm(normalized.partHeight),
          quantity:normalized.quantity
        };
      } catch (_) {
        return null;
      }
    }

    function setStatus(message, kind = "") {
      const element = byId("dieLayoutStatus");
      if (!element) return;
      element.textContent = message;
      element.dataset.kind = kind;
    }

    function candidateLabel(candidate) {
      if (candidate?.contourMode) {
        const rotations = Array.isArray(candidate.rotations) ? candidate.rotations : [];
        return rotations.some(rotation => rotation % 180 === 90) ? "真实刀线交叉 · 含90°" : "真实刀线交叉 · 保持纹向";
      }
      return candidate.orientation === 90 ? "旋转90°" : "保持纹向";
    }

    function renderCandidates() {
      const host = byId("dieCandidateList");
      if (!host || !localState.layout) return;
      host.innerHTML = localState.layout.candidates.map((candidate, index) => {
        const arrangement = candidate.contourMode ? `${candidate.capacityPerSheet}模/张 · 凹凸轮廓互嵌` : `${candidate.columns}列 × ${candidate.rows}行 · ${candidate.capacityPerSheet}模/张`;
        return `<button type="button" class="die-candidate${candidate.id === localState.selected?.id ? " is-active" : ""}" data-die-candidate="${candidate.id}"><span>${index === 0 ? "推荐" : "备选"} · ${candidateLabel(candidate)}</span><strong>${arrangement}</strong><em>${candidate.sheetsRequired}张 · 超产${candidate.overrun}个 · 有效利用率${candidate.utilization.effectiveBoardPercent}%</em></button>`;
      }).join("");
      host.querySelectorAll("[data-die-candidate]").forEach(button => button.addEventListener("click", () => selectCandidate(button.dataset.dieCandidate)));
    }

    function renderMetrics(layout) {
      const host = byId("dieMetrics");
      if (!host) return;
      host.innerHTML = [
        ["每张模数", `${layout.capacityPerSheet} 模`], ["生产张数", `${layout.sheetsRequired} 张`],
        ["末张数量", `${layout.lastSheetCount} 个`], ["预计超产", `${layout.overrun} 个`],
        ["有效利用率", `${layout.utilization.effectiveBoardPercent}%`], ["排版方向", candidateLabel(layout)]
      ].map(([label, value]) => `<div><span>${label}</span><strong>${value}</strong></div>`).join("");
    }

    function renderWarnings(layout) {
      const host = byId("dieWarnings");
      if (!host) return;
      host.innerHTML = layout.warnings.map(message => `<li>${escapeXml(message)}</li>`).join("");
    }

    function sourceWarnings() {
      if (!localState.dieSource) return ["尚未上传客户刀板图；当前只能按手填外接尺寸预排。"];
      if (localState.dieSource.kind === "svg") return ["SVG 真实刀线已保留在预览和 SVG 导出中；当前仍按其外接尺寸计算阵列。"];
      if (localState.dieSource.kind === "pdf" && localState.dieSource.exactContour) return ["已排除 PDF 页面空白，并按真实凹凸刀线做交叉咬合；预览、SVG、PDF 和 DXF 均使用提取到的刀线。"];
      if (localState.dieSource.kind === "pdf") return ["PDF 没有可靠闭合刀线，不能按页面外框代替刀板排版。"];
      if (localState.dieSource.dielineBounds && localState.dieSource.dimensionReliable) return [`已从图片中识别${localState.dieSource.dielineBounds.repeatedCount || 1}套刀板，并按单套刀线外框自动排版；生产刀线仍以客户原文件为准。`];
      return ["图片只用于确认外观，当前按人工填写的毫米外接尺寸排版；生产刀线请以客户原文件为准。"];
    }

    function layoutWithCandidate(candidate) {
      return { ...localState.layout, ...candidate, warnings:[...new Set([...(candidate.warnings || []), ...sourceWarnings(), ...(localState.layout?.sharedWarnings || [])])] };
    }

    function selectCandidate(id) {
      const candidate = localState.layout?.candidates?.find(item => item.id === id);
      if (!candidate) return;
      localState.selected = candidate;
      const selectedLayout = layoutWithCandidate(candidate);
      const preview = byId("dieLayoutPreview");
      if (preview) preview.innerHTML = renderSvg(selectedLayout, { showNumbers:true, dieSource:localState.dieSource });
      renderCandidates(); renderMetrics(selectedLayout); renderWarnings(selectedLayout);
      setStatus(`已选择${candidateLabel(candidate)}方案：${candidate.capacityPerSheet}模/张，共需${candidate.sheetsRequired}张。`, "ok");
    }

    function calculate() {
      try {
        localState.layout = calculateLayout({ ...readForm(), contour:localState.dieSource?.exactContour ? localState.dieSource : null });
        localState.signature = JSON.stringify(localState.layout.input);
        localState.lastError = null;
        localState.selected = localState.layout.candidates[0];
        selectCandidate(localState.selected.id);
        return true;
      } catch (error) {
        localState.layout = null; localState.selected = null; localState.signature = ""; localState.lastError = error;
        const preview = byId("dieLayoutPreview");
        if (preview) preview.innerHTML = `<div class="die-empty"><strong>暂时无法排版</strong><span>${escapeXml(error.message)}</span></div>`;
        const metrics = byId("dieMetrics"); if (metrics) metrics.innerHTML = "";
        const candidates = byId("dieCandidateList"); if (candidates) candidates.innerHTML = "";
        const warnings = byId("dieWarnings"); if (warnings) warnings.innerHTML = "";
        setStatus(error.message, "error");
        return false;
      }
    }

    function selectedLayout() {
      const currentSignature = JSON.stringify(publicOptions(normalizeOptions(readForm())));
      if (!localState.layout || !localState.selected || currentSignature !== localState.signature) {
        if (!calculate()) throw localState.lastError || new Error("请检查当前排版参数");
      }
      return layoutWithCandidate(localState.selected);
    }

    function saveMachineSettings() {
      try {
        const settings = publicMachineOptions(normalizeMachineOptions(readForm()));
        if (typeof root.__saveDieLayoutSettings === "function") root.__saveDieLayoutSettings(settings);
        setStatus("机台幅面、边距、咬口和刀间距已经保存。", "ok");
      } catch (error) { setStatus(error.message, "error"); }
    }

    function renderFileInfo(source) {
      const host = byId("dieLayoutFileInfo");
      const clearButton = byId("dieLayoutFileClear");
      if (!host) return;
      if (!source) {
        host.hidden = true; host.innerHTML = "";
        if (clearButton) clearButton.hidden = true;
        return;
      }
      const dimensions = source.widthMm && source.heightMm
        ? `${source.widthMm} × ${source.heightMm} mm`
        : source.pixelWidth ? `${source.pixelWidth} × ${source.pixelHeight} px` : "尺寸待补充";
      host.hidden = false;
      host.innerHTML = `<span class="die-file-kind">${escapeXml(source.kind.toUpperCase())}</span><div><strong>${escapeXml(source.name)}</strong><em>${escapeXml(dimensions)} · ${escapeXml(source.note)}</em></div>`;
      if (clearButton) clearButton.hidden = false;
    }

    function clearDieFile() {
      localState.dieSource = null;
      const input = byId("dieLayoutFile"); if (input) input.value = "";
      renderFileInfo(null);
      localState.layout = null; localState.selected = null; localState.signature = "";
      const preview = byId("dieLayoutPreview");
      if (preview) preview.innerHTML = `<div class="die-empty"><strong>请上传客户刀板图</strong><span>也可以继续按手填尺寸做外接框预排。</span></div>`;
      setStatus("客户刀板图已清除。", "");
    }

    async function handleDieFile(file) {
      const upload = byId("dieLayoutUpload");
      try {
        upload?.classList?.add("is-reading");
        setStatus("正在本机解析真实刀线，不会发送到外部服务器……", "");
        const source = await inspectDieFile(file);
        localState.dieSource = source;
        renderFileInfo(source);
        const currentName = String(byId("dieProjectName")?.value || "").trim();
        if (!currentName || currentName === "纸盒刀板排版") setValue("dieProjectName", source.name.replace(/\.[^.]+$/, ""));
        if (source.dimensionReliable) {
          setValue("diePartWidth", source.widthMm); setValue("diePartHeight", source.heightMm);
        } else {
          setValue("diePartWidth", ""); setValue("diePartHeight", "");
        }
        localState.layout = null; localState.selected = null; localState.signature = "";
        if (source.dimensionReliable) {
          calculate();
          setStatus(`${source.note}；已自动比较排版方向。`, "ok");
        } else {
          const preview = byId("dieLayoutPreview");
          if (preview) preview.innerHTML = `<div class="die-empty"><strong>请补填刀板图实际尺寸</strong><span>${escapeXml(source.note)}</span></div>`;
          setStatus(source.note, "error");
        }
      } catch (error) {
        setStatus(error.message, "error");
      } finally { upload?.classList?.remove("is-reading"); }
    }

    function downloadBlob(blob, filename) {
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove();
      root.setTimeout(() => URL.revokeObjectURL(url), 1500);
    }

    function downloadSvg() {
      try {
        const layout = selectedLayout();
        downloadBlob(new Blob([renderSvg(layout, { showNumbers:true, dieSource:localState.dieSource })], { type:"image/svg+xml;charset=utf-8" }), `${safeFilename(layout.input.projectName)}.svg`);
        setStatus(localState.dieSource?.kind === "svg" || localState.dieSource?.exactContour
          ? "已下载保留客户真实刀线的 SVG 排版图。"
          : "SVG 外接框核对图已下载。", "ok");
      } catch (error) { setStatus(error.message, "error"); }
    }

    function downloadCsv() {
      try {
        const layout = selectedLayout();
        downloadBlob(new Blob([layoutToCsv(layout)], { type:"text/csv;charset=utf-8" }), `${safeFilename(layout.input.projectName)}-刀位坐标.csv`);
        setStatus("刀位坐标表已下载。", "ok");
      } catch (error) { setStatus(error.message, "error"); }
    }

    function downloadProduction(format) {
      try {
        const layout = selectedLayout();
        const exporters = root.JianglaiVectorExporters;
        if (!exporters) throw new Error("生产格式导出模块尚未加载，请重启软件后重试");
        const documentResult = toVectorDocument(layout, { forDxf:format === "dxf", dieSource:localState.dieSource });
        const output = exporters.exportResult(documentResult, format, { widthMm:layout.input.boardWidthMm, filename:safeFilename(layout.input.projectName) });
        downloadBlob(new Blob([exporters.toUint8Array(output)], { type:output.mime }), output.filename);
        setStatus(localState.dieSource?.exactContour
          ? `${format.toUpperCase()}真实刀线排版文件已下载；上机前仍须由刀模师复核桥位、清废和共刀。`
          : `${format.toUpperCase()}外接框核对文件已下载；上机前须由刀模师复核。`, "ok");
      } catch (error) { setStatus(`${format.toUpperCase()}导出失败：${error.message}`, "error"); }
    }

    function setBoxStatus(message, kind = "") {
      const element = byId("dieBoxStatus");
      if (!element) return;
      element.textContent = message;
      element.dataset.kind = kind;
    }

    function presetBoxDieline(definition) {
      if (boxPreviewCache.has(definition.id)) return boxPreviewCache.get(definition.id);
      const [, , , material] = definition.preset;
      /*
       * The structure catalogue is a chooser, not a live-size preview.  Keep
       * every compatible catalogue card on the engine's canonical 120×60×160
       * manufacturing profile so its silhouette stays identical to the
       * reference catalogue.  The selected summary/detail previews continue
       * to use the customer's actual L/W/H values.
       */
      const enginePreset = definition.shengdaType && root.JianglaiShengdaBoxEngine?.defaultParams;
      const lengthMm = Number(enginePreset?.boxLength ?? definition.preset[0]);
      const widthMm = Number(enginePreset?.boxWidth ?? definition.preset[1]);
      const heightMm = Number(enginePreset?.boxHeight ?? definition.preset[2]);
      const thicknessMm = definition.referenceThicknessMm || BOX_MATERIAL_THICKNESS[material] || .38;
      const result = calculateBoxDieline({
        type:definition.id, lengthMm, widthMm, heightMm, material,
        thicknessMm,
        glueFlapMm:definition.presetGlueFlapMm || Math.max(12, Math.min(22, heightMm * .15)),
        bleedMm:0, clearanceMm:Math.max(.5, thicknessMm * 1.5),
        projectName:definition.name
      });
      boxPreviewCache.set(definition.id, result);
      return result;
    }

    function boxCatalogPreview(definition) {
      try {
        return previewMarkup(presetBoxDieline(definition));
      } catch (error) {
        return `<span class="die-box-preview-error">${escapeXml(error.message)}</span>`;
      }
    }

    function previewMarkup(result) {
      return renderBoxDielinePreviewSvg(result, { showLabels:false })
        .replace(/^<\?xml[^>]*>\s*/i, "");
    }

    function selectedBoxDefinition() {
      return BOX_TEMPLATE_DEFINITIONS.find(item => item.id === selectValue("dieBoxType")) || BOX_TEMPLATE_DEFINITIONS[0];
    }

    function renderBoxDimensionBasis(definition) {
      const profile = shengdaReferenceProfile(definition);
      const isShengda = definition.dimensionBasis === "manufacturing" && Boolean(profile);
      setText("dieBoxDimensionTitle", isShengda ? "2. 制造尺寸" : "2. 成品内尺寸");
      setText("dieBoxDimensionUnit", isShengda ? "标准参数口径 · mm" : "单位：mm");
      setText("dieBoxDimensionNote", isShengda
        ? `当前款式使用标准制造尺寸口径；示范值 ${profile.lengthMm} × ${profile.widthMm} × ${profile.heightMm} mm，纸厚 ${profile.thicknessMm} mm、出血 ${profile.bleedMm} mm。`
        : "默认按成品内尺寸生成并加入纸厚补偿。正式生产前，请按实物装配、公差和纤维纹向确认。");
      const glue = byId("dieBoxGlueFlap");
      if (glue) {
        glue.disabled = isShengda;
        glue.title = isShengda ? `标准结构固定为 ${profile.glueFlapMm} mm` : "";
      }
      setText("dieBoxGlueFlapHelp", isShengda ? `标准结构固定 ${profile.glueFlapMm} mm` : "可按糊盒设备调整");
    }

    function renderSelectedBoxSummary(result = localState.boxDieline) {
      if (!result?.cutPaths?.length) return false;
      const definition = result.options.definition;
      const path = byId("dieBoxSelectedPath");
      const name = byId("dieBoxSelectedName");
      const code = byId("dieBoxSelectedCode");
      const preview = byId("dieBoxTypeSelectedPreview");
      const description = byId("dieBoxDescription");
      if (path) path.textContent = `${definition.category} / ${definition.subgroup}`;
      if (name) name.textContent = definition.name;
      if (code) code.textContent = definition.code;
      if (preview) preview.innerHTML = previewMarkup(result);
      if (description) {
        description.textContent = definition.production2DVerified
          ? definition.description
          : `待校准结构：${definition.description}。当前仅供结构预览，完成专业复核前不可导出生产刀线。`;
      }
      renderBoxDimensionBasis(definition);
      return true;
    }

    function domPreviewContract(element) {
      const svg = element?.matches?.("svg") ? element : element?.querySelector?.("svg");
      if (!svg) return null;
      let bounds = null;
      try { bounds = JSON.parse(svg.getAttribute("data-geometry-bounds") || "null"); } catch (_) {}
      return {
        templateId:String(svg.getAttribute("data-template-id") || ""),
        geometrySignature:String(svg.getAttribute("data-geometry-signature") || ""),
        bounds,
        viewBox:String(svg.getAttribute("viewBox") || ""),
        preserveAspectRatio:String(svg.getAttribute("preserveAspectRatio") || ""),
        cutPathCount:Number(svg.getAttribute("data-cut-path-count") || 0),
        nativePathCount:Number(svg.getAttribute("data-native-path-count") || 0),
        foldPathCount:Number(svg.getAttribute("data-fold-path-count") || 0),
        renderedCutCount:svg.querySelectorAll?.('[data-role="cut"]').length || 0,
        renderedFoldCount:svg.querySelectorAll?.('[data-role="fold"]').length || 0
      };
    }

    function inspectBoxPreviewContracts() {
      const result = localState.boxDieline;
      const templateId = result?.options?.definition?.id || "";
      const selectedCard = document.querySelector?.(`[data-box-template="${templateId}"] .die-box-template-card-preview`);
      return {
        current:result ? boxDielinePreviewContract(result) : null,
        selector:domPreviewContract(selectedCard),
        summary:domPreviewContract(byId("dieBoxTypeSelectedPreview")),
        detail:domPreviewContract(byId("dieBoxPreview")),
        model:localState.boxViewer?.model ? {
          templateId:localState.boxViewer.model.templateId,
          geometrySignature:localState.boxViewer.model.geometrySignature,
          bounds:localState.boxViewer.model.geometryBounds
        } : null
      };
    }

    function boxCatalogSelection() {
      const category = BOX_TEMPLATE_CATALOG.find(item => item.category === localState.boxCatalogCategory) || BOX_TEMPLATE_CATALOG[0];
      const group = category.groups.find(item => item.subgroup === localState.boxCatalogGroup) || category.groups[0];
      localState.boxCatalogCategory = category.category;
      localState.boxCatalogGroup = group.subgroup;
      return { category, group };
    }

    function renderBoxCatalog() {
      const { category, group } = boxCatalogSelection();
      const main = byId("dieBoxMainCategories");
      if (main) {
        main.innerHTML = BOX_TEMPLATE_CATALOG.map(item => {
          const count = item.groups.reduce((sum, current) => sum + current.templates.length, 0);
          return `<button type="button" class="${item.category === category.category ? "is-active" : ""}" data-box-category="${escapeXml(item.category)}"><strong>${escapeXml(item.category)}</strong><span>${count}</span></button>`;
        }).join("");
        main.querySelectorAll?.("[data-box-category]").forEach(button => button.addEventListener("click", () => {
          localState.boxCatalogCategory = button.dataset.boxCategory;
          localState.boxCatalogGroup = "";
          localState.boxCatalogSearch = "";
          const search = byId("dieBoxCatalogSearch"); if (search) search.value = "";
          renderBoxCatalog();
        }));
      }
      const subcategories = byId("dieBoxSubcategories");
      if (subcategories) {
        subcategories.innerHTML = category.groups.map(item => `<button type="button" class="${item.subgroup === group.subgroup ? "is-active" : ""}" data-box-group="${escapeXml(item.subgroup)}"><strong>${escapeXml(item.subgroup)}</strong><span>${item.templates.length}</span></button>`).join("");
        subcategories.querySelectorAll?.("[data-box-group]").forEach(button => button.addEventListener("click", () => {
          localState.boxCatalogGroup = button.dataset.boxGroup;
          localState.boxCatalogSearch = "";
          const search = byId("dieBoxCatalogSearch"); if (search) search.value = "";
          renderBoxCatalog();
        }));
      }
      const query = localState.boxCatalogSearch.trim().toLowerCase();
      const templates = group.templates.filter(item => !query || `${item.name} ${item.code} ${item.description}`.toLowerCase().includes(query));
      const title = byId("dieBoxCatalogGroupTitle"); if (title) title.textContent = group.subgroup;
      const count = byId("dieBoxCatalogGroupCount"); if (count) count.textContent = `${templates.length} 个结构`;
      const grid = byId("dieBoxTemplateGrid");
      if (grid) {
        const selected = selectedBoxDefinition();
        grid.innerHTML = templates.length ? templates.map(item => `
          <button type="button" class="die-box-template-card ${item.id === selected.id ? "is-active" : ""} ${item.production2DVerified ? "is-verified" : "is-pending"}" data-box-template="${escapeXml(item.id)}">
            <span class="die-box-template-card-preview">${boxCatalogPreview(item)}</span>
            <span class="die-box-template-card-copy">
              <strong>${escapeXml(item.name)}</strong>
              <span>${escapeXml(item.code)}</span>
              <em class="die-box-template-state ${item.production2DVerified ? "is-verified" : "is-pending"}">${item.production2DVerified ? "✓ 2D 已核验" : "待校准 · 暂不可生产导出"}</em>
              <small>${escapeXml(item.description)}</small>
            </span>
          </button>`).join("") : `<div class="die-box-template-empty">当前分类没有匹配的盒型，请更换关键词。</div>`;
        grid.querySelectorAll?.("[data-box-template]").forEach(button => button.addEventListener("click", () => {
          setValue("dieBoxType", button.dataset.boxTemplate);
          applyBoxDefaults(button.dataset.boxTemplate);
          closeBoxCatalog();
        }));
      }
    }

    function openBoxCatalog() {
      const current = selectedBoxDefinition();
      localState.boxCatalogCategory = current.category;
      localState.boxCatalogGroup = current.subgroup;
      localState.boxCatalogSearch = "";
      const search = byId("dieBoxCatalogSearch"); if (search) search.value = "";
      renderBoxCatalog();
      const modal = byId("dieBoxCatalogModal");
      if (modal) {
        modal.hidden = false;
        document.body?.classList?.add("die-box-catalog-open");
        root.setTimeout(() => byId("dieBoxCatalogClose")?.focus?.(), 0);
      }
    }

    function mountBoxCatalogAtViewportRoot() {
      const modal = byId("dieBoxCatalogModal");
      if (modal && modal.parentElement !== document.body) {
        // A transformed or scrolling page ancestor changes the containing block
        // of position:fixed on embedded desktop browsers.  Keep the catalog under
        // <body> so its height and internal scroll areas always match the window.
        document.body.appendChild(modal);
      }
    }

    function closeBoxCatalog() {
      const modal = byId("dieBoxCatalogModal");
      if (modal) modal.hidden = true;
      document.body?.classList?.remove("die-box-catalog-open");
      byId("dieBoxTypeButton")?.focus?.();
    }

    function readBoxForm() {
      return {
        type:selectValue("dieBoxType"),
        projectName:byId("dieBoxProjectName")?.value,
        material:selectValue("dieBoxMaterial"),
        lengthMm:numberValue("dieBoxLength"),
        widthMm:numberValue("dieBoxWidth"),
        heightMm:numberValue("dieBoxHeight"),
        thicknessMm:numberValue("dieBoxThickness"),
        glueFlapMm:numberValue("dieBoxGlueFlap"),
        bleedMm:numberValue("dieBoxBleed"),
        clearanceMm:numberValue("dieBoxClearance")
      };
    }

    function setDieMode(mode) {
      const selected = mode === "upload" || mode === "batch" ? mode : "generate";
      const generator = byId("dieBoxGenerator");
      const uploader = byId("dieUploadWorkspace");
      const batchWorkspace = byId("dieBatchWorkspace");
      if (generator) generator.hidden = selected !== "generate";
      if (uploader) uploader.hidden = selected !== "upload";
      if (batchWorkspace) batchWorkspace.hidden = selected !== "batch";
      const generateButton = byId("dieModeGenerate");
      const uploadButton = byId("dieModeUpload");
      const batchButton = byId("dieModeBatch");
      const activeButton = selected === "generate" ? generateButton : selected === "upload" ? uploadButton : batchButton;
      [generateButton, uploadButton, batchButton].forEach(button => {
        if (!button) return;
        const active = button === activeButton;
        button.classList?.toggle("is-active", active);
        button.setAttribute?.("aria-selected", active ? "true" : "false");
      });
      if (selected === "generate" && !localState.boxDieline) calculateBoxTemplate();
      if (selected === "batch") root.initMultiOrderImpositionWorkbench?.();
    }

    function destroyBoxDieline3D() {
      try {
        localState.boxViewer?.destroy?.();
      } catch (_) {
        // A lost WebGL context may already have released its resources.
      }
      localState.boxViewer = null;
    }

    function renderBoxDieline3D(result) {
      const mount = byId("dieBox3dMount");
      if (!mount) return false;
      const viewerApi = root.JianglaiDieline3D;
      if (!viewerApi || typeof viewerApi.createViewer !== "function") {
        destroyBoxDieline3D();
        mount.innerHTML = '<div class="die-empty"><strong>实时3D暂不可用</strong><span>仍可使用上方2D刀线并下载生产文件。</span></div>';
        return false;
      }
      try {
        if (localState.boxViewer && typeof localState.boxViewer.setDieline === "function") {
          localState.boxViewer.setDieline(result);
        } else {
          destroyBoxDieline3D();
          mount.innerHTML = "";
          localState.boxViewer = viewerApi.createViewer(mount, result, {
            fold:1,
            color:"#d2aa64",
            opacity:1
          });
        }
        const model = localState.boxViewer?.model;
        const expectedBounds = JSON.stringify(result.geometryBounds || boxDielineGeometryBounds(result));
        if (!model
          || model.templateId !== result.options.definition.id
          || model.geometrySignature !== result.geometrySignature
          || JSON.stringify(model.geometryBounds || {}) !== expectedBounds) {
          throw new Error("3D 预览未绑定当前刀板结构，已停止显示旧模型");
        }
        return Boolean(localState.boxViewer);
      } catch (error) {
        destroyBoxDieline3D();
        mount.innerHTML = `<div class="die-empty"><strong>实时3D暂不可用</strong><span>${escapeXml(error?.message || "请使用2D刀线预览。")}</span></div>`;
        return false;
      }
    }

    function renderBoxTemplate(result) {
      const preview = byId("dieBoxPreview");
      if (preview) preview.innerHTML = previewMarkup(result);
      renderBoxDieline3D(result);
      const manufacturing = result.manufacturing || {};
      const inputDimensions = `${result.options.length} × ${result.options.width} × ${result.options.height} mm`;
      const production = `${Number(manufacturing.length || result.options.length).toFixed(1)} × ${Number(manufacturing.width || result.options.width).toFixed(1)} × ${Number(manufacturing.height || result.options.height).toFixed(1)} mm`;
      const dimensionMetrics = result.options.definition.dimensionBasis === "manufacturing"
        ? [["制造尺寸", inputDimensions]]
        : [["成品内尺寸", inputDimensions], ["制造尺寸", production]];
      const metrics = byId("dieBoxMetrics");
      if (metrics) metrics.innerHTML = [
        ["结构编号", result.options.definition.code],
        ["盒型", result.options.definition.name],
        ...dimensionMetrics,
        ["结构展开", `${result.flatWidthMm} × ${result.flatHeightMm} mm`],
        ["导出画布", `${result.widthMm} × ${result.heightMm} mm（含安全边）`],
        ["材料", `${result.options.material} · ${result.options.thickness}mm`],
        ["结构线", `${result.cutPaths.length}组切线 · ${result.foldPaths.length}组折线`]
      ].map(([label, value]) => `<div><span>${escapeXml(label)}</span><strong>${escapeXml(value)}</strong></div>`).join("");
      const warnings = byId("dieBoxWarnings");
      if (warnings) warnings.innerHTML = result.warnings.map(message => `<li>${escapeXml(message)}</li>`).join("");
      const description = byId("dieBoxDescription");
      if (description) description.textContent = result.options.definition.description;
      renderSelectedBoxSummary(result);
      if (!byId("dieBoxCatalogModal")?.hidden) renderBoxCatalog();
    }

    function selectBoxTemplateForVerification(type, dimensions = {}) {
      const definition = BOX_TEMPLATE_DEFINITIONS.find(item => item.id === type);
      if (!definition) throw new Error(`未知盒型：${type}`);
      const preset = definition.preset;
      const profile = shengdaReferenceProfile(definition);
      setValue("dieBoxType", definition.id);
      setValue("dieBoxLength", dimensions.lengthMm ?? profile?.lengthMm ?? preset[0]);
      setValue("dieBoxWidth", dimensions.widthMm ?? profile?.widthMm ?? preset[1]);
      setValue("dieBoxHeight", dimensions.heightMm ?? profile?.heightMm ?? preset[2]);
      setValue("dieBoxMaterial", dimensions.material ?? preset[3]);
      setValue("dieBoxThickness", dimensions.thicknessMm ?? profile?.thicknessMm ?? definition.referenceThicknessMm ?? BOX_MATERIAL_THICKNESS[dimensions.material ?? preset[3]]);
      setValue("dieBoxGlueFlap", profile?.glueFlapMm ?? definition.presetGlueFlapMm ?? 18);
      setValue("dieBoxBleed", dimensions.bleedMm ?? profile?.bleedMm ?? 3);
      setValue("dieBoxClearance", dimensions.clearanceMm ?? Math.max(.5, Number(dimensions.thicknessMm ?? profile?.thicknessMm ?? .38) * 1.5));
      setValue("dieBoxProjectName", `${definition.name}一致性验证`);
      localState.boxCatalogCategory = definition.category;
      localState.boxCatalogGroup = definition.subgroup;
      const modal = byId("dieBoxCatalogModal");
      if (modal) modal.hidden = false;
      if (!calculateBoxTemplate()) throw new Error(`${definition.name}生成失败`);
      renderBoxCatalog();
      return inspectBoxPreviewContracts();
    }

    function calculateBoxTemplate() {
      if (!byId("dieBoxGenerator")) return false;
      try {
        const result = calculateBoxDieline(readBoxForm());
        localState.boxDieline = result;
        renderBoxTemplate(result);
        const referenceSource = result.structureMeta?.exact2D && result.options.definition.referenceProvider
          ? `已载入${result.options.definition.referenceProvider}原始刀线`
          : "已按当前制造尺寸生成结构刀线";
        setBoxStatus(`${referenceSource} · ${result.options.definition.name}：黑色切线、红色折线、蓝色出血线均可独立编辑。`, "ok");
        return true;
      } catch (error) {
        localState.boxDieline = null;
        const preview = byId("dieBoxPreview");
        if (preview) preview.innerHTML = `<div class="die-empty"><strong>暂时无法生成</strong><span>${escapeXml(error.message)}</span></div>`;
        const metrics = byId("dieBoxMetrics"); if (metrics) metrics.innerHTML = "";
        const warnings = byId("dieBoxWarnings"); if (warnings) warnings.innerHTML = "";
        setBoxStatus(error.message, "error");
        return false;
      }
    }

    function selectedBoxDieline() {
      if (!localState.boxDieline && !calculateBoxTemplate()) throw new Error("请检查盒型尺寸");
      const current = normalizeBoxTemplateOptions(readBoxForm());
      const previous = localState.boxDieline.options;
      const signature = value => JSON.stringify({
        type:value.type, projectName:value.projectName, material:value.material,
        length:value.length, width:value.width, height:value.height, thickness:value.thickness,
        glueFlap:value.glueFlap, bleed:value.bleed, clearance:value.clearance
      });
      if (signature(current) !== signature(previous) && !calculateBoxTemplate()) throw new Error("请检查盒型尺寸");
      return localState.boxDieline;
    }

    function assertProduction2DVerified(result) {
      if (result?.options?.definition?.production2DVerified === true) return result;
      throw new Error(`${result?.options?.definition?.name || "当前盒型"}仍在进行 2D 结构校准，目前只能预览，暂不能导出 SVG/PDF/DXF 生产文件`);
    }

    function downloadBoxSvg() {
      try {
        const result = assertProduction2DVerified(selectedBoxDieline());
        downloadBlob(new Blob([renderBoxDielineSvg(result, { showLabels:false })], { type:"image/svg+xml;charset=utf-8" }), `${safeFilename(result.options.projectName)}.svg`);
        setBoxStatus("SVG 刀板已下载；切线、折线和出血线保留真实毫米尺寸。", "ok");
      } catch (error) { setBoxStatus(error.message, "error"); }
    }

    function downloadBoxProduction(format) {
      try {
        const result = assertProduction2DVerified(selectedBoxDieline());
        const exporters = root.JianglaiVectorExporters;
        if (!exporters) throw new Error("生产格式导出模块尚未加载，请重启软件后重试");
        const vectorDocument = boxDielineToVectorDocument(result);
        const output = exporters.exportResult(vectorDocument, format, { widthMm:vectorDocument.width, filename:safeFilename(result.options.projectName) });
        downloadBlob(new Blob([exporters.toUint8Array(output)], { type:output.mime }), output.filename);
        setBoxStatus(`${format.toUpperCase()} 分层刀板已下载；生产前请完成结构与刀模复核。`, "ok");
      } catch (error) { setBoxStatus(`${format.toUpperCase()}导出失败：${error.message}`, "error"); }
    }

    function applyBoxDefaults(type) {
      const definition = BOX_TEMPLATE_DEFINITIONS.find(item => item.id === type) || BOX_TEMPLATE_DEFINITIONS[0];
      const preset = definition.preset;
      const profile = shengdaReferenceProfile(definition);
      const thickness = profile?.thicknessMm || definition.referenceThicknessMm || BOX_MATERIAL_THICKNESS[preset[3]];
      setValue("dieBoxType", definition.id);
      setValue("dieBoxLength", profile?.lengthMm ?? preset[0]);
      setValue("dieBoxWidth", profile?.widthMm ?? preset[1]);
      setValue("dieBoxHeight", profile?.heightMm ?? preset[2]);
      setValue("dieBoxMaterial", preset[3]);
      setValue("dieBoxThickness", thickness);
      setValue("dieBoxGlueFlap", profile?.glueFlapMm ?? definition.presetGlueFlapMm ?? 18);
      setValue("dieBoxBleed", profile?.bleedMm ?? 3);
      setValue("dieBoxClearance", Math.max(.5, thickness * 1.5));
      setValue("dieBoxProjectName", `${definition?.name || "盒型"}刀板`);
      calculateBoxTemplate();
    }

    function bind() {
      const fileInput = byId("dieLayoutFile");
      const upload = byId("dieLayoutUpload");
      upload?.addEventListener("click", () => fileInput?.click());
      fileInput?.addEventListener("change", event => handleDieFile(event.target.files?.[0]));
      ["dragenter", "dragover"].forEach(type => upload?.addEventListener(type, event => { event.preventDefault(); upload.classList?.add("is-dragging"); }));
      ["dragleave", "drop"].forEach(type => upload?.addEventListener(type, event => { event.preventDefault(); upload.classList?.remove("is-dragging"); }));
      upload?.addEventListener("drop", event => handleDieFile(event.dataTransfer?.files?.[0]));
      byId("dieLayoutFileClear")?.addEventListener("click", clearDieFile);
      byId("dieCalculate")?.addEventListener("click", calculate);
      byId("dieSaveMachine")?.addEventListener("click", saveMachineSettings);
      byId("dieResetDemo")?.addEventListener("click", () => { fillSettings(defaultSettings); setValue("diePartWidth", localState.dieSource?.widthMm || 210); setValue("diePartHeight", localState.dieSource?.heightMm || 160); setValue("dieQuantity", 1000); calculate(); });
      byId("dieDownloadSvg")?.addEventListener("click", downloadSvg);
      byId("dieDownloadPdf")?.addEventListener("click", () => downloadProduction("pdf"));
      byId("dieDownloadDxf")?.addEventListener("click", () => downloadProduction("dxf"));
      byId("dieDownloadCsv")?.addEventListener("click", downloadCsv);
      byId("dieModeGenerate")?.addEventListener("click", () => setDieMode("generate"));
      byId("dieModeUpload")?.addEventListener("click", () => setDieMode("upload"));
      byId("dieModeBatch")?.addEventListener("click", () => setDieMode("batch"));
      byId("dieBoxGenerate")?.addEventListener("click", calculateBoxTemplate);
      byId("dieBoxReset")?.addEventListener("click", () => applyBoxDefaults(selectValue("dieBoxType") || "straight-tuck"));
      byId("dieBoxTypeButton")?.addEventListener("click", openBoxCatalog);
      byId("dieBoxCatalogClose")?.addEventListener("click", closeBoxCatalog);
      byId("dieBoxCatalogCancel")?.addEventListener("click", closeBoxCatalog);
      byId("dieBoxCatalogModal")?.addEventListener("click", event => {
        if (event.target === byId("dieBoxCatalogModal")) closeBoxCatalog();
      });
      byId("dieBoxCatalogSearch")?.addEventListener("input", event => {
        localState.boxCatalogSearch = String(event.target.value || "");
        renderBoxCatalog();
      });
      document.addEventListener?.("keydown", event => {
        if (event.key === "Escape" && !byId("dieBoxCatalogModal")?.hidden) closeBoxCatalog();
      });
      byId("dieBoxMaterial")?.addEventListener("change", event => {
        const thickness = BOX_MATERIAL_THICKNESS[event.target.value];
        if (thickness) setValue("dieBoxThickness", thickness);
        calculateBoxTemplate();
      });
      byId("dieBoxDownloadSvg")?.addEventListener("click", downloadBoxSvg);
      byId("dieBoxDownloadPdf")?.addEventListener("click", () => downloadBoxProduction("pdf"));
      byId("dieBoxDownloadDxf")?.addEventListener("click", () => downloadBoxProduction("dxf"));
      if (!localState.pagehideBound) {
        root.addEventListener?.("pagehide", destroyBoxDieline3D);
        localState.pagehideBound = true;
      }
    }

    function init() {
      if (!byId("dieLayoutWorkbench")) return;
      if (!localState.initialized) {
        fillSettings(readSettings());
        mountBoxCatalogAtViewportRoot();
        bind();
        localState.initialized = true;
        if (byId("dieBoxGenerator")) {
          calculateBoxTemplate();
        }
      }
      const context = customerContextValues();
      const customerSignature = context ? JSON.stringify({
        identity:context.identity,
        projectName:context.projectName,
        partWidthMm:context.partWidthMm,
        partHeightMm:context.partHeightMm,
        quantity:context.quantity,
        ready:context.ready,
        missing:context.missing
      }) : "";
      if (context && customerSignature !== localState.customerSignature) {
        localState.customerSignature = customerSignature;
        localState.layout = null;
        localState.selected = null;
        localState.signature = "";
        if (context.projectName) setValue("dieProjectName", context.projectName);
        if (context.ready) {
          setValue("diePartWidth", context.partWidthMm);
          setValue("diePartHeight", context.partHeightMm);
          setValue("dieQuantity", context.quantity);
          if (calculate()) setStatus(`已从${context.sourceLabel}自动带入展开尺寸和数量，并生成候选刀板图。请确认机台幅面、纹向和刀间距。`, "ok");
        } else {
          setValue("diePartWidth", "");
          setValue("diePartHeight", "");
          setValue("dieQuantity", "");
          const preview = byId("dieLayoutPreview");
          if (preview) preview.innerHTML = `<div class="die-empty"><strong>等待客户资料</strong><span>还缺${escapeXml(context.missing.join("、"))}，补齐后会自动生成候选刀板图。</span></div>`;
          const metrics = byId("dieMetrics"); if (metrics) metrics.innerHTML = "";
          const candidates = byId("dieCandidateList"); if (candidates) candidates.innerHTML = "";
          const warnings = byId("dieWarnings"); if (warnings) warnings.innerHTML = "";
          setStatus(`当前客户还缺${context.missing.join("、")}；已清空上一位客户的刀板结果。`, "error");
        }
        return;
      }
      if (!localState.layout) calculate();
    }

    return {
      init, calculate, selectCandidate, downloadSvg, downloadCsv, downloadProduction,
      handleDieFile, clearDieFile, calculateBoxTemplate, downloadBoxSvg, downloadBoxProduction,
      setDieMode, renderBoxDieline3D, inspectBoxPreviewContracts, selectBoxTemplateForVerification
    };
  }

  const controller = typeof document !== "undefined" ? browserController() : null;
  if (controller) {
    root.initDieLayoutWorkbench = controller.init;
    root.calculateDieLayoutWorkbench = controller.calculate;
    root.inspectBoxPreviewContracts = controller.inspectBoxPreviewContracts;
    root.selectBoxTemplateForVerification = controller.selectBoxTemplateForVerification;
  }

  return {
    MAX_SLOTS,
    MAX_UPLOAD_BYTES,
    BOX_TEMPLATE_DEFINITIONS,
    BOX_TEMPLATE_CATALOG,
    BOX_MATERIAL_THICKNESS,
    normalizeMachineOptions,
    normalizeOptions,
    normalizeBoxTemplateOptions,
    calculateLayout,
    calculateBoxDieline,
    renderSvg,
    renderBoxDielineSvg,
    renderBoxDielinePreviewSvg,
    boxDielineGeometryBounds,
    boxDielineGeometrySignature,
    boxDielinePreviewContract,
    boxDielineDimensionGuides,
    verifyBoxTemplateGeometryContracts,
    toVectorDocument,
    boxDielineToVectorDocument,
    layoutToCsv,
    safeFilename,
    parsePhysicalLength,
    parsePdfPageSize,
    parseRasterDpi,
    inspectSvgText,
    inspectDieFile,
    detectRasterDielineBounds
  };
});
