// =====================================================================
// TESSELLATOR — extracted from tess_patch_v18.html.
// Provides: buildPatch(t0,t1,t2), CalcPlainBaryPtsForTess(t0,t1,t2,forceFullT).
// =====================================================================
// =====================================================================
// Tessellation Patch Visualizer — rebuilt from CalcPlainBaryPtsForTess.
//
// Direct port of upr-engine source (calc_bundle):
//   algorithm/TessUtil.cpp     -> TessUtil namespace
//   algorithm/TessLookup.cpp   -> TessLookup namespace + global TL
//   caller/CalcPlainBaryPtsForTess.cpp -> CalcPlainBaryPtsForTess()
// Topology and barycentric data come exclusively from the reference
// call chain documented in calc_bundle/README.md.
// =====================================================================

// -------- C++ helpers used in the port --------
const ASSERT = (c, msg) => { if (!c) throw new Error('assert: ' + (msg||'')); };
const ClampInt   = (lo, hi, v) => v < lo ? lo : v > hi ? hi : v;
const MaxInt     = (a, b) => a > b ? a : b;
const MinInt     = (a, b) => a < b ? a : b;
const MaxFloat   = (a, b) => a > b ? a : b;
const MinFloat   = (a, b) => a < b ? a : b;
const SafeInv    = (v) => v === 0 ? 0 : 1 / v;
const LerpFloat  = (a, b, t) => a + (b - a) * t;
const Saturate   = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
const AlignSize  = (n, a) => (n + a - 1) & ~(a - 1);
function CountBits(packed) {
  let n = 0;
  for (let i = 0; i < 32; i++) if (packed & (1 << (31 - i))) n++;
  return n;
}
function CalcPrefixSumVec(v) {
  const out = new Array(v.length); let s = 0;
  for (let i = 0; i < v.length; i++) { out[i] = s; s += v[i]; }
  return out;
}

// -------- TessUtil — t-value primitives (TessUtil.cpp) --------
const TessUtil = {};

// HalfExpFrac_GetT (TessUtil.cpp:77)
TessUtil.HalfExpFrac_GetT = function(index, tessFactor, forceFullT) {
  const baseLog2 = Math.ceil(Math.log2(1.0 + tessFactor));
  const numPtsUpper = 1 << baseLog2;
  const numPtsLower = numPtsUpper / 2;
  const numPtsActual = 1 + Math.ceil(tessFactor);
  const pivotPoint = (numPtsActual - numPtsLower) * 2;
  const indexInNextLog2 = (index < pivotPoint) ? index : (pivotPoint + (index - pivotPoint) * 2);
  let tessInterpT = tessFactor - Math.floor(tessFactor);
  if (forceFullT) tessInterpT = 1.0;
  const partialStep = (tessInterpT === 0.0) ? 0.0 : 1.0 - tessInterpT;
  let rawT = indexInNextLog2;
  if (index === pivotPoint - 1) rawT -= partialStep;
  return rawT / numPtsUpper;
};

// HalfExpAlternatingFrac_GetNumPoints (TessUtil.cpp:254)
TessUtil.HalfExpAlternatingFrac_GetNumPoints = function(tessFactor) {
  return 1 + Math.ceil(tessFactor);
};

// HalfExpAlternatingFrac_GetT (TessUtil.cpp:260)
TessUtil.HalfExpAlternatingFrac_GetT = function(index, tessFactor, forceFullT) {
  if (tessFactor <= 3.0) return TessUtil.HalfExpFrac_GetT(index, tessFactor, forceFullT);

  const baseLog2 = Math.ceil(Math.log2(1.0 + tessFactor));
  const numPtsUpper = 1 << baseLog2;
  const numPtsLower = numPtsUpper / 2;
  const numPtsQuarter = numPtsUpper / 4;
  const numPtsActual = 1 + Math.ceil(tessFactor);
  const numPtsExtra = numPtsActual - numPtsLower;
  const numQuads = numPtsUpper / 4;

  const quadLookup2 = [0, 2];
  const quadLookup3 = [0, 1, 2];
  const quadLookup4 = [0, 1, 2, 3];

  let tessInterpT = tessFactor - Math.floor(tessFactor);
  if (forceFullT) tessInterpT = 1.0;
  const partialStepReverse = (tessInterpT === 0.0) ? 0.0 : 1.0 - tessInterpT;

  let numGroups2 = 0, numGroups3 = 0, numGroups4 = 0;
  let quadIter = -1, quadLocalSlot = -1, isPivotPoint = false;

  if (numPtsExtra <= numPtsLower / 2) {
    numGroups2 = numPtsQuarter - numPtsExtra;
    numGroups3 = numPtsQuarter - numGroups2;

    if (index < numGroups3 * 3) {
      quadIter = (index / 3) | 0;
      quadLocalSlot = quadLookup3[index % 3];
      if (index === numGroups3 * 3 - 2) isPivotPoint = true;
    } else {
      const offset = index - numGroups3 * 3;
      quadIter = numGroups3 + ((offset / 2) | 0);
      quadLocalSlot = quadLookup2[offset % 2];
    }
  } else {
    numGroups3 = numPtsQuarter - (numPtsExtra - numPtsQuarter);
    numGroups4 = numPtsQuarter - numGroups3;

    if (index < numGroups4 * 4) {
      quadIter = (index / 4) | 0;
      quadLocalSlot = quadLookup4[index % 4];
      if (index === numGroups4 * 4 - 1) isPivotPoint = true;
    } else {
      const offset = index - numGroups4 * 4;
      quadIter = numGroups4 + ((offset / 3) | 0);
      quadLocalSlot = quadLookup3[offset % 3];
    }
  }

  let dstSlot = quadIter * 4 + quadLocalSlot;
  if (isPivotPoint) dstSlot -= partialStepReverse;
  return dstSlot / numPtsUpper;
};

// GetCeilLog2 (TessUtil.cpp:386)
function GetCeilLog2(v) {
  let r = 0;
  while (r < 31 && Math.ceil(v) > (1 << r)) r++;
  return r;
}

// HalfTileExpAlternatingFrac_GetNumPoints (TessUtil.cpp:397)
TessUtil.HalfTileExpAlternatingFrac_GetNumPoints = function(tessFactor) {
  if (tessFactor <= 7.0) return TessUtil.HalfExpAlternatingFrac_GetNumPoints(tessFactor);
  const tileFactor = (tessFactor + 1.0) / 8.0;
  const tileLog2 = GetCeilLog2(tileFactor);
  const tileNum = 1 << tileLog2;
  const singleTileFactor = (1.0 + tessFactor) / tileNum - 1.0;
  const numPtsPerTile = 1 + Math.ceil(singleTileFactor);
  return tileNum * numPtsPerTile;
};

// HalfTileExpAlternatingFrac_GetT (TessUtil.cpp:421)
TessUtil.HalfTileExpAlternatingFrac_GetT = function(index, tessFactor, forceFullT) {
  if (tessFactor <= 7.0) return TessUtil.HalfExpAlternatingFrac_GetT(index, tessFactor, forceFullT);
  const tileFactor = (tessFactor + 1.0) / 8.0;
  const tileLog2 = GetCeilLog2(tileFactor);
  const tileNum = 1 << tileLog2;
  const singleTileFactor = (1.0 + tessFactor) / tileNum - 1.0;
  const numPtsPerTile = 1 + Math.ceil(singleTileFactor);
  const tileIndex = (index / numPtsPerTile) | 0;
  const indexInTile = index % numPtsPerTile;
  const subTileFactor = TessUtil.HalfExpAlternatingFrac_GetT(indexInTile, singleTileFactor, forceFullT);
  return (tileIndex + subTileFactor) / tileNum;
};

// FullTileExpAlternatingFrac_GetNumPoints (TessUtil.cpp:537)
TessUtil.FullTileExpAlternatingFrac_GetNumPoints = function(tessFactor) {
  if (tessFactor <= 0.0) return 1;
  if (tessFactor < 1.0) return 3;
  const actualTessFactor = MaxFloat(tessFactor, 1.0);
  const tessFactorPerSide = (actualTessFactor + 1.0) / 2.0 - 1.0;
  return 2 * TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(tessFactorPerSide);
};

// FullTileExpAlternatingFrac_GetT (TessUtil.cpp:555)
TessUtil.FullTileExpAlternatingFrac_GetT = function(index, tessFactor, forceFullT) {
  if (tessFactor <= 0.0) {
    ASSERT(index <= 1);
    return index === 0 ? 0.0 : 1.0;
  }
  if (tessFactor < 1.0) {
    const actualTess = forceFullT ? 1.0 : tessFactor;
    if (index === 0) return 0.0;
    if (index === 1) return actualTess / 2.0;
    if (index === 2) return 1.0 - actualTess / 2.0;
    if (index === 3) return 1.0;
    ASSERT(false);
  }
  if (index === 0) return 0.0;
  const actualTessFactor = MaxFloat(tessFactor, 1.0);
  const tessFactorPerSide = (actualTessFactor + 1.0) / 2.0 - 1.0;
  const numPtsPerSide = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(tessFactorPerSide);
  const side = (index / numPtsPerSide) | 0;
  let subIndex = index % numPtsPerSide;
  if (side === 0) subIndex = numPtsPerSide - subIndex;
  let sideT = TessUtil.HalfTileExpAlternatingFrac_GetT(subIndex, tessFactorPerSide, forceFullT);
  if (side === 0) sideT = 1.0 - sideT;
  return (side + sideT) / 2.0;
};

// HalfTileExpAlternatingFrac_MatchBelow (TessUtil.cpp:625)
function RoundFrac256(t) { return Math.round(t * 256) / 256; }
TessUtil.HalfTileExpAlternatingFrac_MatchBelow = function(index, lhsTess, rhsTess) {
  const numRhs = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(rhsTess);
  const lhsT = RoundFrac256(TessUtil.HalfTileExpAlternatingFrac_GetT(index, lhsTess, true));
  let matchRhs = -1, currRhs = 0;
  do {
    const currRhsT = RoundFrac256(TessUtil.HalfTileExpAlternatingFrac_GetT(currRhs, rhsTess, true));
    if (currRhsT > lhsT)        matchRhs = currRhs - 1;
    else if (currRhsT === lhsT) matchRhs = currRhs;
    else                        currRhs++;
    if (currRhs === numRhs)     matchRhs = currRhs - 1;
  } while (matchRhs < 0);
  return matchRhs;
};

// HalfTileExpAlternatingFrac_BuildMatchStripBelow (TessUtil.cpp:744)
TessUtil.HalfTileExpAlternatingFrac_BuildMatchStripBelow = function(lhsTess, rhsTess) {
  const numPts0 = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(lhsTess);
  const numPts1 = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(rhsTess);
  const ms = [];
  for (let i = 0; i < numPts0; i++) {
    const m0 = TessUtil.HalfTileExpAlternatingFrac_MatchBelow(i, lhsTess, rhsTess);
    const m1 = (i < numPts0 - 1)
      ? TessUtil.HalfTileExpAlternatingFrac_MatchBelow(i + 1, lhsTess, rhsTess)
      : (numPts1 - 1);
    for (let m = m0; m < m1; m++) ms.push(true);
    ms.push(false);
  }
  return ms;
};

// FullTileExpAlternatingFrac_BuildMatchStrip (TessUtil.cpp:821)
TessUtil.FullTileExpAlternatingFrac_BuildMatchStrip = function(lhsTess, rhsTess) {
  const ms = [];
  const numLhsPts = TessUtil.FullTileExpAlternatingFrac_GetNumPoints(lhsTess);
  const numRhsPts = TessUtil.FullTileExpAlternatingFrac_GetNumPoints(rhsTess);

  if (numRhsPts <= 1) {
    ms.push(false); ms.push(true);
    for (let i = 0; i < numLhsPts - 1; i++) ms.push(false);
    return ms;
  }
  if (numRhsPts <= 3) {
    ms.push(true); ms.push(false);
    if (numRhsPts > 1) ms.push(true);
    for (let i = 0; i < numLhsPts - 1; i++) ms.push(false);
    return ms;
  }
  if (numLhsPts === 1) {
    ms.push(true); ms.push(false);
    for (let i = 0; i < numRhsPts - 1; i++) ms.push(true);
    return ms;
  }
  if (numLhsPts <= 3) {
    ms.push(false); ms.push(true);
    if (numLhsPts > 1) ms.push(false);
    for (let i = 0; i < numRhsPts - 1; i++) ms.push(true);
    return ms;
  }

  const lhsTessFactorPerSide = (MaxFloat(lhsTess, 1.0) + 1.0) / 2.0 - 1.0;
  const rhsTessFactorPerSide = (MaxFloat(rhsTess, 1.0) + 1.0) / 2.0 - 1.0;
  const half = TessUtil.HalfTileExpAlternatingFrac_BuildMatchStripBelow(lhsTessFactorPerSide, rhsTessFactorPerSide);
  ms.push(true);
  for (let i = 0; i < half.length; i++) ms.push(half[half.length - 1 - i]);
  for (let i = 0; i < half.length; i++) ms.push(half[i]);
  return ms;
};

