{"version":3,"file":"floorPlan.json.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.json.ts"],"sourcesContent":["/**\n * Reading and writing a plan as JSON, which is the interchange format.\n *\n * Writing is trivial. Reading is not: the text comes off a disk, so it is untrusted, and\n * the component that receives the result renders it immediately. `planFromJson` therefore\n * rebuilds the plan field by field rather than casting the parsed object, so nothing that\n * was not checked can reach the renderer, and returns everything it had to fix so a\n * caller can tell the difference between a clean file and a salvaged one.\n *\n * It repairs rather than rejects wherever a repair is unambiguous: a wall whose node is\n * missing is dropped, a thickness of `\"200\"` is read as 200, an unknown wall type becomes\n * `Interior`. A file is only refused when there is nothing to salvage, because a plan a\n * user spent an hour on should not be lost to one bad field.\n */\n\nimport { cleanupPlan, newId } from './floorPlan.graph';\nimport { emptyFloor } from './floorPlan.plan';\nimport { DoorSwing, OpeningKind, PlanUnitSystem, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanNode, PlanOpening, PlanPoint, PlanRoom, PlanWall } from './floorPlan.types';\n\n/** The only `version` a file may carry. Bump only for a breaking shape change. */\nexport const PLAN_FORMAT_VERSION = 1;\n\nexport interface PlanParseResult {\n  /** `null` only when the file could not be read as a plan at all. */\n  plan: FloorPlan | null;\n  /**\n   * Everything that had to be repaired or dropped, in plain language. Empty means the\n   * file was clean. A non-empty list with a plan means it was salvaged.\n   */\n  issues: string[];\n}\n\n/** A plan as JSON text, ready to write to a file. Indented unless told otherwise. */\nexport function planToJson(plan: FloorPlan, options: { pretty?: boolean } = {}): string {\n  return JSON.stringify(plan, null, options.pretty === false ? undefined : 2);\n}\n\n// ─── Field readers ─────────────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** A finite number, also accepting the numeric strings a hand-edited file tends to grow. */\nfunction num(value: unknown, fallback: number): number {\n  const n = typeof value === 'string' ? Number(value) : value;\n  return typeof n === 'number' && Number.isFinite(n) ? n : fallback;\n}\n\nfunction str(value: unknown, fallback: string): string {\n  return typeof value === 'string' ? value : fallback;\n}\n\nfunction bool(value: unknown, fallback: boolean): boolean {\n  return typeof value === 'boolean' ? value : fallback;\n}\n\nfunction oneOf<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {\n  return typeof value === 'string' && (allowed as readonly string[]).includes(value) ? (value as T) : fallback;\n}\n\nfunction point(value: unknown, fallback: PlanPoint): PlanPoint {\n  if (!isRecord(value)) return fallback;\n  return { x: num(value.x, fallback.x), y: num(value.y, fallback.y) };\n}\n\n/**\n * The entries of a `Record<string, T>`, tolerating the array a different exporter might\n * have written. Entries without a usable `id` are dropped by the caller.\n */\nfunction entries(value: unknown): unknown[] {\n  if (Array.isArray(value)) return value;\n  if (isRecord(value)) return Object.values(value);\n  return [];\n}\n\nfunction byId<T extends { id: string }>(items: T[]): Record<string, T> {\n  const out: Record<string, T> = {};\n  for (const item of items) out[item.id] = item;\n  return out;\n}\n\n// ─── Floor ─────────────────────────────────────────────────────────────────────\n\nfunction readFloor(raw: unknown, index: number, issues: string[]): PlanFloor {\n  const where = `floor ${index + 1}`;\n  const source = isRecord(raw) ? raw : {};\n  if (!isRecord(raw)) issues.push(`${where} was not an object, so it was read as empty`);\n\n  const floor = emptyFloor();\n  floor.id = str(source.id, floor.id);\n  floor.label = str(source.label, floor.label);\n  floor.elevation = num(source.elevation, 0);\n  floor.height = num(source.height, floor.height);\n\n  const nodes: PlanNode[] = [];\n  for (const raw of entries(source.nodes)) {\n    if (!isRecord(raw) || typeof raw.id !== 'string') continue;\n    nodes.push({ id: raw.id, x: num(raw.x, 0), y: num(raw.y, 0) });\n  }\n  floor.nodes = byId(nodes);\n\n  const walls: PlanWall[] = [];\n  let orphanWalls = 0;\n  for (const raw of entries(source.walls)) {\n    if (!isRecord(raw) || typeof raw.id !== 'string') continue;\n    const a = str(raw.a, '');\n    const b = str(raw.b, '');\n    // A wall with no node to stand on would break face extraction, which assumes every\n    // wall resolves. There is no way to guess where it was meant to go.\n    if (!floor.nodes[a] || !floor.nodes[b] || a === b) {\n      orphanWalls += 1;\n      continue;\n    }\n    walls.push({\n      id: raw.id,\n      a,\n      b,\n      thickness: Math.max(1, num(raw.thickness, 100)),\n      height: raw.height === undefined ? undefined : num(raw.height, 0),\n      baseHeight: raw.baseHeight === undefined ? undefined : num(raw.baseHeight, 0),\n      type: oneOf(raw.type, Object.values(WallType), WallType.Interior),\n    });\n  }\n  floor.walls = byId(walls);\n  if (orphanWalls > 0) {\n    issues.push(`${where}: dropped ${orphanWalls} wall(s) whose corners were missing`);\n  }\n\n  const openings: PlanOpening[] = [];\n  let orphanOpenings = 0;\n  for (const raw of entries(source.openings)) {\n    if (!isRecord(raw) || typeof raw.id !== 'string') continue;\n    const wallId = str(raw.wallId, '');\n    if (!floor.walls[wallId]) {\n      orphanOpenings += 1;\n      continue;\n    }\n    openings.push({\n      id: raw.id,\n      wallId,\n      kind: oneOf(raw.kind, Object.values(OpeningKind), OpeningKind.Door),\n      offset: num(raw.offset, 0),\n      width: Math.max(1, num(raw.width, 800)),\n      sill: num(raw.sill, 0),\n      head: num(raw.head, 2100),\n      swing: raw.swing === undefined ? undefined : oneOf(raw.swing, Object.values(DoorSwing), DoorSwing.StartLeft),\n    });\n  }\n  floor.openings = byId(openings);\n  if (orphanOpenings > 0) {\n    issues.push(`${where}: dropped ${orphanOpenings} opening(s) with no wall to sit in`);\n  }\n\n  const rooms: PlanRoom[] = [];\n  for (const raw of entries(source.rooms)) {\n    if (!isRecord(raw) || typeof raw.id !== 'string') continue;\n    // A room is only a name attached to a face, and the face is recomputed on load, so a\n    // room whose walls have gone is kept rather than dropped: re-adding the wall brings\n    // its name back, and dropping it is the one way this model loses data for good.\n    rooms.push({\n      id: raw.id,\n      name: str(raw.name, ''),\n      colour: typeof raw.colour === 'string' ? raw.colour : undefined,\n      usage: typeof raw.usage === 'string' ? raw.usage : undefined,\n      anchor: point(raw.anchor, { x: 0, y: 0 }),\n      anchorPinned: bool(raw.anchorPinned, false),\n      seq: num(raw.seq, rooms.length),\n      wallIds: Array.isArray(raw.wallIds)\n        ? raw.wallIds.filter((id): id is string => typeof id === 'string' && !!floor.walls[id])\n        : [],\n      area: num(raw.area, 0),\n    });\n  }\n  floor.rooms = byId(rooms);\n\n  const cleaned = cleanupPlan(floor);\n  if (cleaned.result.length > 0) {\n    issues.push(`${where}: repaired ${cleaned.result.length} inconsistency(ies) in the geometry`);\n  }\n  return cleaned.floor;\n}\n\n// ─── Document ──────────────────────────────────────────────────────────────────\n\n/**\n * Reads JSON text as a plan, repairing what it can and reporting what it could not.\n *\n * `plan` is `null` only when the text is not JSON, is not an object, or carries a\n * `version` this build does not know. Everything else comes back as a usable plan.\n */\nexport function planFromJson(text: string): PlanParseResult {\n  const issues: string[] = [];\n\n  let raw: unknown;\n  try {\n    raw = JSON.parse(text);\n  } catch (error) {\n    return { plan: null, issues: [`Not valid JSON: ${(error as Error).message}`] };\n  }\n\n  if (!isRecord(raw)) return { plan: null, issues: ['Not a floor plan: expected an object'] };\n\n  if (raw.version !== undefined && raw.version !== PLAN_FORMAT_VERSION) {\n    // Refusing beats guessing: a future version may mean something different by the same\n    // field names, and silently misreading it loses the file's real contents.\n    return {\n      plan: null,\n      issues: [\n        `Plan format version ${String(raw.version)} is not supported ` +\n          `(this build reads version ${PLAN_FORMAT_VERSION})`,\n      ],\n    };\n  }\n  if (raw.version === undefined) issues.push('No format version, read as version 1');\n\n  const rawFloors = Array.isArray(raw.floors) ? raw.floors : [];\n  if (rawFloors.length === 0) {\n    if (raw.floors !== undefined) issues.push('No floors in the file, started an empty one');\n    else return { plan: null, issues: ['Not a floor plan: no `floors`'] };\n  }\n\n  const floors = (rawFloors.length > 0 ? rawFloors : [undefined]).map((floor, i) => readFloor(floor, i, issues));\n\n  return {\n    plan: {\n      id: str(raw.id, newId('plan')),\n      name: typeof raw.name === 'string' ? raw.name : undefined,\n      version: PLAN_FORMAT_VERSION,\n      unitSystem: oneOf(raw.unitSystem, Object.values(PlanUnitSystem), PlanUnitSystem.Metric),\n      floors,\n      // Bumped, not carried over: the plan in hand is not the one the file described if\n      // anything was repaired, and `derivePlan` memoises on this.\n      revision: num(raw.revision, 0) + 1,\n    },\n    issues,\n  };\n}\n"],"mappings":"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"}