// stl.jsx — per-order STL generation: base blank tag + QR in relief, recto/verso.
// The base tag (assets/base-tag.stl): plate 46×37mm at x∈[0,46], y∈[140,177],
// z∈[0,4] with rounded edges; ring + hole on the right (x>36) — QR area avoids it.
// Exports: downloadQRStl, buildQRStlBuffer (for testing)

const STL_GEOM = {
  qrX0: 3.0,      // QR area left (mm)
  qrY0: 143.0,    // QR area bottom (mm)
  qrSize: 31.0,   // QR area side (mm)
  zTop: 4.0,      // top face of the plate (thickened model)
  zBottom: 0.0,   // bottom face of the plate
  relief: 0.8,    // QR relief height (mm)
  mirrorX: 37.0,  // verso QR mirror axis = 2×(qrX0 + qrSize/2) — keeps the QR in the same spot on both faces
};

let _baseStlPromise = null;
function loadBaseStl() {
  if (!_baseStlPromise) {
    _baseStlPromise = fetch("assets/base-tag.stl").then((r) => {
      if (!r.ok) throw new Error("base STL not found");
      return r.arrayBuffer();
    });
  }
  return _baseStlPromise;
}

// Full QR matrix (finders included — raw standard matrix, best for printing).
function fullQrMatrix(text, ecc) {
  const qr = window.qrcode(0, ecc || "M");
  qr.addData(text && text.length ? text : " ");
  qr.make();
  const n = qr.getModuleCount();
  const m = [];
  for (let r = 0; r < n; r++) {
    const row = [];
    for (let c = 0; c < n; c++) row.push(qr.isDark(r, c));
    m.push(row);
  }
  return m;
}

// Merge consecutive dark modules in a row into [colStart, colEnd] runs.
function rowRuns(row) {
  const runs = [];
  let s = -1;
  for (let c = 0; c <= row.length; c++) {
    const dark = c < row.length && row[c];
    if (dark && s < 0) s = c;
    if (!dark && s >= 0) { runs.push([s, c - 1]); s = -1; }
  }
  return runs;
}

// Binary STL writer helpers: each box = 12 triangles.
function writeTri(dv, off, n, a, b, c) {
  dv.setFloat32(off, n[0], true); dv.setFloat32(off + 4, n[1], true); dv.setFloat32(off + 8, n[2], true);
  const pts = [a, b, c];
  for (let i = 0; i < 3; i++) {
    dv.setFloat32(off + 12 + i * 12, pts[i][0], true);
    dv.setFloat32(off + 16 + i * 12, pts[i][1], true);
    dv.setFloat32(off + 20 + i * 12, pts[i][2], true);
  }
  dv.setUint16(off + 48, 0, true);
  return off + 50;
}

function writeBox(dv, off, x0, y0, z0, x1, y1, z1) {
  const A = [x0, y0, z0], B = [x1, y0, z0], C = [x1, y1, z0], D = [x0, y1, z0];
  const E = [x0, y0, z1], F = [x1, y0, z1], G = [x1, y1, z1], H = [x0, y1, z1];
  // bottom (z0, normal -z), top (z1, +z), 4 sides
  off = writeTri(dv, off, [0, 0, -1], A, C, B); off = writeTri(dv, off, [0, 0, -1], A, D, C);
  off = writeTri(dv, off, [0, 0, 1], E, F, G); off = writeTri(dv, off, [0, 0, 1], E, G, H);
  off = writeTri(dv, off, [0, -1, 0], A, B, F); off = writeTri(dv, off, [0, -1, 0], A, F, E);
  off = writeTri(dv, off, [1, 0, 0], B, C, G); off = writeTri(dv, off, [1, 0, 0], B, G, F);
  off = writeTri(dv, off, [0, 1, 0], C, D, H); off = writeTri(dv, off, [0, 1, 0], C, H, G);
  off = writeTri(dv, off, [-1, 0, 0], D, A, E); off = writeTri(dv, off, [-1, 0, 0], D, E, H);
  return off;
}