// =====================================================================
// TessLookup (TessLookup.cpp). Mirrors struct + methods 1:1.
// =====================================================================
const TessLookup = {
  m_isInitialized: false,
  m_tokenInfo: [],          // [{m_ptsNum, m_ptsStart, m_log2Scale}]
  m_tokenPtsData: [],       // u8 array (numTokens-many tokens, 2 entries per pt)
  m_interiorInfo: [],       // [25*25] array of InteriorInfo
  m_interiorRowBaryData: [],
  m_interiorRowStripData: [],
  m_interiorReverseStripPackedData: [],
  m_interiorReverseBaryPackedData: [],
  m_gapInfo: [],            // [25*25]
  m_gapData: [],
  m_gapRhsPrimLookupData: [],
  m_gapLhsPrimLookupData: [],
};

// -------- TessTokenRange / token-id mappers (TessLookup.cpp:6, 165, 230) --------
TessLookup.GetTessTokenRange = function(tess) {
  let dstLow = 0, dstHigh = 0, log2Scale = 0, dstToken = 0;
  ASSERT(0.0 <= tess && tess <= 255.0);
  if (tess === 0.0) {
    dstLow = 0; dstHigh = 0; dstToken = 0; log2Scale = 0;
  } else if (tess <= 1.0) {
    dstLow = 0; dstHigh = 1; dstToken = 1; log2Scale = 1;
  } else if (tess <= 15.0) {
    const base = (tess - 1.0) / 2.0;
    let iter = Math.floor(base);
    if ((base - iter) === 0.0) iter--;
    iter = ClampInt(0, 6, iter);
    dstLow = 1 + iter * 2; dstHigh = 1 + (iter + 1) * 2; dstToken = 2 + iter;
    log2Scale = tess <= 3.0 ? 2 : tess <= 7.0 ? 3 : 4;
  } else if (tess <= 31.0) {
    const base = (tess - 15.0) / 4.0;
    let iter = Math.floor(base);
    if ((base - iter) === 0.0) iter--;
    iter = ClampInt(0, 3, iter);
    dstLow = 15 + iter * 4; dstHigh = 15 + (iter + 1) * 4; dstToken = 9 + iter; log2Scale = 5;
  } else if (tess <= 63.0) {
    const base = (tess - 31.0) / 8.0;
    let iter = Math.floor(base);
    if ((base - iter) === 0.0) iter--;
    iter = ClampInt(0, 3, iter);
    dstLow = 31 + iter * 8; dstHigh = 31 + (iter + 1) * 8; dstToken = 13 + iter; log2Scale = 6;
  } else if (tess <= 127.0) {
    const base = (tess - 63.0) / 16.0;
    let iter = Math.floor(base);
    if ((base - iter) === 0.0) iter--;
    iter = ClampInt(0, 3, iter);
    dstLow = 63 + iter * 16; dstHigh = 63 + (iter + 1) * 16; dstToken = 17 + iter; log2Scale = 7;
  } else if (tess <= 255.0) {
    const base = (tess - 127.0) / 32.0;
    let iter = Math.floor(base);
    if ((base - iter) === 0.0) iter--;
    iter = ClampInt(0, 3, iter);
    dstLow = 127 + iter * 32; dstHigh = 127 + (iter + 1) * 32; dstToken = 21 + iter; log2Scale = 8;
  } else { ASSERT(false); }
  return { dstLow, dstHigh, log2Scale, dstToken };
};

TessLookup.GetHighTessFromTokenId = function(tokenId) {
  if (tokenId === 0) return 0;
  if (tokenId < 2)   return 1;
  if (tokenId < 9)   return 1 + 2 * (tokenId - 2) + 2;
  if (tokenId < 13)  return 15 + (tokenId - 9) * 4 + 4;
  if (tokenId < 17)  return 31 + (tokenId - 13) * 8 + 8;
  if (tokenId < 21)  return 63 + (tokenId - 17) * 16 + 16;
  if (tokenId < 25)  return 127 + (tokenId - 21) * 32 + 32;
  ASSERT(false);
};

TessLookup.GetBaseTessFromTokenId = function(tokenId) {
  if (tokenId === 0) return 0;
  if (tokenId < 2)   return 0;
  if (tokenId < 9)   return 1 + 2 * (tokenId - 2);
  if (tokenId < 13)  return 15 + (tokenId - 9) * 4;
  if (tokenId < 17)  return 31 + (tokenId - 13) * 8;
  if (tokenId < 21)  return 63 + (tokenId - 17) * 16;
  if (tokenId < 25)  return 127 + (tokenId - 21) * 32;
  ASSERT(false);
};

// LerpToU16 (TessLookup.cpp:365)
function LerpToU16(val) {
  ASSERT(val >= 0.0 && val <= 1.0);
  const quantized = Math.floor(val * 256.0 + 0.5);
  return ClampInt(0, 256, quantized);
}

// Pack/Unpack helpers (TessLookup.cpp:1455 / 1471)
TessLookup.PackRowBaryData = function(ptsOffset, ptsInRow, unused) {
  let r = 0;
  r |= (ptsOffset & 0xFFFF);
  r |= ((ptsInRow & 0xFF) << 16);
  r |= ((unused & 0xFF) << 24);
  return r >>> 0;
};
TessLookup.UnpackRowBaryData = function(packed) {
  return {
    ptsOffset: (packed >>> 0) & 0xFFFF,
    ptsInRow:  (packed >>> 16) & 0xFF,
    unused:    (packed >>> 24) & 0xFF,
  };
};
TessLookup.PackRowStripData = function(primOffset, ptsInRowBelow, ptsInRowAbove) {
  const ptsInRowBelowMinusOne = ptsInRowBelow - 1;
  ASSERT(ptsInRowBelow >= 1 && ptsInRowBelow <= 256);
  ASSERT(ptsInRowBelow >= ptsInRowAbove);
  let r = 0;
  r |= (primOffset & 0xFFFF);
  r |= ((ptsInRowAbove & 0xFF) << 16);
  r |= ((ptsInRowBelowMinusOne & 0xFF) << 24);
  return r >>> 0;
};
TessLookup.UnpackRowStripData = function(packed) {
  const primOffset = (packed >>> 0) & 0xFFFF;
  const ptsInRowAbove = (packed >>> 16) & 0xFF;
  const ptsInRowBelowMinusOne = (packed >>> 24) & 0xFF;
  return { primOffset, ptsInRowBelow: ptsInRowBelowMinusOne + 1, ptsInRowAbove };
};

// -------- Token construction (TessLookup.cpp:294, 316) --------
TessLookup.GetTokenFromTessFactorAnalytic = function(tess) {
  const r = TessLookup.GetTessTokenRange(tess);
  ASSERT(r.dstLow <= tess && tess <= r.dstHigh);
  const lerpT = (tess - r.dstLow) * SafeInv(r.dstHigh - r.dstLow);
  const tk = TessLookup.m_tokenInfo[r.dstToken];
  return {
    m_lerpT: lerpT,
    m_table: r.dstToken,
    m_numPts: tk.m_ptsNum,
    m_log2Scale: tk.m_log2Scale,
    m_originalTess: tess,
  };
};

const ORIGINAL_TESS_LOOKUP = [
  0,1,3,5,7,9,11,13,15,
  19,23,27,31,
  39,47,55,63,
  63+16, 63+32, 63+48, 63+64,
  127+32, 127+64, 127+96, 127+128,
];

TessLookup.GetTokenFromTessIdAnalyticRaw = function(id) {
  const tk = TessLookup.m_tokenInfo[id];
  return {
    m_lerpT: 0.0,
    m_table: id,
    m_numPts: tk.m_ptsNum,
    m_log2Scale: tk.m_log2Scale,
    m_originalTess: ORIGINAL_TESS_LOOKUP[id],
  };
};

// -------- t-value evaluators (TessLookup.cpp:1609 / 1627 / 1643) --------
TessLookup.EvaluateTokenT = function(token, index, forceFullT) {
  const start = TessLookup.m_tokenInfo[token.m_table].m_ptsStart;
  const v0 = TessLookup.m_tokenPtsData[start * 2 + index * 2 + 0];
  const v1 = TessLookup.m_tokenPtsData[start * 2 + index * 2 + 1];
  const ratio = SafeInv(1 << token.m_log2Scale);
  const dst0 = v0 * ratio, dst1 = v1 * ratio;
  const t = (forceFullT && token.m_lerpT < 1.0) ? 0.0 : token.m_lerpT;
  return LerpFloat(dst0, dst1, t);
};
TessLookup.EvaluateTokenTSafe = function(token, index, forceFullT) {
  const numPts = TessLookup.m_tokenInfo[token.m_table].m_ptsNum;
  if (index >= numPts) return 1.0;
  if (index < 0) return 0.0;
  return TessLookup.EvaluateTokenT(token, index, forceFullT);
};
TessLookup.EvaluateTokenTMinMax = function(token, index) {
  const start = TessLookup.m_tokenInfo[token.m_table].m_ptsStart;
  ASSERT(index < token.m_numPts);
  const v0 = TessLookup.m_tokenPtsData[start * 2 + index * 2 + 0];
  const v1 = TessLookup.m_tokenPtsData[start * 2 + index * 2 + 1];
  const ratio = SafeInv(1 << token.m_log2Scale);
  const dst0 = v0 * ratio, dst1 = v1 * ratio;
  return { minT: MinFloat(dst0, dst1), maxT: MaxFloat(dst0, dst1) };
};

// -------- Lookup_* family (TessLookup.cpp:378+) --------
TessLookup.LookupInteriorNumRows = function(lhs, rhs) {
  return TessLookup.m_interiorInfo[rhs * 25 + lhs].m_packRowBaryNum;
};
TessLookup.LookupInteriorNumInteriorVtx = function(lhs, rhs) {
  return TessLookup.m_interiorInfo[rhs * 25 + lhs].m_numInteriorVtx;
};
TessLookup.LookupInteriorRowVtxNum = function(lhs, rhs, row) {
  const ii = TessLookup.m_interiorInfo[rhs * 25 + lhs];
  ASSERT(row < ii.m_packRowBaryNum);
  const packed = TessLookup.m_interiorRowBaryData[ii.m_packRowBaryStart + row];
  return TessLookup.UnpackRowBaryData(packed).ptsInRow;
};
TessLookup.LookupInteriorRowVtxStart = function(lhs, rhs, row) {
  const ii = TessLookup.m_interiorInfo[rhs * 25 + lhs];
  ASSERT(row < ii.m_packRowBaryNum);
  const packed = TessLookup.m_interiorRowBaryData[ii.m_packRowBaryStart + row];
  return TessLookup.UnpackRowBaryData(packed).ptsOffset;
};
TessLookup.LookupNumStrips = function(t0, t2) {
  return TessLookup.m_interiorInfo[t2 * 25 + t0].m_packRowStripNum;
};
TessLookup.LookupInteriorNumPrims = function(t0, t2) {
  return TessLookup.m_interiorInfo[t2 * 25 + t0].m_numPrims;
};

TessLookup.InteriorStripNumPrims = function(ptsLower, ptsUpper) {
  ASSERT(ptsLower >= ptsUpper);
  const numQuads = MaxInt(ptsUpper, 0);
  const numTrisOnEnd = ptsLower - ptsUpper;
  return 2 * numQuads + numTrisOnEnd;
};
TessLookup.LookupStripNumPrims = function(lhs, rhs, strip) {
  const ii = TessLookup.m_interiorInfo[rhs * 25 + lhs];
  ASSERT(strip < ii.m_packRowStripNum);
  const u = TessLookup.UnpackRowStripData(TessLookup.m_interiorRowStripData[ii.m_packRowStripStart + strip]);
  return TessLookup.InteriorStripNumPrims(u.ptsInRowBelow, u.ptsInRowAbove);
};
TessLookup.LookupStripPrimOffset = function(lhs, rhs, strip) {
  const ii = TessLookup.m_interiorInfo[rhs * 25 + lhs];
  ASSERT(strip < ii.m_packRowStripNum);
  return TessLookup.UnpackRowStripData(TessLookup.m_interiorRowStripData[ii.m_packRowStripStart + strip]).primOffset;
};
TessLookup.LookupStripNumPrimsQuads = function(lhs, rhs, strip) {
  const ii = TessLookup.m_interiorInfo[rhs * 25 + lhs];
  ASSERT(strip < ii.m_packRowStripNum);
  const u = TessLookup.UnpackRowStripData(TessLookup.m_interiorRowStripData[ii.m_packRowStripStart + strip]);
  return MaxInt(u.ptsInRowAbove, 0);
};

