{"version":3,"file":"floorPlan.faces.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.faces.ts"],"sourcesContent":["/**\n * Planar face extraction: turns the wall graph into the enclosed rooms it forms.\n *\n * Each wall becomes two darts (directed half-edges), wall `i` owning darts `2i` and\n * `2i + 1`, so `twin(d) === d ^ 1` with no lookup. The darts leaving each node are\n * sorted by angle, and the face walk is:\n *\n *     next(d) = the cyclic PREDECESSOR of twin(d) in the sorted ring at head(d)\n *\n * Because the shoelace sum runs on the same raw coordinates as the angular sort, the\n * rule needs no orientation convention: bounded faces come out with POSITIVE signed\n * area and each connected component's outer boundary comes out negative, whichever way\n * you read the y axis.\n *\n * A dangling wall (one endpoint of degree 1) is handled by the rule itself: a\n * one-element ring's cyclic predecessor is itself, so the walk goes out along the spur\n * and straight back. Both of its darts land in the same cycle, no extra face appears,\n * and the face gains a zero-width slit that contributes nothing to area, fill or\n * point-in-polygon.\n */\n\nimport {\n  EPS_AREA,\n  MIN_WALL_LENGTH,\n  areaCentroid,\n  dist,\n  perimeter as ringPerimeter,\n  pointInRing,\n  representativePoint,\n  signedArea,\n} from './floorPlan.geometry';\nimport type { PlanFace, PlanFloor, PlanNode, PlanPoint, PlanWall } from './floorPlan.types';\n\ninterface Dart {\n  wallId: string;\n  from: string;\n  to: string;\n}\n\ninterface DartGraph {\n  darts: Dart[];\n  /** Node id to its outgoing dart indices, sorted by angle ascending. */\n  outgoing: Map<string, number[]>;\n  /** Dart index to its position within its own outgoing ring. */\n  ringIndex: Int32Array;\n  nodeOf: (id: string) => PlanNode;\n}\n\n/** Walls that are safe to traverse: real nodes at both ends, and long enough to have an angle. */\nfunction usableWalls(floor: PlanFloor): PlanWall[] {\n  return Object.values(floor.walls).filter((w) => {\n    if (w.a === w.b) return false;\n    const a = floor.nodes[w.a];\n    const b = floor.nodes[w.b];\n    if (!a || !b) return false;\n    return dist(a, b) >= MIN_WALL_LENGTH;\n  });\n}\n\nfunction buildDartGraph(floor: PlanFloor): DartGraph {\n  const walls = usableWalls(floor);\n  const darts: Dart[] = new Array(walls.length * 2);\n\n  for (let i = 0; i < walls.length; i++) {\n    const w = walls[i];\n    darts[2 * i] = { wallId: w.id, from: w.a, to: w.b };\n    darts[2 * i + 1] = { wallId: w.id, from: w.b, to: w.a };\n  }\n\n  const nodeOf = (id: string): PlanNode => floor.nodes[id];\n\n  const outgoing = new Map<string, number[]>();\n  for (let d = 0; d < darts.length; d++) {\n    const list = outgoing.get(darts[d].from);\n    if (list) list.push(d);\n    else outgoing.set(darts[d].from, [d]);\n  }\n\n  const angleOf = (d: number): number => {\n    const from = nodeOf(darts[d].from);\n    const to = nodeOf(darts[d].to);\n    return Math.atan2(to.y - from.y, to.x - from.x);\n  };\n\n  for (const ring of outgoing.values()) {\n    ring.sort((x, y) => {\n      const ax = angleOf(x);\n      const ay = angleOf(y);\n      if (ax !== ay) return ax - ay;\n      // Deterministic tie-break. A genuine tie means two walls leave this node in the\n      // same direction, which validatePlan reports as OverlappingWalls: the sort stays\n      // stable so the walk cannot hang, but the resulting face is still wrong.\n      const wx = darts[x].wallId;\n      const wy = darts[y].wallId;\n      if (wx !== wy) return wx < wy ? -1 : 1;\n      return x - y;\n    });\n  }\n\n  const ringIndex = new Int32Array(darts.length);\n  for (const ring of outgoing.values()) {\n    for (let i = 0; i < ring.length; i++) ringIndex[ring[i]] = i;\n  }\n\n  return { darts, outgoing, ringIndex, nodeOf };\n}\n\nfunction nextDart(g: DartGraph, d: number): number {\n  const ring = g.outgoing.get(g.darts[d].to);\n  /* c8 ignore next -- head() always has the twin in its ring by construction */\n  if (!ring || ring.length === 0) return d ^ 1;\n  const i = g.ringIndex[d ^ 1];\n  return ring[(i - 1 + ring.length) % ring.length];\n}\n\n/** Every dart belongs to exactly one cycle. */\nfunction walkCycles(g: DartGraph): number[][] {\n  const visited = new Uint8Array(g.darts.length);\n  const cycles: number[][] = [];\n\n  for (let d0 = 0; d0 < g.darts.length; d0++) {\n    if (visited[d0]) continue;\n    const cycle: number[] = [];\n    let d = d0;\n    let guard = 0;\n    do {\n      visited[d] = 1;\n      cycle.push(d);\n      d = nextDart(g, d);\n      // Only reachable if the dart permutation is corrupt (duplicate walls producing\n      // an exact angular tie). Bail rather than spin.\n      if (++guard > g.darts.length + 1) break;\n    } while (d !== d0 && !visited[d]);\n    cycles.push(cycle);\n  }\n\n  return cycles;\n}\n\n/** Union-find over node ids, joined by each wall. */\nfunction connectedComponents(g: DartGraph): Map<string, string> {\n  const parent = new Map<string, string>();\n  const find = (x: string): string => {\n    let root = x;\n    while (parent.get(root) !== root) root = parent.get(root) ?? root;\n    let cur = x;\n    while (parent.get(cur) !== root) {\n      const nxt = parent.get(cur) ?? root;\n      parent.set(cur, root);\n      cur = nxt;\n    }\n    return root;\n  };\n\n  for (const nodeId of g.outgoing.keys()) parent.set(nodeId, nodeId);\n  for (let d = 0; d < g.darts.length; d += 2) {\n    const ra = find(g.darts[d].from);\n    const rb = find(g.darts[d].to);\n    if (ra !== rb) parent.set(ra, rb);\n  }\n\n  const out = new Map<string, string>();\n  for (const nodeId of g.outgoing.keys()) out.set(nodeId, find(nodeId));\n  return out;\n}\n\nfunction cycleRing(g: DartGraph, cycle: number[]): PlanPoint[] {\n  return cycle.map((d) => {\n    const n = g.nodeOf(g.darts[d].from);\n    return { x: n.x, y: n.y };\n  });\n}\n\n/**\n * A face id derived from the boundary itself, so it stays stable across derives for as\n * long as the geometry is unchanged.\n */\nfunction faceId(g: DartGraph, cycle: number[]): string {\n  let best = '';\n  for (const d of cycle) {\n    const key = `${g.darts[d].wallId}>${g.darts[d].from}`;\n    if (best === '' || key < best) best = key;\n  }\n  return `f:${best}`;\n}\n\n/**\n * Extracts every enclosed face from the wall graph, with nested components attached as\n * holes.\n *\n * Returned faces are positively wound (visually clockwise in SVG space) and their\n * `area` already has holes subtracted.\n */\nexport function deriveFaces(floor: PlanFloor): PlanFace[] {\n  const g = buildDartGraph(floor);\n  if (g.darts.length === 0) return [];\n\n  const componentOf = connectedComponents(g);\n  const cycles = walkCycles(g);\n\n  interface Candidate {\n    face: PlanFace;\n    component: string;\n    outerArea: number;\n  }\n\n  const candidates: Candidate[] = [];\n  /** One outer (negatively wound) cycle per connected component. */\n  const outerRings = new Map<string, PlanPoint[]>();\n\n  for (const cycle of cycles) {\n    const ring = cycleRing(g, cycle);\n    const area = signedArea(ring);\n    const component = componentOf.get(g.darts[cycle[0]].from) ?? '';\n\n    if (area > EPS_AREA) {\n      candidates.push({\n        face: {\n          id: faceId(g, cycle),\n          outer: ring,\n          holes: [],\n          area,\n          perimeter: ringPerimeter(ring),\n          wallIds: cycle.map((d) => g.darts[d].wallId),\n          nodeIds: cycle.map((d) => g.darts[d].from),\n        },\n        component,\n        outerArea: area,\n      });\n    } else if (area < -EPS_AREA) {\n      // This component's outer boundary. Kept at its negative winding, which is what\n      // `PlanFace.holes` promises: opposite to the parent's positively wound `outer`.\n      outerRings.set(component, ring);\n    }\n    // |area| <= EPS_AREA encloses nothing: a tree component or an isolated stub.\n  }\n\n  // Attach each component's outer boundary as a hole in the smallest face from a\n  // different component that encloses it. Smallest-containing-area is what makes\n  // arbitrary nesting depth work: with A > B > C, C's ring goes to B, never to A.\n  for (const [component, ring] of outerRings) {\n    const ringArea = Math.abs(signedArea(ring));\n    if (ringArea <= EPS_AREA) continue;\n    // An interior probe rather than a vertex, so two components that happen to touch\n    // do not produce an on-boundary coin flip.\n    const probe = representativePoint(ring);\n    let parent: Candidate | null = null;\n    for (const cand of candidates) {\n      if (cand.component === component) continue;\n      // The probe test alone is not sufficient: with one component nested inside\n      // another, the OUTER component's probe also falls inside the inner component's\n      // face, which would attach the big room as a hole of the small one. A face can\n      // only enclose a ring smaller than itself.\n      if (cand.outerArea <= ringArea) continue;\n      if (!pointInRing(probe, cand.face.outer)) continue;\n      if (!parent || cand.outerArea < parent.outerArea) parent = cand;\n    }\n    // No parent simply means this is a top-level component.\n    if (parent) parent.face.holes.push(ring);\n  }\n\n  for (const cand of candidates) {\n    let area = cand.outerArea;\n    for (const hole of cand.face.holes) area -= Math.abs(signedArea(hole));\n    cand.face.area = area;\n  }\n\n  return candidates.map((c) => c.face);\n}\n\n/**\n * Euler's formula per connected component: `V - E + F === 2C`, counting every cycle as\n * a face, including each component's own outer boundary.\n *\n * Note it is `2C`, not the `1 + C` of a single connected embedding. The dart walk\n * cannot bridge between disconnected components, so the region between two of them is\n * produced once per component rather than once in total.\n *\n * Exported for tests and dev assertions: it is the cheapest whole-graph check that the\n * dart permutation is intact.\n */\nexport function eulerCheck(floor: PlanFloor): {\n  v: number;\n  e: number;\n  f: number;\n  c: number;\n  holds: boolean;\n} {\n  const g = buildDartGraph(floor);\n  const componentOf = connectedComponents(g);\n  const v = g.outgoing.size;\n  const e = g.darts.length / 2;\n  const f = walkCycles(g).length;\n  const c = new Set(componentOf.values()).size;\n  return { v, e, f, c, holds: v - e + f === 2 * c };\n}\n\n/** Test/diagnostic helper: how many cycles each dart appears in. Must be all 1s. */\nexport function dartCycleCounts(floor: PlanFloor): number[] {\n  const g = buildDartGraph(floor);\n  const counts = new Array<number>(g.darts.length).fill(0);\n  for (const cycle of walkCycles(g)) for (const d of cycle) counts[d] += 1;\n  return counts;\n}\n\n/** Degree of each node, by id. Used by wall-path chaining and by validation. */\nexport function nodeDegrees(floor: PlanFloor): Map<string, number> {\n  const degrees = new Map<string, number>();\n  for (const w of usableWalls(floor)) {\n    degrees.set(w.a, (degrees.get(w.a) ?? 0) + 1);\n    degrees.set(w.b, (degrees.get(w.b) ?? 0) + 1);\n  }\n  return degrees;\n}\n\nexport { areaCentroid };\n"],"mappings":"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"}