// Build the final STL ArrayBuffer.
// mode: "top"  = upper half  (z 1→2 + QR recto), laid flat (cut face on the bed)
//       "bottom" = lower half (z 0→1 + QR verso), rotated 180° so it also lies flat
//       "full" = whole tag with both reliefs
async function buildQRStlBuffer(text, opts) {
  const o = opts || {};
  const mode = o.mode || "full";
  const g = { ...STL_GEOM, ...(o.geom || {}) };
  const base = await loadBaseStl();
  const baseDv = new DataView(base);
  const baseCount = baseDv.getUint32(80, true);
  const zMid = (g.zTop + g.zBottom) / 2; // 1.0 — cut plane

  const matrix = fullQrMatrix(text, o.ecc);
  const n = matrix.length;
  const m = g.qrSize / n; // module pitch in mm
  const eps = 0.01;       // sink slightly into the plate to guarantee fusion

  // collect QR boxes for the side(s) we keep
  const boxes = [];
  for (let r = 0; r < n; r++) {
    const y0 = g.qrY0 + (n - 1 - r) * m;
    const y1 = y0 + m;
    for (const [c0, c1] of rowRuns(matrix[r])) {
      if (mode !== "bottom")
        boxes.push([g.qrX0 + c0 * m, y0, g.zTop - eps, g.qrX0 + (c1 + 1) * m, y1, g.zTop + g.relief]);
      if (mode !== "top") {
        const mx0 = g.mirrorX - (g.qrX0 + (c1 + 1) * m);
        const mx1 = g.mirrorX - (g.qrX0 + c0 * m);
        boxes.push([mx0, y0, g.zBottom - g.relief, mx1, y1, g.zBottom + eps]);
      }
    }
  }

  // gather all triangles as {n:[3], v:[3][3]} in ORIGINAL coordinates
  const tris = [];
  for (let i = 0; i < baseCount; i++) {
    const off = 84 + i * 50;
    const nor = [baseDv.getFloat32(off, true), baseDv.getFloat32(off + 4, true), baseDv.getFloat32(off + 8, true)];
    const v = [];
    for (let k = 0; k < 3; k++) v.push([baseDv.getFloat32(off + 12 + k * 12, true), baseDv.getFloat32(off + 16 + k * 12, true), baseDv.getFloat32(off + 20 + k * 12, true)]);
    // halve the base plate: clamp the far face's vertices to the cut plane
    if (mode === "top") for (const p of v) { if (p[2] < zMid) p[2] = zMid; }
    if (mode === "bottom") for (const p of v) { if (p[2] > zMid) p[2] = zMid; }
    // drop triangles fully collapsed onto the cut plane that came from the
    // removed face (zero area after clamping) — keep cut-face geometry from
    // the opposite face instead (it keeps the silhouette, normals stay valid)
    const flat = v.every((p) => Math.abs(p[2] - zMid) < 1e-6);
    const wasFar = mode === "top" ? nor[2] < -0.5 : mode === "bottom" ? nor[2] > 0.5 : false;
    if (flat && !wasFar && mode !== "full") continue; // collapsed wall slivers
    tris.push({ n: nor, v });
  }
  // QR boxes → triangles
  const boxBuf = new ArrayBuffer(50 * 12);
  for (const b of boxes) {
    const tmp = new DataView(new ArrayBuffer(12 * 50));
    writeBox(tmp, 0, b[0], b[1], b[2], b[3], b[4], b[5]);
    for (let i = 0; i < 12; i++) {
      const off = i * 50;
      const nor = [tmp.getFloat32(off, true), tmp.getFloat32(off + 4, true), tmp.getFloat32(off + 8, true)];
      const v = [];
      for (let k = 0; k < 3; k++) v.push([tmp.getFloat32(off + 12 + k * 12, true), tmp.getFloat32(off + 16 + k * 12, true), tmp.getFloat32(off + 20 + k * 12, true)]);
      tris.push({ n: nor, v });
    }
  }

  // transform so each half lies flat on the bed (cut face at z=0, QR up)
  function xform(p) {
    if (mode === "top") return [p[0], p[1], p[2] - zMid];            // translate down
    if (mode === "bottom") return [46 - p[0], p[1], zMid - p[2]];    // 180° about Y axis
    return p;
  }
  function xformN(nr) {
    if (mode === "bottom") return [-nr[0], nr[1], -nr[2]];
    return nr;
  }

  const out = new ArrayBuffer(84 + tris.length * 50);
  const dv = new DataView(out);
  const header = "QR Bag Tag - " + mode + " (QR relief, print flat side down)";
  for (let i = 0; i < 80; i++) dv.setUint8(i, i < header.length ? header.charCodeAt(i) : 32);
  dv.setUint32(80, tris.length, true);
  let off = 84;
  for (const t of tris) {
    off = writeTri(dv, off, xformN(t.n), xform(t.v[0]), xform(t.v[1]), xform(t.v[2]));
  }
  return out;
}