// -------- Packed match strip (TessLookup.cpp:540+) --------
TessLookup.BuildPackedMatchLookup = function(matchStrip) {
  const numFound = matchStrip.length;
  const numElem = AlignSize(numFound, 16) / 16;
  const out = new Array(numElem).fill(0);
  let numTrue = 0;
  for (let elemIter = 0; elemIter < numElem; elemIter++) {
    let packed = (numTrue << 16) >>> 0;
    for (let localIndex = 0; localIndex < 16; localIndex++) {
      const srcIndex = elemIter * 16 + localIndex;
      if (srcIndex < numFound && matchStrip[srcIndex]) {
        packed = (packed | (0x8000 >>> localIndex)) >>> 0;
        numTrue++;
      }
    }
    out[elemIter] = packed;
  }
  return out;
};
TessLookup.FetchPackedMatchLookup = function(packedMatchLookup, start, index) {
  const packed = packedMatchLookup[start + ((index / 16) | 0)] >>> 0;
  const packedBitsOnly = packed & 0xFFFF;
  const baseNumBelowTrue = (packed >>> 16) & 0xFFFF;
  // Bits are packed MSB-first (localIndex=0 -> bit 15). `aboveBitMask`
  // has the top `bit` bits of the low 16 set, so popcount(bitsOnly &
  // aboveBitMask) is the rank-of-true at this position. Was
  // ~((1<<(16-bit))-1) & 0xFFFF; per Fabien, simpler to build the top
  // run directly.
  const bit = index % 16;
  const aboveBitMask = 0x10000 - (0x10000 >>> bit);
  const numInGroupTrue = CountBits(packedBitsOnly & aboveBitMask);
  const numBelowTrue = baseNumBelowTrue + numInGroupTrue;
  const numBelowFalse = index - numBelowTrue;
  const isTrue = (packed & (0x8000 >>> bit)) !== 0;
  return { numBelowTrue, numBelowFalse, isTrue };
};

TessLookup.LookupMatchData = function(token0, token1, index) {
  const gapInfo = TessLookup.m_gapInfo[token1.m_table * 25 + token0.m_table];
  return TessLookup.FetchPackedMatchLookup(TessLookup.m_gapData, gapInfo.m_gapStart, index);
};
TessLookup.LookupMatchRhsPrimLookup = function(token0, token1, index) {
  const gapInfo = TessLookup.m_gapInfo[token1.m_table * 25 + token0.m_table];
  ASSERT(index >= 0 && index < gapInfo.m_rhsPrimLookupNum);
  return TessLookup.m_gapRhsPrimLookupData[gapInfo.m_rhsPrimLookupStart + index];
};
TessLookup.LookupMatchLhsPrimLookup = function(token0, token1, index) {
  const gapInfo = TessLookup.m_gapInfo[token1.m_table * 25 + token0.m_table];
  ASSERT(index >= 0 && index < gapInfo.m_lhsPrimLookupNum);
  return TessLookup.m_gapLhsPrimLookupData[gapInfo.m_lhsPrimLookupStart + index];
};
TessLookup.LookupMatchLhsLastPrim = function(token0, token1) {
  return TessLookup.m_gapInfo[token1.m_table * 25 + token0.m_table].m_lhsLastPrim;
};

// -------- BuildNewMatchStrip (uses matches→bits algorithm) --------
// Replaces the reference TessLookup.BuildMatchStrip when baking the
// gap-data lookup table. Walks the strong matches produced by
// buildMatchesView and converts them to the matchstrip's R/L bit
// sequence via buildBitsFromMatches. The result has the same length
// as the reference (n0+n1-1) but a different triangulation that
// cleanly aligns with the user's strong+weak match scheme.
TessLookup.BuildNewMatchStrip = function(token0, token1) {
  const view = buildMatchesView(token0, token1);
  const bits = buildBitsFromMatches(view);
  return bits.map(b => b === 'R');
};

// -------- BuildMatchStrip (TessLookup.cpp:670) --------
// Note: the reference TessLookup::BuildMatchStrip has its own
// special-case branches that *shadow* the ones in
// TessUtil::FullTileExpAlternatingFrac_BuildMatchStrip. This is a
// verbatim port of TessLookup's version. The forced last-bit-is-true
// at the bottom is kept (line 810: matchStrip[lastIndex] = true).
TessLookup.BuildMatchStrip = function(token0, token1) {
  const numLhsPts = token0.m_numPts;
  const numRhsPts = token1.m_numPts;

  let ms;
  if (numRhsPts === 1 && numLhsPts === 1) {
    ms = [true];
    return ms;
  }
  if (numRhsPts <= 1) {
    ms = [];
    for (let i = 0; i < numLhsPts - 1; i++) ms.push(false);
    ms.push(true);
    return ms;
  }
  if (numRhsPts <= 3) {
    ms = [true];
    for (let i = 0; i < numLhsPts - 1; i++) ms.push(false);
    ms.push(true);
    ms.push(true);
    return ms;
  }
  if (numLhsPts === 1) {
    ms = [true];
    for (let i = 0; i < numRhsPts - 1; i++) ms.push(true);
    return ms;
  }
  if (numLhsPts <= 3) {
    ms = [false, true];
    if (numLhsPts > 1) ms.push(false);
    for (let i = 0; i < numRhsPts - 1; i++) ms.push(true);
    return ms;
  }

  // main path: TessUtil
  ms = TessUtil.FullTileExpAlternatingFrac_BuildMatchStrip(token0.m_originalTess, token1.m_originalTess);
  ms[ms.length - 1] = true;
  return ms;
};

// -------- CalcGap / CalcGapMin / CalcRowNumInteriorMax (TessLookup.cpp:1428 / 1496 / 1527) --------
TessLookup.CalcGap = function(lhsToken, rhsToken, forceFullT) {
  let lhs1 = TessLookup.EvaluateTokenTSafe(lhsToken, 1, forceFullT);
  let rhs1 = TessLookup.EvaluateTokenTSafe(rhsToken, 1, forceFullT);
  if (lhsToken.m_table === 0)      lhs1 = 1.0;
  else if (lhsToken.m_table === 1) lhs1 = 1.0 - lhs1;
  if (rhsToken.m_table === 0)      rhs1 = 1.0;
  else if (rhsToken.m_table === 1) rhs1 = 1.0 - rhs1;
  return MaxFloat(lhs1, rhs1);
};
TessLookup.CalcGapMin = function(lhsToken, rhsToken) {
  if (lhsToken.m_table === 0 || rhsToken.m_table === 0) return 1.0;
  let { minT: lhsMin } = TessLookup.EvaluateTokenTMinMax(lhsToken, 1);
  let { minT: rhsMin } = TessLookup.EvaluateTokenTMinMax(rhsToken, 1);
  if (lhsToken.m_table === 1) lhsMin = 0.0;
  if (rhsToken.m_table === 1) rhsMin = 0.0;
  return MaxFloat(lhsMin, rhsMin);
};
TessLookup.CalcRowNumInteriorMax = function(row, lhsTokenId, rhsTokenId) {
  const lhsToken = TessLookup.GetTokenFromTessIdAnalyticRaw(lhsTokenId);
  const rhsToken = TessLookup.GetTokenFromTessIdAnalyticRaw(rhsTokenId);
  const gap = TessLookup.CalcGapMin(lhsToken, rhsToken);
  const { minT: minTy } = TessLookup.EvaluateTokenTMinMax(lhsToken, row);
  const bound = MaxFloat(0.0, 1.0 - (gap + minTy));
  if (bound === 0.0) return 0;
  let numFound = 0, done = false;
  do {
    numFound++;
    if (numFound >= rhsToken.m_numPts - 1) { done = true; }
    else {
      const { minT: minTx } = TessLookup.EvaluateTokenTMinMax(rhsToken, numFound);
      if (LerpToU16(minTx) >= LerpToU16(bound)) done = true;
    }
  } while (!done);
  if (lhsTokenId === 24 && rhsTokenId === 24) numFound = 255 - row;
  ASSERT(row < lhsToken.m_numPts);
  if (row === lhsToken.m_numPts - 1) numFound = 0;
  ASSERT(numFound <= rhsToken.m_numPts - 1);
  return numFound;
};

