{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://archlang.uk/plan.schema.json",
  "title": "ArchLang Plan",
  "description": "A floor plan as structured JSON (RPLAN / DStruct2Design convention). Coordinates are millimetres; the origin is top-left with +x right and +y DOWN. Fields marked output-only are produced by planToJson and ignored on input. Scripting (let/for/if/component) and import are not representable — author those in .arch source.",
  "type": "object",
  "required": [
    "plan",
    "rooms",
    "walls",
    "openings",
    "furniture"
  ],
  "additionalProperties": false,
  "properties": {
    "version": {
      "const": 1,
      "description": "Schema version. Always 1."
    },
    "plan": {
      "type": "string",
      "description": "Plan name (shown in the title block)."
    },
    "units": {
      "const": "mm",
      "description": "Distance unit. Only millimetres are supported."
    },
    "grid": {
      "type": "number",
      "description": "Snap module in millimetres; 0 or omitted disables snapping."
    },
    "scale": {
      "type": "string",
      "pattern": "^\\d+:\\d+$",
      "description": "Drawing scale, e.g. \"1:50\". Annotation only in Plan JSON — the operative-scale form needs a `paper` sheet, which is authored in .arch (see the language reference)."
    },
    "north": {
      "description": "North orientation: a cardinal keyword or an explicit bearing in degrees.",
      "oneOf": [
        {
          "enum": [
            "up",
            "down",
            "left",
            "right"
          ]
        },
        {
          "type": "object",
          "required": [
            "deg"
          ],
          "additionalProperties": false,
          "properties": {
            "deg": {
              "type": "number"
            }
          }
        }
      ]
    },
    "site": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "street"
      ],
      "description": "Where the building sits relative to its street. `street` is the TRUE compass direction the frontage faces; `hemisphere` (default `north`) decides only which way the equator lies. It draws nothing and moves nothing — it names directions, so a brief can say `equator_side` instead of `S`. NOTE: the derived names ArchLang reports from it (`back`, `equator_side`, `sunrise_side`, `sunset_side`) are a drafting convention, NOT a daylight measurement: there is no sun model, latitude or date anywhere in ArchLang.",
      "properties": {
        "street": {
          "enum": [
            "north",
            "south",
            "east",
            "west"
          ],
          "description": "Compass direction the street frontage faces."
        },
        "hemisphere": {
          "enum": [
            "north",
            "south"
          ],
          "description": "Hemisphere the building sits in. Defaults to north."
        }
      }
    },
    "dims_auto": {
      "enum": [
        "overall",
        "rooms",
        "walls",
        "all"
      ],
      "description": "`dims auto <mode>` — draw automatic dimension chains: `overall` (the two exterior chains), `rooms`, `walls`, or `all`. Omit it and no chains are drawn. This is a SETTING, not the `dims` array beside it: `dims` lists explicit `dim` statements, and the two compose. It affects output — the chains grow the drawing extent — so a document that drops it renders a differently-sized sheet."
    },
    "storey_height": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 100000,
      "description": "`height <mm>` — the plan's default floor-to-floor storey height in millimetres (default 3000). The VERTICAL datum: it draws nothing at all, because a floor plan is a horizontal cut, and exists so heights are readable facts. A wall's own `height` overrides it. Plan JSON is a single-storey projection, so per-level heights and elevations are not representable here — author those with `level <n> height <mm>` in .arch source."
    },
    "room_count": {
      "type": "integer",
      "description": "Output-only: number of rooms."
    },
    "total_area": {
      "type": "number",
      "description": "Output-only: total floor area in square metres."
    },
    "room_types": {
      "type": "array",
      "description": "Output-only: the distinct room_type values present, in first-appearance order.",
      "items": {
        "enum": [
          "LivingRoom",
          "MasterRoom",
          "Kitchen",
          "Bathroom",
          "DiningRoom",
          "ChildRoom",
          "StudyRoom",
          "SecondRoom",
          "GuestRoom",
          "Balcony",
          "Entrance",
          "Storage",
          "Room"
        ]
      }
    },
    "rooms": {
      "type": "array",
      "description": "Rooms — a rectangle (x/y/width/height) or an explicit `polygon` ring.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "anyOf": [
          {
            "required": [
              "x",
              "y",
              "width",
              "height"
            ]
          },
          {
            "required": [
              "polygon"
            ]
          }
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique room id (referenced by furniture/openings)."
          },
          "label": {
            "type": "string",
            "description": "Human-readable room label drawn in the room."
          },
          "room_type": {
            "enum": [
              "LivingRoom",
              "MasterRoom",
              "Kitchen",
              "Bathroom",
              "DiningRoom",
              "ChildRoom",
              "StudyRoom",
              "SecondRoom",
              "GuestRoom",
              "Balcony",
              "Entrance",
              "Storage",
              "Room"
            ],
            "description": "Canonical RPLAN-style room category (mapped bidirectionally from `uses`)."
          },
          "uses": {
            "type": "array",
            "description": "Explicit ArchLang function tags; authoritative over room_type when present.",
            "items": {
              "enum": [
                "living",
                "kitchen",
                "dining",
                "bedroom",
                "bath",
                "wc",
                "hall",
                "circulation",
                "storage",
                "utility",
                "office",
                "entry",
                "garage"
              ]
            }
          },
          "x": {
            "type": "number",
            "description": "Top-left corner X in millimetres."
          },
          "y": {
            "type": "number",
            "description": "Top-left corner Y in millimetres."
          },
          "width": {
            "type": "number",
            "description": "Room width in millimetres."
          },
          "height": {
            "type": "number",
            "description": "Room height in millimetres."
          },
          "polygon": {
            "type": "array",
            "minItems": 3,
            "description": "An implicitly-closed simple polygon of >=3 vertices — the room's floor when it is not a rectangle. Replaces x/y/width/height on input; on output those remain as its bounding box.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "x",
                "y"
              ],
              "properties": {
                "x": {
                  "type": "number",
                  "description": "X coordinate in millimetres (origin top-left, +x right)."
                },
                "y": {
                  "type": "number",
                  "description": "Y coordinate in millimetres (+y DOWN, screen convention)."
                }
              }
            }
          },
          "area": {
            "type": "number",
            "description": "Output-only: floor area in square metres (2 dp)."
          },
          "floor_polygon": {
            "type": "array",
            "description": "Output-only: the room's floor ring — a rectangle's four corners (clockwise from top-left), or the polygon's own vertices.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "x",
                "y"
              ],
              "properties": {
                "x": {
                  "type": "number",
                  "description": "X coordinate in millimetres (origin top-left, +x right)."
                },
                "y": {
                  "type": "number",
                  "description": "Y coordinate in millimetres (+y DOWN, screen convention)."
                }
              }
            }
          }
        }
      }
    },
    "walls": {
      "type": "array",
      "description": "Wall polylines (poché-filled; host doors/windows/openings).",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "points"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique wall id."
          },
          "category": {
            "type": "string",
            "description": "Free-form category, e.g. \"exterior\" or \"partition\"; also a host reference."
          },
          "points": {
            "type": "array",
            "minItems": 2,
            "description": "Polyline vertices in order.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "x",
                "y"
              ],
              "properties": {
                "x": {
                  "type": "number",
                  "description": "X coordinate in millimetres (origin top-left, +x right)."
                },
                "y": {
                  "type": "number",
                  "description": "Y coordinate in millimetres (+y DOWN, screen convention)."
                }
              }
            }
          },
          "thickness": {
            "type": "number",
            "description": "Wall thickness in millimetres."
          },
          "height": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100000,
            "description": "How tall the wall STANDS, in millimetres — a vertical height, NOT a plan extent like a room's `height`. Defaults to the plan's `storey_height` (3000). It draws nothing: a floor plan is a horizontal cut. An opening's `head` may not exceed it."
          },
          "closed": {
            "type": "boolean",
            "description": "Whether the polyline closes back to its first vertex."
          },
          "material": {
            "type": "string",
            "description": "Hatch material (brick, concrete, …); omitted for the default poché."
          },
          "material_scale": {
            "type": "number",
            "description": "Hatch tile-size multiplier (after material)."
          },
          "material_angle": {
            "type": "number",
            "description": "Extra hatch rotation in degrees (after material)."
          }
        }
      }
    },
    "openings": {
      "type": "array",
      "description": "Doors, windows and leaf-less cased openings — all must lie on a wall.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "kind",
          "width"
        ],
        "properties": {
          "kind": {
            "enum": [
              "door",
              "window",
              "opening"
            ],
            "description": "Opening kind."
          },
          "id": {
            "type": "string",
            "description": "Unique opening id."
          },
          "x": {
            "type": "number",
            "description": "Center X in millimetres (on the wall centerline)."
          },
          "y": {
            "type": "number",
            "description": "Center Y in millimetres (on the wall centerline)."
          },
          "on": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "wall",
              "at"
            ],
            "description": "Wall-attached placement (alternative to x/y): walk `wall` to position `at`.",
            "properties": {
              "wall": {
                "type": "string",
                "description": "Host wall id or category to walk."
              },
              "at": {
                "type": "string",
                "description": "Position along the wall: a percentage (\"40%\"), millimetres (\"1200\"), or \"center\"."
              }
            }
          },
          "width": {
            "type": "number",
            "description": "Opening width in millimetres."
          },
          "wall": {
            "type": "string",
            "description": "Host wall by id or category (else nearest)."
          },
          "hinge": {
            "enum": [
              "left",
              "right"
            ],
            "description": "Door hinge side relative to the wall direction."
          },
          "swing": {
            "enum": [
              "in",
              "out"
            ],
            "description": "Hinged: which side of the wall the leaf sweeps to. Barn/bifold: which face the panel hangs on or folds toward. Not accepted on a sliding or pocket door."
          },
          "door_kind": {
            "enum": [
              "hinged",
              "sliding",
              "barn",
              "bifold",
              "pocket",
              "garage"
            ],
            "description": "Door kind (default `hinged`, in which case this is omitted). A non-hinged kind has no swing arc; `hinge` is not accepted on one."
          },
          "slide": {
            "enum": [
              "left",
              "right"
            ],
            "description": "Which way a sliding-family panel travels to open, along the wall's direction (as `hinge` is). Sliding family only."
          },
          "open": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "How far the panel is DRAWN open, 0–1 (default 0.5). A drawing fact only — no measured output reads it. Sliding family only."
          },
          "sill": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "description": "Bottom of the glazing above this storey's floor, in millimetres (default 900 — GB 50352-2019's minimum for a residential external window). WINDOW ONLY: a door and a cased opening start at the floor. 0 is legal and means a floor-length window. Draws nothing."
          },
          "head": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100000,
            "description": "Top of the opening above this storey's floor, in millimetres — 2100 for a door or a window, the host wall's own height for a cased opening. Must be above `sill` and no higher than the host wall. Draws nothing."
          }
        }
      }
    },
    "furniture": {
      "type": "array",
      "description": "Furniture and plumbing/kitchen fixtures.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "category"
        ],
        "properties": {
          "category": {
            "type": "string",
            "description": "Furniture category, e.g. bed, sofa, wc, basin, stove."
          },
          "id": {
            "type": "string",
            "description": "Unique furniture id."
          },
          "room": {
            "type": "string",
            "description": "Owning room id (`in <room>`)."
          },
          "x": {
            "type": "number",
            "description": "Top-left corner X in millimetres (resolved footprint)."
          },
          "y": {
            "type": "number",
            "description": "Top-left corner Y in millimetres (resolved footprint)."
          },
          "width": {
            "type": "number",
            "description": "Plan-axis width in millimetres."
          },
          "height": {
            "type": "number",
            "description": "Plan-axis height in millimetres."
          },
          "rotate": {
            "enum": [
              0,
              90,
              180,
              270
            ],
            "description": "Quarter-turn rotation of the drawn symbol."
          },
          "centered": {
            "type": "boolean",
            "description": "Room-relative placement: centre inside `room`."
          },
          "anchor": {
            "enum": [
              "top-left",
              "top",
              "top-right",
              "left",
              "center",
              "right",
              "bottom-left",
              "bottom",
              "bottom-right"
            ],
            "description": "Room-relative placement: anchor to a corner/edge of `room`."
          },
          "inset": {
            "type": "number",
            "description": "Inset (mm) from the anchored edge."
          },
          "flush": {
            "type": "boolean",
            "description": "Measure the anchored edge from the backing wall's inner face (needs `anchor`)."
          },
          "against_wall": {
            "type": "string",
            "description": "Wall-anchored placement: back onto this wall id/category."
          },
          "segment": {
            "type": "number",
            "description": "Which segment of a multi-segment wall (for against_wall)."
          },
          "offset": {
            "type": "number",
            "description": "Distance (mm) along the segment (for against_wall)."
          },
          "side": {
            "enum": [
              "left",
              "right"
            ],
            "description": "Which wall face to back onto (for against_wall)."
          },
          "label": {
            "type": "string",
            "description": "Label for the generic rectangle fallback."
          }
        }
      }
    },
    "dims": {
      "type": "array",
      "description": "Dimension lines.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "from",
          "to"
        ],
        "properties": {
          "from": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number",
                "description": "X coordinate in millimetres (origin top-left, +x right)."
              },
              "y": {
                "type": "number",
                "description": "Y coordinate in millimetres (+y DOWN, screen convention)."
              }
            },
            "description": "Start point."
          },
          "to": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "x",
              "y"
            ],
            "properties": {
              "x": {
                "type": "number",
                "description": "X coordinate in millimetres (origin top-left, +x right)."
              },
              "y": {
                "type": "number",
                "description": "Y coordinate in millimetres (+y DOWN, screen convention)."
              }
            },
            "description": "End point."
          },
          "offset": {
            "type": "number",
            "description": "Perpendicular offset of the dimension line in millimetres."
          },
          "text": {
            "type": "string",
            "description": "Override text; defaults to the measured length."
          }
        }
      }
    },
    "columns": {
      "type": "array",
      "description": "Structural columns.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "x",
          "y",
          "width",
          "height"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique column id."
          },
          "x": {
            "type": "number",
            "description": "Top-left corner X in millimetres."
          },
          "y": {
            "type": "number",
            "description": "Top-left corner Y in millimetres."
          },
          "width": {
            "type": "number",
            "description": "Width in millimetres."
          },
          "height": {
            "type": "number",
            "description": "Height in millimetres."
          }
        }
      }
    },
    "title": {
      "type": "object",
      "additionalProperties": false,
      "description": "Title-block metadata.",
      "properties": {
        "project": {
          "type": "string",
          "description": "Project name."
        },
        "drawn_by": {
          "type": "string",
          "description": "Author / drafter."
        },
        "date": {
          "type": "string",
          "description": "Date string."
        }
      }
    },
    "edges": {
      "type": "array",
      "description": "Output-only: connector edges from the modeled door/opening access graph.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "from",
          "to",
          "via",
          "type"
        ],
        "properties": {
          "from": {
            "type": "string",
            "description": "One endpoint (room id or `exterior`)."
          },
          "to": {
            "type": "string",
            "description": "Other endpoint (room id or `exterior`)."
          },
          "via": {
            "enum": [
              "door",
              "opening"
            ],
            "description": "Whether the connector is a door or a cased opening."
          },
          "type": {
            "enum": [
              "interior",
              "front"
            ],
            "description": "`front` connects the exterior (an entrance); else `interior`."
          }
        }
      }
    },
    "input_graph": {
      "type": "object",
      "description": "Output-only: interior-door adjacency dict, keyed by room id → neighbouring room ids.",
      "additionalProperties": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    }
  }
}