async function downloadQRStl(text, opts) {
  const o = opts || {};
  const buf = await buildQRStlBuffer(text, o);
  const blob = new Blob([buf], { type: "model/stl" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = o.filename || "qrbagtag.stl";
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 2000);
}

// Two flat-printable halves (glue together after printing).
async function downloadQRStlSplit(text, opts) {
  const o = opts || {};
  const base = (o.filename || "qrbagtag.stl").replace(/\.stl$/i, "");
  await downloadQRStl(text, { ...o, mode: "top", filename: base + "-recto.stl" });
  setTimeout(() => downloadQRStl(text, { ...o, mode: "bottom", filename: base + "-verso.stl" }), 450);
}

// ---------- FLAT (no relief) version, for AMS / multi-colour printers ----------
// The QR is a thin inlay whose top face is FLUSH with the plate top (zTop),
// so the scanning surface is perfectly smooth. Contrast comes from a second
// filament (AMS), never from relief shadows → scans at any angle, laid flat.
// Output = 2 bodies: the blank plate (your plate colour) + the QR inlay
// (your code colour). In Bambu Studio: import both, select → Merge into one
// object (they become parts), then assign each part a filament.
function buildFlatQrStl(text, opts) {
  const o = opts || {};
  const g = { ...STL_GEOM, ...(o.geom || {}) };
  const inlay = o.inlay != null ? o.inlay : 0.6; // top-layer thickness (mm), ~3 layers
  const matrix = fullQrMatrix(text, o.ecc);
  const n = matrix.length;
  const m = g.qrSize / n;
  const boxes = [];
  for (let r = 0; r < n; r++) {
    const y0 = g.qrY0 + (n - 1 - r) * m;
    const y1 = y0 + m;
    for (const [c0, c1] of rowRuns(matrix[r])) {
      // flush inlay: bottom sinks `inlay` into the plate, top sits exactly at zTop
      boxes.push([g.qrX0 + c0 * m, y0, g.zTop - inlay, g.qrX0 + (c1 + 1) * m, y1, g.zTop]);
    }
  }
  const out = new ArrayBuffer(84 + boxes.length * 12 * 50);
  const dv = new DataView(out);
  const header = "QR Bag Tag - flat QR inlay (AMS multicolor)";
  for (let i = 0; i < 80; i++) dv.setUint8(i, i < header.length ? header.charCodeAt(i) : 32);
  dv.setUint32(80, boxes.length * 12, true);
  let off = 84;
  for (const b of boxes) off = writeBox(dv, off, b[0], b[1], b[2], b[3], b[4], b[5]);
  return out;
}

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

async function downloadFlatTag(text, opts) {
  const o = opts || {};
  const base = (o.filename || "qrbagtag.stl").replace(/\.stl$/i, "");
  // 1) blank plate (+ ring) — assign your PLATE colour in the slicer
  const plate = await loadBaseStl();
  triggerDownload(new Blob([plate], { type: "model/stl" }), base + "-plaque.stl");
  // 2) flush QR inlay — assign your CODE colour
  setTimeout(() => {
    const qr = buildFlatQrStl(text, o);
    triggerDownload(new Blob([qr], { type: "model/stl" }), base + "-qr.stl");
  }, 450);
}

// ---------- 3MF export: ONE file, plate + QR as two distinct meshes ----------
// Imports in Bambu Studio / PrusaSlicer as a single object with TWO selectable
// parts ("Plaque" + "QR code") — assign one AMS filament to each, then print.
// The QR is a flush inlay in the top 0.6 mm (no relief, smooth surface).

const CRC_TABLE = (() => {
  const t = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    let c = i;
    for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
    t[i] = c >>> 0;
  }
  return t;
})();
function crc32(u8) {
  let c = 0xFFFFFFFF;
  for (let i = 0; i < u8.length; i++) c = CRC_TABLE[(c ^ u8[i]) & 0xFF] ^ (c >>> 8);
  return (c ^ 0xFFFFFFFF) >>> 0;
}

