{"version":3,"file":"floorPlan.triangulate.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.triangulate.ts"],"sourcesContent":["/**\n * Ear clipping for simple polygons, with holes bridged into the outer ring first.\n *\n * Written out rather than pulled in: a floor slab is the only thing in this package that\n * needs a triangulator, and a dependency is a poor trade for one file.\n *\n * The rooms this runs on come from face extraction, so they are already closed, already\n * consistently wound, and free of self-intersections. That is what makes plain ear\n * clipping enough: it is O(n²) and it cannot cope with a self-intersecting ring, and\n * neither limit matters for a room with a few dozen corners.\n */\n\nimport { signedArea, stripSpurs } from './floorPlan.geometry';\nimport type { PlanPoint } from './floorPlan.types';\n\nconst EPS = 1e-9;\n\nfunction area2(a: PlanPoint, b: PlanPoint, c: PlanPoint): number {\n  return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n  return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;\n}\n\n/** Inclusive of the edges: a vertex lying on one would still leave a sliver behind. */\nfunction inTriangle(p: PlanPoint, a: PlanPoint, b: PlanPoint, c: PlanPoint): boolean {\n  const d1 = area2(p, a, b);\n  const d2 = area2(p, b, c);\n  const d3 = area2(p, c, a);\n  const neg = d1 < -EPS || d2 < -EPS || d3 < -EPS;\n  const pos = d1 > EPS || d2 > EPS || d3 > EPS;\n  return !(neg && pos);\n}\n\n/**\n * Joins each hole to the outer ring with a pair of coincident edges, leaving one ring\n * that ear clipping can take. The bridge runs from the hole's rightmost vertex to a\n * visible vertex of the outer ring, which is the standard construction: going rightwards\n * from the extreme point of a hole must leave it, so the first outer edge hit is visible\n * from it.\n */\nfunction bridgeHoles(outer: PlanPoint[], holes: PlanPoint[][]): PlanPoint[] {\n  let ring = outer.slice();\n\n  // Rightmost hole first: bridging it cannot cross a hole further right.\n  const queue = holes\n    .filter((h) => h.length >= 3)\n    .map((h) => ({ ring: h, index: h.reduce((best, p, i) => (p.x > h[best].x ? i : best), 0) }))\n    .sort((a, b) => b.ring[b.index].x - a.ring[a.index].x);\n\n  for (const hole of queue) {\n    const from = hole.ring[hole.index];\n\n    // The outer vertex to bridge to: the one that is visible and, among those, closest.\n    let best = -1;\n    let bestDist = Infinity;\n    for (let i = 0; i < ring.length; i++) {\n      if (ring[i].x < from.x) continue; // must lie to the right, or the bridge doubles back\n      const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n      if (d < bestDist && visible(from, ring[i], ring, hole.ring)) {\n        bestDist = d;\n        best = i;\n      }\n    }\n    // Nothing to the right was visible, so fall back to the nearest vertex anywhere. The\n    // ring stays closed either way; at worst the slab gains a seam nobody can see.\n    if (best < 0) {\n      for (let i = 0; i < ring.length; i++) {\n        const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n        if (d < bestDist) {\n          bestDist = d;\n          best = i;\n        }\n      }\n    }\n\n    // Walk the hole from its bridge vertex, then come back along the bridge.\n    const woven: PlanPoint[] = [];\n    for (let k = 0; k <= hole.ring.length; k++) {\n      woven.push(hole.ring[(hole.index + k) % hole.ring.length]);\n    }\n    ring = [...ring.slice(0, best + 1), ...woven, ...ring.slice(best)];\n  }\n\n  return ring;\n}\n\n/** Whether the segment from `a` to `b` crosses any edge of the rings it has to avoid. */\nfunction visible(a: PlanPoint, b: PlanPoint, ring: PlanPoint[], hole: PlanPoint[]): boolean {\n  for (const poly of [ring, hole]) {\n    for (let i = 0; i < poly.length; i++) {\n      const p = poly[i];\n      const q = poly[(i + 1) % poly.length];\n      if (p === a || q === a || p === b || q === b) continue;\n      const d1 = area2(a, b, p);\n      const d2 = area2(a, b, q);\n      const d3 = area2(p, q, a);\n      const d4 = area2(p, q, b);\n      if (d1 * d2 < 0 && d3 * d4 < 0) return false;\n    }\n  }\n  return true;\n}\n\n/**\n * Triangulates a ring and its holes into index triples over the returned vertex list.\n *\n * The vertices come back as well as the indices because bridging a hole duplicates the\n * two vertices it joins, so the caller cannot simply index into what it passed in.\n */\nexport function triangulate(\n  outer: readonly PlanPoint[],\n  holes: readonly (readonly PlanPoint[])[] = [],\n): { vertices: PlanPoint[]; indices: number[] } {\n  // A dangling wall inside a room puts the same vertex in the ring twice, which is not a\n  // simple polygon and would stall ear clipping on a zero-area ear.\n  const cleanOuter = stripSpurs(outer);\n  if (cleanOuter.length < 3) return { vertices: [], indices: [] };\n\n  const ring = bridgeHoles(\n    cleanOuter,\n    holes.map((h) => stripSpurs(h)).filter((h) => h.length >= 3),\n  );\n\n  // Ear clipping wants a counter-clockwise ring under the usual maths convention.\n  const vertices = signedArea(ring) < 0 ? ring.slice().reverse() : ring.slice();\n\n  const live = vertices.map((_, i) => i);\n  const indices: number[] = [];\n  let guard = live.length * live.length;\n\n  while (live.length > 3 && guard-- > 0) {\n    let clipped = false;\n    for (let i = 0; i < live.length; i++) {\n      const ai = live[(i + live.length - 1) % live.length];\n      const bi = live[i];\n      const ci = live[(i + 1) % live.length];\n      const a = vertices[ai];\n      const b = vertices[bi];\n      const c = vertices[ci];\n      if (area2(a, b, c) <= EPS) continue; // reflex or degenerate: not an ear\n\n      let blocked = false;\n      for (const vi of live) {\n        if (vi === ai || vi === bi || vi === ci) continue;\n        const v = vertices[vi];\n        // Bridging a hole duplicates the two vertices it joins, so a candidate can sit\n        // exactly on a corner of the ear. Counting that as \"inside\" blocks every ear in\n        // the ring and the triangulation comes back empty.\n        if (samePoint(v, a) || samePoint(v, b) || samePoint(v, c)) continue;\n        if (inTriangle(v, a, b, c)) {\n          blocked = true;\n          break;\n        }\n      }\n      if (blocked) continue;\n\n      indices.push(ai, bi, ci);\n      live.splice(i, 1);\n      clipped = true;\n      break;\n    }\n    // No ear found: the ring is degenerate in a way this cannot reduce further. Stopping\n    // beats spinning, and the caller gets whatever was triangulated so far.\n    if (!clipped) break;\n  }\n  if (live.length === 3) indices.push(live[0], live[1], live[2]);\n\n  return { vertices, indices };\n}\n"],"mappings":"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"}