// -------- BuildLookup (TessLookup.cpp:815) --------
TessLookup.BuildLookup = function() {
  const NT = 25;

  // ---- token point table ----
  const vecTokenPtsNum         = new Array(NT);
  const vecTokenPtsNumAligned  = new Array(NT);
  TessLookup.m_tokenInfo = new Array(NT);
  const tessFactor0Vec = new Array(NT);
  const tessFactor1Vec = new Array(NT);

  for (let tokenId = 0; tokenId < NT; tokenId++) {
    const baseTess = TessLookup.GetBaseTessFromTokenId(tokenId);
    const nextTess = tokenId < 24 ? TessLookup.GetBaseTessFromTokenId(tokenId + 1) : 255;
    const tessFactor0 = baseTess + 0.01;
    const tessFactor1 = nextTess;
    tessFactor0Vec[tokenId] = tessFactor0;
    tessFactor1Vec[tokenId] = tessFactor1;

    let numPts0 = TessUtil.FullTileExpAlternatingFrac_GetNumPoints(tessFactor0);
    if (tokenId === 0)      numPts0 = 1;
    else if (tokenId === 1) numPts0 = 3;
    else {
      const numPts1 = TessUtil.FullTileExpAlternatingFrac_GetNumPoints(tessFactor1);
      ASSERT(numPts0 === numPts1);
    }
    vecTokenPtsNum[tokenId] = numPts0;
    vecTokenPtsNumAligned[tokenId] = AlignSize(numPts0, 2);
  }

  const vecTokenPtsStart = CalcPrefixSumVec(vecTokenPtsNumAligned);
  const totalNum = vecTokenPtsNumAligned.reduce((a, b) => a + b, 0);
  TessLookup.m_tokenPtsData = new Array(totalNum * 2).fill(0);

  for (let tokenId = 0; tokenId < NT; tokenId++) {
    const tessFactor0 = tessFactor0Vec[tokenId];
    const tessFactor1 = tessFactor1Vec[tokenId];

    let numPts0 = TessUtil.FullTileExpAlternatingFrac_GetNumPoints(tessFactor0);
    if (tokenId === 0)      numPts0 = 1;
    else if (tokenId === 1) numPts0 = 3;
    const start = vecTokenPtsStart[tokenId];

    TessLookup.m_tokenInfo[tokenId] = { m_ptsNum: numPts0, m_ptsStart: start, m_log2Scale: 0 };

    const r = TessLookup.GetTessTokenRange(tessFactor1);
    TessLookup.m_tokenInfo[tokenId].m_log2Scale = r.log2Scale;

    const divLog2Factor = 8 - r.log2Scale;

    for (let ptIter = 0; ptIter < numPts0; ptIter++) {
      let v0, v1;
      if (tokenId === 1) {
        const vals0 = [0, 0, 1], vals1 = [0, 0.5, 0.5];
        v0 = vals0[ptIter]; v1 = vals1[ptIter];
      } else {
        v0 = TessUtil.FullTileExpAlternatingFrac_GetT(ptIter, tessFactor0, false);
        v1 = TessUtil.FullTileExpAlternatingFrac_GetT(ptIter, tessFactor1, true);
      }
      const ch0 = LerpToU16(v0) >> divLog2Factor;
      const ch1 = LerpToU16(v1) >> divLog2Factor;
      TessLookup.m_tokenPtsData[start * 2 + ptIter * 2 + 0] = ch0;
      TessLookup.m_tokenPtsData[start * 2 + ptIter * 2 + 1] = ch1;
    }
  }

  // ---- bary rows ----
  TessLookup.m_interiorInfo = new Array(NT * NT);
  for (let i = 0; i < NT * NT; i++) {
    TessLookup.m_interiorInfo[i] = {
      m_packRowBaryNum: -1, m_packRowBaryStart: -1,
      m_packRowStripNum: -1, m_packRowStripStart: -1,
      m_numInteriorVtx: -1, m_numPrims: -1,
      m_reverseStripPackedNum: -1, m_reverseStripPackedStart: -1,
      m_reverseRowBaryNum: -1, m_reverseRowBaryStart: -1,
    };
  }

  TessLookup.m_interiorRowBaryData = [];
  let totalNumStripsFound = 0;

  for (let rhs = 0; rhs < NT; rhs++) {
    for (let lhs = 0; lhs < NT; lhs++) {
      let numTotalPtsForTri = 0;
      const ii = TessLookup.m_interiorInfo[rhs * NT + lhs];
      const token0 = TessLookup.GetTokenFromTessIdAnalyticRaw(lhs);

      const rowLen = new Array(token0.m_numPts - 1).fill(0);
      let maxIndex = 0;
      for (let row = 1; row < token0.m_numPts; row++) {
        const numAllPtsInRow = TessLookup.CalcRowNumInteriorMax(row, lhs, rhs);
        const numInteriorPts = numAllPtsInRow;
        rowLen[row - 1] = MaxInt(0, numInteriorPts);
        numTotalPtsForTri += numAllPtsInRow;
        if (numInteriorPts > 0) maxIndex = row;
      }

      ii.m_packRowBaryStart = TessLookup.m_interiorRowBaryData.length;
      ii.m_packRowBaryNum   = MaxInt(0, token0.m_numPts - 1);

      let numStrips = maxIndex;
      if (maxIndex > 0 && maxIndex < token0.m_numPts - 1) numStrips++;
      ii.m_packRowStripStart = totalNumStripsFound;
      ii.m_packRowStripNum   = numStrips;
      totalNumStripsFound += numStrips;

      // pack each row, including zero-length rows past maxIndex (m_packRowBaryNum slots total).
      let numPtsAddedInPattern = 0;
      for (let i = 0; i < ii.m_packRowBaryNum; i++) {
        let ptsInRow = 0;
        if (i < maxIndex) ptsInRow = rowLen[i];
        ASSERT(ptsInRow < 256);
        ASSERT(numPtsAddedInPattern < (1 << 16));
        const packed = TessLookup.PackRowBaryData(numPtsAddedInPattern, ptsInRow, 0);
        TessLookup.m_interiorRowBaryData.push(packed);
        numPtsAddedInPattern += ptsInRow;
      }

      ii.m_numInteriorVtx = numTotalPtsForTri;
    }
  }

  // ---- topo rows (strip data) ----
  TessLookup.m_interiorRowStripData = new Array(totalNumStripsFound).fill(0);
  for (let rhs = 0; rhs < NT; rhs++) {
    for (let lhs = 0; lhs < NT; lhs++) {
      const ii = TessLookup.m_interiorInfo[rhs * NT + lhs];
      const token2 = TessLookup.GetTokenFromTessIdAnalyticRaw(rhs);

      let primStart = 0;
      for (let stripIter = 0; stripIter < ii.m_packRowStripNum; stripIter++) {
        const numLower = (stripIter === 0)
          ? token2.m_numPts - 1
          : TessLookup.LookupInteriorRowVtxNum(lhs, rhs, stripIter - 1);
        const numUpper = TessLookup.LookupInteriorRowVtxNum(lhs, rhs, stripIter);
        ASSERT(numUpper <= numLower);
        const numPrimsInStrip = TessLookup.InteriorStripNumPrims(numLower, numUpper);
        const packed = TessLookup.PackRowStripData(primStart, numLower, numUpper);
        const dstIndex = ii.m_packRowStripStart + stripIter;
        ASSERT(TessLookup.m_interiorRowStripData[dstIndex] === 0);
        TessLookup.m_interiorRowStripData[dstIndex] = packed;
        primStart += numPrimsInStrip;
      }
      ii.m_numPrims = primStart;
    }
  }

  // ---- topo strip reverse lookup ----
  TessLookup.m_interiorReverseStripPackedData = [];
  for (let rhs = 0; rhs < NT; rhs++) {
    for (let lhs = 0; lhs < NT; lhs++) {
      const ii = TessLookup.m_interiorInfo[rhs * NT + lhs];
      const numStrips = TessLookup.LookupNumStrips(lhs, rhs);
      const numGroups = AlignSize(ii.m_numPrims, 16) / 16;
      const primReverseData = new Array(numGroups).fill(0);

      for (let stripIter = 0; stripIter < numStrips; stripIter++) {
        const stripPrimStart = TessLookup.LookupStripPrimOffset(lhs, rhs, stripIter);
        const stripPrimNum   = TessLookup.LookupStripNumPrims(lhs, rhs, stripIter);
        const stripRestartIndex = (stripPrimStart + stripPrimNum) - 1;
        ASSERT(stripRestartIndex >= 0);
        const packedIndex = (stripRestartIndex / 16) | 0;
        const packedBit   = stripRestartIndex % 16;
        ASSERT(packedIndex < primReverseData.length);
        primReverseData[packedIndex] = (primReverseData[packedIndex] | (1 << (15 - packedBit))) >>> 0;
      }

      let stripStart = 0;
      for (let i = 0; i < primReverseData.length; i++) {
        const numMarked = CountBits(primReverseData[i]);
        primReverseData[i] = (primReverseData[i] | (stripStart << 16)) >>> 0;
        stripStart += numMarked;
      }

      ii.m_reverseStripPackedNum   = primReverseData.length;
      ii.m_reverseStripPackedStart = TessLookup.m_interiorReverseStripPackedData.length;
      for (let i = 0; i < primReverseData.length; i++) {
        TessLookup.m_interiorReverseStripPackedData.push(primReverseData[i]);
      }
    }
  }

  // ---- bary strip reverse lookup ----
  TessLookup.m_interiorReverseBaryPackedData = [];
  for (let rhs = 0; rhs < NT; rhs++) {
    for (let lhs = 0; lhs < NT; lhs++) {
      const ii = TessLookup.m_interiorInfo[rhs * NT + lhs];
      const numRows = TessLookup.LookupInteriorNumRows(lhs, rhs);
      const numGroups = AlignSize(ii.m_numInteriorVtx, 16) / 16;
      const primReverseData = new Array(numGroups).fill(0);

      for (let rowIter = 0; rowIter < numRows; rowIter++) {
        const stripPrimStart = TessLookup.LookupInteriorRowVtxStart(lhs, rhs, rowIter);
        const stripPrimNum   = TessLookup.LookupInteriorRowVtxNum(lhs, rhs, rowIter);
        if (stripPrimNum > 0) {
          const stripRestartIndex = (stripPrimStart + stripPrimNum) - 1;
          ASSERT(stripRestartIndex >= 0);
          const packedIndex = (stripRestartIndex / 16) | 0;
          const packedBit   = stripRestartIndex % 16;
          ASSERT(packedIndex < primReverseData.length);
          primReverseData[packedIndex] = (primReverseData[packedIndex] | (1 << (15 - packedBit))) >>> 0;
        }
      }

      let baryStart = 0;
      for (let i = 0; i < primReverseData.length; i++) {
        const numMarked = CountBits(primReverseData[i]);
        primReverseData[i] = (primReverseData[i] | (baryStart << 16)) >>> 0;
        baryStart += numMarked;
      }

      ii.m_reverseRowBaryNum   = primReverseData.length;
      ii.m_reverseRowBaryStart = TessLookup.m_interiorReverseBaryPackedData.length;
      for (let i = 0; i < primReverseData.length; i++) {
        TessLookup.m_interiorReverseBaryPackedData.push(primReverseData[i]);
      }
    }
  }

  // ---- topo gap match ----
  TessLookup.m_gapInfo = new Array(NT * NT);
  for (let i = 0; i < NT * NT; i++) {
    TessLookup.m_gapInfo[i] = {
      m_gapStart: -1, m_gapPrims: -1,
      m_rhsPrimLookupStart: -1, m_rhsPrimLookupNum: -1,
      m_lhsPrimLookupStart: -1, m_lhsPrimLookupNum: -1,
      m_lhsLastPrim: -1,
    };
  }
  TessLookup.m_gapData = [];
  TessLookup.m_gapRhsPrimLookupData = [];
  TessLookup.m_gapLhsPrimLookupData = [];

  for (let rhs = 0; rhs < NT; rhs++) {
    for (let lhs = 0; lhs < NT; lhs++) {
      const gapInfo = TessLookup.m_gapInfo[rhs * NT + lhs];
      const token0 = TessLookup.GetTokenFromTessIdAnalyticRaw(lhs);
      const token1 = TessLookup.GetTokenFromTessIdAnalyticRaw(rhs);

      const matchStrip = TessLookup.BuildNewMatchStrip(token0, token1);
      ASSERT(matchStrip.length === (token0.m_numPts + token1.m_numPts - 1),
        `matchStrip size invariant: lhs=${lhs} rhs=${rhs} got=${matchStrip.length} expected=${token0.m_numPts + token1.m_numPts - 1}`);

      const rhsFound = new Array(token1.m_numPts).fill(-1);
      const lhsFound = new Array(token0.m_numPts - 1).fill(-1);

      let lhsLastPrim = -1, rhsIndex = 0, lhsIndex = 0;
      for (let primIter = 0; primIter < matchStrip.length; primIter++) {
        if (matchStrip[primIter]) {
          rhsFound[rhsIndex] = primIter;
          rhsIndex++;
        } else {
          lhsFound[lhsIndex] = primIter;
          lhsLastPrim = lhsIndex;
          lhsIndex++;
        }
      }
      gapInfo.m_lhsLastPrim = lhsLastPrim;
      ASSERT(rhsIndex === token1.m_numPts);
      ASSERT(lhsIndex === token0.m_numPts - 1);

      const packed = TessLookup.BuildPackedMatchLookup(matchStrip);

      gapInfo.m_gapStart = TessLookup.m_gapData.length;
      gapInfo.m_gapPrims = matchStrip.length;
      gapInfo.m_rhsPrimLookupStart = TessLookup.m_gapRhsPrimLookupData.length;
      gapInfo.m_rhsPrimLookupNum   = token1.m_numPts;
      gapInfo.m_lhsPrimLookupStart = TessLookup.m_gapLhsPrimLookupData.length;
      gapInfo.m_lhsPrimLookupNum   = token0.m_numPts - 1;

      for (let i = 0; i < packed.length; i++) TessLookup.m_gapData.push(packed[i]);
      for (let i = 0; i < rhsFound.length; i++) {
        ASSERT(rhsFound[i] >= 0);
        TessLookup.m_gapRhsPrimLookupData.push(rhsFound[i]);
      }
      for (let i = 0; i < lhsFound.length; i++) {
        ASSERT(lhsFound[i] >= 0);
        TessLookup.m_gapLhsPrimLookupData.push(lhsFound[i]);
      }
    }
  }

  // ---- Pack everything into one flat byte blob ----
  // After this call:
  //   - TessLookup.blob is a single ArrayBuffer holding every lookup
  //     table, GPU-uploadable as-is.
  //   - TessLookup.m_tokenInfo / m_interiorInfo / m_gapInfo are arrays
  //     of getter-backed view objects whose property reads pull from
  //     the blob. Existing call sites (m_tokenInfo[i].m_ptsNum, etc.)
  //     keep working unchanged.
  //   - TessLookup.m_tokenPtsData and the 6 *Data arrays become typed
  //     Uint32Array views into the blob (also unchanged at call sites
  //     -- arr[i] still works).
  // CPU tessellation now runs entirely off this blob; if any field
  // offset or pack rule is wrong, tessellation breaks immediately.
  packTessLookupBlob();

  TessLookup.m_isInitialized = true;
};

