{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yarramate.org/schema/visual-layout/v1",
  "title": "YarraMate visual layout",
  "description": "Adapter-owned drag-position sidecar for one saved projection's canvas (ADR 0023). Never validated by Core, never routed through apply.",
  "type": "object",
  "additionalProperties": false,
  "required": ["format", "projectionId", "positions"],
  "properties": {
    "format": {
      "const": "yarramate/visual-layout/v1"
    },
    "projectionId": {
      "$ref": "https://yarramate.org/schema/projection/v1#/$defs/id"
    },
    "positions": {
      "description": "Where the canvas put each subject the view draws, keyed by subject id, in absolute canvas coordinates. Written for the view's own subjects and the boxes that hold them, including any the quick filter or a fold hides at save time; a subject outside the view has no entry (#578). Entries are of three kinds, and only two are INPUT. A LEAF's entry is input: the canvas pins the subject there. A FOLDED box's entry is input: a folded box is drawn as a leaf, so it is pinned there like one. An UNFOLDED box's entry is TESTIMONY, not input: a box's centre is derived from its members (#507), so the entry records where the canvas drew it at save time, a few pixels stale if saved mid-drag, and is never read back. A reader drawing the same picture must place an unfolded box from its members, not from this entry. Files written before 1.40 may carry entries for subjects outside the view; they are inert (#273).",
      "$ref": "#/$defs/positions"
    },
    "folded": {
      "description": "The instances this view draws folded (#473). Written WITH the positions in the same document and always in full, never as a patch: a reader that half-applies a fold state draws a box whose contents are somewhere else on the canvas. Absent means the view's own `presentation.fold` decides.",
      "$ref": "#/$defs/subjectIds"
    },
    "unfolded": {
      "description": "The instances this view draws OPEN even though `presentation.fold` says to fold them (#473). Two lists rather than one, because a reader who opened a box must not have it close again the moment the view's default is read back.",
      "$ref": "#/$defs/subjectIds"
    },
    "routes": {
      "description": "The routes the canvas was drawing when the positions were saved (ADR 0147), keyed by relationship id, in absolute canvas coordinates from the source end. A route is applied only while both of its ends still sit where `positions` says; an edge whose end has moved draws straight. Absent means the layout recomputes every route.",
      "$ref": "#/$defs/routes"
    }
  },
  "$defs": {
    "subjectIds": {
      "type": "array",
      "uniqueItems": true,
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "positions": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "minLength": 1
      },
      "additionalProperties": {
        "$ref": "#/$defs/position"
      }
    },
    "position": {
      "type": "object",
      "additionalProperties": false,
      "required": ["x", "y"],
      "properties": {
        "x": { "type": "number" },
        "y": { "type": "number" }
      }
    },
    "routes": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "minLength": 1
      },
      "additionalProperties": {
        "$ref": "#/$defs/route"
      }
    },
    "route": {
      "type": "object",
      "additionalProperties": false,
      "required": ["points", "labelAt"],
      "properties": {
        "points": {
          "description": "Source end first, both endpoints included, so a route is never fewer than two points.",
          "type": "array",
          "minItems": 2,
          "items": { "$ref": "#/$defs/position" }
        },
        "labelAt": {
          "description": "How far along the route, in px from the source end, the label's centre sits; null for a route with no label.",
          "type": ["number", "null"]
        }
      }
    }
  }
}
