{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Emaki Ink & Wash scene",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "version",
    "title",
    "seed",
    "width",
    "height",
    "layers"
  ],
  "properties": {
    "version": {
      "const": "astra-sketch-1"
    },
    "title": {
      "type": "string",
      "maxLength": 200
    },
    "seed": {
      "type": "integer",
      "minimum": 0,
      "maximum": 4294967295
    },
    "width": {
      "type": "integer",
      "minimum": 256,
      "maximum": 2400
    },
    "height": {
      "type": "integer",
      "minimum": 256,
      "maximum": 3000
    },
    "layers": {
      "type": "array",
      "maxItems": 32,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "intent",
          "marks"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "intent": {
            "type": "string",
            "maxLength": 500
          },
          "marks": {
            "type": "array",
            "maxItems": 6000,
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "points",
                    "color",
                    "opacity"
                  ],
                  "properties": {
                    "type": {
                      "const": "wash"
                    },
                    "points": {
                      "type": "array",
                      "minItems": 3,
                      "maxItems": 256,
                      "items": {
                        "type": "array",
                        "minItems": 2,
                        "maxItems": 2,
                        "items": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        }
                      }
                    },
                    "color": {
                      "type": "string",
                      "pattern": "^#[0-9a-fA-F]{6}$"
                    },
                    "opacity": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "edge": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "texture": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "softness": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 0.02
                    },
                    "dry": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "holes": {
                      "type": "array",
                      "maxItems": 16,
                      "items": {
                        "type": "array",
                        "minItems": 3,
                        "maxItems": 64,
                        "items": {
                          "type": "array",
                          "minItems": 2,
                          "maxItems": 2,
                          "items": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 1
                          }
                        }
                      }
                    }
                  }
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "points",
                    "color",
                    "opacity",
                    "width"
                  ],
                  "properties": {
                    "type": {
                      "const": "line"
                    },
                    "points": {
                      "type": "array",
                      "minItems": 2,
                      "maxItems": 256,
                      "items": {
                        "type": "array",
                        "minItems": 2,
                        "maxItems": 2,
                        "items": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        }
                      }
                    },
                    "color": {
                      "type": "string",
                      "pattern": "^#[0-9a-fA-F]{6}$"
                    },
                    "opacity": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "width": {
                      "type": "number",
                      "minimum": 0.0001,
                      "maximum": 0.03
                    }
                  }
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "points",
                    "color",
                    "opacity",
                    "width"
                  ],
                  "properties": {
                    "type": {
                      "const": "brush"
                    },
                    "points": {
                      "type": "array",
                      "minItems": 2,
                      "maxItems": 256,
                      "items": {
                        "type": "array",
                        "minItems": 2,
                        "maxItems": 2,
                        "items": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        }
                      }
                    },
                    "color": {
                      "type": "string",
                      "pattern": "^#[0-9a-fA-F]{6}$"
                    },
                    "opacity": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "width": {
                      "type": "number",
                      "minimum": 0.0001,
                      "maximum": 0.15
                    },
                    "texture": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "softness": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 0.02
                    },
                    "dry": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "pressure": {
                      "type": "array",
                      "minItems": 2,
                      "maxItems": 256,
                      "items": {
                        "type": "number",
                        "minimum": 0.05,
                        "maximum": 1
                      },
                      "description": "Brush contact fraction per point; length must match points."
                    },
                    "water": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "duration": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 20
                    },
                    "tilt": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "angle": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 360
                    },
                    "load": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "granulation": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "lift": {
                      "type": "number",
                      "minimum": 0,
                      "maximum": 1
                    },
                    "gesture": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "version",
                        "provenance",
                        "samples"
                      ],
                      "properties": {
                        "version": {
                          "const": "pose-v1"
                        },
                        "provenance": {
                          "enum": [
                            "procedural",
                            "measured",
                            "estimated"
                          ]
                        },
                        "samples": {
                          "type": "array",
                          "minItems": 2,
                          "maxItems": 256,
                          "items": {
                            "type": "object",
                            "additionalProperties": false,
                            "required": [
                              "t",
                              "tiltX",
                              "tiltY",
                              "rotation"
                            ],
                            "properties": {
                              "t": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 20
                              },
                              "tiltX": {
                                "type": "number",
                                "minimum": -1,
                                "maximum": 1
                              },
                              "tiltY": {
                                "type": "number",
                                "minimum": -1,
                                "maximum": 1
                              },
                              "rotation": {
                                "type": "number",
                                "minimum": 0,
                                "maximum": 360
                              }
                            }
                          }
                        }
                      },
                      "description": "Experimental material-only pose. Samples match points, time strictly increases from 0 to explicit duration, tilt vector magnitude <=1. Scalar angle/tilt are disallowed. Procedural approximation, not calibrated six-DOF physics."
                    },
                    "dynamics": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "model",
                        "compliance",
                        "recovery"
                      ],
                      "properties": {
                        "model": {
                          "const": "tuft-v1"
                        },
                        "compliance": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 1
                        },
                        "recovery": {
                          "type": "number",
                          "minimum": 0.02,
                          "maximum": 0.4
                        }
                      },
                      "description": "Experimental overdamped single-tuft bend and recovery. Requires gesture. Compliance=0 disables deformation. Recovery is seconds. Not calibrated individual bristles."
                    },
                    "supply": {
                      "oneOf": [
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "model",
                            "id",
                            "action",
                            "capacity",
                            "release",
                            "pickup"
                          ],
                          "properties": {
                            "model": {
                              "const": "reservoir-v1"
                            },
                            "id": {
                              "type": "string",
                              "pattern": "^[a-zA-Z0-9_-]{1,40}$"
                            },
                            "action": {
                              "enum": [
                                "reload",
                                "continue"
                              ]
                            },
                            "capacity": {
                              "type": "number",
                              "minimum": 0.1,
                              "maximum": 30
                            },
                            "release": {
                              "type": "number",
                              "minimum": 0.01,
                              "maximum": 12
                            },
                            "pickup": {
                              "type": "number",
                              "minimum": 0,
                              "maximum": 4
                            }
                          },
                          "description": "Experimental finite persistent brush. reload rinses prior charge to waste then loads; continue retains charge. Requires material, gesture, explicit water, no load or lift. Capacity in full-pressure footprint areas, release/pickup are empirical rates. Runtime validates prior load and unchanged width/color/opacity/water/granulation/capacity on continue."
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "model",
                            "id",
                            "action",
                            "capacity",
                            "conductance"
                          ],
                          "properties": {
                            "model": {
                              "const": "reservoir-v2"
                            },
                            "id": {
                              "type": "string",
                              "pattern": "^[a-zA-Z0-9_-]{1,40}$"
                            },
                            "action": {
                              "enum": [
                                "reload",
                                "continue"
                              ]
                            },
                            "capacity": {
                              "type": "number",
                              "minimum": 0.1,
                              "maximum": 30
                            },
                            "conductance": {
                              "type": "number",
                              "minimum": 0.01,
                              "maximum": 12
                            }
                          },
                          "description": "Opt-in simultaneous wetness-gradient exchange; single conductance replaces release/pickup. Same loading/continuation rules. Relative water content proxy, not calibrated capillary physics."
                        }
                      ]
                    }
                  }
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "type",
                    "seconds"
                  ],
                  "properties": {
                    "type": {
                      "const": "wait"
                    },
                    "seconds": {
                      "type": "number",
                      "minimum": 0.1,
                      "maximum": 180
                    }
                  }
                }
              ]
            }
          },
          "environment": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "drying"
            ],
            "properties": {
              "drying": {
                "enum": [
                  "normal",
                  "hold"
                ]
              }
            },
            "description": "Assisted layer scope. hold pauses fixation, absorption and evaporation; transport continues. Requires material."
          }
        }
      }
    },
    "material": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "model",
        "resolution",
        "roughness",
        "absorbency"
      ],
      "properties": {
        "model": {
          "enum": [
            "paper-brush-v1",
            "paper-brush-v2",
            "paper-brush-v3"
          ]
        },
        "resolution": {
          "enum": [
            384,
            640,
            768,
            960
          ]
        },
        "roughness": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        },
        "absorbency": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        }
      }
    }
  },
  "description": "Runtime additionally limits total marks to6000 and enforces unique layer ids and7.2M pixels.",
  "$comment": "validateScene also enforces material-only brush fields, lift opacity=0, total grid <=1.5M, wet brush/wait only, global time/point budgets. Gesture also requires explicit duration, per-point samples, bounded contact work, increasing times and no scalar angle/tilt; validateScene enforces these."
}