// ---- Blob packer + view installer ----
// Layout (all u32):
//   header[32]: magic, NT, then (offset, count) pairs for each section
//   tokenInfo:    NT * 4 u32  (3 fields + 1 pad per token)
//   tokenPts:     N u32       (1:1 from m_tokenPtsData; values fit in u8)
//   interiorInfo: NT*NT * 12 u32 (10 fields + 2 pad per entry)
//   interiorRowBary, interiorRowStrip:               variable u32
//   interiorReverseStripPacked, interiorReverseBaryPacked: variable u32
//   gapInfo:      NT*NT * 8 u32 (7 fields + 1 pad per entry)
//   gapData, gapRhsPrimLookup, gapLhsPrimLookup:     variable u32
function packTessLookupBlob() {
  const NT = 25;
  const TOK_STRIDE = 4;   // 3 used + 1 pad
  const II_STRIDE  = 12;  // 10 used + 2 pad
  const GI_STRIDE  = 8;   // 7 used + 1 pad

  const tokenInfoN     = NT * TOK_STRIDE;
  const tokenPtsN      = TessLookup.m_tokenPtsData.length;
  const interiorInfoN  = NT * NT * II_STRIDE;
  const intRowBaryN    = TessLookup.m_interiorRowBaryData.length;
  const intRowStripN   = TessLookup.m_interiorRowStripData.length;
  const intRevStripN   = TessLookup.m_interiorReverseStripPackedData.length;
  const intRevBaryN    = TessLookup.m_interiorReverseBaryPackedData.length;
  const gapInfoN       = NT * NT * GI_STRIDE;
  const gapDataN       = TessLookup.m_gapData.length;
  const gapRhsN        = TessLookup.m_gapRhsPrimLookupData.length;
  const gapLhsN        = TessLookup.m_gapLhsPrimLookupData.length;

  const headerN = 32;
  let off = headerN;
  const tokenInfoOff   = off; off += tokenInfoN;
  const tokenPtsOff    = off; off += tokenPtsN;
  const interiorInfoOff= off; off += interiorInfoN;
  const intRowBaryOff  = off; off += intRowBaryN;
  const intRowStripOff = off; off += intRowStripN;
  const intRevStripOff = off; off += intRevStripN;
  const intRevBaryOff  = off; off += intRevBaryN;
  const gapInfoOff     = off; off += gapInfoN;
  const gapDataOff     = off; off += gapDataN;
  const gapRhsOff      = off; off += gapRhsN;
  const gapLhsOff      = off; off += gapLhsN;
  const totalN         = off;

  const buf = new ArrayBuffer(totalN * 4);
  const u32 = new Uint32Array(buf);

  // Header: magic + NT + 11 (offset, count) pairs (24 u32 used, 8 reserved).
  u32[0] = 0x54453553;          // 'TES5'
  u32[1] = NT;
  u32[ 2] = tokenInfoOff;       u32[ 3] = NT;
  u32[ 4] = tokenPtsOff;        u32[ 5] = tokenPtsN;
  u32[ 6] = interiorInfoOff;    u32[ 7] = NT * NT;
  u32[ 8] = intRowBaryOff;      u32[ 9] = intRowBaryN;
  u32[10] = intRowStripOff;     u32[11] = intRowStripN;
  u32[12] = intRevStripOff;     u32[13] = intRevStripN;
  u32[14] = intRevBaryOff;      u32[15] = intRevBaryN;
  u32[16] = gapInfoOff;         u32[17] = NT * NT;
  u32[18] = gapDataOff;         u32[19] = gapDataN;
  u32[20] = gapRhsOff;          u32[21] = gapRhsN;
  u32[22] = gapLhsOff;          u32[23] = gapLhsN;

  // Token info: { m_ptsNum, m_ptsStart, m_log2Scale } per token.
  for (let i = 0; i < NT; i++) {
    const ti = TessLookup.m_tokenInfo[i];
    const base = tokenInfoOff + i * TOK_STRIDE;
    u32[base + 0] = ti.m_ptsNum;
    u32[base + 1] = ti.m_ptsStart;
    u32[base + 2] = ti.m_log2Scale;
  }
  // Token pts: 1:1 copy.
  for (let i = 0; i < tokenPtsN; i++) u32[tokenPtsOff + i] = TessLookup.m_tokenPtsData[i];

  // Interior info: 10 fields per entry, in fixed order.
  for (let i = 0; i < NT * NT; i++) {
    const ii = TessLookup.m_interiorInfo[i];
    const base = interiorInfoOff + i * II_STRIDE;
    u32[base + 0] = ii.m_packRowBaryNum;
    u32[base + 1] = ii.m_packRowBaryStart;
    u32[base + 2] = ii.m_packRowStripNum;
    u32[base + 3] = ii.m_packRowStripStart;
    u32[base + 4] = ii.m_numInteriorVtx;
    u32[base + 5] = ii.m_numPrims;
    u32[base + 6] = ii.m_reverseStripPackedNum;
    u32[base + 7] = ii.m_reverseStripPackedStart;
    u32[base + 8] = ii.m_reverseRowBaryNum;
    u32[base + 9] = ii.m_reverseRowBaryStart;
  }
  for (let i = 0; i < intRowBaryN;  i++) u32[intRowBaryOff  + i] = TessLookup.m_interiorRowBaryData[i];
  for (let i = 0; i < intRowStripN; i++) u32[intRowStripOff + i] = TessLookup.m_interiorRowStripData[i];
  for (let i = 0; i < intRevStripN; i++) u32[intRevStripOff + i] = TessLookup.m_interiorReverseStripPackedData[i];
  for (let i = 0; i < intRevBaryN;  i++) u32[intRevBaryOff  + i] = TessLookup.m_interiorReverseBaryPackedData[i];

  // Gap info: 7 fields per entry, in fixed order.
  for (let i = 0; i < NT * NT; i++) {
    const gi = TessLookup.m_gapInfo[i];
    const base = gapInfoOff + i * GI_STRIDE;
    u32[base + 0] = gi.m_gapStart;
    u32[base + 1] = gi.m_gapPrims;
    u32[base + 2] = gi.m_rhsPrimLookupStart;
    u32[base + 3] = gi.m_rhsPrimLookupNum;
    u32[base + 4] = gi.m_lhsPrimLookupStart;
    u32[base + 5] = gi.m_lhsPrimLookupNum;
    u32[base + 6] = gi.m_lhsLastPrim;
  }
  for (let i = 0; i < gapDataN; i++) u32[gapDataOff + i] = TessLookup.m_gapData[i];
  for (let i = 0; i < gapRhsN;  i++) u32[gapRhsOff  + i] = TessLookup.m_gapRhsPrimLookupData[i];
  for (let i = 0; i < gapLhsN;  i++) u32[gapLhsOff  + i] = TessLookup.m_gapLhsPrimLookupData[i];

  // Stash the blob + raw view for GPU upload / debug inspection.
  TessLookup.blob = buf;
  TessLookup.blobU32 = u32;

  // Replace m_tokenInfo with view objects backed by the blob. Field
  // names and types match the original objects so existing reads
  // (TessLookup.m_tokenInfo[i].m_ptsNum) keep working unchanged --
  // they now go through these getters into the blob.
  const tokenViews = new Array(NT);
  for (let i = 0; i < NT; i++) {
    const base = tokenInfoOff + i * TOK_STRIDE;
    tokenViews[i] = {
      get m_ptsNum()    { return u32[base + 0]; },
      get m_ptsStart()  { return u32[base + 1]; },
      get m_log2Scale() { return u32[base + 2]; },
    };
  }
  TessLookup.m_tokenInfo = tokenViews;

  const interiorViews = new Array(NT * NT);
  for (let i = 0; i < NT * NT; i++) {
    const base = interiorInfoOff + i * II_STRIDE;
    interiorViews[i] = {
      get m_packRowBaryNum()          { return u32[base + 0]; },
      get m_packRowBaryStart()        { return u32[base + 1]; },
      get m_packRowStripNum()         { return u32[base + 2]; },
      get m_packRowStripStart()       { return u32[base + 3]; },
      get m_numInteriorVtx()          { return u32[base + 4]; },
      get m_numPrims()                { return u32[base + 5]; },
      get m_reverseStripPackedNum()   { return u32[base + 6]; },
      get m_reverseStripPackedStart() { return u32[base + 7]; },
      get m_reverseRowBaryNum()       { return u32[base + 8]; },
      get m_reverseRowBaryStart()     { return u32[base + 9]; },
    };
  }
  TessLookup.m_interiorInfo = interiorViews;

  const gapViews = new Array(NT * NT);
  for (let i = 0; i < NT * NT; i++) {
    const base = gapInfoOff + i * GI_STRIDE;
    gapViews[i] = {
      get m_gapStart()           { return u32[base + 0]; },
      get m_gapPrims()           { return u32[base + 1]; },
      get m_rhsPrimLookupStart() { return u32[base + 2]; },
      get m_rhsPrimLookupNum()   { return u32[base + 3]; },
      get m_lhsPrimLookupStart() { return u32[base + 4]; },
      get m_lhsPrimLookupNum()   { return u32[base + 5]; },
      get m_lhsLastPrim()        { return u32[base + 6]; },
    };
  }
  TessLookup.m_gapInfo = gapViews;

  // Replace flat data arrays with typed-array views into the blob.
  // Indexed reads (arr[i]) work unchanged; the underlying storage is
  // now the single shared buffer.
  TessLookup.m_tokenPtsData                  = new Uint32Array(buf, tokenPtsOff   * 4, tokenPtsN);
  TessLookup.m_interiorRowBaryData           = new Uint32Array(buf, intRowBaryOff * 4, intRowBaryN);
  TessLookup.m_interiorRowStripData          = new Uint32Array(buf, intRowStripOff* 4, intRowStripN);
  TessLookup.m_interiorReverseStripPackedData= new Uint32Array(buf, intRevStripOff* 4, intRevStripN);
  TessLookup.m_interiorReverseBaryPackedData = new Uint32Array(buf, intRevBaryOff * 4, intRevBaryN);
  TessLookup.m_gapData                       = new Uint32Array(buf, gapDataOff    * 4, gapDataN);
  TessLookup.m_gapRhsPrimLookupData          = new Uint32Array(buf, gapRhsOff     * 4, gapRhsN);
  TessLookup.m_gapLhsPrimLookupData          = new Uint32Array(buf, gapLhsOff     * 4, gapLhsN);
}

// -------- Topology accessors --------

// LookupEdge0Corner / Edge1 / Edge2 (TessLookup.cpp:2036 / 2059 / 2082)
TessLookup.LookupEdge0Corner = function(token0, index) {
  if (index === 0)                  return { m_type: 0, m_index: -1 };
  if (index < token0.m_numPts)      return { m_type: 3, m_index: index - 1 };
                                    return { m_type: 1, m_index: -1 };
};
TessLookup.LookupEdge1Corner = function(token1, index) {
  if (index === 0)                  return { m_type: 2, m_index: -1 };
  if (index < token1.m_numPts)      return { m_type: 4, m_index: index - 1 };
                                    return { m_type: 1, m_index: -1 };
};
TessLookup.LookupEdge2Corner = function(token2, index) {
  if (index === 0)                  return { m_type: 0, m_index: -1 };
  if (index < token2.m_numPts)      return { m_type: 5, m_index: index - 1 };
                                    return { m_type: 2, m_index: -1 };
};

// LookupGapIndexCorner (TessLookup.cpp:2105)
TessLookup.LookupGapIndexCorner = function(token0, token1, token2, index, isLhs) {
  if (isLhs) {
    if (index === 0) {
      if (token2.m_numPts > 1) return { m_type: 5, m_index: token2.m_numPts - 2 };
      else                     return { m_type: 0, m_index: -1 };
    } else if (index < token0.m_numPts) {
      const rowStart = TessLookup.LookupInteriorRowVtxStart(token0.m_table, token2.m_table, index - 1);
      const rowNum   = TessLookup.LookupInteriorRowVtxNum  (token0.m_table, token2.m_table, index - 1);
      if (rowNum > 0) {
        return {
          m_type: 6,
          m_index: rowStart + (rowNum - 1),
          m_interiorRow: index - 1,
          m_interiorCol: rowNum - 1,
        };
      } else {
        return { m_type: 3, m_index: index - 1 };
      }
    } else {
      ASSERT(index === token0.m_numPts);
      return { m_type: 1, m_index: -1 };
    }
  } else {
    if (index === 0)               return { m_type: 2, m_index: -1 };
    if (index < token1.m_numPts)   return { m_type: 4, m_index: index - 1 };
    ASSERT(index === token1.m_numPts);
    return { m_type: 1, m_index: -1 };
  }
};

// LookupGapMatchIndexCorner (TessLookup.cpp:2177)
TessLookup.LookupGapMatchIndexCorner = function(token0, token1, token2, prim, corner) {
  let lk = TessLookup.LookupMatchData(token0, token1, prim);
  let lookupRhsBelow = lk.numBelowTrue, lookupLhsBelow = lk.numBelowFalse, lookupIsRhs = lk.isTrue;
  if (token2.m_table === 0 && prim < 2 && token1.m_table !== 0) {
    if (prim === 0) { lookupRhsBelow = 0; lookupLhsBelow = 0; lookupIsRhs = false; }
    else            { lookupRhsBelow = 0; lookupLhsBelow = 1; lookupIsRhs = true;  }
  }
  const cornerIndex = [0, 0, 0];
  const cornerIsLhs = [false, false, false];
  if (lookupIsRhs) { cornerIndex[0] = lookupRhsBelow + 1; cornerIsLhs[0] = false; }
  else             { cornerIndex[0] = lookupLhsBelow + 1; cornerIsLhs[0] = true;  }
  cornerIndex[1] = lookupRhsBelow; cornerIsLhs[1] = false;
  cornerIndex[2] = lookupLhsBelow; cornerIsLhs[2] = true;
  return TessLookup.LookupGapIndexCorner(token0, token1, token2, cornerIndex[corner], cornerIsLhs[corner]);
};

// GetTriangleType1NumPrims / Vertex (TessLookup.cpp:1674 / 1921)
TessLookup.GetTriangleType1NumPrims = function(token0, token1, token2) {
  return token0.m_numPts + (token1.m_numPts - 1) + (token2.m_numPts - 1) + 2;
};
TessLookup.GetTriangleType1Vertex = function(token0, token1, token2, prim, corner) {
  const offset0 = token0.m_numPts;
  const offset1 = offset0 + (token1.m_numPts - 1);
  const offset2 = offset1 + (token2.m_numPts - 1);

  let corners;
  if (prim < offset0) {
    const edgeIter = prim;
    corners = [
      TessLookup.LookupEdge0Corner(token0, edgeIter),
      TessLookup.LookupEdge0Corner(token0, edgeIter + 1),
      { m_type: 6, m_index: 0 },
    ];
  } else if (prim < offset1) {
    const edgeIter = (prim - offset0) + 1;
    corners = [
      TessLookup.LookupEdge1Corner(token1, edgeIter + 1),
      TessLookup.LookupEdge1Corner(token1, edgeIter),
      { m_type: 6, m_index: 0 },
    ];
  } else if (prim < offset2) {
    const edgeIter = prim - offset1;
    corners = [
      TessLookup.LookupEdge2Corner(token2, edgeIter + 1),
      TessLookup.LookupEdge2Corner(token2, edgeIter),
      { m_type: 6, m_index: 0 },
    ];
  } else {
    const primIter = prim - offset2;
    if (token1.m_table === 0 || token2.m_table === 0) {
      if (primIter === 0) {
        corners = [
          TessLookup.LookupEdge1Corner(token1, 1),
          TessLookup.LookupEdge1Corner(token1, 0),
          { m_type: 6, m_index: 0 },
        ];
      } else { // primIter === 1
        corners = [
          { m_type: 6, m_index: 0 },
          TessLookup.LookupEdge2Corner(token2, token2.m_numPts),
          TessLookup.LookupEdge2Corner(token2, token2.m_numPts - 1),
        ];
      }
    } else {
      if (primIter === 0) {
        corners = [
          TessLookup.LookupEdge1Corner(token1, 1),
          { m_type: 2, m_index: -1 },
          TessLookup.LookupEdge2Corner(token2, token2.m_numPts - 1),
        ];
      } else { // primIter === 1
        corners = [
          { m_type: 6, m_index: 0 },
          TessLookup.LookupEdge1Corner(token1, 1),
          TessLookup.LookupEdge2Corner(token2, token2.m_numPts - 1),
        ];
      }
    }
  }
  return corners[corner];
};