// Minimal ZIP writer (entries stored, no compression) — enough for 3MF.
function makeZip(files) {
  const enc = new TextEncoder();
  const parts = [], central = [];
  let offset = 0;
  for (const f of files) {
    const name = enc.encode(f.name);
    const data = typeof f.data === "string" ? enc.encode(f.data) : f.data;
    const crc = crc32(data);
    const lh = new DataView(new ArrayBuffer(30));
    lh.setUint32(0, 0x04034b50, true);
    lh.setUint16(4, 20, true);
    lh.setUint32(14, crc, true);
    lh.setUint32(18, data.length, true);
    lh.setUint32(22, data.length, true);
    lh.setUint16(26, name.length, true);
    parts.push(new Uint8Array(lh.buffer), name, data);
    const ch = new DataView(new ArrayBuffer(46));
    ch.setUint32(0, 0x02014b50, true);
    ch.setUint16(4, 20, true); ch.setUint16(6, 20, true);
    ch.setUint32(16, crc, true);
    ch.setUint32(20, data.length, true);
    ch.setUint32(24, data.length, true);
    ch.setUint16(28, name.length, true);
    ch.setUint32(42, offset, true);
    central.push(new Uint8Array(ch.buffer), name);
    offset += 30 + name.length + data.length;
  }
  let cdSize = 0;
  for (const c of central) cdSize += c.length;
  const end = new DataView(new ArrayBuffer(22));
  end.setUint32(0, 0x06054b50, true);
  end.setUint16(8, files.length, true);
  end.setUint16(10, files.length, true);
  end.setUint32(12, cdSize, true);
  end.setUint32(16, offset, true);
  const all = [...parts, ...central, new Uint8Array(end.buffer)];
  let total = 0; for (const a of all) total += a.length;
  const out = new Uint8Array(total);
  let p = 0; for (const a of all) { out.set(a, p); p += a.length; }
  return out;
}

// Indexed mesh with vertex dedup (3MF wants vertices + triangle indices).
function newMesh() { return { verts: [], tris: [], map: new Map() }; }
function meshVert(mesh, x, y, z) {
  const k = x.toFixed(4) + "," + y.toFixed(4) + "," + z.toFixed(4);
  let i = mesh.map.get(k);
  if (i === undefined) { i = mesh.verts.length; mesh.verts.push([x, y, z]); mesh.map.set(k, i); }
  return i;
}
function meshBox(mesh, x0, y0, z0, x1, y1, z1) {
  const A = meshVert(mesh, x0, y0, z0), B = meshVert(mesh, x1, y0, z0), C = meshVert(mesh, x1, y1, z0), D = meshVert(mesh, x0, y1, z0);
  const E = meshVert(mesh, x0, y0, z1), F = meshVert(mesh, x1, y0, z1), G = meshVert(mesh, x1, y1, z1), H = meshVert(mesh, x0, y1, z1);
  const T = mesh.tris;
  T.push([A, C, B], [A, D, C]); // bottom (outward -z)
  T.push([E, F, G], [E, G, H]); // top (+z)
  T.push([A, B, F], [A, F, E]);
  T.push([B, C, G], [B, G, F]);
  T.push([C, D, H], [C, H, G]);
  T.push([D, A, E], [D, E, H]);
}
function meshXml(mesh) {
  let v = "", t = "";
  for (const p of mesh.verts) v += `<vertex x="${p[0].toFixed(4)}" y="${p[1].toFixed(4)}" z="${p[2].toFixed(4)}"/>`;
  for (const tr of mesh.tris) t += `<triangle v1="${tr[0]}" v2="${tr[1]}" v3="${tr[2]}"/>`;
  return `<mesh><vertices>${v}</vertices><triangles>${t}</triangles></mesh>`;
}

