{
  "$id": "https://motorcortex.io/grid/v-3d.schema.json",
  "$schema": "http://motorcortex.io/grid/v-3d-schema#",
  "title": "Grid 3D widget schema",
  "description": "Configuration of a 3D widget in a Motorcortex grid dashboard: the models to load, how they move with controller parameters (objects, traceLine), the camera, the lights and the rendering. Keys ending in \"_\" are ignored; settings marked DEPRECATED still work but will be removed.",
  "type": "object",
  "additionalProperties": false,
  "patternProperties": {
    "_$": { "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments." }
  },
  "definitions": {
    "file": {
      "type": "object",
      "description": "A model file to load. An entry with 'file_' instead of 'file' is switched off (not loaded), keeping its settings.",
      "anyOf": [{ "required": ["file"] }, { "required": ["file_"] }],
      "properties": {
        "file": {
          "type": "string",
          "description": "The model file (glTF: .glb, or .gltf with embedded data), relative to the 3D configuration."
        },
        "animate": {
          "oneOf": [{ "type": "string", "enum": ["once", "repeat", "pingpong", "off"] }, { "type": "boolean" }],
          "description": "Plays the animations of this model (and of all its instances) from loading on, without an 'animations' entry: repeat (or true) repeats each clip on its own length, once plays all clips once and holds the end, pingpong plays all clips forward and back; off (or false) plays nothing, as without animate. An 'animations' entry for a copy of this model replaces this setting for that copy."
        },
        "name": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ],
          "description": "The name this model will get in the 3D scene. An array adds the model once per name, all with this entry's parent, position, rotationZYX and scale; use 'instances' to give each its own. Ignored when 'instances' is set."
        },
        "instances": {
          "oneOf": [
            {
              "type": "array",
              "description": "Adds the loaded model once per item, each with its own name and placement; settings an item leaves out are taken from this file entry. The file is downloaded and parsed once. Instances share geometry and textures but each has its own materials, so links on one instance do not affect the others. Parts inside an instance can be found with a search pattern like 'instancename*partname'.",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The name this instance will get in the 3D scene."
                  },
                  "parent": {
                    "$ref": "#/definitions/file/properties/parent"
                  },
                  "position": {
                    "$ref": "#/definitions/file/properties/position"
                  },
                  "rotationZYX": {
                    "$ref": "#/definitions/file/properties/rotationZYX"
                  },
                  "scale": {
                    "$ref": "#/definitions/file/properties/scale"
                  }
                }
              }
            },
            {
              "type": "object",
              "description": "Adds the loaded model 'count' times, named name_<start> ... (e.g. ball_1 ... ball_1000), all with the same placement, to be moved by links. Drawn with GPU instancing unless 'instancing' is false.",
              "required": ["name", "count"],
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Base name; instance i is named <name>_<i>."
                },
                "count": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "The number of instances."
                },
                "start": {
                  "type": "integer",
                  "description": "The number of the first instance (default 1)."
                },
                "parent": {
                  "$ref": "#/definitions/file/properties/parent"
                },
                "position": {
                  "$ref": "#/definitions/file/properties/position"
                },
                "rotationZYX": {
                  "$ref": "#/definitions/file/properties/rotationZYX"
                },
                "scale": {
                  "$ref": "#/definitions/file/properties/scale"
                }
              }
            }
          ]
        },
        "instancing": {
          "type": "boolean",
          "description": "Draw the instances of this file with GPU instancing: one draw call per mesh of the model for all instances instead of one per copy, much faster for many copies. Default true for { name, count } instances, false otherwise. Instanced copies share their material: a colour per instance works (color links), but opacity, glow, environment map and shadow settings only apply to all instances together; set false to give each copy its own material."
        },
        "parent": {
          "type": "string",
          "description": "The name of the parent object in the 3D scene that this object will become a child of. Also a search pattern may be used to search the hierarchy of the scene graph. A pattern consist of multiple object names separated by a '*' like: 'objectname*childname*subname', meaning that first a search for objectname is executed starting from scene level and then within the remaining hierarchy a search for childname is executed and so on.The global position, orientation and scale of the object will be unchanged; if the object should be moved/aligned to the parent frame, use position and rotationZYX properties to set the local (relative) position/orientation of the child object."
        },
        "position": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "description": "Position of the object [x, y, z], in scene units (usually metres); relative to 'parent' when set.",
          "items": {
            "type": "number"
          }
        },
        "rotationZYX": {
          "type": "array",
          "minItems": 3,
          "maxItems": 3,
          "description": "Rotation of the object as Euler angles [rz, ry, rx] in radians (ZYX order); relative to 'parent' when set.",
          "items": {
            "type": "number"
          }
        },
        "scale": {
          "type": "number",
          "description": "Uniform scale of the loaded object (1 = as in the file)."
        }
      }
    },
    "traceLine": {
      "type": "object",
      "description": "Draws a line in 3D space through the points given by links to the axes x, y and z (or position/pose). A 'color' link colours each segment by a value, e.g. the speed of the tool: { \"axis\": \"color\", \"link\": \"root/ManipulatorControl/autoMotionGenerator/cartVelocities\", \"magnitude\": { \"start\": 0, \"length\": 3 }, \"min\": 0, \"max\": 0.5 } (blue when standing still, red at 0.5 m/s and faster). DEPRECATED older form: links written as named keys directly in the traceLine (e.g. \"px\": { \"axis\": \"x\", ... }) still work; put them in 'links' instead.",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the line object in the scene (default: traceline)."
        },
        "color": {
          "type": "string",
          "description": "A string defining a color as a CSS style string; examples: #ff0000, rgb(255, 0, 0), rgb(100%, 0%, 0%), red, hsl(0, 100%, 50%)"
        },
        "lineWidth": {
          "type": "number",
          "description": "The line thickness in pixels. A 'width' link multiplies it per segment; until the linked parameter has a value lineWidth is used as is. Without a 'width' link browsers draw the line 1 pixel wide."
        },
        "tolerance": {
          "type": "number",
          "description": "The distance in either x, y or z direction that a new point needs to be away from the previous point before it is added to the line."
        },
        "numPoints": {
          "type": "number",
          "description": "The maximum number of points in a line. Points at the tail will be removed first."
        },
        "links": {
          "type": "array",
          "description": "The list of links to apply to this object.",
          "items": {
            "$ref": "#/definitions/link"
          }
        }
      }
    },
    "link": {
      "type": "object",
      "properties": {
        "link": {
          "type": "string",
          "description": "The path of the controller parameter to subscribe to, e.g. 'root/ManipulatorControl/manipulatorToolPoseActual'. In an object entry with 'count', %d (or %02d) becomes the number of the entry (see 'stride')."
        },
        "channel": {
          "type": "number",
          "description": "The index of the element to read in an array parameter (default 0); axes that take several values (pose, position, rotationZYX, color) read the consecutive elements from there. Not used with 'magnitude'. In an object entry with 'count' and a link path without %d (one array parameter for all entries), this is the element of the first entry: entry i reads from channel + i * stride, e.g. channel 3 with stride 1 reads elements 3, 4, 5, ...."
        },
        "start": {
          "type": "integer",
          "description": "Only in an object entry with 'count', for a link path with %d (a separate parameter per entry): the number of the first parameter; entry i uses parameter start + i * stride. Default: the entry's own 'start' (default 1), so names and parameters are numbered alike. E.g. entries Monkey_7 ... Monkey_12 (entry 'start' 7) with a link 'start' of 0 use actuatorControlLoop00 ... 05. Not used without %d: for an array parameter, 'channel' sets the element of the first entry."
        },
        "stride": {
          "type": "number",
          "description": "Only in an object entry with 'count': the step from one numbered entry to the next. With %d in the link path (a separate parameter per entry), the parameter number steps by it: start, start + stride, ... (default 1), e.g. 'root/Axis%02d/position' with stride 2 uses Axis01, Axis03, ...; the channel stays as given, whatever the size of the parameter. Without %d (one array parameter for all entries), the element steps by it: entry i reads from channel + i * stride, e.g. 1 for one value per entry or 6 for one pose (x, y, z, rz, ry, rx) per entry; without 'stride' all entries read the same element."
        },
        "channelStride": {
          "type": "number",
          "deprecated": true,
          "deprecationMessage": "Deprecated: use 'stride' (the same setting under its new name).",
          "description": "DEPRECATED: use 'stride' (the same setting under its new name)."
        },
        "property": {
          "type": "string",
          "description": "A property of the object that the value is set to directly: a name, e.g. 'intensity' of a light or 'visible', or a dotted path, e.g. 'material.color.b' or 'material.opacity'. The value is converted to the property's type. Trace lines can be addressed by their 'name' in 'objects' too, e.g. to change their colour."
        },
        "axis": {
          "type": "string",
          "description": "The axis or property the value is applied to. Positions are in scene units (usually metres), angles in radians. 'x', 'y', 'z': position. 'rx', 'ry', 'rz': rotation about that axis (ZYX order). 's': uniform scale; 'sx', 'sy', 'sz': scale along one axis. 'pose': six elements [x, y, z, rz, ry, rx] from 'channel'. 'position': [x, y, z]. 'rotationZYX': [rz, ry, rx]. 'scaleChildren': the i-th child is scaled by the i-th element (while the object is visible). 'visible': shown when the value is 0.5 or more. 'opacity': 0 (invisible) to 1 (opaque). 'emissiveIntensity': the glow of emissive materials. In traceLine links 'position' and 'pose' set x, y and z from three consecutive array elements starting at 'channel' (a pose's orientation is not used). 'clear', 'pause' and 'width' only apply to traceLine links: while 'clear' is true the trace line is emptied and not updated; while 'pause' is true no points are added and the line continues after the pause with a gap; 'width' scales lineWidth for each newly added segment (1 draws lineWidth, 2 twice as thick). 'color' sets the colour of the object (also of a single instanced copy): an array gives [r, g, b] (0..1) from three elements starting at 'channel', a number a 0xRRGGBB value and a string a CSS colour. In traceLine links 'color' colours each newly added segment by the linked value on a gradient ('colorMap' from 'min' to 'max'), e.g. the speed with 'magnitude'; without a 'color' link the line has its 'color'.",
          "enum": [
            "x",
            "y",
            "z",
            "rx",
            "ry",
            "rz",
            "s",
            "sx",
            "sy",
            "sz",
            "visible",
            "opacity",
            "emissiveIntensity",
            "pose",
            "position",
            "rotationZYX",
            "scaleChildren",
            "color",
            "clear",
            "pause",
            "width"
          ]
        },
        "gain": {
          "oneOf": [
            {
              "type": "number"
            },
            {
              "type": "array",
              "items": {
                "type": "number"
              }
            }
          ],
          "description": "The gain the value is multiplied by before it is applied to the axis. On array axes (pose, position, rotationZYX, scaleChildren) a number multiplies every element and an array gives one gain per element, e.g. [x, y, z, rz, ry, rx] for a pose; missing elements get 1. On a single-value axis an array uses its first element."
        },
        "offset": {
          "oneOf": [
            {
              "type": "number"
            },
            {
              "type": "array",
              "items": {
                "type": "number"
              }
            }
          ],
          "description": "The offset that is added to the gained value before it is applied to the axis. On array axes (pose, position, rotationZYX, scaleChildren) a number is added to every element and an array gives one offset per element, e.g. [x, y, z, rz, ry, rx] for a pose; missing elements get 0. On a single-value axis an array uses its first element."
        },
        "value": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "number"
            },
            {
              "type": "array",
              "items": {
                "type": "number"
              }
            },
            {
              "type": "string",
              "description": "A CSS colour for 'color'. \"true\", \"false\" and numbers written as strings (older configurations) are read as the boolean or number."
            }
          ],
          "description": "The value used until the linked parameter has one, or always when there is no 'link' (a fixed value)."
        },
        "magnitude": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "object",
              "properties": {
                "start": {
                  "type": "number",
                  "description": "The first element (default 0)."
                },
                "length": {
                  "type": "number",
                  "description": "The number of elements (default: up to the end of the array)."
                }
              }
            }
          ],
          "description": "Use the length (Euclidean norm) of a vector instead of one element: true takes the whole array, { \"start\": 0, \"length\": 3 } a part of it, e.g. the linear velocity of a twist [vx, vy, vz, wx, wy, wz] (start 3 for the angular velocity). Replaces 'channel'; gain and offset apply to the length. For axes that take one number (x, rz, s, opacity, a trace line's width or color, a 'property', ...); ignored with a warning on pose, position, rotationZYX, an object's color and scaleChildren. A single number gives its absolute value."
        },
        "min": {
          "type": "number",
          "description": "traceLine 'color' links: the value at the low (cold) end of the colorMap (default 0). Lower values get the first colour."
        },
        "max": {
          "type": "number",
          "description": "traceLine 'color' links: the value at the high (hot) end of the colorMap (default 1). Higher values get the last colour."
        },
        "colorMap": {
          "oneOf": [
            {
              "type": "string",
              "enum": [
                "heat",
                "viridis",
                "inferno",
                "grayscale"
              ]
            },
            {
              "type": "array",
              "minItems": 2,
              "items": {
                "type": "string"
              }
            }
          ],
          "description": "traceLine 'color' links: the gradient from 'min' to 'max'. 'heat' (default): dark blue, blue, cyan, green, yellow, red, dark red. 'viridis' and 'inferno': perceptually uniform maps, also readable for colour-blind viewers. 'grayscale': black to white. Or a list of at least two CSS colours from low to high, e.g. [\"#0000ff\", \"#ffffff\", \"#ff0000\"]."
        }
      }
    },
    "animation": {
      "type": "object",
      "description": "Plays the animation clips of a loaded model copy. Time-based (play: auto, onTrigger or whileHigh; with speed) or position-based (position). Example, a conveyor belt as a speed-controlled device: { \"object\": \"Conveyor\", \"clip\": \"BeltMove\", \"loop\": \"repeat\", \"play\": \"auto\", \"speed\": { \"link\": \"root/Conveyor/beltVelocityActual\", \"gain\": 10 } } runs the belt animation at the belt's actual velocity (gain = clip duration in s / distance per cycle in m; stopped at 0, backwards when negative).",
      "required": ["object"],
      "additionalProperties": false,
      "patternProperties": { "_$": {} },
      "properties": {
        "object": { "type": "string", "description": "The model copy to animate, by name (the name of a files entry or of one of its instances). Several entries can name the same copy when their clips move different parts. With count, %d (or %02d, ...) becomes the number." },
        "count": { "type": "number", "description": "Stands for this many numbered entries, as for objects: %d in object and in the link paths becomes the number." },
        "start": { "type": "number", "description": "The first number of numbered entries (default 1)." },
        "clip": {
          "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" }, "minItems": 1 }],
          "description": "The clip to play, by name, or a list of clips. Without it all clips of the model play. With loop repeat each clip repeats on its own length; with once, pingpong or position several clips play together as one animation, as long as the longest. Several entries can play on one model copy when their clips move different parts (e.g. a door on a trigger and a fan on a speed link); grid3d.animations() in the browser console lists the clips."
        },
        "loop": { "type": "string", "enum": ["once", "repeat", "pingpong"], "description": "once (default): plays to the end and holds it; repeat: starts again at the end (without clip: each clip on its own length); pingpong: plays forward and back." },
        "play": { "type": "string", "enum": ["auto", "onTrigger", "whileHigh"], "description": "Time-based start. auto (default): plays from loading on. onTrigger: a rising trigger edge (value from below 0.5 to 0.5 or more) plays playCount times; edges during a playback are ignored. whileHigh: plays while the trigger is high and pauses when it is low; with loop once, low returns to the start. In every mode a negative trigger value resets to the start." },
        "trigger": { "$ref": "#/definitions/link", "description": "The trigger link for onTrigger and whileHigh (for auto optional: a negative value resets)." },
        "playCount": { "type": "number", "minimum": 0, "description": "onTrigger with repeat or pingpong: plays per trigger (pingpong: forward and back is one play); 0 plays endlessly until a negative trigger. Default 1." },
        "speed": { "oneOf": [{ "type": "number" }, { "$ref": "#/definitions/link" }], "description": "Playback speed multiplier, a number or a link: 0 pauses, negative plays backwards. Without speed it is 1; a speed that is not a number, or a link without a usable value (no data yet, not a number, an invalid 'value'), gives 0." },
        "position": {
          "allOf": [{ "$ref": "#/definitions/link" }],
          "properties": { "unit": { "type": "string", "enum": ["fraction", "seconds"], "description": "fraction (default): 0 is the start, 1 the end; seconds: the time in the clip." } },
          "description": "Position-based control instead of time-based playback: the linked value sets the pose. loop decides outside the range: once clamps, repeat wraps around, pingpong goes back and forth. play, trigger, speed and playCount do not apply."
        }
      }
    },
    "object": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The name of the 3D object or scene that these settings apply to. Also a search pattern may be used to search the hierarchy of the scene graph. A pattern consist of multiple object names separated by a '*' like: 'objectname*childname*subname', meaning that first a search for objectname is executed starting from scene level and then within the remaining hierarchy a search for childname is executed and so on."
        },
        "receiveShadow": {
          "type": "boolean",
          "description": "Defines if the object (and all its children) receive shadows."
        },
        "castShadow": {
          "type": "boolean",
          "description": "Defines if the object (and all its children) cast shadows."
        },
        "addEnvironmentMap": {
          "type": "boolean",
          "deprecated": true,
          "deprecationMessage": "Deprecated, ignored: the environment map (renderSettings.environment or environmentmap) lights all objects.",
          "description": "DEPRECATED, ignored: the environment map (renderSettings.environment or environmentmap) lights all objects."
        },
        "environmentMapIntensity": {
          "type": "number",
          "deprecated": true,
          "deprecationMessage": "Deprecated, ignored: set the strength with the environment's 'intensity', which applies to all objects.",
          "description": "DEPRECATED, ignored: set the strength with the environment's 'intensity', which applies to all objects."
        },
        "count": {
          "type": "integer",
          "minimum": 1,
          "description": "Makes this entry a template for 'count' numbered entries (entry i = 0, 1, ..., count - 1). %d in 'name' becomes the entry number start + i (%03d pads it with zeros): 'Monkey_%d' with count 6 configures Monkey_1 ... Monkey_6. Each link of the entry reads its value in one of two ways. (1) A separate parameter per entry: %d in the link path becomes the parameter number, the link's start + i * stride (defaults: the entry's 'start' and 1), and 'channel' stays as given; e.g. { \"link\": \"root/AxesControl/actuatorControlLoops/actuatorControlLoop%02d/actuatorPositionActual\", \"axis\": \"x\", \"start\": 0 } links Monkey_1 ... Monkey_6 to actuatorControlLoop00 ... 05. (2) One array parameter for all entries: without %d in the link path, entry i reads from element channel + i * stride (defaults 0 and 0); e.g. { \"link\": \"root/Balls/poses\", \"axis\": \"pose\", \"stride\": 6 } gives ball_1 elements 0-5, ball_2 elements 6-11, and so on."
        },
        "start": {
          "type": "integer",
          "description": "The number of the first entry when 'count' is set (default 1). It numbers the names (%d in 'name'), and the parameters in link paths with %d unless a link has its own 'start'. Array links do not use it: their 'channel' sets the element of the first entry."
        },
        "links": {
          "type": "array",
          "description": "The links that move or change this object: each takes a parameter value from the controller and applies it to an axis or property.",
          "items": {
            "$ref": "#/definitions/link"
          }
        },
        "link": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/link" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "axis": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/axis" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "channel": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/channel" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "gain": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/gain" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "offset": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/offset" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "value": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/value" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        },
        "property": {
          "deprecated": true,
          "deprecationMessage": "Deprecated: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }].",
          "allOf": [{ "$ref": "#/definitions/link/properties/property" }],
          "description": "DEPRECATED: older form of a single link written directly on the object; put it in 'links' instead, e.g. \"links\": [{ \"link\": ..., \"axis\": ... }]."
        }
      }
    }
  },
  "properties": {
    "file": {
      "type": "string",
      "deprecated": true,
      "deprecationMessage": "Deprecated: a single model file, the older form of 'files'; use \"files\": [{ \"file\": ... }] instead.",
      "description": "DEPRECATED: a single model file, the older form of 'files'; use \"files\": [{ \"file\": ... }] instead."
    },
    "properties": {
      "type": "array",
      "deprecated": true,
      "deprecationMessage": "Deprecated, ignored: it has no effect. Put links in 'objects' instead.",
      "description": "DEPRECATED, ignored: it has no effect. Put links in 'objects' instead."
    },
    "files": {
      "type": "array",
      "description": "The 3D model files (glTF: .glb, or .gltf with embedded data) to load into the scene, in this order.",
      "items": {
        "$ref": "#/definitions/file"
      }
    },
    "environmentmap": {
      "type": "object",
      "required": ["nx", "ny", "nz", "px", "py", "pz"],
      "description": "Six environment (cube map) images in the standard cube map layout for the widget's Z-up world, lighting all objects; intensity sets its strength. The same as renderSettings.environment with type cubemap, which takes precedence when set (type none turns the environment off).",
      "properties": {
        "nx": { "type": "string", "description": "Image for the -X direction, relative to the configuration." },
        "ny": { "type": "string", "description": "Image for the -Y direction." },
        "nz": { "type": "string", "description": "Image for the -Z direction (down, the floor)." },
        "px": { "type": "string", "description": "Image for the +X direction." },
        "py": { "type": "string", "description": "Image for the +Y direction." },
        "pz": { "type": "string", "description": "Image for the +Z direction (up, the ceiling or sky)." },
        "intensity": {
          "type": "number",
          "description": "Strength of the environment lighting (default 1)."
        }
      }
    },
    "camera": {
      "description": "The name of a camera in the model to view through (or a search pattern, see objects.name). Without it a default perspective camera is used, placed by cameraPosition.",
      "type": "string"
    },
    "cameraLookAt": {
      "type": "object",
      "description": "The point the camera initially looks at (default: the origin), for the default camera and a camera from the model alike. With view navigation the camera orbits around it.",
      "properties": {
        "x": { "type": "number", "description": "Scene units (usually metres); each coordinate is optional." },
        "y": { "type": "number" },
        "z": { "type": "number" }
      }
    },
    "cameraPosition": {
      "type": "object",
      "description": "The position of the default camera (default x -1, y 5, z 1.5). Not applied to a camera from the model ('camera'), which keeps its own position.",
      "properties": {
        "x": { "type": "number", "description": "Scene units (usually metres); each coordinate is optional." },
        "y": { "type": "number" },
        "z": { "type": "number" }
      }
    },
    "hemisphereLight": {
      "type": "object",
      "description": "The ambient light from above (white) and below (grey) that lights the whole scene. Only present when set: without 'hemisphereLight' the scene has no hemisphere light (an environment then provides the ambient light).",
      "properties": {
        "intensity": {
          "type": "number",
          "description": "Brightness (default 1 when hemisphereLight is set). With \"lighting\": \"physical\" about 3 times higher (x pi) gives the same brightness as with the legacy lighting."
        }
      }
    },
    "spotLight": {
      "type": "object",
      "description": "The spot light that casts the shadows, pointing at the origin. Only present when set: without 'spotLight' the scene has no spot light (and no shadows from it).",
      "properties": {
        "intensity": {
          "type": "number",
          "description": "Brightness (default 0.2 when spotLight is set). With \"lighting\": \"physical\" the intensity is in candela and fades with the square of the distance: about pi x distance² times higher gives the legacy brightness (e.g. 0.2 at 19 m needs about 230). unless decay is 0."
        },
        "penumbra": {
          "type": "number",
          "description": "How soft the edge of the light cone is, 0 (sharp) to 1 (soft) (default 1.5, which acts as soft)."
        },
        "angle": {
          "type": "number",
          "description": "Half the opening angle of the light cone, in radians (default 1.0)."
        },
        "castShadow": {
          "type": "boolean",
          "description": "Whether the light casts shadows (default true); objects need castShadow/receiveShadow too."
        },
        "decay": {
          "type": "number",
          "minimum": 0,
          "description": "How the light fades with distance: 0 does not fade, 2 fades with the square of the distance as real light does, values in between fade more gently. Default: 0 with the legacy lighting, 2 with \"lighting\": \"physical\". With decay 0 a far-away spot light needs no large intensity (see intensity)."
        },
        "x": {
          "type": "number",
          "description": "Position in scene units (default x 0, y 0, z 15)."
        },
        "y": {
          "type": "number"
        },
        "z": {
          "type": "number"
        }
      }
    },
    "renderSettings": {
      "properties": {
        "fps": {
          "type": "number",
          "description": "Maximum number of frames per second (default 60). The scene is only redrawn when something changes."
        },
        "lighting": {
          "type": "string",
          "enum": ["legacy", "physical"],
          "description": "'legacy' (default) renders lights as on earlier versions of the 3D widget, so existing configurations keep their look. 'physical' uses physically based light units: light intensities usually need to be set higher (roughly x3 for hemisphere lights), point and spot lights fade with distance, and environment maps are treated as sRGB images. Setting 'physical' (or a toneMapping) also switches to the current output pipeline: tone mapping and sRGB conversion at the end of post-processing, and the current bloom effect."
        },
        "toneMapping": {
          "type": "string",
          "enum": ["none", "agx", "neutral", "aces", "reinhard", "cineon", "linear"],
          "description": "Tone mapping applied to the final image: maps bright areas (lights, LEDs, bloom) to displayable colours without clipping. 'agx' gives natural colours and highlights; 'neutral' keeps base colours closest to the material colours. Default 'none' (as on earlier versions)."
        },
        "toneMappingExposure": {
          "type": "number",
          "description": "Exposure for the tone mapping (default 1). Higher values brighten the image. Only used when toneMapping is set."
        },
        "instancing": {
          "type": "string",
          "enum": ["auto", "off"],
          "description": "'auto' (default) draws many copies of a model with GPU instancing where possible; 'off' always draws separate copies."
        },
        "ambientOcclusion": {
          "type": "object",
          "description": "Screen-space ambient occlusion (GTAO), computed every frame from the rendered depth and normals: soft shadows in corners, crevices and where parts touch, without pre-baked AO textures. Costs roughly 40% more frame time. Transparent parts and trace lines do not occlude.",
          "properties": {
            "enable": {
              "type": "boolean",
              "description": "Turns ambient occlusion on (default false)."
            },
            "radius": {
              "type": "number",
              "description": "How far (in scene units, usually metres) surfaces affect each other (default 0.25)."
            },
            "intensity": {
              "type": "number",
              "description": "Strength of the darkening, 0 to 1 (default 1)."
            },
            "thickness": {
              "type": "number",
              "description": "Assumed thickness of surfaces; lower values reduce dark halos behind thin parts (default 0.1)."
            },
            "samples": {
              "type": "integer",
              "description": "Samples per pixel: more is smoother and slower (default 16)."
            }
          }
        },
        "backgroundColor": {
          "type": "string",
          "description": "Background colour as a CSS colour, e.g. \"#202830\" or \"white\" (default: the standard light grey). Shown as given; with a tone mapping it is tone mapped like the rest of the scene. An environment shown as background (environment.background) covers it."
        },
        "pixelRatio": {
          "description": "Rendering resolution per CSS pixel. 1 (the default) renders at CSS resolution; \"device\" uses the screen's pixel ratio (sharper on high-DPI screens, capped at 2); a number sets it directly. Frame cost grows with the square of the ratio.",
          "oneOf": [
            { "type": "string", "enum": ["device"] },
            { "type": "number", "exclusiveMinimum": 0 }
          ]
        },
        "environment": {
          "description": "Image-based lighting for all standard (PBR) materials: reflections and soft ambient light, optionally shown as the background. Off unless set here or by the top-level environmentmap (which this replaces). \"type\" says where it comes from: none, generated, equirectangular (one 2:1 image) or cubemap (six images). It applies to all objects and takes precedence over the top-level environmentmap. Best combined with \"lighting\": \"physical\" and a tone mapping. Equirectangular images and generated environments are Y-up and turned to the widget's Z-up world; cubemaps are in the standard cube map layout for the Z-up world (pz up), as the top-level environmentmap.",
          "type": "object",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "enum": [
                "none",
                "generated",
                "equirectangular",
                "cubemap"
              ]
            }
          },
          "oneOf": [
            {
              "type": "object",
              "description": "No environment (the same as leaving it out).",
              "required": [
                "type"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "none"
                }
              }
            },
            {
              "type": "object",
              "description": "A generated room (a studio room with soft lights) or hall (a factory hall with ceiling light strips); nothing to download.",
              "required": [
                "type",
                "generator"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "generated"
                },
                "generator": {
                  "enum": [
                    "room",
                    "hall"
                  ],
                  "description": "room: a studio room with soft lights; hall: a factory hall with ceiling light strips; sky: an outdoor sky (the sun itself is left out; the scene's lights stand in for it); gradient: a sky-to-ground colour gradient."
                },
                "intensity": {
                  "type": "number",
                  "description": "Strength of the environment lighting (default 1)."
                },
                "background": {
                  "type": "boolean",
                  "description": "Also show the environment as the background instead of the background colour (default false)."
                },
                "blurriness": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Blur of the background, 0 (sharp) to 1 (default 0.5 for generated, 0 otherwise)."
                },
                "azimuthDegrees": {
                  "type": "number",
                  "description": "Turns the environment, the background as well as its lighting and reflections, around the vertical (Z) axis by this many degrees, counter-clockwise seen from above (default 0): 90 brings what was towards +X to +Y. The scene's own lights (hemisphereLight, spotLight) do not turn along."
                }
              }
            },
            {
              "type": "object",
              "description": "A generated outdoor sky with the sun placed by skySunElevationDegrees and skySunAzimuthDegrees; nothing to download.",
              "required": [
                "type",
                "generator"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "generated"
                },
                "generator": {
                  "enum": [
                    "sky"
                  ],
                  "description": "room: a studio room with soft lights; hall: a factory hall with ceiling light strips; sky: an outdoor sky (the sun itself is left out; the scene's lights stand in for it); gradient: a sky-to-ground colour gradient."
                },
                "intensity": {
                  "type": "number",
                  "description": "Strength of the environment lighting (default 1)."
                },
                "background": {
                  "type": "boolean",
                  "description": "Also show the environment as the background instead of the background colour (default false)."
                },
                "blurriness": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Blur of the background, 0 (sharp) to 1 (default 0.5 for generated, 0 otherwise)."
                },
                "azimuthDegrees": {
                  "type": "number",
                  "description": "Turns the environment, the background as well as its lighting and reflections, around the vertical (Z) axis by this many degrees, counter-clockwise seen from above (default 0): 90 brings what was towards +X to +Y. The scene's own lights (hemisphereLight, spotLight) do not turn along."
                },
                "skySunElevationDegrees": {
                  "type": "number",
                  "description": "height of the sun above the horizon in degrees, 0 on the horizon to 90 straight overhead (default 35). The sun itself is not drawn; it sets where the sky is brightest and from which side its light falls."
                },
                "skySunAzimuthDegrees": {
                  "type": "number",
                  "description": "direction of the sun around the vertical (Z) axis in degrees: 0 towards -Y, 90 towards +X, 180 towards +Y (counter-clockwise seen from above; default 150). azimuthDegrees turns the sun along with the sky."
                },
                "skyTurbidity": {
                  "type": "number",
                  "description": "haziness of the air, 1 (clear) to 20 (default 4)."
                }
              }
            },
            {
              "type": "object",
              "description": "A generated colour gradient from gradientTopColor (straight up) through gradientHorizonColor to gradientBottomColor (straight down); nothing to download.",
              "required": [
                "type",
                "generator"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "generated"
                },
                "generator": {
                  "enum": [
                    "gradient"
                  ],
                  "description": "room: a studio room with soft lights; hall: a factory hall with ceiling light strips; sky: an outdoor sky (the sun itself is left out; the scene's lights stand in for it); gradient: a sky-to-ground colour gradient."
                },
                "intensity": {
                  "type": "number",
                  "description": "Strength of the environment lighting (default 1)."
                },
                "background": {
                  "type": "boolean",
                  "description": "Also show the environment as the background instead of the background colour (default false)."
                },
                "blurriness": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Blur of the background, 0 (sharp) to 1 (default 0.5 for generated, 0 otherwise)."
                },
                "azimuthDegrees": {
                  "type": "number",
                  "description": "Turns the environment, the background as well as its lighting and reflections, around the vertical (Z) axis by this many degrees, counter-clockwise seen from above (default 0): 90 brings what was towards +X to +Y. The scene's own lights (hemisphereLight, spotLight) do not turn along."
                },
                "gradientTopColor": {
                  "type": "string",
                  "description": "colour straight up (default \"#e8f0ff\")."
                },
                "gradientHorizonColor": {
                  "type": "string",
                  "description": "colour at the horizon (default \"#ffffff\")."
                },
                "gradientBottomColor": {
                  "type": "string",
                  "description": "colour straight down (default \"#606468\")."
                }
              }
            },
            {
              "type": "object",
              "description": "One equirectangular (latitude-longitude) image, twice as wide as high: 360° around from left to right, straight up to straight down from top to bottom. The format of Blender world textures and HDRI sites.",
              "required": [
                "type",
                "file"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "equirectangular"
                },
                "file": {
                  "type": "string",
                  "pattern": "\\.([hH][dD][rR]|[eE][xX][rR]|[jJ][pP][eE]?[gG]|[pP][nN][gG]|[wW][eE][bB][pP])$",
                  "description": "The image, relative to the configuration: .hdr or .exr (high dynamic range; use with \"lighting\": \"physical\" and a tone mapping so bright parts such as the sun are not clipped) or .jpg, .png, .webp."
                },
                "intensity": {
                  "type": "number",
                  "description": "Strength of the environment lighting (default 1)."
                },
                "background": {
                  "type": "boolean",
                  "description": "Also show the environment as the background instead of the background colour (default false)."
                },
                "blurriness": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Blur of the background, 0 (sharp) to 1 (default 0.5 for generated, 0 otherwise)."
                },
                "azimuthDegrees": {
                  "type": "number",
                  "description": "Turns the environment, the background as well as its lighting and reflections, around the vertical (Z) axis by this many degrees, counter-clockwise seen from above (default 0): 90 brings what was towards +X to +Y. The scene's own lights (hemisphereLight, spotLight) do not turn along."
                }
              }
            },
            {
              "type": "object",
              "description": "Six square images, one per axis direction.",
              "required": [
                "type",
                "files"
              ],
              "additionalProperties": false,
              "patternProperties": {
                "_$": {
                  "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                }
              },
              "properties": {
                "type": {
                  "const": "cubemap"
                },
                "files": {
                  "type": "object",
                  "required": [
                    "px",
                    "nx",
                    "py",
                    "ny",
                    "pz",
                    "nz"
                  ],
                  "additionalProperties": false,
                  "patternProperties": {
                    "_$": {
                      "description": "Keys ending in \"_\" are ignored by the widget: use them to keep alternative settings, as JSON has no comments."
                    }
                  },
                  "description": "The images for the faces px, nx, py, ny, pz, nz in the standard (OpenGL) cube map layout, for the widget's Z-up world (pz is up, nz down), relative to the configuration. The same images as for the top-level environmentmap.",
                  "properties": {
                    "px": {
                      "type": "string"
                    },
                    "nx": {
                      "type": "string"
                    },
                    "py": {
                      "type": "string"
                    },
                    "ny": {
                      "type": "string"
                    },
                    "pz": {
                      "type": "string"
                    },
                    "nz": {
                      "type": "string"
                    }
                  }
                },
                "intensity": {
                  "type": "number",
                  "description": "Strength of the environment lighting (default 1)."
                },
                "background": {
                  "type": "boolean",
                  "description": "Also show the environment as the background instead of the background colour (default false)."
                },
                "blurriness": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Blur of the background, 0 (sharp) to 1 (default 0.5 for generated, 0 otherwise)."
                },
                "azimuthDegrees": {
                  "type": "number",
                  "description": "Turns the environment, the background as well as its lighting and reflections, around the vertical (Z) axis by this many degrees, counter-clockwise seen from above (default 0): 90 brings what was towards +X to +Y. The scene's own lights (hemisphereLight, spotLight) do not turn along."
                }
              }
            }
          ]
        },
        "bloom": {
          "type": "object",
          "description": "Properties of the bloom effect.",
          "properties": {
            "enable": {
              "type": "boolean",
              "description": "Turns bloom on: a glow around bright parts, e.g. LEDs and lamps (default false)."
            },
            "strength": {
              "type": "number",
              "description": "The overall strength of the bloom (default 1)."
            },
            "radius": {
              "type": "number",
              "description": "The blur radius of the bloom (default 0.1)."
            },
            "threshold": {
              "type": "number",
              "description": "The luminosity above which bloom is applied (default 1). Note: the environment map also adds to the luminosity of a material."
            }
          }
        }
      }
    },
    "traceLine": {
      "description": "One trace line, or an array of trace lines that are each drawn and controlled independently.",
      "oneOf": [
        {
          "$ref": "#/definitions/traceLine"
        },
        {
          "type": "array",
          "items": {
            "$ref": "#/definitions/traceLine"
          }
        }
      ]
    },
    "animations": {
      "type": "array",
      "description": "Animations of loaded models (glTF animation clips), one entry per model copy.",
      "items": { "$ref": "#/definitions/animation" }
    },
    "objects": {
      "type": "array",
      "description": "Settings and links for objects in the loaded models, found by name: shadows and links that move or change them with controller values.",
      "items": {
        "$ref": "#/definitions/object"
      }
    }
  }
}