// LookupStripNumPrimCorner (TessLookup.cpp:2835)
TessLookup.LookupStripNumPrimCorner = function(t0, t2, strip, prim, corner) {
  const numPrims = TessLookup.LookupStripNumPrims(t0, t2, strip);
  ASSERT(0 <= prim && prim < numPrims);
  ASSERT(0 <= corner && corner < 3);
  const numQuads = TessLookup.LookupStripNumPrimsQuads(t0, t2, strip);

  let vtxX = -1, vtxY = -1;
  if (prim < numQuads * 2) {
    const quadPrim = prim;
    const quadIndex = (quadPrim / 2) | 0;
    if (quadPrim % 2 === 0) {
      // Bottom-edge triangle: two verts on row=strip+0, one on row=strip+1.
      // Provoking vertex (corner 0) is the bottom-LEFT. We CYCLICALLY rotate
      // the original (BR, BL, TL) ordering by one to (BL, TL, BR) so
      // the winding direction is preserved while the provoking corner
      // shifts. Don't swap two corners — that flips winding.
      switch (corner) {
        case 0: vtxX = quadIndex + 0; vtxY = strip + 0; break; // BL
        case 1: vtxX = quadIndex + 0; vtxY = strip + 1; break; // TL
        case 2: vtxX = quadIndex + 1; vtxY = strip + 0; break; // BR
      }
    } else {
      switch (corner) {
        case 0: vtxX = quadIndex + 0; vtxY = strip + 1; break;
        case 1: vtxX = quadIndex + 1; vtxY = strip + 1; break;
        case 2: vtxX = quadIndex + 1; vtxY = strip + 0; break;
      }
    }
  } else {
    const triFanLocalIndex = prim - 2 * numQuads;
    const baseCol = numQuads + triFanLocalIndex;
    // Tri-fan tail prim. Geometrically: bottom edge from (baseCol+0, strip+0)
    // to (baseCol+1, strip+0), apex at (numQuads, strip+1) shared by all
    // fan prims in this strip. Original order was (BR, BL, Apex). Rotate
    // cyclically by one to (BL, Apex, BR) so the bottom-LEFT is the
    // provoking vertex. Cyclic rotation preserves winding.
    switch (corner) {
      case 0: vtxX = baseCol + 0;     vtxY = strip + 0; break; // BL
      case 1: vtxX = numQuads + 0;    vtxY = strip + 1; break; // Apex
      case 2: vtxX = baseCol + 1;     vtxY = strip + 0; break; // BR
    }
  }

  if (vtxY === 0) {
    if (vtxX === 0) return { m_type: 0, m_index: -1 };
    return                 { m_type: 5, m_index: vtxX - 1 };
  } else {
    const rowStartVtx = TessLookup.LookupInteriorRowVtxStart(t0, t2, vtxY - 1);
    if (vtxX === 0) return { m_type: 3, m_index: vtxY - 1 };
    return {
      m_type: 6,
      m_index: rowStartVtx + (vtxX - 1),
      m_interiorCol: vtxX - 1,
      m_interiorRow: vtxY - 1,
    };
  }
};

// GetGapNumPrims (TessLookup.cpp:2827)
TessLookup.GetGapNumPrims = function(token0, token1) {
  return TessLookup.m_gapInfo[token1.m_table * 25 + token0.m_table].m_gapPrims;
};

// GetStripIndexForPrim / GetRowIndexForBary (TessLookup.cpp:3045 / 3071)
TessLookup.GetStripIndexForPrim = function(token0, token2, primIndex) {
  const ii = TessLookup.m_interiorInfo[token2.m_table * 25 + token0.m_table];
  const start = ii.m_reverseStripPackedStart;
  const packedIndex = (primIndex / 16) | 0;
  const packedBit = primIndex % 16;
  ASSERT(packedIndex < ii.m_reverseStripPackedNum);
  const packed = TessLookup.m_interiorReverseStripPackedData[start + packedIndex];
  const packedBitsOnly = packed & 0xFFFF;
  const baseNumBelowTrue = (packed >>> 16) & 0xFFFF;
  const aboveBitMask = 0x10000 - (0x10000 >>> packedBit);
  return baseNumBelowTrue + CountBits(packedBitsOnly & aboveBitMask);
};
TessLookup.GetRowIndexForBary = function(token0, token2, baryIndex) {
  const ii = TessLookup.m_interiorInfo[token2.m_table * 25 + token0.m_table];
  const start = ii.m_reverseRowBaryStart;
  const packedIndex = (baryIndex / 16) | 0;
  const packedBit = baryIndex % 16;
  ASSERT(packedIndex < ii.m_reverseRowBaryNum);
  const packed = TessLookup.m_interiorReverseBaryPackedData[start + packedIndex];
  const packedBitsOnly = packed & 0xFFFF;
  const baseNumBelowTrue = (packed >>> 16) & 0xFFFF;
  const aboveBitMask = 0x10000 - (0x10000 >>> packedBit);
  return baseNumBelowTrue + CountBits(packedBitsOnly & aboveBitMask);
};

// GetTriangleType / GetMiddlePointInterp (TessLookup.cpp:3099 / 3157)
function TokenInitialLerp(token) {
  return Saturate((token.m_table - 1) + token.m_lerpT);
}
TessLookup.GetMiddlePointInterp = function(token0, token1, token2, forceFullT) {
  const val0 = TokenInitialLerp(token0);
  const val1 = TokenInitialLerp(token1) * (2.0 / 3.0);
  const val2 = TokenInitialLerp(token2) * (2.0 / 3.0);
  let r = MaxFloat(val0, MaxFloat(val1, val2));
  if (forceFullT) { if (0 < r && r < 1.0) r = 2.0 / 3.0; }
  return r;
};
TessLookup.GetTriangleType = function(token0, token1, token2) {
  const interp = TessLookup.GetMiddlePointInterp(token0, token1, token2, false);
  if (interp === 0.0) return 0;
  if (interp === 1.0) return 2;
  return 1;
};

// GetNumPrimsForType (TessLookup.cpp:2337)
TessLookup.GetNumPrimsForType = function(token0, token1, token2, triangleType) {
  switch (triangleType) {
    case 0: return 1;
    case 1: return TessLookup.GetTriangleType1NumPrims(token0, token1, token2);
    case 2: {
      const numInteriorPrims = TessLookup.LookupInteriorNumPrims(token0.m_table, token2.m_table);
      const gapNumPrims      = TessLookup.GetGapNumPrims(token0, token1);
      return numInteriorPrims + gapNumPrims;
    }
    default: ASSERT(false);
  }
};

// FullLookupCornerForPrim (TessLookup.cpp:2363)
TessLookup.FullLookupCornerForPrim = function(token0, token1, token2, triangleType, prim, cornerIter) {
  if (triangleType === 0) {
    const corners = [
      { m_type: 0, m_index: -1 },
      { m_type: 1, m_index: -1 },
      { m_type: 2, m_index: -1 },
    ];
    return corners[cornerIter];
  }
  if (triangleType === 1) {
    return TessLookup.GetTriangleType1Vertex(token0, token1, token2, prim, cornerIter);
  }
  if (triangleType === 2) {
    const numInteriorPrims = TessLookup.LookupInteriorNumPrims(token0.m_table, token2.m_table);
    const gapNumPrims      = TessLookup.GetGapNumPrims(token0, token1);
    if (prim < numInteriorPrims) {
      const interiorPrim   = prim;
      const stripIter      = TessLookup.GetStripIndexForPrim(token0, token2, interiorPrim);
      const stripPrimStart = TessLookup.LookupStripPrimOffset(token0.m_table, token2.m_table, stripIter);
      const stripPrim      = interiorPrim - stripPrimStart;
      ASSERT(stripPrim >= 0);
      return TessLookup.LookupStripNumPrimCorner(token0.m_table, token2.m_table, stripIter, stripPrim, cornerIter);
    } else if (prim < numInteriorPrims + gapNumPrims) {
      const gapPrim = prim - numInteriorPrims;
      return TessLookup.LookupGapMatchIndexCorner(token0, token1, token2, gapPrim, cornerIter);
    }
    ASSERT(false);
  }
  ASSERT(false);
};

// IndexFromTopoVtx (TessLookup.cpp:3010)
TessLookup.IndexFromTopoVtx = function(triInfo, topoVtx) {
  switch (topoVtx.m_type) {
    case 0: return triInfo.m_corners[0];
    case 1: return triInfo.m_corners[1];
    case 2: return triInfo.m_corners[2];
    case 3: return triInfo.m_edgeStarts[0] + triInfo.m_edgeDirs[0] * topoVtx.m_index;
    case 4: return triInfo.m_edgeStarts[1] + triInfo.m_edgeDirs[1] * topoVtx.m_index;
    case 5: return triInfo.m_edgeStarts[2] + triInfo.m_edgeDirs[2] * topoVtx.m_index;
    case 6: return triInfo.m_interiorStart + topoVtx.m_index;
    default: ASSERT(false);
  }
};

// GetSingleBary / GetNumBary / BuildBaryVec (TessLookup.cpp:2419 / 2581 / 2637)
TessLookup.GetSingleBary = function(shareMask, token0, token1, token2, forceFullT, baryIndex) {
  let numV0 = 0, numV1 = 0, numV2 = 0;
  let numE0 = 0, numE1 = 0, numE2 = 0, numInterior = 0;

  ASSERT((shareMask & (1 << 0)) === 0);
  ASSERT((shareMask & (1 << 1)) === 0);
  ASSERT((shareMask & (1 << 2)) === 0);

  if (shareMask & (1 << 3)) numE0 = token0.m_numPts - 1;
  if (shareMask & (1 << 4)) numE1 = token1.m_numPts - 1;
  if (shareMask & (1 << 5)) numE2 += token2.m_numPts - 1;

  const type = TessLookup.GetTriangleType(token0, token1, token2);
  if (type === 0) numInterior = 0;
  else if (type === 1) numInterior = 1;
  else if (type === 2) numInterior = TessLookup.LookupInteriorNumInteriorVtx(token0.m_table, token2.m_table);

  const pos = [[0, 0], [0, 1], [1, 0]];
  const lerp2 = (a, b, t) => [a[0] * (1 - t) + b[0] * t, a[1] * (1 - t) + b[1] * t];
  const sub2  = (a, b)    => [a[0] - b[0], a[1] - b[1]];
  const add2  = (a, b)    => [a[0] + b[0], a[1] + b[1]];
  const scale2= (a, s)    => [a[0] * s, a[1] * s];

  if (baryIndex < numV0) return pos[0];
  if (baryIndex < numV0 + numV1) return pos[1];
  if (baryIndex < numV0 + numV1 + numV2) return pos[2];
  if (baryIndex < numV0 + numV1 + numV2 + numE0) {
    const localBary = baryIndex - (numV0 + numV1 + numV2);
    const t = TessLookup.EvaluateTokenT(token0, 1 + localBary, forceFullT);
    return lerp2(pos[0], pos[1], t);
  }
  if (baryIndex < numV0 + numV1 + numV2 + numE0 + numE1) {
    const localBary = baryIndex - (numV0 + numV1 + numV2 + numE0);
    const t = TessLookup.EvaluateTokenT(token1, 1 + localBary, forceFullT);
    return lerp2(pos[2], pos[1], t);
  }
  if (baryIndex < numV0 + numV1 + numV2 + numE0 + numE1 + numE2) {
    const localBary = baryIndex - (numV0 + numV1 + numV2 + numE0 + numE1);
    const t = TessLookup.EvaluateTokenT(token2, 1 + localBary, forceFullT);
    return lerp2(pos[0], pos[2], t);
  }
  if (baryIndex < numV0 + numV1 + numV2 + numE0 + numE1 + numE2 + numInterior) {
    const localBary = baryIndex - (numV0 + numV1 + numV2 + numE0 + numE1 + numE2);
    if (type === 1) {
      const t = TessLookup.GetMiddlePointInterp(token0, token1, token2, forceFullT);
      return lerp2([1, 0], [0, 0.5], t);
    }
    if (type === 2) {
      const gap = TessLookup.CalcGap(token0, token2, forceFullT);
      const rowIter = TessLookup.GetRowIndexForBary(token0, token2, localBary);
      const rowStart = TessLookup.LookupInteriorRowVtxStart(token0.m_table, token2.m_table, rowIter);
      const numPtsInRow = TessLookup.LookupInteriorRowVtxNum(token0.m_table, token2.m_table, rowIter);
      const col = localBary - rowStart;
      ASSERT(0 <= col && col < numPtsInRow);
      const dirY = sub2(pos[1], pos[0]);
      const dirX = sub2(pos[2], pos[0]);
      const ty = TessLookup.EvaluateTokenT(token0, 1 + rowIter, forceFullT);
      let   tx = TessLookup.EvaluateTokenT(token2, 1 + col, forceFullT);
      const bound = MaxFloat(0.0, 1.0 - (ty + gap));
      tx = MinFloat(tx, bound);
      return add2(add2(pos[0], scale2(dirY, ty)), scale2(dirX, tx));
    }
    ASSERT(false);
  }
  ASSERT(false);
};
TessLookup.GetNumBary = function(shareMask, token0, token1, token2, forceFullT) {
  let n = 0;
  ASSERT((shareMask & (1 << 0)) === 0);
  ASSERT((shareMask & (1 << 1)) === 0);
  ASSERT((shareMask & (1 << 2)) === 0);
  if (shareMask & (1 << 3)) n += token0.m_numPts - 1;
  if (shareMask & (1 << 4)) n += token1.m_numPts - 1;
  if (shareMask & (1 << 5)) n += token2.m_numPts - 1;
  const type = TessLookup.GetTriangleType(token0, token1, token2);
  if (type === 1) n++;
  else if (type === 2) n += TessLookup.LookupInteriorNumInteriorVtx(token0.m_table, token2.m_table);
  return n;
};
TessLookup.BuildBaryVec = function(triInfo, token0, token1, token2, forceFullT) {
  const total = TessLookup.GetNumBary(triInfo.m_shareMask, token0, token1, token2, forceFullT);
  const out = new Array(triInfo.m_tessBaryStart + total);
  out[triInfo.m_corners[0]] = [0, 0];
  out[triInfo.m_corners[1]] = [0, 1];
  out[triInfo.m_corners[2]] = [1, 0];
  for (let i = 0; i < total; i++) {
    out[triInfo.m_tessBaryStart + i] = TessLookup.GetSingleBary(
      triInfo.m_shareMask, token0, token1, token2, forceFullT, i);
  }
  return out;
};