// Convert the binary-STL base plate into an indexed mesh.
function stlToMesh(buf) {
  const dv = new DataView(buf);
  const count = dv.getUint32(80, true);
  const mesh = newMesh();
  for (let i = 0; i < count; i++) {
    const off = 84 + i * 50;
    const idx = [];
    for (let k = 0; k < 3; k++) {
      idx.push(meshVert(
        mesh,
        dv.getFloat32(off + 12 + k * 12, true),
        dv.getFloat32(off + 16 + k * 12, true),
        dv.getFloat32(off + 20 + k * 12, true)
      ));
    }
    if (idx[0] !== idx[1] && idx[1] !== idx[2] && idx[0] !== idx[2]) mesh.tris.push(idx);
  }
  return mesh;
}

// QR modules as a "logo" mesh: anchored 0.4 mm INTO the plate for fusion,
// proud by 0.2 mm (one layer) ABOVE it — the Bambu-standard way to do
// multicolour logos. Avoids coplanar faces (no z-fighting, unambiguous slicing);
// 0.2 mm is visually flat and casts no usable shadow, so scanning is unaffected.
function flatQrMesh(text, opts) {
  const o = opts || {};
  const g = { ...STL_GEOM, ...(o.geom || {}) };
  const anchor = o.anchor != null ? o.anchor : 0.4; // depth sunk into the plate
  const proud = o.proud != null ? o.proud : 0.2;    // height above the plate (1 layer)
  const matrix = fullQrMatrix(text, o.ecc);
  const n = matrix.length;
  const m = g.qrSize / n;
  const mesh = newMesh();
  for (let r = 0; r < n; r++) {
    const y0 = g.qrY0 + (n - 1 - r) * m, y1 = y0 + m;
    for (const [c0, c1] of rowRuns(matrix[r])) {
      meshBox(mesh, g.qrX0 + c0 * m, y0, g.zTop - anchor, g.qrX0 + (c1 + 1) * m, y1, g.zTop + proud);
    }
  }
  return mesh;
}

async function build3mfTag(text, opts) {
  const base = await loadBaseStl();
  const plate = stlToMesh(base);
  const qr = flatQrMesh(text, opts);
  const model =
    `<?xml version="1.0" encoding="UTF-8"?>` +
    `<model unit="millimeter" xml:lang="fr-FR" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">` +
    `<resources>` +
    `<object id="1" name="Plaque" type="model">${meshXml(plate)}</object>` +
    `<object id="2" name="QR code" type="model">${meshXml(qr)}</object>` +
    `<object id="3" name="QR Bag Tag" type="model"><components><component objectid="1"/><component objectid="2"/></components></object>` +
    `</resources>` +
    `<build><item objectid="3"/></build>` +
    `</model>`;
  return makeZip([
    {
      name: "[Content_Types].xml",
      data: `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/></Types>`,
    },
    {
      name: "_rels/.rels",
      data: `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel-1" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>`,
    },
    { name: "3D/3dmodel.model", data: model },
  ]);
}

async function download3mfTag(text, opts) {
  const o = opts || {};
  const zip = await build3mfTag(text, o);
  const name = (o.filename || "qrbagtag").replace(/\.(stl|3mf)$/i, "") + ".3mf";
  triggerDownload(new Blob([zip], { type: "model/3mf" }), name);
}

// ---------- DOUBLE-SIDED: two glue-together halves (QR on both faces) ----------
// assets/half-tag.stl is one watertight 3 mm half: rounded outer face at z=0,
// flat glue face at z=3, keyring hole preserved. We flip it so the glue face is
// on the bed (z=0) and the rounded face + QR are on top (z=3). The profile is
// symmetric and the hole sits on the plate's centre axis, so BOTH halves are
// identical: print the two, flip one about its long axis, glue glue-face to
// glue-face → a 6 mm tag with a readable QR on each side.
const HALF_TOTAL = 3.0;
let _halfStlPromise = null;
function loadHalfStl() {
  if (!_halfStlPromise) {
    _halfStlPromise = fetch("assets/half-tag.stl").then((r) => {
      if (!r.ok) throw new Error("half STL not found");
      return r.arrayBuffer();
    });
  }
  return _halfStlPromise;
}

