{"version":3,"file":"floorPlan.plan.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.plan.ts"],"sourcesContent":["/**\n * Document-level helpers: creating a plan, addressing a floor within it, and the\n * revision counter that memoisation keys off.\n */\n\nimport { boundsOf } from './floorPlan.geometry';\nimport { freshName, newId } from './floorPlan.graph';\nimport { PlanUnitSystem } from './floorPlan.types';\nimport type { FloorPlan, PlanEdit, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface EmptyFloorPlanOptions {\n  id?: string;\n  name?: string;\n  floorId?: string;\n  floorLabel?: string;\n  unitSystem?: PlanUnitSystem;\n  /** Default storey height in millimetres. */\n  height?: number;\n}\n\nexport function emptyFloor(options: Omit<EmptyFloorPlanOptions, 'id' | 'name'> = {}): PlanFloor {\n  return {\n    id: options.floorId ?? newId('f'),\n    label: options.floorLabel ?? 'Ground floor',\n    elevation: 0,\n    height: options.height ?? 2500,\n    nodes: {},\n    walls: {},\n    openings: {},\n    rooms: {},\n  };\n}\n\nexport function emptyFloorPlan(options: EmptyFloorPlanOptions = {}): FloorPlan {\n  return {\n    id: options.id ?? newId('plan'),\n    name: options.name,\n    version: 1,\n    unitSystem: options.unitSystem ?? PlanUnitSystem.Metric,\n    floors: [emptyFloor(options)],\n    revision: 0,\n  };\n}\n\nexport function getFloor(plan: FloorPlan, floorId?: string): PlanFloor | null {\n  if (!floorId) return plan.floors[0] ?? null;\n  return plan.floors.find((f) => f.id === floorId) ?? null;\n}\n\n/**\n * Applies a floor-level edit to one floor of a plan, bumping `revision` so memoised\n * derivations know to recompute.\n *\n * The revision only moves when the edit actually returned a different floor, so a\n * gesture that changed nothing does not invalidate anything.\n */\nexport function updateFloor<T>(\n  plan: FloorPlan,\n  floorId: string | undefined,\n  edit: (floor: PlanFloor) => PlanEdit<T>,\n): PlanEdit<T> & { plan: FloorPlan } {\n  const floor = getFloor(plan, floorId);\n  if (!floor) {\n    return { plan, floor: emptyFloor(), result: undefined as T, issues: [] };\n  }\n\n  const applied = edit(floor);\n  if (applied.floor === floor) {\n    return { ...applied, plan };\n  }\n\n  return {\n    ...applied,\n    plan: {\n      ...plan,\n      floors: plan.floors.map((f) => (f.id === floor.id ? applied.floor : f)),\n      revision: plan.revision + 1,\n    },\n  };\n}\n\n/** Replaces one floor outright, bumping the revision. */\nexport function setFloor(plan: FloorPlan, floor: PlanFloor): FloorPlan {\n  const exists = plan.floors.some((f) => f.id === floor.id);\n  return {\n    ...plan,\n    floors: exists ? plan.floors.map((f) => (f.id === floor.id ? floor : f)) : [...plan.floors, floor],\n    revision: plan.revision + 1,\n  };\n}\n\n// ─── Floors ────────────────────────────────────────────────────────────────────\n\n/**\n * Floors run bottom to top: index 0 is the lowest storey, and `elevation` rises with\n * the index. Nothing enforces that beyond these helpers, but `floorBelow` and the\n * drawer's underlay both read it, so an out-of-order `floors` array shows the wrong\n * storey underneath.\n */\nexport function floorIndex(plan: FloorPlan, floorId: string): number {\n  return plan.floors.findIndex((f) => f.id === floorId);\n}\n\n/** The storey directly under this one, which is what an underlay traces. */\nexport function floorBelow(plan: FloorPlan, floorId: string): PlanFloor | null {\n  const i = floorIndex(plan, floorId);\n  return i > 0 ? plan.floors[i - 1] : null;\n}\n\nexport interface AddFloorOptions {\n  label?: string;\n  /**\n   * Names the floor when `label` is not given. `n` is the floor count plus one, moved past\n   * any number whose name is taken. Default `` (n) => `Floor ${n}` ``.\n   */\n  nameFor?: (n: number) => string;\n  /** Millimetres. Defaults to the storey height of the floor it is added above. */\n  height?: number;\n  /** Where to put it. Defaults to directly above `aboveFloorId`, else on top. */\n  index?: number;\n  id?: string;\n}\n\nconst defaultFloorName = (n: number) => `Floor ${n}`;\n\n/**\n * Adds an empty storey and returns the plan with it, plus the floor itself.\n *\n * Its elevation is the top of the storey below, so a plan stays physically coherent\n * without the caller doing arithmetic. Everything above is pushed up by the new storey's\n * height, since inserting a floor into a building raises the ones over it.\n */\nexport function addFloor(plan: FloorPlan, options: AddFloorOptions = {}): { plan: FloorPlan; floor: PlanFloor } {\n  const base = plan.floors[0]?.elevation ?? 0;\n  const at = Math.max(0, Math.min(options.index ?? plan.floors.length, plan.floors.length));\n  const below = plan.floors[at - 1];\n  const height = options.height ?? below?.height ?? 2500;\n\n  const floor: PlanFloor = {\n    ...emptyFloor({ height }),\n    id: options.id ?? newId('f'),\n    label:\n      options.label ??\n      freshName(options.nameFor ?? defaultFloorName, plan.floors.length + 1, new Set(plan.floors.map((f) => f.label))),\n    elevation: below ? below.elevation + below.height : 0,\n  };\n\n  const floors = [...plan.floors.slice(0, at), floor, ...plan.floors.slice(at)];\n  return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor };\n}\n\n/**\n * Removes a storey, unless it is the last one: a plan with no floors has nowhere to\n * draw, and every consumer would need a special case for it.\n */\nexport function removeFloor(plan: FloorPlan, floorId: string): FloorPlan {\n  if (plan.floors.length <= 1) return plan;\n  const base = plan.floors[0].elevation;\n  const floors = plan.floors.filter((f) => f.id !== floorId);\n  if (floors.length === plan.floors.length) return plan;\n  return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Copies a storey, including its geometry, directly above itself.\n *\n * Every id is regenerated and every reference rewired to the copy, so the two storeys\n * share nothing: an edit to one must not reach through into the other. This is the usual\n * way to start an upper floor, since the walls below are mostly where the walls above go.\n */\nexport function duplicateFloor(\n  plan: FloorPlan,\n  floorId: string,\n  options: { label?: string } = {},\n): { plan: FloorPlan; floor: PlanFloor | null } {\n  const source = getFloor(plan, floorId);\n  if (!source) return { plan, floor: null };\n\n  const nodeIds = new Map(Object.keys(source.nodes).map((id) => [id, newId('n')]));\n  const wallIds = new Map(Object.keys(source.walls).map((id) => [id, newId('w')]));\n\n  const copy: PlanFloor = {\n    ...source,\n    id: newId('f'),\n    label: options.label ?? `${source.label} copy`,\n    nodes: {},\n    walls: {},\n    openings: {},\n    rooms: {},\n  };\n\n  for (const node of Object.values(source.nodes)) {\n    const id = nodeIds.get(node.id)!;\n    copy.nodes[id] = { ...node, id };\n  }\n  for (const wall of Object.values(source.walls)) {\n    const id = wallIds.get(wall.id)!;\n    copy.walls[id] = { ...wall, id, a: nodeIds.get(wall.a)!, b: nodeIds.get(wall.b)! };\n  }\n  for (const opening of Object.values(source.openings)) {\n    const wallId = wallIds.get(opening.wallId);\n    if (!wallId) continue;\n    const id = newId('o');\n    copy.openings[id] = { ...opening, id, wallId };\n  }\n  for (const room of Object.values(source.rooms)) {\n    const id = newId('r');\n    copy.rooms[id] = {\n      ...room,\n      id,\n      anchor: { ...room.anchor },\n      // Room identity is matched on these, so they have to point at the copy's own walls\n      // or the first derive would strand every room in it.\n      wallIds: room.wallIds.map((w) => wallIds.get(w)).filter((w): w is string => !!w),\n    };\n  }\n\n  const base = plan.floors[0].elevation;\n  const at = floorIndex(plan, floorId) + 1;\n  const floors = [...plan.floors.slice(0, at), copy, ...plan.floors.slice(at)];\n  return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor: copy };\n}\n\n/** Millimetres. Low enough for a crawl space, high enough to stop a zero-height storey. */\nexport const MIN_FLOOR_HEIGHT = 500;\n\n/**\n * Sets a storey's height, and drops or lifts everything above it to suit.\n *\n * This is why it is not a plain `setFloor`: a storey's height is what the next one sits\n * on, so changing it without re-stacking leaves the floors above either buried in it or\n * floating over it, which nothing in 2D would ever show.\n */\nexport function setFloorHeight(plan: FloorPlan, floorId: string, height: number): FloorPlan {\n  const floor = getFloor(plan, floorId);\n  const next = Math.max(MIN_FLOOR_HEIGHT, Math.round(height));\n  if (!floor || floor.height === next) return plan;\n\n  const base = plan.floors[0].elevation;\n  const floors = plan.floors.map((f) => (f.id === floor.id ? { ...f, height: next } : f));\n  return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/** Renames a storey. Its own field, since nothing else about a floor is text. */\nexport function renameFloor(plan: FloorPlan, floorId: string, label: string): FloorPlan {\n  const floor = getFloor(plan, floorId);\n  if (!floor || floor.label === label) return plan;\n  return setFloor(plan, { ...floor, label });\n}\n\n/** Moves a storey up or down the stack, re-stacking the elevations behind it. */\nexport function moveFloor(plan: FloorPlan, floorId: string, toIndex: number): FloorPlan {\n  const from = floorIndex(plan, floorId);\n  const to = Math.max(0, Math.min(toIndex, plan.floors.length - 1));\n  if (from < 0 || from === to) return plan;\n  const base = plan.floors[0].elevation;\n  const floors = [...plan.floors];\n  const [moved] = floors.splice(from, 1);\n  floors.splice(to, 0, moved);\n  return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Re-stacks elevations so each storey sits on the one below. Without this, adding or\n * removing a floor leaves a gap or an overlap in the building that only shows up in 3D.\n *\n * `base` is the building's own ground level and must come from the plan BEFORE the\n * edit: reading it off whichever floor ends up lowest moves the whole building whenever\n * two storeys of different heights are reordered.\n */\nfunction reElevate(floors: PlanFloor[], base: number): PlanFloor[] {\n  let elevation = base;\n  return floors.map((floor) => {\n    const next = floor.elevation === elevation ? floor : { ...floor, elevation };\n    elevation += floor.height;\n    return next;\n  });\n}\n\nexport interface PlanBounds {\n  minX: number;\n  minY: number;\n  maxX: number;\n  maxY: number;\n  width: number;\n  height: number;\n}\n\n/**\n * Bounding box of a floor's geometry, padded by half the thickest wall so the outer\n * face of every wall is inside it rather than its centreline.\n */\nexport function floorBounds(floor: PlanFloor): PlanBounds | null {\n  const points: PlanPoint[] = Object.values(floor.nodes).map((n) => ({ x: n.x, y: n.y }));\n  const raw = boundsOf(points);\n  if (!raw) return null;\n\n  let maxThickness = 0;\n  for (const wall of Object.values(floor.walls)) {\n    if (wall.thickness > maxThickness) maxThickness = wall.thickness;\n  }\n  const pad = maxThickness / 2;\n\n  const minX = raw.minX - pad;\n  const minY = raw.minY - pad;\n  const maxX = raw.maxX + pad;\n  const maxY = raw.maxY + pad;\n  return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n"],"mappings":"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"}