// =====================================================================
// CalcPlainBaryPtsForTess — caller/CalcPlainBaryPtsForTess.cpp:11
// =====================================================================
function CalcPlainBaryPtsForTess(tess0, tess1, tess2, forceFullT) {
  const token0 = TessLookup.GetTokenFromTessFactorAnalytic(tess0);
  const token1 = TessLookup.GetTokenFromTessFactorAnalytic(tess1);
  const token2 = TessLookup.GetTokenFromTessFactorAnalytic(tess2);

  ASSERT(token0.m_numPts >= 1);
  ASSERT(token1.m_numPts >= 1);
  ASSERT(token2.m_numPts >= 1);

  const numPts0 = token0.m_numPts;
  const numPts1 = token1.m_numPts;
  const numPts2 = token2.m_numPts;

  const triInfo = {
    m_corners:   [0, 1, 2],
    m_tessBaryStart: 3,
    m_edgeDirs:  [1, 1, 1],
    m_edgeNums:  [numPts0 - 1, numPts1 - 1, numPts2 - 1],
    m_edgeStarts:[3, 3 + (numPts0 - 1), 3 + (numPts0 - 1) + (numPts1 - 1)],
    m_interiorNum:   TessLookup.LookupInteriorNumInteriorVtx(token0.m_table, token2.m_table),
    m_interiorStart: 3 + (numPts0 - 1) + (numPts1 - 1) + (numPts2 - 1),
    m_shareMask: 0xf8,
  };

  const baryPts = TessLookup.BuildBaryVec(triInfo, token0, token1, token2, forceFullT);
  const triangleType = TessLookup.GetTriangleType(token0, token1, token2);
  const numPrims = TessLookup.GetNumPrimsForType(token0, token1, token2, triangleType);

  // build provoking-vertex reorder list (kept for parity with the ref;
  // not used by the visualizer's drawing, but exposed via patch.provokingReorder)
  const provokingReorder = new Array(numPrims + 2);
  for (let primIter = 0; primIter < numPrims; primIter++) {
    const v = TessLookup.FullLookupCornerForPrim(token0, token1, token2, triangleType, primIter, 0);
    provokingReorder[primIter] = TessLookup.IndexFromTopoVtx(triInfo, v);
  }
  provokingReorder[numPrims + 0] = triInfo.m_corners[0];
  provokingReorder[numPrims + 1] = triInfo.m_corners[2];

  const tessIdxList = new Array(numPrims * 3);
  for (let primIter = 0; primIter < numPrims; primIter++) {
    for (let cornerIter = 0; cornerIter < 3; cornerIter++) {
      const v = TessLookup.FullLookupCornerForPrim(token0, token1, token2, triangleType, primIter, cornerIter);
      tessIdxList[primIter * 3 + cornerIter] = TessLookup.IndexFromTopoVtx(triInfo, v);
    }
  }

  return { baryPts, tessIdxList, triInfo, token0, token1, token2, triangleType, numPrims, provokingReorder };
}

// =====================================================================
// One-time bake on load.
// =====================================================================
TessLookup.BuildLookup();

// =====================================================================
// Adapter glue for the v15 GUI: TL.evalTokT/evalTokTS, getMidInterp,
// buildPatch returning the {tris, ti, tok0..2, ...} shape.
// =====================================================================
function makeOneOffToken(table, lerpT) {
  const tk = TessLookup.m_tokenInfo[table];
  return { m_table: table, m_lerpT: lerpT, m_log2Scale: tk.m_log2Scale, m_numPts: tk.m_ptsNum };
}
const TL = {
  evalTokT:  (table, lerpT, idx, fft) => TessLookup.EvaluateTokenT    (makeOneOffToken(table, lerpT), idx, fft),
  evalTokTS: (table, lerpT, idx, fft) => TessLookup.EvaluateTokenTSafe(makeOneOffToken(table, lerpT), idx, fft),
};
function getMidInterp(tok0, tok1, tok2) {
  return TessLookup.GetMiddlePointInterp(tok0, tok1, tok2, false);
}