// Convert the half STL to an indexed mesh, flipped in Z (z -> zt - z) so the
// glue face lands on the bed; winding reversed to keep normals outward.
function stlToMeshFlipped(buf, zt) {
  const dv = new DataView(buf);
  const count = dv.getUint32(80, true);
  const mesh = newMesh();
  for (let i = 0; i < count; i++) {
    const off = 84 + i * 50;
    const idx = [];
    for (let k = 0; k < 3; k++) {
      idx.push(meshVert(
        mesh,
        dv.getFloat32(off + 12 + k * 12, true),
        dv.getFloat32(off + 16 + k * 12, true),
        zt - dv.getFloat32(off + 20 + k * 12, true)
      ));
    }
    if (idx[0] !== idx[1] && idx[1] !== idx[2] && idx[0] !== idx[2]) mesh.tris.push([idx[0], idx[2], idx[1]]);
  }
  return mesh;
}

// QR logo on the TOP face (z = zFace) of a flipped half.
function flatQrMeshTop(text, opts) {
  const o = opts || {};
  const g = { ...STL_GEOM, ...(o.geom || {}) };
  const zFace = o.zFace != null ? o.zFace : HALF_TOTAL;
  const anchor = o.anchor != null ? o.anchor : 0.4;
  const proud = o.proud != null ? o.proud : 0.2;
  const matrix = fullQrMatrix(text, o.ecc);
  const n = matrix.length;
  const m = g.qrSize / n;
  const mesh = newMesh();
  for (let r = 0; r < n; r++) {
    const y0 = g.qrY0 + (n - 1 - r) * m, y1 = y0 + m;
    for (const [c0, c1] of rowRuns(matrix[r])) {
      meshBox(mesh, g.qrX0 + c0 * m, y0, zFace - anchor, g.qrX0 + (c1 + 1) * m, y1, zFace + proud);
    }
  }
  return mesh;
}

async function build3mfDouble(text, opts) {
  const o = opts || {};
  const half = await loadHalfStl();
  const plate = stlToMeshFlipped(half, HALF_TOTAL);
  const qr = flatQrMeshTop(text, { ...o, zFace: HALF_TOTAL });
  const GAP = 56; // place the 2nd half beside the 1st (plate is 46 mm wide)
  const model =
    `<?xml version="1.0" encoding="UTF-8"?>` +
    `<model unit="millimeter" xml:lang="fr-FR" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">` +
    `<resources>` +
    `<object id="1" name="Plaque (demi)" type="model">${meshXml(plate)}</object>` +
    `<object id="2" name="QR code" type="model">${meshXml(qr)}</object>` +
    `<object id="10" name="Face A" type="model"><components><component objectid="1"/><component objectid="2"/></components></object>` +
    `<object id="11" name="Face B" type="model"><components><component objectid="1"/><component objectid="2"/></components></object>` +
    `</resources>` +
    `<build>` +
    `<item objectid="10"/>` +
    `<item objectid="11" transform="1 0 0 0 1 0 0 0 1 ${GAP} 0 0"/>` +
    `</build>` +
    `</model>`;
  return makeZip([
    {
      name: "[Content_Types].xml",
      data: `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/></Types>`,
    },
    {
      name: "_rels/.rels",
      data: `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel-1" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>`,
    },
    { name: "3D/3dmodel.model", data: model },
  ]);
}

async function download3mfDouble(text, opts) {
  const o = opts || {};
  const zip = await build3mfDouble(text, o);
  const name = (o.filename || "qrbagtag").replace(/\.(stl|3mf)$/i, "") + "-recto-verso.3mf";
  triggerDownload(new Blob([zip], { type: "model/3mf" }), name);
}

Object.assign(window, { downloadQRStl, downloadQRStlSplit, buildQRStlBuffer, buildFlatQrStl, downloadFlatTag, build3mfTag, download3mfTag, build3mfDouble, download3mfDouble });