function buildPatch(tess0, tess1, tess2) {
  const r = CalcPlainBaryPtsForTess(tess0, tess1, tess2, false);
  const { baryPts, tessIdxList, triInfo, token0: tok0, token1: tok1, token2: tok2, triangleType: triType, numPrims } = r;

  // tris in the {prim, sec, flatIdx, pos} shape.
  const numInteriorPrims = triType === 2
    ? TessLookup.LookupInteriorNumPrims(tok0.m_table, tok2.m_table) : 0;
  const tris = [];
  for (let p = 0; p < numPrims; p++) {
    const i0 = tessIdxList[p * 3 + 0];
    const i1 = tessIdxList[p * 3 + 1];
    const i2 = tessIdxList[p * 3 + 2];
    let sec;
    if (triType === 0) sec = 'single';
    else if (triType === 1) {
      const o0 = tok0.m_numPts;
      const o1 = o0 + (tok1.m_numPts - 1);
      const o2 = o1 + (tok2.m_numPts - 1);
      if      (p < o0) sec = 'e0';
      else if (p < o1) sec = 'e1';
      else if (p < o2) sec = 'e2';
      else             sec = 'stitch';
    } else {
      sec = (p < numInteriorPrims) ? 'interior' : 'gap';
    }
    tris.push({
      prim: p,
      sec,
      flatIdx: [i0, i1, i2],
      pos: [baryPts[i0], baryPts[i1], baryPts[i2]],
    });
  }

  // The v15 draw() uses a few summary fields. Provide compatible values:
  //   rows[]    — for drawing interior-grid points (Type 2)
  //   strips[]  — kept as null; not used by draw()
  //   intPrims  — number of interior prims
  //   intVtx    — number of interior vertices
  //   gap       — calc-gap value
  //   midPt     — Type-1 midpoint position
  //   gapLen    — gap-strip length
  let rows = [], strips = null, intPrims = 0, intVtx = 0, gapLen = 0, gap = 0, midPt = [0, 0];
  if (triType === 2) {
    intPrims = numInteriorPrims;
    intVtx   = TessLookup.LookupInteriorNumInteriorVtx(tok0.m_table, tok2.m_table);
    gapLen   = TessLookup.GetGapNumPrims(tok0, tok1);
    gap      = TessLookup.CalcGap(tok0, tok2, false);
    const numRows = TessLookup.LookupInteriorNumRows(tok0.m_table, tok2.m_table);
    for (let ri = 0; ri < numRows; ri++) {
      rows.push([
        TessLookup.LookupInteriorRowVtxNum  (tok0.m_table, tok2.m_table, ri),
        TessLookup.LookupInteriorRowVtxStart(tok0.m_table, tok2.m_table, ri),
      ]);
    }
  } else if (triType === 1) {
    const t = TessLookup.GetMiddlePointInterp(tok0, tok1, tok2, false);
    midPt = [1 - t, 0.5 * t];
    gapLen = TessLookup.GetGapNumPrims(tok0, tok1);
  }

  // Wrap tokens with v15-style {table, numPts, lerpT} fields too,
  // so the existing draw() code keeps working.
  function wrap(tok) {
    return Object.assign({}, tok, { table: tok.m_table, numPts: tok.m_numPts, lerpT: tok.m_lerpT });
  }
  const ti = { totalVtx: baryPts.length };

  // ===========================================================
  // Build a V-buffer-style index list + reorder list.
  // - idxBuf has length 3*N. idxBuf[3P+0] = P (unique per triangle,
  //   so flat-shading carries P as the V-buffer value). idxBuf[3P+1]
  //   and idxBuf[3P+2] are canonical slots referencing the other two
  //   corners' geometric vertices.
  // - reorder has length N+D+2. reorder[K] = the geometric flat idx
  //   of the vertex at virtual slot K. Slots [0, N): provoking-corner
  //   of triangle K. Slots [N, N+D): canonical slots for mult-2 verts.
  //   Slots [N+D, N+D+2): canonical slots for mult-0 verts.
  //
  // The shader-side decode is:
  //   triID = vbufferValue                                   // = idxBuf[3*triID+0]
  //   gv0   = reorder[triID]                                 // provoking vert
  //   gv1   = reorder[idxBuf[3*triID+1]]                     // corner 1
  //   gv2   = reorder[idxBuf[3*triID+2]]                     // corner 2
  // The "Reorder" checkbox in the GUI validates this by displaying
  // reorder[idxBuf[3P+c]] for c=0,1,2 and comparing to the existing
  // tessIdxList (which is the ground-truth geometric flat idx).
  // ===========================================================
  let idxBuf = null, reorder = null, vbufStats = null;
  {
    const N = numPrims;
    // First pass: count how many prims each geometric vertex provokes for.
    const provCount = new Map();    // geomVtx -> count (1 or 2)
    const firstProvSlot = new Map(); // geomVtx -> first prim slot in [0,N) where it provokes
    const secondProvSlot = new Map();// geomVtx -> second prim slot
    for (let p = 0; p < N; p++) {
      const v = tessIdxList[p * 3 + 0];
      const c = (provCount.get(v) || 0) + 1;
      provCount.set(v, c);
      if (c === 1) firstProvSlot.set(v, p);
      else         secondProvSlot.set(v, p);
    }
    // Identify mult-0 verts (referenced as non-provoking corners but never as provoking).
    // For type-0 (single triangle), v1 and v2 are referenced but never provoking (count=2).
    // For type-1 (fan), v2 is referenced but never provoking (count=1).
    // For type-2, v2 + edge2's last interior vert (count=2).
    const referencedAsCorner = new Set();
    for (let p = 0; p < N; p++) {
      referencedAsCorner.add(tessIdxList[p * 3 + 1]);
      referencedAsCorner.add(tessIdxList[p * 3 + 2]);
    }
    const mult0Verts = [];
    for (const v of referencedAsCorner) if (!provCount.has(v)) mult0Verts.push(v);
    mult0Verts.sort((a, b) => a - b);
    // Mult-2 verts in canonical order: by ascending geometric flat idx.
    const mult2Verts = [];
    for (const [v, c] of provCount) if (c === 2) mult2Verts.push(v);
    mult2Verts.sort((a, b) => a - b);
    const D = mult2Verts.length;
    const Z = mult0Verts.length;

    // canonicalSlot[geomVtx] -> slot in [0, N+D+Z) used for non-provoking-corner refs.
    const canonicalSlot = new Map();
    // mult-1 verts: their single provoking slot doubles as canonical.
    for (const [v, c] of provCount) {
      if (c === 1) canonicalSlot.set(v, firstProvSlot.get(v));
    }
    // mult-2 verts: assign a fresh slot at [N, N+D).
    for (let d = 0; d < D; d++) canonicalSlot.set(mult2Verts[d], N + d);
    // mult-0 verts: assign at [N+D, N+D+Z).
    for (let i = 0; i < Z; i++) canonicalSlot.set(mult0Verts[i], N + D + i);

    // Build idxBuf and reorder.
    idxBuf  = new Array(3 * N);
    reorder = new Array(N + D + Z);
    for (let p = 0; p < N; p++) {
      const v0g = tessIdxList[p * 3 + 0];
      const v1g = tessIdxList[p * 3 + 1];
      const v2g = tessIdxList[p * 3 + 2];
      idxBuf[p * 3 + 0] = p;                         // unique provoking slot
      idxBuf[p * 3 + 1] = canonicalSlot.get(v1g);
      idxBuf[p * 3 + 2] = canonicalSlot.get(v2g);
      reorder[p] = v0g;                              // slot p -> provoking geom vtx
    }
    for (let d = 0; d < D; d++) reorder[N + d] = mult2Verts[d];
    for (let i = 0; i < mult0Verts.length; i++) reorder[N + D + i] = mult0Verts[i];

    vbufStats = { N, D, Z, V: N - D + Z, mult0Count: Z };
  }
  return {
    tris, ti,
    tok0: wrap(tok0), tok1: wrap(tok1), tok2: wrap(tok2),
    triType, rows, strips, intPrims, intVtx, gap, midPt, gapLen,
    idxBuf, reorder, vbufStats,
  };
}
function buildMatchesView(tok0, tok1) {
  const lhsHalf = (Math.max(tok0.m_originalTess, 1) + 1) / 2 - 1;
  const rhsHalf = (Math.max(tok1.m_originalTess, 1) + 1) / 2 - 1;
  const nLh = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(lhsHalf);
  const nRh = TessUtil.HalfTileExpAlternatingFrac_GetNumPoints(rhsHalf);

  const seen = new Set(); // dedupe (lhsFull, rhsFull) pairs
  const matches = [];
  function add(lhs, rhs, kind, halfIdx, halfRhs) {
    const key = `${lhs},${rhs}`;
    if (seen.has(key)) return;
    seen.add(key);
    matches.push({ lhs, rhs, kind, halfIdx, halfRhs });
  }
  // Endpoints (corners v0 and v1):
  //   l[0]=r[0]            (both at t=0, the v0..v0/v2 corner side of the gap)
  //   l[2*nLh]=r[2*nRh]    (both at t=1, the v1 corner)
  add(0, 0, 'v0',  null, null);
  add(2 * nLh, 2 * nRh, 'v1', null, null);
  // Half-tile matches projected to full-tile indices.
  for (let i = 0; i < nLh; i++) {
    const m = TessUtil.HalfTileExpAlternatingFrac_MatchBelow(i, lhsHalf, rhsHalf);
    add(nLh - i, nRh - m, 'bot', i, m);
    add(nLh + i, nRh + m, 'top', i, m);
  }

  // ---------------------------------------------------------------
  // Special-case fixups for degenerate edges. The half-tile projection
  // produces (l[2*nLh], r[2*nRh]) for the v1 corner, which is wrong
  // when either side is degenerate (n=1). It can also produce strong
  // matches with rhs indices > n1 (or lhs > n0) that are out of bounds.
  // Rules:
  //   1. e1 == 0 (n1 == 1): all interior lhs match r[0] EXCEPT the last
  //      non-corner lhs which matches r[1].
  //   2. v1 corner always uses (n0, n1) directly.
  //   3. (0, 0) special: l[1] matches r[0] AND r[1] (v1 corner).
  // ---------------------------------------------------------------
  const n0 = tok0.m_numPts, n1 = tok1.m_numPts;
  if (n0 === 1 && n1 === 1) {
    // Rule 3: (0,0) case — only one link, from l[0] to r[1].
    matches.length = 0; seen.clear();
    add(0, 1, 'e0e1zero', null, null);
  } else if (n1 === 1) {
    // Rule 1: rhs is a single edge. Interior lhs[1..n0-2] all match r[0],
    // and the very last interior lhs[n0-1] matches r[1]. v1 = (n0, 1).
    matches.length = 0; seen.clear();
    add(0, 0, 'v0', null, null);
    for (let k = 1; k <= n0 - 2; k++) add(k, 0, 'edge0', null, null);
    if (n0 - 1 >= 1) add(n0 - 1, 1, 'edgeTop', null, null);
    add(n0, 1, 'v1', null, null);
  } else if (n0 === 1) {
    // Symmetric: lhs is a single edge. Interior rhs[1..n1-2] all match l[0],
    // and the very last interior rhs[n1-1] matches l[1]. v1 = (1, n1).
    matches.length = 0; seen.clear();
    add(0, 0, 'v0', null, null);
    for (let k = 1; k <= n1 - 2; k++) add(0, k, 'edge0', null, null);
    if (n1 - 1 >= 1) add(1, n1 - 1, 'edgeTop', null, null);
    add(1, n1, 'v1', null, null);
  } else {
    // Rule 2: clamp v1 corner to (n0, n1) regardless of half-tile projection.
    const v1Idx = matches.findIndex(m => m.kind === 'v1');
    if (v1Idx >= 0) {
      seen.delete(`${matches[v1Idx].lhs},${matches[v1Idx].rhs}`);
      matches.splice(v1Idx, 1);
    }
    // Drop any matches that fell out of bounds due to projection overshoot.
    for (let i = matches.length - 1; i >= 0; i--) {
      if (matches[i].lhs > n0 || matches[i].rhs > n1) {
        seen.delete(`${matches[i].lhs},${matches[i].rhs}`);
        matches.splice(i, 1);
      }
    }
    add(n0, n1, 'v1', null, null);

    // Rule 4: when 0 < e1 < 1.0 (n1 == 3 with bracket from tok 1
    // below the upper end), only the TOP non-1.0 lhs (l[n0-1]) snaps
    // to r[n1-1]. l[n0-2] keeps its regular projection (typically
    // r[1] for this rhs config) so that it strong-matches the lower
    // rhs along with the rest. Replaces only l[n0-1]'s projected
    // strong match.
    const e1Sub1 = (tok1.m_table === 0)
                || (tok1.m_table === 1 && tok1.m_lerpT < 1.0);
    if (e1Sub1 && n0 >= 3 && n1 >= 3) {
      const targetLhs = n0 - 1;
      for (let i = matches.length - 1; i >= 0; i--) {
        if (matches[i].lhs === targetLhs) {
          seen.delete(`${matches[i].lhs},${matches[i].rhs}`);
          matches.splice(i, 1);
        }
      }
      add(targetLhs, n1 - 1, 'topPair', null, null);
    }

    // Rule 5: symmetric partner of Rule 4 for the lhs side. When
    // 0 < e0 < 1.0 (n0 == 3, tok 1 lerpT < 1), the only interior lhs
    // is l[1] = l[n0-2]. The top non-1.0 lhs l[n0-1] = l[2] gets
    // strong matches to the top TWO rhs points (r[n1-1] and r[n1]),
    // while l[n0-2] = l[1] gets all the OTHER interior rhs matches.
    // Removes any prior strongs at l[1] and l[2] before re-emitting.
    // Runs AFTER Rule 4 so it overrides for the (e0<1 && e1<1) case.
    const e0Sub1 = (tok0.m_table === 0)
                || (tok0.m_table === 1 && tok0.m_lerpT < 1.0);
    if (e0Sub1 && n0 >= 3 && n1 >= 3) {
      for (const targetLhs of [n0 - 2, n0 - 1]) {
        for (let i = matches.length - 1; i >= 0; i--) {
          if (matches[i].lhs === targetLhs) {
            seen.delete(`${matches[i].lhs},${matches[i].rhs}`);
            matches.splice(i, 1);
          }
        }
      }
      // l[n0-2] gets r[1..n1-2]
      for (let k = 1; k <= n1 - 2; k++) add(n0 - 2, k, 'topRest', null, null);
      // l[n0-1] gets r[n1-1] and r[n1] (the top two)
      add(n0 - 1, n1 - 1, 'topPair', null, null);
      add(n0 - 1, n1,     'topPair', null, null);
    }
  }

  // ---------------------------------------------------------------
  // Weak matches with HALF-DEPENDENT rules.
  //
  // For each consecutive strong pair (L, R) -> (L+1, R+dR):
  //
  //   TOP half (a.lhs >= nLh):  fan from the LOWER lhs.
  //     Emit l[L] -> r[R+1], r[R+2], ..., r[R+dR]   (count: dR)
  //
  //   BOTTOM half (a.lhs <  nLh):  "up right, up left, then up right rest".
  //     Emit l[L]   -> r[R+1]                       (1 edge)
  //     Emit l[L+1] -> r[R+1]                       (1 edge — drops if redundant w/ strong)
  //     Emit l[L+1] -> r[R+2], r[R+3], ..., r[R+dR-1]   (skip the last; r[R+dR] is next strong)
  //
  // For dR == 0: no weak edges in either half.
  // For dL > 1 (not observed): skipped.
  // ---------------------------------------------------------------
  const sortedStrong = [...matches].sort((a, b) => a.lhs - b.lhs || a.rhs - b.rhs);
  const weak = [];
  function pushWeak(lhs, rhs, spanIdx) {
    // Skip if this would duplicate a strong match.
    if (sortedStrong.some(s => s.lhs === lhs && s.rhs === rhs)) return;
    weak.push({ lhs, rhs, spanIdx });
  }
  for (let i = 0; i < sortedStrong.length - 1; i++) {
    const a = sortedStrong[i], b = sortedStrong[i + 1];
    const dL = b.lhs - a.lhs, dR = b.rhs - a.rhs;
    if (dL !== 1 || dR < 1) continue;
    const isBottom = a.lhs < nLh;
    if (isBottom) {
      // Up right once.
      pushWeak(a.lhs,     a.rhs + 1, i);
      if (dR >= 2) {
        // Up left once.
        pushWeak(a.lhs + 1, a.rhs + 1, i);
        // Up right for the rest, except the very last (which is the next strong).
        for (let j = 2; j <= dR - 1; j++) {
          pushWeak(a.lhs + 1, a.rhs + j, i);
        }
      }
    } else {
      // Top half: fan from lower lhs.
      for (let j = 1; j <= dR; j++) {
        pushWeak(a.lhs, a.rhs + j, i);
      }
    }
  }

  return { matches, weak, nLh, nRh, lhsHalf, rhsHalf, n0: tok0.m_numPts, n1: tok1.m_numPts };
}

// =====================================================================
// Derive a bit list (R/L sequence) from the matches-only view's strong
// matches. Each bit is one matchstrip prim. Bottom-half spans use the
// "R, L, then dR-1 R's" rule; top-half spans use the "dR R's, then L"
// rule (these mirror the weak-edge generation in buildMatchesView).
// dL=0 spans emit dR R's; dL=k dR=0 spans emit k L's. The walk ends
// with a trailing L drop so that the strip's final state is
// (rB=n1, lB=n0-1) — matching the reference matchstrip convention.
//
// Degenerate cases (n0=1 or n1=1) emit pure R / pure L runs because
// the matchstrip in those cases never advances the degenerate side.
// =====================================================================
function buildBitsFromMatches(view) {
  const { matches, nLh, n0, n1 } = view;
  // Special case: e0=0, e1=0 → single triangle covering the gap.
  if (n0 === 1 && n1 === 1) return ['R'];

  // General walk: emit bits for each consecutive strong pair, then trim
  // any L-bit that would push lB above n0-1 (matchstrip convention).
  const sorted = [...matches].sort((a, b) => a.lhs - b.lhs || a.rhs - b.rhs);
  const raw = [];
  for (let i = 0; i < sorted.length - 1; i++) {
    const a = sorted[i], b = sorted[i + 1];
    const dL = b.lhs - a.lhs, dR = b.rhs - a.rhs;
    if (dL === 0 && dR > 0) {
      for (let j = 0; j < dR; j++) raw.push('R');
    } else if (dL > 0 && dR === 0) {
      for (let j = 0; j < dL; j++) raw.push('L');
    } else if (dL >= 1 && dR >= 1) {
      const isBottom = a.lhs < nLh;
      if (isBottom) {
        // R, then dL L's, then dR-1 more R's. Reduces to "R, L, R^(dR-1)"
        // when dL=1 (the common case).
        raw.push('R');
        for (let j = 0; j < dL; j++) raw.push('L');
        for (let j = 0; j < dR - 1; j++) raw.push('R');
      } else {
        // dR R's, then dL L's. Reduces to "R^dR, L" when dL=1.
        for (let j = 0; j < dR; j++) raw.push('R');
        for (let j = 0; j < dL; j++) raw.push('L');
      }
    }
  }
  // Drop excess L-bits (anywhere) so that final lB == n0 - 1.
  // Drop excess R-bits (anywhere) so that final rB == n1.
  // The matchstrip walk ends at (rB=n1, lB=n0-1).
  const bits = [];
  let lCount = 0, rCount = 0;
  for (const b of raw) {
    if (b === 'L') {
      if (lCount < n0 - 1) { bits.push('L'); lCount++; }
    } else {
      if (rCount < n1) { bits.push('R'); rCount++; }
    }
  }
  return bits;
}


// =====================================================================
// END TESSELLATOR
// =====================================================================
